Obtenga el tipo genérico de java.util.List

263

Yo tengo;

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

¿Hay alguna forma (fácil) de recuperar el tipo genérico de la lista?

Thizzer
fuente
Para poder inspeccionar programáticamente un objeto List y ver su tipo genérico. Un método puede querer insertar objetos basados ​​en el tipo genérico de la colección. Esto es posible en idiomas que implementan genéricos en tiempo de ejecución en lugar de tiempo de compilación.
Steve Kuo
44
Correcto: la única forma de permitir la detección de tiempo de ejecución es subclasificar: PUEDE extender el tipo genérico y luego usar la declaración de búsqueda de reflejo de ese subtipo utilizado. Esto es un poco de reflexión, pero posible. Desafortunadamente, no hay una manera fácil de imponer que se debe usar una subclase genérica.
StaxMan
1
¿Seguramente stringList contiene cadenas y enteros de enteros? ¿Por qué hacerlo más complicado?
Ben Thurley

Respuestas:

408

Si esos son realmente campos de una determinada clase, puede obtenerlos con una pequeña ayuda de reflexión:

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

También puede hacerlo para los tipos de parámetros y el tipo de método de retorno.

Pero si están dentro del mismo alcance de la clase / método donde necesita saber sobre ellos, entonces no tiene sentido conocerlos, porque ya los ha declarado usted mismo.

BalusC
fuente
17
Ciertamente, hay situaciones en las que esto es útil. Por ejemplo, en marcos ORM sin configuración.
BalusC
3
..que puede usar la clase # getDeclaredFields () para obtener todos los campos sin la necesidad de conocer el nombre del campo.
BalusC
1
BalusC: Eso me suena como un marco de inyección ... Ese es el tipo de uso que quise decir de todos modos.
falstro
1
@loolooyyyy En lugar de usar TypeLiteral, recomiendo usar TypeToken de Guava. github.com/google/guava/wiki/ReflectionExplained
Babyburger
1
@Oleg Su variable está usando <?>(tipo comodín) en lugar de <Class>(tipo de clase). Reemplace su <?>por <Class>o lo que sea <E>.
BalusC
19

También puede hacer lo mismo para los parámetros del método:

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
fuente
En method.getGenericParameterTypes (); ¿Qué es el método?
Neo Ravi
18

Respuesta corta: no.

Probablemente sea un duplicado, no puedo encontrar uno apropiado en este momento.

Java usa algo llamado borrado de tipo, lo que significa que en tiempo de ejecución ambos objetos son equivalentes. El compilador sabe que las listas contienen enteros o cadenas y, como tal, pueden mantener un entorno de tipo seguro. Esta información se pierde (en base a una instancia de objeto) en tiempo de ejecución, y la lista solo contiene 'Objetos'.

PUEDE averiguar un poco sobre la clase, los tipos por los que podría parametrizarse, pero normalmente esto es cualquier cosa que extienda "Objeto", es decir, cualquier cosa. Si define un tipo como

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

AClass.class solo contendrá el hecho de que el parámetro A está limitado por MyClass, pero más que eso, no hay forma de saberlo.

falstro
fuente
1
Eso será cierto si no se especifica la clase concreta del genérico; en su ejemplo, declara explícitamente las listas como List<Integer>y List<String>; en esos casos, el borrado de tipo no se aplica.
Haroldo_OK
13

El tipo genérico de una colección solo debería importar si realmente tiene objetos, ¿verdad? Entonces, ¿no es más fácil simplemente hacer:

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

No existe un tipo genérico en tiempo de ejecución, pero se garantiza que los objetos dentro del tiempo de ejecución serán del mismo tipo que el genérico declarado, por lo que es bastante fácil probar la clase del elemento antes de procesarlo.

Otra cosa que puede hacer es simplemente procesar la lista para obtener miembros que sean del tipo correcto, ignorando a otros (o procesándolos de manera diferente).

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

Esto le dará una lista de todos los elementos cuyas clases eran subclases de las Numbercuales luego puede procesar según lo necesite. El resto de los elementos se filtraron a otras listas. Debido a que están en el mapa, puede procesarlos como desee o ignorarlos.

Si desea ignorar por completo los elementos de otras clases, se vuelve mucho más simple:

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

Incluso puede crear un método de utilidad para asegurarse de que una lista contenga SOLAMENTE aquellos elementos que coincidan con una clase específica:

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
fuente
55
¿Y si tienes una colección vacía? ¿O si tiene una Colección <Object> con un Entero y una Cadena?
Falci
El contrato para la clase podría requerir que solo se pasen listas de objetos finales, y que la llamada al método devuelva nulo si la lista es nula, vacía o si el tipo de lista no es válido.
ggb667
1
En el caso de una colección vacía, es seguro asignar a cualquier tipo de lista en tiempo de ejecución, porque no hay nada en ella, y después de que se borra el tipo, una Lista es una Lista es una Lista. Debido a esto, no puedo pensar en ninguna instancia en la que el tipo de una colección vacía sea importante.
Steve K
Bueno, "podría importar" si mantienes la referencia en un hilo y la procesas una vez que los objetos comienzan a aparecer en un escenario de consumidor productor. Si la lista tuviera los tipos de objeto incorrectos, podrían suceder cosas malas. Supongo que para evitar que tenga que detener el procesamiento durmiendo hasta que haya al menos un elemento en la lista antes de continuar.
ggb667
Si tiene ese tipo de escenario de productor / consumidor, planear que la lista tenga un cierto tipo genérico sin estar seguro de lo que podría incluirse en la lista es, de todos modos, un mal diseño. En cambio, lo declararía como una colección de Objectsy cuando los retire de la lista, se los entregaría a un controlador apropiado según su tipo.
Steve K
11

Para encontrar el tipo genérico de un campo:

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

Si necesita obtener el tipo genérico de un tipo devuelto, utilicé este enfoque cuando necesitaba encontrar métodos en una clase que devolviera ay Collectionluego acceder a sus tipos genéricos:

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

    }

}

Esto produce:

El tipo genérico es la clase java.lang.String

Aram Kocharyan
fuente
6

Ampliando la respuesta de 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
fuente
Los mismos problemas que con la respuesta de Steve K: ¿Qué pasa si la lista está vacía? ¿Qué sucede si la lista es de tipo <Object> que contiene una cadena y un entero? Su código significa que solo puede agregar más cadenas si el primer elemento de la lista es una cadena y no tiene ningún número entero ...
subrunner
Si la lista está vacía, no tienes suerte. Si la lista es Objeto y en realidad contiene un Objeto, está bien, pero si tiene una combinación de cosas, no tiene suerte. Borrar significa que esto es tan bueno como puedes hacer. Borra falta en mi opinión. Sus ramificaciones son a la vez poco entendidas e insuficientes para abordar los casos de uso interesantes y deseados de preocupación típica. Nada impide crear una clase a la que se le pueda preguntar qué tipo toma, pero simplemente no es así como funcionan las clases integradas. Las colecciones deberían saber lo que contienen, pero por desgracia ...
ggb667
4

En tiempo de ejecución, no, no puedes.

Sin embargo, a través de la reflexión, los parámetros de tipo son accesibles. Tratar

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

El método getGenericType()devuelve un objeto Type. En este caso, será una instancia de ParametrizedType, que a su vez tiene métodos getRawType()(que contendrá List.class, en este caso) y getActualTypeArguments(), que devolverá una matriz (en este caso, de longitud uno, que contiene uno String.classo Integer.class).

Scott Morrison
fuente
2
¿y funciona para los parámetros recibidos por un método en lugar de los campos de una clase?
abre el
@opensas Puede usar method.getGenericParameterTypes()para obtener los tipos declarados de los parámetros de un método.
Radiodef
4

Tuve el mismo problema, pero usé instanceof en su lugar. Lo hizo de esta manera:

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

Esto implica el uso de conversiones no controladas, así que solo haz esto cuando sepas que es una lista y de qué tipo puede ser.

Andy
fuente
3

Generalmente imposible, porque List<String>y List<Integer>comparte la misma clase de tiempo de ejecución.

Sin embargo, es posible que pueda reflexionar sobre el tipo declarado del campo que contiene la lista (si el tipo declarado no se refiere a un parámetro de tipo cuyo valor no conoce).

Meriton
fuente
2

Como otros han dicho, la única respuesta correcta es no, el tipo ha sido borrado.

Si la lista tiene un número de elementos distinto de cero, puede investigar el tipo del primer elemento (utilizando su método getClass, por ejemplo). Eso no le dirá el tipo genérico de la lista, pero sería razonable suponer que el tipo genérico era una superclase de los tipos de la lista.

No recomendaría el enfoque, pero en un apuro podría ser útil.

Chris Arguin
fuente
Eso será cierto si no se especifica la clase concreta del genérico; en su ejemplo, declara explícitamente las listas como List<Integer>y List<String>; en esos casos, el borrado de tipo no se aplica.
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
fuente
1
lo getFieldGenericTypefieldGenericTypeque no se puede encontrar
shareef
0

Use Reflection para obtener Fieldestos, entonces simplemente puede hacer: field.genericTypepara obtener el tipo que contiene la información sobre genéricos también.

Siddharth Shishulkar
fuente
Considere agregar un código de ejemplo, de lo contrario, esto es más adecuado para un comentario y no una respuesta
Alexey Kamenskiy