Общий тип
У меня есть следующий класс (упрощенный, но все же рабочий пример):
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)
не работает?
Ответы
Ответ 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
при попытке добавить строку.