Java 8 stream join and return multiple values

后端 未结 4 1369
时光说笑
时光说笑 2020-12-17 22:32

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          


        
相关标签:
4条回答
  • 2020-12-17 22:41

    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
    ));
    
    0 讨论(0)
  • 2020-12-17 22:59
    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());
    
    0 讨论(0)
  • 2020-12-17 23:01

    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 Contentelement'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.

    0 讨论(0)
  • 2020-12-17 23:04

    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();
    
    0 讨论(0)
提交回复
热议问题