Почему ключ Stream toMap() не может дублироваться, а значение не может быть нулевым

Как мы все знаем, HashMap позволяет быть только одному keynull, и при дублировании он заменит старый. Также нет ограничений на value.

Но API .collect(Collectors.toMap()) в потоке ограничен как в том, так и в том, что key не может дублироваться, а value не может быть нулевым.

Если дубликат ключа нужен для предотвращения конфликта, то почему в HashMap не было такого же ограничения. Кроме того, если value не может быть нулевым, это необходимо для предотвращения чего-то вроде get(), запускающего NPE, тогда почему HashMap тоже не перезапустил.

Потому что ничто не говорит о том, что toMap возвращает HashMap конкретно, так почему же вы ожидаете, что он будет подчиняться тому же правилу, что и HashMap конкретно?

Federico klez Culloca 24.06.2024 16:37

Это хороший вариант, и вы меня поняли, но мне все еще интересно, почему бы не применить ту же стратегию с HashMap.

great-jin 24.06.2024 16:44

Поток не обязательно является упорядоченной операцией. Поэтому, имея несколько ключей, будет неясно, какое сопоставление останется. Какая версия Java имеет Stream#toMap?

matt 24.06.2024 16:52

Извините за недопонимание, я имею в виду API .collect(Collectors.toMap()), кажется начиная с JDK 8, описание подредактирую.

great-jin 24.06.2024 16:56

Если вы отметите Collectors#toMap, там будет указано, что делать с повторяющимися ключами. Точно так же, как при использовании хэш-карты, вы знаете, что последний добавленный ключ обеспечит сопоставление. Я ничего не вижу о нулевом значении. Обратите внимание, что сопоставление ключа со значением null по сути означает удаление ключа с карты.

matt 24.06.2024 17:03

@matt в документации не сказано, что null значения запрещены, но на самом деле это так: ideone.com/Fq3soJ . Кроме того, сопоставление ключа со значением null — это не то же самое, что удаление ключа: все зависит от типа сопоставления. Некоторые карты (например, HashMap) различают несуществующий ключ и ключ с нулевым значением.

k314159 24.06.2024 18:18

Collectors.toMap(p -> p.k, p -> p.v) создаёт именно HashMap, поэтому присоединяюсь к вопросу. Я нашел только такой обходной путь: var m = new HashMap<>(); Stream.of(arr).forEach(p -> m.put(p.k, p.v));

Konstantin Makarov 24.06.2024 18:26

Во многом это обман stackoverflow.com/q/45210398/1553851

shmosel 24.06.2024 20:36

Также stackoverflow.com/q/24630963/1553851

shmosel 24.06.2024 20:48

Что касается проблемы уникальности, тот факт, что HashMap поддерживает замену ключей, на самом деле не является аргументом в пользу того, что Collector делает то же самое. В HashMap также есть putIfAbsent(), который сохраняет первое значение и отбрасывает второе. Кто скажет, что правильнее в общем случае?

shmosel 24.06.2024 20:50

@shmosel, ваш первый «обман» не имеет отношения к этому, так как он спрашивает о Map.of, который был представлен в Java 9, и возвращает карту другого типа, чем HashMap, тогда как этот вопрос касается функции, которая существовала в Java 8 и возвращает фактическое HashMap в большинстве реализации. Однако ответ Николая Парлога на этот вопрос содержит очень полезную информацию, имеющую отношение к этому вопросу.

k314159 24.06.2024 22:05

@ k314159 Я сказал, что это во многом обман, потому что принцип тот же.

shmosel 24.06.2024 22:08

@KonstantinMakarov В документации для toMap() говорится, что нет никакой гарантии относительно типа возвращаемой карты, хотя они добавили эту публикацию Java 8.

David Conrad 25.06.2024 19:42
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
13
167
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В большинстве JDK, включая Oracle и OpenJDK, метод Collectors.toMap запрещает дубликаты, нулевые ключи и нулевые значения, потому что это возможно! Как ни странно, на самом деле это преимущество.

Прежде всего, давайте посмотрим, что говорит документация. Collectors.toMap говорит: «Нет никаких гарантий относительно типа, изменчивости, сериализуемости или потокобезопасности возвращаемой карты».

Поэтому мы можем только предположить, что возвращаемая карта — это какой-то класс, реализующий интерфейс Map. И, в свою очередь, в документации Map говорится, что «некоторые реализации запрещают нулевые ключи и значения». Следовательно, Collectors.toMap() действительно может запрещать нулевые значения.

Но подождите: в реализациях OpenJDK и Oracle мы видим, что Collectors.toMap() на самом деле возвращает HashMap, а класс HashMap четко задокументирован и допускает нулевые ключи и значения. Так почему же toMap() им не позволяет?

Представьте себе, что это так, и вы пишете программу, которая работает хорошо в течение многих лет, а затем передаете ее коллеге или клиенту, и они запускают ее с использованием JDK другого поставщика. Затем происходит сбой, потому что ваше приложение на самом деле вело себя не очень хорошо, но вы об этом не знали. Запрещая нулевые ключи и значения, эта реализация оказывает вам услугу.

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

great-jin 25.06.2024 03:31

HashMap не имеет к этому никакого отношения. Все дело в toMap коллекционере.

Konstantin Makarov 25.06.2024 04:56

@KonstantinMakarov, речь действительно идет о сборщике toMap, а не о HashMap, но на самом деле существует большое количество мнений, что первоначальные дизайнеры HashMap допустили ошибку, разрешив ему поддерживать нулевые значения, и что он должен работать так " современные» карты, такие как Map.of().

k314159 25.06.2024 10:20

Как и в другом комментарии Константину Макарову, toMap разрешает ключам быть нулевыми — см. System.out.println(Stream.of(new String[]{null, "a"}).collect(Collectors.toMap(k->k, v->"value:"+v)));

DuncG 25.06.2024 11:48

@DuncG хорошая мысль. Он действительно допускает нулевые ключи (но не нулевые значения). Но я бы не стал на это полагаться, если какая-то будущая реализация изменит тип карты на тот, который не допускает нулевых ключей (хотя этого, вероятно, никогда не произойдет).

k314159 25.06.2024 12:04

@DuncG, из текста вопроса я понял, что речь идет о нуле values, а не о нуле keys.

Konstantin Makarov 25.06.2024 12:33

Для коллекционера toMap:

  1. Дублирование keys допускается при реализации mergeFunction
  2. Null для 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 25.06.2024 11:47

@DuncG, в первом пункте я имел в виду keys. Во 2-м пункте я имел в виду values.

Konstantin Makarov 25.06.2024 12:29

Другие вопросы по теме

Свойство цвета строки состояния Android не применяется к действиям, выполненным из предустановки действий «Пустые представления»
Область столбца-заполнителя полосы прокрутки в стиле TableView
Невозможно получить доступ к классу Retrofit2.Response. Проверьте путь к классам вашего модуля на предмет отсутствующих или конфликтующих зависимостей
Ошибка запуска log4j2. Нераспознанный спецификатор преобразования и спецификатор формата
Обработка внутренней ошибки сервера, когда эмитент OAuth недоступен в WebFluxSecurity
Почему нам следует устанавливать для -XX:InitialRAMPercentage и -XX:MaxRAMPercentage одно и то же значение для облачной среды?
Как сделать массив потокобезопасным в Java?
Модульное тестирование Новый метод обмена RestClient Spring() с покрытием кода
Org.hibernate.Exception.JDBCConnectionException: невозможно открыть соединение JDBC
Скрипт .bat не может найти javaw при вызове из Python os.command