使用Java流按相同类型的2个键分组

时间:2019-03-26 16:21:21

标签: java java-8 java-stream

使用Java流,如何在同一类上通过2个键创建从列表到索引的映射?

我在这里给出一个代码示例,我希望地图“ personByName”通过firstName或lastName获取所有人,因此我想获取3个“ steve”:当它们是firstName或lastname时。我不知道如何混合使用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

}

我通过在两张地图上循环找到了一种解决方法,但是它不是面向流的。

7 个答案:

答案 0 :(得分:7)

您可以这样做:

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]

答案 1 :(得分:3)

例如,您可以合并两个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

答案 2 :(得分:3)

一种方法是使用最新的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类必须一致地实现hashCodeequals方法。

答案 3 :(得分:0)

如果您想要一个真正的面向流的解决方案,请确保您不产生任何大的中间集合,否则大多数流的感觉就会消失。

如果只想过滤所有Steves,请先过滤,然后再收集:

,[^\-]*(?<=\d)-(?=\d)

如果您想使用流元素来做复杂的事情,例如将元素放入多个集合中,或在几个键下的映射中,只需使用+消耗流,然后在其中编写所需的任何处理逻辑即可。

答案 4 :(得分:0)

您不能通过多个值来键入地图。对于您想要实现的目标,您有三种选择:

  1. 组合您的“ personByFirstName”和“ personByLastName”地图,您将获得重复的值(例如,比尔·盖茨将在地图上,位于Bill键下,并且还将在地图中,位于{{{ 1}})。 @Andreas答案提供了一种基于流的良好方法。

  2. 使用像lucene这样的索引库,并按名字和姓氏索引所有Person对象。

  3. 流方法-在大型数据集上将不起作用,但是您可以流式处理集合并使用Gates进行匹配:

filter
     

(我已经从内存中编写了语法,因此您可能需要对其进行调整)。

答案 5 :(得分:0)

如果我做对了,您希望每个人映射两次,一次映射为名字,一次映射为名字。 为此,您必须以某种方式使流增加一倍。假设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 chenge,请在最后一行加上.collect(Collectors.toMap(Couple::getLeft, c -> new HashSet(c.getRight), Collection::addAll))

答案 6 :(得分:0)

尝试Google Guava或我的图书馆Abacus-Util中的SetMultimap

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);
  });