Как мы все знаем, HashMap позволяет быть только одному keynull, и при дублировании он заменит старый. Также нет ограничений на value.
Но API .collect(Collectors.toMap()) в потоке ограничен как в том, так и в том, что key не может дублироваться, а value не может быть нулевым.
Если дубликат ключа нужен для предотвращения конфликта, то почему в HashMap не было такого же ограничения. Кроме того, если value не может быть нулевым, это необходимо для предотвращения чего-то вроде get(), запускающего NPE, тогда почему HashMap тоже не перезапустил.
Это хороший вариант, и вы меня поняли, но мне все еще интересно, почему бы не применить ту же стратегию с HashMap.
Поток не обязательно является упорядоченной операцией. Поэтому, имея несколько ключей, будет неясно, какое сопоставление останется. Какая версия Java имеет Stream#toMap?
Извините за недопонимание, я имею в виду API .collect(Collectors.toMap()), кажется начиная с JDK 8, описание подредактирую.
Если вы отметите Collectors#toMap, там будет указано, что делать с повторяющимися ключами. Точно так же, как при использовании хэш-карты, вы знаете, что последний добавленный ключ обеспечит сопоставление. Я ничего не вижу о нулевом значении. Обратите внимание, что сопоставление ключа со значением null по сути означает удаление ключа с карты.
@matt в документации не сказано, что null значения запрещены, но на самом деле это так: ideone.com/Fq3soJ . Кроме того, сопоставление ключа со значением null — это не то же самое, что удаление ключа: все зависит от типа сопоставления. Некоторые карты (например, HashMap) различают несуществующий ключ и ключ с нулевым значением.
Collectors.toMap(p -> p.k, p -> p.v) создаёт именно HashMap, поэтому присоединяюсь к вопросу. Я нашел только такой обходной путь: var m = new HashMap<>(); Stream.of(arr).forEach(p -> m.put(p.k, p.v));
Во многом это обман stackoverflow.com/q/45210398/1553851
Также stackoverflow.com/q/24630963/1553851
Что касается проблемы уникальности, тот факт, что HashMap поддерживает замену ключей, на самом деле не является аргументом в пользу того, что Collector делает то же самое. В HashMap также есть putIfAbsent(), который сохраняет первое значение и отбрасывает второе. Кто скажет, что правильнее в общем случае?
@shmosel, ваш первый «обман» не имеет отношения к этому, так как он спрашивает о Map.of, который был представлен в Java 9, и возвращает карту другого типа, чем HashMap, тогда как этот вопрос касается функции, которая существовала в Java 8 и возвращает фактическое HashMap в большинстве реализации. Однако ответ Николая Парлога на этот вопрос содержит очень полезную информацию, имеющую отношение к этому вопросу.
@ k314159 Я сказал, что это во многом обман, потому что принцип тот же.
@KonstantinMakarov В документации для toMap() говорится, что нет никакой гарантии относительно типа возвращаемой карты, хотя они добавили эту публикацию Java 8.




В большинстве JDK, включая Oracle и OpenJDK, метод Collectors.toMap запрещает дубликаты, нулевые ключи и нулевые значения, потому что это возможно! Как ни странно, на самом деле это преимущество.
Прежде всего, давайте посмотрим, что говорит документация. Collectors.toMap говорит: «Нет никаких гарантий относительно типа, изменчивости, сериализуемости или потокобезопасности возвращаемой карты».
Поэтому мы можем только предположить, что возвращаемая карта — это какой-то класс, реализующий интерфейс Map. И, в свою очередь, в документации Map говорится, что «некоторые реализации запрещают нулевые ключи и значения». Следовательно, Collectors.toMap() действительно может запрещать нулевые значения.
Но подождите: в реализациях OpenJDK и Oracle мы видим, что Collectors.toMap() на самом деле возвращает HashMap, а класс HashMap четко задокументирован и допускает нулевые ключи и значения. Так почему же toMap() им не позволяет?
Представьте себе, что это так, и вы пишете программу, которая работает хорошо в течение многих лет, а затем передаете ее коллеге или клиенту, и они запускают ее с использованием JDK другого поставщика. Затем происходит сбой, потому что ваше приложение на самом деле вело себя не очень хорошо, но вы об этом не знали. Запрещая нулевые ключи и значения, эта реализация оказывает вам услугу.
Звучит солидно, и кажется, что у HashMap есть некоторые недостатки в дизайне, но уже слишком поздно что-то менять, не изменяя старый код.
HashMap не имеет к этому никакого отношения. Все дело в toMap коллекционере.
@KonstantinMakarov, речь действительно идет о сборщике toMap, а не о HashMap, но на самом деле существует большое количество мнений, что первоначальные дизайнеры HashMap допустили ошибку, разрешив ему поддерживать нулевые значения, и что он должен работать так " современные» карты, такие как Map.of().
Как и в другом комментарии Константину Макарову, toMap разрешает ключам быть нулевыми — см. System.out.println(Stream.of(new String[]{null, "a"}).collect(Collectors.toMap(k->k, v->"value:"+v)));
@DuncG хорошая мысль. Он действительно допускает нулевые ключи (но не нулевые значения). Но я бы не стал на это полагаться, если какая-то будущая реализация изменит тип карты на тот, который не допускает нулевых ключей (хотя этого, вероятно, никогда не произойдет).
@DuncG, из текста вопроса я понял, что речь идет о нуле values, а не о нуле keys.
Для коллекционера toMap:
keys допускается при реализации mergeFunctionNull для values запрещены, иначе они будут конфликтовать с функцией удаления записей.Коллектор toMap, содержащий BinaryOperator<U> mergeFunction, вызывает функцию map.merge, которой также передается BinaryOperator<U> mergeFunction:
BiConsumer<M, T> accumulator = (map, element) -> map.merge(
keyMapper.apply(element),
valueMapper.apply(element),
mergeFunction); // <---
Функция merge выглядит следующим образом:
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V>
remappingFunction) { // mergeFunction
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value); // <---
V oldValue = get(key);
V newValue = (oldValue == null)
? value
: remappingFunction.apply(oldValue, value);
if (newValue == null) {
remove(key); // <---
} else {
put(key, newValue);
}
return newValue;
}
То есть mergeFunction позволяет не только выбирать между старым и новым значениями одних и тех же ключей, но и полностью удалить запись.
Например:
class Pair {
final String k, v;
public Pair(String k, String v) { this.k = k; this.v = v; }
}
final var arr = new Pair[]{
new Pair("A", "val_1"),
new Pair("B", "val_2"),
new Pair("A", "val_3"),
};
System.out.println(Stream.of(arr).collect(Collectors
.toMap(p -> p.k, p -> p.v, (v1, v2) -> v1))); // {A=val_1, B=val_2}
System.out.println(Stream.of(arr).collect(Collectors
.toMap(p -> p.k, p -> p.v, (v1, v2) -> v2))); // {A=val_3, B=val_2}
System.out.println(Stream.of(arr).collect(Collectors
.toMap(p -> p.k, p -> p.v, (v1, v2) -> v1 + v2))); // {A=val_1val_3, B=val_2}
System.out.println(Stream.of(arr).collect(Collectors
.toMap(p -> p.k, p -> p.v, (v1, v2) -> null))); // {B=val_2}
Что делать, если мне не нужен такой toMap функционал коллектора?
Вы можете использовать свой собственный коллектор:
public static <T, K, V> Collector<T, ?, Map<K, V>> toHashMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper) {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> {
var map = new HashMap<K, V>();
list.forEach(element ->
map.put(
keyMapper.apply(element),
valueMapper.apply(element)));
return map;
});
}
System.out.println(Stream.of(arr).collect(toHashMap(p -> p.k, p -> p.v)));
Пункт 2. неверен (или, возможно, не для всех JDK), сборщик toMap допускает нулевые ключи, по крайней мере, в JDK14/17/21/22. Смотри System.out.println(Stream.of(new String[]{null, "a"}).collect(Collectors.toMap(k->k, v->"value:"+v)));
@DuncG, в первом пункте я имел в виду keys. Во 2-м пункте я имел в виду values.
Потому что ничто не говорит о том, что
toMapвозвращаетHashMapконкретно, так почему же вы ожидаете, что он будет подчиняться тому же правилу, что иHashMapконкретно?