SingletonとExceptionについて考えてたら、なんだか良く分からなくなってしまいました。
Singletonパターンってあるじゃないですか。
↓みたいな感じのヤツ。
public class Singleton { private static final Singleton singleton = new Singleton(); public static Singleton getInstance() { return singleton; } private Singleton() { } }
このSingletonパターンで、インスタンス生成時に例外が発生するケースについて考えてみたんですが、どうもよく分からなくなってきてしまいました。
とりあえず纏めて置きますので、分かる方がいたら突っ込みをお願いしたいと思います。
背景
例えば上の例で、コンストラクタを↓みたいに書き換えて、
private Singleton() { throw new RuntimeException("Singleton is dead!"); }
わざと例外を発生させると、なんだか良く分からないエラーが出ます。
Exception in thread "main" java.lang.ExceptionInInitializerError at singletontest.Util.main(Util.java:13) Caused by: java.lang.RuntimeException: Singleton is dead! at singletontest.Singleton.<init>(Singleton.java:12) at singletontest.Singleton.<clinit>(Singleton.java:5) ... 1 more
このエラーを避ける為に、以前の私は好んで↓のパターンを使用しておりました。
public class Singleton { private static Singleton singleton; public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { try { singleton = new Singleton(); } catch (RuntimeException e) { // 何かエラー処理 throw new ProjectOriginalException("catched by Singleton!", e); } } } } return singleton; } private Singleton() { throw new RuntimeException("Singleton constructor failure!"); } }
ところが、、、、知らずに使っちゃってましたが、実はこの書き方はdouble-checked lockingと呼ばれており、「まだコンストラクタで初期化されていないインスタンスが、getInstance()メソッドから返却される可能性がある」という、かなりシャレにならない誤動作をする可能性があることが知られています。
参考:http://www.ibm.com/developerworks/jp/java/library/j-dcl/
で、結局解決策は「同期化を受け入れるか、あるいはstatic field を使用すること」だそうです。
例外が発生する可能性があるSingletonでは「同期化を受け入れる」を選ぶのが確実そうですが・・・やっぱり無駄に同期化のコストを払い続けるのって気分的に嫌なので、「static fieldを使用する」(つまり最初に例示した、static fieldで直接Singletonクラスをnewして代入するやり方)で実現できないか、考えてみることにしました。
ExceptionInInitializerErrorとは
ExceptionInInitializerErrorですが、名前の通りInitializer内でExceptionが発生してしまった場合にthrowされるErrorです。
Errorではありますが、普通にcatchできるし、発生した例外はgetCause()メソッドで取れるし、catchしてからの処理は難しくありません。
try { return Singleton.getInstance(); } catch (ExceptionInInitializerError e) { Throwable original = e.getCause(); // 何かエラー処理 throw new ProjectOriginalException("catched by Singleton!", original ); }
問題はここからです。
↑はSingleton.getInstance()を呼び出してExceptionInInitializerErrorをcatchしてますが、これをgetInstance()メソッド内部で実行するのは不可能です。
なぜなら例外が発生しているのは↓のstaticフィールドへの初期化処理中、つまりクラス・ローディングのタイミングなので、getInstance()メソッドを呼び出す前に発生してしまいます。
private static final Singleton singleton = new Singleton();
なので、クラスローディングが走る可能性がある箇所全てでExceptionInInitializerErrorをキャッチする必要があることになります。
そんなのやってられないですよね。
F/Wとか使ってて、Exceptionを処理する箇所が決まっているなら単にExceptionInInitializerErrorに対する処理を追記するだけでも良いような気もするのですが、もっと環境に依存せず、Singletonのクラスのみで完結する良い手はないかと考えてみた訳です。
自分の考えた実装
結局、下記のようになりました。
public class Singleton { private static class Inner { private static final Singleton singleton = new Singleton();; } public static Singleton getInstance() { try { return Inner.singleton; } catch (ExceptionInInitializerError e) { Throwable original = e.getCause(); // 何かエラー処理 throw new ProjectOriginalException("catched by Singleton!", original); } } private Singleton() { throw new RuntimeException("Singleton constructor failure!"); } }
staticフィールドへの初期化処理をgetInstance()でハンドリングするために、インナークラスにフィールドを持たせています。
「JVM起動後に、そのクラスを最初に使用する箇所でクラスローディングが発生する」という前提さえ守られていれば、インナークラスが最初に使用されるのは必ずgetInstance内の「return Inner.singleton;」の部分なので、ちゃんとExceptionInInitializerErrorをハンドリングできる、というわけです。
動作テストしてみたところ、少なくとも自分の環境ではちゃんと動きました。
でもこのコード、クラスローディングがいつ発生するのかに強く依存しています。
クラスローディングが起こるタイミングって仕様上はある程度の幅があると聞いたことがあるのですが、そうするとクラスローディングが違うタイミングで発生してしまって、全く関係ないところでExceptionInInitializerErrorが発生する可能性もあるのでしょうか?
分かる方がいたら、突っ込んでいただけると助かります。
ついで。
コンストラクタがthrowするのがチェック済み例外(要するにRuntimeException以外のException)の場合はコンパイルエラーになるので、「Static Initializerでcatch」等の対策が必要です。
public class Singleton { private static class Inner { private static final Singleton singleton; static { try { singleton = new Singleton(); } catch (IOException e) { throw new ExceptionInInitializerError(e); } } } public static Singleton getInstance() { try { return Inner.singleton; } catch (ExceptionInInitializerError e) { Throwable original = e.getCause(); // 何かエラー処理 throw new ProjectOriginalException("catched by Singleton!", original); } } private Singleton() throws IOException { throw new IOException("Singleton constructor failure!"); } }