【问题标题】:How collect / reduce java 8 stream into pojo?如何收集/减少java 8流到pojo?
【发布时间】:2019-02-17 06:08:38
【问题描述】:

看代码:

Collection<MyDto> col = ...

MyBuilder builder = new MyBuilder(); 
for (MyDto dto: col) {
    switch (dto.getType()) {
        case FIELD1:
            builder.field1(dto.getValue());
            break:
        case FIELD2:
            builder.field2(dto.getValue());
            break:
    }    
}

Some result = builder.build();

有没有办法用流来做到这一点,比如:

Some result = col.stream().collect(...)

请注意,所有流值都收集到单个 pojo 中,而不是集合、流或映射。

【问题讨论】:

    标签: java java-8 java-stream reduce collect


    【解决方案1】:

    您的主要问题是每个MyBuilder 方法到每个MyDto 类型的映射是任意的,即Java 无法自动知道为每种类型调用哪个方法:您必须告诉Java 哪个是哪个.

    因此,如果构建器的每个方法都映射到不同的 dto.getType() 值,告诉 Java 的最简单方法是将 switch 移动到 MyBuilder 内的通用方法,以便通知相应的字段,像这样:

    public MyBuilder fieldFromDto(MyDto dto) {
        switch (dto.getType()) {
            case FIELD1: return field1(dto.getValue);
            case FIELD2: return field2(dto.getValue);
            //...
    

    那么你可以这样做:

    MyBuilder builder = new MyBuilder();
    col.stream().forEach(builder::fieldFromDto);
    Some result = builder.build();
    

    另一种可能性是将该开关转换为 lambda 映射(TypeValueMyDto 的字段类型):

    class MyBuilder {
        public final Map<Type, Function<Value, MyBuilder>> mappings = new Map<>();
        public MyBuilder() {
            mappings.put(FIELD1, this::field1);
            mappings.put(FIELD2, this::field2);
            //...
        }
    

    然后在 forEach 中使用这些 lambda:

    MyBuilder builder = new MyBuilder();
    col.stream().forEach(dto -> builder.mappings.get(dto.getType()).apply(dto.getValue()));
    Some result = builder.build();
    

    除此之外,您可以像提出的其他一些答案一样使用反射,但是您需要确保 FIELD1FIELD2 等是实际的 MyBuilder 方法名称,从而失去了一些灵活性。

    最后,我不建议您执行上述任何操作。流很棒,但有时它们与普通的 for 循环相比没有任何优势,并且会使您的代码更丑陋且难以维护。

    【讨论】:

    • 我同意你所说的一切。只是关于使用col.stream().forEach(...)的注意事项...这里不需要流,java中的所有iterables都实现了forEach方法,所以做col.forEach(...)就足够了。
    • @FedericoPeraltaSchaffner 确实如此。最初我使用.stream().sequential().forEach(...) 来避免并发问题,但我在最后一刻放弃了.sequential()(假设它是默认值),留下.stream().forEach(...)。没错,这里通常不需要.stream()(也有例外:synchronizedList(...).stream().forEach(...)synchronizedList(...).forEach(...) 不同)。不过,我想我还是会这样,因为作为答案,我认为它更清楚地表明我们正在使用流来获取结果。
    【解决方案2】:

    假设两个MyBuilder 实例能够合并/合并,那么您可以使用Collector 来做到这一点。

    public class MyCollector implements Collector<MyDto, MyBuilder, Result> {
    
        @Override 
        public Supplier<MyBuilder> supplier() {
            return MyBuilder::new;
        }
    
        @Override
        public BiConsumer<MyBuilder, MyDto> accumulator() {
            return (builder, dto) -> {
                // Add "dto" to "builder" based on type
            };
        }
    
        @Override
        public BinaryOperator<MyBuilder> combiner() {
            return (left, right) -> left.merge(right);
        }
    
        @Override
        public Function<MyBuilder, Result> finisher() {
            return MyBuilder::build;
        }
    
        @Override
        public Set<Characteristics> characteristics() {
            return Set.of();
        }
    
    }
    

    那么你可以这样做:

    Collection<MyDto> col = ...;
    Result r = col.stream().collect(new MyCollector());
    

    如果您不想自定义实现Collector,可以使用Collector.of(...)


    另一种可能更易于维护的方法是让构建器完成所有工作。这样所有的映射逻辑都在一个地方。

    public class ResultBuilder {
    
        public static Collector<MyDto, ?, Result> resultCollector() {
            return Collector.of(ResultBuilder::new, ResultBuilder::add,
                    ResultBuilder::merge, ResultBuilder::build);
        }
    
        public ResultBuilder add(MyDto dto) {
            // Do what is needed based on the type of "dto"
            return this;
        }
    
        public ResultBuilder merge(ResultBuilder other) {
            // Merge "other" into "this"
            return this;
        }
    
        public Result build() {
            // Build result and return it
        }
    
    }
    

    然后您可以使用带有或不带有流的构建器。 With Streams 与之前非常相似:

    Collection<MyDto> col = ...;
    Result r = col.stream().collect(ResultBuilder.resultCollector());
    

    【讨论】:

      【解决方案3】:

      现在,一个令人沮丧的无聊答案:

      不要这样做。

      像这样使用流进行有效映射会降低您的代码在未来的可读性和可维护性。不建议将此 Java 8 功能用于此目的。

      它绝对可以完成,正如一些回答者所开创的那样,但这并不一定意味着它应该完成。

      更简洁地说,您最初的前提是您可以在某种枚举或结构中捕获您的所有字段,您可以switch on,每次引入或删除字段时都会中断,这可能会很耗时下。通过反射获取场的巧妙方法可能稍微更灵活,但是您的反射设置比您可能意识到的更严格;如果您想将 1 映射到 1,这可以正常工作,但如果您想进行一些数据转换,则必须非常小心调整映射器的方式。

      所有这些都说...

      改用映射框架,例如 MapStructDozer

      【讨论】:

        【解决方案4】:

        底线是,不知何故,您需要将MyDto.getType() 的可能返回值映射到MyBuilder 的属性设置方法。您的代码通过switch 语句来实现这一点,这很好。您可以将归约编写为基于流的管道,但您仍然需要以某种方式合并映射。​​

        一个非常直接的方法是构造一个字面量Map,它可以是静态的、最终的和不可修改的。例如,如果您从结构类似的类开始...

        class Some {
        }
        
        class MyBuilder {
            void field1(String s) { }
            void field2(String s) { }
            void field3(String s) { }
            Some build() {
                return null;
            }
        }
        
        class ValueType {}
        
        class MyDto {
            int type;
            ValueType value;
        
            int getType() {
                return type;
            }
        
            ValueType getValue() {
                return value;
            }
        }
        

        ...那么您可以像这样设置您描述的减少:

        public class Reduction {
        
            // Map from DTO types to builder methods
            private final static Map<Integer, BiConsumer<MyBuilder, ValueType>> builderMethods;
        
            static {
                // one-time map initialization
                Map<Integer, BiConsumer<MyBuilder, ValueType>> temp = new HashMap<>();
                temp.put(FIELD1, MyBuilder::field1);
                temp.put(FIELD2, MyBuilder::field2);
                temp.put(FIELD3, MyBuilder::field3);
                builderMethods = Collections.unmodifiableMap(temp);
            }
        
            public Some reduce(Collection<MyDto> col) {
                return col.stream()
                          // this reduction produces the populated builder
                          .reduce(new MyBuilder(),
                                  (b, d) -> { builderMethods.get(d.getType()).accept(b, d); return b; })
                          // obtain the built object
                          .build();
            }
        }
        

        该特定实现每次都使用一个新的构建器,但可以修改为使用通过参数传递给Reduction.reduce() 的构建器,以防您想从预先填充的一些属性开始,和/或保留一个构建返回对象的属性记录。

        最后,请注意,尽管您可以将细节隐藏在某个地方或另一个地方,但我认为没有任何空间可以使整个过程比您开始使用的基于 switch 的代码更简单。

        【讨论】:

        • 我正在编辑我的答案,接近这个......我猜没必要,1+
        • 我们采用了类似的方法:lambda 映射,但我认为您的 reduce() 解决方案比我的更优雅,更适合构建器模式,所以 +1。
        【解决方案5】:

        我没有编译这个,只是给你一个想法:

         Map<Boolean, List<MyDto>> map = col.stream().collect(Collectors.partitioningBy(t -> t.getType() == FIELD2));
        
         map.get(false).forEach(x -> builder.field1(x.getValue()))
        
         map.get(true).forEach(x -> builder.field2(x.getValue()))
        

        【讨论】:

        • 不错,但这仅限于两种类型。如果你有超过 2 种类型,我想你可以使用 groupingBy 收集器来概括它。
        • @marstran 是的!在我的辩护中,无论如何,OP只显示了两个选项
        • 我认为无论如何你都需要使用groupingBypartitioningBy 需要一个Predicate&lt;T&gt;,而MyDto::getType 不会返回bool。或将您的代码更改为partitioningBy(t -&gt; t.getType() == FIELD2)
        • 它确实需要== FIELD2,否则您稍后将构建器方法应用于错误的分区;)
        猜你喜欢
        • 2015-02-24
        • 2014-04-29
        • 2015-11-05
        • 1970-01-01
        • 1970-01-01
        • 2016-08-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多