Java 8 Streams:根据不同的属性多次映射相同的对象 [英] Java 8 Streams: Map the same object multiple times based on different properties
问题描述
我的一位同事向我介绍了一个有趣的问题,我无法找到一个整洁而漂亮的Java 8解决方案。问题在于通过POJO列表进行流式处理,然后根据多个属性在地图中收集它们 - 映射导致POJO多次出现
设想下面的POJO :private static class Customer {
,第三个参数在这个问题中有点不礼貌:在Java 8函数式编程中为'reduce'函数提供第三个参数的目的一>。此外,
public String first;
public String last;
$ b $ public public(String first,String last){
this.first = first;
this.last = last;
public String toString(){
returnCustomer(+ first ++ last +);
$ b将它设置为
列表< Customer>
://客户列表
列表< Customer> customers = Arrays.asList(
新客户(Johnny,Puma),
新客户(Super,Mac));
选择1 :使用
code>>stream之外(或者在
forEach
之外)。
// Alt 1:因为结果地图是
//流的外部,所以不太漂亮。如果使用并行流,它必须是
// ConcurrentHashMap
Map< String,Customer> res1 = new HashMap<>();
customers.stream()。forEach(c - > {
res1.put(c.first,c);
res1.put(c.last,c);
});
替代方案2 :创建地图条目并进行流式处理,然后
flatMap
它们。海事组织有点太冗长,也不容易阅读。// Alt 2:有点冗长,new AbstractMap .SimpleEntry感觉为
//对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 reduce
似乎不太适合这个问题,因为它是变异的,并行流可能不适用于下面的方法。
// Alt 3:使用reduce。不是很漂亮
地图< 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 / *< - 非使用,除非并行* /);
如果上面的代码是这样打印的:
的System.out.println(RES1);
System.out.println(res2);
System.out.println(res3);
结果如下:
{超级=客户(超级Mac),约翰尼=客户(约翰尼彪马),Mac =客户(超级Mac),Puma =客户(约翰尼彪马)}
{客户(超级Mac),Johnny =客户(Johnny Puma),Mac =客户(超级Mac),Puma =客户(Johnny Puma)}
b $ b {超级=客户(超级Mac),Johnny =客户(Johnny Puma),Mac = Customer(超级Mac),Puma =客户(Johnny Puma)}
现在回到我的问题:我应该如何以Java 8的顺序通过
List< Customer>
,然后以某种方式将其收集为Map< ; String,Customer>
将整个事物分成两个键(first
ANDlast
>)即Customer
被映射两次。我不想使用任何第三方库,我不想在alt 1中使用流之外的地图。还有其他不错的选择吗?
完整的代码可以在hastebin上找到,以便简单的复制粘贴来运行整个事情。
解决方案我认为您的备选方案2和3可以重写成更清晰:
替代2 :
地图< 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 :您的代码滥用
reduce
通过改变HashMap。使用collect
:地图< 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必须为每个客户进行一系列的平面分配。
然而,Alternative 2与Alternative相比具有巨大的优势3通过将条目的生成与其聚合分离。这给你一个很大的灵活性。例如,如果您想更改备选方案2以覆盖重复键上的条目而不是抛出异常,则只需添加
(a,b) - > b
到toMap(...)
。如果您决定要将匹配条目收集到列表中,您只需用groupingBy(。)替换
等。toMap(...)
。 ..)I was presented with an interesting problem by a colleague of mine and I was unable to find a neat and pretty Java 8 solution. The problem is to stream through a list of POJOs and then collect them in a map based on multiple properties - the mapping causes the POJO to occur multiple times
Imagine the following 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 + ")"; } }
Set it up as a
List<Customer>
:// The list of customers List<Customer> customers = Arrays.asList( new Customer("Johnny", "Puma"), new Customer("Super", "Mac"));
Alternative 1: Use a
Map
outside of the "stream" (or rather outsideforEach
).// 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); });
Alternative 2: Create map entries and stream them, then
flatMap
them. IMO it is a bit too verbose and not so easy to read.// 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));
Alternative 3: This is another one that I came up with the "prettiest" code so far but it uses the three-arg version of
reduce
and the third parameter is a bit dodgy as found in this question: Purpose of third argument to 'reduce' function in Java 8 functional programming. Furthermore,reduce
does not seem like a good fit for this problem since it is mutating and parallel streams may not work with the approach below.// 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 */);
If the above code is printed like this:
System.out.println(res1); System.out.println(res2); System.out.println(res3);
The result would be:
{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)}So, now to my question: How should I, in a Java 8 orderly fashion, stream through the
List<Customer>
and then somehow collect it as aMap<String, Customer>
where you split the whole thing as two keys (first
ANDlast
) i.e. theCustomer
is mapped twice. I do not want to use any 3rd party libraries, I do not want to use a map outside of the stream as in alt 1. Are there any other nice alternatives?The full code can be found on hastebin for simple copy-paste to get the whole thing running.
解决方案I think your alternatives 2 and 3 can be re-written to be more clear:
Alternative 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));
Alternative 3: Your code abuses
reduce
by mutating the HashMap. To do mutable reduction, usecollect
:Map<String, Customer> res3 = customers.stream() .collect( HashMap::new, (m,c) -> {m.put(c.first, c); m.put(c.last, c);}, HashMap::putAll );
Note that these are not identical. Alternative 2 will throw an exception if there are duplicate keys while Alternative 3 will silently overwrite the entries.
If overwriting entries in case of duplicate keys is what you want, I would personally prefer Alternative 3. It is immediately clear to me what it does. It most closely resembles the iterative solution. I would expect it to be more performant as Alternative 2 has to do a bunch of allocations per customer with all that flatmapping.
However, Alternative 2 has a huge advantage over Alternative 3 by separating the production of entries from their aggregation. This gives you a great deal of flexibility. For example, if you want to change Alternative 2 to overwrite entries on duplicate keys instead of throwing an exception, you would simply add
(a,b) -> b
totoMap(...)
. If you decide you want to collect matching entries into a list, all you would have to do is replacetoMap(...)
withgroupingBy(...)
, etc.这篇关于Java 8 Streams:根据不同的属性多次映射相同的对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!