Obtenha o tipo genérico de java.util.List

262

Eu tenho;

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

Existe uma maneira (fácil) de recuperar o tipo genérico da lista?

Thizzer
fonte
Ser capaz de inspecionar programaticamente um objeto List e ver seu tipo genérico. Um método pode querer inserir objetos com base no tipo genérico da coleção. Isso é possível em idiomas que implementam genéricos em tempo de execução, em vez de tempo de compilação.
21411 Steve Kuo
4
Certo - a única maneira de permitir a detecção de tempo de execução é subclassificando: na verdade, você PODE estender o tipo genérico e, em seguida, usar a declaração de tipo de localização por reflexão usada pelo subtipo. Isso é um pouco de reflexão, mas possível. Infelizmente, não há uma maneira fácil de impor que seja necessário usar uma subclasse genérica.
StaxMan
1
Certamente stringList contém strings e integerList inteiros? Por que torná-lo mais complicado?
Ben Thurley 16/09

Respostas:

408

Se esses são realmente os campos de uma determinada classe, você pode obtê-los com uma pequena ajuda de reflexão:

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

Você também pode fazer isso para tipos de parâmetros e retornar tipos de métodos.

Mas se eles estiverem dentro do mesmo escopo da classe / método em que você precisa conhecê-los, não faz sentido conhecê-los, porque você já os declarou.

BalusC
fonte
17
Certamente, há situações em que isso é útil acima de tudo. Por exemplo, estruturas ORM sem configuração.
BalusC
3
..que pode usar a classe # getDeclaredFields () para obter todos os campos sem a necessidade de saber o nome do campo.
BalusC 21/12/2009
1
BalusC: Isso soa como uma estrutura de injeção para mim .. Esse é o tipo de uso que eu quis dizer de qualquer maneira.
falstro
1
@loolooyyyy Em vez de usar o TypeLiteral, recomendo usar o TypeToken da Guava. github.com/google/guava/wiki/ReflectionExplained
Babyburger
1
@ Oleg Sua variável está usando <?>(tipo curinga) em vez de <Class>(tipo de classe). Substitua seu <?>por <Class>ou o que quer <E>.
BalusC
19

Você também pode fazer o mesmo para os parâmetros do 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
fonte
Em method.getGenericParameterTypes (); o que é método?
Neo Ravi
18

Resposta curta: não.

Provavelmente é uma duplicata, mas não é possível encontrar uma apropriada no momento.

Java usa algo chamado apagamento de tipo, o que significa que no tempo de execução ambos os objetos são equivalentes. O compilador sabe que as listas contêm números inteiros ou seqüências de caracteres e, como tal, pode manter um ambiente seguro de tipo. Essas informações são perdidas (em uma instância de objeto) no tempo de execução e a lista contém apenas 'Objetos'.

Você PODE descobrir um pouco sobre a classe, por quais tipos ela pode ser parametrizada, mas normalmente isso é tudo o que estende "Objeto", ou seja, qualquer coisa. Se você definir um tipo como

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

AClass.class conterá apenas o fato de que o parâmetro A é delimitado por MyClass, mas mais do que isso, não há como saber.

falstro
fonte
1
Isso será verdade se a classe concreta do genérico não for especificada; no exemplo dele, ele está declarando explicitamente as listas como List<Integer>e List<String>; nesses casos, o apagamento do tipo não se aplica.
Haroldo_OK
13

O tipo genérico de uma coleção só deve importar se realmente tiver objetos, certo? Portanto, não é mais fácil apenas fazer:

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

Não existe um tipo genérico no tempo de execução, mas é garantido que os objetos contidos no tempo de execução sejam do mesmo tipo que o genérico declarado; portanto, é fácil testar a classe do item antes de processá-lo.

Outra coisa que você pode fazer é simplesmente processar a lista para obter membros do tipo certo, ignorando outros (ou processando-os de maneira 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());

Isso fornecerá uma lista de todos os itens cujas classes eram subclasses dos Numberquais você poderá processar conforme necessário. O restante dos itens foi filtrado para outras listas. Como eles estão no mapa, você pode processá-los conforme desejado ou ignorá-los.

Se você deseja ignorar itens de outras classes por completo, fica muito mais simples:

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

Você pode até criar um método utilitário para garantir que uma lista contenha APENAS os itens correspondentes a uma classe 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
fonte
5
E se você tem uma coleção vazia? Ou se você tiver uma coleção <object> com um número inteiro e uma string?
Falci 14/05
O contrato para a classe pode exigir que apenas listas de objetos finais sejam passadas e que a chamada do método retorne nulo se a lista for nula, vazia ou se o tipo de lista for inválido.
precisa saber é o seguinte
1
No caso de uma coleção vazia, é seguro atribuir a qualquer tipo de lista em tempo de execução, porque não há nada e, após o apagamento do tipo, uma Lista é uma Lista é uma Lista. Por causa disso, não consigo pensar em nenhuma instância em que o tipo de uma coleção vazia seja importante.
Steve K
Bem, "poderia importar" se você mantivesse a referência em um encadeamento e a processasse quando os objetos começassem a aparecer em um cenário de consumidor produtor. Se a lista apresentasse os tipos de objetos incorretos, coisas ruins poderiam acontecer. Acho que para impedir que você precise interromper o processamento dormindo até haver pelo menos um item na lista antes de continuar.
ggb667
Se você tiver esse tipo de cenário produtor / consumidor, o planejamento da lista para ter um determinado tipo genérico sem ter certeza de que o que poderia ser colocado na lista é de design ruim. Em vez disso, você a declararia como uma coleção de Objects e, quando as retirar da lista, as entregaria a um manipulador apropriado com base no tipo delas.
22416 Steve K
11

Para encontrar o tipo genérico de um campo:

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

Se você precisar obter o tipo genérico de um tipo retornado, usei essa abordagem quando precisei encontrar métodos em uma classe que retornasse um Collectione depois acesse seus 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]);
                    }
                }
            }
        }

    }

}

Isso gera:

Tipo genérico é a classe java.lang.String

Aram Kocharyan
fonte
6

Expandindo a resposta 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
fonte
Mesmos problemas da resposta de Steve K: E se a lista estiver vazia? E se a lista for do tipo <Object> que contém uma String e um Inteiro? Seu código significa que você só pode adicionar mais seqüências de caracteres se o primeiro item da lista for uma sequência e nenhum número inteiro ...
subrunner
Se a lista estiver vazia, você estará sem sorte. Se a lista for Objeto e realmente contiver um Objeto, você estará bem, mas se houver uma mistura de coisas, você estará sem sorte. Apagar significa que isso é tão bom quanto você pode fazer. O apagamento está faltando na minha opinião. As ramificações dele são ao mesmo tempo pouco compreendidas e insuficientes para abordar os casos de uso interessantes e desejados de interesse típico. Nada impede a criação de uma classe que pode ser perguntada sobre o tipo, mas simplesmente não é assim que as classes incorporadas funcionam. Coleções deve saber o que eles contêm, mas infelizmente ...
ggb667
4

Em tempo de execução, não, você não pode.

No entanto, através da reflexão, os parâmetros de tipo são acessíveis. Experimentar

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

O método getGenericType()retorna um objeto Type. Nesse caso, será uma instância de ParametrizedType, que por sua vez possui métodos getRawType()(que conterão List.class, nesse caso) e getActualTypeArguments()que retornará uma matriz (nesse caso, de comprimento um, contendo um String.classou Integer.class).

Scott Morrison
fonte
2
e funciona para parâmetros recebidos por um método em vez de campos de uma classe ???
opensas 26/11/2010
@opensas Você pode usar method.getGenericParameterTypes()para obter os tipos declarados dos parâmetros de um método.
Radiodef 15/08/19
4

Teve o mesmo problema, mas usei instanceof. Fez assim:

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

Isso envolve o uso de projeções não verificadas, portanto, faça isso somente quando você souber que é uma lista e que tipo pode ser.

Andy
fonte
3

Geralmente impossível, porque List<String>e List<Integer>compartilham a mesma classe de tempo de execução.

Você pode refletir sobre o tipo declarado do campo que contém a lista (se o tipo declarado não se referir a um parâmetro de tipo cujo valor você não conhece).

Meriton
fonte
2

Como outros já disseram, a única resposta correta é não, o tipo foi apagado.

Se a lista tiver um número diferente de zero de elementos, você poderá investigar o tipo do primeiro elemento (usando o método getClass, por exemplo). Isso não informa o tipo genérico da lista, mas seria razoável supor que o tipo genérico fosse uma superclasse dos tipos na lista.

Eu não defenderia a abordagem, mas em um momento difícil poderia ser útil.

Chris Arguin
fonte
Isso será verdade se a classe concreta do genérico não for especificada; no exemplo dele, ele está declarando explicitamente as listas como List<Integer>e List<String>; nesses casos, o apagamento do tipo não 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
fonte
1
o que é getFieldGenericTypefieldGenericTypeque não pode ser encontrado
shareef
0

Use o Reflection para obter os itens Fieldpara estes, então você pode apenas fazer: field.genericTypepara obter o tipo que contém as informações sobre os genéricos também.

Siddharth Shishulkar
fonte
Considere a adição de alguns códigos de exemplo, caso contrário, este é mais adequado para um comentário e não uma resposta
Alexey Kamenskiy