Apa kelemahan menerapkan singleton dengan Java enum?

14

Secara tradisional, singleton biasanya diimplementasikan sebagai

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Dengan Java enum, kita dapat mengimplementasikan singleton as

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Sehebat versi ke-2, apakah ada kerugian untuk itu?

(Saya memberikan beberapa pemikiran dan saya akan menjawab pertanyaan saya sendiri; semoga Anda memiliki jawaban yang lebih baik)

tak dapat disangkal
sumber
16
The downside adalah bahwa itu adalah singleton. Sebuah "pola" yang benar-benar berlebihan ( batuk )
Thomas Eding

Jawaban:

32

Beberapa masalah dengan lajang enum:

Berkomitmen pada strategi implementasi

Biasanya, "singleton" mengacu pada strategi implementasi, bukan spesifikasi API. Sangat jarang untuk Foo1.getInstance()secara terbuka menyatakan bahwa itu akan selalu mengembalikan contoh yang sama. Jika diperlukan, implementasi Foo1.getInstance()dapat berkembang, misalnya, untuk mengembalikan satu instance per utas.

Dengan Foo2.INSTANCEkita secara terbuka menyatakan bahwa hal ini adalah dengan contoh, dan tidak ada kesempatan untuk mengubah itu. Strategi implementasi memiliki satu contoh terbuka dan berkomitmen untuk.

Masalah ini tidak melumpuhkan. Misalnya, Foo2.INSTANCE.doo()dapat mengandalkan objek pembantu lokal utas, untuk secara efektif memiliki instance per-utas.

Memperluas kelas Enum

Foo2memperluas kelas super Enum<Foo2>. Kami biasanya ingin menghindari kelas super; terutama dalam kasus ini, kelas super yang dipaksakan Foo2tidak ada hubungannya dengan apa Foo2yang seharusnya. Itu adalah polusi pada jenis hierarki aplikasi kita. Jika kita benar-benar menginginkan kelas super, biasanya kelas aplikasi, tetapi kita tidak bisa, Foo2kelas supernya sudah diperbaiki.

Foo2mewarisi beberapa metode contoh lucu seperti name(), cardinal(), compareTo(Foo2), yang hanya membingungkan Foo2pengguna. Foo2tidak dapat memiliki name()metode sendiri bahkan jika metode itu diinginkan dalam Foo2antarmuka.

Foo2 juga mengandung beberapa metode statis lucu

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

yang tampaknya tidak masuk akal bagi pengguna. Singleton biasanya tidak seharusnya memiliki metode statis pulbic (selain dari getInstance())

Serializability

Sangat umum bagi lajang untuk menyatakan diri. Lajang ini umumnya tidak boleh serial. Saya tidak bisa memikirkan contoh realistis di mana masuk akal untuk mengangkut singleton stateful dari satu VM ke VM lain; singleton berarti "unik di dalam VM", bukan "unik di alam semesta".

Jika serialisasi benar-benar masuk akal untuk singleton stateful, singleton harus secara eksplisit dan tepat menentukan apa artinya deserialisasi singleton di VM lain di mana singleton dari tipe yang sama mungkin sudah ada.

Foo2secara otomatis berkomitmen pada strategi serialisasi / deserialisasi sederhana. Itu hanya kecelakaan yang menunggu untuk terjadi. Jika kita memiliki pohon data yang secara konseptual merujuk variabel keadaan Foo2dalam VM1 di t1, melalui serialisasi / deserialisasi nilai menjadi nilai yang berbeda - nilai variabel yang sama Foo2di dalam VM2 di t2, membuat sulit untuk mendeteksi bug. Bug ini tidak akan terjadi pada yang tidak dapat digunakan Foo1diam-diam.

Batasan pengkodean

Ada hal yang bisa dilakukan di kelas normal, tetapi dilarang di enumkelas. Misalnya, mengakses bidang statis di konstruktor. Programmer harus lebih berhati-hati karena dia bekerja di kelas khusus.

Kesimpulan

Dengan membonceng enum, kami menyimpan 2 baris kode; tetapi harga terlalu tinggi, kita harus membawa semua kantong dan pembatasan enum, kita secara tidak sengaja mewarisi "fitur" enum yang memiliki konsekuensi yang tidak diinginkan. Satu-satunya keuntungan yang diduga - serializability otomatis - ternyata menjadi kerugian.

tak dapat disangkal
sumber
2
-1: Diskusi Anda tentang serialisasi salah. Mekanisme ini tidak sederhana karena enum diperlakukan sangat berbeda dari contoh biasa selama deserialisasi. Masalah yang diuraikan tidak terjadi karena mekanisme deserialisasi sebenarnya tidak mengubah "variabel keadaan".
scarfridge
lihat contoh kebingungan ini: coderanch.com/t/498782/java/java/…
dikembalikan
2
Sebenarnya diskusi terkait menekankan poin saya. Biarkan saya menjelaskan apa yang saya pahami sebagai masalah yang Anda klaim ada. Beberapa objek A referensi objek kedua B. Contoh tunggal S referensi B juga. Sekarang kita deserialize contoh serial sebelumnya dari singleton berbasis enum (yang, pada saat serialisasi, dirujuk B '! = B). Apa yang sebenarnya terjadi adalah, bahwa A dan S referensi B, karena B 'tidak akan diserialisasi. Saya pikir Anda ingin menyatakan bahwa A dan S tidak merujuk objek yang sama lagi.
scarfridge
1
Mungkin kita tidak benar-benar membicarakan masalah yang sama?
scarfridge
1
@Kevin Krumwiede:, Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);misalnya contoh yang sangat bagus adalah :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), diuji di bawah Java 7 dan Java 8 ...
Holger
6

Instance enum tergantung pada loader kelas. yaitu jika Anda memiliki pemuat kelas kedua yang tidak memiliki pemuat kelas pertama sebagai induk yang memuat kelas enum yang sama, Anda bisa mendapatkan beberapa instance dalam memori.


Contoh kode

Buat enum berikut, dan masukkan file .class ke dalam toples dengan sendirinya. (tentu saja toples akan memiliki struktur paket / folder yang benar)

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

Sekarang jalankan tes ini, pastikan tidak ada salinan enum di atas di jalur kelas:

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

Dan kita sekarang memiliki dua objek yang mewakili nilai enum "sama".

Saya setuju bahwa ini adalah kasus sudut yang langka dan dibuat-buat, dan hampir selalu enum dapat digunakan untuk java singleton. Saya melakukannya sendiri. Tetapi pertanyaan yang diajukan tentang potensi kerugian dan catatan kehati-hatian ini layak untuk diketahui.

Gila G
sumber
Bisakah Anda menemukan referensi untuk masalah itu?
Saya sekarang telah mengedit dan meningkatkan jawaban awal saya dengan kode contoh. Semoga mereka akan membantu mengilustrasikan poin dan menjawab pertanyaan MichaelT juga.
Mad G
@MichaelT: Saya harap itu menjawab pertanyaan Anda :-)
Mad G
Jadi, jika alasan penggunaan enum (bukan kelas) untuk singleton hanya keamanannya, tidak ada alasan untuk itu sekarang ... Luar biasa, +1
Gangnus
1
Akankah implementasi singleton "tradisional" berperilaku seperti yang diharapkan bahkan dengan dua loader kelas yang berbeda?
Ron Klein
3

enum pattern tidak dapat digunakan untuk Kelas apa pun yang akan melempar pengecualian pada konstruktor. Jika ini diperlukan, gunakan pabrik:

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;
    }        
}
pengguna3158036
sumber