Pobierz typ parametru ogólnego w Javie z odbiciem

143

Czy można uzyskać typ parametru generycznego?

Przykład:

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>());
    }
}
cimnine
źródło

Odpowiedzi:

156

Jedna konstrukcja, na którą kiedyś się natknąłem, wyglądała jak

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

Więc wydaje się, że jest jakaś magia refleksji, której niestety nie do końca rozumiem ... Przepraszam.

DerMike
źródło
2
Ale wtedy ogłoś to jako abstrakcyjne. kiedy chcesz go użyć, podklasuj go w linii.
Snicolas
42
Mimo że jest to bardzo stare i z jakiegoś powodu zaakceptowane , odrzuciłem głos, ponieważ po prostu nie odpowiada na pytanie. Nie dałoby to „SpiderMana” w przykładzie podanym w pytaniu. Jest to niewątpliwie przydatne w niektórych sytuacjach, ale nie działa w przypadku zadanego pytania.
Jon Skeet
15
Nikt, kto zaszedł tak daleko, nie przyjmie odpowiedzi „Nie da się tego zrobić”. Jesteśmy tutaj, ponieważ jesteśmy zdesperowani.
Addison,
14
Dostaję ten wyjątek: Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType nie jestem pewien, jakie jest ograniczenie.
Danie
4
Podobnie jak @Dish, po prostu dostajęjava.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
Kenny Wyland
133

Chcę spróbować rozbić odpowiedź z @DerMike, aby wyjaśnić:

Po pierwsze, wymazywanie typów nie oznacza, że ​​JDK eliminuje informacje o typach w czasie wykonywania. Jest to metoda umożliwiająca sprawdzanie typów w czasie kompilacji i zgodność typów w środowisku wykonawczym w tym samym języku. Jak sugeruje ten blok kodu, JDK zachowuje usunięte informacje o typie - po prostu nie jest powiązany z zaznaczonymi rzutami i innymi rzeczami.

Po drugie, zapewnia to informacje o typie ogólnym klasie ogólnej dokładnie o jeden poziom w górę w hierarchii od sprawdzanego typu konkretnego - tj. Abstrakcyjna klasa nadrzędna z parametrami typu ogólnego może znaleźć konkretne typy odpowiadające jej parametrom typu w celu konkretnej implementacji samej siebie która bezpośrednio po niej dziedziczy. Gdyby ta klasa nie była abstrakcyjna i została utworzona, lub konkretna implementacja byłaby o dwa poziomy niższa, to nie zadziałałoby (chociaż odrobina jimmyingu mogłaby sprawić, że miałaby zastosowanie do dowolnej z góry określonej liczby poziomów powyżej jednego lub do najniższej klasy z X parametrami typu ogólnego i tak dalej).

W każdym razie do wyjaśnienia. Oto kod ponownie, podzielony na linie dla ułatwienia wyszukiwania:

1 # Klasa genericParameter0OfThisClass = 
2 # (klasa)
3 # ((ParameterizedType)
4 # getClass ()
5 # .getGenericSuperclass ())
6 # .getActualTypeArguments () [0];

Niech „my” będzie klasą abstrakcyjną z typami generycznymi zawierającymi ten kod. Czytając to z grubsza na lewą stronę:

  • Linia 4 pobiera bieżącą instancję klasy konkretnej klasy. To identyfikuje konkretny typ naszego bezpośredniego potomka.
  • Linia 5 otrzymuje nadtyp tej klasy jako typ; to my. Ponieważ jesteśmy typem parametrycznym, możemy bezpiecznie rzutować się na ParameterizedType (wiersz 3). Kluczem jest to, że kiedy Java określa ten obiekt Type, używa informacji o typie obecnych w elemencie potomnym do skojarzenia informacji o typie z naszymi parametrami typu w nowej instancji ParameterizedType. Teraz możemy uzyskać dostęp do konkretnych typów naszych leków generycznych.
  • Linia 6 pobiera tablicę typów odwzorowanych na nasze typy generyczne, w kolejności zadeklarowanej w kodzie klasy. W tym przykładzie wyciągamy pierwszy parametr. To wraca jako typ.
  • Linia 2 rzuca ostateczny typ zwrócony do klasy. Jest to bezpieczne, ponieważ wiemy, jakie typy są w stanie przyjmować nasze parametry typu ogólnego i możemy potwierdzić, że wszystkie będą klasami (nie jestem pewien, jak w Javie można by uzyskać ogólny parametr, który nie ma instancji klasy z tym związane).

... i to wszystko. Więc wypychamy informacje o typie z naszej własnej konkretnej implementacji z powrotem do siebie i używamy ich, aby uzyskać dostęp do uchwytu klasy. moglibyśmy podwoić getGenericSuperclass () i przejść o dwa poziomy lub wyeliminować getGenericSuperclass () i uzyskać wartości dla siebie jako konkretny typ (zastrzeżenie: nie testowałem tych scenariuszy, jeszcze nie pojawiły się dla mnie).

To staje się trudne, jeśli twoje konkretne dzieci są oddalone o dowolną liczbę przeskoków lub jeśli jesteś konkretny i nie ostateczny, a szczególnie trudny, jeśli oczekujesz, że którekolwiek z twoich (zmiennie głębokich) dzieci będzie miało własne rodzaje. Ale zwykle możesz projektować pod kątem tych rozważań, więc to daje Ci większość możliwości.

Mam nadzieję, że to komuś pomogło! Uznaję, że ten post jest starożytny. Prawdopodobnie wytnę to wyjaśnienie i zatrzymam na inne pytania.

Mark McKenna
źródło
3
Odpowiadając tylko na Twoje „Nie jestem pewien, jak w Javie można by uzyskać ogólny parametr, który nie ma skojarzonej z nim instancji klasy)”: Jest to możliwe, jeśli parametr klasy ogólnej jest albo „ wyrażenie wieloznaczne „(powiedzmy:? rozszerza SomeClass) lub„ zmienna typu ”(powiedzmy: <T>, gdzie T pochodzi z podklasy ogólnej). w obu przypadkach zwróconych instancji „Type” nie można rzutować na „Class”, ponieważ każda maszyna wirtualna może mieć inną implementację dla obu, które implementują interfejs Type, ale nie pochodzą bezpośrednio z java.lang.Class.
Roberto Andrade,
19

Właściwie udało mi się to. Rozważ następujący fragment:

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)"

     }
 }

Używam jdk 1.6

Andrey Rodionov
źródło
12
-1: to nie rozwiązuje problemu w pytaniu; podaje tylko typ zadeklarowany w sygnaturze metody, a nie rzeczywisty typ środowiska wykonawczego.
Michael Borgwardt
5
Może nie odpowiedzieć na pytanie OP, ale jest to przydatna technika.
dnault
3
To jest odpowiedź na zupełnie inne pytanie.
Dawood ibn Kareem
Możesz wywołać m.getParameterTypes (), aby uzyskać wszystkie rzeczywiste typy. Na przykład zwróci „List”.
Lee Meador
Jeśli genericParameterTypes [i] nie jest wystąpieniem ParameterizedType, może to być Class. Oznacza to, że określony argument metody nie jest w ogóle sparametryzowany.
Lee Meador
18

Właściwie istnieje rozwiązanie, stosując sztuczkę „klasy anonimowej” i pomysły z tokenów supertypów :

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 {
}

Kłamstwa trick w sprawie utworzenia anonimowych klasy , new ArrayList<SpiderMan>() {}w miejsce oryginału (proste) new ArrayList<SpiderMan>(). Użycie anonimowej klasy (jeśli to możliwe) zapewnia, że ​​kompilator zachowuje informacje o argumencie typu SpiderManpodanym w parametrze typu List<?>. Voilà!

Yann-Gaël Guéhéneuc
źródło
To pośrednio odpowiada na pytanie: pierwotny problem nie ma rozwiązania. Aby typ zachował swoje parametry ogólne, musi być osadzony w innym typie, takim jak podklasa. Ten przykład pokazuje, jak należy zmienić kod wywołujący, aby umożliwić odzyskanie parametru typu ogólnego w chill ().
Florian F
FYI, pierwsze łącze nie działa
Ashvin Sharma
@AshvinSharma Uważam, że ten sam materiał jest dostępny tutaj: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Yann-Gaël Guéhéneuc
7

Ze względu na wymazywanie typów jedynym sposobem na poznanie typu listy byłoby przekazanie typu jako parametru do metody:

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) {

    }

}
Jared Russell
źródło
7

Dodatek do odpowiedzi @ DerMike na uzyskanie ogólnego parametru sparametryzowanego interfejsu (przy użyciu metody #getGenericInterfaces () wewnątrz domyślnej metody Java-8, aby uniknąć powielania):

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
}
Campa
źródło
Nie o to prosił oryginalny post. Tutaj utworzyłeś podklasę Awesomeable<Beer>. W takim przypadku informacje o typie są zachowane. Jeśli przejdziesz new Awesomable<Beer> ()do metody, wt nie zadziała.
Florian F
@FlorianF Jeśli przekazujesz w Awesomable<Beer>locie bez wyraźnej definicji konkretnej podklasy, jak EstrellaGaliciaw tym przypadku, nadal jest to sparametryzowany typ: Uruchomiłem go teraz: System.out.println("Type is: " + new Awesomable<Beer>() {}.parameterizedType());---> Typ to: ParameterizedStuff $ Beer
Campa
6

Nie, to niemożliwe. Ze względu na problemy ze zgodnością w dół, generyczne programy Java opierają się na wymazywaniu typów , między innymi w czasie wykonywania, wszystko co masz to Listobiekt nieogólny . Istnieje kilka informacji o parametrach typu w czasie wykonywania, ale znajdują się one w definicjach klas (tj. Można zapytać „ jakiego typu generycznego używa definicja tego pola? ”), A nie w instancjach obiektów.

Michael Borgwardt
źródło
Chociaż java może przechowywać to jako metadane podczas działania, prawda? Miałem nadzieję, że tak się stanie. Pech.
cimnine
1
@ user99475: Nie. Mam rację, a Andrey się myli. Odnosi się do typu informacji, o których wspominam w drugiej części mojej odpowiedzi, ale nie o to chodzi w pytaniu.
Michael Borgwardt
5

Jak zaznaczył @bertolami, nie jest dla nas możliwy typ zmiennej i pobranie jej przyszłej wartości (zawartość zmiennej typeOfList).

Niemniej jednak możesz przekazać klasę jako parametr w ten sposób:

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 );
    }
}

To mniej więcej to, co robi Google, gdy musisz przekazać zmienną klasy do konstruktora ActivityInstrumentationTestCase2 .

Snicolas
źródło
3

Nie, to niemożliwe.

Możesz uzyskać ogólny typ pola, biorąc pod uwagę, że klasa jest jedynym wyjątkiem od tej reguły, a nawet to trochę hack.

Zobacz Znajomość typów ogólnych w Javie, aby zobaczyć przykład.

cletus
źródło
1

Możesz uzyskać typ parametru generycznego z odbiciem, jak w tym przykładzie, który znalazłem tutaj :

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
źródło
jak zrobiłbyś to samo, aby uzyskać V z Home <K, V>?
Rafael Sanches
1

Szybka odpowiedź na pytanie brzmi: nie, nie możesz, z powodu wymazywania ogólnego typu Java.

Dłuższą odpowiedzią byłoby to, że jeśli utworzyłeś swoją listę w ten sposób:

new ArrayList<SpideMan>(){}

W takim przypadku typ ogólny jest zachowywany w nadklasie ogólnej nowej klasy anonimowej powyżej.

Nie żebym zalecał robienie tego z listami, ale jest to implementacja słuchacza:

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

A ponieważ ekstrapolacja ogólnych typów superklas i super interfejsów zmienia się między maszynami JVM, ogólne rozwiązanie nie jest tak proste, jak mogą sugerować niektóre odpowiedzi.

Oto teraz zrobiłem to.

TacB0sS
źródło
0

Jest to niemożliwe, ponieważ typy generyczne w Javie są uwzględniane tylko w czasie kompilacji. W związku z tym typy generyczne języka Java to tylko pewnego rodzaju preprocesor. Możesz jednak uzyskać aktualną klasę członków listy.

bertolami
źródło
Tak, właśnie to teraz robię. Ale teraz chcę znać typ, nawet jeśli lista jest pusta. Ale czterech facetów nie może się mylić. Dziękuję wam wszystkim)!
cimnine
19
To nie jest prawda. Chociaż jest to trudne, możesz zobaczyć w następnym poście, że ParameterizedType pozwala to zrobić.
Edmondo1984,
1
Zgadzam się, da się to zrobić. Przykład z DerMike przedstawia to (zadziałał dla mnie).
mac
LOL na "czterech facetów nie może się mylić". Ta odpowiedź jest poprawna.
Dawood ibn Kareem
0

Zakodowałem to dla metod, które oczekują akceptacji lub zwrotu Iterable<?...>. Oto kod:

/**
 * 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());
}
Ondra Žižka
źródło
0

Nie można pobrać ogólnego parametru ze zmiennej. Ale możesz z metody lub deklaracji pola:

Method method = getClass().getDeclaredMethod("chill", List.class);
Type[] params = method.getGenericParameterTypes();
ParameterizedType firstParam = (ParameterizedType) params[0];
Type[] paramsOfFirstGeneric = firstParam.getActualTypeArguments();
Anton Pogonets
źródło
To daje mi wyjątek w wątku „main” java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl
Lluis Martinez
„parametry” to „Typy, które mają kilka podinterfejsów”. TypeVariableImpl jest tym, który jest używany dla argumentów metod i podaje nazwę generycznej wewnątrz <> s, a nie klasę, którą reprezentuje.
Lee Meador
0

Czytanie tego fragmentu kodu było trudne, po prostu podzieliłem go na 2 czytelne wiersze:

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

Chciałem utworzyć wystąpienie parametru Type bez żadnych parametrów mojej metody:

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;
}
Ahmed Adel Ismail
źródło
0

Oto kolejna sztuczka. Użyj ogólnej tablicy 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<> ();
    }
}
Paweł
źródło
0

Zauważyłem, że wiele osób skłania się ku getGenericSuperclass()rozwiązaniu:

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

Jednak to rozwiązanie jest podatne na błędy. To będzie nie działać prawidłowo, jeśli są leki generyczne w potomkom. Rozważ to:

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

class Bar extends Foo<Double> {}

Który typ będzie Bar.persistentClassmiał? Class<Integer>? Nie, będzie Class<Double>. Stanie się tak, ponieważ getClass()zawsze zwraca najwyższą najwyższą klasę, co jest Barw tym przypadku, a jego ogólna super klasa tak Foo<Double>. Stąd typ argumentu będzie Double.

Jeśli potrzebujesz niezawodnego rozwiązania, które nie zawodzi, mogę zaproponować dwa.

  1. Użyj Guava. Ma klasę, która została wykonana dokładnie do tego celu: com.google.common.reflect.TypeToken. Doskonale radzi sobie ze wszystkimi narożnikami i oferuje bardziej przyjemną funkcjonalność. Wadą jest dodatkowa zależność. Biorąc pod uwagę, że użyłeś tej klasy, Twój kod wyglądałby prosto i przejrzyście, na przykład:
class RootGeneric<T> {
  @SuppressWarnings("unchecked")
  public final Class<T> persistentClass = (Class<T>) (new TypeToken<T>(getClass()) {}.getType());
}
  1. Użyj poniższej metody niestandardowej. Realizuje znacznie uproszczoną logikę podobną do wspomnianej powyżej klasy Guava. Jednak nie gwarantuję, że jest podatny na błędy. Jednak rozwiązuje problem z potomkami generycznymi.
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
źródło
-1

Posługiwać się:

Class<?> typeOfTheList = aListWithTypeSpiderMan.toArray().getClass().getComponentType();
user4845560
źródło
To jest źle. toArray()zwraca Object[]. Nie możesz i nie powinieneś polegać na tym, że jest to jakiś szczególny podtyp.
Radiodef