Java 22 presenta recolectores de flujos, un nuevo mecanismo para manipular flujos de datos. Los recolectores de flujo son la característica entregada para JEP 461, lo que permite a los desarrolladores crear operadores intermedios personalizados que simplifican operaciones complejas. A primera vista, los recolectores de corrientes parecen un poco complejos y oscuros, y quizás te preguntes por qué los necesitarías. Pero cuando te enfrentas a una situación que requiere cierto tipo de manipulación de flujos, los recolectores se convierten en una adición obvia y bienvenida a Stream API. Stream API y recolectores de flujos Los flujos Java modelan colecciones dinámicas de elementos. Como dice la especificación, «Un flujo es una secuencia de valores potencialmente ilimitada y calculada de forma diferida». Eso significa que puede consumir y operar con flujos de datos sin cesar. Piense en ello como sentarse junto a un río y observar el agua fluir. Nunca se te ocurriría esperar a que se acabe el río. Con los arroyos, simplemente empiezas a trabajar con el río y todo lo que contiene. Cuando haya terminado, se marchará. La API Stream tiene varios métodos integrados para trabajar en los elementos en una secuencia de valores. Estos son los operadores funcionales como filtro y mapa. En Stream API, las transmisiones comienzan con una fuente de eventos y las operaciones como filtrar y mapear se conocen como operaciones «intermedias». Cada operación intermedia devuelve la secuencia, para que puedas componerlas juntas. Pero con Stream API, Java no comenzará a aplicar ninguna de estas operaciones hasta que el flujo alcance una operación «terminal». Esto admite un procesamiento eficiente incluso con muchos operadores encadenados. Los operadores intermedios integrados de Stream son potentes, pero no pueden cubrir todo el ámbito de requisitos imaginables. Para situaciones que están fuera de lo común, necesitamos una forma de definir operaciones personalizadas. Los recolectores nos dan ese camino. Qué puedes hacer con los recolectores de arroyos Digamos que estás en la orilla del río y pasan hojas flotando con números escritos en ellas. Si desea hacer algo simple, como crear una matriz de todos los números pares que ve, puede usar el método de filtro incorporado: Lista números = Arrays.asList(1, 2, 3, 4, 5, 6); números.stream().filter(número -> número % 2 == 0).toArray() // resultado: { 2, 4, 6 } En el ejemplo anterior, comenzamos con una matriz de números enteros (la fuente) y luego conviértelo en un flujo, aplicando un filtro que solo devuelva aquellos números cuya división por dos no deja resto. La llamada a toArray() es la llamada al terminal. Esto equivale a comprobar la uniformidad de cada hoja y dejarla a un lado si pasa. Métodos integrados de Stream Gatherers La interfaz java.util.stream.Gatherers viene con un puñado de funciones integradas que le permiten crear operaciones intermedias personalizadas. Echemos un vistazo a lo que hace cada uno. La ventana Método fijo ¿Qué pasaría si quisieras tomar todas las hojas que flotan y recogerlas en cubos de dos? Esto es sorprendentemente complicado de hacer con operadores funcionales integrados. Requiere transformar una matriz de un solo dígito en una matriz de matrices. El método windowFixed es una forma más sencilla de reunir las hojas en depósitos: Stream.iterate(0, i -> i + 1) .gather(Gatherers.windowFixed(2)) .limit(5) .collect(Collectors.toList() ); Esto dice: Dame una secuencia basada en la iteración de números enteros por 1. Convierte cada dos elementos en una nueva matriz. Hazlo cinco veces. Finalmente, convierta la transmisión en una Lista. El resultado es:
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]La ventana es como mover un marco sobre la secuencia; te permite tomar instantáneas. El método windowSliding Otra función de ventanas es windowSliding, que funciona como windowFixed() excepto que cada ventana comienza en el siguiente elemento de la matriz fuente, en lugar de al final de la última ventana. Aquí hay un ejemplo: Stream.iterate(0, i -> i + 1) .gather(Gatherers.windowSliding(2)) .limit(5) .collect(Collectors.toList()); La salida es:
[[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]Compare la salida de windowSliding con la salida de windowFixed y verá la diferencia. Cada subarreglo en windowSliding contiene el último elemento del subarreglo anterior, a diferencia de windowFixed. El método Gatherers.fold Gatherers.fold es como una versión refinada del método Stream.reduce. Es un poco matizado ver dónde resulta útil fold() sobre reduce(). Una buena discusión se encuentra en este artículo. Esto es lo que el autor, Viktor Klang, tiene que decir sobre las diferencias entre plegar y reducir: Plegar es una generalización de la reducción. Con la reducción, el tipo de resultado es el mismo que el tipo de elemento, el combinador es asociativo y el valor inicial es una identidad para el combinador. Para un pliegue, estas condiciones no son necesarias, aunque renunciamos a la paralelización. Entonces vemos que reducir es una especie de pliegue. La reducción toma una corriente y la convierte en un valor único. El plegado también hace esto, pero afloja los requisitos: 1) que el tipo de retorno sea del mismo tipo que los elementos de la secuencia; 2) que el combinador es asociativo; y 3) que el inicializador en pliegue sea una función generadora real, no un valor estático. El segundo requisito es relevante para la paralelización, que analizaré con más detalle pronto. Llamar a Stream.parallel en una secuencia significa que el motor puede dividir el trabajo en varios subprocesos. Esto sólo funciona si el operador es asociativo; es decir, funciona si el orden de las operaciones no afecta el resultado. A continuación se muestra un uso sencillo de fold: Stream.of(«hello»,»world»,»how»,»are»,»you?») .gather ( Gatherers.fold(() -> «», (acc, elemento) -> acc.isEmpty() ? elemento : acc + «,» + elemento ) ) .findFirst() .get(); Este ejemplo toma la colección de cadenas y las combina con comas. El mismo trabajo realizado por reduce: String result = Stream.of(«hello», «world», «how», «are», «¿you?») .reduce(«», (acc, element) -> acc. isEmpty() ? elemento: acc + «,» + elemento); Puedes ver que con fold, defines una función (() -> “”) en lugar de un valor inicial (“”). Esto significa que si necesita un manejo más complejo del iniciador, puede utilizar la función de cierre. Ahora pensemos en las ventajas del pliegue con respecto a una diversidad de tipos. Digamos que tenemos una secuencia de tipos de objetos mixtos y queremos contar las ocurrencias: var result = Stream.of(1,»hello», true).gather(Gatherers.fold(() -> 0, (acc, el) -> cuenta + 1)); // result.findFirst().get() = 3 La var de resultado es 3. Observe que la secuencia tiene un número, una cadena y un valor booleano. Realizar una hazaña similar con reducir es difícil porque el argumento del acumulador (acc) está fuertemente tipado: // malo, arroja una excepción: var resultado = Stream.of(1, «hello», true).reduce(0, (acc, el ) -> cuenta + 1); // Error: tipos de operandos incorrectos para el operador binario ‘+’ Podríamos usar un recopilador para realizar este trabajo: var result2 = Stream.of(«apple», «banana», «apple», «orange») .collect(Collectors .toMap(palabra -> palabra, palabra -> 1, Entero::suma, HashMap::nuevo)); Pero luego hemos perdido el acceso al inicializador y al cuerpo de las funciones de plegado si necesitamos una lógica más complicada. El método Gatherers.scanScan es algo así como windowFixed pero acumula los elementos en un solo elemento en lugar de una matriz. Nuevamente, un ejemplo brinda más claridad (este ejemplo es de Javadocs): Stream.of(1,2,3,4,5,6,7,8,9) .gather( Gatherers.scan(() -> » «, (cadena, número) -> cadena + número) ) .toList(); La salida es:
[«1», «12», «123», «1234», «12345», «123456», «1234567», «12345678», «123456789»]
Entonces, el escaneo nos permite movernos a través de los elementos de la secuencia y combinarlos de forma acumulativa. El método mapConcurrent Con mapConcurrent, puede especificar una cantidad máxima de subprocesos para usar simultáneamente al ejecutar la función de mapa proporcionada. Se utilizarán hilos virtuales. Aquí hay un ejemplo simple que limita la concurrencia a cuatro subprocesos mientras eleva números al cuadrado (tenga en cuenta que mapConcurrent es excesivo para un conjunto de datos tan simple): Stream.of(1,2,3,4,5).gather(Gatherers.mapConcurrent(4, x -> x * x)).collect(Collectors.toList()); // Resultado: [1, 4, 9, 16, 25]
Además del máximo de subprocesos, mapConcurrent funciona exactamente igual que la función de mapa estándar. Conclusión Hasta que los recolectores de flujo se promuevan como una característica, aún necesitará usar el indicador –enable-preview para acceder a la interfaz de Gatherer y sus características. Una manera fácil de experimentar es usar JShell: $ jshell –enable-preview. Aunque no son una necesidad diaria, los recolectores de transmisiones llenan algunos vacíos de larga data en la API Stream y facilitan a los desarrolladores ampliar y personalizar Java funcional. programas. Copyright © 2024 IDG Communications, Inc.