Singletonのサブクラス化

結城浩

Javaで、Singletonのサブクラス化のコーディングを試みます。インスタンスの唯一性をどのように保証するかを考えましょう。

目次

はじめに

Singletonパターンのサブクラス化に関連したJavaのサンプルコードを紹介します。

このページを作成するにあたっては、GoF本はもちろんのこと、 デザインパターン・メーリングリストでのメールのやりとりを参考にしました。 澤田聡司さん、小山さん、中野靖治さん、宮本さん、杉村貴士さん、出井秀行さん、 その他のみなさんに感謝します。 特に、澤田さんから多くの情報と示唆をいただきました。感謝します。

(メモ:GoF本のSingletonのバリエーションに言及する必要あり。いつか加筆すること)

メモ:以下のソースではRuntimeExceptionを使用しています。 一般に、RuntimeExceptionの利用には注意が必要ですが、 このソースでRuntimeExceptionをthrowしている部分は「プログラマの誤り」に起因するところであり、 正しい用法のはずです。

(1) Singletonの基本形 —— 唯一のインスタンスを作る

JavaでSingletonを作るときの典型的な方法は次の通りです。

例えば、次のようにします。 以下ではDisplayクラスをSingletonとして実現しています。 Displayクラスのインスタンスは唯一で、 Display.getInstance()という式で得ることができます。

class Display {
    private static Display _instance = new Display();
    private Display() {
    }
    public static Display getInstance() {
        return _instance;
    }
    public void display(String msg) {
        System.out.println("Display: " + msg);
    }
}

class Main {
    public static void main(String[] args) {
        Display obj1 = Display.getInstance();
        Display obj2 = Display.getInstance();
        if (obj1 == obj2) {
            System.out.println("obj1 == obj2");
        }
        obj1.display("Hello");
    }
}

実行結果は次の通りです。

obj1 == obj2
Display: Hello

ただし、上のようなコーディングだと、Displayのサブクラスを作れないという制約が生じます。 コンストラクタをprivateにしているため、 サブクラスを作ると、コンパイル時のエラーになってしまうからです。 かといってコンストラクタをpublicやprotectedにすると、 プログラマが誤ってnewする危険性が生じてしまいます。

次の節では、この制約を解消します。

(2) クラスごとに管理 —— クラスごとに唯一のインスタンスを作る

前節の制約を解消する方法を考えてみましょう。

import java.util.HashMap;

class Display {
    private static HashMap _classnameToInstance = new HashMap();
    private static Object _lock = new Object();
    protected Display() {
        synchronized (_lock) {
            String classname = this.getClass().getName();
            if (_classnameToInstance.get(classname) != null) {
                throw new RuntimeException("Already created: " + classname);
            }
            _classnameToInstance.put(classname, this);
        }
    }
    public static Display getInstance(String classname) {
        synchronized (_lock) {
            Display obj = (Display)_classnameToInstance.get(classname);
            if (obj == null) {
                try {
                    Class cls = Class.forName(classname);
                    obj = (Display)cls.newInstance();
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(classname + " is not found");
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(classname + " cannot be accessed.");
                } catch (InstantiationException e) {
                    throw new RuntimeException(classname + " cannot be instantiated.");
                }
            }
            return obj;
        }
    }
    public void display(String msg) {
        System.out.println("Display: " + msg);
    }
}

class ScreenDisplay extends Display {
    public void display(String msg) {
        System.out.println("ScreenDisplay: " + msg);
    }
}

class HyperDisplay extends Display {
    public void display(String msg) {
        System.out.println("HyperDisplay: " + msg);
    }
}

class Main {
    public static void main(String[] args) {
        Display screen1 = Display.getInstance("ScreenDisplay");
        Display screen2 = Display.getInstance("ScreenDisplay");
        Display hyper1 = Display.getInstance("HyperDisplay");
        Display hyper2 = Display.getInstance("HyperDisplay");
        screen1.display("Hello");
        screen2.display("Hello");
        hyper1.display("Hello");
        hyper2.display("Hello");
        if (screen1 == screen2) {
            System.out.println("screen1 == screen2");
        }
        if (hyper1 == hyper2) {
            System.out.println("hyper1 == hyper2");
        }
    }
}

実行結果は次の通りです。

ScreenDisplay: Hello
ScreenDisplay: Hello
HyperDisplay: Hello
HyperDisplay: Hello
screen1 == screen2
hyper1 == hyper2

上のコーディングだと、 Displayクラス・ScreenDisplayクラス・HyperDisplayクラスのそれぞれにインスタンスが1個ずつ存在することになります (正確には0個の場合もありえます。例えば上記のMainクラスを実行したとき、Displayのインスタンスは0個です)。

次の節では、 「クラスごとにインスタンスを1個」確保するのではなく、 「クラス階層全体でインスタンスを1個」する方法を示します。

(3) 1個だけ管理 —— クラス階層全体で唯一のインスタンスを作る

import java.util.HashMap;

class Display {
    private static Display _instance = null;
    private static Object _lock = new Object();
    protected Display() {
        synchronized (_lock) {
            if (_instance != null) {
                throw new RuntimeException("Already created: " + _instance.getClass().getName());
            }
            _instance = this;
        }
    }
    public static Display getInstance(String classname) throws ClassNotFoundException {
        synchronized (_lock) {
            Class cls = Class.forName(classname);
            if (_instance == null) {
                try {
                    Display obj = (Display)cls.newInstance();
                    // assert obj == _instance;
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(classname + " cannot be accessed.");
                } catch (InstantiationException e) {
                    throw new RuntimeException(classname + " cannot be instantiated.");
                }
            } else if (!_instance.getClass().isAssignableFrom(cls)) {
                throw new ClassCastException(classname);
            }
            return _instance;
        }
    }
    public void display(String msg) {
        System.out.println("Display: " + msg);
    }
}

class ScreenDisplay extends Display {
    public void display(String msg) {
        System.out.println("ScreenDisplay: " + msg);
    }
}

class HyperDisplay extends Display {
    public void display(String msg) {
        System.out.println("HyperDisplay: " + msg);
    }
}

class Main {
    public static void main(String[] args) throws Exception {
        Display screen1 = Display.getInstance("ScreenDisplay");
        Display screen2 = Display.getInstance("ScreenDisplay");
        screen1.display("Hello");
        screen2.display("Hello");
        if (screen1 == screen2) {
            System.out.println("screen1 == screen2");
        }
        try {
            Display hyper = Display.getInstance("HyperDisplay");
            hyper.display("Hello");
        } catch (ClassCastException e) {
            System.out.println("ClassCastException (OK)");
        }
    }
}

実行結果は次の通りです。

ScreenDisplay: Hello
ScreenDisplay: Hello
screen1 == screen2
ClassCastException (OK)

他にもバリエーションがありますけれど、まずはここまで。

リンク

更新履歴

ぜひ、感想をお送りください

あなたのご意見・感想をお送りください。 あなたの一言が大きなはげみとなりますので、どんなことでもどうぞ。

あなたの名前: メール:
学年・職業など: 年齢: 男性女性
(上の情報は、いずれも未記入でかまいません)

お手数ですが、以下の問いに答えてから送信してください(迷惑書き込み防止のため)。
今年は西暦何年ですか?

何かの理由でうまく送れない場合にはメールhyuki dot mail at hyuki dot comあてにお願いします。

豊かな人生のための四つの法則