web-dev-qa-db-ja.com

Java 8ストリームで複数の要素にマッピングするには?

私はこのようなクラスを持っています:

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

同じ結果を達成するための多くの異なるアプローチがあることを示しているため、これは興味深い質問です。以下に3つの異なる実装を示します。


コレクションフレームワークのデフォルトメソッド: 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:元のデータ構造をフラット化する代わりに、それぞれにMapを作成することもできますMultiDataPointしてから、すべてのマップをマージしますリデュース操作で単一のマップに。コードは上記のソリューションよりも少し単純です:

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

map mergerの実装はCollectorsクラス内にあります。残念ながら、外部からアクセスするには少し注意が必要です。以下はmap mergerの代替実装です。

<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;
    };
}
55
nosid

これを行うには、中間データ構造を考え出す必要がありました。

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

これを導入すると、各MultiDataPointを(タイムスタンプ、キー、データ)トリプルのリストに「フラット化」し、MultiDataPointのリストからそのようなトリプルをすべて一緒にストリーミングします。

次に、各キーのデータを収集するために、文字列キーにgroupingBy操作を適用します。単純なgroupingByは、各文字列キーから対応するKeyDataPointトリプルのリストへのマップになることに注意してください。トリプルは必要ありません。 (タイムスタンプ、データ)のペアであるDataPointインスタンスが必要です。これを行うには、KeyDataPointトリプルから適切な値を取得して新しいDataPointを構築するgroupingBy操作であるmappingの「ダウンストリーム」コレクターを適用します。 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());
}

私はコンストラクターとゲッターといくつかの自由を取りましたが、それらは明白であるべきだと思います。

11
Stuart Marks