Почему Collectors.toMap выводит значение вместо ключа на Duplicate Key error?

На самом деле это вопрос о мелочах, но мне кажется, что здесь что-то не так. Если вы добавляете дубликаты ключей с помощью метода Collectors.toMap, он выдает исключение с сообщением "дублирующий ключ". Почему значение указано, а не ключ? Или даже оба? Это действительно вводит в заблуждение, не так ли?

Здесь немного теста, чтобы продемонстрировать поведение:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TestToMap {

    public static void main(String[] args) {

        List<Something> list = Arrays.asList(
            new Something("key1", "value1"),
            new Something("key2", "value2"),
            new Something("key3", "value3a"),
            new Something("key3", "value3b"));

        Map<String, String> map = list.stream().collect(Collectors.toMap(o -> o.key, o -> o.value));
        System.out.println(map);
    }

    private static class Something {
        final String key, value;

        Something(final String key, final String value){
            this.key = key;
            this.value = value;
        }
    }
}

Ответ 1

Об этом сообщается как об ошибке, см. JDK-8040892, и она исправлена ​​в Java 9. Чтение commit исправление этого, новое сообщение об исключении будет

String.format("Duplicate key %s (attempted merging values %s and %s)", k, u, v)

где k - дубликат, а u и v - это два конфликтующих значения, сопоставленные с одним и тем же ключом.

Ответ 2

Как уже утверждают другие ответы, это ошибка, которая будет исправлена на Java 9. Причина, почему возникла ошибка, заключается в том, что toMap полагается на Map.merge которого есть подпись

V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)

Этот метод будет вставлять сопоставление key value, если ранее не было предыдущего сопоставления для key, иначе remappingFunction будет вычисляться для вычисления нового значения. Таким образом, если дублирующиеся ключи не разрешены, его прямолинейный способ предоставить функцию remappingFunction которая безоговорочно выдаст исключение, и вы сделаете это. Но... если вы посмотрите на подпись функции, вы заметите, что эта функция получает только два значения, которые нужно объединить, а не ключ.

Когда throwingMerger был реализован для Java 8, было упущено, что первый аргумент не является ключевым, но, что еще хуже, его не исправить.

Вы заметите, что если вы попытаетесь обеспечить альтернативное слияние с помощью перегруженного коллектора toMap. Ключевое значение просто не имеет значения в данный момент. Разработчикам Java 9 пришлось изменить всю реализацию toMap для toMap случая, чтобы иметь возможность предоставлять сообщение об исключении, сообщающее об уязвимом ключе...

Ответ 3

Это ошибка в Jdk 8. В сообщении, которое должно быть указано, должно отображаться как минимум два значения, для которых есть столкновение ключей или в идеале - ключ, для которого произошло столкновение. Прикреплена ссылка для того же

Ответ 4

Если у вас есть эта проблема и вы вынуждены использовать Java 8 в своем проекте, вам придется создать свой собственный сборщик, чтобы получить содержательное сообщение об ошибке.

Ниже вы можете найти пример коллекционера, который дает вам содержательное сообщение, подобное тому, которое вы получите с Java 9:

private static class Something {
    private final String k;
    private final String v;

    public Something(String k, String v) {
        this.k = k;
        this.v = v;
    }
}

public static void main(String[] args) throws IOException, ParseException {
    List<Something> list = Arrays.asList(
            new Something("key1", "value1"),
            new Something("key2", "value2"),
            new Something("key3", "value3a"),
            new Something("key3", "value3b"));

    Collector<Something, ?, Map<String, String>> eloquentCollector =
            Collector.of(HashMap::new, (map, something) -> putUnique(map, something.k, something.v),
                    (m1, m2) -> { m2.forEach((k, v) -> putUnique(m1, k, v)); return m1; });

    Map<String, String> map = list.stream().collect(eloquentCollector);
    System.out.println(map);
}

private static <K,V> void putUnique(Map<K,V> map, K key, V v1){
    V v2 = map.putIfAbsent(key, v1);
    if(v2 != null) throw new IllegalStateException(
            String.format("Duplicate key '%s' (attempted merging incoming value '%s' with existing '%s')", key, v1, v2));
}

Если вы запустите этот код, вы увидите следующее сообщение об ошибке, указывающее на дубликат ключа и значения слияния:

Дублировать ключ "key3" (попытка слияния значений value3a и value3b)

Вы должны получить одно и то же сообщение об исключении, если вы используете в примере выше parallelStream(), например, например:

Map<String, String> map = list.parallelStream().collect(eloquentCollector);

Кредиты Холгеру за то, что они указали, что исходный ответ не подходит для параллельных потоков.