如何将Java 8流收集/减少到pojo中?

时间:2018-09-12 14:43:54

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

查看代码:

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(...)

请注意,所有流值都被收集到单行标记中,而不是收集,流或映射中。

5 个答案:

答案 0 :(得分:2)

最重要的是,无论如何,您都需要将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 :(得分:1)

我没有编译这个,只是给你一个主意:

 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 :(得分:1)

假设两个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
    }

}

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

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

答案 3 :(得分:1)

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

不要这样做。

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

正如一些回答者所倡导的那样,绝对可以做到 ,但这并不一定意味着它应该做到

更简洁地说,您最初的前提是,您可以使用switch所用的某种枚举或结构来捕获所有字段,这在您每次引入或删除字段时都会中断,这可能很耗时追查。 稍微可以灵活地通过反射使场散开,但是您会遇到比您想象的更严格的反射设置;如果您想将1映射为1,则效果很好,但是如果要进行一些数据转换,则必须非常注意如何调整映射器。

所有要说的...

使用诸如MapStructDozer之类的映射框架。

答案 4 :(得分: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循环没有任何优势,并且会使您的代码更丑陋且难以维护。