Java: как получить литерал класса из универсального типа?

194

Как правило, я видел, как люди используют литерал класса следующим образом:

Class<Foo> cls = Foo.class;

Но что, если тип является общим, например, List? Это работает нормально, но есть предупреждение, так как список должен быть параметризован:

Class<List> cls = List.class

Так почему бы не добавить <?>? Ну, это вызывает ошибку несоответствия типов:

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

Я подумал, что-то вроде этого будет работать, но это просто ошибка синтаксиса:

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

Как получить Class<List<Foo>>статически, например, используя литерал класса?

Я мог бы использовать, @SuppressWarnings("unchecked")чтобы избавиться от предупреждений, вызванных непараметрическим использованием List в первом примере Class<List> cls = List.class, но я бы не хотел.

Какие-либо предложения?

Том
источник

Ответы:

161

Вы не можете из-за стирания типа .

Обобщения Java - это не более, чем синтаксический сахар для приведения объектов. Демонстрировать:

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объекта. Литерал класса может использоваться для проверки типов во время выполнения и для отражения.

Параметризованные типы теряют свои аргументы типа, когда они переводятся в байт-код во время компиляции в процессе, называемом стиранием типа. Как побочный эффект стирания типа, все экземпляры универсального типа имеют одно и то же представление времени выполнения, а именно, соответствующего исходного типа. Другими словами, параметризованные типы не имеют собственного представления типов. Следовательно, нет смысла формировать литералы классов, такие как List<String>.class, List<Long>.classи List<?>.class , поскольку таких Classобъектов не существует. Только необработанный тип Listимеет Class объект, который представляет его тип во время выполнения. Это упоминается как List.class.

Клетус
источник
12
List<Integer> list1 = new ArrayList<Integer>(); List<String> list2 = (List<String>)list1; list2.add("foo"); // perfectly legal Вы не можете сделать это в Java, вы получите ошибку компиляции несоответствия типов!
DhafirNz
4
так ... что мне делать, если мне это нужно?
Кристофер Франциско
2
Вы всегда можете обмануть компилятор с помощьюList<String> list2 = (List<String>) (Object) list1;
kftse
17
Еще одно «Это работает только в C #, но не в Java» для меня. Я десериализирую объект JSON, и typeof (List <MyClass>) прекрасно работает в C #, но List <MyClass> .class является синтаксической ошибкой в ​​Java. Да, есть логическое объяснение этому, как обычно писал Cletus, но мне всегда интересно, почему все эти вещи работают только в C #.
Чертовы овощи
2
что вы имеете в виду это совершенно законно? Эта часть кода не компилируется?
Эдуардо Деннис
63

Для параметризованных типов не существует литералов Class, однако есть объекты Type, которые правильно определяют эти типы.

См. Java.lang.reflect.ParameterizedType - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/ParameterizedType.html.

Библиотека Google Gson определяет класс TypeToken, который позволяет просто генерировать параметризованные типы и использует его для спецификации объектов json со сложными параметризованными типами универсальным дружественным способом. В вашем примере вы бы использовали:

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

Я намеревался публиковать ссылки на классы Javadoc TypeToken и Gson, но переполнение стека не позволит мне публиковать более одной ссылки, так как я новый пользователь, вы можете легко найти их с помощью поиска Google

Санти П.
источник
1
Благодаря этому я смог создать класс с универсальным E, а затем использовать его clzz = new TypeToken<E>(){}.getRawType();для последующей итерации по схожим структурированным перечислениям с clzz.getEnumConstants()последующим использованием рефлексии для вызова методов-членов а-ля Method method = clzz.getDeclaredMethod("getSomeFoo");победа! Спасибо!
Наруто
57

Вы можете управлять этим с двойным броском:

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

slaurent
источник
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, во время выполнения удаляются все записи универсальных типов. Обобщения обрабатываются только в компиляторе и используются для обеспечения дополнительной безопасности типов. Они на самом деле просто стенография, которая позволяет компилятору вставлять типы типов в соответствующих местах. Например, ранее вы должны были сделать следующее:

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

Это позволяет компилятору проверять ваш код во время компиляции, но во время выполнения он все еще выглядит как первый пример.

Джим гаррисон
источник
Спасибо за дополнительное объяснение - теперь мое понимание обобщенных шаблонов стало более ясным, и я понял, что они не являются механизмом времени выполнения. :)
Том
2
На мой взгляд, это просто означает, что generic был реализован посредственным образом Sun, надеюсь, Oracle исправит это когда-нибудь. Реализация generic в C # намного лучше (Андерс богоподобен)
Марсель Вальдес Орозко
1
@MarcelValdezOrozco AFAIK, в Java они реализовали это таким образом, потому что они хотели, чтобы старый код (до 1.5) работал на новых JVM без каких-либо проблем. Кажется, это очень умное дизайнерское решение, которое заботится о совместимости. Я не думаю, что в этом есть что-то посредственное.
peter.petrov
3

Java Дженерики FAQ и поэтому также Клетуса ответ звучит как нет смысла в том 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, запрашивающий переключение на использование подстановочных знаков, однако не похоже, что это изменение будет выполнено (очень скоро), поскольку оно несовместимо с предыдущим кодом (см. Этот комментарий ).

Marcono1234
источник
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<>();

где это стирается.

Jatin
источник
Это также обходной путь, используемый JAX-RS, ср. GenericEntityи GenericType.
Хейн Блод
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) {
}

Тем не менее, поскольку он редко действительно полезен, а использование метода отрицательно сказывается на проверке типов компилятором, я бы не рекомендовал реализовывать его там, где он общедоступен.

Aventurin
источник