Используйте поток Java для группировки по 2 клавишам одного типа - PullRequest
4 голосов
/ 26 марта 2019

Используя поток Java, как создать Карту из Списка для индексации по 2 ключам в одном и том же классе?

Я даю здесь код. Пример, я хотел бы, чтобы карта "personByName" получила всех людей.по firstName ИЛИ lastName, так что я бы хотел получить 3 «стивов»: когда это их firstName или фамилия.Я не знаю, как смешать 2 Collectors.groupingBy.

public static class Person {
    final String firstName;
    final String lastName;

    protected Person(String firstName, String lastName) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

}

@Test
public void testStream() {
    List<Person> persons = Arrays.asList(
            new Person("Bill", "Gates"),
            new Person("Bill", "Steve"),
            new Person("Steve", "Jobs"),
            new Person("Steve", "Wozniac"));

    Map<String, Set<Person>> personByFirstName = persons.stream().collect(Collectors.groupingBy(Person::getFirstName, Collectors.toSet()));
    Map<String, Set<Person>> personByLastName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));

    Map<String, Set<Person>> personByName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));// This is wrong, I want bot first and last name

    Assert.assertEquals("we should search by firstName AND lastName", 3, personByName.get("Steve").size()); // This fails

}

Я нашел обходной путь, зациклившись на 2 картах, но он не ориентирован на поток.

Ответы [ 7 ]

7 голосов
/ 26 марта 2019

Вы можете сделать это так:

Map<String, Set<Person>> personByName = persons.stream()
       .flatMap(p -> Stream.of(new SimpleEntry<>(p.getFirstName(), p),
                               new SimpleEntry<>(p.getLastName(), p)))
       .collect(Collectors.groupingBy(SimpleEntry::getKey,
                   Collectors.mapping(SimpleEntry::getValue, Collectors.toSet())));

Если вы добавите метод toString() в класс Person, вы сможете увидеть результат, используя:

List<Person> persons = Arrays.asList(
        new Person("Bill", "Gates"),
        new Person("Bill", "Steve"),
        new Person("Steve", "Jobs"),
        new Person("Steve", "Wozniac"));

// code above here

personByName.entrySet().forEach(System.out::println);

выход

Steve=[Steve Wozniac, Bill Steve, Steve Jobs]
Jobs=[Steve Jobs]
Bill=[Bill Steve, Bill Gates]
Wozniac=[Steve Wozniac]
Gates=[Bill Gates]
3 голосов
/ 26 марта 2019

Одним из способов было бы использование новейших JDK12 Collector.teeing:

Map<String, List<Person>> result = persons.stream()
       .collect(Collectors.teeing(
                Collectors.groupingBy(Person::getFirstName, 
                                      Collectors.toCollection(ArrayList::new)),
                Collectors.groupingBy(Person::getLastName),
                (byFirst, byLast) -> { 
                    byLast.forEach((last, peopleList) -> 
                           byFirst.computeIfAbsent(last, k -> new ArrayList<>())
                                  .addAll(peopleList));
                    return byFirst; 
                }));

Collectors.teeing, собирающих в два отдельных коллектора, а затем объединяющих результаты в окончательное значение.Из документов:

Возвращает коллектор, составленный из двух нижестоящих коллекторов.Каждый элемент, переданный получающемуся сборщику, обрабатывается обоими нижестоящими сборщиками, затем их результаты объединяются с использованием указанной функции слияния в окончательный результат.

Итак, приведенный выше код собирает карту по имениа также на карту по фамилии, а затем объединяет обе карты в итоговую карту путем итерации карты byLast и объединения каждой из ее записей в карту byFirst с помощью Map.computeIfAbsentметод.Наконец, возвращается карта byFirst.

Обратите внимание, что я собрал в Map<String, List<Person>> вместо Map<String, Set<Person>>, чтобы упростить пример.Если вам действительно нужна карта наборов, вы можете сделать это следующим образом:

Map<String, Set<Person>> result = persons.stream().
       .collect(Collectors.teeing(
                Collectors.groupingBy(Person::getFirstName, 
                                      Collectors.toCollection(LinkedHashSet::new)),
                Collectors.groupingBy(Person::getLastName, Collectors.toSet()),
                (byFirst, byLast) -> { 
                    byLast.forEach((last, peopleSet) -> 
                           byFirst.computeIfAbsent(last, k -> new LinkedHashSet<>())
                                  .addAll(peopleSet));
                    return byFirst; 
                }));

Имейте в виду, что если вам нужно иметь Set<Person> в качестве значений карт, класс Person долженреализовать методы hashCode и equals последовательно .

3 голосов
/ 26 марта 2019

Вы можете объединить два Map<String, Set<Person>>, например,

Map<String, Set<Person>> personByFirstName = 
                            persons.stream()
                                   .collect(Collectors.groupingBy(
                                                   Person::getFirstName, 
                                                   Collectors.toCollection(HashSet::new))
                                           );

persons.stream()
       .collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()))
       .forEach((str, set) -> personByFirstName.merge(str, set, (s1, s2) -> { 
            s1.addAll(s2); 
            return s1;
        }));

// personByFirstName contains now all personByName
0 голосов
/ 28 марта 2019

Попробуйте SetMultimap из Google Guava или из моей библиотеки Abacus-Util

SetMultimap<String, Person> result = Multimaps.newSetMultimap(new HashMap<>(), () -> new HashSet<>()); // by Google Guava.
// Or result = N.newSetMultimap(); // By Abacus-Util
persons.forEach(p -> {
     result.put(p.getFirstName(), p);
     result.put(p.getLastName(), p);
  });
0 голосов
/ 26 марта 2019

Если я правильно понял, вы хотите отобразить каждого персонажа дважды, один раз для имени и один раз для последнего. Для этого вам нужно как-то удвоить свой поток. Предполагая, что Couple - это некоторый существующий 2-кортеж (у Guava или Vavr есть хорошая реализация), вы можете:

persons.stream()
    .map(p -> new Couple(new Couple(p.firstName, p), new Couple(p.lastName, p)))
    .flatMap(c -> Stream.of(c.left, c.right)) // Stream of Couple(String, Person)
    .map(c -> new Couple(c.left, Arrays.asList(c.right)))
    .collect(Collectors.toMap(Couple::getLeft, Couple::getRight, Collection::addAll));

Я не проверял это, но концепция такова: создайте поток (имя, человек), (фамилия, человек) ... для каждого человека, затем просто отобразите левое значение каждой пары. AsList должен иметь коллекцию в качестве значения. Если вам нужен Set, измените последнюю строку с .collect(Collectors.toMap(Couple::getLeft, c -> new HashSet(c.getRight), Collection::addAll))

0 голосов
/ 26 марта 2019

Вы не можете набирать свои карты несколькими значениями. Для того, что вы хотите достичь, у вас есть три варианта:

  1. Объедините ваши карты "personByFirstName" и "personByLastName", у вас будут дублированные значения (например, Билл Гейтс будет на карте под ключом Bill, а также на карте под ключом Gates) , @Andreas answer - хороший способ сделать это на основе потока.

  2. Используйте библиотеку индексации, такую ​​как lucene, и индексируйте все ваши объекты Person по имени и фамилии.

  3. Потоковый подход - он не будет эффективен для больших наборов данных, но вы можете транслировать свою коллекцию и использовать filter для получения совпадений:

persons
    .stream()
    .filter(p -> p.getFirstName().equals("Steve") 
         || p.getLastName().equals("Steve"))
    .collect(Collectors.asList());

(Я написал синтаксис из памяти, так что вам, возможно, придется его настроить).

0 голосов
/ 26 марта 2019

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

Если вы просто хотите отфильтровать всех Стивов, сначала отфильтруйте, а потом соберите:

persons.stream
  .filter(p -> p.getFirstName().equals('Steve') || p.getLastName.equals('Steve'))
  .collect(toList());

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

...