I\'m porting a piece of code from .NET to Java and stumbled upon a scenario where I want to use stream to map & reduce.
class Content
{
private String
The most generic way to deal with such tasks would be to combine the result of multiple collectors into a single one.
Using the jOOL library, you could have the following:
Content content =
Seq.seq(contentList)
.collect(
Collectors.mapping(Content::getA, Collectors.joining(", ")),
Collectors.mapping(Content::getB, Collectors.joining(", ")),
Collectors.mapping(Content::getC, Collectors.joining(", "))
).map(Content::new);
This creates a Seq
from the input list and combines the 3 given collectors to create a Tuple3
, which is simply a holder for 3 values. Those 3 values are then mapped into a Content
using the constructor new Content(a, b, c)
. The collector themselves are simply mapping each Content
into its a
, b
or c
value and joining the results together separated with a ", "
.
Without third-party help, we could create our own combiner collector like this (this is based of StreamEx pairing collector, which does the same thing for 2 collectors). It takes 3 collectors as arguments and performs a finisher operation on the result of the 3 collected values.
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
public static <T, A1, A2, A3, R1, R2, R3, R> Collector<T, ?, R> combining(Collector<? super T, A1, R1> c1, Collector<? super T, A2, R2> c2, Collector<? super T, A3, R3> c3, TriFunction<? super R1, ? super R2, ? super R3, ? extends R> finisher) {
final class Box<A, B, C> {
A a; B b; C c;
Box(A a, B b, C c) {
this.a = a;
this.b = b;
this.c = c;
}
}
EnumSet<Characteristics> c = EnumSet.noneOf(Characteristics.class);
c.addAll(c1.characteristics());
c.retainAll(c2.characteristics());
c.retainAll(c3.characteristics());
c.remove(Characteristics.IDENTITY_FINISH);
return Collector.of(
() -> new Box<>(c1.supplier().get(), c2.supplier().get(), c3.supplier().get()),
(acc, v) -> {
c1.accumulator().accept(acc.a, v);
c2.accumulator().accept(acc.b, v);
c3.accumulator().accept(acc.c, v);
},
(acc1, acc2) -> {
acc1.a = c1.combiner().apply(acc1.a, acc2.a);
acc1.b = c2.combiner().apply(acc1.b, acc2.b);
acc1.c = c3.combiner().apply(acc1.c, acc2.c);
return acc1;
},
acc -> finisher.apply(c1.finisher().apply(acc.a), c2.finisher().apply(acc.b), c3.finisher().apply(acc.c)),
c.toArray(new Characteristics[c.size()])
);
}
and finally use it with
Content content = contentList.stream().collect(combining(
Collectors.mapping(Content::getA, Collectors.joining(", ")),
Collectors.mapping(Content::getB, Collectors.joining(", ")),
Collectors.mapping(Content::getC, Collectors.joining(", ")),
Content::new
));
static Content merge(List<Content> list) {
return new Content(
list.stream().map(Content::getA).collect(Collectors.joining(", ")),
list.stream().map(Content::getB).collect(Collectors.joining(", ")),
list.stream().map(Content::getC).collect(Collectors.joining(", ")));
}
EDIT: Expanding on Federico's inline collector, here is a concrete class dedicated to merging Content objects:
class Merge {
public static Collector<Content, ?, Content> collector() {
return Collector.of(Merge::new, Merge::accept, Merge::combiner, Merge::finisher);
}
private StringJoiner a = new StringJoiner(", ");
private StringJoiner b = new StringJoiner(", ");
private StringJoiner c = new StringJoiner(", ");
private void accept(Content content) {
a.add(content.getA());
b.add(content.getB());
c.add(content.getC());
}
private Merge combiner(Merge second) {
a.merge(second.a);
b.merge(second.b);
c.merge(second.c);
return this;
}
private Content finisher() {
return new Content(a.toString(), b.toString(), c.toString());
}
}
Used as:
Content merged = contentList.stream().collect(Merge.collector());
If you don't want to iterate 3 times over the list, or don't want to create too many Content
intermediate objects, then you'd need to collect the stream with your own implementation:
public static Content collectToContent(Stream<Content> stream) {
return stream.collect(
Collector.of(
() -> new StringBuilder[] {
new StringBuilder(),
new StringBuilder(),
new StringBuilder() },
(StringBuilder[] arr, Content elem) -> {
arr[0].append(arr[0].length() == 0 ?
elem.getA() :
", " + elem.getA());
arr[1].append(arr[1].length() == 0 ?
elem.getB() :
", " + elem.getB());
arr[2].append(arr[2].length() == 0 ?
elem.getC() :
", " + elem.getC());
},
(arr1, arr2) -> {
arr1[0].append(arr1[0].length() == 0 ?
arr2[0].toString() :
arr2[0].length() == 0 ?
"" :
", " + arr2[0].toString());
arr1[1].append(arr1[1].length() == 0 ?
arr2[1].toString() :
arr2[1].length() == 0 ?
"" :
", " + arr2[1].toString());
arr1[2].append(arr1[2].length() == 0 ?
arr2[2].toString() :
arr2[2].length() == 0 ?
"" :
", " + arr2[2].toString());
return arr1;
},
arr -> new Content(
arr[0].toString(),
arr[1].toString(),
arr[2].toString())));
}
This collector first creates an array of 3 empty StringBuilder
objects. Then defines an accumulator that appends each Content
element's property to the corresponding StringBuilder
. Then it defines a merge function that is only used when the stream is processed in parallel, which merges two previously accumulated partial results. Finally, it also defines a finisher function that transforms the 3 StringBuilder
objects into a new instance of Content
, with each property corresponding to the accumulated strings of the previous steps.
Please check Stream.collect() and Collector.of() javadocs for further reference.
You can use proper lambda for BinaryOperator in reduce function.
Content c = contentList
.stream()
.reduce((t, u) -> new Content(
t.getA() + ',' + u.getA(),
t.getB() + ',' + u.getB(),
t.getC() + ',' + u.getC())
).get();