読者です 読者をやめる 読者になる 読者になる

JavaでSingletonパターンを実装するのにclassとenumのどちらを使うべきか

Java6でSingletonパターンを実装する方法を勉強した。
JavaでSingletonを実装する方法は大きく分けてclassを使う方法とenumを使う方法の2通りが存在する。
classだとシリアライズ・デシリアライズの際にインスタンスの唯一性を保つためにはあれこれと細かい設定が必要になる。
一方で、enum型を利用するとそういった煩雑さから解放される。

classを利用する場合

classを利用する場合、実装方法は大きく分けて2通り

1. public static finalなフィールドでインスタンスを管理
class Singleton {
    // finalにすることで唯一のインスタンスを上書きできないようにする
    public static final INSTANCE = new Singleton();

    // 単なるフィールド
    private String field;

    // コンストラクタをprivateにすることでクラス外でのインスタンス化が不可能にする
    private Singleton() {}

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }
}
2. privateなフィールドでインスタンスを管理しfactoryメソッドでインスタンスを提供
class Singleton {
    // privateフィールドを利用
    private static Singleton INSTANCE = new Singleton();

    // 単なるフィールド
    private String field;

    // コンストラクタをprivateにすることでクラス外でのインスタンス化が不可能にする
    private Singleton() {}

    // インスタンスを提供するfactoryメソッド
    public static Singleton getInstance() {
        return INSTANCE;
    }

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }
}

しかしながら、これらをシリアライズ可能にするには細心の注意が必要。

1. public static finalなフィールドでインスタンスを管理(シリアライズ可能版)
class Singleton implements Serializable {
    // finalにすることで唯一のインスタンスを上書きできないようにする
    public static final INSTANCE = new Singleton();

    // 全てのフィールドはtransientにする必要がある
    private transient String field;

    // コンストラクタをprivateにすることでクラス外でのインスタンス化が不可能にする
    private Singleton() {}

    // readObjectメソッドの内容を破棄する
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }
}
2. privateなフィールドでインスタンスを管理しfactoryメソッドでインスタンスを提供(シリアライズ可能版)
class Singleton implements Serializable {
    // privateフィールドを利用
    private static Singleton INSTANCE = new Singleton();

    // 全てのフィールドはtransientにする必要がある
    private transient String field;

    // コンストラクタをprivateにすることでクラス外でのインスタンス化が不可能にする
    private Singleton() {}

    // インスタンスを提供するfactoryメソッド
    public static Singleton getInstance() {
        return INSTANCE;
    }

    // readObjectメソッドの内容を破棄する
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }
}

このようにclassベースでSingletonパターンをバグなく実装するのは結構煩雑な作業を要する。
そこで、最近ではenum型を利用した実装法が推奨されるようになっている。

enum型を利用

enum Singleton {
    INSTANCE;
    private String field;
    public String getField() {
        return field;
    }
    public void setField(String field) {
        this.field = field;
    }
}

enum型を用いると宣言された定数の他にインスタンスが存在しないことをJVMが保証してくれることになる。
また、シリアライズ・デシリアライズに関してややこしいことをする必要もない。

enum型を用いる場合の問題点

enum型を使えば非常に簡単かつ堅牢なSingletonを実装できるが、enum型の特性に起因する問題点として、コンパイル時にインスタンスが分かっていないような場合はenumを使えない。
そのため、そのようなインスタンスを実装するにはclassを用いる必要がある。

結論

Singletonを実装するにはenum型を使用すべき。ただ、シリアライズ可能でかつenum型を使えない場合は、注意してreadResolveメソッドを提供し、かつ全ての参照を保持するインスタンスフィールドをtransientにする必要がある。

参考

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)