Отримайте загальний тип java.util.List


262

У мене є;

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

Чи є (простий) спосіб отримати загальний тип списку?


Щоб мати можливість програмно перевірити об'єкт List і побачити його загальний тип. Метод може захотіти вставити об'єкти на основі загального типу колекції. Це можливо в мовах, які реалізують дженерики під час виконання замість часу компіляції.
Стів Куо

4
Праворуч - про єдиний спосіб дозволити виявлення часу виконання - це підкласифікація: ви МОЖЕ фактично розширити загальний тип, а потім, використовуючи декларування пошуку, знайдіть тип підтипу. Це зовсім небагато роздумів, але можливо. На жаль, не існує простого способу встановити, що потрібно використовувати загальний підклас.
StaxMan

1
Напевно stringList містить рядки та цілі числа: Чому це ускладнюється?
Бен Терлі

Відповіді:


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

Ви також можете це зробити для типів параметрів та методів повернення.

Але якщо вони знаходяться в тій самій області класу / методу, де вам потрібно знати про них, тоді їх немає сенсу знати, тому що ви вже заявили їх самі.


17
Звичайно, є ситуації, коли це вище корисно. Наприклад, безконфігураційні рамки ORM.
BalusC

3
..який може використовувати клас # getDeclaredFields () для отримання всіх полів без необхідності знати ім'я поля.
BalusC

1
BalusC: Це звучить як рамка для ін'єкцій. Такий спосіб використання я мав на увазі.
falstro

1
@loolooyyyy Замість використання TypeLiteral я рекомендую використовувати TypeToken від Guava. github.com/google/guava/wiki/ReflectionExplained
Babyburger

1
@Oleg Ваша змінна використовує <?>(wildcard) замість <Class>(class 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

У method.getGenericParameterTypes (); що таке метод?
Нео Раві

18

Коротка відповідь: ні.

Це, мабуть, дублікат, зараз не можна знайти відповідного.

Java використовує щось, що називається стиранням типу, що означає, що під час виконання обидва об'єкти є рівнозначними. Компілятор знає, що списки містять цілі числа чи рядки, і як такий може підтримувати безпечне для навколишнього середовища середовище. Ця інформація втрачається (на основі об'єктного примірника) під час виконання, а список містить лише "Об'єкти".

Ви МОЖЕТЕ трохи дізнатися про клас, якими типами він може бути параметризований, але зазвичай це просто все, що розширює "Об'єкт", тобто будь-що. Якщо ви визначите тип як

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

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


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
А якщо у вас порожня колекція? Або якщо у вас є колекція <Object> з цілим числом і рядком?
Фальчі

Контракт для класу може вимагати передачі лише списків кінцевих об'єктів, і що виклик методу поверне нульове значення, якщо список недійсний, порожній або якщо тип списку недійсний.
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);
   }
}

Ті ж проблеми, що і у відповіді Стіва К: Що робити, якщо список порожній? Що робити, якщо список має тип <Object>, який містить рядок і цілий число? Ваш код означає, що ви можете додавати більше рядків лише у тому випадку, якщо перший елемент у списку є рядком і взагалі немає цілих значень ...
subrunner

Якщо список порожній, вам не пощастило. Якщо у списку "Об'єкт" і він насправді містить "Об'єкт", ви все в порядку, але якщо в ньому є суміш речей, вам не пощастило. Стирання означає, що це так добре, як ви можете зробити. На мою думку стирання бракує. Наслідки цього одразу є недостатньо зрозумілими та недостатніми для вирішення цікавих та бажаних випадків використання, що викликають типовий інтерес. Ніщо не заважає створити клас, який може запитати, якого типу він приймає, але це просто не так, як працюють вбудовані класи. Колекції повинні знати, що вони містять, але на жаль ...
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>один і той же клас у час виконання.

Можливо, ви зможете відобразити задекларований тип поля, що містить список (якщо заявлений тип не посилається на параметр типу, значення якого ви не знаєте).


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

1
що getFieldGenericTypefieldGenericTypeце неможливо знайти
shareef

0

Тип стирається, тому ви не зможете. Дивіться http://en.wikipedia.org/wiki/Type_erasure та http://en.wikipedia.org/wiki/Generics_in_Java#Type_erasure


Це буде правдою, якщо конкретний клас родового не вказаний; у своєму прикладі він чітко оголошує списки як List<Integer>і List<String>; у цих випадках стирання типу не застосовується.
Haroldo_OK

0

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


Подумайте про додавання прикладу коду, інакше це більше підходить для коментаря, а не відповіді
Олексій Каменський
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.