รับ java.util.List ประเภททั่วไป


262

ฉันมี;

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

มีวิธี (ง่าย) ในการดึงประเภททั่วไปของรายการหรือไม่?


เพื่อให้สามารถตรวจสอบรายการวัตถุโดยทางโปรแกรมและดูชนิดทั่วไป วิธีการอาจต้องการแทรกวัตถุตามชนิดทั่วไปของคอลเลกชัน สิ่งนี้มีความเป็นไปได้ในภาษาที่ใช้งาน Generics ณ รันไทม์แทนการรวบรวมเวลา
Steve Kuo

4
ขวา - เกี่ยวกับวิธีเดียวที่จะอนุญาตให้มีการตรวจจับรันไทม์ได้โดยการแบ่งคลาสย่อย: คุณสามารถขยายประเภททั่วไปแล้วใช้การประกาศการค้นหาชนิดการสะท้อนที่ใช้ชนิดย่อย นี่เป็นภาพสะท้อนนิดหน่อย แต่เป็นไปได้ น่าเสียดายที่ไม่มีวิธีที่ง่ายในการบังคับใช้ว่าต้องใช้คลาสย่อยทั่วไป
StaxMan

1
stringList แน่นอนมีสตริงและจำนวนเต็ม integerList? ทำไมจึงซับซ้อนกว่านี้?
Ben Thurley

คำตอบ:


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
.. ซึ่งสามารถใช้ Class # getDeclaredFields () เพื่อรับฟิลด์ทั้งหมดโดยไม่จำเป็นต้องรู้ชื่อฟิลด์
BalusC

1
BalusC: ฟังดูเหมือนกรอบการฉีดสำหรับฉัน .. นั่นคือการใช้งานที่ฉันตั้งใจไว้
falstro

1
@loolooyyyy แทนที่จะใช้ TypeLiteral ฉันแนะนำให้ใช้ TypeToken จาก Guava 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

ใน method.getGenericParameterTypes (); วิธีการคืออะไร?
Neo Ravi

18

คำตอบสั้น ๆ : ไม่

นี่อาจเป็นสิ่งที่ซ้ำกันไม่สามารถหาที่เหมาะสมได้ในขณะนี้

Java ใช้สิ่งที่เรียกว่าการลบประเภทซึ่งหมายความว่า ณ รันไทม์วัตถุทั้งสองจะเทียบเท่า คอมไพเลอร์รู้ว่ารายการมีจำนวนเต็มหรือสตริงและสามารถรักษาสภาพแวดล้อมที่ปลอดภัยประเภท ข้อมูลนี้จะหายไป (บนพื้นฐานอินสแตนซ์ของวัตถุ) ที่รันไทม์และรายการประกอบด้วย 'วัตถุ' เท่านั้น

คุณสามารถหาข้อมูลเล็กน้อยเกี่ยวกับชั้นเรียนสิ่งที่ประเภทมันอาจจะ parametrized โดย แต่ปกตินี้เป็นเพียงสิ่งที่ขยาย "วัตถุ" คืออะไร หากคุณกำหนดประเภทเป็น

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

ไม่มีประเภททั่วไปใน runtime แต่วัตถุภายในที่ runtime รับประกันว่าเป็นประเภทเดียวกับประกาศทั่วไปดังนั้นจึงง่ายพอที่จะทดสอบคลาสของรายการก่อนที่เราจะประมวลผล

อีกสิ่งหนึ่งที่คุณสามารถทำได้คือเพียงดำเนินการรายการเพื่อให้สมาชิกที่เป็นคนประเภทที่ถูกต้องไม่สนใจผู้อื่น

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> ด้วยจำนวนเต็มและสตริง?
Falci

สัญญาสำหรับคลาสนั้นอาจต้องการเพียงรายการของวัตถุสุดท้ายเท่านั้นที่จะถูกส่งเข้ามาและการเรียกเมธอดจะส่งคืนค่าว่างหากรายการนั้นว่างเปล่าว่างเปล่าหรือถ้าประเภทรายการนั้นไม่ถูกต้อง
ggb667

1
ในกรณีของคอลเลกชันที่ว่างเปล่ามันปลอดภัยที่จะกำหนดให้กับชนิดรายการใด ๆ ที่รันไทม์เนื่องจากไม่มีอะไรอยู่ในนั้นและหลังจากการลบประเภทเกิดขึ้นรายการคือรายการคือรายการ ด้วยเหตุนี้ฉันไม่สามารถนึกถึงอินสแตนซ์ใด ๆ ที่ประเภทของคอลเลกชันที่ว่างเปล่าจะมีความสำคัญ
Steve K

ถ้าคุณถือการอ้างอิงในเธรดและประมวลผลเมื่อวัตถุเริ่มปรากฏขึ้นในสถานการณ์ผู้บริโภคโปรดิวเซอร์ หากรายการมีชนิดวัตถุที่ไม่ถูกต้องสิ่งที่ไม่ดีอาจเกิดขึ้นได้ ฉันเดาว่าจะป้องกันไม่ให้คุณต้องหยุดการประมวลผลโดยการนอนจนกว่าจะมีอย่างน้อยหนึ่งรายการในรายการก่อนดำเนินการ
ggb667

หากคุณมีสถานการณ์ผู้ผลิต / ผู้บริโภคประเภทนั้นการวางแผนให้รายการมีประเภททั่วไปบางประเภทโดยไม่แน่ใจว่าสิ่งที่สามารถใส่ในรายการคือการออกแบบที่ไม่ดี แต่คุณจะประกาศว่ามันเป็นคอลเลกชันของObjects และเมื่อคุณดึงพวกเขาออกจากรายการคุณจะให้พวกเขาจัดการที่เหมาะสมตามประเภทของพวกเขา
Steve K

11

สำหรับการค้นหาประเภททั่วไปของหนึ่งฟิลด์:

((Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0]).getSimpleName()

10

หากคุณต้องการรับประเภททั่วไปของประเภทที่ส่งคืนฉันใช้วิธีนี้เมื่อฉันต้องการค้นหาวิธีการในชั้นเรียนที่คืนค่า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

ขยายคำตอบของ 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);
   }
}

ปัญหาเช่นเดียวกับคำตอบของ Steve K: ถ้ารายการว่างเปล่า เกิดอะไรขึ้นถ้ารายการเป็นประเภท <Object> ที่มีสตริงและจำนวนเต็ม? รหัสของคุณหมายความว่าคุณสามารถเพิ่ม Strings ได้ก็ต่อเมื่อรายการแรกในรายการเป็นสตริงและไม่มีจำนวนเต็มเลย ...
subrunner

หากรายการว่างเปล่าคุณไม่มีโชค หากรายการนั้นเป็นวัตถุและมันมีวัตถุจริงคุณก็โอเค แต่ถ้ามันมีสิ่งต่าง ๆ ที่คุณโชคไม่ดี การลบหมายความว่าสิ่งนี้ดีเท่าที่คุณสามารถทำได้ การลบขาดความคิดเห็นของฉัน การขยายกำลังของมันนั้นเป็นที่เข้าใจกันไม่ดีและไม่เพียงพอต่อการจัดการกับกรณีที่น่าสนใจและเป็นที่ต้องการโดยทั่วไป ไม่มีสิ่งใดป้องกันการสร้างคลาสที่สามารถถามได้ว่ามันต้องใช้รูปแบบใด แต่นั่นไม่ใช่วิธีการทำงานของคลาสที่มีอยู่ภายใน ของสะสมควรรู้ว่ามีอะไรบ้าง แต่อนิจจา ...
ggb667

4

ณ รันไทม์ไม่คุณทำไม่ได้

แต่ผ่านการสะท้อนพารามิเตอร์ชนิดมีความสามารถเข้าถึงได้ ลอง

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

วิธีการgetGenericType()ส่งกลับวัตถุประเภท ในกรณีนี้มันจะเป็นตัวอย่างของParametrizedTypeซึ่งจะมีวิธีการgetRawType()(ซึ่งจะมีList.classในกรณีนี้) และgetActualTypeArguments()ซึ่งจะส่งกลับอาร์เรย์ (ในกรณีนี้ของความยาวหนึ่งที่มีอย่างใดอย่างหนึ่งString.classหรือInteger.class)


2
และมันจะทำงานสำหรับพารามิเตอร์ที่ได้รับโดยวิธีการแทนเขตข้อมูลของชั้นเรียนหรือไม่?
opensas

@opensas คุณสามารถใช้method.getGenericParameterTypes()เพื่อรับชนิดพารามิเตอร์ที่ประกาศไว้ของเมธอด
Radiodef

4

มีปัญหาเดียวกัน แต่ฉันใช้อินสแตนซ์ของแทน ทำแบบนี้:

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 เป็นตัวอย่าง) ที่จะไม่บอกคุณประเภททั่วไปของรายการ แต่ก็มีเหตุผลที่จะถือว่าประเภททั่วไปเป็น superclass บางประเภทในรายการ

ฉันจะไม่สนับสนุนวิธีการนี้ แต่ในทางกลับกันมันอาจมีประโยชน์


สิ่งนี้จะเป็นจริงหากไม่ได้ระบุคลาสคอนกรีตทั่วไป ในตัวอย่างของเขาเขาประกาศรายการอย่างชัดเจนว่าเป็น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มันไม่สามารถพบได้
ชารีฟ

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เพื่อรับชนิดที่มีข้อมูลเกี่ยวกับ generic เช่นกัน


ลองเพิ่มโค้ดตัวอย่างมิฉะนั้นจะเหมาะสมกว่าสำหรับความคิดเห็นและไม่ใช่คำตอบ
Alexey Kamenskiy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.