Java: come posso ottenere una classe letterale da un tipo generico?


194

In genere, ho visto le persone usare la classe letterale in questo modo:

Class<Foo> cls = Foo.class;

Ma cosa succede se il tipo è generico, ad esempio Elenco? Funziona bene, ma ha un avvertimento poiché l'elenco dovrebbe essere parametrizzato:

Class<List> cls = List.class

Quindi perché non aggiungere un <?>? Bene, questo provoca un errore di mancata corrispondenza del tipo:

Class<List<?>> cls = List.class

Ho pensato che qualcosa del genere avrebbe funzionato, ma questo è solo un semplice errore di sintassi:

Class<List<Foo>> cls = List<Foo>.class

Come posso ottenere una Class<List<Foo>>statica, ad esempio usando la classe letterale?

Ho potuto usare @SuppressWarnings("unchecked")per sbarazzarsi delle avvertenze causati dall'uso non parametrizzata della lista nel primo esempio, Class<List> cls = List.classma io non preferisco.

Eventuali suggerimenti?

Risposte:


161

Non è possibile a causa della cancellazione del tipo .

I generici Java sono poco più che zucchero sintattico per i lanci di oggetti. Dimostrare:

List<Integer> list1 = new ArrayList<Integer>();
List<String> list2 = (List<String>)list1;
list2.add("foo"); // perfectly legal

L'unica istanza in cui le informazioni di tipo generico vengono conservate in fase di esecuzione è con l' Field.getGenericType()interrogazione dei membri di una classe tramite reflection.

Tutto questo è perché Object.getClass()ha questa firma:

public final native Class<?> getClass();

La parte importante è Class<?>.

Per dirla in altro modo, dalle FAQ di Java Generics :

Perché non esiste una classe letterale per tipi parametrici concreti?

Perché il tipo con parametri non ha una rappresentazione esatta del tipo di runtime.

Un letterale di classe indica un Class oggetto che rappresenta un determinato tipo. Ad esempio, il valore letterale della classe String.classindica l' Class oggetto che rappresenta il tipo Stringed è identico Classall'oggetto restituito quando getClassviene invocato il metodo su un Stringoggetto. Un valore letterale di classe può essere utilizzato per i controlli del tipo di runtime e per la riflessione.

I tipi con parametri perdono i loro argomenti di tipo quando vengono tradotti in codice byte durante la compilazione in un processo chiamato cancellazione di tipo. Come effetto collaterale della cancellazione del tipo, tutte le istanze di un tipo generico condividono la stessa rappresentazione di runtime, vale a dire quella del tipo grezzo corrispondente. In altre parole, i tipi con parametri non hanno una rappresentazione del tipo propria. Di conseguenza, non ha senso formare letterali di classe come List<String>.class, List<Long>.classe List<?>.class , poiché tali Classoggetti non esistono. Solo il tipo raw Listha un Class oggetto che rappresenta il suo tipo di runtime. È indicato come List.class.


12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal Non è possibile farlo in Java, si ottiene un errore di compilazione di tipo non corrispondente!
DhafirNz,

4
quindi ... cosa devo fare se ne ho bisogno?
Christopher Francisco,

2
Puoi sempre ingannare il compilatore diList<String> list2 = (List<String>) (Object) list1;
kftse il

17
Ancora un altro "Funziona solo in C #, ma non in Java" per me. Sto deserializzando un oggetto JSON, e typeof (List <MyClass>) funziona perfettamente in C #, ma List <MyClass> .class è un errore di sintassi in Java. Sì, c'è una spiegazione logica per questo come al solito come scrisse Cletus, ma mi chiedo sempre perché tutte queste cose funzionino semplicemente in C #.
Damn Vegetables,

2
cosa intendi perfettamente legale? Quella parte del codice non viene compilata?
Eduardo Dennis,

63

Non esistono valori letterali di classe per tipi con parametri, tuttavia esistono oggetti Type che definiscono correttamente questi tipi.

Vedi java.lang.reflect.ParameterizedType - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/ParameterizedType.html

La libreria Gson di Google definisce una classe TypeToken che consente di generare semplicemente tipi con parametri e la utilizza per specificare oggetti json con tipi con parametri complessi in modo generico generico. Nel tuo esempio useresti:

Type typeOfListOfFoo = new TypeToken<List<Foo>>(){}.getType()

Ho intenzione di pubblicare collegamenti alle classi TypeToken e Gson javadoc ma StackTranslate.it non mi consente di pubblicare più di un collegamento poiché sono un nuovo utente, puoi trovarli facilmente utilizzando la ricerca di Google


1
Con questo sono stato in grado di creare una classe con una E generica e poi utilizzare clzz = new TypeToken<E>(){}.getRawType();per iterare successivamente su enumerazioni strutturate in modo simile clzz.getEnumConstants()e infine utilizzare la refection per chiamare i metodi dei membri Method method = clzz.getDeclaredMethod("getSomeFoo");così tanto! Grazie!
Naruto Sempai,

57

Puoi gestirlo con un doppio cast:

@SuppressWarnings("unchecked") Class<List<Foo>> cls = (Class<List<Foo>>)(Object)List.class


2
Modificando il secondo cast da Objecta Classprobabilmente è possibile salvare il sovraccarico di un cast di runtime (inutile) controllato.
Clashsoft,

2
@Clashsoft L'utilizzo Classinvece di Object, come suggerisci, sembra più significativo, ma non elimina la necessità @SuppressWarnings("unchecked")dell'annotazione, ma aggiunge anche un nuovo avvertimento:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni,

10
Puoi usare Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr il

@Devstr Vedo che hai ragione quando provo che ... Quali sono gli argomenti per usare (Object) o (Class <?>)?
cellepo,

2
Questa risposta è totalmente inutile. Il motivo per cui OP ha voluto parametrizzare il percorso della classe è perché ha ricevuto un uncheckedavviso. Questa risposta non cambia / migliora nulla di tutto ciò. OP afferma anche nella sua domanda che non vuole usare SuppressWarnings...
Spenhouet,

6

Per spiegare la risposta di Cletus, in fase di esecuzione vengono rimossi tutti i record dei tipi generici. I generici vengono elaborati solo nel compilatore e vengono utilizzati per fornire ulteriore sicurezza del tipo. Sono in realtà solo una scorciatoia che consente al compilatore di inserire i caratteri tipografici nei punti appropriati. Ad esempio, in precedenza avresti dovuto fare quanto segue:

List x = new ArrayList();
x.add(new SomeClass());
Iterator i = x.iterator();
SomeClass z = (SomeClass) i.next();

diventa

List<SomeClass> x = new ArrayList<SomeClass>();
x.add(new SomeClass());
Iterator<SomeClass> i = x.iterator();
SomeClass z = i.next();

Ciò consente al compilatore di controllare il codice in fase di compilazione, ma in fase di esecuzione sembra comunque il primo esempio.


Grazie per la spiegazione aggiuntiva - la mia comprensione dei farmaci generici è molto più chiara ora che mi rendo conto che non sono un meccanismo di runtime. :)
Tom

2
Secondo me, questo significa solo che i generici sono stati implementati in modo mediocre da Sun, speriamo che Oracle lo risolva un giorno. L'implementazione di C # del generico è molto meglio (Anders è divino)
Marcel Valdez Orozco,

1
@MarcelValdezOrozco AFAIK, in Java lo hanno implementato in questo modo perché volevano che il vecchio codice (pre-1.5) funzionasse su nuove JVM senza problemi. Sembra che sia una decisione di progettazione molto intelligente che si preoccupa della compatibilità. Non credo ci sia nulla di mediocre in questo.
peter.petrov,

3

Le FAQ di Java Generics e quindi anche la risposta di Cletus sembrano non avere senso Class<List<T>>, tuttavia il vero problema è che questo è estremamente pericoloso:

@SuppressWarnings("unchecked")
Class<List<String>> stringListClass = (Class<List<String>>) (Class<?>) List.class;

List<Integer> intList = new ArrayList<>();
intList.add(1);
List<String> stringList = stringListClass.cast(intList);
// Surprise!
String firstElement = stringList.get(0);

Lo cast()fa sembrare sicuro, ma in realtà non lo è affatto.


Anche se non riesco a trovare dove non ci può essere List<?>.class= Class<List<?>>poiché questo sarebbe piuttosto utile quando si dispone di un metodo che determina il tipo in base al tipo generico di un Classargomento.

Perché getClass()c'è JDK-6184881 che richiede di passare all'utilizzo di caratteri jolly, tuttavia non sembra che questa modifica verrà eseguita (molto presto) poiché non è compatibile con il codice precedente (vedere questo commento ).


2

Bene, come tutti sappiamo che viene cancellato. Ma può essere noto in alcune circostanze in cui il tipo è esplicitamente menzionato nella gerarchia di classi:

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public abstract class CaptureType<T> {
    /**
     * {@link java.lang.reflect.Type} object of the corresponding generic type. This method is useful to obtain every kind of information (including annotations) of the generic type.
     *
     * @return Type object. null if type could not be obtained (This happens in case of generic type whose information cant be obtained using Reflection). Please refer documentation of {@link com.types.CaptureType}
     */
    public Type getTypeParam() {
        Class<?> bottom = getClass();
        Map<TypeVariable<?>, Type> reifyMap = new LinkedHashMap<>();

        for (; ; ) {
            Type genericSuper = bottom.getGenericSuperclass();
            if (!(genericSuper instanceof Class)) {
                ParameterizedType generic = (ParameterizedType) genericSuper;
                Class<?> actualClaz = (Class<?>) generic.getRawType();
                TypeVariable<? extends Class<?>>[] typeParameters = actualClaz.getTypeParameters();
                Type[] reified = generic.getActualTypeArguments();
                assert (typeParameters.length != 0);
                for (int i = 0; i < typeParameters.length; i++) {
                    reifyMap.put(typeParameters[i], reified[i]);
                }
            }

            if (bottom.getSuperclass().equals(CaptureType.class)) {
                bottom = bottom.getSuperclass();
                break;
            }
            bottom = bottom.getSuperclass();
        }

        TypeVariable<?> var = bottom.getTypeParameters()[0];
        while (true) {
            Type type = reifyMap.get(var);
            if (type instanceof TypeVariable) {
                var = (TypeVariable<?>) type;
            } else {
                return type;
            }
        }
    }

    /**
     * Returns the raw type of the generic type.
     * <p>For example in case of {@code CaptureType<String>}, it would return {@code Class<String>}</p>
     * For more comprehensive examples, go through javadocs of {@link com.types.CaptureType}
     *
     * @return Class object
     * @throws java.lang.RuntimeException If the type information cant be obtained. Refer documentation of {@link com.types.CaptureType}
     * @see com.types.CaptureType
     */
    public Class<T> getRawType() {
        Type typeParam = getTypeParam();
        if (typeParam != null)
            return getClass(typeParam);
        else throw new RuntimeException("Could not obtain type information");
    }


    /**
     * Gets the {@link java.lang.Class} object of the argument type.
     * <p>If the type is an {@link java.lang.reflect.ParameterizedType}, then it returns its {@link java.lang.reflect.ParameterizedType#getRawType()}</p>
     *
     * @param type The type
     * @param <A>  type of class object expected
     * @return The Class<A> object of the type
     * @throws java.lang.RuntimeException If the type is a {@link java.lang.reflect.TypeVariable}. In such cases, it is impossible to obtain the Class object
     */
    public static <A> Class<A> getClass(Type type) {
        if (type instanceof GenericArrayType) {
            Type componentType = ((GenericArrayType) type).getGenericComponentType();
            Class<?> componentClass = getClass(componentType);
            if (componentClass != null) {
                return (Class<A>) Array.newInstance(componentClass, 0).getClass();
            } else throw new UnsupportedOperationException("Unknown class: " + type.getClass());
        } else if (type instanceof Class) {
            Class claz = (Class) type;
            return claz;
        } else if (type instanceof ParameterizedType) {
            return getClass(((ParameterizedType) type).getRawType());
        } else if (type instanceof TypeVariable) {
            throw new RuntimeException("The type signature is erased. The type class cant be known by using reflection");
        } else throw new UnsupportedOperationException("Unknown class: " + type.getClass());
    }

    /**
     * This method is the preferred method of usage in case of complex generic types.
     * <p>It returns {@link com.types.TypeADT} object which contains nested information of the type parameters</p>
     *
     * @return TypeADT object
     * @throws java.lang.RuntimeException If the type information cant be obtained. Refer documentation of {@link com.types.CaptureType}
     */
    public TypeADT getParamADT() {
        return recursiveADT(getTypeParam());
    }

    private TypeADT recursiveADT(Type type) {
        if (type instanceof Class) {
            return new TypeADT((Class<?>) type, null);
        } else if (type instanceof ParameterizedType) {
            ArrayList<TypeADT> generic = new ArrayList<>();
            ParameterizedType type1 = (ParameterizedType) type;
            return new TypeADT((Class<?>) type1.getRawType(),
                    Arrays.stream(type1.getActualTypeArguments()).map(x -> recursiveADT(x)).collect(Collectors.toList()));
        } else throw new UnsupportedOperationException();
    }

}

public class TypeADT {
    private final Class<?> reify;
    private final List<TypeADT> parametrized;

    TypeADT(Class<?> reify, List<TypeADT> parametrized) {
        this.reify = reify;
        this.parametrized = parametrized;
    }

    public Class<?> getRawType() {
        return reify;
    }

    public List<TypeADT> getParameters() {
        return parametrized;
    }
}

E ora puoi fare cose come:

static void test1() {
        CaptureType<String> t1 = new CaptureType<String>() {
        };
        equals(t1.getRawType(), String.class);
    }

    static void test2() {
        CaptureType<List<String>> t1 = new CaptureType<List<String>>() {
        };
        equals(t1.getRawType(), List.class);
        equals(t1.getParamADT().getParameters().get(0).getRawType(), String.class);
    }


    private static void test3() {
            CaptureType<List<List<String>>> t1 = new CaptureType<List<List<String>>>() {
            };
            equals(t1.getParamADT().getRawType(), List.class);
        equals(t1.getParamADT().getParameters().get(0).getRawType(), List.class);
    }

    static class Test4 extends CaptureType<List<String>> {
    }

    static void test4() {
        Test4 test4 = new Test4();
        equals(test4.getParamADT().getRawType(), List.class);
    }

    static class PreTest5<S> extends CaptureType<Integer> {
    }

    static class Test5 extends PreTest5<Integer> {
    }

    static void test5() {
        Test5 test5 = new Test5();
        equals(test5.getTypeParam(), Integer.class);
    }

    static class PreTest6<S> extends CaptureType<S> {
    }

    static class Test6 extends PreTest6<Integer> {
    }

    static void test6() {
        Test6 test6 = new Test6();
        equals(test6.getTypeParam(), Integer.class);
    }



    class X<T> extends CaptureType<T> {
    }

    class Y<A, B> extends X<B> {
    }

    class Z<Q> extends Y<Q, Map<Integer, List<List<List<Integer>>>>> {
    }

    void test7(){
        Z<String> z = new Z<>();
        TypeADT param = z.getParamADT();
        equals(param.getRawType(), Map.class);
        List<TypeADT> parameters = param.getParameters();
        equals(parameters.get(0).getRawType(), Integer.class);
        equals(parameters.get(1).getRawType(), List.class);
        equals(parameters.get(1).getParameters().get(0).getRawType(), List.class);
        equals(parameters.get(1).getParameters().get(0).getParameters().get(0).getRawType(), List.class);
        equals(parameters.get(1).getParameters().get(0).getParameters().get(0).getParameters().get(0).getRawType(), Integer.class);
    }




    static void test8() throws IllegalAccessException, InstantiationException {
        CaptureType<int[]> type = new CaptureType<int[]>() {
        };
        equals(type.getRawType(), int[].class);
    }

    static void test9(){
        CaptureType<String[]> type = new CaptureType<String[]>() {
        };
        equals(type.getRawType(), String[].class);
    }

    static class SomeClass<T> extends CaptureType<T>{}
    static void test10(){
        SomeClass<String> claz = new SomeClass<>();
        try{
            claz.getRawType();
            throw new RuntimeException("Shouldnt come here");
        }catch (RuntimeException ex){

        }
    }

    static void equals(Object a, Object b) {
        if (!a.equals(b)) {
            throw new RuntimeException("Test failed. " + a + " != " + b);
        }
    }

Maggiori informazioni qui . Ma ancora una volta, è quasi impossibile recuperare per:

class SomeClass<T> extends CaptureType<T>{}
SomeClass<String> claz = new SomeClass<>();

dove viene cancellato.


Questa è anche la soluzione alternativa utilizzata da JAX-RS, cfr. GenericEntitye GenericType.
Hein Blöd,

1

A causa del fatto esposto che i letterali di classe non hanno informazioni di tipo generico, penso che dovresti presumere che sarà impossibile eliminare tutti gli avvisi. In un certo senso, usare Class<Something>equivale a usare una raccolta senza specificare il tipo generico. Il meglio che ho potuto uscire è stato:

private <C extends A<C>> List<C> getList(Class<C> cls) {
    List<C> res = new ArrayList<C>();
    // "snip"... some stuff happening in here, using cls
    return res;
}

public <C extends A<C>> List<A<C>> getList() {
    return getList(A.class);
}

1

È possibile utilizzare un metodo di supporto per sbarazzarsi di @SuppressWarnings("unchecked")tutta una classe.

@SuppressWarnings("unchecked")
private static <T> Class<T> generify(Class<?> cls) {
    return (Class<T>)cls;
}

Quindi potresti scrivere

Class<List<Foo>> cls = generify(List.class);

Altri esempi di utilizzo sono

  Class<Map<String, Integer>> cls;

  cls = generify(Map.class);

  cls = TheClass.<Map<String, Integer>>generify(Map.class);

  funWithTypeParam(generify(Map.class));

public void funWithTypeParam(Class<Map<String, Integer>> cls) {
}

Tuttavia, poiché raramente è davvero utile e l'uso del metodo non soddisfa il controllo del tipo del compilatore, non consiglierei di implementarlo in un luogo accessibile al pubblico.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.