TL; DR ваш тест не пройден, исправьте это.
Прежде всего, это легче воспроизвести с помощью:
List<String> list = ImmutableList.of("Kumar", "Kumar", "Jens");
public static Map<String, Long> getValueItemOccurrences1(List<String> list) {
return list
.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}
public static Map<String, Long> getValueItemOccurrences2(List<String> list) {
Map<String, Long> occurrencesOfValueItems = new HashMap<>();
list.forEach(item -> {
if (occurrencesOfValueItems.containsKey(item)) {
occurrencesOfValueItems.put(item, occurrencesOfValueItems.get(item) + 1);
} else {
occurrencesOfValueItems.put(item, 1L);
}
});
return occurrencesOfValueItems;
}
Проблема в том, что после внутреннего HashMap::hash
(также называемого повторным хешированием) и получения последних битов, которые действительно имеют значение при принятии решения о выборе сегмента, они имеют одинаковые значения:
System.out.println(hash("Kumar".hashCode()) & 15);
System.out.println(hash("Jens".hashCode()) & 15);
Проще говоря, HashMap
решает, куда поместить запись (выбран контейнер) на основе hashCode
ваших записей. Ну, почти, как только вычисляется hashCode
, внутренне делается еще один hash
- для лучшего распределения записей. Это окончательное int
значение hashCode
используется для определения сегмента. Когда вы создаете HashMap с емкостью по умолчанию 16
(например, через new HashMap
), только последние 4 бита имеют значение, куда будет входить запись (вот почему я сделал & 15
там - чтобы увидеть последние 4 биты).
, где hash
:
// xor first 16 and last 16 bits
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Теперь оказывается, что ["Kumar" and "Jens"]
или ["Xavier", "Kenneth", "Samuel"]
имеют те же последние 4 цифры после применения алгоритма, описанного выше (3 в первом случае и 1 во втором случае).
Теперь, когда мы знаем эту информацию, это на самом деле можно упростить еще больше:
Map<String, Long> map = new HashMap<>();
map.put("Kumar", 2L);
map.put("Jens", 1L);
System.out.println(map); // {Kumar=2, Jens=1}
map = new HashMap<>();
map.computeIfAbsent("Kumar", x -> 2L);
map.computeIfAbsent("Jens", x -> 1L);
System.out.println(map); // {Jens=1, Kumar=2}
Я использовал map.computeIfAbsent
, потому что это то, что Collectors.groupingBy
использует под капотом.
Оказывается, что put
и computeIfAbsent
, помещать элементы в HashMap
другим способом; это полностью разрешено, так как у карты нет порядка в любом случае - и эти элементы в любом случае оказываются в одном и том же сегменте, который является частью импорта. Так что проверяйте свой код, ключ за ключом, предыдущий код тестирования был сломан.
Это даже забавное чтение, если хотите:
HashMap::put
будет добавлять элементы Linked
способом (до тех пор, пока не будут созданы Tree
записи), поэтому, если у вас есть один элемент, все остальные будут добавлены как:
one --> next --> next ... so on.
элементы добавляются к end of this queue
по мере их поступления в метод put
.
С другой стороны computeIfAbsent
немного отличается, он добавляет элементы в начало очереди. Если взять приведенный выше пример, сначала добавляется Xavier
. Затем, когда добавляется Kenneth
, становится первым:
Kenneth -> Xavier // Xavier was "first"
Когда добавлено Samuel
, оно становится первым:
Samuel -> [Kenneth -> Xavier]