如何使用Java 8流映射到多个元素?

2022-08-31 19:54:25

我有一个这样的类:

class MultiDataPoint {
  private DateTime timestamp;
  private Map<String, Number> keyToData;
}

和我想为每个多数据点生成

class DataSet {
        public String key;    
        List<DataPoint> dataPoints;
}

class DataPoint{
  DateTime timeStamp;
  Number data;
}

当然,一个“密钥”在多个多数据点上可以是相同的。

那么给定一个,我如何使用Java 8流转换为?List<MultiDataPoint>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();
}

答案 1

这是一个有趣的问题,因为它表明有很多不同的方法可以达到相同的结果。下面我将展示三种不同的实现。


集合框架中的默认方法: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;
    };
}

答案 2

为此,我必须想出一个中间数据结构:

class KeyDataPoint {
    String key;
    DateTime timestamp;
    Number data;
    // obvious constructor and getters
}

有了这个,方法是将每个MulityDataPoint“平展”为一个列表(时间戳,键,数据)三元组,并从MultiDataPoint列表中将所有此类三元组流式传输在一起。

然后,我们对字符串键应用操作,以便一起收集每个键的数据。请注意,简单操作将导致从每个字符串键到相应 KeyDataPoint 三元组列表的映射。我们不想要三元组;我们想要DataPoint实例,它们是(时间戳,数据)对。为此,我们应用了一个“下游”收集器,该收集器是通过从 KeyDataPoint 三元组获取正确的值来构造新的 DataPoint 的操作。该操作的下游收集器只是将同一组的 DataPoint 对象收集到一个列表中。groupingBygroupingBygroupingBymappingmappingtoList

现在我们有了一个,我们希望将其转换为数据集对象的集合。我们只需对映射条目进行流出并构造 DataSet 对象,将它们收集到一个列表中,然后将其返回。Map<String, List<DataPoint>>

代码最终如下所示:

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

我对构造函数和 getter 采取了一些自由,但我认为它们应该是显而易见的。