пятница, 21 мая 2010 г.

Нестабильность Enum.hashCode() и HashMap

На днях обнаружилась любопытная особенность enum'ов. В коде сервера я использовал ScalableHashMap для хранения некоторых данных, где ключом является enum. ScalableHashMap - это специальная реализация хешмапа для сервера Reddwarf.

После первого запуска сервера и инициализации карты всё работало замечательно.
Потом ScalableHashMap был сериализован и сохранен на диск.
Дальше сервер был перезагружен, и ScalableHashMap десериализован с диска.
Делаю ScalableHashMap.get(key) и... получаю null!
Вывожу ScalableHashMap на экран с помощью toString() - там всё есть! Но почему-то он не хочет доставать записанные данные.

Оказалось загвоздка вот в чем:
Enum использует hashCode(), унаследованный от Object(). Следовательно, при перезапуске сервера меняются адреса ссылок, соответственно меняются хеш-коды объектов. И из карты невозможно вытащить объект с новым хеш-кодом, сохраненный по старому хеш-коду!

Казалось бы, проблема решается легко - переопределением hashCode() на что-то вроде

public int hashCode() {
   return ordinal();
}

Но не тут-то было! Хеш-код объявлен в enum'е как final!

public final int hashCode() {
   return super.hashCode();
}

Получается, надо смириться с тем, что у enum'а при каждом перезапуске приложения может быть новый хеш-код.

Оказывается, для хранения данных с enum-ключом в Java существует специальный класс: java.util.EnumMap, который как раз использует неизменный Enum.ordinal() вместо непостоянного Object.hashCode().

Но, погодите, как же тогда работают HashMap, ConcurrenHashMap и подобные коллекции? Эксперимент показал, что работа с ними не вызывает никаких проблем: enum'ы в качестве ключей нормально сохраняются, и данные нормально вытаскиваются после перезагрузки.

Причина оказалась весьма простой: при сериализации стандартного HashMap его структура и хеш-коды вообще не сериализуются. Сериализуются только ключи и значения, которые при десериализации заново выстраиваются в правильную структуру. Вот так это выглядит в коде jdk:

transient Entry[] table;

transient int size;

...

private void writeObject(java.io.ObjectOutputStream s) throws IOException
{
    ...
    // Write out keys and values (alternating)
    if (i != null) {
        while (i.hasNext()) {
            Map.Entry<K,V> e = i.next();
            s.writeObject(e.getKey());
            s.writeObject(e.getValue());
        }
    }
}

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
{
    ...
    // Read the keys and values, and put the mappings in the HashMap
    for (int i=0; i<size; i++) {
        K key = (K) s.readObject();
        V value = (V) s.readObject();
        putForCreate(key, value);
    }
}

В результате этого небольшого расследования-исследования можно сделать следующие выводы:

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

Комментариев нет:

Отправить комментарий