NoClassDefFoundErrorの罠

はじめに

久々に、NoClassDefFoundErrorに遭遇しました。
APIによると、


>

通常のメソッド呼び出し、あるいは new 式を使った新しいインスタンスの生成で、Java 仮想マシンまたは ClassLoader インスタンスがクラス定義をロードしようとしたが、クラス定義が見からない場合にスローされます。


検索されるクラス定義は、現在実行中のクラスをコンパイルする時点では存在していましたが、その後見つからなくなっています。

となっているので、クラスパスに漏れがあるのかと思っったんですが、
今更そんなミスするかな〜?と思いながら調査すると、
実はクラスパスの設定ミスではなく、クラスの初期化に失敗すると、
このエラーが発生するとわかりました。

言語仕様にあたる

ここで,そのオブジェクトClassは既に検証及び準備済みであること,並びにそのオブジェクトClassは,次の四つの状態の一つを示す状態を含むことを仮定する。
<中略>

  • このオブジェクトClassは,エラー状態にある。恐らく,検証又は準備段階で失敗しているか,又は初期化を実行して失敗したためとする。

<中略>
5. そのオブジェクトClassがエラー状態にある場合,初期化は不可能とする。そのオブジェクトClassのロックを解除し,NoClassDefFoundErrorを投げる。
<中略>

10. (初期化子の実行が正常に完了しなかった場合),初期化子は,ある例外Eを投げることによって中途完了しなければならない。EのクラスがError又はそのサブクラスの一つでない場合,クラスExceptionInInitializerErrorの新しいインスタンスを,実引数としてEを伴って生成し,それから次の段階のEの所でこのオブジェクトを使用する。

つまり。。。?

最初の一回目はExceptionInInitializerErrorがでるぞ、と。
その初期化に失敗したクラスをまた呼び出そうとするとNoClassDefFoundErrorだぞ、と。


実際このエラーが起こったのは、マルチスレッドのプログラムで、
同じクラスの初期化にことごとく失敗しているという事象なので
つじつまは合っているようです。

検証

とはいえ、やっぱり検証は必要ですね。

検証コード
public class MyTest {

    public static void main(String[] args) throws Exception {
        try {
            MyClass.class.getClass();  // ExceptionInInitializerError発生
            
            throw new Exception();     // ここにはこない
        } catch (ExceptionInInitializerError e) {
            // ここにくる
        }
        
        // 静的初期化子の実行に失敗してエラー状態に遷移したMyClassに
        // 再度アクセス
        int i = MyClass.i;   // MyClassはエラー状態なので、NoClassDefFoundError発生
        System.out.println(i);     // ここにはこない  
    }
}

class MyClass {

    static int i = 0;
    
    static {
        i = 1 / 0;    // 0で割り算(必ずエラー)
    }
}
実行結果
Exception in thread "main" java.lang.NoClassDefFoundError
	at MyTest.main(MyTest.java:13)    

MyClass.iの箇所でNoClassDefFoundErrorが発生します。
予想通りの動きです。

まとめ

  • あるクラスの静的初期化子実行に失敗すると、そのクラスは「エラー状態」に遷移する。
  • その後、再度そのクラスにアクセスするとExceptionInInitializerErrorではなく、NoClassDefFoundErrorが発生する。