Capítulo 1 ¡Hola, expresión lambda!
Sección 1
El estilo de codificación de Java se enfrenta a cambios tremendos.
Nuestro trabajo diario será más sencillo, más cómodo y más expresivo. Java, un nuevo método de programación, apareció en otros lenguajes de programación hace décadas. Una vez que se introduzcan estas nuevas funciones en Java, podremos escribir código que sea más conciso, elegante, más expresivo y con menos errores. Podemos implementar varias estrategias y patrones de diseño con menos código.
En este libro exploraremos la programación de estilo funcional a través de ejemplos de programación cotidiana. Antes de utilizar esta nueva y elegante forma de diseñar y codificar, echemos un vistazo a sus ventajas.
Cambió tu forma de pensar
Estilo imperativo: este es el enfoque que ha proporcionado el lenguaje Java desde sus inicios. Usando este estilo, tenemos que decirle a Java qué hacer en cada paso y luego observar cómo lo ejecuta paso a paso. Por supuesto, esto es bueno, pero parece un poco rudimentario. El código parece un poco detallado y desearíamos que el lenguaje fuera un poco más inteligente. Deberíamos simplemente decirle lo que queremos en lugar de decirle cómo hacerlo; Afortunadamente, Java finalmente puede ayudarnos a hacer realidad este deseo. Veamos algunos ejemplos para entender las ventajas y diferencias de este estilo.
manera normal
Comencemos con dos ejemplos familiares. Este es un método de comando para verificar si Chicago está en la colección de ciudades especificada; recuerde, el código enumerado en este libro es solo un fragmento parcial.
Copie el código de código de la siguiente manera:
booleano encontrado = falso;
para (Cadena ciudad: ciudades) {
if(ciudad.equals("Chicago")) {
encontrado = verdadero;
romper;
}
}
System.out.println("¿Encontraste chicago?:" + encontrado);
Esta versión imperativa parece un poco detallada y rudimentaria y está dividida en varias partes de ejecución. Primero, inicialice una etiqueta booleana llamada encontrada y luego recorra cada elemento de la colección, si se encuentra la ciudad que estamos buscando, configure esta etiqueta, luego salga del bucle y finalmente imprima los resultados de la búsqueda;
una mejor manera
Después de leer este código, los programadores Java cuidadosos pensarán rápidamente en una forma más concisa y clara, como esta:
Copie el código de código de la siguiente manera:
System.out.println("¿Encontraste chicago?:" + ciudades.contains("Chicago"));
Este también es un estilo de escritura imperativo: el método contiene lo hace directamente por nosotros.
mejoras reales
Escribir código como este tiene varias ventajas:
1. No más problemas con esa variable mutable
2. Encapsular la iteración en la capa inferior.
3. El código es más sencillo.
4. El código es más claro y centrado.
5. Tome menos desvíos e integre más estrechamente el código y las necesidades comerciales
6. Menos propenso a errores
7. Fácil de entender y mantener
Tomemos un ejemplo más complicado.
Este ejemplo es demasiado simple. La consulta imperativa si un elemento existe en una colección se puede ver en todas partes en Java. Ahora supongamos que queremos utilizar programación imperativa para realizar algunas operaciones más avanzadas, como analizar archivos, interactuar con bases de datos, llamar a servicios WEB, programación concurrente, etc. Ahora podemos usar Java para escribir código más conciso, elegante y sin errores, no sólo en este sencillo escenario.
la vieja manera
Veamos otro ejemplo. Definimos un rango de precios y calculamos el precio total descontado de diferentes formas.
Copie el código de código de la siguiente manera:
Lista final<BigDecimal> precios = Arrays.asList(
nuevo BigDecimal("10"), nuevo BigDecimal("30"), nuevo BigDecimal("17"),
nuevo BigDecimal("20"), nuevo BigDecimal("15"), nuevo BigDecimal("18"),
nuevo BigDecimal("45"), nuevo BigDecimal("12"));
Suponiendo que hay un descuento del 10% si supera los 20 yuanes, implementémoslo primero de la forma habitual.
Copie el código de código de la siguiente manera:
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(Precio BigDecimal: precios) {
si(precio.compareTo(BigDecimal.valueOf(20)) > 0)
totalDePreciosDescuentos =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Total de precios con descuento: " + totalOfDiscountedPrices);
Este código debería ser muy familiar; primero use una variable para almacenar el precio total; luego repita todos los precios, encuentre aquellos que sean superiores a 20 yuanes, calcule sus precios con descuento y finalmente agréguelos al precio total; Precio después del descuento.
Aquí está el resultado del programa:
Copie el código de código de la siguiente manera:
Total de precios rebajados: 67,5
El resultado es completamente correcto, pero el código es un poco confuso. No es culpa nuestra que sólo podamos escribir como lo hemos hecho. Sin embargo, dicho código es un poco rudimentario y no solo sufre de paranoia básica, sino que también viola el principio de responsabilidad única. Si trabajas desde casa y tienes hijos que quieren ser programadores, tienes que ocultar tu código, en caso de que lo vean y suspiren decepcionados y digan: "¿Te ganas la vida haciendo esto?".
Hay una mejor manera
Podemos hacerlo mejor... y mucho mejor. Nuestro código es un poco como una especificación de requisitos. Esto puede reducir la brecha entre los requisitos comerciales y el código implementado, reduciendo la posibilidad de una mala interpretación de los requisitos.
Ya no permitimos que Java cree una variable y la asigne infinitamente. Necesitamos comunicarnos con ella desde un nivel superior de abstracción, como el siguiente código.
Copie el código de código de la siguiente manera:
final BigDecimal totalOfDiscountedPrices =
precios.stream()
.filtro(precio -> precio.compareTo(BigDecimal.valueOf(20)) > 0)
.map(precio -> precio.multiplicar(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::añadir);
System.out.println("Total de precios con descuento: " + totalOfDiscountedPrices);
Léalo en voz alta: filtre los precios superiores a 20 yuanes, conviértalos en precios con descuento y luego súmelos. Este código es exactamente el mismo que el proceso que utilizamos para describir nuestros requisitos. En Java, también es muy conveniente doblar una línea larga de código y alinearla por línea de acuerdo con el punto delante del nombre del método, como se muestra arriba.
El código es muy simple, pero usamos muchas cosas nuevas en Java8. Primero, llamamos a un método de flujo de la lista de precios. Esto abre la puerta a innumerables iteradores convenientes, que discutiremos más adelante.
Usamos algunos métodos especiales, como filtrar y mapear, en lugar de recorrer directamente toda la lista. Estos métodos no son como los del JDK que usamos antes, aceptan una función anónima (expresión lambda) como parámetro. (Discutiremos esto en profundidad más adelante). Llamamos al método reduce() para calcular la suma de los precios devueltos por el método map().
Al igual que el método contiene, el cuerpo del bucle está oculto. Sin embargo, el método del mapa (y el método de filtrado) es mucho más complicado. Llama a la expresión lambda pasada para calcular cada precio en la lista de precios y coloca el resultado en una nueva colección. Finalmente llamamos al método reduce en esta nueva colección para obtener el resultado final.
Este es el resultado del código anterior:
Copie el código de código de la siguiente manera:
Total de precios rebajados: 67,5
áreas de mejora
Esta es una mejora significativa con respecto a la implementación anterior:
1. Bien estructurado pero no abarrotado
2. Sin operaciones de bajo nivel
3. Lógica fácil de mejorar o modificar
4. Iteración por biblioteca de métodos.
5. Evaluación eficiente y perezosa del cuerpo del bucle.
6. Fácilmente paralelizado
A continuación hablaremos sobre cómo Java implementa esto.
Las expresiones lambda están aquí para salvar el mundo
Las expresiones lambda son un atajo que nos salva de los problemas de la programación imperativa. Esta nueva característica proporcionada por Java ha cambiado nuestro método de programación original, haciendo que el código que escribimos no solo sea conciso y elegante, menos propenso a errores, sino también más eficiente, fácil de optimizar, mejorar y paralelizar.
Sección 2: La mayor ganancia de la programación funcional
El código de estilo funcional tiene una relación señal-ruido más alta; se escribe menos código, pero se hace más por línea o expresión. En comparación con la programación imperativa, la programación funcional nos ha beneficiado mucho:
Se evita la modificación o asignación explícita de variables, que a menudo son fuente de errores y dificultan la paralelización del código. En la programación de línea de comandos, asignamos valores continuamente a la variable totalOfDiscountedPrices en el cuerpo del bucle. En el estilo funcional, el código ya no sufre operaciones de modificación explícitas. Cuantas menos variables se modifiquen, menos errores tendrá el código.
El código de estilo funcional se puede paralelizar fácilmente. Si el cálculo lleva mucho tiempo, podemos ejecutar fácilmente los elementos de la lista al mismo tiempo. Si queremos paralelizar el código imperativo, también debemos preocuparnos de los problemas causados por la modificación simultánea de la variable totalOfDiscountedPrices. En la programación funcional solo accedemos a esta variable después de que se haya procesado por completo, eliminando así los problemas de seguridad de los subprocesos.
El código es más expresivo. La programación imperativa se divide en varios pasos para explicar qué hacer (crear un valor de inicialización, iterar a través de precios, agregar precios de descuento a variables, etc.), mientras que la programación funcional solo necesita que el método de mapa de la lista devuelva un valor que incluya el descuento. Simplemente cree una nueva lista de precios y luego acumulelos.
La programación funcional es más sencilla; se necesita menos código para lograr el mismo resultado que la programación imperativa. Un código más limpio significa menos código para escribir, menos para leer y menos para mantener; consulte "¿Es menos conciso lo suficiente como para ser conciso?" en la página 7.
El código funcional es más intuitivo (leer el código es como describir el problema) y es fácil de entender una vez que nos familiarizamos con la sintaxis. El método de mapa ejecuta la función dada (calcula el precio de descuento) para cada elemento de la colección y luego devuelve el conjunto de resultados, como se muestra en la siguiente figura.
Figura 1: el mapa realiza la función dada en cada elemento de la colección
Con las expresiones lambda, podemos aprovechar al máximo el poder de la programación funcional en Java. Con un estilo funcional, puede escribir código que sea más expresivo, conciso, que tenga menos asignaciones y menos errores.
La compatibilidad con la programación orientada a objetos es una de las principales ventajas de Java. La programación funcional y la programación orientada a objetos no son mutuamente excluyentes. El verdadero cambio de estilo es de la programación por línea de comandos a la programación declarativa. En Java 8, lo funcional y lo orientado a objetos se pueden integrar de manera efectiva. Podemos seguir usando el estilo OOP para modelar entidades de dominio y sus estados y relaciones. Además, también podemos utilizar funciones para modelar comportamientos o transiciones de estado, flujo de trabajo y procesamiento de datos, y crear funciones compuestas.
Sección 3: ¿Por qué utilizar un estilo funcional?
Hemos visto las ventajas de la programación funcional, pero ¿merece la pena utilizar este nuevo estilo? ¿Es esto sólo una pequeña mejora o un cambio completo? Todavía quedan muchas preguntas prácticas que es necesario responder antes de que realmente dediquemos tiempo a esto.
Copie el código de código de la siguiente manera:
Xiao Ming preguntó:
¿Menos código significa simplicidad?
La simplicidad significa menos, pero no desorden. En última instancia, significa poder expresar la intención de manera efectiva. Los beneficios son de gran alcance.
Escribir código es como apilar ingredientes. La simplicidad significa poder mezclar ingredientes para sazonar. Escribir código conciso requiere trabajo duro. Hay menos código para leer y el código verdaderamente útil es transparente para usted. Un shortcode que es difícil de entender o que oculta detalles es más breve que conciso.
Un código simple en realidad significa un diseño ágil. Código simple sin burocracia. Esto significa que podemos probar ideas rápidamente, seguir adelante si funcionan bien y saltar rápidamente si no funcionan bien.
Escribir código en Java no es difícil y la sintaxis es sencilla. Y ya conocemos muy bien las bibliotecas y API existentes. Lo realmente difícil es usarlo para desarrollar y mantener aplicaciones de nivel empresarial.
Necesitamos asegurarnos de que los colegas cierren la conexión de la base de datos en el momento correcto, que no sigan ocupando transacciones, que las excepciones se manejen correctamente en la capa apropiada, que los bloqueos se adquieran y liberen correctamente, etc.
Considerados individualmente, cualquiera de estos problemas no es gran cosa. Sin embargo, cuando se combina con la complejidad del campo, el problema se vuelve muy difícil, los recursos de desarrollo son escasos y el mantenimiento es difícil.
¿Qué pasaría si encapsuláramos estas estrategias en muchos pequeños fragmentos de código y les permitiéramos realizar la gestión de restricciones de forma independiente? Entonces no tendremos que gastar energía constantemente para implementar estrategias. Esta es una gran mejora, echemos un vistazo a cómo lo hace la programación funcional.
Iteraciones locas
Hemos estado escribiendo varias iteraciones para procesar listas, conjuntos y mapas. El uso de iteradores en Java es muy común, pero demasiado complicado. No sólo ocupan varias líneas de código, sino que también son difíciles de encapsular.
¿Cómo recorremos la colección y la imprimimos? Puedes usar un bucle for. ¿Cómo filtramos algunos elementos de la colección? Todavía use un bucle for, pero necesita agregar algunas variables modificables adicionales. Después de seleccionar estos valores, ¿cómo usarlos para encontrar el valor final, como el valor mínimo, el valor máximo, el valor promedio, etc.? Luego hay que reciclar y modificar las variables.
Este tipo de iteración es como una panacea: puede hacer de todo, pero todo es escaso. Java ahora proporciona iteradores integrados para muchas operaciones: por ejemplo, aquellos que sólo hacen bucles, aquellos que hacen operaciones de mapa, aquellos que filtran valores, aquellos que reducen operaciones y hay muchas funciones convenientes como máximo, mínimo y promedio, etc. Además, estas operaciones se pueden combinar bien, por lo que podemos unirlas para implementar la lógica empresarial, que es simple y requiere menos código. Además, el código escrito es muy legible porque es lógicamente coherente con el orden de descripción del problema. Veremos varios ejemplos de este tipo en el Capítulo 2, Uso de colecciones, en la página 19, y este libro está lleno de ejemplos de este tipo.
Aplicar estrategia
Las políticas se implementan en todas las aplicaciones de toda la empresa. Por ejemplo, debemos confirmar que una operación se ha autenticado correctamente por motivos de seguridad, debemos asegurarnos de que la transacción se pueda ejecutar rápidamente y que el registro de modificaciones se actualice correctamente. Estas tareas normalmente terminan siendo un fragmento de código ordinario en el lado del servidor, similar al siguiente pseudocódigo:
Copie el código de código de la siguiente manera:
Transacción transacción = getFromTransactionFactory();
//... operación a ejecutar dentro de la transacción...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
Hay dos problemas con este enfoque. En primer lugar, a menudo resulta en una duplicación de esfuerzos y también aumenta los costos de mantenimiento. En segundo lugar, es fácil olvidarse de las excepciones que pueden generarse en el código comercial, lo que puede afectar el ciclo de vida de la transacción y la actualización del registro de modificaciones. Esto debería implementarse usando bloques try y finalmente, pero cada vez que alguien toca este código, tenemos que volver a confirmar que esta estrategia no ha sido destruida.
Hay otra forma: podemos eliminar la fábrica y poner este código delante. En lugar de obtener el objeto de transacción, pase el código ejecutado a una función bien mantenida, como esta:
Copie el código de código de la siguiente manera:
runWithinTransaction((Transacción de transacción) -> {
//... operación a ejecutar dentro de la transacción...
});
Es un pequeño paso para ti, pero te ahorra muchos problemas. La estrategia de verificar el estado y actualizar el registro al mismo tiempo se abstrae y se encapsula en el método runWithinTransaction. Enviamos a este método un fragmento de código que debe ejecutarse en el contexto de una transacción. Ya no tenemos que preocuparnos de que alguien se olvide de realizar este paso o no maneje la excepción correctamente. La función que implementa la política ya se encarga de eso.
Cubriremos cómo usar expresiones lambda para aplicar esta estrategia en el Capítulo 5.
Estrategia de expansión
Las estrategias parecen estar en todas partes. Además de aplicarlos, las aplicaciones empresariales también necesitan ampliarlos. Esperamos agregar o eliminar algunas operaciones a través de alguna información de configuración. En otras palabras, podemos procesarlas antes de que se ejecute la lógica central del módulo. Esto es muy común en Java, pero es necesario pensarlo y diseñarlo con anticipación.
Los componentes que deben ampliarse suelen tener una o más interfaces. Necesitamos diseñar cuidadosamente la interfaz y la estructura jerárquica de las clases de implementación. Esto puede funcionar bien, pero le dejará con un montón de interfaces y clases que deben mantenerse. Un diseño de este tipo puede volverse fácilmente difícil de manejar y de mantener, frustrando en última instancia el propósito de escalar en primer lugar.
Existe otra solución: interfaces funcionales y expresiones lambda, que podemos utilizar para diseñar estrategias escalables. No tenemos que crear una nueva interfaz ni seguir el mismo nombre de método. Podemos centrarnos más en la lógica empresarial que se implementará, que mencionaremos al usar expresiones lambda para la decoración en la página 73.
La simultaneidad es fácil
Una aplicación grande se acerca a un hito de lanzamiento cuando, de repente, surge un problema grave de rendimiento. El equipo determinó rápidamente que el cuello de botella en el rendimiento estaba en un módulo enorme que procesa cantidades masivas de datos. Alguien del equipo sugirió que el rendimiento del sistema podría mejorarse si se pudieran aprovechar al máximo las ventajas del multinúcleo. Sin embargo, si este enorme módulo está escrito en el antiguo estilo Java, la alegría que trae esta sugerencia pronto se hará añicos.
El equipo rápidamente se dio cuenta de que cambiar este gigante de la ejecución en serie a la ejecución en paralelo requeriría mucho esfuerzo, agregaría complejidad adicional y fácilmente causaría errores relacionados con subprocesos múltiples. ¿No existe una mejor manera de mejorar el rendimiento?
¿Es posible que el código serie y paralelo sean iguales, independientemente de si elige la ejecución en serie o en paralelo, como presionar un interruptor y expresar su idea?
Parece que esto sólo es posible en Narnia, pero si nos desarrollamos completamente en términos funcionales, todo esto se hará realidad. Los iteradores integrados y el estilo funcional eliminarán el último obstáculo para la paralelización. El diseño del JDK permite cambiar entre la ejecución en serie y en paralelo con sólo unos pocos cambios de código discretos, que mencionaremos en "Completar el salto a la paralelización" en la página 145.
contar historias
Se pierden muchas cosas en el proceso de convertir los requisitos comerciales en implementación de código. Cuanto más se pierda, mayor será la probabilidad de error y el coste de gestión. Si el código parece describir los requisitos, será más fácil de leer, será más fácil discutirlo con la gente de requisitos y será más fácil satisfacer sus necesidades.
Por ejemplo, escucha al gerente de producto decir: "Obtenga los precios de todas las acciones, busque aquellas con precios superiores a 500 yuanes y calcule los activos totales que pueden pagar dividendos". Utilizando las nuevas funciones proporcionadas por Java, puede escribir:
Copie el código de código de la siguiente manera:
tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
Este proceso de conversión prácticamente no produce pérdidas porque básicamente no hay nada que convertir. Este es un estilo funcional en acción y verá muchos más ejemplos de esto a lo largo del libro, especialmente en el Capítulo 8, Creación de programas con expresiones Lambda, página 137.
Centrarse en la cuarentena
En el desarrollo de sistemas, normalmente es necesario aislar el negocio principal y la lógica detallada que requiere. Por ejemplo, un sistema de procesamiento de pedidos puede querer utilizar diferentes estrategias impositivas para diferentes fuentes de transacciones. Aislar los cálculos de impuestos del resto de la lógica de procesamiento hace que el código sea más reutilizable y escalable.
En programación orientada a objetos llamamos a esto aislamiento de preocupaciones y el patrón de estrategia generalmente se usa para resolver este problema. La solución generalmente es crear algunas interfaces y clases de implementación.
Podemos lograr el mismo efecto con menos código. También podemos probar rápidamente nuestras propias ideas de productos sin tener que crear un montón de código y estancarnos. Exploraremos más a fondo cómo crear este patrón y realizar el aislamiento de preocupaciones a través de funciones ligeras en Aislamiento de preocupaciones mediante expresiones Lambda en la página 63.
evaluación perezosa
Al desarrollar aplicaciones de nivel empresarial, podemos interactuar con servicios WEB, llamar a bases de datos, procesar XML, etc. Hay muchas operaciones que debemos realizar, pero no todas son necesarias todo el tiempo. Evitar ciertas operaciones o al menos retrasar algunas operaciones temporalmente innecesarias es una de las formas más fáciles de mejorar el rendimiento o reducir el inicio del programa y el tiempo de respuesta.
Esto es solo una pequeña cosa, pero requiere mucho trabajo implementarlo de forma puramente orientada a objetos. Para retrasar la inicialización de algunos objetos pesados, tenemos que manejar varias referencias de objetos, verificar si hay punteros nulos, etc.
Sin embargo, si utiliza la nueva clase Optinal y algunas API de estilo funcional que proporciona, este proceso será muy simple y el código será más claro. Hablaremos de esto en la inicialización diferida en la página 105.
Mejorar la capacidad de prueba
Cuanta menos lógica de procesamiento tenga el código, menos probable será que se corrijan los errores. En términos generales, el código funcional es más fácil de modificar y de probar.
Además, al igual que el Capítulo 4, Diseño con expresiones Lambda y el Capítulo 5, Uso de recursos, las expresiones lambda se pueden usar como un objeto simulado liviano para hacer que las pruebas de excepciones sean más claras y fáciles de entender. Las expresiones lambda también pueden servir como una gran ayuda para las pruebas. Muchos casos de prueba comunes pueden aceptar y manejar expresiones lambda. Los casos de prueba escritos de esta manera pueden capturar la esencia de la funcionalidad que necesita ser probada por regresión. Al mismo tiempo, se pueden completar varias implementaciones que deben probarse pasando diferentes expresiones lambda.
Los casos de prueba automatizados propios de JDK también son un buen ejemplo de aplicación de expresiones lambda; si desea saber más, puede consultar el código fuente en el repositorio de OpenJDK. A través de estos programas de prueba, puede ver cómo las expresiones lambda parametrizan los comportamientos clave del caso de prueba, por ejemplo, construyen el programa de prueba de esta manera, "Crea un contenedor para los resultados" y luego "Agrega algunas condiciones posteriores parametrizadas".
Hemos visto que la programación funcional no solo nos permite escribir código de alta calidad, sino que también resuelve elegantemente varios problemas durante el proceso de desarrollo. Esto significa que desarrollar programas será más rápido y sencillo, con menos errores, siempre que siga algunas pautas que presentaremos más adelante.
Sección 4: Evolución, no revolución
No necesitamos cambiar a otro lenguaje para disfrutar de los beneficios de la programación funcional; todo lo que necesitamos cambiar es la forma en que usamos Java. Lenguajes como C++, Java y C# admiten programación imperativa y orientada a objetos. Pero ahora están empezando a adoptar la programación funcional. Acabamos de analizar ambos estilos de código y analizamos los beneficios que puede aportar la programación funcional. Ahora veamos algunos de sus conceptos clave y ejemplos para ayudarnos a aprender este nuevo estilo.
El equipo de desarrollo del lenguaje Java ha dedicado mucho tiempo y energía a agregar capacidades de programación funcional al lenguaje Java y al JDK. Para disfrutar de los beneficios que aporta, primero debemos introducir algunos conceptos nuevos. Podemos mejorar la calidad de nuestro código siempre que sigamos las siguientes reglas:
1. Declarativo
2. Promover la inmutabilidad
3. Evite los efectos secundarios
4. Prefiere expresiones a declaraciones
5. Diseñar utilizando funciones de orden superior.
Echemos un vistazo a estas pautas prácticas.
declarativo
El núcleo de lo que conocemos como programación imperativa es la variabilidad y la programación basada en comandos. Creamos variables y luego modificamos continuamente sus valores. También proporcionamos instrucciones detalladas para ejecutar, como generar el indicador de índice de la iteración, incrementar su valor, verificar si el ciclo ha finalizado, actualizar el enésimo elemento de la matriz, etc. En el pasado, debido a las características de las herramientas y limitaciones del hardware, solo podíamos escribir código de esta manera. También hemos visto que en una colección inmutable, el método contiene declarativo es más fácil de usar que el imperativo. Todos los problemas difíciles y operaciones de bajo nivel se implementan en funciones de biblioteca y ya no necesitamos preocuparnos por estos detalles. En aras de la simplicidad, también deberíamos utilizar programación declarativa. La inmutabilidad y la programación declarativa son la esencia de la programación funcional, y ahora Java finalmente la hace realidad.
Promover la inmutabilidad
El código con variables mutables tendrá muchas rutas de actividad. Cuantas más cosas cambies, más fácil será destruir la estructura original e introducir más errores. El código con múltiples variables que se modifican es difícil de entender y de paralelizar. La inmutabilidad esencialmente elimina estas preocupaciones. Java admite la inmutabilidad pero no la requiere, pero nosotros podemos. Necesitamos cambiar el viejo hábito de modificar el estado de los objetos. Deberíamos utilizar objetos inmutables tanto como sea posible. Al declarar variables, miembros y parámetros, intente declararlos como finales, como el famoso dicho de Joshua Bloch en "Effective Java", "Trate los objetos como inmutables". Al crear objetos, intente crear objetos inmutables, como String. Al crear una colección, intente crear una colección inmutable o no modificable, como usar métodos como Arrays.asList() y Collections' unmodifiableList(). Evitando la variabilidad podemos escribir funciones puras, es decir, funciones sin efectos secundarios.
evitar efectos secundarios
Suponga que está escribiendo un fragmento de código para tomar el precio de una acción de Internet y escribirlo en una variable compartida. Si tenemos muchos precios que recuperar, tenemos que realizar estas operaciones que consumen mucho tiempo en serie. Si queremos aprovechar el poder del subproceso múltiple, tenemos que lidiar con las molestias del subproceso y la sincronización para evitar condiciones de carrera. El resultado final es que el rendimiento del programa es muy pobre y la gente se olvida de comer y dormir para mantener el hilo. Si se eliminaran los efectos secundarios, podríamos evitar por completo estos problemas. Una función sin efectos secundarios promueve la inmutabilidad y no modifica ninguna entrada ni nada más dentro de su alcance. Este tipo de función es muy legible, tiene pocos errores y es fácil de optimizar. Como no hay efectos secundarios, no hay necesidad de preocuparse por las condiciones de carrera o modificaciones simultáneas. No solo eso, podemos ejecutar fácilmente estas funciones en paralelo, lo cual discutiremos en la página 145.
Preferir expresiones
Las declaraciones son una papa caliente porque obligan a modificarlas. Las expresiones promueven la inmutabilidad y la composición de funciones. Por ejemplo, primero usamos la declaración for para calcular el precio total después de los descuentos. Este tipo de código genera variabilidad y código detallado. El uso de versiones más expresivas y declarativas de los métodos map y sum no solo evita operaciones de modificación, sino que también permite encadenar funciones. Al escribir código, debes intentar utilizar expresiones en lugar de declaraciones. Esto hace que el código sea más simple y fácil de entender. El código se ejecutará siguiendo la lógica empresarial, tal como cuando describimos el problema. Sin duda, una versión concisa es más fácil de modificar si cambian los requisitos.
Diseño utilizando funciones de orden superior.
Java no impone la inmutabilidad como los lenguajes funcionales como Haskell, pero nos permite modificar variables. Por tanto, Java no es, ni será nunca, un lenguaje de programación puramente funcional. Sin embargo, podemos utilizar funciones de orden superior para la programación funcional en Java. Las funciones de orden superior llevan la reutilización al siguiente nivel. Con funciones de alto nivel, podemos reutilizar fácilmente código maduro que sea pequeño, especializado y altamente cohesivo. En POO, estamos acostumbrados a pasar objetos a métodos, crear nuevos objetos en los métodos y luego devolver los objetos. Las funciones de orden superior hacen con las funciones lo mismo que los métodos con los objetos. Con funciones de orden superior podemos hacerlo.
1. Pasar función a función
2. Crea una nueva función dentro de la función.
3. Devolver funciones dentro de funciones
Ya hemos visto un ejemplo de cómo pasar parámetros de una función a otra función, y luego veremos ejemplos de cómo crear y devolver funciones. Veamos nuevamente el ejemplo de “pasar parámetros a una función”:
Copie el código de código de la siguiente manera:
precios.stream()
.filter(precio -> precio.compareTo(BigDecimal.valueOf(20)) > 0) .map(precio -> precio.multiply(BigDecimal.valueOf(0.9)))
informar erratas • discutir
.reduce(BigDecimal.ZERO, BigDecimal::añadir);
En este código, pasamos la función precio -> precio.multiplicar(BigDecimal.valueOf(0.9)) a la función de mapa. La función pasada se crea cuando se llama al mapa de funciones de orden superior. En términos generales, una función tiene un cuerpo de función, un nombre de función, una lista de parámetros y un valor de retorno. Esta función creada sobre la marcha tiene una lista de parámetros seguida de una flecha (->) y luego un cuerpo de función breve. El compilador de Java deduce los tipos de parámetros y el tipo de retorno también está implícito. Esta es una función anónima, no tiene nombre. Pero no la llamamos función anónima, la llamamos expresión lambda. Pasar funciones anónimas como parámetros no es nada nuevo en Java, a menudo hemos pasado clases internas anónimas antes. Incluso si una clase anónima tiene un solo método, todavía tenemos que pasar por el ritual de crear una clase y crear una instancia de ella. Con expresiones lambda podemos disfrutar de una sintaxis ligera. No solo eso, siempre estábamos acostumbrados a abstraer algunos conceptos en varios objetos, pero ahora podemos abstraer algunos comportamientos en expresiones lambda. Programar con este estilo de codificación todavía requiere algo de reflexión. Tenemos que transformar nuestro pensamiento imperativo ya arraigado en un pensamiento funcional. Puede que sea un poco doloroso al principio, pero pronto se acostumbrará. A medida que continúe profundizando, esas API no funcionales quedarán atrás gradualmente. Primero detengamos este tema. Veamos cómo Java maneja las expresiones lambda. Solíamos pasar siempre objetos a métodos, ahora podemos almacenar funciones y pasarlas. Echemos un vistazo al secreto detrás de la capacidad de Java para tomar funciones como parámetros.
Sección 5: Se agregó algo de azúcar de sintaxis
Esto también se puede lograr usando las funciones originales de Java, pero las expresiones lambda agregan algo de azúcar sintáctico, ahorrando algunos pasos y simplificando nuestro trabajo. El código escrito de esta manera no sólo se desarrolla más rápido, sino que también expresa mejor nuestras ideas. Muchas interfaces que usamos en el pasado tenían un solo método: ejecutable, invocable, etc. Estas interfaces se pueden encontrar en todas partes de la biblioteca JDK y, cuando se utilizan, normalmente se pueden realizar con una función. Las funciones de biblioteca que antes requerían solo una interfaz de un solo método ahora pueden pasar funciones livianas, gracias al azúcar sintáctico proporcionado por las interfaces funcionales. La interfaz funcional es una interfaz con un solo método abstracto. Mire esas interfaces con un solo método, Runnable, Callable, etc., esta definición se aplica a ellas. Hay más interfaces de este tipo en JDK8: Función, Predicado, Consumidor, Proveedor, etc. (página 157, Apéndice 1 tiene una lista de interfaces más detallada). Las interfaces funcionales pueden tener múltiples métodos estáticos y métodos predeterminados, que se implementan en la interfaz. Podemos usar la anotación @FunctionalInterface para anotar una interfaz funcional. El compilador no utiliza esta anotación, pero puede identificar más claramente el tipo de esta interfaz. No solo eso, si anotamos una interfaz con esta anotación, el compilador verificará enérgicamente si cumple con las reglas de las interfaces funcionales. Si un método recibe una interfaz funcional como parámetro, los parámetros que podemos pasar incluyen:
1. Clases internas anónimas, la forma más antigua
2. Expresión de Lambda, al igual que lo hicimos en el método del mapa
3. Referencia a un método o constructor (hablaremos de ello más tarde)
Si el parámetro del método es una interfaz funcional, el compilador aceptará felizmente una expresión o referencia de método lambda como parámetro. Si pasamos una expresión de lambda a un método, el compilador primero convertirá la expresión en una instancia de la interfaz funcional correspondiente. Esta transformación es más que solo generar una clase interna. Los métodos de esta instancia generada sincrónicamente corresponden a los métodos abstractos de la interfaz funcional del parámetro. Por ejemplo, el método MAP recibe la función de interfaz funcional como un parámetro. Al llamar al método del mapa, el compilador Java lo generará sincrónicamente, como se muestra en la figura a continuación.
Los parámetros de la expresión de Lambda deben coincidir con los parámetros del método abstracto de la interfaz. Este método generado devolverá el resultado de la expresión de Lambda. Si el tipo de retorno no coincide directamente con el método abstracto, este método convertirá el valor de retorno al tipo apropiado. Ya hemos tenido una visión general de cómo se pasan las expresiones lambda a los métodos. Revisemos rápidamente de lo que acabamos de hablar y luego comencemos nuestra exploración de las expresiones Lambda.
Resumir
Esta es una área completamente nueva de Java. A través de funciones de orden superior, ahora podemos escribir un código de estilo funcional elegante y fluido. El código escrito de esta manera es conciso y fácil de entender, tiene pocos errores y es propicio para el mantenimiento y la paralelización. El compilador Java funciona su magia, y donde recibimos parámetros de interfaz funcional, podemos pasar en expresiones lambda o referencias de métodos. Ahora podemos ingresar al mundo de las expresiones Lambda y las bibliotecas JDK adaptadas para que sientan la diversión de ellas. En el próximo capítulo, comenzaremos con las operaciones establecidas más comunes en la programación y liberaremos el poder de las expresiones lambda.