1. 概述
在这个快速教程中,我们将了解导致Java抛出ExceptionInInitializerError异常实例的原因。
我们将从一些理论开始。然后我们将在实践中看到这个异常的几个例子。
2. ExceptionInInitializerError
ExceptionInInitializerError表示静态初始化器中发生了意外异常。基本上,当我们看到这个异常时,我们应该知道Java未能评估静态初始化块或实例化静态变量。
事实上,每当静态初始化器中发生任何异常时,Java都会自动将该异常包装在ExceptionInInitializerError类的实例中。这样,它还维护对实际异常的引用作为根本原因。
现在我们知道了这个异常背后的基本原理,让我们在实践中看看它。
3. 静态初始化块
要有一个失败的静态块初始化器,我们要有意地将一个整数除以零:
public class StaticBlock {
private static int state;
static {
state = 42 / 0;
}
}
现在,如果我们触发类初始化:
new StaticBlock();
然后,我们会看到以下异常:
java.lang.ExceptionInInitializerError
at cn.tuyucheng.taketoday...(ExceptionInInitializerErrorUnitTest.java:18)
Caused by: java.lang.ArithmeticException: / by zero
at cn.tuyucheng.taketoday.StaticBlock.<clinit>(ExceptionInInitializerErrorUnitTest.java:35)
... 23 more
如前所述,Java抛出ExceptionInInitializerError异常的同时维护对根本原因的引用:
assertThatThrownBy(StaticBlock::new)
.isInstanceOf(ExceptionInInitializerError.class)
.hasCauseInstanceOf(ArithmeticException.class);
另外值得一提的是,<clinit>方法是JVM中的类初始化方法。
4. 静态变量初始化
如果Java未能初始化静态变量,也会发生同样的事情:
public class StaticVar {
private static int state = initializeState();
private static int initializeState() {
throw new RuntimeException();
}
}
同样,如果我们触发类初始化过程:
new StaticVar();
然后出现同样的异常:
java.lang.ExceptionInInitializerError
at cn.tuyucheng.taketoday...(ExceptionInInitializerErrorUnitTest.java:11)
Caused by: java.lang.RuntimeException
at cn.tuyucheng.taketoday.StaticVar.initializeState(ExceptionInInitializerErrorUnitTest.java:26)
at cn.tuyucheng.taketoday.StaticVar.<clinit>(ExceptionInInitializerErrorUnitTest.java:23)
... 23 more
与静态初始化块类似,也保留了异常的根本原因:
assertThatThrownBy(StaticVar::new)
.isInstanceOf(ExceptionInInitializerError.class)
.hasCauseInstanceOf(RuntimeException.class);
5. 受检异常
作为Java语言规范(JLS-11.2.3)的一部分,我们不能在静态初始化块或静态变量初始化程序中抛出受检异常。例如,如果我们尝试这样做:
public class NoChecked {
static {
throw new Exception();
}
}
编译器将因以下编译错误而失败:
java: initializer must be able to complete normally
作为约定,当我们的静态初始化逻辑抛出受检异常时,我们应该将可能的受检异常包装在ExceptionInInitializerError实例中:
public class CheckedConvention {
private static Constructor<?> constructor;
static {
try {
constructor = CheckedConvention.class.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
}
}
}
如上所示,getDeclaredConstructor()方法抛出一个受检异常。因此,我们捕获了受检的异常并按照惯例对其进行了包装。
由于我们已经明确地返回了一个ExceptionInInitializerError异常的实例,因此Java不会将这个异常包装在另一个ExceptionInInitializerError实例中。
但是,如果我们抛出任何其他非受检的异常,Java将抛出另一个ExceptionInInitializerError:
static {
try {
constructor = CheckedConvention.class.getConstructor();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
在这里,我们将受检的异常包装在非受检的异常中。由于这个非受检的异常不是ExceptionInInitializerError的实例,因此Java将再次包装它,从而导致以下意外的堆栈跟踪:
java.lang.ExceptionInInitializerError
at cn.tuyucheng.taketoday.exceptionininitializererror...
Caused by: java.lang.RuntimeException: java.lang.NoSuchMethodException: ...
Caused by: java.lang.NoSuchMethodException: cn.tuyucheng.taketoday.CheckedConvention.<init>()
at java.base/java.lang.Class.getConstructor0(Class.java:3427)
at java.base/java.lang.Class.getConstructor(Class.java:2165)
如上所示,如果我们遵循约定,那么堆栈跟踪将比这更清晰。
5.1 OpenJDK
最近,这个约定甚至用在OpenJDK源代码本身中。例如,下面是AtomicReference如何使用这种方法:
public class AtomicReference<V> implements java.io.Serializable {
private static final VarHandle VALUE;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private volatile V value;
// omitted
}
6. 总结
在本教程中,我们了解了导致Java抛出ExceptionInInitializerError异常实例的原因。
与往常一样,本教程的完整源代码可在GitHub上获得。