Java: jak uzyskać literał klasy od typu ogólnego?

193

Zazwyczaj widziałem, jak ludzie używają literału klasowego w następujący sposób:

Class<Foo> cls = Foo.class;

Ale co, jeśli typ jest ogólny, np. Lista? Działa to dobrze, ale ma ostrzeżenie, ponieważ Listę należy sparametryzować:

Class<List> cls = List.class

Dlaczego więc nie dodać <?>? To powoduje błąd niedopasowania typu:

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

Pomyślałem, że coś takiego będzie działać, ale jest to zwykły błąd składniowy:

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

Jak mogę uzyskać statystyki Class<List<Foo>>, np. Używając literału klasowego?

I mógłby użyć @SuppressWarnings("unchecked"), aby pozbyć się ostrzeżeń wywołanych przez nie-parametrycznego wykorzystaniem listy w pierwszym przykładzie Class<List> cls = List.class, ale raczej nie.

Jakieś sugestie?

Tomek
źródło

Odpowiedzi:

160

Nie możesz tego zrobić z powodu skasowania .

Generyczne Java to niewiele więcej niż cukier syntaktyczny dla rzutowań Object. Aby zademonstrować:

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

Jedynym przypadkiem, w którym ogólne informacje o typie są zachowywane w czasie wykonywania, jest Field.getGenericType()przesłuchanie członków klasy poprzez refleksję.

Wszystko to Object.getClass()ma ten podpis:

public final native Class<?> getClass();

Ważną częścią jest Class<?>.

Innymi słowy, z najczęściej zadawanych pytań dotyczących Java Generics :

Dlaczego nie ma literału klasowego dla konkretnych sparametryzowanych typów?

Ponieważ typ sparametryzowany nie ma dokładnej reprezentacji typu środowiska wykonawczego.

Literał klasy oznacza Class obiekt reprezentujący dany typ. Na przykład literał klasy String.classoznacza Class obiekt reprezentujący typ Stringi jest identyczny z Classobiektem zwracanym po getClasswywołaniu metody na Stringobiekcie. Literału klasy można używać do sprawdzania typu środowiska wykonawczego i do refleksji.

Typy sparametryzowane tracą argumenty typu, gdy są tłumaczone na kod bajtowy podczas kompilacji w procesie zwanym kasowaniem typu. Jako efekt uboczny usuwania typu wszystkie instancje typu ogólnego mają tę samą reprezentację środowiska wykonawczego, a mianowicie odpowiadającego typu surowego. Innymi słowy, sparametryzowane typy nie mają własnej reprezentacji typu. W konsekwencji, nie ma sensu w tworzeniu klasy literały takich jak List<String>.class, List<Long>.classi List<?>.class , ponieważ żadne takie Classistnieją obiekty. Tylko typ raw Listma Class obiekt reprezentujący typ środowiska wykonawczego. Jest to określane jako List.class.

Cletus
źródło
12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal Możesz; t to zrobić w Javie, pojawia się błąd kompilacji niezgodności typów!
DhafirNz
4
więc ... co mam zrobić, jeśli go potrzebuję?
Christopher Francisco
2
Zawsze możesz oszukać kompilator:List<String> list2 = (List<String>) (Object) list1;
kftse 21.04.16
17
Jeszcze jedno „To po prostu działa w C #, ale nie w Javie”. Jestem deserializacją obiektu JSON, a typeof (List <MyClass>) działa doskonale w C #, ale List <MyClass> .class to błąd składniowy w Javie. Tak, istnieje logiczne wytłumaczenie tego, jak zwykle pisał Cletus, ale zawsze zastanawiam się, dlaczego wszystkie te rzeczy działają po prostu w języku C #.
Cholerne warzywa,
2
co masz na myśli całkowicie legalne? Ta część kodu się nie kompiluje?
Eduardo Dennis
63

Brak literałów klas dla sparametryzowanych typów, jednak istnieją obiekty Type, które poprawnie definiują te typy.

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

Biblioteka Google Gson definiuje klasę TypeToken, która pozwala po prostu generować sparametryzowane typy i używa jej do określania obiektów json ze złożonymi sparametryzowanymi typami w ogólny przyjazny sposób. W twoim przykładzie użyłbyś:

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

Zamierzałem publikować linki do javadoc klas TypeToken i Gson, ale przepełnienie stosu nie pozwoli mi opublikować więcej niż jednego linku, ponieważ jestem nowym użytkownikiem, można je łatwo znaleźć za pomocą wyszukiwarki Google

Santi P.
źródło
1
Dzięki temu mogłem stworzyć klasę z ogólnym E, a następnie użyć clzz = new TypeToken<E>(){}.getRawType();do późniejszej iteracji podobnie wyliczonych wyliczeń, clzz.getEnumConstants()a następnie użyć refection, aby nazwać metody członkowskie Method method = clzz.getDeclaredMethod("getSomeFoo");wygraną! Dziękuję Ci!
Naruto Sempai
57

Możesz nim zarządzać za pomocą podwójnej obsady:

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

slaurent
źródło
2
Zmieniając drugi rzut z Objectna Class, prawdopodobnie możesz zaoszczędzić na narzutach (bezcelowego) sprawdzonego rzutu.
Clashsoft
2
@Clashsoft Używanie Classzamiast Object, jak sugerujesz, wydaje się bardziej znaczące, ale nie usuwa potrzeby @SuppressWarnings("unchecked")adnotacji, dodaje nawet nowe ostrzeżenie:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni
10
Możesz użyć Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr
@Devstr Widzę, że masz rację, gdy próbuję ... Jakie są argumenty przemawiające za użyciem (Object) lub (Class <?>)?
cellepo
2
Ta odpowiedź jest całkowicie bezcelowa. Powodem, dla którego OP chciał sparametryzować ścieżkę klasy, było to, że otrzymał uncheckedostrzeżenie. Ta odpowiedź nic nie zmienia / nie poprawia. OP nawet stwierdza w swoim pytaniu, że nie chce używać SuppressWarnings...
Spenhouet,
6

Aby wyjaśnić odpowiedź Cletusa, w czasie wykonywania usuwany jest cały zapis typów ogólnych. Generyczne są przetwarzane tylko w kompilatorze i służą do zapewnienia dodatkowego bezpieczeństwa typu. Są to tak naprawdę tylko stenogramy, które pozwalają kompilatorowi wstawiać typecasty w odpowiednich miejscach. Na przykład wcześniej trzeba wykonać następujące czynności:

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

staje się

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

Pozwala to kompilatorowi sprawdzić kod w czasie kompilacji, ale w czasie wykonywania nadal wygląda jak pierwszy przykład.

Jim Garrison
źródło
Dzięki za dodatkowe wyjaśnienia - moje rozumienie generycznych jest teraz o wiele jaśniejsze, kiedy zdałem sobie sprawę, że nie są one mechanizmem uruchomieniowym. :)
Tom
2
Moim zdaniem oznacza to po prostu, że generyczne zostały zaimplementowane przez firmę Sun w mierny sposób, mam nadzieję, że Oracle to kiedyś naprawi. Implementacja generyczna w C # jest znacznie lepsza (Anders jest boski)
Marcel Valdez Orozco
1
@MarcelValdezOrozco AFAIK, w Javie zaimplementowali go w ten sposób, ponieważ chcieli, aby stary kod (wcześniejszy niż 1.5) działał na nowych maszynach JVM bez żadnych problemów. Wydaje się, że jest to bardzo mądra decyzja projektowa, która dba o kompatybilność. Nie sądzę, żeby było w tym coś przeciętnego.
peter.petrov
3

Java Generics Pytania i dlatego też Cletus' odpowiedź brzmi jak nie ma sensu mając Class<List<T>>jednak prawdziwym problemem jest to, że jest to bardzo niebezpieczne:

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

W cast()sprawia, że wygląda tak, jakby to bezpieczne, ale w rzeczywistości tak nie jest bezpieczny w ogóle.


Chociaż nie rozumiem, gdzie nie może być List<?>.class=, Class<List<?>>ponieważ byłoby to bardzo pomocne, gdy masz metodę, która określa typ na podstawie ogólnego typu Classargumentu.

Dla getClass() istnieje JDK-6184881 prośbą, aby przełączyć się za pomocą symboli wieloznacznych, jednak nie wygląda jak ta zmiana zostanie przeprowadzone (bardzo szybko), ponieważ nie jest kompatybilny z poprzedniego kodu (patrz ten komentarz ).

Marcono1234
źródło
2

Jak wszyscy wiemy, to się kasuje. Ale może być znany w pewnych okolicznościach, w których typ jest wyraźnie wymieniony w hierarchii klas:

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

A teraz możesz robić takie rzeczy jak:

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

Więcej informacji tutaj . Ale znowu prawie niemożliwe jest odzyskanie:

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

gdzie zostaje skasowany.

Jatin
źródło
Jest to również obejście stosowane przez JAX-RS, por. GenericEntitya GenericType.
Hein Blöd
1

Ze względu na ujawniony fakt, że literały klasowe nie zawierają ogólnych informacji o typie, myślę, że powinieneś założyć, że nie będzie można pozbyć się wszystkich ostrzeżeń. W pewien sposób użycie Class<Something>jest takie samo, jak użycie kolekcji bez określania typu ogólnego. Najlepsze, co mogłem wymyślić, to:

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);
}
Thiago Chaves
źródło
1

Możesz użyć metody pomocnika, aby pozbyć się @SuppressWarnings("unchecked")całej klasy.

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

Wtedy mógłbyś pisać

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

Inne przykłady użycia to

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

Ponieważ jednak rzadko kiedy jest to naprawdę przydatne, a użycie metody nie pozwala na sprawdzenie typu kompilatora, nie zalecałbym implementowania go w miejscu, gdzie jest publicznie dostępny.

awenturyn
źródło