«1. Обзор

Проверка существования класса может быть полезна при определении используемой реализации интерфейса. Этот метод обычно используется в старых установках JDBC.

В этом руководстве мы рассмотрим нюансы использования Class.forName() для проверки существования класса в пути к классам Java.

2. Использование Class.forName()

Мы можем проверить существование класса, используя Java Reflection, в частности, Class.forName(). Документация показывает, что если класс не может быть найден, будет выброшено исключение ClassNotFoundException.

2.1. Когда следует ожидать ClassNotFoundException

Во-первых, давайте напишем тест, который обязательно вызовет ClassNotFoundException, чтобы мы могли знать, что наши положительные тесты безопасны:

@Test(expected = ClassNotFoundException.class)
public void givenNonExistingClass_whenUsingForName_thenClassNotFound() throws ClassNotFoundException {
    Class.forName("class.that.does.not.exist");
}

Итак, мы доказали, что класс, который не exists вызовет исключение ClassNotFoundException. Давайте напишем тест для класса, который действительно существует:

@Test
public void givenExistingClass_whenUsingForName_thenNoException() throws ClassNotFoundException {
    Class.forName("java.lang.String");
}

Эти тесты доказывают, что выполнение Class.forName() без перехвата ClassNotFoundException эквивалентно указанному классу, существующему в пути к классам. Однако это не совсем идеальное решение из-за побочных эффектов.

2.2. Побочный эффект: инициализация класса

Важно отметить, что без указания загрузчика класса Class.forName() должен запускать статический инициализатор в запрошенном классе. Это может привести к неожиданному поведению.

Чтобы проиллюстрировать это поведение, давайте создадим класс, который генерирует исключение RuntimeException при выполнении его блока статического инициализатора, чтобы мы могли сразу узнать, когда он выполняется:

public static class InitializingClass {
    static {
        if (true) { //enable throwing of an exception in a static initialization block
            throw new RuntimeException();
        }
    }
}

Из документации forName() мы можем видеть, что он выдает ExceptionInInitializerError, если инициализация, спровоцированная этим методом, не удалась.

Давайте напишем тест, который будет ожидать ExceptionInInitializerError при попытке найти наш InitializingClass без указания загрузчика класса:

@Test(expected = ExceptionInInitializerError.class)
public void givenInitializingClass_whenUsingForName_thenInitializationError() throws ClassNotFoundException {
    Class.forName("path.to.InitializingClass");
}

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

3. Указание Class.forName() пропустить инициализацию

К счастью для нас, существует перегруженный метод forName(), который принимает загрузчик класса и указывает, следует ли выполнять инициализацию класса.

Согласно документации, следующие вызовы эквивалентны:

Class.forName("Foo")
Class.forName("Foo", true, this.getClass().getClassLoader())

Изменив true на false, мы теперь можем написать тест, который проверяет существование нашего класса InitializingClass, не запуская его статический блок инициализации:

@Test
public void givenInitializingClass_whenUsingForNameWithoutInitialization_thenNoException() throws ClassNotFoundException {
    Class.forName("path.to.InitializingClass", false, getClass().getClassLoader());
}

4. Модули Java 9

Для проектов Java 9+ существует третья перегрузка Class.forName(), которая принимает имя класса Module и String. Эта перегрузка не запускает инициализатор класса по умолчанию. Кроме того, в частности, он возвращает null, когда запрошенный класс не существует, а не генерирует исключение ClassNotFoundException.

5. Заключение

В этом кратком руководстве мы рассмотрели побочный эффект инициализации класса при использовании Class.forName() и обнаружили, что вы можете использовать перегрузки forName(), чтобы предотвратить это.

Исходный код со всеми примерами из этого руководства можно найти на GitHub.