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

我的同事向我提出了一个有趣的问题,我无法找到一个整洁而漂亮的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

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

备选方案1 :在“流”之外使用Map (或者更确切地说,在“每个”之外)。

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

备选方案2 :创建地图条目并流式传输,然后展平它们。 IMO它有点过于冗长而且不那么容易阅读。

 // Alt 2: A bit verbose and "new AbstractMap.SimpleEntry" feels as // a "hard" dependency to AbstractMap Map res2 = customers.stream() .map(p -> { Map.Entry firstEntry = new AbstractMap.SimpleEntry(p.first, p); Map.Entry 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 :这是另一个我提出的“最漂亮”的代码到目前为止,但它使用了三个arg版本的reduce ,第三个参数有点狡猾,就像在这个问题中找到的那样: 第三个参数的目的是’减少’函数在Java 8函数式编程中 。 此外, reduce似乎不适合这个问题,因为它是变异的并且并行流可能不适用于下面的方法。

 // Alt 3: using reduce. Not so pretty Map 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 = Customer(超级Mac),Johnny =客户(Johnny Puma),Mac =客户(Super Mac),Puma =客户(Johnny Puma)}
{Super = Customer(超级Mac),Johnny =客户(Johnny Puma),Mac =客户(Super Mac),Puma =客户(Johnny Puma)}
{Super = Customer(超级Mac),Johnny =客户(Johnny Puma),Mac =客户(Super Mac),Puma =客户(Johnny Puma)}

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

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

我认为您的替代品2和3可以重写为更清晰:

备选方案2

 Map 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 :通过改变HashMap来reduce代码滥用。 要进行可变缩减,请使用collect

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

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

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

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