streams procesamiento print parte libreria examples ejemplo datos con collection java lambda closures java-8 java-stream

procesamiento - stream java 8 ejemplo



Cómo mapear a múltiples elementos con Java 8 streams? (2)

Tengo una clase como esta:

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

y quiero producir, para cada MultiDataPoint

class DataSet { public String key; List<DataPoint> dataPoints; } class DataPoint{ DateTime timeStamp; Number data; }

por supuesto, una ''clave'' puede ser la misma en múltiples MultiDataPoints.

Entonces, dado un List<MultiDataPoint> , ¿cómo uso las secuencias de Java 8 para convertir a List<DataSet> ?

Así es como actualmente estoy haciendo la conversión sin streams:

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


Es una pregunta interesante, porque muestra que hay muchos enfoques diferentes para lograr el mismo resultado. A continuación, muestro tres implementaciones diferentes.

Métodos predeterminados en Collection Framework: Java 8 agregó algunos métodos a las clases de colecciones, que no están directamente relacionados con Stream API . Al usar estos métodos, puede simplificar significativamente la implementación de la implementación no continua:

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

Stream API con estructura de datos plana e intermedia: la siguiente implementación es casi idéntica a la solución proporcionada por Stuart Marks. A diferencia de su solución, la siguiente implementación usa una clase interna anónima como estructura de datos intermedia.

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

Stream API con fusión de mapas: en lugar de acoplar las estructuras de datos originales, también puede crear un Mapa para cada MultiDataPoint y luego fusionar todos los mapas en un solo mapa con una operación de reducción. El código es un poco más simple que la solución anterior:

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

Puede encontrar una implementación de la fusión de mapas dentro de la clase de colectores . Desafortunadamente, es un poco complicado acceder desde el exterior. Lo que sigue es una implementación alternativa de la fusión del mapa :

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


Para hacer esto, tuve que idear una estructura de datos intermedia:

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

Con esto en su lugar, el enfoque es "aplanar" cada MultiDataPoint en una lista de tripletas (marca de hora, clave, datos) y transmitir todas esas tripletas de la lista de MultiDataPoint.

Luego, aplicamos una operación groupingBy en la clave de cadena para juntar los datos de cada clave. Tenga en cuenta que una groupingBy simpleBy daría como resultado un mapa de cada clave de cadena a una lista de las tripletas de KeyDataPoint correspondientes. No queremos los triples; queremos instancias de DataPoint, que son parejas (marca de tiempo, datos). Para ello, aplicamos un recopilador "descendente" de la groupingBy que es una operación de mapping que construye un nuevo DataPoint obteniendo los valores correctos del triple de KeyDataPoint. El recopilador de la operación de mapping toList es simplemente toList que recoge los objetos de DataPoint del mismo grupo en una lista.

Ahora tenemos un Map<String, List<DataPoint>> y queremos convertirlo en una colección de objetos DataSet. Simplemente transmitimos las entradas del mapa y construimos objetos DataSet, los reunimos en una lista y los devolvemos.

El código termina luciendo así:

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

Me tomé algunas libertades con los constructores y los getters, pero creo que deberían ser obvios.