Как могло произойти исключение ClassCastException после стирания?
После применения стирания может быть трудно определить, вызовет ли неконтролируемое приведение ClassCastException
.В конце концов, типы уменьшены до Object
;почему приведение к Object
вызывает исключение?Одной из причин этой ошибки является универсальный код, который вызывает неуниверсальные реализации, где типы явно указаны.Возьмем следующий пример:
class Test<K> {
public void foo(Object o) {
bar((K) o);
}
public void bar(K k) {
System.out.println(k);
}
public static void main(String[] args) {
Test<Integer> test = new Test<>();
test.foo("hello");
}
}
Приведенный выше пример будет по-прежнему печатать "hello"
правильно, даже если аргумент универсального типа был Integer
.После стирания для метода bar
требуется только объект:
public bar(Ljava/lang/Object;)V
Если мы расширим Test
и переопределим bar
там, где тип явный, тогда мы выдадим ошибку.
class TestInteger extends Test<Integer> {
@Override
public void bar(Integer k) {
super.bar(k);
}
public static void main(String[] args) {
Test<Integer> test = new TestInteger();
test.foo("hello");
}
}
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at TestInteger.bar(TestInteger.java:17)
at Test.foo(TestInteger.java:9)
at TestInteger.foo(TestInteger.java:17)
at TestInteger.main(TestInteger.java:24)
В этом дочернем классе переопределяемый метод имеет подпись, отличную от метода, созданного Test<K>
.Компилятор создает новый перегруженный метод, называемый синтетическим или мостовым методом , для вызова bar
, как написано в TestInteger
.Этот метод моста, где ClassCastException
происходит.Это будет выглядеть так:
public void bar(Object k) {
bar((Integer) k); //java.lang.String cannot be cast to java.lang.Integer
}
public void bar(Integer k) {
System.out.println(k);
}
В вашем примере, где-то внутри вызова keyExtractor.apply((E)o)
лежит подпись, которая опирается на явный тип, вызывающий исключение приведения.
Можно ли проверить тип перед передачей?
Да, это возможно, но вам придется предоставить вашему классу KeyedHashSet
дополнительные данные.Вы не можете напрямую получить объект Class
, связанный с параметром типа.
Одним из способов является внедрение типа Class
в контейнер и вызов isInstance
:
Этот метод является динамическим эквивалентом оператора языка instanceof
языка Java.Метод возвращает true
, если указанный аргумент Object
не равен нулю и может быть приведен к ссылочному типу, представленному этим объектом Class
, без вызова ClassCastException
.В противном случае возвращается false
.
public class Test<K> {
final Class<K> clazz;
Test(Class<K> clazz) { this.clazz = clazz; }
public void foo(Object o) {
if (clazz.isInstance(o)) {
bar((K) o);
}
}
...
Test<Integer> test = new Test<>(Integer.class);
test.foo("string");
Вы также можете использовать некоторую стратегию проверки, когда выполняется проверка экземпляра:
public class Test<K> {
final Function<Object, Boolean> validator;
Test(Function<Object, Boolean> validator) { this.validator = validator; }
public void foo(Object o) {
if (validator.apply(o)) {
bar((K) o);
}
}
...
Test<Integer> test = new Test<>(k -> k instanceof Integer);
test.foo("string");
Другой вариант может заключаться в перемещении типапроверка внутри экземпляра Function<E,K> keyExtractor
и параметры типа становятся Function<Object,K> keyExtractor
, возвращая null
, если тип был неправильным.
Теоретически возможно также рефлексивно исследовать сигнатуры методов для keyExtractor
и получить экземпляр Class
, но не гарантируется, что его реализация также явно определит параметры типа.
Замедлит ли проверка instanceof мое приложение?
Время выполнения isInstance
на самом деле довольно быстро. Есть интересная статья , в которой экспериментально сравниваются скорости триггера с небезопасным приведением к решению isInstance
.В результатах эксперимента решение, которое явно проверяет тип, лишь немного медленнее, чем небезопасное решение.
Учитывая, что снижение производительности слишком низкое, я бы выбрал безопасный путь и добавил проверку класса в ваш метод contains
.Если вы сохраните решение try-catch как есть, вы можете в конечном итоге маскировать будущие ошибки, вызванные реализацией keyExtractor.apply
, map.get
, elem.equals
и т. Д.