Java 8 Streams:根据不同的属性多次映射同一个对象

was*_*ren 26 java lambda functional-programming java-8 collectors

我的一位同事向我提出了一个有趣的问题,我无法找到一个整洁而漂亮的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 + ")";
    }
}
Run Code Online (Sandbox Code Playgroud)

将其设置为List<Customer>:

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

备选方案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);
});
Run Code Online (Sandbox Code Playgroud)

方案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));
Run Code Online (Sandbox Code Playgroud)

备选方案3:这是另一个我提出的"最漂亮"的代码到目前为止,但是它使用的是三个arg版本,reduce第三个参数有点狡猾,就像在这个问题中找到的那样:'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 */);
Run Code Online (Sandbox Code Playgroud)

如果上面的代码打印如下:

System.out.println(res1);
System.out.println(res2);
System.out.println(res3);
Run Code Online (Sandbox Code Playgroud)

结果将是:

{Super = Customer(Super Mac),Johnny = Customer(Johnny Puma),Mac = Customer(超级Mac),Puma = Customer(Johnny Puma)}
{Super = Customer(超级Mac)),Johnny = Customer(Johnny Puma), Mac =客户(超级Mac),Puma =客户(Johnny Puma)}
{超级=客户(超级Mac),Johnny =客户(Johnny Puma),Mac =客户(超级Mac),Puma =客户(Johnny Puma)}

那么,现在我的问题是:我应该如何以Java 8有序的方式流式传输List<Customer>然后以某种方式将其收集为Map<String, Customer>将整个事物拆分为两个键(firstAND last),即Customer映射两次.我不想使用任何第三方库,我不想像在alt 1中那样使用流之外的地图.还有其他不错的选择吗?

完整的代码可以在hastebin找到,用于简单的复制粘贴,以使整个事情运行.

Mis*_*sha 20

我认为您的替代品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));
Run Code Online (Sandbox Code Playgroud)

备选方案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
    );
Run Code Online (Sandbox Code Playgroud)

请注意,这些并不完全相同.如果存在重复键,备选2将抛出异常,而备选3将静默覆盖条目.

如果在重复密钥的情况下覆盖条目是你想要的,我个人更喜欢备选方案3.我立即清楚它的作用.它最类似于迭代解决方案.我希望它更具性能,因为备选方案2必须为每个客户进行一系列分配,并进行所有平面映射.

但是,备选方案2通过将条目的生成与其聚合分开,具有优于备选方案3的巨大优势.这为您提供了极大的灵活性.例如,如果要更改备选2以覆盖重复键上的条目而不是抛出异常,则只需添加(a,b) -> btoMap(...).如果您决定要将匹配的条目收集到列表中,那么您只需要替换toMap(...)groupingBy(...)等等.

  • 这不仅仅是'collect()`是"首选方式"; 使用`reduce()`这是完全错误的.减少价值; 备选方案3经历的扭曲违反了`reduce()`的规范.如果稍后您尝试并行运行该流,您将得到错误的答案.另一方面,使用`collect()`作为Misha节目设计用于处理这种情况,并且可以顺序或并行安全. (5认同)
  • @BrianGoetz谢谢你,Brian.当你发表你的评论时,我修改了我的答案,使其更加有力. (2认同)