我的一位同事向我提出了一个有趣的问题,但是我找不到一个整洁漂亮的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 :在“stream”之外(或在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 :创建 map 条目并将其流化,然后对其进行flatMap。海事组织,这有点太冗长,不太容易阅读。
// 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的三参数版本,第三个参数有点奇怪,如此问题中所示:Purpose of third argument to 'reduce' function in Java 8 functional programming。此外,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);

结果将是:



所以,现在我的问题是:我应该如何以Java 8的有序方式流过List<Customer>,然后以某种方式将其收集为Map<String, Customer>,在其中将整个内容拆分为两个键(firstlast),即Customer被映射两次。我不想使用任何第三方库,也不想像alt 1那样在流之外使用 map 。还有其他不错的选择吗?

完整的代码可以是found on hastebin,用于简单的复制粘贴以使整个过程运行。

最佳答案

我认为您的备选方案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 :您的代码通过使HashMap发生变异来滥用reduce。要进行可变减少,请使用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(...)等。

10-06 15:55
查看更多