В Java 5 появились generic-типы, а вместе с ним и концепция type erasure, которая буквально означает стирание информации о generic-типе после компиляции. Действительно, во многих случаях это просто синтаксический сахар, помогающий писать типо-безопасный код на уровне компиляции, и в runtime с такими типами работать нельзя. Например, невозможно получить тип T внутри ArrayList<T>, поэтому он в своей реализации создает массив Object[], а не T[] для хранения элементов.
Однако, в ряде случаев это очень даже возможно. Например, можно объявить поле
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
...
@Autowired
private Set<BeanPostProcessor> beanPostProcessors;
и spring в него заинжектит все объекты контекста, которые реализуют интерфейс BeanPostProcessor.
Можно написать и так:
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
...
List<String> strings = new ObjectMapper()
.readValue("[1, 2, 3]", new TypeReference<>() {});
// все элементы strings - строки (не Integer и не Long)
List<Integer> ints = new ObjectMapper()
.readValue("[1, 2, 3]", new TypeReference<>() {});
// все элементы ints - Integer (не String и не Long)
Можно написать даже так:
public abstract class AbstractComposite<T> {
@Autowired
private Set<T> autowiredBeans;
}
@Service
public class BeanPostProcessorComposite extends AbstractComposite<BeanPostProcessor> {
}
И в runtime созданный бин BeanPostProcessorComposite будет содержать все BeanPostProcessor в поле autowiredBeans.
Дело в том, что после компиляции информация о generic-типах частично остается на уровне байт-кода и ее возможно получить в runtime.
Почему компилятор стирает информацию о типе
Это связано с сильными контрактами совместимости между Java-версиями: модули скомпилированные до Java 5, продолжат работать — вызывать классы с generic-типами. Это работает и назад. В случае более строгой реализации generic-типов без erasure совместимость была бы нарушена.
После компиляции такой типо-безопасный код
превратится в такой код
Эта тема хорошо раскрыта в этой и этой статьях.
После компиляции такой типо-безопасный код
List<String> list = new ArrayList<>();
list.add("value");
String value = list.get(0);
превратится в такой код
List list = new ArrayList();
list.add("value");
String value = (String) list.get(0);
Эта тема хорошо раскрыта в этой и этой статьях.
JDK дает нам достаточно богатый функционал для работы с типами. Самый очевидный — Class, но он нам не сильно поможет:
import java.lang.reflect.Field;
public class ListType {
private List<Integer> list;
public static void main(String[] args) throws NoSuchFieldException {
Field field = ListType.class.getDeclaredField("list");
Class fieldType = field.getType();
// "interface java.util.List"
System.out.println(fieldType);
}
}
Метод getType() лишь дает информацию, что тип поля — интерфейс List, но не его generic-тип. Кроме getType() у Field есть метод Type getGenericType() и тут уже сильно больше подробностей:
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
...
Type fieldGenericType = field.getGenericType();
// "class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl"
System.out.println(fieldGenericType.getClass());
// "java.util.List<java.lang.Integer>"
System.out.println(fieldGenericType);
if (fieldGenericType instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) fieldGenericType).getActualTypeArguments();
// "[class java.lang.Integer]"
System.out.println(Arrays.toString(typeArguments));
}
}
}
Таким образом, мы получили и тип коллекции List и ее generic-тип Integer. И это средствами JDK без посторонних библиотек и магии.
Пример можно усложнить, поддерживаются и вложенные структуры:
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
...
private List<Set<Integer>> nestedList;
...
Field field = ListType.class.getDeclaredField("nestedList");
Type fieldGenericType = field.getGenericType();
// "java.util.List<java.util.Set<java.lang.Integer>>"
System.out.println(fieldGenericType);
if (fieldGenericType instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) fieldGenericType).getActualTypeArguments();
// "[java.util.Set<java.lang.Integer>]"
System.out.println(Arrays.toString(typeArguments));
}
}
Трюк с TypeReference
В начале статьи был пример парсинга через Jackson:
List<Integer> ints = new ObjectMapper()
.readValue("[1, 2, 3]", new TypeReference<>() {});
Работает это так: вызов конструктора полностью выглядит длиннее:
List<Integer> ints = new ObjectMapper()
.readValue("[1, 2, 3]", new TypeReference<List<Integer>>() {});
и в момент компиляции создается анонимный класс-наследник:
class ListType$1 extends TypeReference<List<Integer>> {
}
который и используется в вызове:
List<Integer> ints = new ObjectMapper()
.readValue("[1, 2, 3]", new ListType$1());
В итоге, jackson получает информацию о типе через класс. Стоит отметить, что такая короткая запись работает только начиная с JDK 9.
Kotlin
Язык Kotlin работает поверх JVM, поэтому подвержен всем ограничениям платформы. Тем не менее благодаря хитростям, kotlin привносит несколько новых возможностей работы с типами. Например, можно писать так:
import com.fasterxml.jackson.databind.ObjectMapper
fun main(args: Array<String>) {
val strings: List<String> = parseJson("[1, 2, 3]")
println(strings)
// strings-список строк (но не чисел)
}
inline fun <reified T> parseJson(str: String): T {
return ObjectMapper().readValue(str, T::class.java);
}
Здесь используется связка inline+reified — фактически тело метода parseJson вставляется в место вызова, поэтому jackson в качестве аргумента неявно получает дескриптор типа, а в коде мы можем писать T::class.java, что невозможно в обычной java.
Параметризованные типы
Особый случай — вычисление типа по параметру, когда значение зависит от конкретного подтипа. Например
public abstract class AbstractComposite<T> {
@Autowired // в runtime тип зависит от наследника
private Set<T> autowiredBeans;
}
public static class BeanPostProcessorComposite extends AbstractComposite<BeanPostProcessor> {
}
Для вычисления типа можем использовать утилитарные методы. Например, из guava:
import com.google.common.reflect.TypeToken;
...
TypeToken<?> modelType = TypeToken.of(BeanPostProcessorComposite.class);
Type actualType = modelType.resolveType(
AbstractComposite.class.getDeclaredField("autowiredBeans").getGenericType()
).getType();
// "java.util.Set<org.springframework.beans.factory.config.BeanPostProcessor>"
System.out.println(actualType);
Или spring:
import org.springframework.core.GenericTypeResolver;
...
Type actualType = GenericTypeResolver.resolveType(
AbstractComposite.class.getDeclaredField("autowiredBeans").getGenericType(),
BeanPostProcessorComposite.class
);
// "java.util.Set<org.springframework.beans.factory.config.BeanPostProcessor>"
System.out.println(actualType);
Мы рассмотрели случаи с полями класса, но все то же самое применимо к generic-аргументам и типам возвращаемого значения методов.
Комментарии (2)
Lewigh
19.07.2022 06:36Это связано с сильными контрактами совместимости между Java-версиями: модули скомпилированные до Java 5, продолжат работать — вызывать классы с generic-типами. Это работает и назад. В случае более строгой реализации generic-типов без erasure совместимость была бы нарушена.
На самом деле не совсем так. Когда для Java проектировалось решение для добавления дженериков было создано сразу оба экспериментальных варианта - как с боксингом так и со специализацией. Но по результатам тестов, на JVM того времени, вариант со специализацией выглядел неубедительно и привносит дополнительные проблемы. Потому предпочтение было отдано варианту реализации через боксинг.
Anarchist
implicit ev: ClassTag[T] :)