usar sirve settitle que para codigo java java-8 java-stream

sirve - ¿Cómo usar las secuencias de Java 8 para encontrar todos los valores que preceden a un valor mayor?



settitle java (6)

Caso de uso

A través de algunos Katas de codificación publicados en el trabajo, me topé con este problema que no estoy seguro de cómo resolver.

El uso de Java 8 Streams, dada una lista de enteros positivos, produce una lista de enteros donde el entero precedió a un valor mayor.

[10, 1, 15, 30, 2, 6]

La entrada anterior daría lugar a:

[1, 15, 2]

como 1 precede a 15, 15 precede a 30 y 2 precede a 6.

Solución sin transmisión

public List<Integer> findSmallPrecedingValues(final List<Integer> values) { List<Integer> result = new ArrayList<Integer>(); for (int i = 0; i < values.size(); i++) { Integer next = (i + 1 < values.size() ? values.get(i + 1) : -1); Integer current = values.get(i); if (current < next) { result.push(current); } } return result; }

Lo que he intentado

El problema que tengo es que no puedo averiguar cómo acceder a continuación en la lambda.

return values.stream().filter(v -> v < next).collect(Collectors.toList());

Pregunta

  • ¿Es posible recuperar el siguiente valor en una secuencia?
  • ¿Debo usar el map y la asignación a un Pair para poder acceder a continuación?

Eso no es un Java8 puro, pero recientemente he publicado una pequeña biblioteca llamada StreamEx que tiene un método exactamente para esta tarea:

// Find all numbers where the integer preceded a larger value. Collection<Integer> numbers = Arrays.asList(10, 1, 15, 30, 2, 6); List<Integer> res = StreamEx.of(numbers).pairMap((a, b) -> a < b ? a : null) .nonNull().toList(); assertEquals(Arrays.asList(1, 15, 2), res);

La operación pairMap implementada internamente usando un spliterator personalizado. Como resultado, tiene un código bastante limpio que no depende de si la fuente es List o cualquier otra cosa. Por supuesto que también funciona bien con la transmisión en paralelo.

Comprometido un testcase para esta tarea.


La respuesta aceptada funciona bien si la secuencia es secuencial o paralela, pero puede sufrir si la List subyacente no es de acceso aleatorio, debido a las múltiples llamadas a get .

Si su flujo es secuencial, puede tirar este colector:

public static Collector<Integer, ?, List<Integer>> collectPrecedingValues() { int[] holder = {Integer.MAX_VALUE}; return Collector.of(ArrayList::new, (l, elem) -> { if (holder[0] < elem) l.add(holder[0]); holder[0] = elem; }, (l1, l2) -> { throw new UnsupportedOperationException("Don''t run in parallel"); }); }

y un uso:

List<Integer> precedingValues = list.stream().collect(collectPrecedingValues());

Sin embargo, también podría implementar un colector para que funcione para secuencias secuenciales y paralelas. Lo único es que necesita aplicar una transformación final, pero aquí tiene el control sobre la implementación de la List para no sufrir el rendimiento de get .

La idea es generar primero una lista de pares (representada por una matriz int[] de tamaño 2) que contenga los valores de la secuencia divididos por una ventana de tamaño dos con un espacio de uno. Cuando necesitamos fusionar dos listas, verificamos el vacío y combinamos el espacio del último elemento de la primera lista con el primer elemento de la segunda lista. Luego aplicamos una transformación final para filtrar solo los valores deseados y mapearlos para obtener el resultado deseado.

Puede que no sea tan simple como la respuesta aceptada, pero puede ser una solución alternativa.

public static Collector<Integer, ?, List<Integer>> collectPrecedingValues() { return Collectors.collectingAndThen( Collector.of(() -> new ArrayList<int[]>(), (l, elem) -> { if (l.isEmpty()) l.add(new int[]{Integer.MAX_VALUE, elem}); else l.add(new int[]{l.get(l.size() - 1)[1], elem}); }, (l1, l2) -> { if (l1.isEmpty()) return l2; if (l2.isEmpty()) return l1; l2.get(0)[0] = l1.get(l1.size() - 1)[1]; l1.addAll(l2); return l1; }), l -> l.stream().filter(arr -> arr[0] < arr[1]).map(arr -> arr[0]).collect(Collectors.toList())); }

Luego puede envolver estos dos recolectores en un método de recolector de utilidades, verificar si la secuencia es paralela con paralela y luego decidir qué recolector devolver.


No es una línea única (es una línea doble), pero esto funciona:

List<Integer> result = new ArrayList<>(); values.stream().reduce((a,b) -> {if (a < b) result.add(a); return b;});

En lugar de resolverlo "mirando el siguiente elemento", esto lo resuelve "mirando el elemento anterior , que reduce() da gratis. He doblado su uso previsto al inyectar un fragmento de código que llena la lista en función de la comparación de elementos anteriores y actuales, luego devuelve la corriente para que la próxima iteración la vea como su elemento anterior.

Algún código de prueba:

List<Integer> result = new ArrayList<>(); IntStream.of(10, 1, 15, 30, 2, 6).reduce((a,b) -> {if (a < b) result.add(a); return b;}); System.out.println(result);

Salida:

[1, 15, 2]


Puede lograrlo utilizando una cola limitada para almacenar elementos que fluyen a través de la secuencia (que se basa en la idea que describí en detalle aquí: ¿Es posible obtener el siguiente elemento en la corriente?

El ejemplo de Belows primero define la instancia de la clase BoundedQueue que almacenará los elementos que pasan por el flujo (si no le gusta la idea de extender la lista enlazada, consulte el enlace mencionado anteriormente para obtener un enfoque alternativo y más genérico). Más tarde, simplemente examina los dos elementos subsiguientes, gracias a la clase de ayuda:

public class Kata { public static void main(String[] args) { List<Integer> input = new ArrayList<Integer>(asList(10, 1, 15, 30, 2, 6)); class BoundedQueue<T> extends LinkedList<T> { public BoundedQueue<T> save(T curElem) { if (size() == 2) { // we need to know only two subsequent elements pollLast(); // remove last to keep only requested number of elements } offerFirst(curElem); return this; } public T getPrevious() { return (size() < 2) ? null : getLast(); } public T getCurrent() { return (size() == 0) ? null : getFirst(); } } BoundedQueue<Integer> streamHistory = new BoundedQueue<Integer>(); final List<Integer> answer = input.stream() .map(i -> streamHistory.save(i)) .filter(e -> e.getPrevious() != null) .filter(e -> e.getCurrent() > e.getPrevious()) .map(e -> e.getPrevious()) .collect(Collectors.toList()); answer.forEach(System.out::println); } }


Si está dispuesto a usar una biblioteca de terceros y no necesita paralelismo, jOOλ ofrece las siguientes funciones de ventana de estilo SQL

System.out.println( Seq.of(10, 1, 15, 30, 2, 6) .window() .filter(w -> w.lead().isPresent() && w.value() < w.lead().get()) .map(w -> w.value()) .toList() );

Flexible

[1, 15, 2]

La función lead() accede al siguiente valor en orden transversal desde la ventana.

Descargo de responsabilidad: trabajo para la empresa detrás de jOOλ


Utilizando IntStream.range :

static List<Integer> findSmallPrecedingValues(List<Integer> values) { return IntStream.range(0, values.size() - 1) .filter(i -> values.get(i) < values.get(i + 1)) .mapToObj(values::get) .collect(Collectors.toList()); }

Es ciertamente más agradable que una solución imperativa con un bucle grande, pero aún así un poco hasta el objetivo de "usar un flujo" de una manera idiomática.

¿Es posible recuperar el siguiente valor en una secuencia?

No, realmente no. La mejor cita que conozco está en la descripción del paquete java.util.stream :

Los elementos de un flujo solo se visitan una vez durante la vida de un flujo. Al igual que un Iterator , se debe generar una nueva secuencia para volver a visitar los mismos elementos de la fuente.

(Recuperar elementos además del elemento actual en el que se está operando implicaría que podrían visitarse más de una vez).

También podríamos hacerlo técnicamente de otras dos maneras:

  • Stately (muy meh).
  • El uso del iterator un flujo técnicamente sigue siendo el uso de un flujo.