Получить тип универсального параметра в Java с отражением

147

Можно ли получить тип универсального параметра?

Пример:

public final class Voodoo {
    public static void chill(List<?> aListWithTypeSpiderMan) {
        // Here I'd like to get the Class-Object 'SpiderMan'
        Class typeOfTheList = ???;
    }

    public static void main(String... args) {
        chill(new ArrayList<SpiderMan>());
    }
}
цимнин
источник

Ответы:

158

Одна конструкция, на которую я однажды наткнулся, выглядела как

Class<T> persistentClass = (Class<T>)
   ((ParameterizedType)getClass().getGenericSuperclass())
      .getActualTypeArguments()[0];

Так что, кажется, есть какая-то магия отражения, которую я, к сожалению, не совсем понимаю ... Извини.

DerMike
источник
2
Но тогда объявите это абстрактным. когда вы хотите его использовать, тогда подклассифицируйте его встроенным.
Snicolas,
44
Несмотря на то, что это очень старый и принятый по какой-то причине, я проголосовал против, потому что он просто не отвечает на вопрос. В примере, приведенном в вопросе, не будет "Человек-паук". Это, несомненно, полезно в некоторых ситуациях, но не работает с заданным вопросом.
Джон Скит
17
Никто, зашедший так далеко, не собирается принимать в качестве ответа «это невозможно». Мы здесь, потому что в отчаянии.
Аддисон
14
Я получаю исключение: Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType не знаю, в чем заключается ограничение.
Блюдо
4
Like @Dish I just got ajava.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
Kenny Wyland
137

Я хочу попытаться разбить ответ @DerMike, чтобы объяснить:

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

Во-вторых, это предоставляет информацию об общем типе универсальному классу ровно на один уровень выше по иерархии от проверяемого конкретного типа - т. Е. Абстрактный родительский класс с параметрами универсального типа может найти конкретные типы, соответствующие параметрам его типа, для конкретной реализации самого себя. что напрямую от него наследуется. Если бы этот класс не был абстрактным и был создан, или конкретная реализация была бы на два уровня ниже, это не сработало бы (хотя немного подергивания могло бы заставить его применяться к любому заранее определенному количеству уровней выше одного или до самого низкого класса. с параметрами универсального типа X и так далее).

Во всяком случае, к объяснению. Вот снова код, разделенный на строки для удобства:

1 # Класс genericParameter0OfThisClass = 
2 # (класс)
3 # ((ParameterizedType)
4 # getClass ()
5 # .getGenericSuperclass ())
6 # .getActualTypeArguments () [0];

Пусть «нас» будет абстрактным классом с универсальными типами, который содержит этот код. Читая это примерно наизнанку:

  • Строка 4 получает экземпляр класса текущего конкретного класса. Это определяет конкретный тип нашего непосредственного потомка.
  • Строка 5 получает супертип этого класса как Тип; это мы. Поскольку мы параметрический тип, мы можем безопасно привести себя к ParameterizedType (строка 3). Ключ в том, что когда Java определяет этот объект Type, он использует информацию о типе, присутствующую в дочернем элементе, для связывания информации о типе с нашими параметрами типа в новом экземпляре ParameterizedType. Итак, теперь мы можем получить доступ к конкретным типам для наших дженериков.
  • Строка 6 получает массив типов, отображаемых в наши обобщенные типы, в порядке, объявленном в коде класса. Для этого примера мы извлекаем первый параметр. Это возвращается как Тип.
  • Строка 2 приводит последний тип, возвращаемый классу. Это безопасно, потому что мы знаем, какие типы могут принимать параметры нашего универсального типа, и можем подтвердить, что все они будут классами (я не уверен, как в Java можно было бы получить общий параметр, у которого нет экземпляра класса связанные с этим, собственно).

... и это почти все. Таким образом, мы отправляем информацию о типе из нашей собственной конкретной реализации обратно в себя и используем ее для доступа к дескриптору класса. мы могли бы удвоить getGenericSuperclass () и перейти на два уровня или исключить getGenericSuperclass () и получить значения для себя как конкретного типа (предостережение: я не тестировал эти сценарии, они еще не подошли для меня).

Это становится сложно, если ваши конкретные дети находятся на произвольном расстоянии от вас, или если вы конкретны, а не окончательны, и особенно сложно, если вы ожидаете, что у любого из ваших (с разной глубиной) детей будут свои собственные дженерики. Но обычно вы можете проектировать, исходя из этих соображений, так что это поможет вам в большинстве случаев.

Надеюсь, это кому-то помогло! Я понимаю, что этот пост старый. Я, вероятно, уберу это объяснение и оставлю его для других вопросов.

Марк МакКенна
источник
3
Просто отвечая на ваш «Я не уверен, как в Java можно было бы получить общий параметр, который не имеет связанного с ним экземпляра класса, на самом деле)»: это возможно, если параметр универсального класса имеет значение « выражение подстановочного знака »(скажем:? extends SomeClass) или« переменная типа »(скажем: <T>, где T происходит от общего подкласса). в обоих случаях возвращенные экземпляры «Type» не могут быть преобразованы в «Class», поскольку каждая виртуальная машина может иметь разные реализации для обоих, которые реализуют интерфейс Type, но не являются производными напрямую от java.lang.Class.
Роберто Андраде
19

На самом деле я заставил это работать. Рассмотрим следующий фрагмент:

Method m;
Type[] genericParameterTypes = m.getGenericParameterTypes();
for (int i = 0; i < genericParameterTypes.length; i++) {
     if( genericParameterTypes[i] instanceof ParameterizedType ) {
                Type[] parameters = ((ParameterizedType)genericParameterTypes[i]).getActualTypeArguments();
//parameters[0] contains java.lang.String for method like "method(List<String> value)"

     }
 }

Я использую jdk 1.6

Андрей Родионов
источник
12
-1: это не решает проблему в вопросе; он дает вам только тип, объявленный в сигнатуре метода, но не фактический тип среды выполнения.
Майкл Боргвардт,
5
Не могу ответить на вопрос ОП, но это полезный метод.
dnault
3
Это ответ на совершенно другой вопрос.
Давуд ибн Карим
Вы можете вызвать m.getParameterTypes (), чтобы получить все фактические типы. Для примера он вернет "Список".
Ли Мидор
Если genericParameterTypes [i] не является экземпляром ParameterizedType, это может быть класс. Это означает, что конкретный аргумент метода вообще не параметризован.
Ли Мидор
18

На самом деле есть решение, применяя уловку «анонимного класса» и идеи из токенов супертипа :

public final class Voodoo {
    public static void chill(final List<?> aListWithSomeType) {
        // Here I'd like to get the Class-Object 'SpiderMan'
        System.out.println(aListWithSomeType.getClass().getGenericSuperclass());
        System.out.println(((ParameterizedType) aListWithSomeType
            .getClass()
            .getGenericSuperclass()).getActualTypeArguments()[0]);
    }
    public static void main(String... args) {
        chill(new ArrayList<SpiderMan>() {});
    }
}
class SpiderMan {
}

Хитрость заключается в создании анонимного класса , new ArrayList<SpiderMan>() {}в месте оригинала (простой) new ArrayList<SpiderMan>(). Использование анонимного класса (если возможно) гарантирует, что компилятор сохраняет информацию об аргументе типа, SpiderManзаданном параметру типа List<?>. Вуаля!

Янн-Гаэль Генек
источник
1
Это косвенно отвечает на вопрос: исходная проблема не имеет решения. Чтобы тип сохранил свои общие параметры, он должен быть встроен в другой тип, например, в подкласс. В этом примере показано, как нужно изменить вызывающий код, чтобы можно было восстановить параметр универсального типа в chill ().
Florian F
К вашему сведению, первая ссылка мертва
Ашвин Шарма
@AshvinSharma Я полагаю, что тот же материал доступен здесь: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Янн-Гаэль Геенек
7

Из-за стирания типа единственный способ узнать тип списка - передать тип в качестве параметра методу:

public class Main {

    public static void main(String[] args) {
        doStuff(new LinkedList<String>(), String.class);

    }

    public static <E> void doStuff(List<E> list, Class<E> clazz) {

    }

}
Джаред Рассел
источник
7

Приложение к ответу @ DerMike для получения универсального параметра параметризованного интерфейса (с использованием метода #getGenericInterfaces () внутри метода по умолчанию Java-8, чтобы избежать дублирования):

import java.lang.reflect.ParameterizedType; 

public class ParametrizedStuff {

@SuppressWarnings("unchecked")
interface Awesomable<T> {
    default Class<T> parameterizedType() {
        return (Class<T>) ((ParameterizedType)
        this.getClass().getGenericInterfaces()[0])
            .getActualTypeArguments()[0];
    }
}

static class Beer {};
static class EstrellaGalicia implements Awesomable<Beer> {};

public static void main(String[] args) {
    System.out.println("Type is: " + new EstrellaGalicia().parameterizedType());
    // --> Type is: ParameterizedStuff$Beer
}
Кампа
источник
Это не то, о чем просил исходный пост. Здесь вы создали подкласс Awesomeable<Beer>. В этом случае информация о типе сохраняется. Если перейти new Awesomable<Beer> ()к методу wt, работать не будет.
Florian F
@FlorianF Если вы перейдете Awesomable<Beer>на лету без явного определения конкретного подкласса, как EstrellaGaliciaв этом случае, он все равно получит параметризованный тип: Я запустил его прямо сейчас: System.out.println("Type is: " + new Awesomable<Beer>() {}.parameterizedType());---> Тип: ParameterizedStuff $ Beer
Campa
6

Нет, это невозможно. Из-за проблем совместимости вниз универсальные шаблоны Java основаны на стирании типа, например , во время выполнения, все, что у вас есть, - это неуниверсальный Listобъект. Существует некоторая информация о параметрах типа во время выполнения, но она находится в определениях классов (т. Е. Вы можете спросить, « какой общий тип используется в определении этого поля? »), А не в экземплярах объектов.

Майкл Боргвардт
источник
Хотя Java может хранить это как метаданные во время выполнения, не так ли? Я надеялся, что так и будет. Невезение.
cimnine
1
@ user99475: Нет. Я прав, а Андрей ошибается. Он имеет в виду информацию о типе, которую я упоминаю во второй части своего ответа, но вопрос не об этом.
Майкл Боргвардт,
5

Как указывает @bertolami, невозможно использовать тип переменной и получить ее будущее значение (содержимое переменной typeOfList).

Тем не менее, вы можете передать класс в качестве параметра следующим образом:

public final class voodoo {
    public static void chill(List<T> aListWithTypeSpiderMan, Class<T> clazz) {
        // Here I'd like to get the Class-Object 'SpiderMan'
        Class typeOfTheList = clazz;
    }

    public static void main(String... args) {
        chill(new List<SpiderMan>(), Spiderman.class );
    }
}

Это более или менее то, что делает Google, когда вам нужно передать переменную класса конструктору ActivityInstrumentationTestCase2 .

Snicolas
источник
3

Нет, это невозможно.

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

См. Пример этого в разделе « Знание общего типа в Java» .

Cletus
источник
1

Вы можете получить тип универсального параметра с отражением, как в этом примере, который я нашел здесь :

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Home<E> {
    @SuppressWarnings ("unchecked")
    public Class<E> getTypeParameterClass(){
        Type type = getClass().getGenericSuperclass();
        ParameterizedType paramType = (ParameterizedType) type;
        return (Class<E>) paramType.getActualTypeArguments()[0];
    }

    private static class StringHome extends Home<String>{}
    private static class StringBuilderHome extends Home<StringBuilder>{}
    private static class StringBufferHome extends Home<StringBuffer>{}   

    /**
     * This prints "String", "StringBuilder" and "StringBuffer"
     */
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        Object object0 = new StringHome().getTypeParameterClass().newInstance();
        Object object1 = new StringBuilderHome().getTypeParameterClass().newInstance();
        Object object2 = new StringBufferHome().getTypeParameterClass().newInstance();
        System.out.println(object0.getClass().getSimpleName());
        System.out.println(object1.getClass().getSimpleName());
        System.out.println(object2.getClass().getSimpleName());
    }
}
Madx
источник
как бы вы сделали то же самое, чтобы получить V of Home <K, V>?
Рафаэль Санчес
1

Быстрый ответ на вопрос : нет, вы не можете из-за стирания универсального типа Java.

Более длинный ответ был бы таким, если бы вы создали свой список следующим образом:

new ArrayList<SpideMan>(){}

Тогда в этом случае общий тип сохраняется в универсальном суперклассе нового анонимного класса выше.

Не то чтобы я рекомендовал делать это со списками, но это реализация слушателя:

new Listener<Type>() { public void doSomething(Type t){...}}

И поскольку экстраполяция общих типов суперклассов и суперинтерфейсов меняется между JVM, общее решение не так прямолинейно, как можно было бы предположить в некоторых ответах.

Вот теперь я это сделал.

TacB0sS
источник
0

Это невозможно, поскольку в Java универсальные шаблоны рассматриваются только во время компиляции. Таким образом, дженерики Java - это всего лишь своего рода препроцессор. Однако вы можете получить фактический класс членов списка.

бертолами
источник
Да, это то, чем я сейчас занимаюсь. Но теперь я хочу знать тип, даже если список пуст. Но четверо парней не могут ошибаться. Спасибо вам всем)!
cimnine
19
Это неправда. Хотя это сложно, в следующем посте вы увидите, что ParameterizedType позволяет это делать.
Edmondo1984
1
Согласитесь, это можно сделать. Пример из DerMike описывает это (сработал для меня).
mac
LOL на тему «четыре парня не могут ошибаться». Это правильный ответ.
Давуд ибн Карим
0

Я закодировал это для методов, которые ожидают принятия или возврата Iterable<?...>. Вот код:

/**
 * Assuming the given method returns or takes an Iterable<T>, this determines the type T.
 * T may or may not extend WindupVertexFrame.
 */
private static Class typeOfIterable(Method method, boolean setter)
{
    Type type;
    if (setter) {
        Type[] types = method.getGenericParameterTypes();
        // The first parameter to the method expected to be Iterable<...> .
        if (types.length == 0)
            throw new IllegalArgumentException("Given method has 0 params: " + method);
        type = types[0];
    }
    else {
        type = method.getGenericReturnType();
    }

    // Now get the parametrized type of the generic.
    if (!(type instanceof ParameterizedType))
        throw new IllegalArgumentException("Given method's 1st param type is not parametrized generic: " + method);
    ParameterizedType pType = (ParameterizedType) type;
    final Type[] actualArgs = pType.getActualTypeArguments();
    if (actualArgs.length == 0)
        throw new IllegalArgumentException("Given method's 1st param type is not parametrized generic: " + method);

    Type t = actualArgs[0];
    if (t instanceof Class)
        return (Class<?>) t;

    if (t instanceof TypeVariable){
        TypeVariable tv =  (TypeVariable) actualArgs[0];
        AnnotatedType[] annotatedBounds = tv.getAnnotatedBounds();///
        GenericDeclaration genericDeclaration = tv.getGenericDeclaration();///
        return (Class) tv.getAnnotatedBounds()[0].getType();
    }

    throw new IllegalArgumentException("Unknown kind of type: " + t.getTypeName());
}
Ондра Жижка
источник
0

Вы не можете получить универсальный параметр из переменной. Но вы можете из объявления метода или поля:

Method method = getClass().getDeclaredMethod("chill", List.class);
Type[] params = method.getGenericParameterTypes();
ParameterizedType firstParam = (ParameterizedType) params[0];
Type[] paramsOfFirstGeneric = firstParam.getActualTypeArguments();
Антон Погонец
источник
Это дает мне исключение в потоке «main» java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl
Lluis Martinez
"params" - это Type, который имеет несколько подинтерфейсов. TypeVariableImpl - это тот, который используется для аргументов метода, и он сообщает имя универсального элемента внутри <> s, а не класс, который он представляет.
Ли Мидор,
0

Мне было сложно читать этот фрагмент кода, я просто разделил его на 2 читаемые строки:

// assuming that the Generic Type parameter is of type "T"
ParameterizedType p = (ParameterizedType) getClass().getGenericSuperclass();
Class<T> c =(Class<T>)p.getActualTypeArguments()[0];

Я хотел создать экземпляр параметра Type без каких-либо параметров для моего метода:

publc T getNewTypeInstance(){
    ParameterizedType p = (ParameterizedType) getClass().getGenericSuperclass();
    Class<T> c =(Class<T>)p.getActualTypeArguments()[0];

    // for me i wanted to get the type to create an instance
    // from the no-args default constructor
    T t = null;
    try{
        t = c.newInstance();
    }catch(Exception e){
        // no default constructor available
    }
    return t;
}
Ахмед Адель Исмаил
источник
0

Вот еще одна хитрость. Используйте общий массив vararg

import java.util.ArrayList;

class TypedArrayList<E> extends ArrayList<E>
{
    @SafeVarargs
    public TypedArrayList (E... typeInfo)
    {
        // Get generic type at runtime ...
        System.out.println (typeInfo.getClass().getComponentType().getTypeName());
    }
}

public class GenericTest
{
    public static void main (String[] args)
    {
        // No need to supply the dummy argument
        ArrayList<Integer> ar1 = new TypedArrayList<> ();
        ArrayList<String> ar2 = new TypedArrayList<> ();
        ArrayList<?> ar3 = new TypedArrayList<> ();
    }
}
Павел
источник
0

Я заметил, что многие склоняются к getGenericSuperclass()решению:

class RootGeneric<T> {
  public Class<T> persistentClass = (Class<T>)
    ((ParameterizedType)getClass().getGenericSuperclass())
      .getActualTypeArguments()[0];
}

Однако это решение подвержено ошибкам. Он не будет работать должным образом, если в потомках есть дженерики. Учти это:

class Foo<S> extends RootGeneric<Integer> {}

class Bar extends Foo<Double> {}

Какой будет тип Bar.persistentClass? Class<Integer>? Нет, будет Class<Double>. Это произойдет из-заgetClass() всегда возвращается самый верхний класс, который Barв данном случае есть, и его общий суперкласс Foo<Double>. Следовательно, тип аргумента будет Double.

Если вам нужно надежное и надежное решение, я могу предложить два.

  1. Используйте Guava. Он имеет класс , который был сделан именно для этой цели: com.google.common.reflect.TypeToken. Он отлично справляется со всеми угловыми случаями и предлагает более приятную функциональность. Обратной стороной является дополнительная зависимость. Если вы использовали этот класс, ваш код выглядел бы просто и понятно, например:
class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  public final Class<T> persistentClass = (Class<T>) (new TypeToken<T>(getClass()) {}.getType());
}
  1. Используйте настраиваемый метод ниже. Он реализует значительно упрощенную логику, аналогичную упомянутому выше классу Guava. Однако я не гарантирую, что это подвержено ошибкам. Тем не менее, это решает проблему с родовыми потомками.
abstract class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  private Class<T> getTypeOfT() {
    Class<T> type = null;
    Class<?> iter = getClass();
    while (iter.getSuperclass() != null) {
      Class<?> next = iter.getSuperclass();
      if (next != null && next.isAssignableFrom(RootGeneric.class)) {
        type =
            (Class<T>)
                ((ParameterizedType) iter.getGenericSuperclass()).getActualTypeArguments()[0];
        break;
      }
      iter = next;
    }
    if (type == null) {
      throw new ClassCastException("Cannot determine type of T");
    }
    return type;
  }
}
real4x
источник
-1

Использование:

Class<?> typeOfTheList = aListWithTypeSpiderMan.toArray().getClass().getComponentType();
user4845560
источник
Это не правильно. toArray()возвращается Object[]. Вы не можете и не должны зависеть от конкретного подтипа.
Radiodef