multiple - Java Stream: ¿hay una manera de iterar tomando dos elementos a la vez en lugar de uno?
java stream map (4)
Digamos que tenemos esta corriente
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");
y quiero guardar en un mapa las parejas de cadenas adyacentes en las que la primera comienza con "err".
Lo que pensé es algo como esto
Map<String, String> map = new HashMap<>();
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.reduce((acc, next) -> {
if (acc.startsWith("err"))
map.put(acc,next);
if (next.startsWith("err"))
return next;
else
return "";
});
Pero no estoy totalmente satisfecho con él por dos razones principales
- Estoy "abusando" de
reduce
función dereduce
. En Stream API, cada función tiene un propósito claro y bien definido: se supone que max calcula el valor máximo, se supone que el filtro se filtra en función de una condición, se supone quereduce
produce un valor acumulado de manera incremental y así sucesivamente. - Hacer eso me impide usar los poderosos mecanismos de Streams: ¿y si quisiera limitar mi búsqueda a los dos primeros resultados?
Aquí utilicé reduce
porque (por lo que sé) es la única función que te permite comparar algunos valores que puedes, de alguna manera, regresar a algo similar a los conceptos de "valor actual" y "valor siguiente".
¿Hay una manera más directa? ¿Algo que te permita recorrer la secuencia considerando más de un valor para cada iteración?
EDITAR
Lo que estoy pensando es en algún mecanismo que, dado el elemento actual, le permite definir una "ventana de elementos" a considerar, para cada iteración.
Algo como
<R> Stream<R> mapMoreThanOne(
int elementsBeforeCurrent,
int elementsAfterCurrent,
Function<List<? super T>, ? extends R> mapper);
en lugar de
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Eso sería una poderosa "actualización" a la API actual.
EDIT2
Aprecio el esfuerzo de las personas que proponen su solución, pero el problema no es el algoritmo en sí. Hay diferentes maneras de lograr mi objetivo al juntar flujos, índices, variables temporales para almacenar valores previos ... pero me preguntaba si había algún método en Stream API que fue diseñado para la tarea de tratar con elementos distintos al actual. Sin romper el "paradigma de la corriente". Algo como esto
List<String> list =
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.filterFunctionImWonderingIfExist(/*filters couples of elements*/)
.limit(2)
.collect(Collectors.toList());
Dadas las respuestas, creo que no hay una solución "clara y rápida", a menos que se use la biblioteca StreamEx
Aquí hay un sencillo forro que usa un colector listo para usar:
Stream<String> stream = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");
Map<String, String> map = Arrays.stream(stream
.collect(Collectors.joining(",")).split(",(?=(([^,]*,){2})*[^,]*$)"))
.filter(s -> s.startsWith("err"))
.map(s -> s.split(","))
.collect(Collectors.toMap(a -> a[0], a -> a[1]));
El "truco" aquí es unir primero todos los términos en una sola Cadena, luego dividirla en Cadenas de pares, por ejemplo, "a,b"
, "err1,c"
, etc. Una vez que tenga un flujo de pares, el procesamiento es sencillo.
Las cosas serían más fáciles si su entrada se encuentra en la lista de acceso aleatorio. De esta manera puede utilizar un buen método antiguo List.subList
como este:
List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e",
"f", "g", "h", "err3", "i", "j");
Map<String, String> map = IntStream.range(0, list.size()-1)
.mapToObj(i -> list.subList(i, i+2))
.filter(l -> l.get(0).startsWith("err"))
.collect(Collectors.toMap(l -> l.get(0), l -> l.get(1)));
Lo mismo podría hacerse con la biblioteca StreamEx ya mencionada (escrita por mí) de una manera un poco más breve:
List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e",
"f", "g", "h", "err3", "i", "j");
Map<String, String> map = StreamEx.ofSubLists(list, 2, 1)
.mapToEntry(l -> l.get(0), l -> l.get(1))
.filterKeys(key -> key.startsWith("err"))
.toMap();
Aunque si no desea la dependencia de terceros, la pobre solución de API de Stream tampoco parece muy mala.
Puede escribir un recopilador personalizado o utilizar el método mucho más sencillo de transmitir a través de los índices de la lista:
Map<String, String> result = IntStream.range(0, data.size() - 1)
.filter(i -> data.get(i).startsWith("err"))
.boxed()
.collect(toMap(data::get, i -> data.get(i+1)));
Esto supone que sus datos están en una lista fácil de acceso aleatorio o que puede volcarlos temporalmente en uno.
Si no puede acceder aleatoriamente a los datos o cargarlos en una lista o matriz para su procesamiento, siempre puede hacer un recopilador de pairing
personalizado para que pueda escribir
Map<String, String> result = data.stream()
.collect(pairing(
(a, b) -> a.startsWith("err"),
AbstractMap.SimpleImmutableEntry::new,
toMap(Map.Entry::getKey, Map.Entry::getValue)
));
Aquí está la fuente para el coleccionista. Es compatible en paralelo y puede ser útil en otras situaciones:
public static <T, V, A, R> Collector<T, ?, R> pairing(BiPredicate<T, T> filter, BiFunction<T, T, V> map, Collector<? super V, A, R> downstream) {
class Pairing {
T left, right;
A middle = downstream.supplier().get();
boolean empty = true;
void add(T t) {
if (empty) {
left = t;
empty = false;
} else if (filter.test(right, t)) {
downstream.accumulator().accept(middle, map.apply(right, t));
}
right = t;
}
Pairing combine(Pairing other) {
if (!other.empty) {
this.add(other.left);
this.middle = downstream.combiner().apply(this.middle, other.middle);
this.right = other.right;
}
return this;
}
R finish() {
return downstream.finisher().apply(middle);
}
}
return Collector.of(Pairing::new, Pairing::add, Pairing::combine, Pairing::finish);
}
Puedes construir un colector personalizado para esta tarea.
Map<String, String> map =
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.collect(MappingErrors.collector());
con:
private static final class MappingErrors {
private Map<String, String> map = new HashMap<>();
private String first, second;
public void accept(String str) {
first = second;
second = str;
if (first != null && first.startsWith("err")) {
map.put(first, second);
}
}
public MappingErrors combine(MappingErrors other) {
throw new UnsupportedOperationException("Parallel Stream not supported");
}
public Map<String, String> finish() {
return map;
}
public static Collector<String, ?, Map<String, String>> collector() {
return Collector.of(MappingErrors::new, MappingErrors::accept, MappingErrors::combine, MappingErrors::finish);
}
}
En este colector, se mantienen dos elementos en ejecución. Cada vez que se acepta una String
, se actualizan y si la primera comienza con "err"
, los dos elementos se agregan a un mapa.
Otra solución es usar la biblioteca StreamEx que proporciona un método pairMap
que aplica una función dada a cada par de elementos adyacentes de esta corriente. En el siguiente código, la operación devuelve una matriz de cadenas que consta del primer y segundo elemento del par si el primer elemento comienza con "err"
, de lo contrario, null
. null
elementos null
se filtran y la secuencia se recopila en un mapa.
Map<String, String> map =
StreamEx.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.pairMap((s1, s2) -> s1.startsWith("err") ? new String[] { s1, s2 } : null)
.nonNull()
.toMap(a -> a[0], a -> a[1]);
System.out.println(map);