我一直在努力阅读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;
}
任何人都可以提供任何帮助,试图以更易于使用的数据类型获得相同的整体结果吗?
提前致谢
答案 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;
}
然后只需map
新CodeToBrands
个实例的每个条目:
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来简化代码,但我认为一步一步地执行它会更容易。