Java の enum のコンストラクタで例外を投げてはいけない

Effective Java に書かれている通り、 Java でシングルトンを作りたい場合は単一要素の enum を作成するという方法があります。そんでまぁ結構気軽に enum を使っていたんですが、コンストラクタで例外が発生しうるような実装にしてしまうと辛いということに気づきました。

例えばこんな enum を作って、

public enum SampleEnum {

    INSTANCE;

    private SampleEnum() {
        throw new RuntimeException("enumコンストラクタで例外");
    }

    public void doHoge() {
        System.out.println("doHoge");
    }
}

こんな感じで使ってみるとします。

    public static void main(String[] args) {
        try {
            System.out.println("main");
            SampleEnum.INSTANCE.doHoge();
        } catch (Exception ex) {
            System.err.println("nice catch!");
        }
    }

実行すると以下の様な出力になります。

main
Exception in thread "main" java.lang.ExceptionInInitializerError
	at sample.App.main(App.java:40)
Caused by: java.lang.RuntimeException: enumコンストラクタで例外
	at sample.SampleEnum.<init>(SampleEnum.java:14)
	at sample.SampleEnum.<clinit>(SampleEnum.java:11)
	... 1 more

catch 節に入ってないですね。これは enum のコンストラクタでスローした例外が ExceptionInInitializerError に変換*1されており、 Exceptioncatch では捕捉できないためです。 java.lang.Error をキャッチすれば捕捉可能ですが、エラーは一般的に JVM がヤバい感じの時にスローされるものなので、キャッチすべきではなく*2、通常エラーのキャッチ節を書くことも無いと思います。

これの何が辛いって、通常のアプリケーション例外処理に乗らないことです。例えば上記のように例外をキャッチしてログ出力している場合、エラーなのでキャッチできず意図したログが出ません。また、 enum のコンストラクタ内で try/catch しても、コンストラクタ内では static なロガーを参照できないのでやはりログを出力できません。

そもそも enum にかぎらずコンストラクタから例外投げるのはどうなのか、という話もありますが、こと enum に関しては特に扱いが厄介になります。 enum のコンストラクタから例外が発生しうるような実装は避けたほうが良いです。

enum のコンストラクタで例外が発生する実装ってどんなんやねん、という話ですが、 enumfinal フィールドを生やしてコンストラクタで外部ファイルから値を読み込んで設定する、みたいな作りとかですかね。アプリケーションスコープな値オブジェクトとして使えるかなと思ったんですが、読み込みに失敗した場合辛いということがわかりました。こういう場合はアプリケーションの初期化処理とかで明示的に読み込み処理を呼ぶような作りにしたほうが無難かなと思います。

*1:通常の class であればそのまま例外が飛んでくるので、 enum 特有の動作

*2:エラー発生後に JVM がまともに動作する保証はないため