Подтвердить что ты не робот

Как сопоставить несколько элементов с потоками 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.

Итак, для a 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();
}
4b9b3361

Ответ 1

Это интересный вопрос, потому что он показывает, что существует множество разных подходов к достижению одного и того же результата. Ниже я показываю три различные реализации.


Методы по умолчанию в 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, а затем объединить все карты в одну карту с операцией уменьшения. Код немного проще, чем приведенное выше решение:

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
}

При этом подход заключается в том, чтобы "сгладить" каждый MultiDataPoint в список (timestamp, key, data) троек и объединить все такие тройки из списка MultiDataPoint.

Затем мы применяем операцию groupingBy для строкового ключа, чтобы собрать данные для каждой группы вместе. Обратите внимание, что простой groupingBy приведет к отображению каждой строковой строки в список соответствующих троек KeyDataPoint. Мы не хотим троек; нам нужны экземпляры DataPoint, которые представляют собой пары (метка времени, данные). Для этого мы применяем "нижестоящий" сборщик groupingBy, который представляет собой операцию mapping, которая строит новую DataPoint, получая правильные значения из тройки KeyDataPoint. Непосредственный сборщик операции 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());
}

Я взял некоторые свободы с конструкторами и геттерами, но я думаю, что они должны быть очевидными.