Ответ 1
Реализация enum
hashCode
использует значение по умолчанию, указанное Object
. В документации этого метода упоминается:
Всякий раз, когда он вызывается одним и тем же объектом более одного раза во время выполнения приложения Java, метод hashCode должен последовательно возвращать одно и то же целое число, если информация, используемая при равных сравнениях с объектом, не изменяется. Это целое число не должно оставаться согласованным с одним исполнением приложения на другое выполнение того же приложения.
Так как хэш-код определяет порядок ведер внутри HashMap
(что используется groupingBy
), порядок изменяется при изменении хеш-кода. Как генерируется этот хеш-код, это деталь реализации виртуальной машины (как отметил Юджин). Комментируя и не комментируя строку с помощью peek
, вы просто нашли способ повлиять (надежно или нет) на эту реализацию.
Поскольку этот вопрос получил щедрость, кажется, что люди не удовлетворены моим ответом. Пойду немного глубже и посмотрю на реализацию open-jdk8 (потому что он с открытым исходным кодом) hashCode
. ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Я еще раз заявлю, что реализация алгоритма хеш-кода идентификатора не задана и может быть совершенно различной для другой виртуальной машины или между разными версиями одной и той же виртуальной машины. Поскольку OP наблюдает это поведение, Я буду считать, что используемая им виртуальная машина - это Hotspot (Oracle one, который afaik использует ту же реализацию hashcode, что и opendjk). Но главное в этом состоит в том, чтобы показать, что комментарий или отказ от комментариев, казалось бы, несвязанной строки кода может изменить порядок ведер в HashMap
.. Это также одна из причин, почему вы должен никогда полагаться на порядок итерации коллекции, которая не указывает ее (например, HashMap
).
Теперь, фактический алгоритм хэширования для openjdk8 определен в synchronizer.cpp
:
// Marsaglia xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
Как вы можете видеть, хеш-код основан на этих _hashState
полях объекта Thread
, и выход изменяется от одного вызова к другому, так как значения переменных "перетасовываются".
Эти переменные инициализируются в конструкторе Thread
следующим образом:
_hashStateX = os::random() ;
_hashStateY = 842502087 ;
_hashStateZ = 0x8767 ; // (int)(3579807591LL & 0xffff) ;
_hashStateW = 273326509 ;
Единственная движущаяся часть здесь os::random
, которая определена в os.cpp
, которая имеет комментарий, описывающий алгоритм как:
next_rand = (16807*seed) mod (2**31-1)
Этот seed
является единственной движущейся частью и определяется _rand_seed
и инициализируется через функцию, называемую init_random
, а в конце функции возвращаемое значение используется как семя для следующий звонок. A grep
через репо показывает следующее:
PS $> grep -r init_random
os/bsd/vm/os_bsd.cpp: init_random(1234567);
os/linux/vm/os_linux.cpp: init_random(1234567);
os/solaris/vm/os_solaris.cpp: init_random(1234567);
os/windows/vm/os_windows.cpp: init_random(1234567);
... test methods
Похоже, что начальное семя является константой на тестируемой платформе (windows).
Из этого я пришел к выводу, что сгенерированный хэш-код идентичности (в openjdk-8), изменения, основанные на том, сколько хэш-кодов идентификаторов было создано в одном и том же потоке до него и сколько раз os::random
было вызываемый до создания потока, генерирующего хэш-код, который остается неизменным для примера программы. Мы уже можем видеть это, потому что порядок ключей не изменяется от запуска до запуска программы, если программа остается прежней. Но еще один способ увидеть это - положить System.out.println(new Object().hashCode());
в начале метода main
и увидеть, что вывод всегда один и тот же, если вы запускаете программу несколько раз.
Вы также заметите, что генерация идентификационных кодов хеша до вызовов потоков также изменит хэш-коды констант перечисления и, следовательно, может изменить порядок ковшей на карте.
Теперь вернемся к примеру Java. Если хэш-код идентификатора константы перечисления изменяется в зависимости от того, сколько кодов идентификации хэша было создано до него, логический вывод будет заключаться в том, что где-то в вызове peek
генерируется хэш-код идентификатора, который меняет хэш-коды, которые затем генерируются для константы перечисления на линии с collect
:
Map<Continent, List<String>> regionNames = couList
.stream()
//.peek(System.out::println) // Does this call Object.hashCode?
.collect(Collectors.groupingBy(Country::getRegion,
Collectors.mapping(Country::getName, Collectors.toList()))); // hash code for constant generated here
Вы можете увидеть это с помощью обычного отладчика Java. Я поместил точку останова на Object#hashCode
и стал ждать, если это вызовет строка с peek
. (Если вы попробуете это самостоятельно, я бы заметил, что VM использует HashMap
сам и будет вызывать hashCode
несколько раз перед методом main
. Поэтому имейте это в виду)
Et voila:
Object.hashCode() line: not available [native method]
HashMap<K,V>.hash(Object) line: 338
HashMap<K,V>.put(K, V) line: 611
HashSet<E>.add(E) line: 219
Collections$SynchronizedSet<E>(Collections$SynchronizedCollection<E>).add(E) line: 2035
Launcher$AppClassLoader(ClassLoader).checkPackageAccess(Class<?>, ProtectionDomain) line: 508
Main.main(String...) line: 19
Строка с peek
вызывает hashCode
на объекте ProtectionDomain
, который используется загрузчиком классов, который загружает класс LambdaMetafactory
(который является Class<?>
, который вы видите, я могу получить значение из моего отладчик). Метод hashCode
на самом деле называется кучей раз (может быть, несколько сотен?), Для строки с peek
, в рамках платформы MethodHandle.
Итак, поскольку строка с peek
вызывает Object#hashCode
, до того, как генерируются хэш-коды для констант перечисления (также путем вызова Object#hashCode
), хэш-коды констант меняются. Таким образом, добавление или удаление строки с помощью peek
изменяет хэш-коды констант, что изменяет порядок ковшей на карте.
Последний способ подтвердить, состоит в том, чтобы генерировать хэш-коды констант перед строкой с помощью peek
, добавляя:
Continent.ASIA.hashCode();
Continent.EUROPE.hashCode();
В начало метода main
.
Теперь вы увидите, что комментарий или отказ от комментария строки с peek
не влияет на порядок ведер.