Java: bagaimana saya mendapatkan literal kelas dari tipe generik?

193

Biasanya, saya telah melihat orang menggunakan kelas literal seperti ini:

Class<Foo> cls = Foo.class;

Tetapi bagaimana jika tipenya generik, misalnya Daftar? Ini berfungsi dengan baik, tetapi memiliki peringatan karena Daftar harus diparameterisasi:

Class<List> cls = List.class

Jadi mengapa tidak menambahkan <?>? Nah, ini menyebabkan kesalahan tipe ketidakcocokan:

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

Saya pikir sesuatu seperti ini akan berhasil, tetapi ini hanyalah kesalahan sintaksis:

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

Bagaimana saya bisa mendapatkan yang Class<List<Foo>>statis, misalnya menggunakan literal kelas?

Saya bisa menggunakan @SuppressWarnings("unchecked")untuk menghilangkan peringatan yang disebabkan oleh penggunaan Daftar yang tidak parameter pada contoh pertama Class<List> cls = List.class,, tapi saya lebih suka tidak.

Ada saran?

Tom
sumber

Jawaban:

160

Anda tidak dapat karena mengetikkan penghapusan .

Java generics hanya sedikit lebih dari gula sintaksis untuk benda gips. Untuk menunjukkan:

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

Satu-satunya contoh di mana informasi tipe umum disimpan saat runtime adalah dengan Field.getGenericType()jika menginterogasi anggota kelas melalui refleksi.

Semua ini mengapa Object.getClass()memiliki tanda tangan ini:

public final native Class<?> getClass();

Bagian yang penting adalah Class<?>.

Dengan kata lain, dari FAQ Java Generics :

Mengapa tidak ada kelas literal untuk tipe parameter beton?

Karena tipe parameter tidak memiliki representasi tipe runtime yang tepat.

Kelas literal menunjukkan Class objek yang mewakili tipe tertentu. Sebagai contoh, kelas literal String.classmenunjukkan Class objek yang mewakili tipe Stringdan identik dengan Classobjek yang dikembalikan ketika metode getClassdipanggil pada Stringobjek. Literal kelas dapat digunakan untuk pemeriksaan tipe runtime dan untuk refleksi.

Tipe parameter kehilangan argumen tipenya ketika diterjemahkan ke kode byte selama kompilasi dalam proses yang disebut tipe erasure. Sebagai efek samping dari penghapusan tipe, semua instantiasi dari tipe generik berbagi representasi runtime yang sama, yaitu dari tipe mentah yang sesuai. Dengan kata lain, tipe parameter tidak memiliki representasi tipe sendiri. Akibatnya, tidak ada gunanya membentuk literal kelas seperti List<String>.class, List<Long>.classdan List<?>.class , karena tidak Classada objek seperti itu ada. Hanya tipe mentah yang Listmemiliki Class objek yang mewakili tipe runtime-nya. Ini disebut sebagai List.class.

cletus
sumber
12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal Anda tidak dapat melakukannya di Jawa, Anda mendapatkan kesalahan kompilasi tipe ketidakcocokan!
DhafirNz
4
jadi ... apa yang harus saya lakukan jika saya membutuhkannya?
Christopher Francisco
2
Anda selalu dapat membodohi kompiler denganList<String> list2 = (List<String>) (Object) list1;
kftse
17
Namun yang lain "Ini hanya bekerja di C #, tetapi tidak di Jawa" untuk saya. Saya deserialising objek JSON, dan typeof (List <MyClass>) berfungsi dengan baik di C #, tetapi List <MyClass> .class adalah kesalahan sintaksis di Java. Ya, ada penjelasan logis untuk itu seperti yang biasa ditulis Cletus, tapi saya selalu bertanya-tanya mengapa semua itu hanya bekerja di C #.
Damn Vegetables
2
apa maksudmu itu legal? Bagian kode itu tidak dikompilasi?
Eduardo Dennis
63

Tidak ada literal kelas untuk tipe parameter, namun ada objek tipe yang benar mendefinisikan tipe ini.

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

Pustaka Gson Google mendefinisikan kelas TypeToken yang memungkinkan untuk hanya menghasilkan tipe parameter dan menggunakannya untuk menentukan objek json dengan tipe parameter kompleks dengan cara yang ramah umum. Dalam contoh Anda, Anda akan menggunakan:

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

Saya bermaksud memposting tautan ke javadoc kelas TypeToken dan Gson tetapi Stack Overflow tidak akan membiarkan saya memposting lebih dari satu tautan karena saya pengguna baru, Anda dapat dengan mudah menemukannya menggunakan pencarian Google

Santi P.
sumber
1
Dengan ini saya dapat membuat kelas dengan E generik dan kemudian menggunakannya clzz = new TypeToken<E>(){}.getRawType();untuk kemudian beralih pada enum yang terstruktur dengan cara yang sama clzz.getEnumConstants()dan kemudian akhirnya menggunakan refection untuk memanggil metode anggota ala Method method = clzz.getDeclaredMethod("getSomeFoo");begitu banyak kemenangan! Terima kasih!
Naruto Sempai
57

Anda dapat mengelolanya dengan gips ganda:

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

slaurent
sumber
2
Dengan mengubah cast kedua dari Objectmenjadi ClassAnda mungkin dapat menyimpan overhead dari cast runtime yang diperiksa (tidak berguna).
Clashsoft
2
@Clashsoft Menggunakan Classalih-alih Object, seperti yang Anda sarankan, tampaknya lebih bermakna tetapi tidak menghilangkan kebutuhan @SuppressWarnings("unchecked")anotasi, bahkan menambahkan peringatan baru:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni
10
Anda dapat menggunakan Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr
@Devstr Saya melihat Anda benar ketika saya mencoba itu ... Apa argumen untuk menggunakan (Obyek) atau (Kelas <?>)?
cellepo
2
Jawaban ini sama sekali tidak ada gunanya. Alasan OP ingin membuat parameter jalur kelas adalah karena dia mendapat uncheckedperingatan. Jawaban ini tidak mengubah / meningkatkan semua itu. OP bahkan menyatakan dalam pertanyaannya bahwa dia tidak ingin menggunakan SuppressWarnings...
Spenhouet
6

Untuk menjelaskan jawaban cletus, pada saat runtime semua record dari tipe generik dihapus. Generik diproses hanya dalam kompiler dan digunakan untuk memberikan keamanan tipe tambahan. Mereka benar-benar hanya singkatan yang memungkinkan kompiler untuk memasukkan typecast di tempat yang tepat. Misalnya, sebelumnya Anda harus melakukan hal berikut:

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

menjadi

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

Ini memungkinkan kompiler untuk memeriksa kode Anda pada waktu kompilasi, tetapi pada saat runtime masih terlihat seperti contoh pertama.

Jim Garrison
sumber
Terima kasih atas penjelasan tambahan - pemahaman saya tentang obat generik jauh lebih jelas sekarang karena saya menyadari mereka bukan mekanisme runtime. :)
Tom
2
Menurut pendapat saya, ini hanya berarti bahwa generik diimplementasikan secara biasa-biasa saja oleh Sun, semoga Oracle memperbaikinya suatu hari nanti. Implementasi C # dari generik jauh lebih baik (Anders seperti dewa)
Marcel Valdez Orozco
1
@MarcelValdezOrozco AFAIK, di Jawa mereka menerapkannya seperti itu karena mereka ingin kode lama (pra-1.5) untuk bekerja pada JVM baru tanpa masalah. Tampaknya ini adalah keputusan desain yang sangat cerdas yang peduli tentang kompatibilitas. Saya tidak berpikir ada yang biasa-biasa saja dalam hal itu.
peter.petrov
3

The Java Generics FAQ dan karena itu juga Cletus' jawaban suara seperti tidak ada gunanya memiliki Class<List<T>>, namun masalah sebenarnya adalah bahwa ini sangat berbahaya:

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

Itu cast()membuatnya tampak seolah-olah aman, tetapi pada kenyataannya itu tidak aman sama sekali.


Meskipun saya tidak mendapatkan dimana tidak ada List<?>.class= Class<List<?>>karena ini akan sangat membantu ketika Anda memiliki metode yang menentukan tipe berdasarkan tipe generik dariClass argumen.

Karena getClass()ada JDK-6184881 yang meminta untuk beralih menggunakan wildcard, namun sepertinya perubahan ini tidak akan dilakukan (segera) karena tidak kompatibel dengan kode sebelumnya (lihat komentar ini ).

Marcono1234
sumber
2

Seperti kita semua tahu bahwa itu terhapus. Tetapi itu dapat diketahui dalam beberapa keadaan di mana tipe tersebut secara eksplisit disebutkan dalam hirarki kelas:

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

Dan sekarang Anda dapat melakukan hal-hal seperti:

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

Info lebih lanjut di sini . Tetapi sekali lagi, hampir mustahil untuk mengambil untuk:

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

di mana itu terhapus.

Jatin
sumber
Ini juga merupakan solusi yang digunakan oleh JAX-RS, lih. GenericEntitydan GenericType.
Hein Blöd
1

Karena fakta yang terbuka bahwa literal Kelas tidak memiliki informasi tipe umum, saya pikir Anda harus berasumsi bahwa tidak mungkin untuk menghilangkan semua peringatan. Di satu sisi, menggunakan Class<Something>sama dengan menggunakan koleksi tanpa menentukan jenis generik. Yang terbaik yang bisa saya dapatkan adalah:

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
sumber
1

Anda bisa menggunakan metode pembantu untuk menyingkirkan @SuppressWarnings("unchecked")seluruh kelas.

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

Maka Anda bisa menulis

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

Contoh penggunaan lainnya adalah

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

Namun, karena jarang benar-benar bermanfaat, dan penggunaan metode ini mengalahkan pengecekan tipe kompiler, saya tidak akan merekomendasikan untuk mengimplementasikannya di tempat yang dapat diakses publik.

aventurin
sumber