我有一个这样的课程:
class MultiDataPoint {
private DateTime timestamp;
private Map<String, Number> keyToData;
}
我希望为每个MultiDataPoint生成
class DataSet {
public String key;
List<DataPoint> dataPoints;
}
class DataPoint{
DateTime timeStamp;
Number data;
}
当然,“密钥”在多个MultiDataPoints中可以是相同的。
所以给定List<MultiDataPoint>
,如何使用Java 8流转换为List<DataSet>
?
这就是我目前在没有流的情况下进行转换的方式:
Collection<DataSet> convertMultiDataPointToDataSet(List<MultiDataPoint> multiDataPoints)
{
Map<String, DataSet> setMap = new HashMap<>();
multiDataPoints.forEach(pt -> {
Map<String, Number> data = pt.getData();
data.entrySet().forEach(e -> {
String seriesKey = e.getKey();
DataSet dataSet = setMap.get(seriesKey);
if (dataSet == null)
{
dataSet = new DataSet(seriesKey);
setMap.put(seriesKey, dataSet);
}
dataSet.dataPoints.add(new DataPoint(pt.getTimestamp(), e.getValue()));
});
});
return setMap.values();
}
答案 0 :(得分:53)
这是一个有趣的问题,因为它表明有很多不同的方法可以实现相同的结果。下面我展示了三种不同的实现方式。
Collection Framework中的默认方法: Java 8为集合类添加了一些方法,这些方法与 Stream API 没有直接关系。使用这些方法,您可以显着简化非流实现的实现:
Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
Map<String, DataSet> result = new HashMap<>();
multiDataPoints.forEach(pt ->
pt.keyToData.forEach((key, value) ->
result.computeIfAbsent(
key, k -> new DataSet(k, new ArrayList<>()))
.dataPoints.add(new DataPoint(pt.timestamp, value))));
return result.values();
}
具有展平和中间数据结构的流API :以下实现几乎与Stuart Marks提供的解决方案相同。与他的解决方案相反,以下实现使用匿名内部类作为中间数据结构。
Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
return multiDataPoints.stream()
.flatMap(mdp -> mdp.keyToData.entrySet().stream().map(e ->
new Object() {
String key = e.getKey();
DataPoint dataPoint = new DataPoint(mdp.timestamp, e.getValue());
}))
.collect(
collectingAndThen(
groupingBy(t -> t.key, mapping(t -> t.dataPoint, toList())),
m -> m.entrySet().stream().map(e -> new DataSet(e.getKey(), e.getValue())).collect(toList())));
}
使用地图合并的流API :您还可以为每个 MultiDataPoint 创建地图,而不是展平原始数据结构,然后使用reduce操作将所有映射合并到单个映射中。代码比上面的解决方案简单一点:
Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
return multiDataPoints.stream()
.map(mdp -> mdp.keyToData.entrySet().stream()
.collect(toMap(e -> e.getKey(), e -> asList(new DataPoint(mdp.timestamp, e.getValue())))))
.reduce(new HashMap<>(), mapMerger())
.entrySet().stream()
.map(e -> new DataSet(e.getKey(), e.getValue()))
.collect(toList());
}
您可以在 Collectors 类中找到地图合并的实现。不幸的是,从外部访问它有点棘手。以下是地图合并的替代实现:
<K, V> BinaryOperator<Map<K, List<V>>> mapMerger() {
return (lhs, rhs) -> {
Map<K, List<V>> result = new HashMap<>();
lhs.forEach((key, value) -> result.computeIfAbsent(key, k -> new ArrayList<>()).addAll(value));
rhs.forEach((key, value) -> result.computeIfAbsent(key, k -> new ArrayList<>()).addAll(value));
return result;
};
}
答案 1 :(得分:11)
为此,我必须提出一个中间数据结构:
class KeyDataPoint {
String key;
DateTime timestamp;
Number data;
// obvious constructor and getters
}
有了这个,方法是将每个MultiDataPoint“压扁”成一个(时间戳,密钥,数据)三元组列表,并将所有这些三元组从MultiDataPoint列表中流式传输。
然后,我们对字符串键应用groupingBy
操作,以便将每个键的数据收集在一起。请注意,简单的groupingBy
将导致从每个字符串键到相应KeyDataPoint三元组列表的映射。我们不想要三元组;我们想要DataPoint实例,它们是(时间戳,数据)对。为此,我们应用groupingBy
的“下游”收集器,这是一个mapping
操作,通过从KeyDataPoint三元组中获取正确的值来构造新的DataPoint。 mapping
操作的下游收集器只是toList
,它将同一组的DataPoint对象收集到列表中。
现在我们有一个Map<String, List<DataPoint>>
,我们想将它转换为DataSet对象的集合。我们简单地流出映射条目并构造DataSet对象,将它们收集到一个列表中,然后将其返回。
代码最终看起来像这样:
Collection<DataSet> convertMultiDataPointToDataSet(List<MultiDataPoint> multiDataPoints) {
return multiDataPoints.stream()
.flatMap(mdp -> mdp.getData().entrySet().stream()
.map(e -> new KeyDataPoint(e.getKey(), mdp.getTimestamp(), e.getValue())))
.collect(groupingBy(KeyDataPoint::getKey,
mapping(kdp -> new DataPoint(kdp.getTimestamp(), kdp.getData()), toList())))
.entrySet().stream()
.map(e -> new DataSet(e.getKey(), e.getValue()))
.collect(toList());
}
我对建设者和吸气者采取了一些自由,但我认为它们应该是显而易见的。