Nhược điểm của việc triển khai một singleton với enum của Java là gì?


14

Theo truyền thống, một singleton thường được thực hiện như

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Với enum của Java, chúng ta có thể triển khai một singleton như

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Tuyệt vời như phiên bản thứ 2, có bất kỳ nhược điểm nào không?

(Tôi đã cho nó một vài suy nghĩ và tôi sẽ trả lời câu hỏi của riêng tôi; hy vọng bạn có câu trả lời tốt hơn)


16
Nhược điểm của nó là một singleton. Một "mẫu" hoàn toàn được đánh giá cao ( ho )
Thomas Eding

Câu trả lời:


32

Một số vấn đề với enum singletons:

Cam kết thực hiện chiến lược

Thông thường, "singleton" đề cập đến một chiến lược triển khai, không phải là một đặc tả API. Rất hiếm khi Foo1.getInstance()tuyên bố công khai rằng nó sẽ luôn trả về cùng một ví dụ. Nếu cần, việc triển khai Foo1.getInstance()có thể phát triển, ví dụ, để trả về một thể hiện cho mỗi luồng.

Với Foo2.INSTANCEchúng tôi công khai tuyên bố rằng trường hợp này là các ví dụ, và không có cơ hội để thay đổi điều đó. Chiến lược thực hiện có một trường hợp duy nhất được đưa ra và cam kết.

Vấn đề này không làm tê liệt. Ví dụ, Foo2.INSTANCE.doo()có thể dựa vào một đối tượng trợ giúp cục bộ của luồng, để có một thể hiện trên mỗi luồng một cách hiệu quả .

Mở rộng lớp Enum

Foo2mở rộng một siêu hạng Enum<Foo2>. Chúng ta thường muốn tránh các siêu lớp; đặc biệt trong trường hợp này, siêu hạng bị ép buộc Foo2không liên quan gì đến những gì Foo2được cho là. Đó là một sự ô nhiễm đối với hệ thống phân cấp loại ứng dụng của chúng tôi. Nếu chúng ta thực sự muốn một siêu hạng, thường thì đó là một lớp ứng dụng, nhưng chúng ta không thể, Foo2siêu hạng đó đã được sửa.

Foo2kế thừa một số phương thức ví dụ thú vị như name(), cardinal(), compareTo(Foo2), gây nhầm lẫn cho Foo2người dùng. Foo2không thể có name()phương thức riêng ngay cả khi phương thức đó được mong muốn trong Foo2giao diện.

Foo2 cũng chứa một số phương thức tĩnh vui

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

mà dường như là vô nghĩa với người dùng. Dù sao thì một singleton thường không nên có các phương thức tĩnh xung (trừ các phương thức getInstance())

Tính tuần tự

Nó là rất phổ biến cho các singletons là trạng thái. Những singletons nói chung không nên được tuần tự hóa. Tôi không thể nghĩ ra bất kỳ ví dụ thực tế nào có ý nghĩa khi vận chuyển một singleton trạng thái từ VM này sang VM khác; một singleton có nghĩa là "duy nhất trong VM", không phải "duy nhất trong vũ trụ".

Nếu việc tuần tự hóa thực sự có ý nghĩa đối với một singleton có trạng thái, thì singleton nên xác định rõ ràng và chính xác ý nghĩa của việc khử lưu trữ một singleton trong một VM khác trong đó một singleton cùng loại có thể tồn tại.

Foo2tự động cam kết chiến lược tuần tự hóa / giải tuần tự đơn giản hóa. Đó chỉ là một tai nạn đang chờ để xảy ra. Nếu chúng ta có một cây dữ liệu tham chiếu một cách khái niệm một biến trạng thái Foo2trong VM1 tại t1, thông qua việc tuần tự hóa / giải tuần tự hóa, giá trị sẽ trở thành một giá trị khác - giá trị của cùng một biến Foo2trong VM2 tại t2, tạo ra một lỗi khó phát hiện. Lỗi này sẽ không xảy ra với Foo1âm thầm không thể chỉnh sửa.

Hạn chế của mã hóa

Có những thứ có thể được thực hiện trong các lớp học bình thường, nhưng bị cấm trong enumcác lớp học. Ví dụ: truy cập một trường tĩnh trong hàm tạo. Lập trình viên phải cẩn thận hơn vì anh ta làm việc trong một lớp học đặc biệt.

Phần kết luận

Bằng cách cõng trên enum, chúng tôi lưu được 2 dòng mã; nhưng giá quá cao, chúng tôi phải mang theo tất cả các hành lý và hạn chế của enum, chúng tôi vô tình thừa hưởng "tính năng" của enum có hậu quả không lường trước được. Ưu điểm duy nhất bị cáo buộc - tính tuần tự tự động - hóa ra là một bất lợi.


2
-1: Thảo luận của bạn về tuần tự hóa là sai. Cơ chế này không đơn giản vì enum được đối xử rất khác so với các trường hợp thông thường trong quá trình khử lưu huỳnh. Vấn đề được mô tả không xảy ra vì cơ chế khử lưu huỳnh thực tế không sửa đổi "biến trạng thái".
Scarfridge

xem ví dụ về sự nhầm lẫn này: coderanch.com/t/498782/java/java/ Kẻ
chối cãi

2
Trên thực tế các cuộc thảo luận liên kết nhấn mạnh quan điểm của tôi. Hãy để tôi giải thích những gì tôi hiểu là vấn đề bạn tuyên bố tồn tại. Một số đối tượng A tham chiếu một đối tượng thứ hai B. Ví dụ singleton S cũng tham chiếu B. Bây giờ chúng tôi giải tuần tự hóa một phiên bản được xê-ri hóa trước đó của singleton dựa trên enum (tại thời điểm xê-ri hóa, đã tham chiếu B '! = B). Điều thực sự xảy ra là, A S tham chiếu B, vì B 'sẽ không được nối tiếp. Tôi nghĩ bạn muốn bày tỏ rằng A và S không tham chiếu cùng một đối tượng nữa.
Scarfridge

1
Có lẽ chúng ta không thực sự nói về cùng một vấn đề?
Scarfridge

1
@Kevin Krumwiede : Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);, ví dụ: một ví dụ thực sự hay sẽ là : Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);; ^), được thử nghiệm theo Java 7 và Java 8 khắc
Holger

6

Một thể hiện enum phụ thuộc vào trình nạp lớp. tức là nếu bạn có trình tải lớp thứ hai không có trình nạp lớp thứ nhất với tư cách là cha mẹ đang tải cùng một lớp enum, bạn có thể nhận được nhiều phiên bản trong bộ nhớ.


Mẫu mã

Tạo enum sau đây và đặt tập tin. Class của nó vào một cái bình. (tất nhiên jar sẽ có cấu trúc gói / thư mục chính xác)

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

Bây giờ hãy chạy thử nghiệm này, đảm bảo rằng không có bản sao của enum trên trên đường dẫn lớp:

@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);
}

Và bây giờ chúng ta có hai đối tượng đại diện cho giá trị enum "giống nhau".

Tôi đồng ý rằng đây là một trường hợp góc hiếm gặp và khó khăn, và hầu như luôn luôn là một enum có thể được sử dụng cho một đĩa đơn java. Tôi tự làm nó. Nhưng câu hỏi hỏi về nhược điểm tiềm năng và lưu ý thận trọng này là đáng để biết.


Bạn có thể tìm thấy bất kỳ tài liệu tham khảo cho mối quan tâm đó?

Bây giờ tôi đã chỉnh sửa và nâng cao câu trả lời ban đầu của mình bằng mã ví dụ. Hy vọng họ cũng sẽ giúp minh họa điểm và trả lời câu hỏi của MichaelT.
Mad G

@MichaelT: Tôi hy vọng rằng câu trả lời cho câu hỏi của bạn :-)
Mad G

Vì vậy, nếu lý do sử dụng enum (thay vì lớp học) cho singleton chỉ là sự an toàn của nó, thì không có lý do nào cho nó bây giờ ... Tuyệt vời, +1
Gangnus

1
Việc triển khai singleton "truyền thống" sẽ hoạt động như mong đợi ngay cả với hai trình nạp lớp khác nhau?
Ron Klein

3

mẫu enum không thể được sử dụng cho bất kỳ Lớp nào sẽ ném ngoại lệ vào hàm tạo. Nếu điều này là bắt buộc, sử dụng một nhà máy:

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;
    }        
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.