Подтвердить что ты не робот

Общий тип

У меня есть следующий класс (упрощенный, но все же рабочий пример):

class Test<T> {
    List<T> l = new ArrayList<>();

    public Test() {
    }

    public void add(Object o) {
        l.add((T)o);
    }
}

И тестовый код:

Test<Double> t = new Test<>();
t.add(1);
t.add(1.2);
t.add(-5.6e-2);
t.add("hello");

Все работает отлично, и это не то, что я ожидал. Должен ли метод add выбрасывать ClassCastException? Если я добавлю метод get, более или менее то же самое:

    public T get(int i) {
        return l.get(i);
    }
.../...
t.get(1);             // OK.
t.get(3);             // OK (?)
Double d = t.get(3);  // throws ClassCastException

Почему только при присваивании переменной генерируется исключение? Как я могу обеспечить соответствие типов, если приведение (T) не работает?

4b9b3361

Ответ 1

Не следует ли методу добавления бросать ClassCastException?

Нет, это не должно (хотя я бы этого хотел). Короче говоря, Java-реализация генериков отбрасывает информацию о типе после компиляции вашего кода, поэтому List<T> разрешено принимать любые Object, а литье внутри вашего метода add не проверяется.

Почему только при присваивании переменной генерируется исключение?

Поскольку приведение к Double вставляется компилятором. Компилятор Java знает, что тип возврата get равен T, который является Double, поэтому он вставляет приведение в соответствие типу переменной d, которому назначается результат.

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

class Test<T> {
    private final Class<T> cl;
    List<T> l = new ArrayList<>();

    public Test(Class<T> c) {
        cl = c;
    }

    public void add(Object o) {
        l.add(cl.cast(o));
    }
}

Теперь трансляция выполняется объектом Class<T>, поэтому вы получите ClassCastException при попытке вставить объект неправильного типа.

Ответ 2

В качестве альтернативного решения вы можете использовать Collections.checkedList:

class Test<T> {
    List<T> l;

    public Test(Class<T> c) {
        l = Collections.checkedList(new ArrayList<T>(), c);
    }

    public void add(Object o) {
        l.add((T) o);
    }
}

Таким образом вы получите следующее исключение:

Exception in thread "main" java.lang.ClassCastException: Attempt to insert 
  class java.lang.Integer element into collection with element type class java.lang.Double
    at java.util.Collections$CheckedCollection.typeCheck(Collections.java:3037)
    at java.util.Collections$CheckedCollection.add(Collections.java:3080)
    at Test.add(Test.java:13)

Ответ 3

Для полноты этого ресурса здесь разница в скомпилированном байт-коде между броском в родовом:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: invokeinterface #7,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    10: pop
    11: return

И явное приведение к Double без дженериков:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class java/lang/Double
     8: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

Вы можете видеть, что версия с дженериками вообще не выполняет команду checkcast (благодаря стирать стилю, поэтому вам не следует ожидать исключения при предоставлении ему данных с непревзойденным классом. К сожалению, это не более строго соблюдается, но это имеет смысл, поскольку generics предназначены для более строгих проверок типов компиляции и не очень помогают в из-за стирания типа.

Java проверит типы аргументов функции, чтобы увидеть, есть ли соответствие типа, или может быть выполнена продвижение типа. В вашем случае String является типом аргумента, и его можно повысить до Object, который является степенью проверки типа времени компиляции, гарантирующей, что вызов функции работает.

Есть несколько вариантов, и решение dasblinkenlight, вероятно, является самым элегантным. (Возможно, вы не сможете изменить подпись метода, скажем, если вы переопределяете унаследованный метод add или планируете передать метод add и т.д.).

Другим вариантом, который может помочь, является использование параметра ограниченного типа вместо неограниченного. Параметры Unbounded type полностью теряются после компиляции из-за стирания типа, но использование параметра ограниченного типа заменяет экземпляры родового типа тем, что он должен продлить.

class Test<T extends Number> {

Конечно, T в данный момент не является действительно общим, но использование этого определения класса будет применять типы во время выполнения, поскольку приведение будет проверено на суперкласс класса Number. Здесь байт-код, чтобы доказать это:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class java/lang/Number
     8: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

Это определение класса генерирует желаемый ClassCastException при попытке добавить строку.