繁体   English   中英

如何收集/减少java 8流到pojo?

[英]How collect / reduce java 8 stream into pojo?

看看代码:

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

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

底线是,某处,不知何故,你需要的可能的返回值映射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的代码更简单。

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

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

假设两个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());

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

不要这样做。

使用流来有效地映射这样会使您的代码在将来更易于阅读和维护。 为此目的使用此Java 8功能是不明智的。

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

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

所有这些都说......

使用映射框架,而不是像MapStructDozer

您的主要问题是每个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使用这些lambdas:

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

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

最后,我不建议做上述任何一项。 流很好,但有时它们没有提供优于正常for循环的任何优势,并且可能使您的代码更难以维护。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM