Java: ฉันจะได้รับตัวอักษรในชั้นเรียนจากประเภททั่วไปได้อย่างไร


194

โดยปกติแล้วฉันเคยเห็นคนใช้ตัวอักษรในชั้นเรียนเช่นนี้:

Class<Foo> cls = Foo.class;

แต่ถ้าเป็นประเภททั่วไปเช่นรายชื่อจะเป็นอย่างไร วิธีนี้ใช้ได้ผลดี แต่มีคำเตือนเนื่องจากรายการควรได้รับการกำหนดพารามิเตอร์:

Class<List> cls = List.class

ดังนั้นทำไมไม่เพิ่ม<?>? นี่ทำให้เกิดข้อผิดพลาดประเภทไม่ตรงกัน:

Class<List<?>> cls = List.class

ฉันคิดบางอย่างเช่นนี้จะทำงานได้ แต่นี่เป็นเพียงข้อผิดพลาดทางไวยากรณ์ธรรมดา ol:

Class<List<Foo>> cls = List<Foo>.class

ฉันจะได้รับClass<List<Foo>>แบบคงที่ได้อย่างไรเช่นใช้ตัวอักษรของชั้นเรียน?

ฉันสามารถใช้@SuppressWarnings("unchecked")เพื่อกำจัดคำเตือนที่เกิดจากการใช้ List แบบไม่มีพารามิเตอร์ในตัวอย่างแรกClass<List> cls = List.classแต่ฉันไม่ต้องการ

ข้อเสนอแนะใด ๆ

คำตอบ:


161

คุณไม่สามารถเนื่องจากการพิมพ์การลบออก

Java generics มีน้อยกว่าน้ำตาล syntactic สำหรับ Object casts เพื่อสาธิต:

List<Integer> list1 = new ArrayList<Integer>();
List<String> list2 = (List<String>)list1;
list2.add("foo"); // perfectly legal

อินสแตนซ์เดียวที่เก็บข้อมูลประเภททั่วไปไว้ที่รันไทม์ก็คือการField.getGenericType()สอบถามสมาชิกของคลาสผ่านการสะท้อนกลับ

ทั้งหมดนี้เป็นสาเหตุที่Object.getClass()มีลายเซ็นนี้:

public final native Class<?> getClass();

Class<?>ที่สำคัญส่วนหนึ่งเป็น

หากต้องการกล่าวอีกวิธีหนึ่งจากคำถามที่พบบ่อยของJava Generics :

ทำไมจึงไม่มีคลาสตัวอักษรสำหรับประเภทพารามิเตอร์คอนกรีต?

เนื่องจากชนิดที่กำหนดพารามิเตอร์ไม่มีการแทนค่าชนิดรันไทม์ที่แน่นอน

คลาสตัวอักษรหมายถึงClass วัตถุที่แสดงถึงประเภทที่กำหนด ยกตัวอย่างเช่นคลาสตัวอักษร String.classหมายถึงClass วัตถุที่แสดงประเภท Stringและเป็นเหมือน Classวัตถุที่ถูกส่งกลับเมื่อgetClassมีการเรียกวิธีการบน Stringวัตถุ คลาสตัวอักษรสามารถใช้สำหรับการตรวจสอบประเภทรันไทม์และการสะท้อนกลับ

ชนิดที่กำหนดพารามิเตอร์จะสูญเสียอาร์กิวเมนต์ชนิดเมื่อมีการแปลเป็นรหัสไบต์ระหว่างการคอมไพล์ในกระบวนการที่เรียกว่าการลบประเภท ในฐานะที่เป็นผลข้างเคียงของการลบประเภทการสร้างอินสแตนซ์ทั้งหมดของประเภททั่วไปจะใช้การแสดงแบบรันไทม์เดียวกันนั่นคือชนิด raw ที่สอดคล้องกัน กล่าวอีกนัยหนึ่งประเภทพารามิเตอร์ไม่ได้เป็นตัวแทนประเภทของตัวเอง จึงมีจุดในการสร้างตัวอักษรระดับเช่นไม่มีList<String>.class, List<Long>.classและList<?>.class เนื่องจากไม่มีเช่นClassวัตถุที่มีอยู่ เฉพาะประเภท raw เท่านั้นที่ListมีClass วัตถุที่แสดงถึงชนิดของ runtime List.classมันจะเรียกว่าเป็น


12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal คุณไม่สามารถทำเช่นนั้นได้ใน Java คุณจะได้รับข้อผิดพลาดในการรวบรวมชนิดไม่ตรงกัน!
DhafirNz

4
ดังนั้น ... ฉันจะทำอย่างไรถ้าฉันต้องการ
Christopher Francisco

2
คุณสามารถหลอกผู้แปลได้โดยList<String> list2 = (List<String>) (Object) list1;
kftse

17
อีก "มันใช้งานได้ใน C # แต่ไม่ใช่ใน Java" สำหรับฉัน ฉัน deserialising วัตถุ JSON และ typeof (List <MyClass>) ทำงานได้อย่างสมบูรณ์แบบใน C # แต่ List <MyClass> .class เป็นข้อผิดพลาดทางไวยากรณ์ใน Java ใช่มีคำอธิบายเชิงตรรกะสำหรับสิ่งนั้นตามปกติตามที่ Cletus เขียน แต่ฉันมักจะสงสัยว่าทำไมสิ่งเหล่านั้นทั้งหมดจึงทำงานใน C #
ผักแช่ง

2
คุณหมายถึงอะไรมันถูกกฎหมายอย่างสมบูรณ์แบบ? ส่วนนั้นของรหัสไม่ได้รวบรวม?
Eduardo Dennis

63

ไม่มีตัวอักษรคลาสสำหรับประเภทพารามิเตอร์อย่างไรก็ตามมีวัตถุประเภทที่กำหนดประเภทเหล่านี้ได้อย่างถูกต้อง

ดู java.lang.reflect.ParameterizedType - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/ParameterizedType.html

ไลบรารี Gson ของ Google กำหนดคลาส TypeToken ที่อนุญาตให้สร้างประเภทพารามิเตอร์และใช้เพื่อระบุวัตถุ json ด้วยประเภทพารามิเตอร์ที่ซับซ้อนในแบบที่เป็นมิตรทั่วไป ในตัวอย่างของคุณคุณจะใช้:

Type typeOfListOfFoo = new TypeToken<List<Foo>>(){}.getType()

ฉันต้องการโพสต์ลิงก์ไปยังคลาสของ TypeToken และ Gson javadoc แต่ Stack Overflow จะไม่ให้ฉันโพสต์มากกว่าหนึ่งลิงก์เนื่องจากฉันเป็นผู้ใช้ใหม่คุณสามารถค้นหาได้อย่างง่ายดายโดยใช้การค้นหาโดย Google


1
ด้วยสิ่งนี้ฉันสามารถสร้างคลาสที่มี E ทั่วไปและจากนั้นใช้clzz = new TypeToken<E>(){}.getRawType();เพื่อย้ำผ่าน enums ที่มีโครงสร้างคล้ายกันด้วยclzz.getEnumConstants()และจากนั้นในที่สุดก็ใช้ refection เพื่อเรียกใช้วิธีการของสมาชิกMethod method = clzz.getDeclaredMethod("getSomeFoo");เพื่อให้ชนะอย่างมาก! ขอบคุณ!
Naruto Sempai

57

คุณสามารถจัดการได้ด้วยการโยนคู่:

@SuppressWarnings("unchecked") Class<List<Foo>> cls = (Class<List<Foo>>)(Object)List.class


2
โดยการเปลี่ยนการร่ายครั้งที่สองจากObjectเป็นClassคุณสามารถบันทึกโอเวอร์เฮดของการร่ายรันไทม์ที่ไม่มีจุดหมาย
Clashsoft

2
@Clashsoft ใช้ClassแทนObjectตามที่คุณขอแนะนำให้ดูเหมือนว่ามีความหมายมากขึ้น แต่ก็ไม่ได้เอาความต้องการของ@SuppressWarnings("unchecked")คำอธิบายประกอบก็ยังเพิ่มการเตือนใหม่:Class is a raw type. References to generic type Class<T> should be parameterized
Ortomala Lokni

10
คุณสามารถใช้Class<?>:(Class<List<Foo>>)(Class<?>)List.class
Devstr

@Devstr ฉันเห็นว่าคุณถูกต้องเมื่อฉันลอง ... อาร์กิวเมนต์สำหรับการใช้ (Object) หรือ (Class <?>) คืออะไร
cellepo

2
คำตอบนี้ไม่มีจุดหมายโดยสิ้นเชิง เหตุผลที่ OP ต้องการให้พารามิเตอร์เส้นทางคลาสเป็นเพราะเขาได้uncheckedรับคำเตือน คำตอบนี้จะไม่เปลี่ยนแปลง / ปรับปรุงใด ๆ OP กล่าวถึงคำถามของเขาว่าเขาไม่ต้องการใช้SuppressWarnings...
Spenhouet

6

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

List x = new ArrayList();
x.add(new SomeClass());
Iterator i = x.iterator();
SomeClass z = (SomeClass) i.next();

กลายเป็น

List<SomeClass> x = new ArrayList<SomeClass>();
x.add(new SomeClass());
Iterator<SomeClass> i = x.iterator();
SomeClass z = i.next();

สิ่งนี้ทำให้คอมไพเลอร์ตรวจสอบโค้ดของคุณในเวลาคอมไพล์ แต่ที่รันไทม์มันยังคงดูเหมือนตัวอย่างแรก


ขอบคุณสำหรับคำอธิบายเพิ่มเติม - ความเข้าใจของฉันเกี่ยวกับยาชื่อสามัญมีความชัดเจนมากขึ้นในขณะนี้ที่ฉันรู้ว่าพวกเขาไม่ใช่กลไกแบบไทม์ :)
Tom

2
ในความคิดของฉันนี่แค่หมายความว่ามีการใช้งานทั่วไปในลักษณะที่ปานกลางโดย Sun หวังว่า Oracle จะสามารถแก้ไขได้ในบางวัน การใช้งานทั่วไปของ C # นั้นดีขึ้นมาก (Anders is godlike)
Marcel Valdez Orozco

1
@MarcelValdezOrozco AFAIK ใน Java พวกเขาใช้มันอย่างนั้นเพราะพวกเขาต้องการรหัสเก่า (ก่อน 1.5) เพื่อทำงานกับ JVM ใหม่โดยไม่มีปัญหาใด ๆ ดูเหมือนว่าเป็นการตัดสินใจออกแบบที่ชาญฉลาดซึ่งคำนึงถึงความเข้ากันได้ ฉันไม่คิดว่าจะมีอะไรธรรมดา
peter.petrov

3

Java Generics คำถามที่พบบ่อยและดังนั้นจึงยัง cletus' คำตอบเสียงเหมือนมีจุดในการไม่มีClass<List<T>>แต่ปัญหาที่แท้จริงคือว่าเป็นอันตรายอย่างยิ่ง:

@SuppressWarnings("unchecked")
Class<List<String>> stringListClass = (Class<List<String>>) (Class<?>) List.class;

List<Integer> intList = new ArrayList<>();
intList.add(1);
List<String> stringList = stringListClass.cast(intList);
// Surprise!
String firstElement = stringList.get(0);

cast()ทำให้ดูราวกับว่ามันมีความปลอดภัย แต่ในความเป็นจริงมันไม่ได้เป็นความปลอดภัยตลอด


แม้ว่าฉันจะไม่ได้ตำแหน่งที่ไม่สามารถList<?>.class= Class<List<?>>เนื่องจากมันจะมีประโยชน์มากเมื่อคุณมีวิธีที่กำหนดประเภทตามClassอาร์กิวเมนต์ประเภททั่วไป

เนื่องจากgetClass()มีJDK-6184881 ที่ขอให้เปลี่ยนไปใช้ wildcard แต่ดูเหมือนว่าการเปลี่ยนแปลงนี้จะดำเนินการ (เร็ว ๆ นี้) เนื่องจากไม่สามารถใช้งานร่วมกับรหัสก่อนหน้า (ดูความคิดเห็นนี้ )


2

อย่างที่เราทุกคนรู้ว่ามันถูกลบ แต่สามารถทราบได้ในบางกรณีที่มีการกล่าวถึงประเภทไว้อย่างชัดเจนในลำดับชั้นของคลาส:

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public abstract class CaptureType<T> {
    /**
     * {@link java.lang.reflect.Type} object of the corresponding generic type. This method is useful to obtain every kind of information (including annotations) of the generic type.
     *
     * @return Type object. null if type could not be obtained (This happens in case of generic type whose information cant be obtained using Reflection). Please refer documentation of {@link com.types.CaptureType}
     */
    public Type getTypeParam() {
        Class<?> bottom = getClass();
        Map<TypeVariable<?>, Type> reifyMap = new LinkedHashMap<>();

        for (; ; ) {
            Type genericSuper = bottom.getGenericSuperclass();
            if (!(genericSuper instanceof Class)) {
                ParameterizedType generic = (ParameterizedType) genericSuper;
                Class<?> actualClaz = (Class<?>) generic.getRawType();
                TypeVariable<? extends Class<?>>[] typeParameters = actualClaz.getTypeParameters();
                Type[] reified = generic.getActualTypeArguments();
                assert (typeParameters.length != 0);
                for (int i = 0; i < typeParameters.length; i++) {
                    reifyMap.put(typeParameters[i], reified[i]);
                }
            }

            if (bottom.getSuperclass().equals(CaptureType.class)) {
                bottom = bottom.getSuperclass();
                break;
            }
            bottom = bottom.getSuperclass();
        }

        TypeVariable<?> var = bottom.getTypeParameters()[0];
        while (true) {
            Type type = reifyMap.get(var);
            if (type instanceof TypeVariable) {
                var = (TypeVariable<?>) type;
            } else {
                return type;
            }
        }
    }

    /**
     * Returns the raw type of the generic type.
     * <p>For example in case of {@code CaptureType<String>}, it would return {@code Class<String>}</p>
     * For more comprehensive examples, go through javadocs of {@link com.types.CaptureType}
     *
     * @return Class object
     * @throws java.lang.RuntimeException If the type information cant be obtained. Refer documentation of {@link com.types.CaptureType}
     * @see com.types.CaptureType
     */
    public Class<T> getRawType() {
        Type typeParam = getTypeParam();
        if (typeParam != null)
            return getClass(typeParam);
        else throw new RuntimeException("Could not obtain type information");
    }


    /**
     * Gets the {@link java.lang.Class} object of the argument type.
     * <p>If the type is an {@link java.lang.reflect.ParameterizedType}, then it returns its {@link java.lang.reflect.ParameterizedType#getRawType()}</p>
     *
     * @param type The type
     * @param <A>  type of class object expected
     * @return The Class<A> object of the type
     * @throws java.lang.RuntimeException If the type is a {@link java.lang.reflect.TypeVariable}. In such cases, it is impossible to obtain the Class object
     */
    public static <A> Class<A> getClass(Type type) {
        if (type instanceof GenericArrayType) {
            Type componentType = ((GenericArrayType) type).getGenericComponentType();
            Class<?> componentClass = getClass(componentType);
            if (componentClass != null) {
                return (Class<A>) Array.newInstance(componentClass, 0).getClass();
            } else throw new UnsupportedOperationException("Unknown class: " + type.getClass());
        } else if (type instanceof Class) {
            Class claz = (Class) type;
            return claz;
        } else if (type instanceof ParameterizedType) {
            return getClass(((ParameterizedType) type).getRawType());
        } else if (type instanceof TypeVariable) {
            throw new RuntimeException("The type signature is erased. The type class cant be known by using reflection");
        } else throw new UnsupportedOperationException("Unknown class: " + type.getClass());
    }

    /**
     * This method is the preferred method of usage in case of complex generic types.
     * <p>It returns {@link com.types.TypeADT} object which contains nested information of the type parameters</p>
     *
     * @return TypeADT object
     * @throws java.lang.RuntimeException If the type information cant be obtained. Refer documentation of {@link com.types.CaptureType}
     */
    public TypeADT getParamADT() {
        return recursiveADT(getTypeParam());
    }

    private TypeADT recursiveADT(Type type) {
        if (type instanceof Class) {
            return new TypeADT((Class<?>) type, null);
        } else if (type instanceof ParameterizedType) {
            ArrayList<TypeADT> generic = new ArrayList<>();
            ParameterizedType type1 = (ParameterizedType) type;
            return new TypeADT((Class<?>) type1.getRawType(),
                    Arrays.stream(type1.getActualTypeArguments()).map(x -> recursiveADT(x)).collect(Collectors.toList()));
        } else throw new UnsupportedOperationException();
    }

}

public class TypeADT {
    private final Class<?> reify;
    private final List<TypeADT> parametrized;

    TypeADT(Class<?> reify, List<TypeADT> parametrized) {
        this.reify = reify;
        this.parametrized = parametrized;
    }

    public Class<?> getRawType() {
        return reify;
    }

    public List<TypeADT> getParameters() {
        return parametrized;
    }
}

และตอนนี้คุณสามารถทำสิ่งต่าง ๆ เช่น:

static void test1() {
        CaptureType<String> t1 = new CaptureType<String>() {
        };
        equals(t1.getRawType(), String.class);
    }

    static void test2() {
        CaptureType<List<String>> t1 = new CaptureType<List<String>>() {
        };
        equals(t1.getRawType(), List.class);
        equals(t1.getParamADT().getParameters().get(0).getRawType(), String.class);
    }


    private static void test3() {
            CaptureType<List<List<String>>> t1 = new CaptureType<List<List<String>>>() {
            };
            equals(t1.getParamADT().getRawType(), List.class);
        equals(t1.getParamADT().getParameters().get(0).getRawType(), List.class);
    }

    static class Test4 extends CaptureType<List<String>> {
    }

    static void test4() {
        Test4 test4 = new Test4();
        equals(test4.getParamADT().getRawType(), List.class);
    }

    static class PreTest5<S> extends CaptureType<Integer> {
    }

    static class Test5 extends PreTest5<Integer> {
    }

    static void test5() {
        Test5 test5 = new Test5();
        equals(test5.getTypeParam(), Integer.class);
    }

    static class PreTest6<S> extends CaptureType<S> {
    }

    static class Test6 extends PreTest6<Integer> {
    }

    static void test6() {
        Test6 test6 = new Test6();
        equals(test6.getTypeParam(), Integer.class);
    }



    class X<T> extends CaptureType<T> {
    }

    class Y<A, B> extends X<B> {
    }

    class Z<Q> extends Y<Q, Map<Integer, List<List<List<Integer>>>>> {
    }

    void test7(){
        Z<String> z = new Z<>();
        TypeADT param = z.getParamADT();
        equals(param.getRawType(), Map.class);
        List<TypeADT> parameters = param.getParameters();
        equals(parameters.get(0).getRawType(), Integer.class);
        equals(parameters.get(1).getRawType(), List.class);
        equals(parameters.get(1).getParameters().get(0).getRawType(), List.class);
        equals(parameters.get(1).getParameters().get(0).getParameters().get(0).getRawType(), List.class);
        equals(parameters.get(1).getParameters().get(0).getParameters().get(0).getParameters().get(0).getRawType(), Integer.class);
    }




    static void test8() throws IllegalAccessException, InstantiationException {
        CaptureType<int[]> type = new CaptureType<int[]>() {
        };
        equals(type.getRawType(), int[].class);
    }

    static void test9(){
        CaptureType<String[]> type = new CaptureType<String[]>() {
        };
        equals(type.getRawType(), String[].class);
    }

    static class SomeClass<T> extends CaptureType<T>{}
    static void test10(){
        SomeClass<String> claz = new SomeClass<>();
        try{
            claz.getRawType();
            throw new RuntimeException("Shouldnt come here");
        }catch (RuntimeException ex){

        }
    }

    static void equals(Object a, Object b) {
        if (!a.equals(b)) {
            throw new RuntimeException("Test failed. " + a + " != " + b);
        }
    }

ข้อมูลเพิ่มเติมที่นี่ แต่อีกครั้งเป็นไปไม่ได้ที่จะเรียกคืน:

class SomeClass<T> extends CaptureType<T>{}
SomeClass<String> claz = new SomeClass<>();

มันจะถูกลบที่ไหน


นี่เป็นวิธีแก้ปัญหาที่ใช้โดย JAX-RS, cf และGenericEntity GenericType
Hein Blöd

1

เนื่องจากข้อเท็จจริงที่เปิดเผยว่าตัวอักษรของคลาสไม่มีข้อมูลประเภททั่วไปฉันคิดว่าคุณควรคิดว่ามันเป็นไปไม่ได้ที่จะกำจัดคำเตือนทั้งหมด ในทางใดทางหนึ่งการใช้Class<Something>ก็เหมือนกับการใช้คอลเล็กชันโดยไม่ระบุประเภททั่วไป สิ่งที่ดีที่สุดที่ฉันสามารถทำได้คือ:

private <C extends A<C>> List<C> getList(Class<C> cls) {
    List<C> res = new ArrayList<C>();
    // "snip"... some stuff happening in here, using cls
    return res;
}

public <C extends A<C>> List<A<C>> getList() {
    return getList(A.class);
}

1

คุณสามารถใช้วิธีช่วยเพื่อกำจัด@SuppressWarnings("unchecked")ชั้นเรียนทั้งหมด

@SuppressWarnings("unchecked")
private static <T> Class<T> generify(Class<?> cls) {
    return (Class<T>)cls;
}

จากนั้นคุณสามารถเขียน

Class<List<Foo>> cls = generify(List.class);

ตัวอย่างการใช้งานอื่น ๆ ได้แก่

  Class<Map<String, Integer>> cls;

  cls = generify(Map.class);

  cls = TheClass.<Map<String, Integer>>generify(Map.class);

  funWithTypeParam(generify(Map.class));

public void funWithTypeParam(Class<Map<String, Integer>> cls) {
}

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

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