ข้อเสียของการนำ singleton ไปใช้กับ enum ของ Java คืออะไร


14

ตามเนื้อผ้ามักจะใช้เป็นซิงเกิล

public class Foo1
{
    private static final Foo1 INSTANCE = new Foo1();

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

ด้วย enum ของ Java เราสามารถใช้ singleton เป็น

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

น่ากลัวเหมือนรุ่นที่ 2 คือมีข้อเสียหรือไม่

(ฉันให้ความคิดกับฉันและฉันจะตอบคำถามของฉันเองหวังว่าคุณจะได้คำตอบที่ดีกว่า)


16
ข้อเสียคือมันเป็นซิงเกิล "รูปแบบ" overrated ( ไอ ) ทั้งหมด
โทมัส Eding

คำตอบ:


32

ปัญหาบางอย่างกับ enum singletons:

มุ่งมั่นที่จะใช้กลยุทธ์การดำเนินงาน

โดยทั่วไปแล้ว "ซิงเกิล" หมายถึงกลยุทธ์การนำไปใช้งานไม่ใช่ข้อกำหนดของ API มันเป็นเรื่องยากมากที่Foo1.getInstance()จะประกาศต่อสาธารณะว่ามันจะส่งคืนอินสแตนซ์เดียวกันเสมอ หากจำเป็นการดำเนินการของFoo1.getInstance()สามารถพัฒนาตัวอย่างเช่นเพื่อส่งกลับหนึ่งอินสแตนซ์ต่อเธรด

กับFoo2.INSTANCEเราต่อสาธารณชนประกาศว่ากรณีนี้เป็นเช่นและมีโอกาสที่จะเปลี่ยนแปลง กลยุทธ์การดำเนินงานของการมีอินสแตนซ์เดียวมีการเปิดเผยและมุ่งมั่น

ปัญหานี้ไม่ทำให้หมดอำนาจ ตัวอย่างเช่นFoo2.INSTANCE.doo()สามารถพึ่งพาอ็อบเจ็กต์ตัวช่วยโลคัลเธรดเพื่อให้มีอินสแตนซ์ต่อเธรดได้อย่างมีประสิทธิภาพ

การขยายคลาส Enum

Foo2Enum<Foo2>ขยายชั้นยอด โดยปกติเราต้องการหลีกเลี่ยงคลาสที่ยอดเยี่ยม โดยเฉพาะอย่างยิ่งในกรณีนี้ผู้บังคับการระดับสูงFoo2ไม่มีส่วนเกี่ยวข้องกับสิ่งที่Foo2ควรจะเป็น นั่นคือมลพิษต่อลำดับชั้นชนิดของแอปพลิเคชันของเรา ถ้าเราต้องการซุปเปอร์คลาสจริงๆโดยปกติแล้วจะเป็นคลาสแอปพลิเคชัน แต่เราไม่สามารถทำได้Foo2ซูเปอร์คลาสของจะได้รับการแก้ไข

Foo2สืบทอดเมธอดอินสแตนซ์ตลกบางอย่างเช่นname(), cardinal(), compareTo(Foo2)ซึ่งสร้างความสับสนให้กับFoo2ผู้ใช้ Foo2ไม่สามารถมีname()วิธีการของตัวเองแม้ว่าวิธีการนั้นเป็นที่ต้องการในFoo2ส่วนต่อประสานของ

Foo2 ยังมีวิธีการคงที่ตลกบางอย่าง

    public static Foo2[] values() { ... }
    public static Foo2 valueOf(String name) { ... }
    public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)

ซึ่งดูเหมือนจะไร้สาระต่อผู้ใช้ ซิงเกิลตันไม่ควรมีวิธีคงที่ pulbic อย่างไรก็ตาม (นอกเหนือจากgetInstance())

Serializability

มันเป็นเรื่องธรรมดามากสำหรับซิงเกิลที่จะเป็นมลรัฐ โดยทั่วไปแล้วซิงเกิลเหล่านี้ไม่ควรต่อเนื่องกันได้ ฉันไม่สามารถนึกถึงตัวอย่างที่เป็นจริงใด ๆ ที่ทำให้การขนส่งซิงเกิลที่เป็นรัฐจาก VM ไปยัง VM อื่น ซิงเกิลตันหมายถึง "เอกสิทธิ์ภายใน VM" ไม่ใช่ "เอกสิทธิ์ในเอกภพ"

หากการทำให้เป็นอนุกรมนั้นสมเหตุสมผลสำหรับ singleton stateful Singleton ควรระบุอย่างชัดเจนและแม่นยำว่าการ deserialize singleton ใน VM อื่นที่มี singleton ชนิดเดียวกันนั้นมีอยู่จริง

Foo2มุ่งมั่นกับกลยุทธ์การทำให้เป็นอันดับ / การดีซีเรียลไลเซชันแบบง่ายๆโดยอัตโนมัติ นั่นเป็นเพียงอุบัติเหตุที่รอให้เกิดขึ้น หากเรามีโครงสร้างข้อมูลอ้างอิงแนวคิดของตัวแปรสถานะFoo2ใน VM1 ที่ t1 ผ่านการทำให้เป็นอนุกรม / การดีซีเรียลไลเซชันค่าจะกลายเป็นค่าที่แตกต่าง - มูลค่าของตัวแปรเดียวกันFoo2ใน VM2 ที่ t2 ทำให้เกิดข้อผิดพลาดในการตรวจจับยาก ข้อผิดพลาดนี้จะไม่เกิดขึ้นกับคนที่ไม่สามารถตั้งค่าได้Foo1อย่างเงียบ ๆ

ข้อ จำกัด ของการเข้ารหัส

มีสิ่งต่าง ๆ ที่สามารถทำได้ในชั้นเรียนปกติ แต่ต้องห้ามในenumชั้นเรียน ตัวอย่างเช่นการเข้าถึงฟิลด์คงที่ในตัวสร้าง โปรแกรมเมอร์จะต้องระมัดระวังมากขึ้นเนื่องจากเขาทำงานในชั้นเรียนพิเศษ

ข้อสรุป

โดย piggybacking บน enum เราบันทึกโค้ดไว้ 2 บรรทัด; แต่ราคาสูงเกินไปเราต้องแบกกระเป๋าและข้อ จำกัด ทั้งหมดของ enums เราได้รับมรดก "คุณสมบัติ" ของ enum ที่มีผลกระทบโดยไม่ตั้งใจ ข้อได้เปรียบที่ถูกกล่าวหาเพียงอย่างเดียว - การต่ออนุกรมอัตโนมัติ - กลายเป็นข้อเสีย


2
-1: การอภิปรายเกี่ยวกับการทำให้เป็นอันดับเป็นสิ่งที่ผิด กลไกนั้นไม่ง่ายนักเนื่องจาก enums จะได้รับการปฏิบัติแตกต่างจากอินสแตนซ์ทั่วไประหว่างการดีซีเรียลไลเซชัน ปัญหาที่อธิบายไม่ได้เกิดขึ้นเนื่องจากกลไกการดีซีเรียลไลซ์เซชั่นที่แท้จริงไม่ได้แก้ไข "ตัวแปรสถานะ"
scarfridge

ดูตัวอย่างของความสับสนนี้: coderanch.com/t/498782/java/java/ …
แก้ไขได้

2
ที่จริงแล้วการสนทนาที่เชื่อมโยงเน้นจุดของฉัน ให้ฉันอธิบายสิ่งที่ฉันเข้าใจว่าเป็นปัญหาที่คุณอ้างว่ามีอยู่ วัตถุบางอย่าง A อ้างอิงถึงวัตถุที่สอง B อินสแตนซ์ซิงเกิล S อ้างอิง B เช่นกัน ตอนนี้เราเลิกใช้อินสแตนซ์ที่ทำให้เป็นอนุกรมก่อนหน้านี้ของซิงเกิลอิง enum (ซึ่ง ณ เวลาของการทำให้เป็นอนุกรมอ้างอิง B '! = B) สิ่งที่เกิดขึ้นจริงคือ A และ S อ้างอิง B เพราะ B 'จะไม่ถูกทำให้เป็นอนุกรม ฉันคิดว่าคุณต้องการแสดงว่า A และ S ไม่ได้อ้างอิงวัตถุเดียวกันอีกต่อไป
scarfridge

1
บางทีเราไม่ได้พูดถึงปัญหาเดียวกันนี้จริงเหรอ?
scarfridge

1
@Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);เช่นเป็นตัวอย่างที่ดีจริงๆ: Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);; ^) ทดสอบภายใต้ Java 7 และ Java 8 ...
Holger

6

ตัวอย่าง enum ขึ้นอยู่กับ class loader นั่นคือหากคุณมีตัวโหลดคลาสที่สองที่ไม่มีตัวโหลดคลาสแรกที่พาเรนต์โหลดคลาส enum เดียวกันคุณสามารถรับอินสแตนซ์หลายตัวในหน่วยความจำ


ตัวอย่างโค้ด

สร้าง enum ต่อไปนี้และวางไฟล์. class ลงใน jar ด้วยตัวเอง (แน่นอนว่า jar จะมีโครงสร้างแพ็คเกจ / โฟลเดอร์ที่ถูกต้อง)

package mad;
public enum Side {
  RIGHT, LEFT;
}

ตอนนี้ทำการทดสอบนี้ตรวจสอบให้แน่ใจว่าไม่มีสำเนาของ enum ด้านบนเส้นทางคลาส:

@Test
public void testEnums() throws Exception
{
    final ClassLoader root = MadTest.class.getClassLoader();

    final File jar = new File("path to jar"); // Edit path
    assertTrue(jar.exists());
    assertTrue(jar.isFile());

    final URL[] urls = new URL[] { jar.toURI().toURL() };
    final ClassLoader cl1 = new URLClassLoader(urls, root);
    final ClassLoader cl2 = new URLClassLoader(urls, root);

    final Class<?> sideClass1 = cl1.loadClass("mad.Side");
    final Class<?> sideClass2 = cl2.loadClass("mad.Side");

    assertNotSame(sideClass1, sideClass2);

    assertTrue(sideClass1.isEnum());
    assertTrue(sideClass2.isEnum());
    final Field f1 = sideClass1.getField("RIGHT");
    final Field f2 = sideClass2.getField("RIGHT");
    assertTrue(f1.isEnumConstant());
    assertTrue(f2.isEnumConstant());

    final Object right1 = f1.get(null);
    final Object right2 = f2.get(null);
    assertNotSame(right1, right2);
}

และตอนนี้เรามีวัตถุสองชิ้นที่แสดงถึงค่า enum ที่เหมือนกัน

ฉันยอมรับว่านี่เป็นกรณีมุมที่หายากและมีการประดิษฐ์และเกือบจะสามารถใช้ enum สำหรับ java singleton ได้ ฉันทำมันเอง แต่คำถามที่ถามเกี่ยวกับข้อเสียที่อาจเกิดขึ้นและหมายเหตุข้อควรระวังนี้ก็คุ้มค่าที่จะรู้


คุณสามารถค้นหาการอ้างอิงถึงข้อกังวลนั้นได้หรือไม่?

ตอนนี้ฉันได้แก้ไขและปรับปรุงคำตอบเบื้องต้นด้วยรหัสตัวอย่าง หวังว่าพวกเขาจะช่วยอธิบายจุดและตอบคำถามของ MichaelT เช่นกัน
Mad G

@MichaelT: ฉันหวังว่าจะตอบคำถามของคุณ :-)
Mad G

ดังนั้นถ้าเหตุผลในการใช้ enum (แทนที่จะเป็นคลาส) สำหรับ singleton เป็นเพียงความปลอดภัยเท่านั้นไม่มีเหตุผลอะไรเลยในตอนนี้ ... ยอดเยี่ยม +1
Gangnus

1
การใช้งานซิงเกิลตันแบบ "ดั้งเดิม" จะทำงานตามที่คาดไว้แม้จะใช้กับรถตักสองชั้นที่แตกต่างกันหรือไม่?
Ron Klein

3

ไม่สามารถใช้รูปแบบ enum สำหรับคลาสใด ๆ ที่อาจทำให้เกิดข้อยกเว้นในตัวสร้าง หากจำเป็นต้องใช้โรงงาน:

class Elvis {
    private static Elvis self = null;
    private int age;

    private Elvis() throws Exception {
        ...
    }

    public synchronized Elvis getInstance() throws Exception {
        return self != null ? self : (self = new Elvis());
    }

    public int getAge() {
        return age;
    }        
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.