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


14

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

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番目のバージョンと同じくらいすごいのですが、欠点はありますか?

(私はそれにいくつかの考えを与え、私は自分の質問に答えます;うまくいけば、より良い答えがあります)


16
欠点は、シングルトンであるということです。完全に過大評価()「パターン」
トーマスエディング

回答:


32

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

実装戦略へのコミット

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

ではFoo2.INSTANCE、私たち公にこのインスタンスがあることを宣言し、インスタンス、およびそれを変更する可能性はありません。単一のインスタンスを持つという実装戦略が公開され、コミットされています。

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

Enumクラスの拡張

Foo2スーパークラスを拡張しEnum<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 staticメソッドを持つべきではありません(以外getInstance()

シリアライズ可能性

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

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

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

コーディングの制限

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

結論

enumに便乗することで、2行のコードを保存します。しかし、価格が高すぎるため、enumのすべての荷物と制限を持たなければなりません。意図しない結果をもたらすenumの「機能」を不注意に継承します。主張されている唯一の利点-自動シリアライズ可能性-は、欠点であることが判明しました。


2
-1:シリアル化の説明が間違っています。列挙体は、逆シリアル化中に通常のインスタンスと非常に異なる方法で処理されるため、メカニズムは単純化されていません。実際の逆シリアル化メカニズムは「状態変数」を変更しないため、説明されている問題発生しません。
スカーフリッジ

:混乱のこの例を参照coderanch.com/t/498782/java/java/...
irreputable

2
実際、リンクされた議論は私のポイントを強調します。あなたが存在すると主張する問題として私が理解したことを説明させてください。一部のオブジェクトAは2番目のオブジェクトBを参照します。シングルトンインスタンスSもBを参照します。次に、以前にシリアル化された列挙ベースのシングルトンのインスタンスをシリアル化解除します(シリアル化時にB '!= Bを参照しました)。実際に起こるのは、B 'がシリアル化されないため、A SがBを参照することです。AとSはもう同じオブジェクトを参照していないことを表明したいと思っていました。
スカーフリッジ

1
たぶん、私たちは本当に同じ問題について話しているのではないでしょうか?
スカーフリッジ

1
@Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);、例えばA本当に素晴らしい例は次のようになります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の下でテスト...
ホルガー

6

enumインスタンスは、クラスローダーに依存しています。つまり、同じ列挙クラスをロードする親として最初のクラスローダーを持たない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シングルトンに使用できることに同意します。自分でやります。しかし、潜在的な欠点について質問されたため、この注意書きは知っておく価値があります。


その懸念への参照を見つけることができますか?

サンプルコードを使用して、最初の回答を編集および強化しました。うまくいけば、ポイントを説明し、MichaelTのクエリにも答えることができます。
マッドG

@MichaelT:それがあなたの質問に答えることを願っています:
マッドG

シングルトンのために(代わりにクラスの)列挙型を使用する理由は、唯一、その安全性をしたのであれば、それについてどのような理由は、...今+1優秀ありません
Gangnus

1
「従来の」シングルトン実装は、2つの異なるクラスローダーを使用しても期待どおりに動作しますか?
ロンクライン

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.