Перейти к основному содержимому

Когда Java выдает ошибку ExceptionInInitializerError?

· 4 мин. чтения

1. Обзор

В этом кратком руководстве мы увидим, что заставляет Java создавать экземпляр исключения ExceptionInInitializerError .

Начнем с небольшого количества теории. Затем мы увидим несколько примеров этого исключения на практике.

2. Ошибка ExceptionInInitializer

ExceptionInInitializerError указывает , что в статическом инициализаторе возникло непредвиденное исключение . По сути, когда мы видим это исключение, мы должны знать, что Java не удалось оценить блок статического инициализатора или создать экземпляр статической переменной.

Фактически каждый раз, когда внутри статического инициализатора возникает какое-либо исключение, Java автоматически заключает это исключение в экземпляр класса ExceptionInInitializerError . Таким образом, он также поддерживает ссылку на фактическое исключение как на основную причину.

Теперь, когда мы знаем причину этого исключения, давайте посмотрим на это на практике.

3. Статический блок инициализатора

Чтобы инициализатор статического блока вышел из строя, мы намеренно разделим целое число на ноль:

public class StaticBlock {

private static int state;

static {
state = 42 / 0;
}
}

Теперь, если мы вызовем инициализацию класса чем-то вроде:

new StaticBlock();

Тогда мы увидим следующее исключение:

java.lang.ExceptionInInitializerError
at com.foreach...(ExceptionInInitializerErrorUnitTest.java:18)
Caused by: java.lang.ArithmeticException: / by zero
at com.foreach.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 com.foreach...(ExceptionInInitializerErrorUnitTest.java:11)
Caused by: java.lang.RuntimeException
at com.foreach.StaticVar.initializeState(ExceptionInInitializerErrorUnitTest.java:26)
at com.foreach.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 com.foreach.exceptionininitializererror...
Caused by: java.lang.RuntimeException: java.lang.NoSuchMethodException: ...
Caused by: java.lang.NoSuchMethodException: com.foreach.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 .