Получить универсальный тип java.util.List

263

У меня есть;

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

Есть ли (простой) способ получить общий тип списка?

Thizzer
источник
Чтобы иметь возможность программно проверять объект List и видеть его универсальный тип. Метод может захотеть вставить объекты на основе универсального типа коллекции. Это возможно в языках, которые реализуют обобщения во время выполнения, а не во время компиляции.
Стив Куо
4
Правильно - единственный способ разрешить обнаружение во время выполнения - это создать подклассы: вы МОЖЕТЕ фактически расширить универсальный тип, а затем использовать рефлексию найти тип поиска, который использовал подтип. Это немножко размышлений, но возможно. К сожалению, нет простого способа обеспечить использование этого подкласса.
StaxMan
1
Конечно, stringList содержит строки и целые числа integerList? Зачем делать это сложнее?
Бен Терли

Ответы:

408

Если это на самом деле поля определенного класса, то вы можете получить их с небольшой помощью отражения:

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

Вы также можете сделать это для типов параметров и типов возвращаемых методов.

Но если они находятся в той же области действия класса / метода, где вы должны знать о них, то нет смысла их знать, потому что вы уже объявили их сами.

BalusC
источник
17
Конечно, есть ситуации, когда это полезно. Например, в конфигурационных ORM-системах.
BalusC
3
.. которые могут использовать Class # getDeclaredFields (), чтобы получить все поля без необходимости знать имя поля.
BalusC
1
BalusC: Для меня это звучит как инъекционный каркас ... Такого рода использование я имел в виду.
Фальстро
1
@loolooyyyy Вместо того, чтобы использовать TypeLiteral, я рекомендую использовать TypeToken из Гуавы. github.com/google/guava/wiki/ReflectionExplained
Babyburger
1
@Oleg Ваша переменная использует <?>(подстановочный тип) вместо <Class>(тип класса). Заменить <?>ВУ <Class>или любой другой <E>.
BalusC
19

Вы можете сделать то же самое для параметров метода:

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
источник
In method.getGenericParameterTypes (); что такое метод?
Нео Рави
18

Краткий ответ: нет.

Это, вероятно, дубликат, не могу найти подходящий прямо сейчас.

Java использует так называемое стирание типов, что означает, что во время выполнения оба объекта эквивалентны. Компилятор знает, что списки содержат целые числа или строки, и поэтому могут поддерживать безопасную среду типов. Эта информация теряется (на основе экземпляра объекта) во время выполнения, и список содержит только «Объекты».

Вы МОЖЕТЕ узнать немного о классе, какими типами он может быть параметризован, но обычно это просто все, что расширяет «Объект», то есть что угодно. Если вы определяете тип как

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

AClass.class будет содержать только тот факт, что параметр A ограничен MyClass, но более того, невозможно сказать.

falstro
источник
1
Это будет верно, если конкретный класс универсального не указан; в своем примере он явно объявляет списки как List<Integer>и List<String>; в этих случаях стирание типа не применяется.
Haroldo_OK
13

Универсальный тип коллекции должен иметь значение только в том случае, если в ней есть объекты, верно? Так не проще ли просто сделать:

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

Универсального типа во время выполнения не существует, но объекты внутри во время выполнения гарантированно совпадают с типом объявленного универсального, поэтому достаточно просто проверить класс элемента перед его обработкой.

Еще одна вещь, которую вы можете сделать, - это просто обработать список, чтобы получить членов правильного типа, игнорируя других (или обрабатывая их по-другому).

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

Это даст вам список всех элементов, чьи классы были подклассами, Numberкоторые вы можете затем обрабатывать по мере необходимости. Остальные элементы были отфильтрованы в другие списки. Поскольку они находятся на карте, вы можете обрабатывать их по своему усмотрению или игнорировать их.

Если вы хотите полностью игнорировать элементы других классов, это становится намного проще:

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

Вы даже можете создать служебный метод, чтобы гарантировать, что список содержит ТОЛЬКО те элементы, которые соответствуют определенному классу:

public <V> List<V> getTypeSafeItemList(Collection<Object> input, Class<V> cls) {
    return input.stream()
            .filter(cls::isInstance)
            .map(cls::cast)
            .collect(Collectors.toList());
}
Стив К
источник
5
А если у вас есть пустая коллекция? Или если у вас есть Collection <Object> с целым числом и строкой?
Falci
Контракт для класса может требовать передачи только списков конечных объектов, и что вызов метода вернет ноль, если список пуст, пуст или если тип списка недопустим.
ggb667
1
В случае пустой коллекции, можно безопасно назначить любому типу списка во время выполнения, потому что в нем ничего нет, и после стирания типа Список - это Список - это Список. Из-за этого я не могу вспомнить ни одного случая, когда будет иметь значение тип пустой коллекции.
Стив К
Ну, это «может иметь значение», если вы удерживаете ссылку в потоке и обрабатываете ее, как только объекты начинают появляться в сценарии с продюсером. Если в списке указаны неправильные типы объектов, могут произойти плохие вещи. Я полагаю, чтобы предотвратить остановку обработки во время сна, пока в списке не останется хотя бы один элемент, прежде чем продолжить.
ggb667
Если у вас есть сценарий типа производитель / потребитель, то планирование списка, имеющего определенный общий тип, не будучи уверенным в том, что может быть добавлено в список, в любом случае является плохим дизайном. Вместо этого вы объявляете его как коллекцию Objects, а когда вы вытаскиваете их из списка, вы передаете их соответствующему обработчику в зависимости от их типа.
Стив К
11

Для нахождения универсального типа одного поля:

((Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0]).getSimpleName()
Мохсен Каши
источник
10

Если вам нужно получить универсальный тип возвращаемого типа, я использовал этот подход, когда мне нужно было найти методы в классе, который возвратил a, Collectionи затем получить доступ к их универсальным типам:

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

    }

}

Это выводит:

Общий тип это класс java.lang.String

Арам Кочарян
источник
6

Расширяя ответ Стива К:

/** 
* 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
источник
Те же проблемы, что и в ответе Стива К. Что, если список пуст? Что если список имеет тип <Object>, который содержит строку и целое число? Ваш код означает, что вы можете добавлять больше строк только в том случае, если первый элемент в списке является строкой, и вообще не содержит целых чисел ...
subrunner
Если список пуст, вам не повезло. Если в списке есть Объект, и он на самом деле содержит Объект, то вы в порядке, но если в нем есть что-то смешанное, вам не повезло. Стирание означает, что это так хорошо, как вы можете сделать. На мой взгляд, Erasure не хватает. Последствия этого не всегда понятны и недостаточны для решения интересных и желательных случаев использования типичных проблем. Ничто не мешает создать класс, который можно спросить, какой тип он принимает, но это просто не то, как работают встроенные классы. Коллекции должны знать, что они содержат, но увы ...
ggb667
4

Во время выполнения нет, вы не можете.

Однако с помощью отражения параметры типа являются доступными. Пытаться

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

Метод getGenericType()возвращает объект Type. В этом случае это будет экземпляр класса ParametrizedType, который, в свою очередь, имеет методы getRawType()(которые List.classв этом случае будут содержать ) и getActualTypeArguments()который будет возвращать массив (в данном случае, длины один, содержащий либо String.classили Integer.class).

Скотт Моррисон
источник
2
и это работает для параметров, полученных методом вместо полей класса ???
открывается
@opensas Вы можете использовать method.getGenericParameterTypes()для получения объявленных типов параметров метода.
Radiodef
4

У меня была та же проблема, но я использовал instanceof. Сделал это так:

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

Это включает использование непроверенных приведений, поэтому делайте это только тогда, когда вы знаете, что это список, и какой тип он может быть.

Энди
источник
3

Как правило, невозможно, потому что List<String>и List<Integer>совместно использовать один и тот же класс времени выполнения.

Возможно, вы сможете отразить объявленный тип поля, содержащего список (если объявленный тип сам по себе не ссылается на параметр типа, значение которого вы не знаете).

Meriton
источник
2

Как уже говорили другие, единственный правильный ответ - нет, тип был стерт.

Если список содержит ненулевое количество элементов, вы можете исследовать тип первого элемента (например, используя его метод getClass). Это не скажет вам универсальный тип списка, но было бы разумно предположить, что универсальный тип был неким суперклассом типов в списке.

Я бы не защищал такой подход, но в безвыходном положении он может быть полезен.

Крис Аргуин
источник
Это будет верно, если конкретный класс универсального не указан; в своем примере он явно объявляет списки как List<Integer>и List<String>; в этих случаях стирание типа не применяется.
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
источник
1
что getFieldGenericTypefieldGenericTypeэто не может быть найдено
Шариф
0

Тип стирается, поэтому вы не сможете. См. Http://en.wikipedia.org/wiki/Type_erasure и http://en.wikipedia.org/wiki/Generics_in_Java#Type_erasure.

peter.murray.rust
источник
Это будет верно, если конкретный класс универсального не указан; в своем примере он явно объявляет списки как List<Integer>и List<String>; в этих случаях стирание типа не применяется.
Haroldo_OK
0

Используйте Reflection, чтобы получить Fieldих, тогда вы можете просто: field.genericTypeполучить тип, который также содержит информацию об общем.

Сиддхарт Шишулкар
источник
Попробуйте добавить пример кода, иначе он больше подходит для комментария, а не ответа
Алексей Каменский