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

我的一位同事向我提出了一个有趣的问题,我无法找到一个整洁漂亮的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:使用“流”的外部(或更确切地说是外部)。MapforEach

// 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:创建地图条目并对其进行流化,然后对其进行流化。IMO它有点太冗长了,不那么容易阅读。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:这是我迄今为止想出的另一个“最漂亮”的代码,但它使用了三个参数版本,第三个参数有点狡猾,如这个问题所示:Java 8 函数式编程中“reduce”函数的第三个参数的目的。此外,它似乎不太适合这个问题,因为它正在发生变化,并且并行流可能无法与下面的方法一起使用。reducereduce

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

结果将是:

{Super=Customer(Super Mac), Johnny=Customer(Johnny Puma), Mac=Customer(Super Mac), Puma=Customer(Johnny Puma)}
{Super=Customer(Super Mac), Johnny=Customer(Johnny Puma), Mac=Customer(Super Mac), Puma=Customer(Johnny Puma)}
{Super=Customer(Super Mac), Johnny=Customer(Johnny Puma), Mac=Customer(Super Mac), Mac=Customer(Super Mac), Puma=Customer(Johnny Puma)}

所以,现在来回答我的问题:我应该如何以Java 8有序的方式流经,然后以某种方式收集它,作为一个你将整个东西拆分为两个键(AND)的地方,即映射两次。我不想使用任何第三方库,我不想像alt 1那样在流之外使用地图。还有其他不错的选择吗?List<Customer>Map<String, Customer>firstlastCustomer

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


答案 1

我认为你的备选方案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 来滥用代码。要执行可变还原,请使用 :reducecollect

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) -> btoMap(...)toMap(...)groupingBy(...)


答案 2