Java Language
Corrientes
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:
Crear una
Stream<String>
contiene un secuenciado clasificadasStream
de frutaString
elementos usando el método de fábrica estáticaStream.of(values)
.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 .La operación
map()
transforma cada elemento usando una función dada, llamada mapeador. En este caso, cadaString
fruta se asigna a su versión deString
mayúsculas utilizando el métodoString::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 unaStream<String>
llamada a.map(String::isEmpty)
devuelve unStream<Boolean>
La operación
sorted()
ordena los elementos de laStream
acuerdo con su ordenamiento natural (lexicográficamente, en el caso deString
).Finalmente, la
forEach(action)
realiza una acción que actúa en cada elemento de laStream
, 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 objetoStream
vuelve inutilizable.
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 deStream
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
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
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);
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);
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:
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()");
}
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
.
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:
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 sin
es negativo omaxSize
es negativo - tanto el
skip(long)
como ellimit(long)
son operaciones intermedias - si un flujo contiene menos de
n
elementos, entoncesskip(n)
devuelve un flujo vacío - tanto
skip(long)
comolimit(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 unaIllegalStateException
cuando se realiza la operación de recolección. Si las claves asignadas pueden tener duplicados, usetoMap(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]}
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 ):
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:
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:
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.