JavaRush /Blog Java /Random-ES /Traducción del libro. Programación funcional en Java. Cap...
timurnav
Nivel 21

Traducción del libro. Programación funcional en Java. Capítulo 1

Publicado en el grupo Random-ES
Estaré encantado de ayudarle a encontrar errores y mejorar la calidad de la traducción. Traduzco para mejorar mis habilidades en el idioma inglés, y si lees y buscas errores de traducción, mejorarás incluso mejor que yo. El autor del libro escribe que el libro implica mucha experiencia trabajando con Java; para ser honesto, yo mismo no tengo mucha experiencia, pero entendí el material del libro. El libro trata sobre alguna teoría que es difícil de explicar con los dedos. Si hay artículos decentes en la wiki, proporcionaré enlaces a ellos, pero para una mejor comprensión le recomiendo que lo busque en Google usted mismo. Buena suerte a todos. :) Para aquellos que quieran corregir mi traducción, así como para aquellos que la encuentren demasiado pobre para leerla en ruso, pueden descargar el libro original aquí . Contenido Capítulo 1 Hola, Expresiones Lambda - actualmente leyendo Capítulo 2 Uso de colecciones - en desarrollo Capítulo 3 Cadenas, comparadores y filtros - en desarrollo Capítulo 4 Desarrollo con expresiones Lambda - en desarrollo Capítulo 5 Trabajar con recursos - en desarrollo Capítulo 6 Ser vago - en desarrollo Capítulo 7 Optimización de recursos - en desarrollo Capítulo 8 Diseño con expresiones lambda - en desarrollo Capítulo 9 Juntándolo todo - en desarrollo

Capítulo 1 ¡Hola, expresiones Lambda!

Nuestro código Java está listo para transformaciones notables. Las tareas diarias que realizamos se vuelven más simples, fáciles y expresivas. La nueva forma de programar Java se utiliza desde hace décadas en otros lenguajes. Con estos cambios en Java, podemos escribir código conciso, elegante y expresivo con menos errores. Podemos usar esto para aplicar estándares fácilmente e implementar patrones de diseño comunes con menos líneas de código. En este libro, exploramos el estilo funcional de programación utilizando ejemplos sencillos de problemas que hacemos todos los días. Antes de sumergirnos en este estilo elegante y esta nueva forma de desarrollar software, veamos por qué es mejor.
cambia tu forma de pensar
El estilo imperativo es lo que Java nos ha brindado desde los inicios del lenguaje. Este estilo sugiere que describamos a Java cada paso de lo que queremos que haga el lenguaje, y luego simplemente nos aseguramos de que esos pasos se sigan fielmente. Esto funcionó muy bien, pero aún es de bajo nivel. El código terminó siendo demasiado detallado y a menudo queríamos un lenguaje que fuera un poco más inteligente. Entonces podríamos decirlo declarativamente: lo que queremos, y no ahondar en cómo hacerlo. Gracias a los desarrolladores, Java ahora puede ayudarnos a hacer esto. Veamos algunos ejemplos para comprender los beneficios y las diferencias entre estos enfoques.
La forma habitual
Comencemos con conceptos básicos familiares para ver los dos paradigmas en acción. Esto utiliza un método imperativo para buscar Chicago en la colección de ciudades: los listados de este libro solo muestran fragmentos de código. boolean found = false; for(String city : cities) { if(city.equals("Chicago")) { found = true; break; } } System.out.println("Found chicago?:" + found); La versión imperativa del código es ruidosa (¿qué tiene que ver esta palabra con ella?) y de bajo nivel, hay varias partes mutables. Primero creamos esta bandera booleana apestosa llamada encontrada y luego iteramos sobre cada elemento de la colección. Si encontramos la ciudad que buscamos, configuramos la bandera en verdadero y rompemos el ciclo. Finalmente imprimimos el resultado de nuestra búsqueda a la consola.
Hay una mejor manera
Como programadores observadores de Java, un vistazo a este código puede convertirlo en algo más expresivo y más fácil de leer, como este: System.out.println("Found chicago?:" + cities.contains("Chicago")); Aquí hay un ejemplo del estilo declarativo: el método contiene() nos ayuda a llegar directamente a lo que necesitamos.
Cambios reales
Estos cambios traerán una cantidad decente de mejoras a nuestro código:
  • Sin problemas con variables mutables
  • Las iteraciones de bucle están ocultas bajo el capó.
  • Menos desorden de códigos
  • Mayor claridad del código, centra la atención
  • Menos impedancia; El código sigue de cerca la intención comercial.
  • Menos posibilidades de error
  • Más fácil de entender y apoyar
Más allá de los casos simples
Este fue un ejemplo simple de una función declarativa que verifica la presencia de un elemento en una colección; se ha utilizado durante mucho tiempo en Java. Ahora imagine no tener que escribir código imperativo para operaciones más avanzadas como analizar archivos, trabajar con bases de datos, realizar solicitudes de servicios web, crear subprocesos múltiples, etc. Java ahora permite escribir código conciso y elegante que hace que sea más difícil cometer errores, no sólo en operaciones simples, sino en toda nuestra aplicación.
la vieja manera
Veamos otro ejemplo. Estamos creando una colección con precios y probaremos varias formas de calcular la suma de todos los precios con descuento. Supongamos que nos piden que sumemos todos los precios cuyo valor exceda los $20, con un 10% de descuento. Primero hagamos esto de la forma habitual en Java. Este código debería resultarnos muy familiar: primero creamos una variable mutable totalOfDiscountedPrices en la que almacenaremos el valor resultante. Luego recorremos la recopilación de precios, seleccionamos precios que están por encima de $20, obtenemos el precio con descuento y agregamos ese valor a totalOfDiscountedPrices . Al final mostramos la suma de todos los precios teniendo en cuenta el descuento. A continuación se muestra lo que se envía a la consola. final List prices = Arrays.asList( new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"), new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("18"), new BigDecimal("45"), new BigDecimal("12")); BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO; for(BigDecimal price : prices) { if(price.compareTo(BigDecimal.valueOf(20)) > 0) totalOfDiscountedPrices = totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9))); } System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
Total de precios rebajados: 67,5
Funciona, pero el código parece confuso. Pero no es culpa nuestra, utilizamos lo que estaba disponible. El código es de un nivel bastante bajo: sufre de una obsesión con las primitivas (búsquelo en Google, cosas interesantes) y va en contra del principio de responsabilidad única . Aquellos de nosotros que trabajamos en casa debemos mantener ese código alejado de los ojos de los niños que aspiran a convertirse en programadores, ya que puede alarmar sus frágiles mentes, estar preparados para la pregunta "¿Es esto lo que tienes que hacer para sobrevivir?"
Hay una manera mejor, otra
Ahora podemos hacerlo mejor, mucho mejor. Nuestro código puede parecerse a un requisito de especificación. Esto nos ayudará a reducir la brecha entre las necesidades comerciales y el código que las implementa, reduciendo aún más la probabilidad de que se malinterpreten los requisitos. En lugar de crear una variable y luego cambiarla repetidamente, trabajemos en un nivel superior de abstracción, como en el siguiente listado. final BigDecimal totalOfDiscountedPrices = prices.stream() .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> price.multiply(BigDecimal.valueOf(0.9))) .reduce(BigDecimal.ZERO, BigDecimal::add); System.out.println("Total of discounted prices: " + totalOfDiscountedPrices); Leámoslo en voz alta: el filtro de precios es mayor que 20, mapee (cree pares de "clave" y "valor") usando la clave "precio", el precio incluye el descuento y luego agréguelos
- el comentario del traductor significa palabras que aparecen en tu cabeza mientras lees el código .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
El código se ejecuta en conjunto en la misma secuencia lógica que hemos leído. El código fue acortado, pero usamos un montón de cosas nuevas de Java 8. Primero, llamamos al método stream() en la lista de precios . Esto abre la puerta a un iterador personalizado con un rico conjunto de características convenientes que analizaremos más adelante. En lugar de recorrer directamente todos los valores en la lista de precios , utilizamos varios métodos especiales como filter() y map() . A diferencia de los métodos que utilizamos en Java y JDK, estos métodos toman una función anónima (una expresión lambda) como parámetro entre paréntesis. Lo estudiaremos con más detalle más adelante. Al llamar al método reduce() , calculamos la suma de los valores (precio con descuento) obtenidos en el método map() . El bucle se oculta de la misma manera que cuando se utiliza el método contiene() . Sin embargo, los métodos filter() y map() son aún más complejos. Para cada precio en la lista de precios , llaman a la función lambda pasada y la guardan en una nueva colección. Se llama al método reduce() en esta colección para producir el resultado final. A continuación se muestra lo que se envía a la consola.
Total de precios rebajados: 67,5
Cambios
A continuación se detallan los cambios relativos al método habitual:
  • El código es agradable a la vista y no está abarrotado.
  • Sin operaciones de bajo nivel
  • Es más fácil mejorar o cambiar la lógica
  • La iteración está controlada por una biblioteca de métodos.
  • Evaluación de bucle diferido y eficiente
  • Más fácil de paralelizar según sea necesario
Más adelante discutiremos cómo Java proporciona estas mejoras.
Lambda al rescate :)
Lambda es la clave funcional para liberarnos de las molestias de la programación imperativa. Al cambiar la forma en que programamos, con las últimas características de Java, podemos escribir código que no solo sea elegante y conciso, sino también menos propenso a errores, más eficiente y más fácil de optimizar, mejorar y convertir en multiproceso.
Gane a lo grande con la programación funcional
El estilo de programación funcional tiene una relación señal-ruido más alta ; Escribimos menos líneas de código, pero cada línea o expresión realiza más funciones. Ganamos poco con la versión funcional del código en comparación con el imperativo:
  • Evitamos cambios no deseados o reasignaciones de variables, que son fuente de errores y dificultan el procesamiento de código de diferentes subprocesos al mismo tiempo. En la versión imperativa, establecemos diferentes valores para la variable totalOfDiscountedPrices a lo largo del ciclo . En la versión funcional, no hay ningún cambio explícito en la variable en el código. Menos cambios generan menos errores en el código.
  • La versión funcional del código es más fácil de paralelizar. Incluso si los cálculos en el método map() fueran largos, podemos ejecutarlos en paralelo sin temor a nada. Si accedemos a código de estilo imperativo desde diferentes hilos, tendremos que preocuparnos de cambiar la variable totalOfDiscountedPrices al mismo tiempo . En la versión funcional, accedemos a la variable solo después de que se hayan realizado todos los cambios, esto nos libera de preocuparnos por la seguridad de los subprocesos del código.
  • El código es más expresivo. En lugar de ejecutar el código en varios pasos (crear e inicializar una variable con un valor ficticio, recorrer la lista de precios, agregar precios de descuento a la variable, etc.), simplemente le pedimos al método map() de la lista que devuelva otra lista. de precios rebajados y sumarlos .
  • El estilo funcional es más conciso: se requieren menos líneas de código que la versión imperativa. Un código más compacto significa menos que escribir, menos que leer y más fácil de mantener.
  • La versión funcional del código es intuitiva y fácil de entender, una vez que conoces su sintaxis. El método map() aplica la función pasada (que calcula el precio descontado) a cada elemento de la colección y produce una colección con el resultado, como podemos ver en la imagen a continuación.

Imagen Figura 1: el método de mapa aplica la función pasada a cada elemento de la colección
Con el soporte de expresiones lambda, podemos aprovechar plenamente el poder del estilo funcional de programación en Java. Si dominamos este estilo, podremos crear código más expresivo y conciso con menos cambios y errores. Anteriormente, una de las ventajas clave de Java era su compatibilidad con el paradigma orientado a objetos. Y el estilo funcional no contradice la programación orientada a objetos. Excelencia real en pasar de la programación imperativa a la declarativa. Con Java 8 podemos combinar la programación funcional con un estilo orientado a objetos de forma bastante eficaz. Podemos seguir aplicando el estilo OO a los objetos, su alcance, estado y relaciones. Además, podemos modelar el comportamiento y el estado de cambio, los procesos comerciales y el procesamiento de datos como una serie de conjuntos de funciones.
¿Por qué codificar en estilo funcional?
Hemos visto los beneficios generales del estilo funcional de programación, pero ¿vale la pena aprender este nuevo estilo? ¿Será este un cambio menor en el lenguaje o cambiará nuestras vidas? Debemos obtener respuestas a estas preguntas antes de perder nuestro tiempo y energía. Escribir código Java no es tan difícil; la sintaxis del lenguaje es simple. Nos sentimos cómodos con bibliotecas y API familiares. Lo que realmente requiere que nos esforcemos en escribir y mantener el código son las aplicaciones empresariales típicas en las que utilizamos Java para el desarrollo. Necesitamos asegurarnos de que los compañeros programadores cierren las conexiones a la base de datos en el momento correcto, que no las retengan ni realicen transacciones más tiempo del necesario, que detecten las excepciones por completo y en el nivel correcto, que apliquen y liberen los bloqueos correctamente. ... esta hoja se puede continuar durante mucho tiempo. Cada uno de los argumentos anteriores por sí solos no tiene peso, pero en conjunto, cuando se combinan con las complejidades inherentes de su implementación, se vuelven abrumadores, requieren mucho tiempo y son difíciles de implementar. ¿Qué pasaría si pudiéramos encapsular estas complejidades en pequeños fragmentos de código que también pudieran gestionarlas bien? Entonces no gastaríamos energía constantemente en implementar normas. Esto daría una gran ventaja, así que veamos cómo puede ayudar un estilo funcional.
joe pregunta
¿Un código corto* simplemente significa menos letras de código?
* estamos hablando de la palabra conciso , que caracteriza el estilo funcional del código utilizando expresiones lambda
En este contexto, el código debe ser conciso, sin florituras y reducido a un impacto directo para transmitir la intención de manera más efectiva. Estos son beneficios de gran alcance. Escribir código es como juntar ingredientes: hacerlo conciso es como agregarle salsa. A veces se necesita más esfuerzo para escribir dicho código. Menos código para leer, pero lo hace más transparente. Es importante mantener el código claro al acortarlo. El código conciso es similar a los trucos de diseño. Este código requiere menos baile con pandereta. Esto significa que podemos implementar rápidamente nuestras ideas y seguir adelante si funcionan y abandonarlas si no están a la altura de las expectativas.
Iteraciones con esteroides
Usamos iteradores para procesar listas de objetos, así como para trabajar con Conjuntos y Mapas. Los iteradores que utilizamos en Java nos son familiares, aunque son primitivos, no son simples. No sólo ocupan varias líneas de código, sino que también son bastante difíciles de escribir. ¿Cómo iteramos a través de todos los elementos de una colección? Podríamos usar un bucle for. ¿Cómo seleccionamos algunos elementos de la colección? Usando el mismo bucle for, pero usando algunas variables mutables adicionales que deben compararse con algo de la colección. Luego, después de seleccionar un valor específico, ¿cómo realizamos operaciones en un solo valor, como mínimo, máximo o algún valor promedio? Nuevamente ciclos, nuevamente nuevas variables. Esto recuerda al proverbio, no se pueden ver los árboles debido al bosque (el original usa un juego de palabras relacionado con las iteraciones y significa "Todo se asume, pero no todo tiene éxito" - nota del traductor). jdk ahora proporciona iteradores internos para varias declaraciones: uno para simplificar el bucle, uno para vincular la dependencia de resultados requerida, uno para filtrar los valores de salida, uno para devolver los valores y varias funciones convenientes para obtener mínimos, máximos, promedios, etc. Además, la funcionalidad de estas operaciones se puede combinar muy fácilmente, de modo que podemos combinar diferentes conjuntos de ellas para implementar la lógica de negocio con mayor facilidad y menos código. Cuando hayamos terminado, el código será más fácil de entender ya que crea una solución lógica en la secuencia requerida por el problema. Veremos algunos ejemplos de dicho código en el Capítulo 2 y más adelante en este libro.
Aplicación de algoritmos
Los algoritmos impulsan las aplicaciones empresariales. Por ejemplo, necesitamos proporcionar una operación que requiera verificación de autoridad. Tendremos que asegurarnos de que las transacciones se completen rápidamente y los controles se completen correctamente. Estas tareas a menudo se reducen a un método muy común, como se muestra en la siguiente lista: Transaction transaction = getFromTransactionFactory(); //... Операция выполняющаяся во время транзакции... checkProgressAndCommitOrRollbackTransaction(); UpdateAuditTrail(); Hay dos problemas con este enfoque. En primer lugar, esto a menudo lleva a duplicar el esfuerzo de desarrollo, lo que a su vez conduce a un aumento en el costo de mantenimiento de la aplicación. En segundo lugar, es muy fácil pasar por alto excepciones que pueden aparecer en el código de esta aplicación, poniendo así en peligro la ejecución de la transacción y la aprobación de las comprobaciones. Podemos usar un bloque try-finally adecuado, pero cada vez que alguien toque este código, necesitaremos volver a verificar que la lógica del código no se haya roto. De lo contrario, podríamos abandonar la fábrica y poner todo el código patas arriba. En lugar de recibir transacciones, podríamos enviar código de procesamiento a una función bien administrada, como el código siguiente. runWithinTransaction((Transaction transaction) -> { //... Операция выполняющаяся во время транзакции... }); Estos pequeños cambios suman grandes ahorros. El algoritmo para la verificación de estado y de aplicaciones recibe un nuevo nivel de abstracción y se encapsula utilizando el método runWithinTransaction() . En este método colocamos un fragmento de código que debe ejecutarse en el contexto de una transacción. Ya no tenemos que preocuparnos por olvidarnos de hacer algo o si detectamos la excepción en el lugar correcto. Las funciones algorítmicas se encargan de esto. Este tema se discutirá con más detalle en el Capítulo 5.
Extensiones de algoritmo
Los algoritmos se utilizan cada vez con más frecuencia, pero para que puedan utilizarse plenamente en el desarrollo de aplicaciones empresariales, se requieren formas de ampliarlos.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION