web-dev-qa-db-fra.com

Comment mapper sur plusieurs éléments avec des flux Java 8?

J'ai un cours comme celui-ci:

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

et je veux produire, pour chaque MultiDataPoint

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

class DataPoint{
  DateTime timeStamp;
  Number data;
}

bien entendu, une "clé" peut être identique pour plusieurs MultiDataPoints.

Donc, étant donné un List<MultiDataPoint>, comment utiliser Java 8 flux à convertir en List<DataSet>?

Voici comment je procède actuellement à la conversion sans flux:

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

C'est une question intéressante, car elle montre qu'il existe de nombreuses approches différentes pour atteindre le même résultat. Ci-dessous, je montre trois implémentations différentes.


Méthodes par défaut dans Collection Framework: Java 8 a ajouté des méthodes aux classes de collections qui ne sont pas directement liées à l'API Stream . . À l'aide de ces méthodes, vous pouvez considérablement simplifier la mise en œuvre de la mise en œuvre sans flux:

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 de flux avec structure de données aplatie et intermédiaire: La mise en œuvre suivante est presque identique à la solution fournie par Stuart Marks. Contrairement à sa solution, l'implémentation suivante utilise une classe interne anonyme comme structure de données intermédiaire.

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 de flux avec fusion de cartes: Au lieu d’aplatir les structures de données originales, vous pouvez également créer une carte pour chaque MultiDataPoint , puis fusionnez toutes les cartes en une seule carte avec une opération de réduction. Le code est un peu plus simple que la solution ci-dessus:

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

Vous pouvez trouver une implémentation de la fusion de cartes dans la classe Collectors . Malheureusement, il est un peu difficile d'y accéder de l'extérieur. Voici une autre implémentation de la fusion de cartes :

<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

Pour ce faire, je devais proposer une structure de données intermédiaire:

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

Ceci mis en place, l’approche consiste à "aplatir" chaque MultiDataPoint en une liste de triplets (horodatage, clé, données) et à les regrouper tous dans la liste MultiDataPoint.

Ensuite, nous appliquons une opération groupingBy sur la clé de chaîne afin de rassembler les données pour chaque clé. Notez qu'un simple groupingBy donnerait une carte de chaque clé de chaîne à une liste des triples KeyDataPoint correspondants. Nous ne voulons pas les triples; nous voulons des instances DataPoint, qui sont des paires (timestamp, data). Pour ce faire, nous appliquons un collecteur "en aval" du groupingBy qui est une opération mapping qui construit un nouveau DataPoint en obtenant les bonnes valeurs à partir du triple KeyDataPoint. Le collecteur en aval de l'opération mapping est tout simplement toList qui collecte les objets DataPoint du même groupe dans une liste.

Maintenant nous avons un Map<String, List<DataPoint>> et nous voulons le convertir en une collection d'objets DataSet. Nous lisons simplement les entrées de la carte et construisons des objets DataSet, nous les rassemblons dans une liste et nous le renvoyons.

Le code finit par ressembler à ceci:

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

J'ai pris des libertés avec les constructeurs et les accesseurs, mais je pense qu'elles devraient être évidentes.

11
Stuart Marks