
В 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-аргументам и типам возвращаемого значения методов.