Uzyskaj ogólny typ pliku java.util.List

262

Mam;

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

Czy istnieje (łatwy) sposób na uzyskanie ogólnego typu listy?

Thizzer
źródło
Aby móc programowo sprawdzić obiekt List i zobaczyć jego ogólny typ. Metoda może chcieć wstawiać obiekty w oparciu o ogólny typ kolekcji. Jest to możliwe w językach, które implementują generyczne w czasie wykonywania zamiast czasu kompilacji.
Steve Kuo,
4
Racja - jedynym sposobem na umożliwienie wykrycia środowiska wykonawczego jest podklasa: w rzeczywistości MOŻNA rozszerzyć typ ogólny, a następnie użyć deklaracji typu odbicia znalezionego, która została użyta. To trochę refleksji, ale możliwe. Niestety nie ma łatwego sposobu na wymuszenie, że należy użyć ogólnej podklasy.
StaxMan,
1
Z pewnością stringList zawiera ciągi i liczby całkowite? Po co to komplikować?
Ben Thurley,

Odpowiedzi:

408

Jeśli faktycznie są to pola pewnej klasy, możesz je uzyskać z niewielką pomocą refleksji:

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

Możesz to również zrobić dla typów parametrów i zwracanych metod.

Ale jeśli znajdują się w tym samym zakresie klasy / metody, w którym musisz o nich wiedzieć, to nie ma sensu ich znać, ponieważ już je sam zadeklarowałeś.

BalusC
źródło
17
Z pewnością istnieją sytuacje, w których jest to powyżej przydatne. Na przykład w bezkonfiguracyjnych ramach ORM.
BalusC,
3
.. który może użyć klasy # getDeclaredFields (), aby uzyskać wszystkie pola bez konieczności znajomości nazwy pola.
BalusC,
1
BalusC: Dla mnie to brzmi jak szkielet do wstrzykiwań.
falstro
1
@loolooyyyy Zamiast używać TypeLiteral, zalecam używanie TypeToken z Guava. github.com/google/guava/wiki/ReflectionExplained
Babyburger
1
@Oleg Twoja zmienna używa <?>(typ wieloznaczny) zamiast <Class>(typ klasy). Wymień <?>By <Class>lub cokolwiek <E>.
BalusC
19

Możesz zrobić to samo dla parametrów metody:

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
źródło
W method.getGenericParameterTypes (); jaka jest metoda
Neo Ravi
18

Krótka odpowiedź: nie.

To prawdopodobnie duplikat, nie można teraz znaleźć odpowiedniego.

Java używa czegoś o nazwie kasowanie typu, co oznacza, że ​​w czasie wykonywania oba obiekty są równoważne. Kompilator wie, że listy zawierają liczby całkowite lub ciągi i jako takie mogą utrzymywać bezpieczne środowisko typu. Informacje te są tracone (na podstawie instancji obiektu) w czasie wykonywania, a lista zawiera tylko „Obiekty”.

Możesz dowiedzieć się trochę o klasie, jakie typy może być parametryzowane, ale zwykle jest to po prostu wszystko, co rozszerza „Obiekt”, tj. Cokolwiek. Jeśli zdefiniujesz typ jako

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

AClass.class będzie zawierał tylko fakt, że parametr A jest ograniczony przez MyClass, ale co więcej, nie można tego stwierdzić.

falstro
źródło
1
Będzie to prawdą, jeśli konkretna klasa ogólna nie zostanie określona; w swoim przykładzie wyraźnie deklaruje listy jako List<Integer>i List<String>; w takich przypadkach usunięcie typu nie ma zastosowania.
Haroldo_OK,
13

Ogólny typ kolekcji powinien mieć znaczenie tylko wtedy, gdy rzeczywiście zawiera w sobie obiekty, prawda? Czy nie jest łatwiej zrobić:

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

W środowisku wykonawczym nie ma czegoś takiego jak rodzaj ogólny, ale na pewno obiekty wewnątrz środowiska wykonawczego są tego samego typu co deklarowany rodzaj ogólny, więc wystarczy przetestować klasę elementu przed przetworzeniem.

Inną rzeczą, którą możesz zrobić, to po prostu przetworzyć listę, aby uzyskać członków odpowiedniego typu, ignorując innych (lub przetwarzając ich inaczej).

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

To da ci listę wszystkich przedmiotów, których klasy były podklasami, Numberktóre możesz następnie przetwarzać według potrzeb. Pozostałe elementy zostały odfiltrowane na inne listy. Ponieważ znajdują się na mapie, możesz je przetwarzać zgodnie z potrzebami lub ignorować.

Jeśli chcesz całkowicie zignorować elementy innych klas, staje się to znacznie prostsze:

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

Możesz nawet utworzyć metodę narzędzia, aby upewnić się, że lista zawiera TYLKO te elementy, które pasują do określonej klasy:

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.
źródło
5
A jeśli masz pustą kolekcję? A może masz kolekcję <Obiekt> z liczbą całkowitą i łańcuchem?
Falci
Umowa dla klasy może wymagać przekazania tylko list obiektów końcowych, a wywołanie metody zwróci null, jeśli lista jest pusta lub pusta lub jeśli typ listy jest nieprawidłowy.
ggb667,
1
W przypadku pustej kolekcji można bezpiecznie przypisać dowolny typ listy w czasie wykonywania, ponieważ nic w niej nie ma, a po usunięciu typu lista jest listą jest listą. Z tego powodu nie jestem w stanie wymyślić żadnego przypadku, w którym typ pustej kolekcji będzie miał znaczenie.
Steve K
Cóż, „może mieć znaczenie”, jeśli trzymasz referencję w wątku i przetwarzasz ją, gdy obiekty zaczną pojawiać się w scenariuszu konsumenta producenta. Jeśli lista zawiera niewłaściwe typy obiektów, mogą się zdarzyć złe rzeczy. Chyba nie pozwolę, byś musiał przerwać przetwarzanie, śpiąc, dopóki nie będzie co najmniej jednej pozycji na liście, zanim przejdziesz dalej.
ggb667,
Jeśli masz tego rodzaju scenariusz producenta / konsumenta, planowanie listy ma określony rodzaj ogólny, bez pewności, że to, co można by umieścić na liście, jest złe. Zamiast tego zadeklarujesz go jako zbiór Objects, a kiedy wyciągniesz je z listy, podasz je odpowiedniemu programowi obsługi na podstawie ich typu.
Steve K,
11

Aby znaleźć ogólny typ jednego pola:

((Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0]).getSimpleName()
Mohsen Kashi
źródło
10

Jeśli chcesz uzyskać ogólny typ zwracanego typu, zastosowałem to podejście, gdy potrzebowałem znaleźć metody w klasie, która zwróciła a, Collectiona następnie uzyskać dostęp do ich ogólnych typów:

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

    }

}

To daje:

Typ ogólny to klasa java.lang.String

Aram Kocharyan
źródło
6

Rozwijając odpowiedź Steve'a 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
źródło
Te same problemy, co w przypadku odpowiedzi Steve'a K: Co, jeśli lista jest pusta? Co jeśli lista jest typu <Obiekt>, który zawiera ciąg znaków i liczbę całkowitą? Twój kod oznacza, że ​​możesz dodać więcej Ciągów, tylko jeśli pierwszy element na liście jest ciągiem i nie ma żadnych liczb całkowitych ...
subrunner
Jeśli lista jest pusta, nie masz szczęścia. Jeśli lista jest Obiektem i faktycznie zawiera Obiekt, wszystko jest w porządku, ale jeśli zawiera wiele rzeczy, nie masz szczęścia. Kasowanie oznacza, że ​​jest to tak dobre, jak tylko możesz. Moim zdaniem brakuje skasowania. Konsekwencje tego są jednocześnie słabo zrozumiałe i niewystarczające do rozwiązania interesujących i pożądanych przypadków typowego zainteresowania. Nic nie stoi na przeszkodzie, aby stworzyć klasę, w której można zapytać, jakiego typu ona potrzebuje, ale po prostu nie tak działa klasa wbudowana. Kolekcje powinny wiedzieć, co zawierają, ale niestety ...
ggb667
4

W czasie wykonywania nie możesz.

Jednak poprzez odbicie parametry typu dostępne. Próbować

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

Metoda getGenericType()zwraca obiekt typu. W tym przypadku będzie to instancja ParametrizedType, która z kolei ma metody getRawType()(które List.classw tym przypadku będą zawierać ) i getActualTypeArguments()która zwróci tablicę (w tym przypadku o długości 1, zawierającą albo String.classlub Integer.class).

Scott Morrison
źródło
2
i czy to działa dla parametrów otrzymanych przez metodę zamiast pól klasy ???
otwiera się
@opensas Możesz użyć, method.getGenericParameterTypes()aby uzyskać zadeklarowane typy parametrów metody.
Radiodef,
4

Miałem ten sam problem, ale zamiast tego użyłem instanceof. Czy to w ten sposób:

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

Wymaga to użycia niezaznaczonych rzutów, więc rób to tylko wtedy, gdy wiesz, że jest to lista i jaki może być typ.

Andy
źródło
3

Zasadniczo niemożliwe, ponieważ List<String>i List<Integer>współużytkują tę samą klasę wykonawczą.

Możesz jednak zastanowić się nad zadeklarowanym typem pola zawierającego listę (jeśli zadeklarowany typ sam nie odnosi się do parametru typu, którego wartości nie znasz).

meriton
źródło
2

Jak powiedzieli inni, jedyną prawidłową odpowiedzią jest nie, typ został usunięty.

Jeśli lista zawiera niezerową liczbę elementów, możesz zbadać typ pierwszego elementu (na przykład używając jego metody getClass). To nie powie ci o ogólnym typie listy, ale rozsądnie byłoby założyć, że typem ogólnym była jakaś nadklasa typów na liście.

Nie opowiadałbym się za tym podejściem, ale w przypadku powiązania może być przydatne.

Chris Arguin
źródło
Będzie to prawdą, jeśli konkretna klasa ogólna nie zostanie określona; w swoim przykładzie wyraźnie deklaruje listy jako List<Integer>i List<String>; w takich przypadkach usunięcie typu nie ma zastosowania.
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
źródło
1
czego getFieldGenericTypefieldGenericTypenie można znaleźć
shareef,
0

Użyj Reflection, aby uzyskać Fielddla nich, a następnie możesz po prostu: field.genericTypeuzyskać typ, który zawiera również informacje o ogólnych.

Siddharth Shishulkar
źródło
Zastanów się nad dodaniem przykładowego kodu, w przeciwnym razie jest to bardziej odpowiednie dla komentarza, a nie odpowiedzi
Alexey Kamenskiy,