Ottieni un tipo generico di java.util.List


263

Io ho;

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

Esiste un modo (semplice) per recuperare il tipo generico dell'elenco?


Per essere in grado di ispezionare a livello di codice un oggetto Elenco e vedere il suo tipo generico. Un metodo potrebbe voler inserire oggetti in base al tipo generico della raccolta. Ciò è possibile nei linguaggi che implementano generics in fase di esecuzione anziché in fase di compilazione.
Steve Kuo,

4
Esatto - l'unico modo per consentire il rilevamento di runtime è la sottoclasse: puoi effettivamente estendere il tipo generico e quindi usare la dichiarazione del tipo di tipo reflection usato dal sottotipo. Questo è un bel po 'di riflessione, ma possibile. Sfortunatamente non esiste un modo semplice per imporre che si debba usare una sottoclasse generica.
StaxMan,

1
Sicuramente stringList contiene stringhe e numeri interi integerList? Perché renderlo più complicato?
Ben Thurley,

Risposte:


408

Se quelli sono effettivamente campi di una certa classe, puoi ottenerli con un piccolo aiuto di riflessione:

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

Puoi anche farlo per tipi di parametri e tipi di metodi di restituzione.

Ma se sono all'interno dello stesso ambito della classe / metodo in cui è necessario conoscerli, allora non ha senso conoscerli, perché li hai già dichiarati tu stesso.


17
Ci sono certamente situazioni in cui questo è utile. Ad esempio in framework ORM senza configurazione.
BalusC,

3
..che può usare Class # getDeclaredFields () per ottenere tutti i campi senza la necessità di conoscere il nome del campo.
BalusC,

1
BalusC: A me sembra un quadro di iniezione .. Questo è il tipo di utilizzo che intendevo comunque.
falstro,

1
@loolooyyyy Piuttosto che usare TypeLiteral, ti consiglio di usare TypeToken di Guava. github.com/google/guava/wiki/ReflectionExplained
Babyburger

1
@Oleg La variabile utilizza <?>(tipo jolly) anziché <Class>(tipo di classe). Sostituisci il tuo <?>con <Class>o altro <E>.
BalusC

19

Puoi fare lo stesso anche per i parametri del metodo:

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

In method.getGenericParameterTypes (); qual è il metodo?
Neo Ravi,

18

Risposta breve: no.

Questo è probabilmente un duplicato, non riesco a trovarne uno appropriato in questo momento.

Java utilizza una cosiddetta cancellazione del tipo, il che significa che in fase di esecuzione entrambi gli oggetti sono equivalenti. Il compilatore sa che le liste contengono numeri interi o stringhe e come tali possono mantenere un ambiente sicuro di tipo. Questa informazione viene persa (in base all'istanza di un oggetto) in fase di esecuzione e l'elenco contiene solo "Oggetti".

PUOI scoprire qualcosa sulla classe, su quali tipi potrebbe essere parametrizzata, ma normalmente questo è tutto ciò che estende "Object", cioè qualsiasi cosa. Se si definisce un tipo come

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

AClass.class conterrà solo il fatto che il parametro A è limitato da MyClass, ma soprattutto non c'è modo di dirlo.


1
Ciò sarà vero se non viene specificata la classe concreta del generico; nel suo esempio, sta dichiarando esplicitamente le liste come List<Integer>e List<String>; in questi casi, la cancellazione del tipo non si applica.
Haroldo_OK

13

Il tipo generico di una raccolta dovrebbe avere importanza solo se contiene effettivamente degli oggetti, giusto? Quindi non è più semplice fare semplicemente:

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

Non esiste un tipo generico in runtime, ma gli oggetti all'interno in runtime sono garantiti dello stesso tipo del generico dichiarato, quindi è abbastanza semplice testare la classe dell'articolo prima di elaborarlo.

Un'altra cosa che puoi fare è semplicemente elaborare l'elenco per ottenere membri del tipo giusto, ignorandone altri (o elaborandoli in modo diverso).

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

Questo ti darà un elenco di tutti gli elementi le cui classi erano sottoclassi di Numbercui puoi quindi elaborare secondo le tue necessità. Il resto degli elementi è stato filtrato in altri elenchi. Poiché si trovano nella mappa, è possibile elaborarli come desiderato o ignorarli.

Se vuoi ignorare del tutto gli elementi di altre classi, diventa molto più semplice:

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

È anche possibile creare un metodo di utilità per assicurare che un elenco contenga SOLO quegli elementi che corrispondono a una classe specifica:

public <V> List<V> getTypeSafeItemList(Collection<Object> input, Class<V> cls) {
    return input.stream()
            .filter(cls::isInstance)
            .map(cls::cast)
            .collect(Collectors.toList());
}

5
E se hai una collezione vuota? O se hai una raccolta <oggetto> con un numero intero e una stringa?
Falci,

Il contratto per la classe potrebbe richiedere il passaggio solo di elenchi di oggetti finali e che la chiamata al metodo restituirà null se l'elenco è null, vuoto o se il tipo di elenco non è valido.
ggb667,

1
Nel caso di una raccolta vuota, è sicuro assegnare a qualsiasi tipo di elenco in fase di esecuzione, poiché non contiene nulla e, dopo che si verifica la cancellazione del tipo, un Elenco è un Elenco è un Elenco. Per questo motivo, non riesco a pensare a nessuna istanza in cui il tipo di una raccolta vuota avrà importanza.
Steve K,

Beh, "potrebbe importare" se ti trattenessi sul riferimento in un thread e lo elaborassi una volta che gli oggetti hanno iniziato a comparire in uno scenario di consumo del produttore. Se la lista avesse i tipi di oggetti sbagliati potrebbero accadere cose brutte. Immagino che dovresti interrompere l'elaborazione dormendo fino a quando non ci fosse almeno un elemento nell'elenco prima di continuare.
ggb667,

Se hai quel tipo di scenario produttore / consumatore, la pianificazione dell'elenco deve avere un certo tipo generico senza essere sicuro che ciò che potrebbe essere inserito nell'elenco sia comunque un progetto errato. Invece lo dichiareresti come una raccolta di Objects e quando li tirerai fuori dall'elenco, li daresti a un gestore appropriato in base al loro tipo.
Steve K,

11

Per trovare un tipo generico di un campo:

((Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0]).getSimpleName()

10

Se è necessario ottenere il tipo generico di un tipo restituito, ho utilizzato questo approccio quando avevo bisogno di trovare metodi in una classe che ha restituito un Collectione quindi accedere ai loro tipi generici:

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

    }

}

Questo produce:

Il tipo generico è la classe java.lang.String


6

Espandendo la risposta di 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);
   }
}

Stessi problemi della risposta di Steve K: cosa succede se l'elenco è vuoto? Che cosa succede se l'elenco è di tipo <oggetto> che contiene una stringa e un numero intero? Il tuo codice significa che puoi aggiungere più stringhe solo se il primo elemento nell'elenco è una stringa e nessun numero intero ...
subrunner,

Se l'elenco è vuoto, sei sfortunato. Se la lista è Object e contiene effettivamente un Object, allora sei OK, ma se ha un mix di cose sei sfortunato. La cancellazione significa che questo è il massimo che puoi fare. La cancellazione è carente secondo me. Le sue ramificazioni sono allo stesso tempo poco comprese e insufficienti per affrontare i casi d'uso interessanti e desiderati di interesse tipico. Nulla impedisce la creazione di una classe alla quale si può chiedere che tipo prende, ma semplicemente non è come funzionano le classi integrate. Le collezioni dovrebbero sapere cosa contengono, ma purtroppo ...
ggb667,

4

In fase di esecuzione, no, non puoi.

Tuttavia, tramite la riflessione, i parametri del tipo sono accessibili. Provare

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

Il metodo getGenericType()restituisce un oggetto Type. In questo caso, sarà un'istanza di ParametrizedType, che a sua volta ha metodi getRawType()(che conterrà List.class, in questo caso) e getActualTypeArguments(), che restituirà un array (in questo caso, di lunghezza uno, contenente String.classo Integer.class).


2
e funziona per i parametri ricevuti da un metodo anziché per i campi di una classe ???
apre il

@opensas È possibile utilizzare method.getGenericParameterTypes()per ottenere i tipi dichiarati dei parametri di un metodo.
Radiodef,

4

Ho avuto lo stesso problema, ma ho usato invece instanceof. Ha fatto così:

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

Ciò implica l'uso di cast non controllati, quindi fallo solo quando sai che è un elenco e che tipo può essere.


3

Generalmente impossibile, perché List<String>e List<Integer>condividere la stessa classe di runtime.

Tuttavia, potresti essere in grado di riflettere sul tipo dichiarato del campo che contiene l'elenco (se il tipo dichiarato non si riferisce a un parametro di tipo di cui non conosci il valore).


2

Come altri hanno già detto, l'unica risposta corretta è no, il tipo è stato cancellato.

Se l'elenco ha un numero di elementi diverso da zero, è possibile esaminare il tipo del primo elemento (utilizzando il metodo getClass, ad esempio). Ciò non ti dirà il tipo generico dell'elenco, ma sarebbe ragionevole supporre che il tipo generico fosse una superclasse dei tipi nell'elenco.

Non difenderei l'approccio, ma in un certo senso potrebbe essere utile.


Ciò sarà vero se non viene specificata la classe concreta del generico; nel suo esempio, sta dichiarando esplicitamente le liste come List<Integer>e List<String>; in questi casi, la cancellazione del tipo non si applica.
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();
    }
}

1
che cosa getFieldGenericTypefieldGenericTypenon può essere trovato
Shareef


0

Usa Reflection per ottenere Fieldquesti, quindi puoi semplicemente fare: field.genericTypeper ottenere il tipo che contiene anche le informazioni su generico.


Prendi in considerazione l'aggiunta di un codice di esempio, altrimenti è più adatto a un commento e non a una risposta
Alexey Kamenskiy,
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.