После их появления в JDK5, дженерики Java быстро стали неотъемлемым элементом многих программ на Java. Однако, как на первый взгляд кажутся простыми дженерики Java, программист может быстро потеряться с этой функцией.
Большинство программистов Java знают о стирании типов компилятора Java. Вообще говоря, стирание типов означает, что вся информация общего типа о классе Java теряется во время компиляции его исходного кода. Это дань обратной совместимости Java: все общие варианты класса Java имеют одно представление в работающем приложении Java. Если экземпляр ArrayList <String> должен будет помнить, что его универсальный тип имеет тип String, он должен будет хранить эту информацию где-то в своем функциональном описании, чтобы указать, что, например, List.get действительно возвращает тип String. (Под функциональным описанием я имею в виду свойства, которые являются общими для всех экземпляров класса. Это включает, например, определения методов или полей. В отличие от его функционального описания, состояние экземпляра, индивидуальное для каждого экземпляра, сохраняется в его объектном представлении. Функциональное описание экземпляра ArrayList <String>, таким образом, представлено его классом ArrayList.class. Поскольку экземпляр ArrayList.class используется совместно с другими экземплярами, которые также могут иметь тип ArrayList <Integer>, для этого уже потребуется две разные версии ArrayList.class. Однако такие модификации представления класса были бы непонятны для старых JRE и, таким образом, нарушали бы обратную совместимость приложений Java. Как следствие, следующее сравнение всегда будет успешным:
1
|
assert new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass(); |
Поскольку такое сравнение проводится во время выполнения, когда универсальный тип класса уже был удален, это сравнение преобразуется в ArrayList.class == ArrayList.class, что является тривиальным. Точнее говоря, работающее приложение определит, что ArrayList.class равен самому себе, и вернет true, несмотря на String.class! = Integer.class. В этом основное отличие Java от других языков программирования, таких как, например, C ++, а также причина частых жалоб на Java. (С академической точки зрения C ++ на самом деле не знает универсальных типов. Вместо этого C ++ предлагает шаблоны, которые, тем не менее, похожи на универсальные.)
Пока что в этом нет ничего нового для многих разработчиков. Однако, вопреки распространенному мнению, иногда можно получить информацию общего типа даже во время выполнения. Прежде чем объяснить, когда это возможно, давайте рассмотрим пример. Для этого мы определим следующие два класса:
1
2
|
class MyGenericClass<T> { } class MyStringSubClass extends MyGenericClass<String> { } |
MyGenericClass имеет один аргумент для универсального типа T. MyStringSubClass расширяет этот универсальный класс и присваивает T = String в качестве параметра типа. В результате компилятор Java может хранить информацию о типе общего аргумента String суперкласса MyGenericClass в байтовом коде своего подкласса MyStringSubClass. Эта модификация может быть достигнута без нарушения обратной совместимости, потому что эта информация просто хранится в области байтового кода скомпилированного класса, которая игнорируется старыми версиями JRE. В то же время все экземпляры MyStringSubClass могут по-прежнему совместно использовать одно представление класса, поскольку T = String устанавливается для всех экземпляров MyStringSubClass.
Но как мы можем получить эту информацию, хранящуюся в байт-коде? Java API предоставляет метод Class.getGenericSuperclass, который можно использовать для получения экземпляра типа Type . Если прямой суперкласс на самом деле является универсальным, возвращаемый экземпляр дополнительно имеет тип ParameterizedType и может быть приведен к нему. (Тип является ничем иным, как интерфейсом маркера . Фактический экземпляр будет экземпляром внутреннего класса ParameterizedTypeImpl, однако следует всегда приводить его к интерфейсу.) Благодаря приведению к интерфейсу ParameterizedType теперь можно вызывать метод ParameterizedType.getActualTypeArguments чтобы получить массив, который снова имеет тип Type. Любой аргумент универсального типа универсального суперкласса будет содержаться в этом массиве с тем же индексом, что и в определении типа. Любой экземпляр Type, который представляет неуниверсальный класс, является просто реализацией класса Java Class. (Предполагается, что вы не обрабатываете массив, в котором возвращаемый тип имеет тип GenericArrayType. Я пропущу этот сценарий в этой статье для простоты.)
Теперь мы можем использовать эти знания для написания служебной функции:
01
02
03
04
05
06
07
08
09
10
|
public static Class<?> findSuperClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) { Class<?> subClass = instance.getClass(); while (subClass != subClass.getSuperclass()) { // instance.getClass() is no subclass of classOfInterest or instance is a direct instance of classOfInterest subClass = subClass.getSuperclass(); if (subClass == null ) throw new IllegalArgumentException(); } ParameterizedType parameterizedType = (ParameterizedType) subClass.getGenericSuperclass(); return (Class<?>) parameterizedType.getActualTypeArguments()[parameterIndex]; } |
Эта функция просматривает иерархию классов экземпляра, пока не распознает classOfInterest следующим прямым подклассом в иерархии. В этом случае этот суперкласс будет получен с помощью метода Class.getGenericSuperclass. Как описано выше, этот метод возвращает суперкласс класса в упакованном представлении (ParamererizedType), которое включает в себя универсальные типы, которые находятся в подклассе . Это позволяет нам успешно запустить следующее приложение:
1
2
|
Class<?> genericType = findSuperClassParameterType( new MyStringSubClass(), MyGenericClass. class , 0 ); assert genericType == String. class ; |
Однако помните, что
1
|
findSuperClassParamerterType( new MyGenericClass<String>(), MyGenericClass. class , 0 ) |
выбросит исключение в этой реализации. Как указано выше: общая информация может быть получена только с помощью подкласса. MyGenericClass <String>, однако, не является подклассом MyGenericClass.class, а является прямым экземпляром с универсальным аргументом. Но без явного подкласса не существует представления <thing> .class для хранения аргумента String. Поэтому на этот раз универсальный тип был безвозвратно удален во время компиляции. По этой причине рекомендуется определить MyGenericClass абстрактным, если вы планируете выполнять такие запросы в классе.
Тем не менее, мы еще не решили проблему, так как есть несколько подводных камней, которые мы до сих пор игнорировали. Чтобы показать почему, подумайте о следующей иерархии классов:
1
2
3
|
class MyGenericClass<T> { } class MyGenericSubClass<U> extends MyGenericClass<U> class MyStringSubSubClass extends MyGenericSubClass<String> { } |
Если мы сейчас позвоним
1
|
findSuperClassParameterType( new MyStringSubClass(), MyGenericClass. class , 0 ); |
исключение будет брошено. Но почему это так? До сих пор мы предполагали, что параметр типа T для MyGenericClass был сохранен в прямом подклассе. В нашем первом примере это был MyStringSubClass, который отображал универсальный параметр T = String. Напротив, теперь MyStringSubSubClass хранит ссылку U = String, в то время как MyGenericSubClass знает только, что U = T. U, однако, не фактический класс, а переменная типа типа Java TypeVariable. Если мы хотим разрешить эту иерархию, мы должны разрешить все эти зависимости. Это может быть достигнуто путем настройки нашего примера кода:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public static Class<?> findSubClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) { Map<Type, Type> typeMap = new HashMap<Type, Type>(); Class<?> instanceClass = instance.getClass(); while (classOfInterest != instanceClass.getSuperclass()) { extractTypeArguments(typeMap, instanceClass); instanceClass = instanceClass.getSuperclass(); if (instanceClass == null ) throw new IllegalArgumentException(); } ParameterizedType parameterizedType = (ParameterizedType) instanceClass.getGenericSuperclass(); Type actualType = parameterizedType.getActualTypeArguments()[parameterIndex]; if (typeMap.containsKey(actualType)) { actualType = typeMap.get(actualType); } if (actualType instanceof Class) { return (Class<?>) actualType; } else { throw new IllegalArgumentException(); } private static void extractTypeArguments(Map<Type, Type> typeMap, Class<?> clazz) { Type genericSuperclass = clazz.getGenericSuperclass(); if (!(genericSuperclass instanceof ParameterizedType)) { return ; } ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; Type[] typeParameter = ((Class<?>) parameterizedType.getRawType()).getTypeParameters(); Type[] actualTypeArgument = parameterizedType.getActualTypeArguments(); for ( int i = 0 ; i < typeParameter.length; i++) { if (typeMap.containsKey(actualTypeArgument[i])) { actualTypeArgument[i] = typeMap.get(actualTypeArgument[i]); } typeMap.put(typeParameter[i], actualTypeArgument[i]); } } |
Приведенный выше код разрешит любые определения связанного универсального типа, отслеживая их на карте. Обратите внимание, что недостаточно изучить все определения типов по определенному индексу, поскольку MyClass <A, B> расширяет MyOtherClass <B, A> определяет совершенно допустимый подтип.
Однако мы еще не закончили. Опять же, мы сначала рассмотрим пример:
1
2
3
4
5
6
|
class MyGenericOuterClass<U> { public class MyGenericInnerClass<U> { } } class MyStringOuterSubClass extends MyGenericOuterClass<String> { } MyStringOuterSubClass.MyGenericInnerClass inner = new MyStringOuterSubClass(). new MyGenericInnerClass(); |
На этот раз размышление о внутреннем классе , позвонив
1
|
findSuperClassParameterType(inner, MyGenericInnerClass. class , 0 ); |
не удастся. На первый взгляд, это может показаться последовательным. Мы ищем универсальный тип аргумента в MyGenericInnerClass для экземпляра того же класса. Как мы описали выше, это обычно невозможно, поскольку в MyGenericInnerClass.class не может храниться информация общего типа. Здесь, однако, мы исследуем экземпляр (нестатического) внутреннего класса подтипа универсального класса. MyStringOuterSubClass знает, что U = String. Мы должны принять это во внимание при отражении типа параметра MyGenericInnterClass.
Теперь вот где все становится действительно сложно. Чтобы найти общие объявления во внешних классах, мы должны сначала овладеть этим внешним классом. Это может быть достигнуто путем отражения и того факта, что компилятор Java добавляет синтетическое (это означает без представления исходного кода) это $ 0 к любому внутреннему классу. Это поле можно получить, вызвав Class.getDeclaredField («this $ 0»). Получая экземпляр внешнего класса, в котором содержится текущий внутренний класс, мы автоматически получаем доступ к его классу Java. Теперь мы можем просто продолжить, как описано выше, и сканировать включающий класс на предмет общих определений и добавить их в нашу карту. Однако переменная типа U в MyGenericOuterClass не будет равна представлению U в MyGenericInnerClass. Насколько нам известно, MyGenericInnerClass может быть статическим и определять свое собственное общее пространство имен переменных. Поэтому любой тип TypeVariable, который представляет общие переменные в Java API, оснащен свойством genericDeclaration. Если две общие переменные были определены в разных классах, представления TypeVariable не равны по своему определению, даже если они разделяют имя в одном и том же пространстве имен, поскольку один класс является нестатическим внутренним классом другого.
Поэтому мы должны сделать следующее:
- Сначала попробуйте найти универсальный тип во внутренней иерархии суперклассов классов. Точно так же, как вы сделали бы с не вложенным классом.
- Если вы не можете разрешить тип: Для (нестатического) внутреннего класса и всех его внешних классов разрешите переменные типа как можно полнее. Это может быть достигнуто с помощью того же алгоритма extractTypeArguments и составляет в основном 1. для каждого вложенного класса. Мы можем получить внешние классы, проверив, определено ли поле this $ 0 для внутреннего класса.
- Проверьте, содержит ли один из внешних классов определение универсальной переменной с идентичным именем переменной. Если это так, вы нашли фактический тип универсальной переменной, которую вы искали.
В коде это выглядит так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
public static Class<?> findSubClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) { Map<Type, Type> typeMap = new HashMap<Type, Type>(); Class<?> instanceClass = instance.getClass(); while (classOfInterest != instanceClass.getSuperclass()) { extractTypeArguments(typeMap, instanceClass); instanceClass = instanceClass.getSuperclass(); if (instanceClass == null ) throw new IllegalArgumentException(); } ParameterizedType parameterizedType = (ParameterizedType) instanceClass.getGenericSuperclass(); Type actualType = parameterizedType.getActualTypeArguments()[parameterIndex]; if (typeMap.containsKey(actualType)) { actualType = typeMap.get(actualType); } if (actualType instanceof Class) { return (Class<?>) actualType; } else if (actualType instanceof TypeVariable) { return browseNestedTypes(instance, (TypeVariable<?>) actualType); } else { throw new IllegalArgumentException(); } } private static Class<?> browseNestedTypes(Object instance, TypeVariable<?> actualType) { Class<?> instanceClass = instance.getClass(); List<Class<?>> nestedOuterTypes = new LinkedList<Class<?>>(); for ( Class<?> enclosingClass = instanceClass.getEnclosingClass(); enclosingClass != null ; enclosingClass = enclosingClass.getEnclosingClass()) { try { Field this $ 0 = instanceClass.getDeclaredField( "this$0" ); Object outerInstance = this $ 0 .get(instance); Class<?> outerClass = outerInstance.getClass(); nestedOuterTypes.add(outerClass); Map<Type, Type> outerTypeMap = new HashMap<Type, Type>(); extractTypeArguments(outerTypeMap, outerClass); for (Map.Entry<Type, Type> entry : outerTypeMap.entrySet()) { if (!(entry.getKey() instanceof TypeVariable)) { continue ; } TypeVariable<?> foundType = (TypeVariable<?>) entry.getKey(); if (foundType.getName().equals(actualType.getName()) && isInnerClass(foundType.getGenericDeclaration(), actualType.getGenericDeclaration())) { if (entry.getValue() instanceof Class) { return (Class<?>) entry.getValue(); } actualType = (TypeVariable<?>) entry.getValue(); } } } catch (NoSuchFieldException e) { /* this should never happen */ } catch (IllegalAccessException e) { /* this might happen */ } } throw new IllegalArgumentException(); } private static boolean isInnerClass(GenericDeclaration outerDeclaration, GenericDeclaration innerDeclaration) { if (!(outerDeclaration instanceof Class) || !(innerDeclaration instanceof Class)) { throw new IllegalArgumentException(); } Class<?> outerClass = (Class<?>) outerDeclaration; Class<?> innerClass = (Class<?>) innerDeclaration; while ((innerClass = innerClass.getEnclosingClass()) != null ) { if (innerClass == outerClass) { return true ; } } return false ; } |
Вау, это безобразно! Но приведенный выше код заставляет findSubClassParameterType работать даже с вложенными классами. Мы могли бы углубиться в еще более подробные сведения, поскольку мы также можем найти типы универсальных интерфейсов, универсальных методов, полей или массивов. Однако идея всех таких извлечений остается неизменной. Если подкласс знает общие аргументы своего суперкласса, их можно получить с помощью отражений. В противном случае из-за стирания типов общие аргументы будут безвозвратно потеряны во время выполнения.
Но в конце концов, для чего это нужно? Многим разработчикам это создает впечатление выполненной черной магии, так что они скорее избегают написания такого кода. По общему признанию, есть вообще более простые способы выполнить такой запрос. Мы могли бы определить MyGenericSubclass следующим образом:
1
2
3
4
5
6
7
8
9
|
class MyGenericClass<T> { private final Class<T> clazz; public MyGenericClass(Class<T> clazz) { this .clazz = clazz; } public Class<T> getGenericClass() { return clazz; } } |
Конечно, это работает так же, и это еще меньше кода. Однако, когда вы пишете API, которые должны использоваться другими разработчиками, вы часто хотите, чтобы они были максимально тонкими и простыми. (Это может перейти от написания большой платформы к написанию программного обеспечения в команде из двух человек.) С помощью описанной выше реализации вы заставляете пользователей вашего класса предоставлять избыточную информацию, которую вы могли бы получить по-разному. Кроме того, этот подход не очень хорошо работает для интерфейсов, где неявно требуется, чтобы реализующие классы добавляли соответствующие конструкторы. Этот вопрос станет еще более актуальным, если взглянуть на Java 8 и его функциональные интерфейсы (также известные как замыкания или лямбда-выражения). Если вам требуется, чтобы ваши универсальные интерфейсы предоставляли метод getGenericClass помимо их функционального метода, вы больше не сможете использовать их в лямбда-выражении.
PS: я взломал этот код, когда писал эту статью в блоге, и никогда не проверял его, кроме как отладкой дупы . Если вам нужна такая функциональность, есть отличная библиотека gentyref, которая предоставляет анализ и многое другое.