Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 stream join and return multiple values

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 propA, propB, propC;
  Content(String a, String b, String c)
  {
    propA = a; propB = b; propC = c;
  }
  public String getA() { return propA; }
  public String getB() { return propB; }
  public String getC() { return propC; }
}

List<Content> contentList = new ArrayList();
contentList.add(new Content("A1", "B1", "C1"));
contentList.add(new Content("A2", "B2", "C2"));
contentList.add(new Content("A3", "B3", "C3"));

I want to write a function that can stream through the contents of contentlist and return a class with result

content { propA = "A1, A2, A3", propB = "B1, B2, B3", propC = "C1, C2, C3" }

I'm fairly new to Java so you might find some code that resembles more like C# than java

like image 344
Vedanth Avatar asked Mar 22 '16 18:03

Vedanth


Video Answer


2 Answers

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
));
like image 128
Tunaki Avatar answered Oct 14 '22 00:10

Tunaki


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());
like image 23
AJNeufeld Avatar answered Oct 13 '22 23:10

AJNeufeld