Как проверить универсальный параметр метода, чтобы избежать ClassCastException? - PullRequest
0 голосов
/ 25 ноября 2018

Java использует стирание типов везде, где это применимо, но, конечно, при вызове методов типы параметров должны совпадать.

Как избежать ClassCastException, когда я не могу обеспечить тип во время компиляции? Пример:

class KeyedHashSet<K,E> // implements Set<E>
{ final Map<K,E> map = new HashMap<>();
  final Function<E,K> keyExtractor; // extracts intrusive key from value

  public KeyedHashSet(Function<E,K> keyExtractor)
  { this.keyExtractor = keyExtractor; }

  public boolean contains(Object o)
  { if (o != null)
      try
      { @SuppressWarnings("unchecked") // <- BAD
        K key = keyExtractor.apply((E)o);
        E elem = map.get(key);
        return elem != null && elem.equals(o);
      } catch (ClassCastException ex) // <- EVIL!!!
      {}
    return false;
  }
  // more methods
}

Метод Set.contains принимает произвольный объект в качестве параметра.Но я не могу извлечь ключ, необходимый для моего поиска по хешу, из произвольного объекта.Это работает только для объектов типа E.

На самом деле меня не интересует ключ, когда объект не относится к типу E, потому что в этом случае я уверен, что коллекция не содержит объект.

Но описанный выше способ обхода ClassCastException имеет несколько недостатков:

  • Прежде всего, не следует выдавать исключение, когда это обычный путь к программе.
  • Во-вторых, я мог бы поймать ClassCastException, брошенный изнутри реализации keyExtractor, которая не предназначена .

Можно ли проверитьтип o против параметра keyExtractor перед вызовом .apply и без необоснованных накладных расходов времени выполнения?


Примечание: я знаю, что вышедизайн требует, чтобы E имел неизменный ключ.Но это не страшно и случается довольно часто.

1 Ответ

0 голосов
/ 25 ноября 2018

Как могло произойти исключение 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 и т. Д.

...