Dapatkan tipe generik java.util.List

263

Saya sudah;

List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();

Apakah ada cara (mudah) untuk mengambil tipe generik dari daftar?

Thizzer
sumber
Untuk dapat secara terprogram memeriksa objek Daftar dan melihat jenis generiknya. Suatu metode mungkin ingin menyisipkan objek berdasarkan tipe generik dari koleksi. Ini dimungkinkan dalam bahasa yang mengimplementasikan generik pada saat runtime alih-alih waktu kompilasi.
Steve Kuo
4
Kanan - tentang satu-satunya cara untuk memungkinkan deteksi runtime adalah dengan subklasifikasi: Anda BISA benar-benar memperluas tipe generik dan kemudian menggunakan deklarasi tipe temukan refleksi yang digunakan subtipe. Ini sedikit refleksi, tetapi mungkin. Sayangnya tidak ada cara mudah untuk menegakkan bahwa seseorang harus menggunakan sub-kelas generik.
StaxMan
1
Tentunya stringList berisi string dan integerList integer? Mengapa membuatnya lebih rumit?
Ben Thurley

Jawaban:

408

Jika itu benar-benar bidang kelas tertentu, maka Anda bisa mendapatkannya dengan sedikit bantuan refleksi:

package test;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;

public class Test {

    List<String> stringList = new ArrayList<String>();
    List<Integer> integerList = new ArrayList<Integer>();

    public static void main(String... args) throws Exception {
        Field stringListField = Test.class.getDeclaredField("stringList");
        ParameterizedType stringListType = (ParameterizedType) stringListField.getGenericType();
        Class<?> stringListClass = (Class<?>) stringListType.getActualTypeArguments()[0];
        System.out.println(stringListClass); // class java.lang.String.

        Field integerListField = Test.class.getDeclaredField("integerList");
        ParameterizedType integerListType = (ParameterizedType) integerListField.getGenericType();
        Class<?> integerListClass = (Class<?>) integerListType.getActualTypeArguments()[0];
        System.out.println(integerListClass); // class java.lang.Integer.
    }
}

Anda juga dapat melakukan itu untuk tipe parameter dan mengembalikan jenis metode.

Tetapi jika mereka berada dalam ruang lingkup yang sama dari kelas / metode di mana Anda perlu tahu tentang mereka, maka tidak ada gunanya mengenal mereka, karena Anda sudah menyatakannya sendiri.

BalusC
sumber
17
Pasti ada situasi di mana ini di atas berguna. Sebagai contoh, kerangka kerja ORM tanpa konfigurasi.
BalusC
3
..which dapat menggunakan Kelas # getDeclaredFields () untuk mendapatkan semua bidang tanpa perlu tahu nama bidang.
BalusC
1
BalusC: Kedengarannya seperti kerangka injeksi bagi saya .. Itu jenis penggunaan yang saya maksudkan pula.
falstro
1
@loolooyyyy Daripada menggunakan TypeLiteral, saya sarankan menggunakan TypeToken dari Guava. github.com/google/guava/wiki/ReflectionExplained
Babyburger
1
@Oleg Variabel Anda menggunakan <?>(tipe wildcard) alih-alih <Class>(tipe kelas). Ganti <?>dengan <Class>atau apa pun <E>.
BalusC
19

Anda dapat melakukan hal yang sama untuk parameter metode juga:

Type[] types = method.getGenericParameterTypes();
//Now assuming that the first parameter to the method is of type List<Integer>
ParameterizedType pType = (ParameterizedType) types[0];
Class<?> clazz = (Class<?>) pType.getActualTypeArguments()[0];
System.out.println(clazz); //prints out java.lang.Integer
tsaixingwei
sumber
Dalam method.getGenericParameterTypes (); apa itu metode?
Neo Ravi
18

Jawaban singkat: tidak.

Ini mungkin duplikat, tidak dapat menemukan yang tepat sekarang.

Java menggunakan sesuatu yang disebut tipe erasure, yang berarti pada saat runtime kedua objek sama. Kompiler tahu daftar berisi bilangan bulat atau string, dan dengan demikian dapat mempertahankan tipe lingkungan yang aman. Informasi ini hilang (berdasarkan instance objek) saat runtime, dan daftar hanya berisi 'Objek'.

Anda BISA mencari tahu sedikit tentang kelas, apa jenisnya dapat ditentukan oleh, tetapi biasanya ini hanya apa saja yang memperluas "Objek", yaitu apa saja. Jika Anda mendefinisikan tipe sebagai

class <A extends MyClass> AClass {....}

AClass.class hanya akan berisi fakta bahwa parameter A dibatasi oleh MyClass, tetapi lebih dari itu, tidak ada cara untuk mengatakannya.

Falstro
sumber
1
Itu akan benar jika kelas konkret generik tidak ditentukan; dalam contohnya, dia secara eksplisit menyatakan daftar sebagai List<Integer>dan List<String>; dalam kasus tersebut, tipe penghapusan tidak berlaku.
Haroldo_OK
13

Jenis generik koleksi seharusnya hanya masalah jika itu benar-benar memiliki objek di dalamnya, bukan? Jadi bukankah lebih mudah melakukannya:

Collection<?> myCollection = getUnknownCollectionFromSomewhere();
Class genericClass = null;
Iterator it = myCollection.iterator();
if (it.hasNext()){
    genericClass = it.next().getClass();
}
if (genericClass != null) { //do whatever we needed to know the type for

Tidak ada yang namanya tipe generik dalam runtime, tetapi objek di dalam saat runtime dijamin menjadi tipe yang sama dengan generik yang dideklarasikan, jadi cukup mudah hanya untuk menguji kelas item sebelum kita memprosesnya.

Hal lain yang dapat Anda lakukan adalah hanya memproses daftar untuk mendapatkan anggota yang tipe yang tepat, mengabaikan orang lain (atau memprosesnya secara berbeda).

Map<Class<?>, List<Object>> classObjectMap = myCollection.stream()
    .filter(Objects::nonNull)
    .collect(Collectors.groupingBy(Object::getClass));

// Process the list of the correct class, and/or handle objects of incorrect
// class (throw exceptions, etc). You may need to group subclasses by
// filtering the keys. For instance:

List<Number> numbers = classObjectMap.entrySet().stream()
        .filter(e->Number.class.isAssignableFrom(e.getKey()))
        .flatMap(e->e.getValue().stream())
        .map(Number.class::cast)
        .collect(Collectors.toList());

Ini akan memberi Anda daftar semua item yang kelasnya adalah subclass Numberyang kemudian dapat Anda proses sesuai kebutuhan. Sisa item disaring ke daftar lain. Karena mereka ada di peta, Anda dapat memprosesnya sesuai keinginan, atau mengabaikannya.

Jika Anda ingin mengabaikan item dari kelas lain sekaligus, itu menjadi jauh lebih sederhana:

List<Number> numbers = myCollection.stream()
    .filter(Number.class::isInstance)
    .map(Number.class::cast)
    .collect(Collectors.toList());

Anda bahkan dapat membuat metode utilitas untuk memastikan bahwa daftar berisi HANYA item yang cocok dengan kelas tertentu:

public <V> List<V> getTypeSafeItemList(Collection<Object> input, Class<V> cls) {
    return input.stream()
            .filter(cls::isInstance)
            .map(cls::cast)
            .collect(Collectors.toList());
}
Steve K.
sumber
5
Dan jika Anda memiliki koleksi kosong? Atau jika Anda memiliki Koleksi <Object> dengan Integer dan String?
Falci
Kontrak untuk kelas hanya bisa mengharuskan daftar objek final diteruskan, dan bahwa pemanggilan metode akan mengembalikan nol jika daftar tersebut nol, kosong, atau jika jenis daftar tidak valid.
ggb667
1
Dalam kasus koleksi kosong, aman untuk menetapkan ke semua jenis daftar saat runtime, karena tidak ada apa pun di dalamnya, dan setelah penghapusan tipe terjadi, daftar adalah daftar adalah daftar. Karena itu, saya tidak dapat memikirkan contoh di mana jenis koleksi kosong akan penting.
Steve K
Yah itu "bisa jadi masalah" jika Anda memegang referensi di utas dan memprosesnya setelah objek mulai muncul dalam skenario konsumen produsen. Jika daftar memiliki jenis objek yang salah, hal-hal buruk dapat terjadi. Saya kira untuk mencegah Anda harus menghentikan pemrosesan dengan tidur sampai ada setidaknya satu item dalam daftar sebelum melanjutkan.
ggb667
Jika Anda memiliki skenario produsen / konsumen seperti itu, rencanakan daftar untuk memiliki jenis generik tertentu tanpa yakin apa yang dapat dimasukkan ke dalam daftar adalah desain yang buruk. Alih-alih Anda akan mendeklarasikannya sebagai koleksi Objectdan ketika Anda menariknya keluar dari daftar, Anda akan memberikannya kepada penangan yang tepat berdasarkan jenisnya.
Steve K
11

Untuk menemukan tipe generik dari satu bidang:

((Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0]).getSimpleName()
Mohsen Kashi
sumber
10

Jika Anda perlu mendapatkan tipe generik dari tipe yang dikembalikan, saya menggunakan pendekatan ini ketika saya perlu menemukan metode di kelas yang mengembalikan a Collectiondan kemudian mengakses tipe generiknya:

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;

public class Test {

    public List<String> test() {
        return null;
    }

    public static void main(String[] args) throws Exception {

        for (Method method : Test.class.getMethods()) {
            Class returnClass = method.getReturnType();
            if (Collection.class.isAssignableFrom(returnClass)) {
                Type returnType = method.getGenericReturnType();
                if (returnType instanceof ParameterizedType) {
                    ParameterizedType paramType = (ParameterizedType) returnType;
                    Type[] argTypes = paramType.getActualTypeArguments();
                    if (argTypes.length > 0) {
                        System.out.println("Generic type is " + argTypes[0]);
                    }
                }
            }
        }

    }

}

Output ini:

Jenis generik adalah kelas java.lang.String

Aram Kocharyan
sumber
6

Memperluas jawaban Steve K:

/** 
* Performs a forced cast.  
* Returns null if the collection type does not match the items in the list.
* @param data The list to cast.
* @param listType The type of list to cast to.
*/
static <T> List<? super T> castListSafe(List<?> data, Class<T> listType){
    List<T> retval = null;
    //This test could be skipped if you trust the callers, but it wouldn't be safe then.
    if(data!=null && !data.isEmpty() && listType.isInstance(data.iterator().next().getClass())) {
        @SuppressWarnings("unchecked")//It's OK, we know List<T> contains the expected type.
        List<T> foo = (List<T>)data;
        return retval;
    }
    return retval;
}
Usage:

protected WhateverClass add(List<?> data) {//For fluant useage
    if(data==null) || data.isEmpty(){
       throw new IllegalArgumentException("add() " + data==null?"null":"empty" 
       + " collection");
    }
    Class<?> colType = data.iterator().next().getClass();//Something
    aMethod(castListSafe(data, colType));
}

aMethod(List<Foo> foo){
   for(Foo foo: List){
      System.out.println(Foo);
   }
}

aMethod(List<Bar> bar){
   for(Bar bar: List){
      System.out.println(Bar);
   }
}
ggb667
sumber
Masalah yang sama dengan jawaban Steve K: Bagaimana jika daftar itu kosong? Bagaimana jika daftar ini bertipe <Object> yang berisi String dan Integer? Kode Anda berarti bahwa Anda hanya dapat menambahkan lebih banyak String jika item pertama dalam daftar adalah string, dan tidak ada bilangan bulat sama sekali ...
subrunner
Jika daftar kosong Anda kurang beruntung. Jika daftar itu adalah Objek dan sebenarnya berisi Obyek Anda OK, tetapi jika memiliki campuran hal Anda kurang beruntung. Penghapusan berarti ini sebaik yang bisa Anda lakukan. Penghapusan kurang dalam pendapat saya. Konsekuensi dari itu sekaligus kurang dipahami dan tidak cukup untuk menangani kasus penggunaan yang menarik dan diinginkan yang menjadi perhatian khusus. Tidak ada yang mencegah membuat kelas yang bisa ditanyakan jenis apa yang diperlukan, tetapi itu bukan bagaimana kelas built in bekerja. Koleksi seharusnya tahu apa yang dikandungnya, tapi sayang ...
ggb667
4

Saat runtime, tidak, Anda tidak bisa.

Namun melalui refleksi parameter tipe dapat diakses. Mencoba

for(Field field : this.getDeclaredFields()) {
    System.out.println(field.getGenericType())
}

Metode getGenericType()mengembalikan objek Type. Dalam hal ini, ini akan menjadi turunan dari ParametrizedType, yang pada gilirannya memiliki metode getRawType()(yang akan berisi List.class, dalam kasus ini) dan getActualTypeArguments(), yang akan mengembalikan array (dalam hal ini, panjang satu, berisi salah satu String.classatau Integer.class).

Scott Morrison
sumber
2
dan apakah itu berfungsi untuk parameter yang diterima oleh metode, bukan bidang kelas ???
opensas
@opensas Anda dapat menggunakan method.getGenericParameterTypes()untuk mendapatkan tipe parameter metode yang dideklarasikan.
Radiodef
4

Punya masalah yang sama, tapi saya menggunakan instanceof. Apakah dengan cara ini:

List<Object> listCheck = (List<Object>)(Object) stringList;
    if (!listCheck.isEmpty()) {
       if (listCheck.get(0) instanceof String) {
           System.out.println("List type is String");
       }
       if (listCheck.get(0) instanceof Integer) {
           System.out.println("List type is Integer");
       }
    }
}

Ini melibatkan penggunaan gips yang tidak dicentang sehingga hanya melakukan ini ketika Anda tahu itu adalah daftar, dan apa jenisnya.

Andy
sumber
3

Umumnya tidak mungkin, karena List<String>danList<Integer> berbagi kelas runtime yang sama.

Anda mungkin dapat merefleksikan tipe yang dideklarasikan dari field yang menyimpan daftar, meskipun (jika tipe yang dideklarasikan itu sendiri tidak merujuk pada parameter tipe yang nilainya tidak Anda ketahui).

meriton
sumber
2

Seperti yang dikatakan orang lain, satu-satunya jawaban yang benar adalah tidak, tipe telah dihapus.

Jika daftar memiliki jumlah elemen yang tidak nol, Anda bisa menyelidiki tipe elemen pertama (misalnya menggunakan metode getClass). Itu tidak akan memberi tahu Anda tipe generik dari daftar, tetapi akan masuk akal untuk menganggap bahwa tipe generik adalah beberapa superclass dari tipe dalam daftar.

Saya tidak akan menganjurkan pendekatan itu, tetapi mungkin itu berguna.

Chris Arguin
sumber
Itu akan benar jika kelas konkret generik tidak ditentukan; dalam contohnya, dia secara eksplisit menyatakan daftar sebagai List<Integer>dan List<String>; dalam kasus tersebut, tipe penghapusan tidak berlaku.
Haroldo_OK
2
import org.junit.Assert;
import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class GenericTypeOfCollectionTest {
    public class FormBean {
    }

    public class MyClazz {
        private List<FormBean> list = new ArrayList<FormBean>();
    }

    @Test
    public void testName() throws Exception {
        Field[] fields = MyClazz.class.getFields();
        for (Field field : fields) {
            //1. Check if field is of Collection Type
            if (Collection.class.isAssignableFrom(field.getType())) {
                //2. Get Generic type of your field
                Class fieldGenericType = getFieldGenericType(field);
                //3. Compare with <FromBean>
                Assert.assertTrue("List<FormBean>",
                  FormBean.class.isAssignableFrom(fieldGenericType));
            }
        }
    }

    //Returns generic type of any field
    public Class getFieldGenericType(Field field) {
        if (ParameterizedType.class.isAssignableFrom(field.getGenericType().getClass())) {
            ParameterizedType genericType =
             (ParameterizedType) field.getGenericType();
            return ((Class)
              (genericType.getActualTypeArguments()[0])).getSuperclass();
        }
        //Returns dummy Boolean Class to compare with ValueObject & FormBean
        return new Boolean(false).getClass();
    }
}
Ashish
sumber
1
apa getFieldGenericTypefieldGenericTypeitu tidak dapat ditemukan
shareef
0

Gunakan Reflection untuk mendapatkan Fieldini maka Anda bisa melakukan: field.genericTypeuntuk mendapatkan jenis yang berisi informasi tentang generik juga.

Siddharth Shishulkar
sumber
Pertimbangkan untuk menambahkan beberapa contoh kode, jika tidak, ini lebih cocok untuk komentar dan bukan jawaban
Alexey Kamenskiy