Singleton大分裂

たった1つのインスタンス? 〜Singletonを破ってみよう〜

GoFデザインパターンの中に、Singletonと呼ばれる便利なパターンがあります。

結城浩著『増補改訂版Java言語で学ぶデザインパターン入門』によると、Singletonとは

・指定したクラスのインスタンスが絶対に1個しか存在しないことを保証したい
インスタンスが1個しか存在しないことをプログラム上で表現したい

といった要件を充足するパターンのことを指します(p.58より)。

Singletonクラスを実装するときの定石は、コンストラクタをprivateにして、外部から無分別にインスタンス化されないようにします。ひとつ例を示しましょう。

Singleton.java

// 従来のイディオムを用いて書いたシングルトンクラス
public class Singleton {
    private static final Singleton instance = new Singleton();
    private int value;

    // コンストラクタ
    private Singleton() { value = 0; }
    
    // アクセサ
    public static Singleton getInstance() { return instance;    }
    
    public int  getValue()                { return value;       }
    public void setValue(int value)       { this.value = value; }

    
    @Override 
    public String toString() { return String.valueOf(getValue()); }
}


ご覧のとおり、Singletonクラスはコンストラクタが隠蔽されています。外部からはnewできないため、代わりにgetInstance()メソッドを使って内部生成された単一インスタンスを取得することになります。

ここで改めて、『増補改訂版Java言語で学ぶデザインパターン入門』の59ページから引用します。

Singletonパターンは、プログラマがどう間違ってもインスタンスが1個しか生成されないことを保証する、というパターンでしたね。この保証のために、コンストラクタをprivateにしておく必要があるのです。

確かにコンストラクタをprivateにすれば、クラスの外からインスタンスをnewすることはできなくなります。

しかし残念ながらそれだけではインスタンスの単一性は保証されません。インスタンス化する方法は、なにもnewだけではないからです。

Main.java

import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args)  {
        // 通常は、クラスメソッド経由でインスタンスを取得する
        Singleton instance = Singleton.getInstance();
        instance.setValue(100);

        // しかし、悪い人はこうやってprivateコンストラクタを破る
        Singleton anotherInstance = null;
        try {
            final String CLASS_NAME = "Singleton";
            Constructor<?> ctor = Class.forName(CLASS_NAME).getDeclaredConstructor();
            ctor.setAccessible(true);
            anotherInstance = (Singleton)ctor.newInstance();
            anotherInstance.setValue(50);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 中身の出力
        if(instance != null)        System.out.println("instance:        " + instance);
        if(anotherInstance != null) System.out.println("anotherInstance: " + anotherInstance);

        // 同一性比較
        if (!instance.equals(anotherInstance)) {
            System.err.println("両インスタンスは異なります");
        }
    }
}

実行すると、シングルトンの原則が破綻してしまっていることが分かります。

実行結果

両インスタンスは異なります
instance:        100
anotherInstance: 50

ではどうするか

Effective Java 第2版 (The Java Series)』では、好ましいSingleton実装として、enum型を用いたイディオムが挙げられています。

実装し直してみると、こんな感じになります。

Singleton.java

// 望ましいシングルトン実装
public enum Singleton {
    instance;
    
    private int value;

    // コンストラクタ
    private Singleton() { value = 0; }
    
    // アクセサ
    public int  getValue()                { return value;       }
    public void setValue(int value)       { this.value = value; }

    @Override 
    public String toString() { return String.valueOf(getValue()); }
}

従来のgetInstance()メソッドの代わりに、instance要素にアクセスすることで、シングルトンオブジェクトを取得します。

Singleton instance = Singleton.instance;

あとは、普通のオブジェクト操作と同様の構文で大丈夫。先程のようなリフレクション攻撃に対しても免疫があります。構文も簡潔になり、一石二鳥*1

めでたし。

*1:本当は直列化まわりの問題も解決されるため三鳥。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.