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

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

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

我已经想出了如何使用 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;
}

任何人都可以提供任何帮助来尝试以更易于使用的数据类型获得相同的总体结果吗?

提前致谢

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

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

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

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

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

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

此方法与上述方法非常相似,但只是 "another" 的方法。因此,toMap 收集器的这个特定重载采用一个键映射器(在本例中为 RowData::getId),它为映射生成键。

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

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

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

您可以使用 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 class:

中有特定的构造函数和合并方法,则可以简化上面的代码
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();

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

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

// 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())collectingAndThen();但根据我的经验,您不妨将 Set 和 returns 用于几乎所有用例。

您可以直接使用 lambda 来简化代码,但我认为按照步骤进行操作会更容易。