Подтвердить что ты не робот

Потоки Java 8: отображение одного и того же объекта несколько раз на основе разных свойств

Мне представилась интересная проблема со стороны моего коллеги, и я не смог найти четкое и красивое решение Java 8. Проблема состоит в том, чтобы передать список POJO, а затем собрать их на карте, основанной на нескольких свойствах - сопоставление вызывает POJO несколько раз

Представьте следующее POJO:

private static class Customer {
    public String first;
    public String last;

    public Customer(String first, String last) {
        this.first = first;
        this.last = last;
    }

    public String toString() {
        return "Customer(" + first + " " + last + ")";
    }
}

Задайте его как List<Customer>:

// The list of customers
List<Customer> customers = Arrays.asList(
        new Customer("Johnny", "Puma"),
        new Customer("Super", "Mac"));

Альтернатива 1. Используйте Map вне "потока" (или, скорее, вне forEach).

// Alt 1: not pretty since the resulting map is "outside" of
// the stream. If parallel streams are used it must be
// ConcurrentHashMap
Map<String, Customer> res1 = new HashMap<>();
customers.stream().forEach(c -> {
    res1.put(c.first, c);
    res1.put(c.last, c);
});

Альтернатива 2. Создайте записи в карте и поместите их, а затем flatMap их. IMO это немного слишком многословно и не так просто читать.

// Alt 2: A bit verbose and "new AbstractMap.SimpleEntry" feels as
// a "hard" dependency to AbstractMap
Map<String, Customer> res2 =
        customers.stream()
                .map(p -> {
                    Map.Entry<String, Customer> firstEntry = new AbstractMap.SimpleEntry<>(p.first, p);
                    Map.Entry<String, Customer> lastEntry = new AbstractMap.SimpleEntry<>(p.last, p);
                    return Stream.of(firstEntry, lastEntry);
                })
                .flatMap(Function.identity())
                .collect(Collectors.toMap(
                        Map.Entry::getKey, Map.Entry::getValue));

Альтернатива 3. Это еще одна, к которой я придумал "самый красивый" код, но он использует версию с тремя аргументами reduce, а третий параметр немного изворотлив, найденный в этом вопросе: Назначение третьего аргумента функции "уменьшить" в функциональном программировании Java 8. Кроме того, reduce не кажется подходящим для этой проблемы, так как он мутирует и параллельные потоки могут не работать с подходом ниже.

// Alt 3: using reduce. Not so pretty
Map<String, Customer> res3 = customers.stream().reduce(
        new HashMap<>(),
        (m, p) -> {
            m.put(p.first, p);
            m.put(p.last, p);
            return m;
        }, (m1, m2) -> m2 /* <- NOT USED UNLESS PARALLEL */);

Если вышеприведенный код напечатан следующим образом:

System.out.println(res1);
System.out.println(res2);
System.out.println(res3);

Результат:

{Супер = Клиент (Super Mac), Джонни = Клиент (Джонни Пума), Mac = Клиент (Супер Mac), Пума = Клиент (Джонни Пума)}
{Super = Клиент (Super Mac), Джонни = Клиент (Джонни Пума), Mac = Клиент (Супер Mac), Пума = Клиент (Джонни Пума)}
{Super = Клиент (Super Mac), Джонни = Клиент (Джонни Пума), Mac = Клиент (Супер Mac), Пума = Клиент (Джонни Пума)}

Итак, теперь на мой вопрос: как я должен, упорядоченным способом Java 8, передать поток через List<Customer>, а затем как-то собрать его как Map<String, Customer>, где вы разделили все на две клавиши (first И last), т.е. Customer отображается дважды. Я не хочу использовать какие-либо сторонние библиотеки, я не хочу использовать карту вне потока, как в alt 1. Есть ли другие приятные альтернативы?

Полный код может быть найден на hastebin для простой копии-вставки, чтобы все это выполнялось.

4b9b3361

Ответ 1

Я думаю, что ваши альтернативы 2 и 3 могут быть переписаны, чтобы быть более ясными:

Альтернатива 2:

Map<String, Customer> res2 = customers.stream()
    .flatMap(
        c -> Stream.of(c.first, c.last)
        .map(k -> new AbstractMap.SimpleImmutableEntry<>(k, c))
    ).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

Альтернатива 3: ваш код злоупотребляет reduce, изменяя HashMap. Чтобы выполнить изменчивое восстановление, используйте collect:

Map<String, Customer> res3 = customers.stream()
    .collect(
        HashMap::new, 
        (m,c) -> {m.put(c.first, c); m.put(c.last, c);}, 
        HashMap::putAll
    );

Обратите внимание, что они не идентичны. Альтернатива 2 будет генерировать исключение, если есть дубликаты ключей, в то время как Альтернатива 3 будет молча перезаписывать записи.

Если перезапись записей в случае дубликатов ключей - это то, что вы хотите, я лично предпочту альтернативу 3. Мне сразу понятно, что она делает. Это наиболее похоже на итерационное решение. Я ожидаю, что это будет более результативным, так как Альтернатива 2 должна сделать кучу распределений для каждого клиента со всеми этими плоскими картами.

Однако альтернатива 2 имеет огромное преимущество перед Альтернативой 3, отделяя производство записей от их агрегации. Это дает вам большую гибкость. Например, если вы хотите изменить вариант 2, чтобы перезаписать записи дублированных ключей вместо того, чтобы бросать исключение, просто добавьте (a,b) -> b в toMap(...). Если вы решите, что хотите собрать соответствующие записи в список, все, что вам нужно сделать, это заменить toMap(...) на groupingBy(...) и т.д.