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

时间:2018-03-20 19:47:05

标签: java lambda java-8 java-stream collectors

我一直在努力阅读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;
}

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

提前致谢

4 个答案:

答案 0 :(得分:7)

您可以使用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();

答案 1 :(得分:4)

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

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) -> {...}是合并函数,用于解决与同一个键关联的值之间的冲突。

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

答案 2 :(得分:2)

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

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

然后只需mapCodeToBrands个实例的每个条目:

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

答案 3 :(得分:1)

其他答案似乎热衷于首先创建您已有的原始地图,然后在第二次创建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())collectingAndThen();但根据我的经验,您最好使用它返回的Set几乎所有用例。

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