簡體   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