Получение generic-типа в runtime

от автора


src

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


ссылка на оригинал статьи https://habr.com/ru/post/588252/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *