Java 8 Streams:根据不同的属性多次映射同一个对象
Java 8 Streams: Map the same object multiple times based on different properties
我的一位同事向我提出了一个有趣的问题,但我找不到简洁漂亮的 Java8 解决方案。问题是通过 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:创建地图条目并流式传输它们,然后 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));
备选方案 3:这是到目前为止我想出的另一个 "prettiest" 代码,但它使用 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);
结果将是:
{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), Puma=Customer(Johnny Puma)}
所以,现在回答我的问题:我应该如何以 Java 8 有序的方式流过 List<Customer>
,然后以某种方式将其收集为 Map<String, Customer>
并在其中拆分整个事情作为两个键(first
和 last
)即 Customer
被映射了两次。我不想使用任何第 3 方库,我不想像 alt 1 那样在流之外使用地图。还有其他不错的选择吗?
完整代码可以是 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(...)
,等等
我的一位同事向我提出了一个有趣的问题,但我找不到简洁漂亮的 Java8 解决方案。问题是通过 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:创建地图条目并流式传输它们,然后 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));
备选方案 3:这是到目前为止我想出的另一个 "prettiest" 代码,但它使用 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);
结果将是:
{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), Puma=Customer(Johnny Puma)}
所以,现在回答我的问题:我应该如何以 Java 8 有序的方式流过 List<Customer>
,然后以某种方式将其收集为 Map<String, Customer>
并在其中拆分整个事情作为两个键(first
和 last
)即 Customer
被映射了两次。我不想使用任何第 3 方库,我不想像 alt 1 那样在流之外使用地图。还有其他不错的选择吗?
完整代码可以是 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(...)
,等等