繁体   English   中英

如何使用Java lambda来收集新类型列表中的元素?

[英]How to use Java lambdas to collect elements in a list of a new type?

我一直在努力阅读javadocs,以确定如何使用lambdas优雅地将一种类型的行列表组合成另一种类型的分组列表。

我已经想出如何使用Collectors.groupingBy语法将数据转换为Map<String, List<String>>但由于结果将用于各种后续函数调用...我理想情况下喜欢将这些缩减为包含新映射的对象列表。

这是我的数据类型, RowData是源...我想将数据组合成CodesToBrands列表:

class RowData {
    private String id;
    private String name;

    public RowData() {

    }

    public RowData(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class CodeToBrands {
    private String code;
    private List<String> brands = new ArrayList<>();

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public List<String> getBrands() {
        return brands;
    }

    public void addBrands(List<String> brands) {
        this.brands.addAll(brands);
    }

    public void addBrand(String brand) {
        this.brands.add(brand);
    }
}

这是我正在编写的测试,试图弄明白......

@Test
public void testMappingRows() {
    List<RowData> rows = new ArrayList<>();
    rows.add(new RowData("A", "Foo"));
    rows.add(new RowData("B", "Foo"));
    rows.add(new RowData("A", "Bar"));
    rows.add(new RowData("B", "Zoo"));
    rows.add(new RowData("C", "Elf"));

    // Groups a list of elements to a Map<String, List<String>>
    System.out.println("\nMapping the codes to a list of brands");
    Map<String, List<String>> result = rows.stream()
                    .collect(Collectors.groupingBy(RowData::getId, Collectors.mapping(RowData::getName, Collectors.toList())));

    // Show results are grouped nicely
    result.entrySet().forEach((entry) -> {
        System.out.println("Key: " + entry.getKey());
        entry.getValue().forEach((value) -> System.out.println("..Value: " + value));
    });

    /**Prints:
     * Mapping the codes to a list of brands
        Key: A
        ..Value: Foo
        ..Value: Bar
        Key: B
        ..Value: Foo
        ..Value: Zoo
        Key: C
        ..Value: Elf*/

    // How to get these as a List<CodeToBrand> objects where each CodeToBrand objects to avoid working with a Map<String, List<String>>?
    List<CodeToBrands> resultsAsNewType;
}

任何人都可以提供任何帮助,试图在一个更容易使用的数据类型中获得相同的整体结果?

提前致谢

您可以使用Collectors.toMap在一次传递中执行此操作:

Collection<CodeToBrands> values = rows.stream()
    .collect(Collectors.toMap(
        RowData::getId,
        rowData -> {
            CodeToBrands codeToBrands = new CodeToBrands();
            codeToBrands.setCode(rowData.getId());
            codeToBrands.addBrand(row.getName());
            return codeToBrands;
        },
        (left, right) -> {
            left.addBrands(right.getBrands());
            return left;
        }))
    .values();

然后,如果您需要List而不是Collection ,只需执行以下操作:

List<CodeToBrands> result = new ArrayList<>(values);

如果在CodeToBrands类中有特定的构造函数和合并方法,则可以简化上面的代码:

public CodeToBrands(String code, String brand) {
    this.code = code;
    this.brands.add(brand);
}

public CodeToBrands merge(CodeToBrands another) {
    this.brands.addAll(another.getBrands());
    return this;
}

然后,只需:

Collection<CodeToBrands> values = rows.stream()
    .collect(Collectors.toMap(
        RowData::getId,
        rowData -> new CodeToBrands(rowData.getId(), rowData.getName()),
        CodeToBrands::merge))
    .values();

您可以在分组后简单地链接进一步的操作并收集结果,如下所示:

List<CodeToBrands> resultSet = rows.stream()
                .collect(Collectors.groupingBy(RowData::getId,
                        Collectors.mapping(RowData::getName, Collectors.toList())))
                .entrySet()
                .stream()
                .map(e -> {
                     CodeToBrands codeToBrand = new CodeToBrands();
                     codeToBrand.setCode(e.getKey());
                     codeToBrand.addBrands(e.getValue());
                     return codeToBrand;
                 }).collect(Collectors.toCollection(ArrayList::new));

这种方法在分组后在entrySet创建一个流,然后简单地将每个Map.Entry<String, List<String>>映射到CodeToBrands实例中,最后我们将这些元素累积到一个列表实现中。

另一种方法是使用toMap收集器:

List<CodeToBrands> resultSet = rows.stream()
         .collect(Collectors.toMap(RowData::getId,
                 valueMapper -> new ArrayList<>(Collections.singletonList(valueMapper.getName())),
                 (v, v1) -> {
                     v.addAll(v1);
                     return v;
                 })).entrySet()
                    .stream()
                    .map(e -> {
                        CodeToBrands codeToBrand = new CodeToBrands();
                        codeToBrand.setCode(e.getKey());
                        codeToBrand.addBrands(e.getValue());
                        return codeToBrand;
                }).collect(Collectors.toCollection(ArrayList::new));

这种方法与上述方法非常相似,但只是“另一种”方式。 因此, toMap收集器的这个特定重载需要一个键映射器(在这种情况下为RowData::getId ),它产生映射的键。

函数valueMapper -> new ArrayList<>(Collections.singletonList(valueMapper.getName()))是生成映射值的值映射器。

最后,函数(v, v1) -> {...}是合并函数,用于解决与同一个键关联的值之间的冲突。

以下链接函数与所示的第一个示例相同。

一种简单的方法是使用其字段在CodeToBrands创建构造函数:

public CodeToBrands(String code, List<String> brands) {
    this.code = code;
    this.brands = brands;
}

然后只需map每个条目map到新的CodeToBrands实例:

List<CodeToBrands> resultsAsNewType = result
        .entrySet()
        .stream()
        .map(e -> new CodeToBrands(e.getKey(), e.getValue()))
        .collect(Collectors.toList());

其他答案似乎热衷于首先创建您已有的原始地图,然后在第二次创建CodeToBrands

IIUC,你想要一次性完成,你可以通过创建自己的Collector 它需要做的就是直接收集到目标类,而不是先创建列表。

// the target map
Supplier<Map<String,CodeToBrands>> supplier = HashMap::new;

// for each id, you want to reuse an existing CodeToBrands,
// or create a new one if we don't have one already
BiConsumer<Map<String,CodeToBrands>,RowData> accumulator = (map, rd) -> {
    // this assumes a CodeToBrands(String id) constructor
    CodeToBrands ctb = map.computeIfAbsent(rd.getId(), CodeToBrands::new);
    ctb.addBrand(rd.getName());
};

// to complete the collector, we need to be able to combine two CodeToBrands objects
BinaryOperator<Map<String,CodeToBrands>> combiner = (map1, map2) -> {
    // add all map2 entries to map1
    for (Entry<String,CodeToBrands> entry : map2.entrySet()) {
        map1.merge(entry.getKey(), entry.getValue(), (ctb1, ctb2) -> {
            // add all ctb2 brands to ctb1 and continue collecting in there
            ctb1.addBrands(ctb2.getBrands());
            return ctb1;
        });
    }
    return map1;
};

// now, you have everything, use these for a new Collector
Collector<RowData,?,Map<String,CodeToBrands>> collector =
    Collector.of(supplier, accumulator, combiner);

// let's give it a go
System.out.println("\nMapping the codes by new collector");
Map<String,CodeToBrands> map = rows.stream().collect(collector);
map.forEach((s, ctb) -> {
    System.out.println("Key: " + s);
    ctb.getBrands().forEach((value) -> System.out.println("..Value: " + value));
});

现在,如果您坚持使用List ,可能会执行new ArrayList<>(map.entrySet())new ArrayList<>(map.entrySet()) collectingAndThen() ; 但根据我的经验,你不妨使用几乎所有用例的Set it返回。

您可以直接使用lambdas来简化代码,但我认为一步一步地执行它会更容易。

暂无
暂无

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

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