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

Токены супертипа в Java Generics

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

1. Обзор

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

2. Стирание

Иногда нам нужно передать определенную информацию о типе методу . Например, здесь мы ожидаем, что Джексон преобразует массив байтов JSON в строку:

byte[] data = // fetch json from somewhere
String json = objectMapper.readValue(data, String.class);

Мы передаем это ожидание через токен буквального класса, в данном случае String.class.

Однако мы не можем так же легко установить такое же ожидание для универсальных типов:

Map<String, String> json = objectMapper.readValue(data, Map<String, String>.class); // won't compile

Java стирает информацию об универсальном типе во время компиляции. Следовательно, параметры универсального типа являются просто артефактом исходного кода и будут отсутствовать во время выполнения.

2.1. Овеществление

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

Овеществленные типы в Java следующие:

  • Простые примитивные типы, такие как long
  • Неуниверсальные абстракции, такие как String или Runnable
  • Необработанные типы, такие как List или HashMap
  • Универсальные типы, в которых все типы являются неограниченными подстановочными знаками, например List<?> или HashMap<?, ?>
  • Массивы других материализованных типов, таких как String[], int[], List[] или Map<?, ?>[]

Следовательно, мы не можем использовать что-то вроде Map<String, String>.class, потому что Map<String, String> не является материализованным типом.

3. Жетон супертипа

Как оказалось, мы можем воспользоваться преимуществами анонимных внутренних классов в Java, чтобы сохранить информацию о типе во время компиляции:

public abstract class TypeReference<T> {

private final Type type;

public TypeReference() {
Type superclass = getClass().getGenericSuperclass();
type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}

public Type getType() {
return type;
}
}

Этот класс является абстрактным, поэтому мы можем наследовать только от него подклассы.

Например, мы можем создать анонимный внутренний:

TypeReference<Map<String, Integer>> token = new TypeReference<Map<String, String>>() {};

Конструктор выполняет следующие шаги для сохранения информации о типе:

  • Во-первых, он получает общие метаданные суперкласса для этого конкретного экземпляра — в этом случае общий суперкласс — это TypeReference<Map<String, Integer>>.
  • Затем он получает и сохраняет фактический параметр типа для универсального суперкласса — в данном случае это будет Map<String, Integer>.

Этот подход к сохранению информации об универсальном типе обычно известен как токен супертипа :

TypeReference<Map<String, Integer>> token = new TypeReference<Map<String, Integer>>() {};
Type type = token.getType();

assertEquals("java.util.Map<java.lang.String, java.lang.Integer>", type.getTypeName());

Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
assertEquals("java.lang.String", typeArguments[0].getTypeName());
assertEquals("java.lang.Integer", typeArguments[1].getTypeName());

Используя токены супертипа, мы знаем, что тип контейнера — Map, а также параметры его типа — String и Integer.

Этот шаблон настолько известен, что такие библиотеки, как Jackson, и такие фреймворки, как Spring, имеют свои собственные реализации. Разобрать объект JSON в Map<String, String> можно, определив этот тип с токеном супертипа:

TypeReference<Map<String, String>> token = new TypeReference<Map<String, String>>() {};
Map<String, String> json = objectMapper.readValue(data, token);

4. Вывод

В этом руководстве мы узнали, как использовать токены супертипа для сохранения информации об универсальном типе во время выполнения.

Как обычно, все примеры доступны на GitHub .