Итерация по EnumMap # entrySet - PullRequest
9 голосов
/ 01 июня 2011

Перечисление свыше Map#entrySet не работает должным образом для всех реализаций Map, особенно для EnumMap, IdentityHashMap, и вот пример кода из презентации головоломки Josh Bloch's (Puzzle 5) -

public class Size {

    private enum Sex { MALE, FEMALE }

    public static void main(String[] args) { 
        printSize(new HashMap<Sex, Sex>()); 
        printSize(new EnumMap<Sex, Sex>(Sex.class)); 
    }

    private static void printSize(Map<Sex, Sex> map) { 
        map.put(Sex.MALE,   Sex.FEMALE); 
        map.put(Sex.FEMALE, Sex.MALE); 
        map.put(Sex.MALE,   Sex.MALE); 
        map.put(Sex.FEMALE, Sex.FEMALE); 
        Set<Map.Entry<Sex, Sex>> set = 
            new HashSet<Map.Entry<Sex, Sex>>(map.entrySet()); 
        System.out.println(set.size()); 
    }
}

и да, это дает неправильный результат -

должно быть

 2 
 2

но производит

2 
1

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

UPDATE
Хотя размер полученного набора равен 2, но записи одинаковы.

public class Test{

 private enum Sex { MALE, FEMALE } 

    public static void main(String... args){
        printSize(new HashMap<Sex, String>());
        printSize(new EnumMap<Sex, String>(Sex.class));
    }


    private static void printSize(Map<Sex, String> map) {
        map.put(Sex.MALE,   "1");
        map.put(Sex.FEMALE, "2");
        map.put(Sex.MALE,   "3");
        map.put(Sex.FEMALE, "4");
        Set<Map.Entry<Sex, String>> set =
            new HashSet<Map.Entry<Sex, String>>(map.entrySet());
        System.out.println(set.size());
    }
}

Я даже попробовал приведенный выше код с двумя различными типами enum в качестве ключа и значения.

Это кажется проблемой, только если EnumMap имеет то же самое перечисление, что и ключ и значение.

Я хотел бы знать, почему это? или я что-то упускаю. почему это не исправлено, когда ConcurrentHashMap был исправлен давно?

Ответы [ 3 ]

9 голосов
/ 01 июня 2011

Посмотрите на реализацию EnumMap.EntryIterator.next(). Этого должно быть достаточно, чтобы выяснить проблему.

Подсказка в том, что результирующий набор:

[FEMALE=2, FEMALE=2]

что не является правильным результатом.

Эффект, который вы видите, связан с реализацией EnumMap.EntryIterator.hashCode() (здесь это Map.Entry). Это

h = key ^ value

Это приводит к тому же хеш-значению для записей, создаваемых

map.put(Sex.MALE,   Sex.MALE); 
map.put(Sex.FEMALE, Sex.FEMALE); 

стабильный 0.

или

map.put(Sex.MALE,   Sex.FEMALE); 
map.put(Sex.FEMALE, Sex.MALE);

здесь это нестабильное (для нескольких исполнений) значение типа int. Вы всегда увидите эффект, если хэши ключей и значений совпадают, поскольку: a ^ b == b ^ a. Это приводит к тому же хеш-значению для Entry.

Если записи имеют одинаковое хеш-значение, они попадают в один и тот же сегмент хеш-таблицы, и равенства всегда будут работать, так как они в любом случае являются одним и тем же объектом.

Обладая этими знаниями, мы теперь можем также производить тот же эффект с другими типами, такими как Integer (где мы знаем реализацию hashCode):

map.put(Sex.MALE,   Integer.valueOf(Sex.MALE.hashCode())); 
map.put(Sex.FEMALE, Integer.valueOf(Sex.MALE.hashCode()));

[FEMALE=1671711, FEMALE=1671711]

Бонус : реализация EnumMap нарушает контракт equals ():

EnumMap<Sex, Object> enumMap = new EnumMap<Sex, Object>(Sex.class);
enumMap.put(Sex.MALE, "1");
enumMap.entrySet().iterator().next().equals(enumMap.entrySet().iterator());

Выдает:

Exception in thread "main" java.lang.IllegalStateException: Entry was removed
    at java.util.EnumMap$EntryIterator.checkLastReturnedIndexForEntryUse(EnumMap.java:601)
    at java.util.EnumMap$EntryIterator.getValue(EnumMap.java:557)
    at java.util.EnumMap$EntryIterator.equals(EnumMap.java:576)
    at com.Test.main(Test.java:13)
2 голосов
/ 01 июня 2011

EnumMap.EntryIterator.next() возвращает this ссылку.Вы можете проверить это следующим образом:

Iterator<? extends Map.Entry<Sex, Sex>> e = map.entrySet().iterator();
while (e.hasNext()) {
    Map.Entry<Sex, Sex> x = e.next();
    System.out.println(System.identityHashCode(x));
}
0 голосов
/ 01 июня 2011

Проблема не в карте, а в реализации EntryIterator и спецификации HashSet, которые принимают только не равные элементы.

В случае, если карты 1 и 2 должны иметь два элемента, вы можете убедиться, что вызов

map.entrySet().size();

«Проблема» заключается в реализации EntryIterator классом EnumMap, так как это головоломкапопробуй сам разобраться почему.

пс.используйте отладчик.

Редактировать:

Это то, что вы действительно делаете:

    Set<Map.Entry<Sex, Sex>> set =  new HashSet<Map.Entry<Sex, Sex>>();



    Iterator<Map.Entry<Sex, Sex>> e = entrySet.iterator();
    while (e.hasNext()) {
        set.add(e.next());
    }

Помните, что HashSet реализован через HashMap, значения, добавленные в hashMap на основехэш-код и равенство.

Кстати, все объяснено в OP, ссылка на головоломку.Ошибка заключается в методе равно, что после второго вызова метода next (), изменить способ работы и сравнить тип класса, чем значения return o == this;.

...