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

Скрытые классы в Java 15

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

1. Обзор

Java 15 представила множество функций . В этой статье мы обсудим одну из новых функций под названием «Скрытые классы» в JEP-371 . Эта функция представлена как альтернатива Unsafe API , который не рекомендуется использовать за пределами JDK.

Функция скрытых классов полезна для всех, кто работает с динамическим байт-кодом или языками JVM.

2. Что такое скрытый класс?

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

Начиная с Java 15 скрытые классы стали стандартным способом создания динамических классов.

Скрытые классы — это классы, которые не могут использоваться непосредственно байт-кодом или другими классами. Несмотря на то, что он упоминается как класс, его следует понимать как скрытый класс или интерфейс. Он также может быть определен как элемент гнезда управления доступом и может быть выгружен независимо от других классов.

3. Свойства скрытых классов

Давайте посмотрим на свойства этих динамически генерируемых классов:

  • Необнаруживаемый — скрытый класс не может быть обнаружен ни JVM во время компоновки байт-кода, ни программами, явно использующими загрузчики классов. Рефлексивные методы Class::forName , ClassLoader::findLoadedClass и Lookup::findClass их не найдут.
  • Мы не можем использовать скрытый класс в качестве суперкласса, типа поля, типа возвращаемого значения или типа параметра.
  • Код в скрытом классе может использовать его напрямую, не полагаясь на объект класса.
  • Поля final , объявленные в скрытых классах, не могут быть изменены независимо от их доступных флагов.
  • Он расширяет гнездо управления доступом за счет необнаруживаемых классов.
  • Он может быть выгружен, даже если его условный определяющий загрузчик классов все еще доступен.
  • Трассировки стека по умолчанию не отображают методы или имена скрытых классов, однако их можно отобразить, настроив параметры JVM.

4. Создание скрытых классов

Скрытый класс не создается никаким загрузчиком классов. Он имеет тот же определяющий загрузчик класса, пакет среды выполнения и домен защиты, что и класс поиска.

Во-первых, давайте создадим объект Lookup :

MethodHandles.Lookup lookup = MethodHandles.lookup();

Метод Lookup::defineHiddenClass создает скрытый класс. Этот метод принимает массив байтов.

Для простоты мы определим простой класс с именем HiddenClass , у которого есть метод для преобразования заданной строки в верхний регистр:

public class HiddenClass {
public String convertToUpperCase(String s) {
return s.toUpperCase();
}
}

Давайте получим путь к классу и загрузим его во входной поток. После этого мы конвертируем этот класс в байты, используя IOUtils.toByteArray() :

Class<?> clazz = HiddenClass.class;
String className = clazz.getName();
String classAsPath = className.replace('.', '/') + ".class";
InputStream stream = clazz.getClassLoader()
.getResourceAsStream(classAsPath);
byte[] bytes = IOUtils.toByteArray();

Наконец, мы передаем эти сконструированные байты в Lookup::defineHiddenClass :

Class<?> hiddenClass = lookup.defineHiddenClass(IOUtils.toByteArray(stream),
true, ClassOption.NESTMATE).lookupClass();

Второй логический аргумент true инициализирует класс. Третий аргумент ClassOption.NESTMATE указывает, что созданный скрытый класс будет добавлен в качестве соседа к классу поиска, чтобы он имел доступ к закрытым членам всех классов и интерфейсов в том же гнезде.

Предположим, мы хотим жестко связать скрытый класс с его загрузчиком классов ClassOption.STRONG . Это означает, что скрытый класс может быть выгружен только в том случае, если определяющий его загрузчик недоступен.

5. Использование скрытых классов

Скрытые классы используются фреймворками, которые генерируют классы во время выполнения и используют их косвенно через отражение.

В предыдущем разделе мы рассмотрели создание скрытого класса. В этом разделе мы увидим, как его использовать и создать экземпляр.

Поскольку приведение классов, полученных из Lookup.defineHiddenClass , невозможно ни с каким другим объектом класса, мы используем Object для хранения экземпляра скрытого класса. Если мы хотим привести скрытый класс, мы можем определить интерфейс и создать скрытый класс, реализующий интерфейс:

Object hiddenClassObject = hiddenClass.getConstructor().newInstance();

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

Method method = hiddenClassObject.getClass()
.getDeclaredMethod("convertToUpperCase", String.class);
Assertions.assertEquals("HELLO", method.invoke(hiddenClassObject, "Hello"));

Теперь мы можем проверить несколько свойств скрытого класса, вызвав некоторые из его методов:

Метод isHidden() вернет true для этого класса:

Assertions.assertEquals(true, hiddenClass.isHidden());

Кроме того, поскольку для скрытого класса нет фактического имени, его каноническое имя будет нулевым :

Assertions.assertEquals(null, hiddenClass.getCanonicalName());

Скрытый класс будет иметь тот же определяющий загрузчик, что и класс, выполняющий поиск. Поскольку поиск происходит в том же классе, следующее утверждение будет успешным:

Assertions.assertEquals(this.getClass()
.getClassLoader(), hiddenClass.getClassLoader());

Если мы попытаемся получить доступ к скрытому классу с помощью любых методов, они вызовут исключение ClassNotFoundException . Это очевидно, так как имя скрытого класса достаточно необычное и неквалифицированное, чтобы быть видимым для других классов. Давайте проверим пару утверждений, чтобы доказать, что скрытый класс невозможно обнаружить:

Assertions.assertThrows(ClassNotFoundException.class, () -> Class.forName(hiddenClass.getName()));
Assertions.assertThrows(ClassNotFoundException.class, () -> lookup.findClass(hiddenClass.getName()));

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

6. Анонимный класс против скрытого класса

В предыдущих разделах мы создали скрытый класс и поэкспериментировали с некоторыми его свойствами. Теперь давайте остановимся на различиях между анонимными классами — внутренними классами без явных имен — и скрытыми классами:

  • Анонимный класс имеет динамически сгенерированное имя с символом $ между ними, в то время как скрытый класс, производный от com.foreach.reflection.hiddenclass.HiddenClass , будет com.foreach.reflection.hiddenclass.HiddenClass/1234.
  • Анонимный класс создается с использованием Unsafe::defineAnonymousClass , который устарел, тогда как Lookup::defineHiddenClass создает экземпляр скрытого класса .
  • Скрытые классы не поддерживают исправление константного пула. Это помогает определить анонимные классы с их постоянными записями пула, уже преобразованными в конкретные значения.
  • В отличие от скрытого класса, анонимный класс может получить доступ к защищенным членам основного класса, даже если он находится в другом пакете, а не в подклассе.
  • Анонимный класс может заключать в себе другие классы для доступа к своим членам, но скрытый класс не может заключать в себе другие классы.

Хотя скрытый класс не является заменой анонимного класса , он заменяет некоторые способы использования анонимных классов в JDK. Начиная с Java 15, лямбда-выражения используют скрытые классы .

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

В этой статье мы подробно обсудили новую языковую функцию под названием «Скрытые классы». Как всегда, код доступен на GitHub .