web-dev-qa-db-ja.com

Javaの列挙型を使用してシングルトンを実装することの欠点は何ですか?

従来、シングルトンは通常、次のように実装されています。

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Javaの列挙型を使用すると、シングルトンを次のように実装できます。

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

2番目のバージョンと同じくらい素晴らしいですが、欠点はありますか?

(私はそれにいくつかの考えを与えました、そして私は私自身の質問に答えます;うまくいけばあなたはより良い答えがあるでしょう)

14
irreputable

列挙型シングルトンに関するいくつかの問題:

実装戦略への取り組み

通常、「シングルトン」は、API仕様ではなく実装戦略を指します。 Foo1.getInstance()が常に同じインスタンスを返すことを公に宣言することは非常にまれです。必要に応じて、Foo1.getInstance()の実装を進化させ、たとえば、スレッドごとに1つのインスタンスを返すことができます。

_Foo2.INSTANCE_を使用して、このインスタンスがtheインスタンスであることを公に宣言します。これを変更することはできません。単一のインスタンスを持つ実装戦略が公開され、コミットされます。

この問題は問題ではありません。たとえば、Foo2.INSTANCE.doo()は、スレッドローカルヘルパーオブジェクトに依存して、スレッドごとのインスタンスを実質的ににすることができます。

Enumクラスの拡張

_Foo2_はスーパークラス_Enum<Foo2>_を拡張します。通常、スーパークラスは避けたいものです。特にこの場合、_Foo2_に強制されるスーパークラスは、_Foo2_が想定されているものとは何の関係もありません。これは、アプリケーションのタイプ階層に対する汚染です。スーパークラスが本当に必要な場合、通常はアプリケーションクラスですが、できません。_Foo2_のスーパークラスは修正されています。

_Foo2_は、_Foo2_のユーザーを混乱させるname(), cardinal(), compareTo(Foo2)などの面白いインスタンスメソッドを継承します。 _Foo2_のインターフェースでそのメソッドが望ましい場合でも、_Foo2_は独自のname()メソッドを持つことができません。

_Foo2_には、面白い静的メソッドもいくつか含まれています

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

これはユーザーにとって無意味なようです。シングルトンは通常、とにかく静的な静的メソッドを持つべきではありません(getInstance()以外)

シリアライザビリティ

シングルトンがステートフルであることは非常に一般的です。これらのシングルトンは一般的にnotでシリアライズ可能でなければなりません。 VMから別のVMにステートフルシングルトンを転送することが理にかなっている現実的な例は考えられません。シングルトンは、「ユニバース内で一意」ではなく、「VM内で一意」を意味します。 」.

シリアル化がステートフルシングルトンに対して本当に意味がある場合、シングルトンは、別のVMで同じタイプのシングルトンがすでに存在する可能性がある場合にシングルトンを逆シリアル化することの意味を明示的かつ正確に指定する必要があります。

_Foo2_は、単純化されたシリアライゼーション/デシリアライゼーション戦略に自動的にコミットします。それは起こるのを待っているただの事故です。 VM1でt1に_Foo2_の状態変数を概念的に参照しているデータのツリーがある場合、シリアル化/逆シリアル化により、値は異なる値になります-t2でVM2の_Foo2_の同じ変数の値、検出が困難なバグを作成します。このバグは、シリアライズできない_Foo1_には通知せずに発生しません。

コーディングの制限

通常のクラスでできることはありますが、enumクラスでは禁止されています。たとえば、コンストラクターの静的フィールドにアクセスします。プログラマーは特別なクラスで作業しているため、より注意深くする必要があります。

結論

列挙型に便乗することで、2行のコードを節約できます。しかし、価格が高すぎるため、列挙型のすべての手荷物と制限を運ぶ必要があります。意図しない結果をもたらす列挙型の「機能」をうっかり継承してしまいます。申し立てられている唯一の利点-自動シリアライザビリティ-は欠点であることが判明しました。

32
irreputable

列挙インスタンスはクラスローダーに依存しています。つまり、同じ列挙型クラスをロードする親として最初のクラスローダーを持たない2番目のクラスローダーがある場合、メモリ内に複数のインスタンスを取得できます。


コードサンプル

次の列挙型を作成し、その.classファイルを単独でjarに入れます。 (もちろん、jarは正しいパッケージ/フォルダー構造になります)

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

次に、このテストを実行して、クラスパスに上記の列挙型のコピーがないことを確認します。

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

これで、「同じ」列挙値を表す2つのオブジェクトができました。

これはまれで不自然なコーナーケースであり、ほとんどの場合、enumをJavaシングルトンに使用できます。私は自分で行います。しかし、潜在的な欠点とこのメモについて質問しました注意は知っておく価値があります。

6
Mad G

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;
    }        
}
3
user3158036