Buscar..


Introducción

Un Stream representa una secuencia de elementos y admite diferentes tipos de operaciones para realizar cálculos sobre esos elementos. Con Java 8, la interfaz de la Collection tiene dos métodos para generar un Stream : stream() y parallelStream() . Stream operaciones de Stream son intermedias o terminales. Las operaciones intermedias devuelven un Stream por lo que se pueden encadenar múltiples operaciones intermedias antes de que se cierre el Stream . Las operaciones de la terminal son nulas o devuelven un resultado no continuo.

Sintaxis

  • collection.stream ()
  • Arrays.stream (array)
  • Stream.iterate (firstValue, currentValue -> nextValue)
  • Stream.generate (() -> valor)
  • Stream.of (elementOfT [, elementOfT, ...])
  • Stream.empty ()
  • StreamSupport.stream (iterable.spliterator (), false)

Usando Streams

Un Stream es una secuencia de elementos sobre los cuales se pueden realizar operaciones agregadas secuenciales y paralelas. Cualquier Stream dado puede potencialmente tener una cantidad ilimitada de datos que fluyen a través de él. Como resultado, los datos recibidos de un Stream se procesan individualmente a medida que llegan, en lugar de realizar el procesamiento por lotes de los datos por completo. Cuando se combinan con expresiones lambda , proporcionan una forma concisa de realizar operaciones en secuencias de datos utilizando un enfoque funcional.

Ejemplo: ( verlo funciona en Ideone )

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .forEach(System.out::println);

Salida:

MANZANA
PLÁTANO
NARANJA
PERA

Las operaciones realizadas por el código anterior se pueden resumir de la siguiente manera:

  1. Crear una Stream<String> contiene un secuenciado clasificadas Stream de fruta String elementos usando el método de fábrica estática Stream.of(values) .

  2. La operación de filter() retiene solo los elementos que coinciden con un predicado dado (los elementos que cuando son probados por el predicado devuelven verdadero). En este caso, retiene los elementos que contienen una "a" . El predicado se da como una expresión lambda .

  3. La operación map() transforma cada elemento usando una función dada, llamada mapeador. En este caso, cada String fruta se asigna a su versión de String mayúsculas utilizando el método String::toUppercase .

    Tenga en cuenta que la operación map() devolverá un flujo con un tipo genérico diferente si la función de mapeo devuelve un tipo diferente a su parámetro de entrada. Por ejemplo, en una Stream<String> llamada a .map(String::isEmpty) devuelve un Stream<Boolean>

  4. La operación sorted() ordena los elementos de la Stream acuerdo con su ordenamiento natural (lexicográficamente, en el caso de String ).

  5. Finalmente, la forEach(action) realiza una acción que actúa en cada elemento de la Stream , pasándola a un Consumidor . En el ejemplo, cada elemento simplemente se imprime en la consola. Esta operación es una operación de terminal, por lo que es imposible volver a operar en ella.

    Tenga en cuenta que las operaciones definidas en el Stream se realizan debido a la operación del terminal. Sin una operación de terminal, el flujo no se procesa. Las transmisiones no pueden ser reutilizadas. Una vez que se llama a una operación de terminal, el objeto Stream vuelve inutilizable.

Operaciones encadenadas

Las operaciones (como se ve arriba) se encadenan para formar lo que se puede ver como una consulta en los datos.


Cierre de arroyos

Tenga en cuenta que un Stream generalmente no tiene que estar cerrado. Solo se requiere cerrar secuencias que operan en canales IO. La mayoría de Stream tipos no operan sobre los recursos y por lo tanto no requieren de cierre.

La interfaz de Stream extiende a AutoCloseable . Las secuencias se pueden cerrar llamando al método de close o usando declaraciones try-with-resource.

Un ejemplo de caso de uso en el que se debería cerrar una Stream es cuando crea una Stream de líneas desde un archivo:

try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
    lines.forEach(System.out::println);
}

La interfaz de Stream también declara el método Stream.onClose() que le permite registrar los controladores de Runnable que se Runnable cuando se cierre el stream. Un ejemplo de caso de uso es cuando el código que produce una transmisión necesita saber cuándo se consume para realizar una limpieza.

public Stream<String>streamAndDelete(Path path) throws IOException {
    return Files.lines(path).onClose(() -> someClass.deletePath(path));
}

El controlador de ejecución solo se ejecutará si se llama al método close() , ya sea explícita o implícitamente mediante una declaración try-with-resources.


Orden de procesamiento

El procesamiento de un objeto Stream puede ser secuencial o paralelo .

En un modo secuencial , los elementos se procesan en el orden de la fuente de la Stream . Si se ordena el Stream (como una implementación de SortedMap o una List ), se garantiza que el procesamiento coincidirá con el orden de la fuente. En otros casos, sin embargo, se debe tener cuidado de no depender de la ordenación (ver: ¿ es consistente el orden de iteración de Java HashMap keySet() ? ).

Ejemplo:

List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 42); 

// sequential 
long howManyOddNumbers = integerList.stream()
                                    .filter(e -> (e % 2) == 1)
                                    .count(); 

System.out.println(howManyOddNumbers); // Output: 2

Vivir en Ideone

El modo paralelo permite el uso de múltiples hilos en múltiples núcleos, pero no hay garantía del orden en que se procesan los elementos.

Si se invocan varios métodos en una secuencia Stream , no es necesario invocar todos los métodos. Por ejemplo, si un Stream se filtra y el número de elementos se reduce a uno, no se producirá una llamada posterior a un método como la sort . Esto puede aumentar el rendimiento de un Stream secuencial, una optimización que no es posible con un Stream paralelo.

Ejemplo:

// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
                                            .filter(e -> (e % 2) == 1)
                                            .count();

System.out.println(howManyOddNumbersParallel); // Output: 2

Vivir en Ideone


Diferencias de Contenedores (o Colecciones )

Si bien algunas acciones se pueden realizar tanto en Contenedores como en Flujos, en última instancia tienen propósitos diferentes y admiten diferentes operaciones. Los contenedores están más enfocados en cómo se almacenan los elementos y cómo se puede acceder a esos elementos de manera eficiente. Un Stream , por otro lado, no proporciona acceso directo y manipulación a sus elementos; está más dedicado al grupo de objetos como una entidad colectiva y realiza operaciones en esa entidad en su conjunto. Stream y Collection son abstracciones de alto nivel separadas para estos propósitos diferentes.

Recoge los elementos de una corriente en una colección

Recopilar con toList() y toSet()

Los elementos de un Stream se pueden recopilar fácilmente en un contenedor utilizando la operación Stream.collect :

System.out.println(Arrays
    .asList("apple", "banana", "pear", "kiwi", "orange")
    .stream()
    .filter(s -> s.contains("a"))
    .collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]

Se pueden crear otras instancias de recopilación, como un Set , utilizando otros métodos integrados de Collectors . Por ejemplo, Collectors.toSet() recopila los elementos de un Stream en un Set .


Control explícito sobre la implementación de List o Set

De acuerdo con la documentación de los Collectors#toList() y los Collectors#toSet() , no hay garantías sobre el tipo, mutabilidad, serialización o seguridad de subprocesos de la List o el Set devuelto.

Para que el control explícito sobre la implementación sea devuelto, los Collectors#toCollection(Supplier) pueden usarse en su lugar, donde el proveedor dado devuelve una colección nueva y vacía.

// syntax with method reference
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(ArrayList::new))
);

// syntax with lambda
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(() -> new LinkedHashSet<>()))
);

Recopilando elementos usando toMap

El recopilador acumula elementos en un mapa, donde la clave es la identificación del estudiante y el valor es el valor del estudiante.

  List<Student> students = new ArrayList<Student>(); 
    students.add(new Student(1,"test1"));
    students.add(new Student(2,"test2"));
    students.add(new Student(3,"test3"));
    
    Map<Integer, String> IdToName = students.stream()
        .collect(Collectors.toMap(Student::getId, Student::getName));
    System.out.println(IdToName);

Salida:

{1=test1, 2=test2, 3=test3}

Collectors.toMap tiene otra implementación Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) . El mergeFunction se usa principalmente para seleccionar un nuevo valor o retener un valor antiguo si la clave se repite al agregar un nuevo miembro en el Mapa de una lista.

El mergeFunction a menudo se ve así: (s1, s2) -> s1 para retener el valor correspondiente a la tecla repetida, o (s1, s2) -> s2 para poner un nuevo valor para la tecla repetida.

Recopilación de elementos al mapa de colecciones

Ejemplo: de ArrayList a Map <String, List <>>

A menudo se requiere hacer un mapa de la lista de una lista primaria. Ejemplo: de un estudiante de la lista, necesitamos hacer un mapa de la lista de temas para cada estudiante.

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 12.9));
    list.add(new Student("Davis", SUBJECT.GEOGRAPHY, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 80.0));
    list.add(new Student("Sascha", SUBJECT.SCIENCE, 12.0));
    list.add(new Student("Sascha", SUBJECT.LITERATURE, 50.0));

    list.add(new Student("Robert", SUBJECT.LITERATURE, 12.0));

    Map<String, List<SUBJECT>> map = new HashMap<>();
    list.stream().forEach(s -> {
                map.computeIfAbsent(s.getName(), x -> new ArrayList<>()).add(s.getSubject());
            });
    System.out.println(map);

Salida:

{ Robert=[LITERATURE], 
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE], 
Davis=[MATH, SCIENCE, GEOGRAPHY] }

Ejemplo: de ArrayList a Map <String, Map <>>

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 1, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 2, 12.9));
    list.add(new Student("Davis", SUBJECT.MATH, 3, 37.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 4, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 5, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 1, 80.0));
    list.add(new Student("Sascha", SUBJECT.ENGLISH, 6, 12.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 3, 50.0));

    list.add(new Student("Robert", SUBJECT.ENGLISH, 5, 12.0));

    Map<String, Map<SUBJECT, List<Double>>> map = new HashMap<>();

    list.stream().forEach(student -> {
        map.computeIfAbsent(student.getName(), s -> new HashMap<>())
                .computeIfAbsent(student.getSubject(), s -> new ArrayList<>())
                .add(student.getMarks());
    });

    System.out.println(map);

Salida:

{ Robert={ENGLISH=[12.0]}, 
Sascha={MATH=[80.0, 50.0], ENGLISH=[85.0, 12.0]}, 
Davis={MATH=[35.0, 37.0], SCIENCE=[12.9, 37.0]} }

Hoja de trucos

Gol Código
Recoger en una List Collectors.toList()
Recoger en un ArrayList con tamaño asignado previamente Collectors.toCollection(() -> new ArrayList<>(size))
Recoger a un Set Collectors.toSet()
Recoger a un Set con un mejor rendimiento de iteración Collectors.toCollection(() -> new LinkedHashSet<>())
Recoger en un Set<String> distinga mayúsculas y minúsculas Set<String> Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))
Recopilar en un EnumSet<AnEnum> (mejor rendimiento para enums) Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class))
Recoger en un Map<K,V> con claves únicas Collectors.toMap(keyFunc,valFunc)
Asignar MyObject.getter () a MyObject único Collectors.toMap(MyObject::getter, Function.identity())
Mapear MyObject.getter () a múltiples MyObjects Collectors.groupingBy(MyObject::getter)

Corrientes infinitas

Es posible generar un Stream que no termine. Al llamar a un método de terminal en un Stream infinito Stream el Stream entra en un bucle infinito. El método de limit de un Stream se puede usar para limitar la cantidad de términos del Stream que Java procesa.

Este ejemplo genera una Stream de todos los números naturales, comenzando con el número 1. Cada término sucesivo de la Stream es uno más alto que el anterior. Al llamar al método de límite de este Stream , solo se consideran e imprimen los primeros cinco términos del Stream .

// Generate infinite stream - 1, 2, 3, 4, 5, 6, 7, ...
IntStream naturalNumbers = IntStream.iterate(1, x -> x + 1);

// Print out only the first 5 terms
naturalNumbers.limit(5).forEach(System.out::println);

Salida:

1
2
3
4
5


Otra forma de generar un flujo infinito es usando el método Stream.generate . Este método lleva una lambda de tipo Proveedor .

// Generate an infinite stream of random numbers
Stream<Double> infiniteRandomNumbers = Stream.generate(Math::random);

// Print out only the first 10 random numbers
infiniteRandomNumbers.limit(10).forEach(System.out::println);

Corrientes de consumo

Un Stream solo se atravesará cuando haya una operación de terminal , como count() , collect() o forEach() . De lo contrario, no se realizará ninguna operación en el Stream .

En el siguiente ejemplo, no se agrega ninguna operación de terminal a la Stream , por lo que la operación de filter() no se invocará y no se producirá ninguna salida porque peek() NO es una operación de terminal .

IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);

Vivir en Ideone

Esta es una secuencia de Stream con una operación de terminal válida, por lo que se produce una salida.

También forEach usar forEach lugar de peek :

IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println); 

Vivir en Ideone

Salida:

2
4
6
8

Una vez realizada la operación del terminal, el Stream se consume y no se puede reutilizar.


Aunque un objeto de flujo dado no puede ser reutilizado, es fácil crear un Iterable reutilizable que delega a un flujo de flujo. Esto puede ser útil para devolver una vista modificada de un conjunto de datos en vivo sin tener que recopilar resultados en una estructura temporal.

List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();

for (String str : iterable) {
    System.out.println(str);
}
for (String str : iterable) {
    System.out.println(str);
}

Salida:

foo
bar
foo
bar

Esto funciona porque Iterable declara un solo método abstracto Iterator<T> iterator() . Eso hace que sea efectivamente una interfaz funcional, implementada por un lambda que crea una nueva transmisión en cada llamada.


En general, un Stream funciona como se muestra en la siguiente imagen:

Operación de la corriente

NOTA : Las comprobaciones de argumentos siempre se realizan, incluso sin una operación de terminal :

try {
    IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
    System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}

Vivir en Ideone

Salida:

Obtuvimos una NullPointerException ya que null se pasó como un argumento para filtrar ()

Creando un Mapa de Frecuencia

El recopilador groupingBy(classifier, downstream) permite la recopilación de elementos Stream en un Map al clasificar cada elemento en un grupo y realizar una operación descendente en los elementos clasificados en el mismo grupo.

Un ejemplo clásico de este principio es usar un Map para contar las ocurrencias de elementos en una Stream . En este ejemplo, el clasificador es simplemente la función de identidad, que devuelve el elemento como está. La operación descendente cuenta el número de elementos iguales, utilizando counting() .

Stream.of("apple", "orange", "banana", "apple")
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
      .entrySet()
      .forEach(System.out::println);

La operación descendente es en sí misma un colector ( Collectors.counting() ) que opera en elementos de tipo String y produce un resultado de tipo Long . El resultado de la llamada al método de collect es un Map<String, Long> .

Esto produciría el siguiente resultado:

plátano = 1
naranja = 1
manzana = 2

Corriente paralela

Nota: antes de decidir qué Stream usar, consulte ParallelStream vs Sequential Stream .

Cuando desee realizar operaciones de Stream simultánea, puede utilizar cualquiera de estas formas.

List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();

O:

Stream<String> aParallelStream = data.parallelStream();

Para ejecutar las operaciones definidas para el flujo paralelo, llame a un operador de terminal:

aParallelStream.forEach(System.out::println);

(Una posible) salida de la Stream paralela:

Tres
Cuatro
Uno
Dos
Cinco

El orden puede cambiar ya que todos los elementos se procesan en paralelo (lo que puede hacer que sea más rápido). Use parallelStream cuando ordene no importa.

Impacto en el rendimiento

En el caso de que se trate de redes, los Stream paralelo pueden degradar el rendimiento general de una aplicación porque todos los Stream paralelo utilizan un conjunto común de subprocesos para la red.

Por otro lado, los Stream paralelo pueden mejorar significativamente el rendimiento en muchos otros casos, dependiendo del número de núcleos disponibles en la CPU en ejecución en este momento.

Convertir un flujo de opcionales a un flujo de valores

Es posible que necesite convertir un Stream emite un Optional en un Stream de valores, emitiendo solo valores del Optional existente. (es decir, sin valor null y sin tratar con Optional.empty() ).

 Optional<String> op1 = Optional.empty();
 Optional<String> op2 = Optional.of("Hello World");

 List<String> result = Stream.of(op1, op2)
                             .filter(Optional::isPresent)
                             .map(Optional::get)
                             .collect(Collectors.toList());

 System.out.println(result); //[Hello World]

Creando un Stream

Todos los java Collection<E> tienen métodos stream() y parallelStream() partir de los cuales se puede construir un Stream<E> :

Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();

Se puede crear un Stream<E> partir de una matriz usando uno de dos métodos:

String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);

La diferencia entre Arrays.stream() y Stream.of() es que Stream.of() tiene un parámetro varargs, por lo que se puede usar como:

Stream<Integer> integerStream = Stream.of(1, 2, 3);

También hay Stream primitivas que puedes usar. Por ejemplo:

IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);

Estas corrientes primitivas también se pueden construir utilizando el método Arrays.stream() :

IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });

Es posible crear un Stream desde una matriz con un rango específico.

int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);

Tenga en cuenta que cualquier flujo primitivo se puede convertir a flujo de tipo en caja utilizando el método en boxed :

Stream<Integer> integerStream = intStream.boxed();

Esto puede ser útil en algunos casos si desea recopilar datos, ya que la secuencia primitiva no tiene ningún método de collect que tome un Collector como argumento.

Reutilización de operaciones intermedias de una cadena de flujo.

La secuencia se cierra cuando se llama a la operación del terminal. Reutilizando el flujo de operaciones intermedias, cuando solo la operación del terminal solo varía. podríamos crear un proveedor de flujo para construir un nuevo flujo con todas las operaciones intermedias ya configuradas.

Supplier<Stream<String>> streamSupplier = () -> Stream.of("apple", "banana","orange", "grapes", "melon","blueberry","blackberry")
.map(String::toUpperCase).sorted();

  streamSupplier.get().filter(s ->   s.startsWith("A")).forEach(System.out::println);

// APPLE

  streamSupplier.get().filter(s -> s.startsWith("B")).forEach(System.out::println);

  // BANANA
  // BLACKBERRY
  // BLUEBERRY

int[] matrices int[] se pueden convertir a la List<Integer> usando flujos

int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());

Encontrar estadísticas sobre flujos numéricos

Java 8 proporciona clases denominadas IntSummaryStatistics , DoubleSummaryStatistics y LongSummaryStatistics que proporcionan un objeto de estado para recopilar estadísticas como count , min , max , sum y average .

Java SE 8
List<Integer> naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
                                           .mapToInt((x) -> x)
                                           .summaryStatistics();
System.out.println(stats);

Lo que resultará en:

Java SE 8
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}

Obtener un trozo de un arroyo

Ejemplo: Obtenga un Stream de 30 elementos, que contiene del elemento 21 al 50 (inclusive) de una colección.

final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);

Notas:

  • IllegalArgumentException se lanza si n es negativo o maxSize es negativo
  • tanto el skip(long) como el limit(long) son operaciones intermedias
  • si un flujo contiene menos de n elementos, entonces skip(n) devuelve un flujo vacío
  • tanto skip(long) como limit(long) son operaciones baratas en tuberías de flujo secuencial, pero pueden ser bastante caras en tuberías ordenadas en paralelo

Corrientes de concatenación

Declaración de variables para ejemplos:

Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");

Ejemplo 1 - Concatenar dos Stream

final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());

concat1.forEach(System.out::print);
// prints: abc123

Ejemplo 2 - Concatenar más de dos Stream s

final Stream<String> concat2 = Stream.concat(
    Stream.concat(abc.stream(), digits.stream()),
    greekAbc.stream());

System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Alternativamente, para simplificar la sintaxis concat() anidada, los Stream también se pueden concatenar con flatMap() :

final Stream<String> concat3 = Stream.of(
    abc.stream(), digits.stream(), greekAbc.stream())
    .flatMap(s -> s);
    // or `.flatMap(Function.identity());` (java.util.function.Function)

System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Tenga cuidado al construir Stream a partir de una concatenación repetida, ya que el acceso a un elemento de un Stream profundamente concatenado puede dar lugar a cadenas de llamadas profundas o incluso a una StackOverflowException .

IntStream to String

Java no tiene un flujo de caracteres , por lo que al trabajar con String y construir un Stream de Character , una opción es obtener un IntStream de puntos de código utilizando el método String.codePoints() . Por IntStream tanto, IntStream se puede obtener de la siguiente manera:

public IntStream stringToIntStream(String in) {
  return in.codePoints();
}

Es un poco más complicado hacer la conversión de otra manera, por ejemplo, IntStreamToString. Eso se puede hacer de la siguiente manera:

public String intStreamToString(IntStream intStream) {
  return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}

Ordenar usando Stream

List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");

System.out.println(data);

List<String> sortedData = data.stream().sorted().collect(Collectors.toList());

System.out.println(sortedData);

Salida:

[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]

También es posible usar un mecanismo de comparación diferente, ya que hay una versión sorted sobrecargada que toma un comparador como argumento.

Además, puedes usar una expresión lambda para ordenar:

List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());

Esto generaría [Sydney, New York, Mumbai, London, California, Amsterdam]

Puede usar Comparator.reverseOrder() para tener un comparador que impone el orden reverse al reverse .

List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

Arroyos de primitivos

Java proporciona Stream especializadas para tres tipos de primitivas IntStream (para int s), LongStream (para s long ) y DoubleStream (para s double ). Además de ser implementaciones optimizadas para sus primitivas respectivas, también proporcionan varios métodos terminales específicos, típicamente para operaciones matemáticas. P.ej:

IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0

Recopilar los resultados de una secuencia en una matriz

Analógico para obtener una colección para un Stream por collect() se puede obtener una matriz mediante el método Stream.toArray() :

List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");

String[] filteredFruits = fruits.stream()
    .filter(s -> s.contains("a"))
    .toArray(String[]::new);     

// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));

String[]::new es un tipo especial de referencia de método: una referencia de constructor.

Encontrar el primer elemento que coincide con un predicado

Es posible encontrar el primer elemento de un Stream que coincida con una condición.

Para este ejemplo, encontraremos el primer Integer cuyo cuadrado es más de 50000 .

IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
    .filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
    .findFirst(); // Find the first filtered element

Esta expresión devolverá un OptionalInt con el resultado.

Tenga en cuenta que con un Stream infinito, Java seguirá revisando cada elemento hasta que encuentre un resultado. Con un Stream finito, si Java se queda sin elementos pero aún no puede encontrar un resultado, devuelve un OptionalInt vacío.

Usando IntStream para iterar sobre los índices

Stream de elementos generalmente no permiten el acceso al valor de índice del elemento actual. Para iterar sobre una matriz o ArrayList mientras tiene acceso a los índices, use IntStream.range(start, endExclusive) .

String[] names = { "Jon", "Darin", "Bauke", "Hans", "Marc" };

IntStream.range(0, names.length)
    .mapToObj(i -> String.format("#%d %s", i + 1, names[i]))
    .forEach(System.out::println);

El método range(start, endExclusive) devuelve otro ÌntStream y mapToObj(mapper) devuelve una secuencia de String .

Salida:

# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc

Esto es muy similar a usar un bucle normal for con un contador, pero con el beneficio de la canalización y la paralelización:

for (int i = 0; i < names.length; i++) {
    String newName = String.format("#%d %s", i + 1, names[i]);
    System.out.println(newName);
}

Aplanar arroyos con mapa plano ()

Un Stream de elementos que a su vez se pueden transmitir puede aplanarse en un solo Stream continuo:

La matriz de Lista de elementos se puede convertir en una sola Lista.

List<String> list1 =  Arrays.asList("one", "two");
      List<String> list2 =  Arrays.asList("three","four","five");
      List<String> list3 =  Arrays.asList("six");
          List<String> finalList = Stream.of(list1, list2, list3).flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(finalList);

// [one, two, three, four, five, six]

El mapa que contiene la Lista de elementos como valores se puede acoplar a una lista combinada

Map<String, List<Integer>> map = new LinkedHashMap<>();
map.put("a", Arrays.asList(1, 2, 3));
map.put("b", Arrays.asList(4, 5, 6));

List<Integer> allValues = map.values() // Collection<List<Integer>>
        .stream()                      // Stream<List<Integer>>
        .flatMap(List::stream)         // Stream<Integer>
        .collect(Collectors.toList());

System.out.println(allValues);
// [1, 2, 3, 4, 5, 6]

List del Map se puede aplanar en una sola Stream continua

List<Map<String, String>> list = new ArrayList<>();
Map<String,String> map1 = new HashMap();
map1.put("1", "one");
map1.put("2", "two");

Map<String,String> map2 = new HashMap();
map2.put("3", "three");
map2.put("4", "four");
list.add(map1);
list.add(map2);


Set<String> output= list.stream()  //  Stream<Map<String, String>>
    .map(Map::values)              // Stream<List<String>>
    .flatMap(Collection::stream)   // Stream<String>
    .collect(Collectors.toSet());  //Set<String>
// [one, two, three,four]

Crear un mapa basado en una corriente

Caso simple sin llaves duplicadas

Stream<String> characters = Stream.of("A", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}

Para hacer las cosas más declarativa, podemos utilizar el método estático en Function la interfaz - Function.identity() . Podemos reemplazar este element -> element lambda element -> element con Function.identity() .

Caso donde puede haber duplicados de llaves.

El javadoc para Collectors.toMap establece:

Si las claves asignadas contienen duplicados (de acuerdo con Object.equals(Object) ), se lanza una IllegalStateException cuando se realiza la operación de recolección. Si las claves asignadas pueden tener duplicados, use toMap(Function, Function, BinaryOperator) lugar.

Stream<String> characters = Stream.of("A", "B", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(
                element -> element.hashCode(),
                element -> element,
                (existingVal, newVal) -> (existingVal + newVal)));

// map = {65=A, 66=BB, 67=C}

El BinaryOperator pasado a Collectors.toMap(...) genera el valor que se almacenará en el caso de una colisión. Puede:

  • devuelve el valor anterior, de modo que el primer valor de la secuencia tenga prioridad,
  • devuelve el nuevo valor, de modo que el último valor de la secuencia tenga prioridad, o
  • Combina los valores antiguos y nuevos.

Agrupación por valor

Puede usar Collectors.groupingBy cuando necesite realizar el equivalente de una base de datos en cascada "agrupar por" operación. Para ilustrar, lo siguiente crea un mapa en el que los nombres de las personas se asignan a los apellidos:

List<Person> people = Arrays.asList(
    new Person("Sam", "Rossi"),
    new Person("Sam", "Verdi"),
    new Person("John", "Bianchi"),
    new Person("John", "Rossi"),
    new Person("John", "Verdi")
);

Map<String, List<String>> map = people.stream()
        .collect(
                // function mapping input elements to keys
                Collectors.groupingBy(Person::getName, 
                // function mapping input elements to values,
                // how to store values
                Collectors.mapping(Person::getSurname, Collectors.toList()))
        );

// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}

Vivir en Ideone

Generando cadenas aleatorias utilizando Streams

A veces es útil crear Strings aleatorias, tal vez como Session-ID para un servicio web o una contraseña inicial después del registro para una aplicación. Esto se puede lograr fácilmente usando Stream s.

Primero necesitamos inicializar un generador de números aleatorios. Para mejorar la seguridad de las String generadas, es una buena idea usar SecureRandom .

Nota : crear un SecureRandom es bastante costoso, por lo que es una buena práctica hacer esto solo una vez y llamar a uno de sus métodos setSeed() de vez en cuando para reiniciarlo.

private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20)); 
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example

Al crear String aleatorias, generalmente queremos que utilicen solo ciertos caracteres (por ejemplo, solo letras y dígitos). Por lo tanto, podemos crear un método que devuelva un valor boolean que luego se puede usar para filtrar el Stream .

//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
    //check for range to avoid using all unicode Letter (e.g. some chinese symbols)
    return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}

A continuación, podemos utilizar el RNG para generar una cadena aleatoria de longitud específica que contiene el conjunto de caracteres que pasa nuestra verificación de uso Este useThisCharacter .

public String generateRandomString(long length){
    //Since there is no native CharStream, we use an IntStream instead
    //and convert it to a Stream<Character> using mapToObj.
    //We need to specify the boundaries for the int values to ensure they can safely be cast to char
    Stream<Character> randomCharStream = rng.ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT).mapToObj(i -> (char)i).filter(c -> this::useThisCharacter).limit(length);

    //now we can use this Stream to build a String utilizing the collect method.
    String randomString = randomCharStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
    return randomString;
}

Usando Streams para Implementar Funciones Matemáticas

Stream , y especialmente los IntStream , son una forma elegante de implementar los términos de resumen (∑). Los rangos de la Stream se pueden usar como los límites de la suma.

Por ejemplo, la aproximación de Madhava de Pi viene dada por la fórmula (Fuente: wikipedia ): La aproximación de Madhava

Esto se puede calcular con una precisión arbitraria. Por ejemplo, para 101 términos:

double pi = Math.sqrt(12) * 
            IntStream.rangeClosed(0, 100)
                     .mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
                     .sum();

Nota: Con precisión de double , seleccionar un límite superior de 29 es suficiente para obtener un resultado que no se puede distinguir de Math.Pi

Uso de flujos y referencias de métodos para escribir procesos de autodocumentación

Las referencias de métodos son un excelente código de auto-documentación, y el uso de referencias de métodos con Stream hace que los procesos complicados sean fáciles de leer y comprender. Considere el siguiente código:

public interface Ordered {
    default int getOrder(){
        return 0;
    }
}

public interface Valued<V extends Ordered> {
    boolean hasPropertyTwo();
    V getValue();
}

public interface Thing<V extends Ordered> {
    boolean hasPropertyOne();
    Valued<V> getValuedProperty();
}

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    List<V> results = new ArrayList<V>();
    for (Thing<V> thing : things) {
        if (thing.hasPropertyOne()) {
            Valued<V> valued = thing.getValuedProperty();
            if (valued != null && valued.hasPropertyTwo()){
                V value = valued.getValue();
                if (value != null){
                    results.add(value);
                }
            }
        }
    }
    results.sort((a, b)->{
        return Integer.compare(a.getOrder(), b.getOrder());
    });
    return results;
}

Este último método reescrito utilizando Stream s y las referencias de métodos es mucho más legible y cada paso del proceso se comprende rápida y fácilmente. No solo es más corto, también muestra de un vistazo qué interfaces y clases son responsables del código en cada paso:

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    return things.stream()
        .filter(Thing::hasPropertyOne)
        .map(Thing::getValuedProperty)
        .filter(Objects::nonNull)
        .filter(Valued::hasPropertyTwo)
        .map(Valued::getValue)
        .filter(Objects::nonNull)
        .sorted(Comparator.comparing(Ordered::getOrder))
        .collect(Collectors.toList());
}    

Uso de flujos de Map.Entry para conservar los valores iniciales después del mapeo

Cuando tiene un Stream , debe Map.Entry<K,V> pero también desea conservar los valores iniciales, puede mapear el Stream a un Map.Entry<K,V> usando un método de utilidad como el siguiente:

public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
    return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}

Luego, puede usar su convertidor para procesar los Stream tienen acceso a los valores originales y asignados:

Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
    .map(entryMapper(transformer));

A continuación, puede continuar procesando esa Stream forma normal. Esto evita la sobrecarga de crear una colección intermedia.

Categorías de operaciones de flujo

Las operaciones de flujo se dividen en dos categorías principales, operaciones intermedias y terminales, y dos subcategorías, sin estado y con estado.


Operaciones intermedias:

Una operación intermedia siempre es perezosa , como un simple Stream.map . No se invoca hasta que la secuencia se consume realmente. Esto se puede verificar fácilmente:

Arrays.asList(1, 2 ,3).stream().map(i -> {
    throw new RuntimeException("not gonna happen");
    return i;
});

Las operaciones intermedias son los bloques de construcción comunes de una secuencia, encadenados después de la fuente y generalmente son seguidos por una operación de terminal que dispara la cadena de la corriente.


Terminal de Operaciones

Las operaciones de terminal son las que activan el consumo de un flujo. Algunos de los más comunes son Stream.forEach o Stream.collect . Por lo general, se colocan después de una cadena de operaciones intermedias y casi siempre están ansiosos .


Operaciones sin Estado

La falta de estado significa que cada artículo se procesa sin el contexto de otros artículos. Las operaciones sin estado permiten el procesamiento eficiente de la memoria de flujos. Las operaciones como Stream.map y Stream.filter que no requieren información sobre otros elementos de la transmisión se consideran sin estado.


Operaciones de estado

La notificación de estado significa que la operación en cada elemento depende de (algunos) otros elementos de la transmisión. Esto requiere un estado para ser preservado. Las operaciones de estado de estado pueden romperse con flujos largos o infinitos. Las operaciones como Stream.sorted requieren que la totalidad de la secuencia se procese antes de que se emita cualquier elemento que se rompa en una secuencia lo suficientemente larga. Esto se puede demostrar mediante un flujo largo ( ejecutado bajo su propio riesgo ):

// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);

Esto causará una falta de memoria debido a la condición de estado de Stream.sorted :

// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);

Convertir un iterador a un flujo

Use Spliterators.spliterator() o Spliterators.spliteratorUnknownSize() para convertir un iterador en una secuencia:

Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();    
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);

Reducción con arroyos

La reducción es el proceso de aplicar un operador binario a cada elemento de un flujo para obtener un valor.

El método sum() de un IntStream es un ejemplo de una reducción; se aplica la adición a cada término de la corriente, lo que resulta en un valor final: Suma de reduccion

Esto es equivalente a (((1+2)+3)+4)

El método de reduce de un flujo permite crear una reducción personalizada. Es posible utilizar el método de reduce para implementar el método sum() :

IntStream istr;
    
//Initialize istr
    
OptionalInt istr.reduce((a,b)->a+b);

La versión Optional se devuelve para que las secuencias vacías se puedan manejar adecuadamente.

Otro ejemplo de reducción es la combinación de un Stream<LinkedList<T>> en un solo LinkedList<T> :

Stream<LinkedList<T>> listStream;
    
//Create a Stream<LinkedList<T>>
    
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

También puede proporcionar un elemento de identidad . Por ejemplo, el elemento de identidad para la adición es 0, como x+0==x . Para la multiplicación, el elemento de identidad es 1, como x*1==x . En el caso anterior, el elemento de identidad es una lista LinkedList<T> vacía LinkedList<T> , porque si agrega una lista vacía a otra lista, la lista a la que está "agregando" no cambia:

Stream<LinkedList<T>> listStream;

//Create a Stream<LinkedList<T>>

LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

Tenga en cuenta que cuando se proporciona un elemento de identidad, el valor de retorno no se ajusta en un Optional si se llama en una secuencia vacía, reduce() devolverá el elemento de identidad.

El operador binario también debe ser asociativo , lo que significa que (a+b)+c==a+(b+c) . Esto se debe a que los elementos pueden reducirse en cualquier orden. Por ejemplo, la reducción de adición anterior se podría realizar de la siguiente manera:

Otra reducción de suma

Esta reducción es equivalente a la escritura ((1+2)+(3+4)) . La propiedad de asociatividad también permite que Java reduzca el Stream en paralelo; cada procesador puede reducir una parte del flujo, con una reducción que combina el resultado de cada procesador al final.

Unir un flujo a una sola cadena

Un caso de uso que aparece con frecuencia es crear una String partir de una secuencia, donde los elementos de la secuencia están separados por un determinado carácter. El método Collectors.joining() se puede usar para esto, como en el siguiente ejemplo:

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

String result = fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", "));
           
System.out.println(result);

Salida:

Manzana, plátano, naranja, pera.

El método Collectors.joining() también puede atender pre y postfixes:

String result = fruitStream.filter(s -> s.contains("e"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", ", "Fruits: ", "."));
           
System.out.println(result);

Salida:

Frutos: MANZANA, NARANJA, PERA.

Vivir en Ideone



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow