Кастинг для общего типа в Java не вызывает ClassCastException?

Я столкнулся с странным поведением Java, которое кажется ошибкой. Это? Приведение объекта к общему типу (например, K) не бросает ClassCastException, даже если объект не является экземпляром K. Вот пример:

import java.util.*;
public final class Test {
  private static<K,V> void addToMap(Map<K,V> map, Object ... vals) {
    for(int i = 0; i < vals.length; i += 2)
      map.put((K)vals[i], (V)vals[i+1]); //Never throws ClassCastException!
  }
  public static void main(String[] args) {
    Map<String,Integer> m = new HashMap<String,Integer>();
    addToMap(m, "hello", "world"); //No exception
    System.out.println(m.get("hello")); //Prints "world", which is NOT an Integer!!
  }
}

Обновление: благодаря cletus и Andrzej Doyle за ваши полезные ответы. Поскольку я могу только принять его, я принимаю ответ Andrzej Doyle, потому что это привело меня к решению, которое, я думаю, не так уж плохо. Я думаю, что это немного лучший способ инициализации небольшой карты в однострочном пространстве.

  /**
   * Creates a map with given keys/values.
   * 
   * @param keysVals Must be a list of alternating key, value, key, value, etc.
   * @throws ClassCastException if provided keys/values are not the proper class.
   * @throws IllegalArgumentException if keysVals has odd length (more keys than values).
   */
  public static<K,V> Map<K,V> build(Class<K> keyClass, Class<V> valClass, Object ... keysVals)
  {
    if(keysVals.length % 2 != 0)
      throw new IllegalArgumentException("Number of keys is greater than number of values.");

    Map<K,V> map = new HashMap<K,V>();
    for(int i = 0; i < keysVals.length; i += 2)
      map.put(keyClass.cast(keysVals[i]), valClass.cast(keysVals[i+1]));

    return map;
  }

И тогда вы называете это следующим образом:

Map<String,Number> m = MapBuilder.build(String.class, Number.class, "L", 11, "W", 17, "H", 0.001);

Ответ 1

Как говорит cletus, стирание означает, что вы не можете проверить это во время выполнения (и благодаря вашему кастингу вы не можете проверить это во время компиляции).

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

Кроме того, имейте в виду, что означает кастинг - это способ сообщить компилятору "Я знаю, что вы не можете проверить, соответствуют ли типы, но доверять мне, я знаю, что они делают", Когда вы переопределяете проверку типов (неправильно), а затем закончите с несоответствием типа, кого вы обвиняете?; -)

Кажется, ваша проблема кроется в отсутствии гетерогенных структур данных общего характера. Я бы предположил, что сигнатура типа вашего метода должна быть больше похожа на private static<K,V> void addToMap(Map<K,V> map, List<Pair<K, V>> vals), но я не уверен, что вам что-то действительно нравится. Список пар в основном представляет собой карту, поэтому для описания метода typeafe vals для вызова метода будет работать столько же, сколько просто поместить карту напрямую.

Если вы действительно хотите сохранить свой класс примерно так, как он есть, но добавьте безопасность во время выполнения, возможно, следующее даст вам несколько идей:

private static<K,V> void addToMap(Map<K,V> map, Object ... vals, Class<K> keyClass, Class<V> valueClass) {
  for(int i = 0; i < vals.length; i += 2) {
    if (!keyClass.isAssignableFrom(vals[i])) {
      throw new ClassCastException("wrong key type: " + vals[i].getClass());
    }
    if (!valueClass.isAssignableFrom(vals[i+1])) {
      throw new ClassCastException("wrong value type: " + vals[i+1].getClass());
    }

    map.put((K)vals[i], (V)vals[i+1]); //Never throws ClassCastException!
  }
}

Ответ 2

В Java-генераторах используется стирание типа, что означает, что эти параметризованные типы не сохраняются во время выполнения, поэтому это совершенно легально:

List<String> list = new ArrayList<String>();
list.put("abcd");
List<Integer> list2 = (List<Integer>)list;
list2.add(3);

потому что скомпилированный байт-код выглядит примерно так:

List list = new ArrayList();
list.put("abcd");
List list2 = list;
list2.add(3); // auto-boxed to new Integer(3)

Генераторы Java - это просто синтаксический сахар при литье Object s.

Ответ 3

Генераторы Java выполняются с использованием "стирания типа", что означает, что во время выполнения код не знает, что у вас есть Map < String, Integer > - он просто видит карту. И так как вы конвертируете материал в объекты (в виде списка параметров функции addToMap), во время компиляции код "выглядит правильно". Он не пытается запускать материал при компиляции.

Если вы беспокоитесь о типах во время компиляции, не называйте их Object.:) Сделайте вашу функцию addToMap похожим на

private static<K,V> void addToMap(Map<K, V> map, K key, V value) {

Если вы хотите вставить несколько элементов на карту, вам нужно сделать класс вроде java.util Map.Entry и обернуть пары ключ/значение в экземплярах этого класса.

Ответ 4

Это довольно хорошее объяснение того, что Generics делают и не делают в Java: http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html

Это очень сильно отличается от С#!

Я думаю, вы пытались сделать что-то подобное? Где компилируется безопасность времени для пар, которые вы добавляете к карте:

addToMap(new HashMap<String, Integer>(), new Entry<String,Integer>("FOO", 2), new Entry<String, Integer>("BAR", 8));

    public static<K,V> void addToMap(Map<K,V> map, Entry<K,V>... entries) {
        for (Entry<K,V> entry: entries) {
            map.put(entry.getKey(), entry.getValue());
        }
    }

    public static class Entry<K,V> {

        private K key;
        private V value;

        public Entry(K key,V value) {
            this.key = key;
            this.value = value;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }
    }

Изменить после комментария:

Ahh, тогда, возможно, все, что вы действительно ищете, - это тайный синтаксис, используемый для преследования новых сотрудников, которые просто переключились на Java.

Map<String, Integer> map = new HashMap<String,Integer>() {{
  put("Foo", 1);
  put("Bar", 2);
}};

Ответ 5

Генераторы Java применяются только во время компиляции и не запускаются. Проблема заключается в том, как вы реализовали, java-компилятор не получает возможности гарантировать безопасность типа во время компиляции.

Так как ur K, V никогда не говорит, что они расширяют какой-либо конкретный класс, во время компиляции java не может знать, что он должен быть целым числом.

Если вы измените свой код следующим образом

private static void addToMap (Карта карты, Объект... vals)

он даст вам ошибку времени компиляции