0% encontró este documento útil (0 votos)
621 vistas705 páginas

L1

Cargado por

cesar santos
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
621 vistas705 páginas

L1

Cargado por

cesar santos
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 705

Java 11 Cookbook Second Edition

Copyright © 2018 Packt Publishing

Todos los derechos reservados. Ninguna parte de este libro puede reproducirse,
almacenarse en un sistema de recuperación o transmitirse de ninguna forma o por
ningún medio, sin el permiso previo por escrito del editor, excepto en el caso de citas
breves incluidas en artículos críticos o reseñas.

Se ha hecho todo lo posible en la preparación de este libro para garantizar la precisión


de la información presentada. Sin embargo, la información contenida en este libro se
vende sin garantía, ya sea expresa o implícita. Ni los autores, ni Packt Publishing o sus
distribuidores y distribuidores serán responsables de los daños causados o
presuntamente causados directa o indirectamente por este libro.

Packt Publishing se ha esforzado por proporcionar información de marcas comerciales sobre todas las empresas y
productos mencionados en este libro mediante el uso apropiado de los capitales. Sin embargo, Packt Publishing no
puede garantizar la exactitud de esta información.

Editor de puesta en servicio : Richa Tripathi


Editor de adquisición: Denim Pinto
Editor de desarrollo de contenido: Pooja Parvatkar
Editor técnico: Mehul Singh
Editor de copia: Safis
Coordinador del proyecto de edición: Ulhas Kambali
Corrector de pruebas: Safis Indizador de edición
: Rekha Nair
Gráficos: Tom Scaria
Coordinador de producción: Arvindkumar Gupta

Primera publicación: agosto de 2017


Segunda edición: septiembre de 2018

Referencia de producción: 1210918

Publicado por Packt Publishing Ltd.


Livery Place
35 Livery Street
Birmingham
B3 2PB, Reino Unido.

ISBN 978-1-78913-235-9

www.packtpub.com

¿Por qué suscribirse?


• Pase menos tiempo aprendiendo y más tiempo codificando con libros
electrónicos y videos prácticos de más de 4,000 profesionales de la industria
• Mejore su aprendizaje con los planes de habilidades creados especialmente para
usted
• Obtenga un libro electrónico o video gratis cada mes
• Mapt se puede buscar por completo
• Copie y pegue, imprima y marque contenido

Packt.com
¿Sabía que Packt ofrece versiones de libros electrónicos de cada libro publicado, con
archivos PDF y ePub disponibles? Puede actualizar a la versión de eBook
en www.packt.com y, como cliente de un libro impreso, tiene derecho a un descuento en
la copia del eBook. Póngase en contacto con
nosotros [email protected] para más detalles.

En www.packt.com , también puede leer una colección de artículos técnicos gratuitos,


suscribirse a una variedad de boletines gratuitos y recibir descuentos exclusivos y
ofertas en libros y libros electrónicos de Packt.

Contribuyentes
Sobre los autores
Nick Samoylov se graduó como ingeniero físico del Instituto de Física y Tecnología de
Moscú , trabajó como físico teórico y aprendió a programar como herramienta para
probar sus modelos matemáticos usando FORTRAN y C ++. Después de la desaparición
de la URSS, Nick creó y dirigió con éxito una compañía de software, pero se vio obligado
a cerrarla bajo la presión de las estafas gubernamentales y criminales. En 1999, con su
esposa Luda y sus dos hijas, emigró a los Estados Unidos y desde entonces vive en
Colorado, trabajando como programador en Java. En su tiempo libre, a Nick le gusta leer
(principalmente no ficción), escribir (novelas de ficción y blogs) y caminar por las
Montañas Rocosas.

Mohamed Sanaulla es un desarrollador full-stack con más de 8 años de


experiencia en el desarrollo de aplicaciones empresariales y soluciones de back-end
basadas en Java para aplicaciones de comercio electrónico.
Sus intereses incluyen el desarrollo de software empresarial, refactorización y
rediseño de aplicaciones, diseño e implementación de servicios web RESTful,
solución de problemas de aplicaciones Java por problemas de rendimiento y TDD.
Tiene una gran experiencia en el desarrollo de aplicaciones basadas en Java, ADF
(un marco web Java EE basado en JSF), SQL, PL / SQL, JUnit, diseño de servicios
RESTful, Spring, Spring Boot, Struts, Elasticsearch y MongoDB. También es
programador de Java certificado por Sun para la plataforma Java 6. Es moderador
de JavaRanch y le gusta compartir sus hallazgos en su blog.

Me gustaría agradecer a mi familia por su aliento, apoyo y por soportar mi


ausencia mientras estaba ocupado compilando este libro. También me gustaría
agradecer al equipo de Packt Publishing por darme la oportunidad de crear el
libro.

Sobre el revisor
Aristides Villarreal Bravo es un desarrollador de Java, miembro del equipo NetBeans
Dream Team y líder de grupos de usuarios de Java. El vive en Panamá. Ha organizado y
participado en varias conferencias y seminarios relacionados con Java, JavaEE,
NetBeans, la plataforma NetBeans, software libre y dispositivos móviles. Es autor
de jmoordb y tutoriales y blogs sobre Java, NetBeans y desarrollo web. Aristides ha
participado en varias entrevistas en sitios sobre temas como NetBeans, NetBeans
DZone y JavaHispano. Es desarrollador de complementos para NetBeans.

Me gustaría agradecer a mi madre, mi padre y toda mi familia y amigos.

Packt está buscando autores como tú


Si está interesado en convertirse en autor de Packt,
visite author.packtpub.com y solicítelo hoy. Hemos trabajado con miles de
desarrolladores y profesionales de la tecnología, como usted, para ayudarlos a
compartir sus ideas con la comunidad tecnológica global. Puede hacer una solicitud
general, solicitar un tema candente específico para el que estamos reclutando un autor
o presentar su propia idea.

Prefacio
Este libro de cocina ofrece una variedad de ejemplos de desarrollo de software que se
ilustran mediante un código simple y directo, que proporciona recursos paso a paso y
métodos que ahorran tiempo para ayudarlo a resolver problemas de datos de manera
eficiente. Comenzando con la instalación de Java, cada receta aborda un problema
específico y se acompaña de una discusión que explica la solución y ofrece información
sobre cómo funciona. Cubrimos conceptos importantes sobre el lenguaje de
programación central, así como tareas comunes involucradas en la construcción de una
amplia variedad de software. Seguirás recetas para aprender sobre las nuevas
características de la última versión de Java 11 para hacer que tu aplicación sea modular,
segura y rápida.
Tabla de contenido
Para quien es este libro ......................................................................................................................1
Lo que cubre este libro .......................................................................................................................1
Para aprovechar al máximo este libro ................................................................................................3
Descargue los archivos de código de ejemplo .................................................................................3
Convenciones utilizadas .....................................................................................................................3
Secciones............................................................................................................................................4
Prepararse ..........................................................................................................................................5
Cómo hacerlo… ..................................................................................................................................5
Cómo funciona… ................................................................................................................................5
Hay más… ...........................................................................................................................................5
Ver también .......................................................................................................................................5
Estar en contacto ...............................................................................................................................5
Comentarios .......................................................................................................................................6
Instalación y un adelanto en Java 11 ..............................................................................................6
Introducción .......................................................................................................................................6
Instalar JDK 18.9 en Windows y configurar la variable PATH .............................................................7
Cómo hacerlo... .................................................................................................................................7
Instalar JDK 18.9 en Linux (Ubuntu, x64) y configurar la variable PATH ...........................................11
Cómo hacerlo... ...............................................................................................................................11
Instalar Eclipse .................................................................................................................................12
Compilar y ejecutar una aplicación Java ...........................................................................................16
Prepararse ........................................................................................................................................17
Cómo hacerlo... ...............................................................................................................................17
¿Qué hay de nuevo en Java 11? .......................................................................................................19
Prepararse ......................................................................................................................................19
Cómo hacerlo... ................................................................................................................................20
Bloques ............................................................................................................................................20
Variables ..........................................................................................................................................20
Tipos .................................................................................................................................................21
Matrices ...........................................................................................................................................22
Expresiones .....................................................................................................................................23
Bucles ...............................................................................................................................................25
Ejecución condicional .......................................................................................................................28
Variables finales ...............................................................................................................................29
Clases ...............................................................................................................................................30
Clases internas, anidadas, locales y anónimas. ................................................................................32
Paquetes ..........................................................................................................................................34
Métodos ...........................................................................................................................................35
Interfaces .........................................................................................................................................37
Argumento que pasa ........................................................................................................................38
Campos ............................................................................................................................................39
Modificadores ..................................................................................................................................40
Inicializadores y constructores de objetos. ......................................................................................41
JEP 318 - Epsilon...............................................................................................................................42
JEP 321 - Cliente HTTP (estándar) ....................................................................................................42
JEP 323 - Sintaxis de variable local para parámetros Lambda ..........................................................42
JEP 333 - ZGC ....................................................................................................................................42
Nueva API .......................................................................................................................................43
Hay más... .......................................................................................................................................45
Uso de uso compartido de datos de clase de aplicación ..................................................................45
Prepararse ......................................................................................................................................46
Cómo hacerlo... ...............................................................................................................................46
Fast Track to OOP - Clases e interfaces .......................................................................................48
Introducción ...................................................................................................................................48
Implementación de diseño orientado a objetos (OOD) ....................................................................49
Prepararse ........................................................................................................................................49
Cómo hacerlo... ...............................................................................................................................50
Cómo funciona... ..............................................................................................................................51
Hay más... .......................................................................................................................................52
Prepararse ........................................................................................................................................53
Cómo hacerlo... ...............................................................................................................................54
Cómo funciona... ..............................................................................................................................55
Hay más... .........................................................................................................................................58
Usando herencia y agregación .........................................................................................................58
Prepararse ........................................................................................................................................58
Cómo hacerlo... ...............................................................................................................................59
Cómo funciona... ..............................................................................................................................60
La agregación hace que el diseño sea más extensible ......................................................................66
Codificación a una interfaz ...............................................................................................................67
Prepararse ........................................................................................................................................68
Cómo hacerlo... ...............................................................................................................................68
Cómo funciona... ..............................................................................................................................69
Hay más... .........................................................................................................................................71
Crear interfaces con métodos predeterminados y estáticos ............................................................72
Prepararse ........................................................................................................................................72
Cómo hacerlo... ...............................................................................................................................73
Cómo funciona... ..............................................................................................................................75
Crear interfaces con métodos privados............................................................................................76
Prepararse ........................................................................................................................................76
Cómo hacerlo... ...............................................................................................................................76
Cómo funciona... ..............................................................................................................................77
Hay más... .........................................................................................................................................77
Una mejor manera de trabajar con nulos usando Opcional .............................................................78
Prepararse ........................................................................................................................................78
Cómo hacerlo... ...............................................................................................................................80
Cómo funciona... ..............................................................................................................................83
Hay más... .........................................................................................................................................84
Usando la clase de utilidad Objetos .................................................................................................85
Prepararse ......................................................................................................................................85
Cómo hacerlo... ...............................................................................................................................85
Cómo funciona................................................................................................................................91
Programacion Modular .................................................................................................................92
Introducción ...................................................................................................................................92
Usando jdeps para encontrar dependencias en una aplicación Java ...............................................93
Prepararse ......................................................................................................................................94
Cómo hacerlo... ...............................................................................................................................95
Cómo funciona..............................................................................................................................100
Hay más... .....................................................................................................................................101
Crear una aplicación modular simple .............................................................................................101
Prepararse ....................................................................................................................................102
Cómo hacerlo... .............................................................................................................................102
Cómo funciona..............................................................................................................................107
Ver también ...................................................................................................................................109
Crear un JAR modular.....................................................................................................................110
Prepararse ......................................................................................................................................110
Cómo hacerlo... .............................................................................................................................110
Uso de un módulo JAR con aplicaciones JDK de Jigsaw anteriores al proyecto..............................112
Prepararse ......................................................................................................................................112
Cómo hacerlo... .............................................................................................................................112
Migración de abajo hacia arriba .................................................................................................113
Prepararse ....................................................................................................................................115
Cómo hacerlo... ..............................................................................................................................116
Modularizing banking.util.jar .....................................................................................................117
Modularizing math.util.jar ..........................................................................................................119
Modularizing calculator.jar ........................................................................................................122
Cómo funciona... ............................................................................................................................123
Migración de arriba hacia abajo .....................................................................................................124
Prepararse ......................................................................................................................................124
Cómo hacerlo... ..............................................................................................................................125
Modularizando la calculadora.....................................................................................................125
Modularizing banking.util ...........................................................................................................127
Modularizing math.util ................................................................................................................128
Usar servicios para crear un acoplamiento flexible entre los módulos de consumidor y proveedor
.......................................................................................................................................................129
Prepararse ......................................................................................................................................129
Cómo hacerlo... .............................................................................................................................130
Crear una imagen de tiempo de ejecución modular personalizada usando jlink ....................134
Prepararse ....................................................................................................................................135
Cómo hacerlo... .............................................................................................................................135
Compilación para versiones de plataforma anteriores ..................................................................136
Prepararse ......................................................................................................................................136
Cómo hacerlo... .............................................................................................................................137
Cómo funciona... ............................................................................................................................139
Crear JAR de lanzamiento múltiple ................................................................................................139
Cómo hacerlo... .............................................................................................................................139
Cómo funciona... ............................................................................................................................141
Usando Maven para desarrollar una aplicación modular ...............................................................142
Prepararse ....................................................................................................................................142
Cómo hacerlo... .............................................................................................................................143
Hacer que su biblioteca sea amigable con los módulos .................................................................146
Prepararse ......................................................................................................................................146
Cómo hacerlo... .............................................................................................................................146
Cómo funciona..............................................................................................................................148
Hay más... .......................................................................................................................................150
Cómo abrir un módulo para reflexionar .........................................................................................151
Prepararse ......................................................................................................................................151
Cómo hacerlo... .............................................................................................................................151
Cómo funciona..............................................................................................................................152
Funcionando .................................................................................................................................153
Introducción ...................................................................................................................................153
Usar interfaces funcionales estándar .............................................................................................154
Prepararse ......................................................................................................................................155
Cómo hacerlo... .............................................................................................................................157
Cómo funciona... ............................................................................................................................160
Hay más... .......................................................................................................................................162
Crear una interfaz funcional ...........................................................................................................164
Prepararse ......................................................................................................................................164
Cómo hacerlo... ..............................................................................................................................165
Cómo funciona... ............................................................................................................................167
Hay más... .......................................................................................................................................167
Comprender las expresiones lambda .............................................................................................168
Prepararse ......................................................................................................................................169
Cómo hacerlo... ..............................................................................................................................169
Cómo funciona... ............................................................................................................................170
Hay más... .....................................................................................................................................171
Usar expresiones lambda ...............................................................................................................172
Prepararse ......................................................................................................................................173
Cómo hacerlo... ..............................................................................................................................175
Cómo funciona... ............................................................................................................................178
Hay más... .......................................................................................................................................179
Usando referencias de métodos ....................................................................................................179
Prepararse ......................................................................................................................................179
Cómo hacerlo... ..............................................................................................................................180
Referencia de método no enlazado estático ................................................................................180
Referencia de método enlazado no estático ................................................................................182
Referencia de método independiente no estático .......................................................................184
Referencias de métodos de constructor ......................................................................................185
Hay más... .....................................................................................................................................187
Aprovechando expresiones lambda en tus programas ..................................................................188
Prepararse ......................................................................................................................................188
Cómo hacerlo... .............................................................................................................................191
Hay más... .......................................................................................................................................197
Arroyos y tuberías ........................................................................................................................198
Introducción ...................................................................................................................................198
Crear colecciones inmutables utilizando los métodos de fábrica of () y copyOf () .........................199
Prepararse ......................................................................................................................................199
Cómo hacerlo... ..............................................................................................................................203
Hay más... .......................................................................................................................................204
Crear y operar en streams ..............................................................................................................205
Prepararse ....................................................................................................................................205
Cómo hacerlo... ..............................................................................................................................207
Cómo funciona... ............................................................................................................................211
Usando flujos numéricos para operaciones aritméticas.................................................................223
Prepararse ....................................................................................................................................223
Cómo hacerlo... .............................................................................................................................224
Hay más... .....................................................................................................................................228
Completando transmisiones produciendo colecciones ..................................................................230
Prepararse ....................................................................................................................................231
Cómo hacerlo... .............................................................................................................................232
Completando transmisiones produciendo mapas ..........................................................................236
Prepararse ....................................................................................................................................236
Cómo hacerlo... .............................................................................................................................240
Completar transmisiones produciendo mapas con recopiladores de agrupación ..........................247
Prepararse ....................................................................................................................................247
Cómo hacerlo... .............................................................................................................................251
Hay más... .....................................................................................................................................256
Crear canalización de operación de flujo .......................................................................................257
Prepararse ......................................................................................................................................257
Cómo hacerlo... ..............................................................................................................................258
Hay más... .......................................................................................................................................263
Procesando flujos en paralelo ........................................................................................................263
Prepararse ......................................................................................................................................264
Cómo hacerlo... ..............................................................................................................................264
Programación de bases de datos .................................................................................................266
Introducción .................................................................................................................................267
Conexión a una base de datos utilizando JDBC ..............................................................................268
Cómo hacerlo... .............................................................................................................................268
Cómo funciona... ............................................................................................................................269
Hay más... .......................................................................................................................................271
Configurar las tablas necesarias para las interacciones de DB .......................................................272
Prepararse ......................................................................................................................................272
Cómo funciona... ............................................................................................................................272
Hay más... .......................................................................................................................................275
Realizando operaciones CRUD usando JDBC ..................................................................................276
Prepararse ......................................................................................................................................276
Cómo hacerlo... ..............................................................................................................................277
Hay más... .....................................................................................................................................280
Uso del conjunto de conexiones de Hikari (HikariCP).....................................................................282
Prepararse ......................................................................................................................................282
Cómo hacerlo... .............................................................................................................................282
Cómo funciona... ............................................................................................................................285
Hay más... .......................................................................................................................................286
Usar declaraciones preparadas ......................................................................................................286
Prepararse ......................................................................................................................................286
Cómo hacerlo... ..............................................................................................................................287
Hay más... .......................................................................................................................................287
Usar transacciones .........................................................................................................................288
Prepararse ......................................................................................................................................288
Cómo hacerlo... ..............................................................................................................................288
Hay más... .......................................................................................................................................293
Trabajando con objetos grandes ....................................................................................................294
Prepararse ......................................................................................................................................294
Cómo hacerlo... ..............................................................................................................................295
Hay más... .......................................................................................................................................303
Ejecutar procedimientos almacenados ..........................................................................................304
Prepararse ....................................................................................................................................304
Cómo hacerlo... .............................................................................................................................305
Hay más... .....................................................................................................................................312
Uso de operaciones por lotes para un gran conjunto de datos ......................................................312
Prepararse ......................................................................................................................................312
Cómo hacerlo... .............................................................................................................................313
Cómo funciona... ............................................................................................................................317
Hay más... .......................................................................................................................................318
Usando MyBatis para operaciones CRUD .......................................................................................318
Prepararse ......................................................................................................................................319
Cómo hacerlo... .............................................................................................................................320
Cómo funciona... ............................................................................................................................330
Hay más... .......................................................................................................................................331
Uso de la API de persistencia de Java e Hibernate .........................................................................331
Prepararse ......................................................................................................................................331
Cómo hacerlo... ..............................................................................................................................332
Cómo funciona... ............................................................................................................................339
Programación concurrente y multiproceso ....................................................................................340
Introducción ...................................................................................................................................340
Usando el elemento básico de concurrencia - hilo.........................................................................341
Prepararse ......................................................................................................................................341
Cómo hacerlo... ..............................................................................................................................342
Hay más... .......................................................................................................................................347
Diferentes enfoques de sincronización ..........................................................................................347
Prepararse ......................................................................................................................................347
Cómo hacerlo... ..............................................................................................................................348
Hay más... .......................................................................................................................................351
La inmutabilidad como un medio para lograr la concurrencia .......................................................352
Prepararse ......................................................................................................................................352
Cómo hacerlo... ..............................................................................................................................353
Hay más... .......................................................................................................................................354
Usar colecciones concurrentes.......................................................................................................354
Prepararse ......................................................................................................................................354
Cómo hacerlo... ..............................................................................................................................355
Cómo funciona..............................................................................................................................370
Usar el servicio ejecutor para ejecutar tareas asíncronas ..............................................................371
Prepararse ......................................................................................................................................371
Cómo hacerlo... ..............................................................................................................................372
Cómo funciona... ............................................................................................................................378
Hay más... .......................................................................................................................................382
Usando fork / join para implementar divide-and-conquer.............................................................384
Prepararse ......................................................................................................................................384
Cómo hacerlo... ..............................................................................................................................386
Uso del flujo para implementar el patrón de publicación-suscripción ...........................................393
Prepararse ......................................................................................................................................394
Cómo hacerlo... ..............................................................................................................................395
Mejor gestión del proceso del sistema operativo ..........................................................................398
Introducción ...................................................................................................................................399
Engendrando un nuevo proceso ....................................................................................................400
Prepararse ....................................................................................................................................400
Cómo hacerlo... ..............................................................................................................................400
Cómo funciona... ............................................................................................................................401
Redirigir la salida del proceso y las secuencias de error al archivo ................................................402
Prepararse ....................................................................................................................................402
Cómo hacerlo... ..............................................................................................................................403
Hay más... .....................................................................................................................................404
Cambiar el directorio de trabajo de un subproceso .......................................................................405
Prepararse ....................................................................................................................................405
Cómo hacerlo... ..............................................................................................................................406
Cómo funciona... ............................................................................................................................407
Establecer la variable de entorno para un subproceso ..................................................................407
Cómo hacerlo... ..............................................................................................................................408
Cómo funciona..............................................................................................................................409
Ejecutar scripts de shell ..................................................................................................................409
Prepararse ......................................................................................................................................410
Cómo hacerlo... ..............................................................................................................................410
Cómo funciona... ............................................................................................................................412
Obtención de la información del proceso de la JVM actual ...........................................................412
Cómo hacerlo... ..............................................................................................................................413
Cómo funciona... ............................................................................................................................414
Obtención de la información del proceso del proceso generado ...................................................415
Prepararse ......................................................................................................................................416
Cómo hacerlo... ..............................................................................................................................416
Gestionar el proceso generado ......................................................................................................417
Cómo hacerlo... ..............................................................................................................................417
Enumerar procesos en vivo en el sistema ......................................................................................419
Cómo hacerlo... ..............................................................................................................................419
Conectando múltiples procesos usando tubería ............................................................................420
Prepararse ......................................................................................................................................421
Cómo hacerlo... ..............................................................................................................................422
Cómo funciona... ............................................................................................................................422
Administrar subprocesos................................................................................................................423
Prepararse ......................................................................................................................................423
Cómo hacerlo... ..............................................................................................................................424
Cómo funciona... ............................................................................................................................425
Servicios web RESTful usando Spring Boot .....................................................................................425
Introducción ...................................................................................................................................426
Crear una aplicación Spring Boot simple ........................................................................................426
Prepararse ......................................................................................................................................426
Cómo hacerlo... ..............................................................................................................................428
Cómo funciona... ............................................................................................................................428
Interactuando con la base de datos ...............................................................................................430
Prepararse ......................................................................................................................................430
Instalar herramientas MySQL .........................................................................................................430
Crear una base de datos de muestra..............................................................................................432
Crear una tabla de persona ............................................................................................................432
Completar datos de muestra ..........................................................................................................433
Cómo hacerlo... ..............................................................................................................................433
Cómo funciona... ............................................................................................................................436
Crear un servicio web RESTful ........................................................................................................437
Prepararse ......................................................................................................................................437
Cómo hacerlo... ..............................................................................................................................438
Cómo funciona... ............................................................................................................................442
Crear múltiples perfiles para Spring Boot.......................................................................................443
Prepararse ......................................................................................................................................444
Cómo hacerlo... ..............................................................................................................................446
Cómo funciona... ............................................................................................................................447
Hay más... .......................................................................................................................................448
Implementación de servicios web RESTful en Heroku....................................................................449
Prepararse ......................................................................................................................................450
Configurar una cuenta de Heroku ..................................................................................................450
Crear una nueva aplicación desde la interfaz de usuario ..........................................................452
Crear una nueva aplicación desde la CLI ........................................................................................453
Cómo hacerlo... ..............................................................................................................................454
Hay más... .......................................................................................................................................455
Contenedor del servicio web RESTful usando Docker ....................................................................458
Prepararse ......................................................................................................................................460
Cómo hacerlo... ..............................................................................................................................461
Cómo funciona... ............................................................................................................................463
Monitoreo de la aplicación Spring Boot 2 usando Micrometer y Prometheus ...............................464
Prepararse ......................................................................................................................................465
Cómo hacerlo... ..............................................................................................................................466
Cómo funciona... ............................................................................................................................469
Hay más ..........................................................................................................................................472
Redes..............................................................................................................................................473
Introducción ...................................................................................................................................473
Hacer una solicitud HTTP GET ........................................................................................................474
Cómo hacerlo... ..............................................................................................................................474
Cómo funciona... ............................................................................................................................475
Hacer una solicitud HTTP POST................................................................................................477
Cómo hacerlo... ..............................................................................................................................477
Realizar una solicitud HTTP para un recurso protegido ..................................................................479
Cómo hacerlo... ..............................................................................................................................479
Cómo funciona... ............................................................................................................................481
Hacer una solicitud HTTP asincrónica.............................................................................................482
Cómo hacerlo... ..............................................................................................................................482
Realizar una solicitud HTTP utilizando Apache HttpClient..............................................................483
Prepararse ....................................................................................................................................484
Cómo hacerlo... ..............................................................................................................................484
Hay más... .......................................................................................................................................485
Realizar una solicitud HTTP utilizando la biblioteca de cliente HTTP Unirest .................................486
Prepararse ......................................................................................................................................486
Cómo hacerlo... ..............................................................................................................................487
Hay más... .......................................................................................................................................487
Gestión de memoria y depuración .................................................................................................488
Introducción ...................................................................................................................................488
Comprender el recolector de basura G1 ........................................................................................490
Prepararse ......................................................................................................................................491
Cómo hacerlo... ..............................................................................................................................493
Cómo funciona... ............................................................................................................................496
Registro unificado para JVM...........................................................................................................498
Prepararse ......................................................................................................................................499
Cómo hacerlo... ..............................................................................................................................500
Usando el comando jcmd para la JVM ...........................................................................................502
Cómo hacerlo... ..............................................................................................................................504
Cómo funciona... ............................................................................................................................506
Prueba con recursos para un mejor manejo de recursos ...............................................................508
Cómo hacerlo... ..............................................................................................................................509
Cómo funciona..............................................................................................................................511
Apilamiento para mejorar la depuración .......................................................................................512
Prepararse ......................................................................................................................................514
Cómo hacerlo... ..............................................................................................................................515
Cómo funciona... ............................................................................................................................516
Usando el estilo de codificación de memoria ..............................................................................518
Cómo hacerlo... .............................................................................................................................519
Mejores prácticas para un mejor uso de la memoria. ....................................................................528
Cómo hacerlo... .............................................................................................................................528
Entendiendo Epsilon, un recolector de basura de bajo costo ........................................................530
Cómo hacerlo... .............................................................................................................................531
El bucle de lectura-evaluación-impresión (REPL) usando JShell ............................................533
Introducción ...................................................................................................................................534
Familiarizarse con REPL ..................................................................................................................534
Prepararse ......................................................................................................................................535
Cómo hacerlo... ..............................................................................................................................535
Cómo funciona... ............................................................................................................................536
Navegando por JShell y sus comandos ...........................................................................................536
Cómo hacerlo... .............................................................................................................................537
Evaluación de fragmentos de código ...........................................................................................539
Cómo hacerlo... .............................................................................................................................540
Hay más... .......................................................................................................................................542
Programación orientada a objetos en JShell ..................................................................................543
Cómo hacerlo... .............................................................................................................................543
Guardar y restaurar el historial de comandos de JShell .................................................................544
Cómo hacerlo... .............................................................................................................................544
Usando la API Java JShell ................................................................................................................546
Cómo hacerlo... .............................................................................................................................547
Cómo funciona..............................................................................................................................549
Trabajar con nuevas API de fecha y hora .......................................................................................549
Introducción ...................................................................................................................................550
Cómo trabajar con instancias de fecha y hora independientes de la zona horaria ........................550
Prepararse ......................................................................................................................................551
Cómo hacerlo… ............................................................................................................................551
Cómo funciona…..........................................................................................................................552
Hay más….....................................................................................................................................553
Cómo construir instancias horarias dependientes de la zona horaria ............................................554
Prepararse ......................................................................................................................................554
Cómo hacerlo… ............................................................................................................................555
Cómo funciona… ............................................................................................................................555
Hay más….....................................................................................................................................559
Cómo crear un período basado en fechas entre instancias de fechas ...........................................560
Prepararse ......................................................................................................................................560
Cómo hacerlo… ............................................................................................................................560
Cómo funciona… ............................................................................................................................561
Hay más….....................................................................................................................................562
Cómo crear un período basado en el tiempo entre instancias de tiempo .................................564
Prepararse ......................................................................................................................................564
Cómo hacerlo… ............................................................................................................................565
Cómo funciona… ............................................................................................................................565
Hay más… .......................................................................................................................................566
Cómo representar el tiempo de época ...........................................................................................567
Prepararse ......................................................................................................................................568
Cómo hacerlo… ............................................................................................................................568
Cómo funciona…..........................................................................................................................568
Hay más… .......................................................................................................................................569
Cómo manipular instancias de fecha y hora...................................................................................569
Prepararse ......................................................................................................................................569
Cómo hacerlo… ............................................................................................................................570
Hay más… .......................................................................................................................................570
Cómo comparar fecha y hora .........................................................................................................570
Prepararse ......................................................................................................................................571
Cómo hacerlo… ............................................................................................................................571
Hay más… .......................................................................................................................................572
Cómo trabajar con diferentes sistemas de calendario. ..................................................................572
Prepararse ......................................................................................................................................572
Cómo hacerlo… ............................................................................................................................572
Cómo funciona… ............................................................................................................................573
Hay más… .......................................................................................................................................574
Cómo formatear fechas usando DateTimeFormatter ................................................................575
Prepararse ......................................................................................................................................575
Cómo hacerlo… ..............................................................................................................................575
Cómo funciona… ............................................................................................................................576
Pruebas .........................................................................................................................................577
Introducción ...................................................................................................................................577
Pruebas de comportamiento con cucumber ...............................................................................578
Cómo hacerlo... ..............................................................................................................................579
Cómo funciona... ............................................................................................................................586
Prueba unitaria de una API utilizando JUnit ...................................................................................587
Prepararse ......................................................................................................................................587
Cómo hacerlo... ..............................................................................................................................588
Cómo funciona... ............................................................................................................................592
Pruebas unitarias burlándose de dependencias .............................................................................594
Prepararse ......................................................................................................................................594
Cómo hacerlo... ..............................................................................................................................595
Cómo funciona... ............................................................................................................................601
Hay más... .......................................................................................................................................602
Uso de accesorios para llenar datos para pruebas .........................................................................604
Cómo hacerlo... ..............................................................................................................................604
Cómo funciona... ............................................................................................................................605
Pruebas de integración ..................................................................................................................608
Prepararse ......................................................................................................................................609
Cómo hacerlo... ..............................................................................................................................610
Cómo funciona... ............................................................................................................................613
La nueva forma de codificación con Java 10 y Java 11 .............................................................618
Introducción ...................................................................................................................................618
Uso de inferencia de tipo variable local .........................................................................................618
Prepararse ......................................................................................................................................618
Cómo hacerlo... .............................................................................................................................620
Uso de sintaxis de variable local para parámetros lambda ............................................................621
Prepararse ......................................................................................................................................622
Cómo hacerlo... .............................................................................................................................622
Programación de GUI usando JavaFX .......................................................................................624
Introducción ...................................................................................................................................624
Crear una GUI usando controles JavaFX .........................................................................................625
Prepararse ......................................................................................................................................626
Cómo hacerlo... ..............................................................................................................................626
Cómo funciona... ............................................................................................................................630
Usando el marcado FXML para crear una GUI ................................................................................634
Prepararse ......................................................................................................................................635
Cómo hacerlo... ..............................................................................................................................635
Cómo funciona... ............................................................................................................................639
Usando CSS para los elementos de estilo en JavaFX ......................................................................640
Prepararse ......................................................................................................................................642
Cómo hacerlo... ..............................................................................................................................642
Cómo funciona... ............................................................................................................................646
Crear un gráfico de barras ..............................................................................................................648
Prepararse ......................................................................................................................................649
Cómo hacerlo... .............................................................................................................................651
Cómo funciona..............................................................................................................................654
Crear un gráfico circular .................................................................................................................657
Prepararse ......................................................................................................................................657
Cómo hacerlo... .............................................................................................................................658
Cómo funciona..............................................................................................................................660
Incrustar HTML en una aplicación ............................................................................................661
Prepararse ......................................................................................................................................662
Cómo hacerlo... ..............................................................................................................................662
Cómo funciona..............................................................................................................................666
Hay más... .....................................................................................................................................669
Incrustar medios en una aplicación ................................................................................................669
Prepararse ......................................................................................................................................669
Cómo hacerlo... .............................................................................................................................670
Cómo funciona..............................................................................................................................672
Hay más... .......................................................................................................................................673
Agregar efectos a los controles ......................................................................................................673
Prepararse ......................................................................................................................................674
Cómo hacerlo... .............................................................................................................................674
Cómo funciona..............................................................................................................................676
Hay más... .....................................................................................................................................677
Usando la API Robot .......................................................................................................................677
Prepararse ......................................................................................................................................678
Cómo hacerlo... .............................................................................................................................678
Cómo funciona..............................................................................................................................682
Referencias....................................................................................................................................684
[1] Java Projects – Second Edition ..............................................................................................684
Referencias... ...................................................................................... ¡Error! Marcador no definido.
Para quien es este libro
La audiencia prevista incluye principiantes, programadores con experiencia intermedia
e incluso expertos; todos podrán acceder a estas recetas, que demuestran las últimas
funciones lanzadas con Java 11.

Lo que cubre este libro


El Capítulo 1, Instalación y un adelanto de Java 11 , le ayuda a configurar el entorno
de desarrollo para ejecutar sus programas Java y ofrece una breve descripción de las
nuevas funciones y herramientas en Java 11.

El Capítulo 2 , Fast Track to OOP - Classes and Interfaces , cubre los principios de
programación orientada a objetos (OOP) y las soluciones de diseño, incluidas las
clases internas, la herencia, la composición, las interfaces, las enumeraciones y los
cambios de Java 9 a Javadocs.

El Capítulo 3 , Programación modular , presenta Jigsaw como una característica


importante y un gran salto para el ecosistema de Java. Este capítulo muestra cómo
usar herramientas, como jdeps y jlink, para crear aplicaciones modulares simples y
artefactos relacionados, como JAR modulares, y finalmente, cómo modularizar sus
aplicaciones previas a Jigsaw.

El Capítulo 4 , Pasando a ser funcional , presenta un paradigma de programación


llamado programación funcional. Los temas cubiertos incluyen interfaces
funcionales, expresiones lambda y API compatibles con lambda.

El Capítulo 5 , Flujos y tuberías , muestra cómo aprovechar los flujos y encadenar


múltiples operaciones en una colección para crear una tubería, usar métodos de
fábrica para crear objetos de colección, crear y operar en flujos y crear una tubería
de operación en
flujos, incluidos los cálculos paralelos.

El Capítulo 6, Programación de bases de datos , cubre las interacciones básicas y


de uso común entre una aplicación Java y una base de datos, desde la conexión a la
base de datos y la realización de operaciones CRUD hasta la creación de
transacciones, el almacenamiento de procedimientos y el trabajo
con objetos grandes.

Capítulo 7 , La programación concurrente y multiproceso presenta diferentes


formas de incorporar la concurrencia y algunas mejores prácticas, como la
sincronización y la inmutabilidad. El capítulo también analiza la implementación de
algunos patrones de uso común, como dividir-conquistar y publicar-suscribir,
utilizando las construcciones proporcionadas por Java.

pág. 1
El Capítulo 8, Mejor gestión del proceso del sistema operativo , explica las nuevas
mejoras de la API con respecto a la API del proceso.

El Capítulo 9, Servicios web RESTful con Spring Boot , trata de crear servicios web
RESTful simples con Spring Boot, implementarlos en Heroku, acoplar aplicaciones
de servicio web RESTful basadas en Spring Boot y monitorear aplicaciones Spring
Boot con Prometheus.

El Capítulo 10, Redes , le muestra cómo usar diferentes bibliotecas API de cliente
HTTP; a saber, la nueva API de cliente HTTP incluida con el último JDK, el cliente
Apache HTTP y la API de cliente HTTP Unirest.

El Capítulo , Administración y depuración de memoria , explora la


11
administración de la memoria de una aplicación Java, incluida una introducción al
algoritmo de recolección de basura utilizado en Java 9 y algunas características
nuevas que ayudan en el diagnóstico avanzado de aplicaciones. También
mostraremos cómo administrar los recursos mediante el uso de la nueva
construcción de prueba con recursos y la nueva API de paso de pila.

El Capítulo 12, El ciclo Leer-Evaluar-Imprimir (REPL) Usando JShell , le muestra


cómo trabajar con la nueva herramienta REPL y JShell, que se proporciona como
parte del JDK.

El Capítulo 13 , Trabajo con nuevas API de fecha y hora , muestra cómo construir
instancias de fecha y hora independientes y dependientes de la zona horaria, cómo
crear un período basado en fecha y hora entre la instancia de fecha, cómo representar
el tiempo de época, cómo manipular y comparar instancias de fecha y hora, cómo
trabajar con diferentes sistemas de calendario y cómo formatear fechas
usando DateTimeFormatter.

El Capítulo 14 , Pruebas , explica cómo probar la unidad de sus API antes de que se
integren con otros componentes, incluidas las dependencias de copiado con algunos
datos ficticios y las dependencias simuladas. También le mostraremos cómo escribir
accesorios para llenar datos de prueba y luego cómo probar el comportamiento de
su aplicación integrando diferentes API y probándolas.

El Capítulo 15 , La nueva forma de codificación con Java 10 y Java 11 , muestra cómo


usar la inferencia de tipo de variable local y cuándo y cómo usar la sintaxis de
variable local para los parámetros lambda.

, Programación de GUI con JavaFX , explica cómo usar JavaFX para


El Capítulo 16
crear una GUI con FXML Markup y CSS. Demostrará la creación de un gráfico de
barras, un gráfico circular, un gráfico de líneas, un gráfico de área. También
mostrará cómo incrustar HTML en una aplicación y una fuente de medios y cómo
agregar efectos a los controles. Además, aprenderemos sobre la API Robot
recientemente lanzada en la actualización de OpenJFX 11.

pág. 2
Para aprovechar al máximo este libro
Para aprovechar al máximo este libro, se requiere cierto conocimiento de Java y la
capacidad de ejecutar programas Java. Además, ayuda tener su editor favorito o, mejor
aún, un IDE instalado y configurado para usar en las recetas. Debido a que el libro es
esencialmente una colección de recetas, y cada receta se basa en ejemplos específicos,
los beneficios del libro se perderán si el lector no ejecuta los ejemplos
proporcionados. Los lectores obtendrán aún más de este libro si reproducen cada
ejemplo que se proporciona en su IDE, lo ejecutan y comparan su resultado con el que
se muestra en el libro.

Descargue los archivos de código de


ejemplo
Puede descargar los archivos de código de ejemplo para este libro desde su cuenta
en www.packt.com . Si compró este libro en otro lugar, puede
visitar www.packt.com/support y registrarse para recibir los archivos directamente por
correo electrónico.

Puede descargar los archivos de código siguiendo estos pasos:

1. Inicie sesión o regístrese en www.packt.com .


2. Seleccione la pestaña SOPORTE .
3. Haga clic en Descargas de códigos y erratas .
4. Ingrese el nombre del libro en el cuadro de búsqueda y siga las instrucciones
en pantalla.

Una vez descargado el archivo, asegúrese de descomprimir o extraer la carpeta con


la última versión de:

• WinRAR / 7-Zip para Windows


• Zipeg / iZip / UnRarX para Mac
• 7-Zip / PeaZip para Linux

El paquete de códigos para el libro también está alojado en GitHub


en https://github.com/PacktPublishing/Java-11-Cookbook-Second-Edition . También
tenemos otros paquetes de códigos de nuestro rico catálogo de libros y videos
disponibles en https://github.com/PacktPublishing/ . ¡Míralos!

Convenciones utilizadas
pág. 3
Hay una serie de convenciones de texto utilizadas en este libro.

CodeInText:Indica palabras de código en texto, nombres de tablas de bases de datos,


nombres de carpetas, nombres de archivos, extensiones de archivos, nombres de
ruta, URL ficticias, entradas de usuarios y identificadores de Twitter. Aquí hay un
ejemplo: "Use el método allProcesses() en la interfaz ProcessHandle para obtener
una secuencia de los procesos actualmente activos"

Un bloque de código se establece de la siguiente manera:

public class Thing {


private int someInt;
public Thing(int i) { this.someInt = i; }
public int getSomeInt() { return someInt; }
public String getSomeStr() {
return Integer.toString(someInt); }
}

Cuando deseamos llamar su atención sobre una parte particular de un bloque de


código, las líneas o elementos relevantes están en negrita:

Object[] os = Stream.of(1,2,3).toArray();
Arrays.stream(os).forEach(System.out::print);
System.out.println();
String[] sts = Stream.of(1,2,3)
.map(i -> i.toString())
.toArray(String[]::new);
Arrays.stream(sts).forEach(System.out::print);

Cualquier entrada o salida de línea de comandos se escribe de la siguiente manera:

jshell> ZoneId.getAvailableZoneIds (). stream (). count ()


$ 16 ==> 599

Negrita : indica un nuevo término, una palabra importante o palabras que ve en

pantalla. Por ejemplo, las palabras en los menús o cuadros de diálogo aparecen en
el texto de esta manera. Aquí hay un ejemplo: "Haga clic derecho en Mi PC y luego
haga clic en Propiedades . Verá la información de su sistema " .

Las advertencias o notas importantes aparecen así.


Consejos y trucos aparecen así.

Secciones
En este libro, encontrará varios encabezados que aparecen con frecuencia
( Preparación , Cómo hacerlo ... , Cómo funciona ... , Hay más ... y Vea también ). Para dar

pág. 4
instrucciones claras sobre cómo completar una receta, use estas secciones de la
siguiente manera:

Prepararse
Esta sección le dice qué esperar en la receta y describe cómo configurar cualquier
software o cualquier configuración preliminar requerida para la receta.

Cómo hacerlo…
Esta sección contiene los pasos necesarios para seguir la receta.

Cómo funciona…
Esta sección generalmente consiste en una explicación detallada de lo que sucedió en
la sección anterior.

Hay más…
Esta sección consta de información adicional sobre la receta para que conozca mejor
la receta.

Ver también
Esta sección proporciona enlaces útiles a otra información útil para la receta.

Estar en contacto
Los comentarios de nuestros lectores es siempre bienvenido.

Comentarios generales : si tiene preguntas sobre cualquier aspecto de este libro,


mencione el título del libro en el tema de su mensaje y envíenos un correo electrónico
a [email protected].

Errata : Aunque hemos tomado todas las precauciones para garantizar la precisión
de nuestro contenido, ocurren errores. Si ha encontrado un error en este libro, le

pág. 5
agradeceríamos que nos lo informe. Visite www.packt.com/submit-errata , seleccione su
libro, haga clic en el enlace Formulario de envío de erratas e ingrese los detalles.

Piratería : Si encuentra copias ilegales de nuestros trabajos en cualquier forma en


Internet, le agradeceríamos que nos proporcionara la dirección de la ubicación o el
nombre del sitio web. Póngase en contacto con nosotros
en [email protected] un enlace al material.

Si está interesado en convertirse en autor : si hay un tema en el que tiene


experiencia y le interesa escribir o contribuir a un libro, visite autores.packtpub.com .

Comentarios
Por favor deja un comentario. Una vez que haya leído y usado este libro, ¿por qué no
dejar una reseña en el sitio donde lo compró? Los lectores potenciales pueden ver y
usar su opinión imparcial para tomar decisiones de compra, en Packt podemos
entender lo que piensa sobre nuestros productos, y nuestros autores pueden ver sus
comentarios sobre su libro. ¡Gracias!

Para obtener más información acerca de Packt, visite packt.com .

Instalación y un adelanto en Java 11


En este capítulo, cubriremos las siguientes recetas:

• Instalar JDK 18.9 en Windows y configurar la variable PATH


• Instalar JDK 18.9 en Linux (Ubuntu, x64) y configurar la variable PATH
• Compilar y ejecutar una aplicación Java
• Qué hay de nuevo en JDK 18.9
• Uso de uso compartido de datos de clase de aplicación

Introducción
Cada búsqueda para aprender un lenguaje de programación comienza con la
configuración del entorno para experimentar con nuestro aprendizaje. En sintonía con
esta filosofía, en este capítulo, le mostraremos cómo configurar su entorno de
desarrollo y luego ejecutar una aplicación modular simple para probar nuestra
instalación. Después de eso, le daremos una introducción a las nuevas características y
herramientas en JDK 18.9. Luego, compararemos JDK 9, 18.3 y 18.9. Terminaremos el
capítulo con una nueva característica introducida en JDK 18.3 que permite compartir
datos de clase de aplicación.

pág. 6
Instalar JDK 18.9 en Windows y
configurar la variable PATH
En esta receta, vamos a ver la instalación de JDK en Windows y cómo configurar
la variable PATH para poder acceder a los ejecutables de Java (como javac, java y jar)
desde cualquier lugar dentro de la consola de comandos.

Cómo hacerlo...
1. Visite http://jdk.java.net/11/ y acepte el acuerdo de licencia de los primeros
usuarios, que se ve así:

2. Después de aceptar la licencia, obtendrá una cuadrícula de los paquetes JDK


disponibles en función del sistema operativo y la arquitectura (32/64 bits). Haga
clic para descargar el ejecutable JDK relevante ( .exe) para su plataforma Windows.
3. Ejecute el JDK ( .exe) y siga las instrucciones en pantalla para instalar JDK
en su sistema.
4. Si ha elegido todos los valores predeterminados durante la instalación,
encontrará JDK instalado C:/Program Files/Java para 64 bits y C:/Program Files
(x86)/Javapara 32 bits.

Ahora que hemos terminado de instalar JDK, veamos cómo podemos establecer
la variable PATH.

Las herramientas proporcionadas con JDK, es decir javac, java, jconsole, y jlink,
están disponibles en el directorio bin de la instalación de JDK. Hay dos formas de
ejecutar estas herramientas desde el símbolo del sistema:

1. Navegue hasta el directorio donde están instaladas las herramientas y ejecútelas


de la siguiente manera:

cd "C: \ Archivos de programa \ Java \ jdk-11 \ bin"


javac -version

2. Exporte la ruta al directorio para que las herramientas estén disponibles


desde cualquier directorio en el símbolo del sistema. Para lograr esto, tenemos que
agregar la ruta a las herramientas JDK en la variable de entorno PATH. El símbolo del

pág. 7
sistema buscará la herramienta relevante en todas las ubicaciones declaradas en
la variable PATH de entorno.

Veamos cómo puede agregar el directorio bin JDK a la variable PATH:

1. Haga clic derecho en Mi PC y luego haga clic en Propiedades. Verá la


información de su sistema. Busque la configuración avanzada del sistema y
haga clic en ella para obtener una ventana, como se muestra en la siguiente
captura de pantalla:

2. Haga clic en Variables de entorno para ver las variables definidas en su


sistema. Verá que hay bastantes variables de entorno ya definidas, como se muestra
en la siguiente captura de pantalla (las variables diferirán entre los sistemas; en la
siguiente captura de pantalla, hay algunas variables predefinidas y algunas
agregadas por mí):

pág. 8
Las variables definidas en Variables del sistema están disponibles para todos los
usuarios del sistema, y las definidas en Variables de usuario para <nombre de
usuario> están disponibles solo para el usuario específico.

3. Una nueva variable, con el nombre JAVA_HOME y su valor como ubicación de la


instalación de JDK 9. Por ejemplo, sería C:\Program Files\Java\jdk-11 (para 64 bits)
o C:\Program Files (x86)\Java\jdk-11 (para 32 bits):

pág. 9
4. Actualice la variable de entorno PATH con la ubicación del directorio bin de su
instalación de JDK (definida en la variable de entorno JAVA_HOME). Si ya ve la variable
PATH definida en la lista, debe seleccionar esa variable y hacer clic en Editar . Si PATH
no se ve la variable, haga clic en Nuevo.

5. Cualquiera de las acciones en el paso anterior le dará una ventana


emergente, como se muestra en la siguiente captura de pantalla (en Windows 10):

La siguiente captura de pantalla muestra las otras versiones de Windows:

pág. 10
6. Puede hacer clic en Nuevo en la primera captura de pantalla e insertar
el valor %JAVA_HOME%\bin , o puede agregar el valor al campo Valor de
variable agregando ; %JAVA_HOME%\bin. El punto y coma ( ;) en Windows se usa para
separar múltiples valores para un nombre de variable dado.

7. Después de establecer los valores, abra el símbolo del sistema y ejecútelo javac
-version. Debería poder ver javac 11-ea como salida. Si no lo ve, significa que el
directorio bin de su instalación JDK no se ha agregado correctamente a
la PATHvariable.

Instalar JDK 18.9 en Linux


(Ubuntu, x64) y configurar la
variable PATH
En esta receta, vamos a ver la instalación de JDK en Linux (Ubuntu, 64), y cómo
configurar la variable PATH para hacer que las herramientas del JDK
(como javac, javay jar) disponible desde cualquier ubicación dentro de la terminal.

Cómo hacerlo...
1. Siga los pasos 1 y 2 de Instalación de JDK 18.9 en Windows y configure
la receta variable PATH para llegar a la página de descargas.
2. Copie el enlace de descarga ( tar.gz) para el JDK para la plataforma Linux x64 desde
la página de descargas.
3. Descargar el JDK utilizando $> wget <copied link>, por ejemplo, $> wget
https://download.java.net/java/early_access/jdk11/26/BCL/jdk-11-
ea+26_linux-x64_bin.tar.gz.
4. Una vez finalizada la descarga, usted debe tener el JDK pertinente disponible, por
ejemplo, jdk-11-ea+26_linux-x64_bin.tar.gz. Puede enumerar los contenidos
utilizando $> tar -tf jdk-11-ea+26_linux-x64_bin.tar.gz. Incluso puede

pág. 11
canalizarlo a morepaginar la salida: $> tar -tf jdk-11-ea+26_linux-x64_bin.tar.gz
| more.
5. Extraiga el contenido del archivo tar.gz debajo /usr/lib mediante $> tar -xvzf
jdk-11-ea+26_linux-x64_bin.tar.gz -C /usr/lib. Esto extraerá el contenido en un
directorio /usr/lib/jdk-11,. Luego puede enumerar el contenido de JDK 11
utilizando $> ls /usr/lib/jdk-11.
6. Actualice las variables JAVA_HOMEy PATH editando el archivo .bash_aliases en su
directorio de inicio de Linux:

$> vim ~ / .bash_aliases


export JAVA_HOME = / usr / lib / jdk-11
export PATH = $ PATH: $ JAVA_HOME / bin

Fuente del archivo .bashr c para aplicar los nuevos alias:

$> source ~ / .bashrc


$> echo $ JAVA_HOME
/ usr / lib / jdk-11
$> javac -version
javac 11-ea
$> java -version
Java versión "11-ea" 2018-09-25
Java (TM) SE Runtime Environment 18.9 (compilación 11-ea + 22)
Java HotSpot (TM) 64-Bit Server VM 18.9 (compilación 11-ea + 22,
modo mixto )

Todos los ejemplos en este libro se ejecutan contra JDK instalado en Linux (Ubuntu, x64),
excepto en los lugares donde hemos mencionado específicamente que se ejecutan en
Windows. Hemos tratado de proporcionar scripts de ejecución para ambas plataformas.

Instalar Eclipse
1. Ir al siguiente link: https://www.eclipse.org/ y descargar eclipse.

pág. 12
pág. 13
Descomprimir la carpeta ZIP y dar doble clic en el archivo ejecutable de eclipse.

pág. 14
Elegir la ubicación donde se desean guardar los proyectos y dar clic en “Launch”.

Se carga eclipse

pág. 15
Entorno gráfico de Eclipse:

Compilar y ejecutar una


aplicación Java
En esta receta, escribiremos un programa modular Hello world muy simple para
probar nuestra instalación JDK. Este simple ejemplo se imprime Hello world en
XML; después de todo, es el mundo de los servicios web.

pág. 16
Prepararse
Debe tener JDK instalado y la variable PATH actualizada para apuntar a la instalación de
JDK.

Cómo hacerlo...
1. Definamos el objeto modelo con las propiedades y anotaciones relevantes que se
serializarán en XML:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
class Messages{
@XmlElement
public final String message = "Hello World in XML";
}

En el código anterior, @XmlRootElement se usa para definir la etiqueta


raíz, @XmlAccessorType se usa para definir el tipo de fuente para el nombre y los valores
de la etiqueta, y @XmlElement se usa para identificar las fuentes que se convierten en el
nombre y los valores de la etiqueta en el XML.

2. Vamos a serializar una instancia de la clase Message en XML usando JAXB:

public class HelloWorldXml{


public static void main(String[] args) throws JAXBException{
JAXBContext jaxb = JAXBContext.newInstance(Messages.class);
Marshaller marshaller = jaxb.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FRAGMENT,Boolean.TRUE);
StringWriter writer = new StringWriter();
marshaller.marshal(new Messages(), writer);
System.out.println(writer.toString());
}
}

3. Ahora crearemos un módulo llamado com.packt. Para crear un módulo, necesitamos


crear un archivo llamado module-info.java, que contenga la definición del módulo. La
definición del módulo contiene las dependencias del módulo y los paquetes exportados por
el módulo a otros módulos:

module com.packt{
//depends on the java.xml.bind module
requires java.xml.bind;
//need this for Messages class to be available to java.xml.bind
exports com.packt to java.xml.bind;
}

pág. 17
Explicaremos los módulos en detalle en Capítulo 3 , Programación modular . Pero este
ejemplo es solo para darle una idea de la programación modular y para probar su
instalación de JDK.

La estructura del directorio con los archivos anteriores es la siguiente:

4. Vamos a compilar y ejecutar el código. Desde el directorio hellowordxml, cree un


nuevo directorio en el que colocar sus archivos de clase compilados:

mkdir -p mods / com.packt

Compile la fuente HelloWorldXml.javay module-info.java, en


el mods/com.packt directorio:

javac -d mods / com.packt / src / com.packt / module-info.java


src / com.packt / com / packt / HelloWorldXml.java

5. Ejecute el código compilado usando java --module-path mods -m


com.packt/com.packt.HelloWorldXml. Verá el siguiente resultado:

<messages> <message> Hola mundo en XML </message> </messages>

No se preocupe si no puede entender las opciones pasadas con


los comandos javao javac. Aprenderá sobre ellos en el Capítulo 3 , Programación
modular .

PARA WINDOWS:

Presione el botón “ejecutar” en la barra de herramientas:

pág. 18
¿Qué hay de nuevo en Java 11?
El lanzamiento de Java 9 fue un hito en el ecosistema de Java. El marco modular
desarrollado bajo Project Jigsaw se convirtió en parte del lanzamiento de Java SE. Otra
característica importante fue la herramienta JShell, que es una herramienta REPL para
Java. Muchas otras características nuevas introducidas con Java 9 se enumeran en las
notas de la versión: http://www.oracle.com/technetwork/java/javase/9all-relnotes-3704433.html .

En esta receta, enumeraremos y discutiremos algunas de las nuevas características


introducidas con JDK 18.3 y 18.9 (Java 10 y 11).

Prepararse
La versión Java 10 (JDK 18.3) inició un ciclo de lanzamiento de seis meses, cada marzo
y cada septiembre, y un nuevo sistema de numeración de lanzamiento. También
introdujo muchas características nuevas, las más importantes (para los desarrolladores
de aplicaciones) son las siguientes:

• Inferencia de tipo de variable local que permite la declaración de una variable


utilizando el tipo reservado var (consulte el Capítulo 15 , La nueva forma de
codificación con Java 10 y Java 11 )
• Recolección de basura completa paralela para el recolector de basura G1, que mejora
las peores latencias
• Un nuevo método, Optional.orElseThrow()que ahora es la alternativa preferida
al get()método existente
• Las nuevas API para crear colecciones no modificables: Los
métodos List.copyOf(), Set.copyOf()y Map.copyOf()del paquete java.util y los
nuevos métodos de
la clase java.util.stream.Collectors : toUnmodifiableList(), toUnmodifiableSet
(), y toUnmodifiableMap() (ver Capítulo 5 , arroyos y Tuberías )
• Un conjunto predeterminado de autoridades de certificación raíz, lo que hace que las
compilaciones de OpenJDK sean más atractivas para los desarrolladores
• Una nueva opción de línea de comandos Javadoc --add-stylesheet, proporciona
soporte para el uso de múltiples hojas de estilo en la documentación generada
• Extender la función de intercambio de datos de clase existente para permitir que las
clases de aplicación se coloquen en el archivo compartido que mejora el tiempo de
inicio y reduce la huella (consulte la receta Uso de uso compartido de datos de
clase de aplicación )

• Se puede utilizar un compilador experimental justo a tiempo, Graal, en la


plataforma Linux / x64

pág. 19
• Una interfaz limpia de recolector de basura (GC) que simplifica la adición de un
nuevo GC a HotSpot sin perturbar la base del código actual y hace que sea más fácil
excluir un GC de una compilación JDK
• Habilitación de HotSpot para asignar el montón de objetos en un dispositivo de
memoria alternativo, como un módulo de memoria NVDIMM, especificado por el
usuario
• Apretones de manos locales para subprocesos, para ejecutar una devolución de
llamada en subprocesos sin realizar un punto seguro global de VM
• Conocimiento de Docker: JVM sabrá si se está ejecutando en un contenedor Docker
en un sistema Linux y puede extraer información de configuración específica del
contenedor en lugar de consultar el sistema operativo
• Tres nuevas opciones de JVM, para dar a los usuarios de contenedores Docker un
mayor control sobre la memoria del sistema

Consulte la lista completa de las nuevas características de Java 10 en las notas de la


versión: https://www.oracle.com/technetwork/java/javase/10-relnote-issues-
4108729.html .

Discutiremos las nuevas características de JDK 18.9 con más detalle en la siguiente
sección.

Cómo hacerlo...
Hemos elegido algunas características que consideramos las más importantes y útiles
para un desarrollador de aplicaciones.

Bloques
El código en Java se crea en bloques de código. Cualquier cosa que esté entre
los caracteres {y }es un bloque. En el ejemplo anterior, el código del método es un
bloque. Contiene comandos, y algunos de ellos, como el bucle while, también contienen
un bloque. Dentro de ese bloque, hay dos comandos. Uno de ellos es un bucle for,
nuevamente con un bloque. Aunque podemos tener expresiones individuales para
formar el cuerpo de un bucle, generalmente usamos bloques. Discutiremos los bucles
en detalle en unas pocas páginas.

Como pudimos ver en el ejemplo anterior, los bucles se pueden anidar y, por lo tanto,
los caracteres {y }forman pares. Un bloque puede estar dentro de otro bloque, pero dos
bloques no pueden solaparse. Cuando el código contiene un }carácter, está cerrando el
bloque que se abrió por última vez.[1]

Variables
pág. 20
En Java, al igual que en casi cualquier lenguaje de programación, utilizamos
variables. Las variables en Java están escritas. Esto significa que una variable puede
contener un valor de un solo tipo. No es posible que una variable contenga un tipo int
en algún punto del programa y luego un tipo String. Cuando se declaran las variables,
su tipo se escribe delante del nombre de la variable. Cuando una variable local obtiene
el valor inicial en la línea donde se declara, es posible utilizar un tipo reservado especial
llamado var. Significa el tipo que es exactamente el mismo que el tipo de la expresión
en el lado derecho del operador de asignación.

Así es como se ve el código:

var n = nombres.length;

También se puede escribir de la siguiente manera:

int n = nombres.length;

Esto se debe a que la expresión names.length tiene un tipo int. Esta característica se
denomina inferencia de tipo de variable local, ya que el tipo se infiere del lado
derecho. Esto no se puede usar en caso de que la variable no sea local para un método.

Cuando declaramos un campo (una variable que está en el nivel de clase fuera del
cuerpo de los métodos de la clase y no dentro de un bloque inicializador o constructor)
tenemos que especificar el tipo exacto que queremos que sea la variable.

Las variables también tienen alcance de visibilidad. Las variables locales en los métodos
solo pueden usarse dentro del bloque en el que están definidas. Se puede usar una
variable dentro de los métodos o pueden pertenecer a una clase o un objeto. Para
diferenciar los dos, generalmente llamamos a estos campos variables. [1]

Tipos
Cada variable tiene un tipo. En Java, hay dos grupos principales de tipos: tipos
primitivos y de referencia. Los tipos primitivos están predefinidos y no puede definir
o crear un nuevo tipo primitivo. Hay ocho tipos-
primitivo byte, short, int, long, float, double, boolean, y char.

Los primeros cuatro tipos, byte, short, int, y long son firmados tipos enteros numéricos,
capaces de almacenar números positivos y negativos en 8, 16, 32, y 64 bits.

Los tipos float y double almacenan números de coma flotante en 32 y 64 bits en el


formato de coma flotante IEEE 754.

El tipo boolean es un tipo primitivo que solo puede ser true o false.

pág. 21
El tipo char es un tipo de datos de caracteres que almacena un solo carácter Unicode de
16 bits.

Para cada tipo primitivo, hay una clase correspondiente. Una instancia de la clase puede
almacenar el mismo tipo de valor. Cuando un tipo primitivo tiene que convertirse al
tipo de clase coincidente, se hace automáticamente. Se llama auto-boxeo. Estos tipos
son Byte, Short, Integer, Long, Float, Double, Boolean, y Character. Tomemos, por
ejemplo, la siguiente declaración de variable:

Integer a = 113;

Esto convierte el valor 113, que es un número int, en un objeto Integer.

Estos tipos son parte del tiempo de ejecución y también del lenguaje.

Hay una clase especial, llamada String. Un objeto de este tipo contiene
caracteres. String no tiene una contraparte primitiva, pero la usamos muchas veces
como si fuera un tipo primitivo, que no lo es. Es omnipresente en los programas Java y
hay algunas construcciones de lenguaje, como la concatenación de cadenas que
funcionan directamente con este tipo.

Las principales diferencias entre los tipos primitivos y los objetos son que los tipos
primitivos no se pueden usar para invocar métodos en ellos. Son solo valores. No se
pueden usar como bloqueo cuando creamos un programa concurrente. Por otro lado,
consumen menos memoria. Esta diferencia entre el consumo de memoria y sus
consecuencias para la velocidad es importante, especialmente cuando tenemos una
variedad de valores. [1]

Matrices
Las variables pueden ser un tipo primitivo según su declaración, o pueden contener una
referencia a un objeto. Un tipo de objeto especial es una matriz. Cuando una variable
contiene una referencia a una matriz, se puede indexar con los caracteres [y ], junto con
un valor integral que consiste en 0 o un valor positivo que varía a uno menos que la
longitud de la matriz, para acceder a un determinado elemento de la matriz. Las
matrices multidimensionales también son compatibles con Java cuando una matriz
tiene elementos que también son matrices. Las matrices se indexan desde cero en
Java. La indexación insuficiente o excesiva se verifica en tiempo de ejecución y el
resultado es una excepción.

Una excepción es una condición especial que interrumpe el flujo de ejecución normal y detiene la
ejecución del código o salta a la declaración catch adjunta más cercana . Discutiremos las
excepciones y cómo manejarlas en el próximo capítulo.

pág. 22
Cuando un código tiene una matriz de un tipo primitivo, la matriz contiene ranuras de
memoria, cada una con el valor del tipo. Cuando la matriz tiene un tipo de referencia,
en otras palabras, cuando se trata de una matriz de objetos, los elementos de la matriz
son referencias a objetos, cada uno de los cuales se refiere a una instancia del tipo. En
el caso de int, por ejemplo, cada elemento de la matriz es de 32 bits, que es de 4 bytes. Si
la matriz es un tipo de Integer, entonces los elementos son referencias a objetos,
punteros, por ejemplo, que generalmente es de 64 bits utilizando JVM de 64 bits y 32
bits en JVM de 32 bits. Además de eso, hay un objeto Integer en algún lugar de la
memoria que contiene el valor de 4 bytes y también un encabezado de objeto que puede
tener hasta 24 bytes.

El tamaño real de la información adicional necesaria para administrar cada objeto no está
definido en el estándar. Puede ser diferente en diferentes implementaciones de la JVM. La
codificación real, o incluso la optimización del código en un entorno, no debe depender del tamaño
real. Sin embargo, los desarrolladores deben tener en cuenta que esta sobrecarga existe y está en
el rango de alrededor de 20 bytes para cada objeto. Los objetos son caros en términos de consumo
de memoria.

El consumo de memoria es un problema, pero hay algo más. Cuando el programa


trabaja con una gran cantidad de datos y el trabajo necesita los elementos consecutivos
de la matriz, la CPU carga una gran cantidad de memoria en el caché del
procesador. Significa que la CPU puede acceder a elementos de la matriz que son
consecutivamente más rápidos. Si la matriz es de tipo primitivo, es rápida. Si la matriz
es de algún tipo de clase, entonces la CPU puede necesitar acceder a la memoria para
obtener el valor real de un elemento de la matriz a través de la referencia contenida en
la matriz. Esto puede ser hasta 50 veces más lento. [1]

Expresiones
Las expresiones en Java son muy similares a las de otros lenguajes de
programación. Puede usar los operadores que pueden ser similares a lenguajes como C
o C ++. Son los siguientes:

• Operadores de incremento de prefijo unario y postfijo ( --y ++antes y después de una


variable)
• Operadores de signo unario ( +y -)
• Negación lógica ( !) y bit a bit ( ~)
• Multiplicación ( *), división ( /) y módulo ( %)
• Suma y resta ( +y de -nuevo, pero esta vez como operadores binarios)
• Los operadores de desplazamiento mueven los valores bit a bit, y hay desplazamiento
a la izquierda ( <<), >>desplazamiento a la derecha ( ) y desplazamiento a la derecha
sin signo ( >>>)
• Los operadores que comparan son <, >, <=, >=, ==, !=, y instanceof ese resultado en
el valor boolean

pág. 23
• Hay operadores a nivel de bit o ( |), y ( &), exclusivos o ( ^), y operadores igualmente
lógicos o ( ||) y ( &&)

Cuando se evalúan los operadores lógicos, se evalúan los accesos directos. Esto significa
que el operando de la derecha se evalúa solo si el resultado no puede identificarse a
partir del resultado del operando de la izquierda.

El operador ternario también es similar al uno, como en C, seleccionando una de las


expresiones en función de alguna condición— condition ? expression 1 : expression
2. Por lo general, no hay ningún problema con el operador ternario, pero a veces hay
que tener cuidado ya que existe una regla compleja que controla las conversiones de
tipos en caso de que las dos expresiones no sean del mismo tipo. Siempre es mejor tener
dos expresiones del mismo tipo.

Finalmente, hay un operador de asignación ( =) que asigna el valor de una expresión a


una variable. Para cada operador binario, hay una versión de asignación que se
combina =con un operador binario para realizar una operación que involucra el
operando derecho y asigna el resultado al operando izquierdo, que debe ser una
variable. Estos son +=, -=, *=, /=, %=, &=, ^=, |=, <<=, >>=, y >>>=.

Los operadores tienen prioridad y pueden anularse entre paréntesis, como de


costumbre.

Una parte importante de las expresiones es invocar métodos. Los métodos estáticos se
pueden invocar por el nombre de la clase y el nombre del método separado por
puntos. Por ejemplo, para calcular el seno de 1.22, podemos escribir la siguiente línea
de código:

double z = Math.sin(1.22);

Aquí Math está la clase del paquete java.lang. El método sin se invoca sin usar una
instancia de Math. Este método es static y no es probable que alguna vez necesitemos
otra implementación que no sea la que se proporciona en la clase Math.

Los métodos no estáticos pueden invocarse utilizando una instancia y el nombre del
método con un punto que separa los dos. Por ejemplo, considere el siguiente código:

System.out.println("Hello World");

El código utiliza una instancia de la PrintStream clase fácilmente disponible a través de


un campo estático en la System clase. Esta variable se llama out, y cuando escribimos
nuestro código, tenemos que hacer referencia a él como System.out. El println se
define en la clase método PrintStream y lo invocamos en el objeto al que hace
referencia la out variable. Este ejemplo también muestra que los campos estáticos
también pueden ser referenciados a través del nombre de la clase y el campo separado

pág. 24
por un punto. Del mismo modo, cuando necesitamos hacer referencia a un campo no
estático, podemos hacerlo a través de una instancia de la clase.

Los métodos estáticos definidos en la misma clase desde donde se invoca o hereda se
pueden invocar sin el nombre de la clase. La invocación de un método no estático
definido en la misma clase o la herencia se puede invocar sin una notación de instancia
explícita. En este caso, la instancia es el objeto actual en el que se encuentra la ejecución.
Este objeto también está disponible a través de la palabra clave this. De manera similar,
cuando usamos un campo de la misma clase donde está nuestro código, simplemente
usamos el nombre. En el caso de un campo estático, la clase en la que estamos es la
predeterminada. En el caso de un campo no estático, la instancia es el objeto al que hace
referencia la palabra clave this.

También puede importar un método estático en su código utilizando la función import


static de idioma, en cuyo caso puede invocar el método sin el nombre de la clase.

Los argumentos de las llamadas al método se separan mediante comas. Los métodos y
el paso de argumentos de método es un tema importante que cubriremos más adelante.
[1]

Bucles
Una vez más, echemos un vistazo al código del tipo de cadena. El bucle for dentro
del bucle while pasará por todos los elementos desde el primer elemento (indexado
con cero en Java) hasta el último (indexado con n-1). En general, este bucle for tiene la
misma sintaxis que en C:

for( initial expression ; condition ; increment expression )


block

Primero, se evalúa la expresión inicial. Puede contener una declaración variable, como
en nuestro ejemplo. La j variable en el ejemplo anterior solo es visible dentro del
bloque del bucle. Después de esto, se evalúa la condición y, después de cada ejecución
del bloque, se ejecuta la expresión de incremento. El bucle se repite siempre que la
condición sea verdadera. Si la condición es falsa justo después de la ejecución de la
expresión inicial, el bucle no se ejecuta en absoluto. El bloque es una lista de comandos
separados por punto y coma y encerrados entre los caracteres {y }.

En lugar de {y }, el bloque cerrado Java le permite usar un solo comando siguiendo el encabezado
del forbucle. Lo mismo es cierto en el caso del whilebucle, y también para
las if...elseconstrucciones. La práctica muestra que esto no es algo que un profesional deba
usar. El código profesional siempre usa llaves, incluso cuando solo hay un comando donde el
bloque está en su lugar. Esto evita el elseproblema de colgar y generalmente hace que el código

pág. 25
sea más legible. Esto es similar a muchos lenguajes tipo C. La mayoría de ellos permiten un solo
comando en estos lugares, y los programadores profesionales evitan usar un solo comando en
estos idiomas con fines de legibilidad. Es irónico que el único lenguaje que requiere estrictamente
el uso de {y} las llaves en estos lugares es Perl, el único idioma infame para el código ilegible.

El bucle en la for (var j = 0; j < n - 1; j++) {muestra comienza desde cero y va a n-


2. Escribir j < n-1es lo mismo, en este caso, como j <= n-2. Limitaremos jpara
detenernos en el ciclo antes del final de la matriz, porque llegamos más allá del
índice jcomparando e intercambiando condicionalmente los elementos indexados
por jy j+1. Si avanzáramos un elemento más, trataríamos de acceder a un elemento de
la matriz que no existe, y causaría una excepción en tiempo de ejecución. Tratar de
modificar la condición del bucle a j < no j <= n-1y obtendrá el mensaje de error
siguiente:

pág. 26
Es una característica importante de Java que el tiempo de ejecución verifica el acceso a
la memoria y genera una excepción en el caso de una indexación de matriz
incorrecta. En los viejos tiempos, al codificar en C, a menudo, nos enfrentábamos a
errores inexplicables que detenían nuestro código mucho más tarde y en ubicaciones
de código totalmente diferentes de donde estaba el error real. El índice de matriz en C
corrompió silenciosamente la memoria. Java lo detiene tan pronto como comete un
error. Sigue el enfoque de falla rápida que también debe usar en su código. Si algo está
mal, el programa debería fallar. Ningún código debe tratar de vivir o superar un error

pág. 27
que proviene de un error de codificación. Los errores de codificación deben repararse
antes de que causen aún más daños.

También hay dos construcciones de bucle más en Java: el bucle while y el bucle do. El
siguiente ejemplo contiene un bucle while. Es el bucle externo que se ejecuta siempre
que haya al menos dos elementos que puedan necesitar intercambiarse en la matriz:

while (n > 1) {

La sintaxis general y la semántica del whilebucle es muy simple, como se muestra en el


siguiente código:

while ( condition ) block

Repita la ejecución del bloque siempre que la condición sea true. Si la condición no es
verdadera al comienzo del ciclo, no ejecute el bloque en absoluto. El ciclo do también es
similar, pero verifica la condición después de cada ejecución del bloque:

do block while( condition );

Por alguna razón, los programadores rara vez usan bucles do.

Ejecución condicional
El corazón del género es la condición y el intercambio de valores dentro del bucle.

if (names[j].compareTo(names[j + 1]) > 0) {


final String tmp = names[j + 1];
names[j + 1] = names[j];
names[j] = tmp;
}

Solo hay un comando condicional en Java, el comando if. Tiene el siguiente formato:

if( condition ) block else block

El significado de la estructura del código es bastante sencillo. Si la condición es true,


entonces se ejecuta el primer bloque, de lo contrario, se ejecuta el segundo
bloque. La palabra clave else, junto con el segundo bloque, es opcional. Crear else y un
bloque después es opcional. Si no hay nada que ejecutar cuando la condición es false,
entonces simplemente no creamos la parte else. Si el elemento de matriz indexado jes
posterior en el orden de clasificación que el elemento j+1, entonces los
intercambiamos; sin embargo, si ya están en orden, no hay nada que ver con ellos.

Para intercambiar los dos elementos de la matriz, utilizamos una variable temporal
llamada tmp. El tipo de esta variable es String, y esta variable se declara que

pág. 28
es final. La palabra clave final tiene diferentes significados dependiendo de dónde se
usa en Java. Esto puede ser confuso para los principiantes a menos que se le advierta al
respecto, al igual que ahora. Una finalclase o método es algo completamente diferente
a un campo final, que nuevamente es diferente de una variable local final.

Tenga en cuenta que esta vez utilizamos el tipo explícito String para declarar la
variable. Podríamos usar var y también en su final varlugar, se habría inferido el
mismo tipo. La única razón para usar la escritura explícita aquí es para fines de
demostración.[1]

Variables finales
En nuestro caso, tmpes una variable local final. El alcance de esta variable se limita al
bloque que sigue a la ifinstrucción, y dentro de este bloque, esta variable obtiene un
valor solo una vez. El bloque se ejecuta muchas veces durante la ejecución del código, y
cada vez que la variable entra en el alcance, obtiene un valor. Sin embargo, este valor
no se puede cambiar dentro del bloque y no existe fuera del bloque. Esto puede ser un
poco confuso. Puede pensar que tiene un nuevo tmp cada vez que se ejecuta el
bloque. La variable se declara; primero no está definido, y luego obtiene un valor una
vez.

Las variables locales finales no necesitan obtener el valor donde se declaran. Puede
asignar un valor a una variable final en algún momento posterior. Es importante que
no haya una ejecución de código que asigne un valor a una variable final a la que ya
se le había asignado un valor anteriormente. El compilador lo comprueba y no compila
el código si existe la posibilidad de reasignar una variable final. El compilador también
verifica que el valor de una variable local (no solo las variables final) no debe usarse
mientras la variable no está definida.

Por lo general, declarar una variable final es facilitar la legibilidad del código. Cuando
vea una variable en el código declarado final, puede suponer que el valor de la variable
no cambiará y el significado de la variable siempre será el mismo donde se utilizó en el
método. También lo ayudará a evitar algunos errores cuando intente modificar
algunas variables final y el IDE se quejará de inmediato. En tales situaciones, es
probable que sea un error de programación que se descubra extremadamente
temprano.

En principio, es posible escribir un programa donde estén todas las variables final. En
general, es una buena práctica declarar todas las variables final que se pueden
declarar como final y, en caso de que alguna variable no se declare final, intente
encontrar alguna forma de codificar el método de manera un poco diferente.

Si necesita introducir una nueva variable para hacer eso, probablemente significa que estaba
usando una variable para almacenar dos cosas diferentes. Estas cosas son del mismo tipo y se

pág. 29
almacenan en la misma variable en diferentes momentos pero, lógicamente, aún son cosas
diferentes. No intente optimizar el uso de variables. Nunca use una variable porque ya tiene una
variable del mismo tipo en su código que está disponible. Si lógicamente es algo diferente, declare
una nueva variable. Mientras codifica, siempre prefiere la claridad y legibilidad del código
fuente. En Java, especialmente, el compilador Just In Time optimizará todo esto para usted.

Aunque no tendemos explícitamente a usar la palabra clave final en la lista de


argumentos de un método, es una buena práctica asegurarse de que sus métodos
compilan y funcionan si se declaran los argumentos final. Algunos expertos, incluido
yo, creen que los parámetros del método deberían haberse finalizado por defecto en el
idioma. Esto es algo que no sucederá en ninguna versión de Java, siempre que Java siga
la filosofía de compatibilidad con versiones anteriores.[1]

Clases
Ahora que hemos examinado las líneas de código reales y hemos entendido cómo
funciona el algoritmo, veamos las estructuras más globales del código que lo une: clases
y paquetes que encierran los métodos.

Cada archivo en un programa Java define una clase. Cualquier código en un programa
Java está dentro de una clase. No hay nada como variables globales o funciones globales
como en C, Python, Go u otros lenguajes. Java está totalmente orientado a objetos.

Puede haber más de una clase en un solo archivo, pero generalmente, un archivo es una
clase. Más tarde, veremos que hay clases internas cuando una clase está dentro de otra
clase, pero, por ahora, colocaremos una clase en un archivo.

Hay algunas características en el lenguaje Java que no utilizamos. Cuando se creó el lenguaje,
estas características parecían ser una buena idea. La CPU, la memoria y otros recursos, incluidos
los desarrolladores mediocres, también eran más limitados que hoy. Algunas de las
características, tal vez, tenían más sentido debido a estas limitaciones ambientales. A veces,
mencionaré esto. En el caso de las clases, puede poner más de una clase en un solo archivo siempre
que solo una seapublic. Esa es una mala práctica, y nunca haremos eso. Java nunca pasa de moda
estas características. Era una filosofía de Java permanecer compatible con todas las versiones
anteriores hasta hace poco y esta filosofía cambia lentamente. Esto es bueno para la gran
cantidad de código heredado ya escrito. El código Java escrito y probado con una versión anterior
funcionará en un entorno más nuevo. Al mismo tiempo, estas características atraen a los
principiantes a un estilo incorrecto. Por esta razón, a veces, ni siquiera mencionaré estas
características. Por ejemplo, aquí, podría decir: hay una clase en un archivo. Esto no sería del todo
correcto. Al mismo tiempo, es más o menos inútil explicar con gran detalle una característica que
recomiendo no utilizar. Más tarde, simplemente puedo omitirlos. No hay muchas de estas
características.

pág. 30
Una clase se define usando la palabra clave class, y cada clase debe tener un nombre. El
nombre debe ser único dentro del paquete (consulte la siguiente sección) y debe ser el
mismo que el nombre del archivo. Una clase puede implementar una interfaz o extender
otra clase, para lo cual veremos un ejemplo más adelante. Una clase también puede
ser abstract, final y public. Estos se definen con las palabras clave apropiadas, como
verá en los siguientes ejemplos.

Nuestro programa tiene dos clases. Los dos lo son public. Las clases public son
accesibles desde cualquier lugar. Las clases que no publicson visibles solo están dentro
del paquete. Las clases internas y anidadas también private pueden ser visibles solo
dentro de la clase de nivel superior definida en el nivel de archivo.

Las clases que contienen un método main()para ser invocado por el entorno Java deben
ser public. Eso es porque son invocados por la JVM.

La clase comienza al principio del archivo justo después de la declaración del paquete
y todo entre los caracteres {y }pertenece a la clase. Los métodos, campos, clases
internas o anidadas, etc. son parte de la clase. En general, las llaves indican algún bloque
en Java. Esto fue inventado en el lenguaje C, y muchos lenguajes siguen esta notación. La
declaración de clase es un bloque, los métodos se definen usando un bloque, bucles y
comandos condicionales, todos usan bloques.

Cuando usemos las clases, tendremos que crear instancias de clases. Estas instancias
son objetos. En otras palabras, los objetos se crean creando instancias de una
clase. Para hacer eso, la newpalabra clave se usa en Java. Cuando la línea final Sort
sorter = new Sort();se ejecuta en la clase App, crea un nuevo objeto instanciando
la clase Sort. También diremos que creamos un nuevo objeto Sort o que el tipo de
objeto es Sort. Cuando se crea un nuevo objeto, se invoca un constructor del objeto. Un
poco descuidado, puedo decir, que el constructor es un método especial en la clase que
tiene el mismo nombre que la clase en sí y no tiene valor de retorno. Esto se debe a que
devuelve el objeto creado. Para ser precisos, los constructores no son métodos. Son
inicializadores y no devuelven el nuevo objeto. Trabajan en el objeto que aún no está
listo. Cuando un constructor que ejecuta el objeto no está completamente inicializado,
algunos de los campos finales pueden no inicializarse y la inicialización
general puede aún falla si el constructor arroja una excepción. En nuestro ejemplo, no
tenemos ningún constructor en el código. En tal caso, Java crea un constructor
predeterminado que no acepta argumentos y no modifica el objeto ya asignado pero no
inicializado. Si el código Java define un inicializador, el compilador de Java no crea uno
predeterminado.

Una clase puede tener muchos constructores, cada uno con una lista de parámetros
diferente.

Además de los constructores, las clases Java pueden contener bloques


inicializadores. Son bloques en el nivel de clase, el mismo nivel que el constructor y los

pág. 31
métodos. El código en estos bloques se compila en los constructores y se ejecuta cuando
el constructor se está ejecutando.

También es posible inicializar campos estáticos en bloques de inicializador


estático. Estos son los bloques en el nivel superior dentro de la clase con la palabra
clave static en frente de ellos. Se ejecutan solo una vez, es decir, cuando se carga la
clase.

Nombramos las clases en nuestro ejemplo App y Sort. Esta es una convención en el
ejemplo de Java App y Sort. Esta es una convención en Java donde debes nombrar casi
todo en CamelCase.

CamelCase es cuando las palabras se escriben sin espacios entre ellas. La primera palabra puede
comenzar con minúsculas o mayúsculas y, para denotar el inicio de la segunda palabra y las
siguientes, comienzan con mayúsculas. ForExampleThisIsALongCamelCase.

Los nombres de clase comienzan con una letra mayúscula. Este no es un requisito
formal del lenguaje, pero es una convención que todo programador debe seguir. Estas
convenciones de codificación lo ayudan a crear código que es más fácil de entender por
otros programadores y facilita el mantenimiento. Las herramientas de análisis de
código estático, como Checkstyle ( http://checkstyle.sourceforge.net/ ), también
verifican que los programadores sigan las convenciones. [1]

Clases internas, anidadas, locales


y anónimas.
Ya he mencionado las clases internas y anidadas en la sección anterior. Ahora, los
veremos con un poco más de detalle.

Los detalles de las clases internas y anidadas en este punto pueden ser difíciles. No se sienta
avergonzado si no comprende completamente esta sección. Si es demasiado difícil, pase a la
siguiente sección y lea sobre los paquetes y regrese aquí más tarde. Las clases anidadas, internas
y locales rara vez se usan, aunque tienen sus roles y su uso en Java. Las clases anónimas fueron
muy populares en la programación GUI con la interfaz de usuario Swing que permitía a los
desarrolladores crear aplicaciones Java GUI. Con Java 8 y la función lambda, las clases anónimas
no son tan importantes en estos días, y con la tecnología emergente de JavaScript y navegador, la
GUI de Java se volvió menos popular.

Cuando una clase se define en un archivo por sí sola, se llama clase de nivel superior. Las
clases que están dentro de otra clase, obviamente, no son clases de nivel superior. Si se
definen dentro de una clase en el mismo nivel que los campos (variables que no son
locales para algún método u otro bloque), son clases internas o anidadas. Hay dos

pág. 32
diferencias entre ellos. Una es que las clases anidadas tienen la palabra clave static
antes de la palabra clave class en su definición, y las clases internas no.

La otra diferencia es que pueden existir instancias de clases anidadas sin una instancia
de la clase circundante. Las instancias de clase interna siempre tienen una referencia a
una instancia de la clase circundante.

Debido a que las instancias de clase interna no pueden existir sin una instancia de la
clase circundante, su instancia solo puede crearse proporcionando una instancia de la
clase externa. No veremos ninguna diferencia si la instancia de la clase circundante es
la variable real this, pero si queremos crear una instancia de una clase interna desde
fuera de la clase circundante, entonces tenemos que proporcionar una variable de
instancia antes de la palabra clave new separada por un punto, solo como si nuevo fuera
un método. Por ejemplo, podríamos tener una clase llamada TopLevel que tenga una
clase llamada InnerClass, como se muestra en el siguiente fragmento de código:

public class TopLevel {

class InnerClass { }
}

Luego, podemos crear una instancia de InnerClass afuera con solo un objeto TopLevel,
como se muestra en el siguiente fragmento de código:

TopLevel tl = new TopLevel();


InnerClass ic = tl.new InnerClass();

Como las clases internas no estáticas tienen una referencia implícita a una instancia de
la clase envolvente, el código dentro de la clase interna puede acceder a los campos y
los métodos de la clase envolvente.

Las clases anidadas no tienen una referencia implícita a una instancia de la clase
adjunta, y se pueden crear instancias con la palabra clave new sin ninguna referencia a
ninguna instancia de ninguna otra clase. Debido a eso, no pueden acceder a los campos
de la clase adjunta a menos que sean campos estáticos.

Las clases locales son clases que se definen dentro de un método, constructor o un
bloque inicializador. Pronto hablaremos sobre bloques inicializadores y
constructores. Las clases locales se pueden usar dentro del bloque donde se definen.

Las clases anónimas se definen e instancian en un solo comando. Son una forma corta
de una clase anidada, interna o local, y la instanciación de la clase. Las clases anónimas
siempre implementan una interfaz o extienden una clase con nombre. A la nueva
palabra clave le sigue el nombre de la interfaz o la clase con la lista de argumentos para
el constructor entre paréntesis. El bloque que define el cuerpo de la clase anónima se
encuentra inmediatamente después de la llamada del constructor. En el caso de
extender una interfaz, el constructor puede ser el único sin argumento. La clase

pág. 33
anónima sin nombre no puede tener sus propios constructores. En Java moderno,
usualmente usamos lambda en lugar de clases anónimas.

Por último, pero no menos importante, bueno, en realidad, menos debería mencionar que las clases
anidadas e internas también pueden anidarse en estructuras más profundas. Las clases internas
no pueden contener clases anidadas, pero las clases anidadas pueden contener clases
internas. ¿Por qué? Nunca he conocido a nadie que pueda decirme de manera confiable la
verdadera razón. No hay razón arquitectónica. Podría ser así. Java no permite eso. Sin embargo,
no es realmente interesante. Si usted escribe código que tiene más de un nivel de anidamiento de
clase, simplemente deje de hacerlo. Lo más probable es que estés haciendo algo mal. [1]

Paquetes
Las clases se organizan en paquetes, y la primera línea de código en un archivo debe
especificar el paquete en el que se encuentra la clase:

paquete packt.java11.example.stringsort;

Si no especifica el paquete, la clase estará en el paquete predeterminado. Esto no debe


usarse, excepto en el caso más simple cuando desee probar algún código. Con Java 11,
puede usar jshell para este propósito. Entonces, a diferencia de las versiones
anteriores de Java, ahora la sugerencia se vuelve muy simple: nunca coloque ninguna
clase en el paquete predeterminado.

El nombre de los paquetes es jerárquico. Las partes de los nombres están separadas por
puntos. El uso de nombres de paquetes le ayuda a evitar colisiones de nombres. Los
nombres de las clases generalmente se mantienen cortos, y ponerlos en paquetes ayuda
a la organización del programa. El nombre completo de una clase incluye el nombre del
paquete en el que se encuentra la clase. Por lo general, colocaremos estas clases en un
paquete que de alguna manera esté relacionado y agregaremos algo a un aspecto
similar de un programa. Por ejemplo, los controladores en un programa de patrón MVC
se mantienen en un solo paquete. Los paquetes también lo ayudan a evitar la colisión
de nombres de clases. Sin embargo, esto solo empuja el problema de la colisión del
nombre de la clase a la colisión del nombre del paquete. Tenemos que asegurarnos de
que el nombre del paquete sea único y no cause ningún problema cuando nuestro
código se use junto con cualquier otra biblioteca. Cuando se desarrolla una aplicación,
simplemente no podemos saber qué otras bibliotecas se utilizarán en las versiones
posteriores. Para estar preparado para lo inesperado, la convención es nombrar los
paquetes de acuerdo con algunos nombres de dominio de Internet. Cuando una
empresa de desarrollo tiene el nombre de dominio acmecompany.com, su software
generalmente está debajo de los paquetes com.acmecompany... No es un requisito de
idioma estricto. Es solo una convención escribir el nombre de dominio de derecha a
izquierda y usarlo como el nombre del paquete, pero esto demuestra ser bastante
bueno en la práctica. A veces, como hago en este libro, uno puede desviarse de esta
práctica, por lo que puede ver que esta regla no está tallada en piedra.
pág. 34
Cuando el caucho llega a la carretera, y el código se compila en bytecode, el paquete se
convierte en el nombre de la clase. Por lo tanto, el nombre completo de la clase Sort
es packt.java11.example.stringsort.Sort. Cuando usa una clase de otro paquete,
puede usar este nombre completo o importar la clase a su clase. De nuevo, esto está en
el nivel del idioma. Usar el nombre completo o importar no hace ninguna diferencia
cuando Java se convierte en bytecode. [1]

Métodos
Ya hemos discutido los métodos, pero no en detalle, y todavía hay algunos aspectos que
debemos cumplir antes de continuar.

Hay dos métodos en las clases de muestra. Puede haber muchos métodos en una
clase. Los nombres de los métodos también están en camello por convención, y el
nombre comienza con una letra minúscula, a diferencia de las clases.

Los métodos pueden devolver un valor. Si un método devuelve un valor, el método debe
declarar el tipo del valor que devuelve y, en ese caso, cualquier ejecución del código
debe terminar con una declaración return. La declaración return tiene una expresión
después de la palabra clave, que se evalúa cuando se ejecuta el método y el método la
devuelve. Es una buena práctica tener un solo retorno de un método, pero, en algunos
casos simples, romper esa convención de codificación puede ser perdonado. El
compilador verifica las posibles rutas de ejecución del método, y es un error en tiempo
de compilación si algunas de las rutas no devuelven un valor.

Cuando un método no devuelve ningún valor, debe declararse como void. Este es un
tipo especial que significa que no tiene valor. Los métodos que son void, como el
método public static void main(), pueden simplemente perder la declaración de
devolución y simplemente finalizar. Si hay una declaración return, no hay lugar para
ninguna expresión que defina un valor de retorno después de la palabra clave
return. Nuevamente, esta es una convención de codificación para no usar
la returndeclaración en el caso de un método que no devuelve ningún valor, pero en
algunos patrones de codificación, esto no se puede seguir.

Los métodos pueden ser private, protected, public, y static, y vamos a discutir su
significado más tarde.

Hemos visto que el método main()que se invocó cuando se inició el programa es


un método static. Tal método pertenece a la clase y se puede invocar sin tener una
instancia de la clase. Los métodos estáticos se declaran con el modificador static y no
pueden acceder a ningún campo o método que no sea estático.

En nuestro ejemplo, el método sort()no es estático, pero como no accede a ningún


campo y no llama a ningún método no estático (de hecho, no llama a ningún método en

pág. 35
absoluto); bien podría ser static. Si cambiamos la declaración del método a public
static void sort(String[] names) {(tenga en cuenta la palabra static), el programa
aún funciona, pero el IDE emitirá una advertencia durante la edición, como se muestra
en el siguiente ejemplo:

Static member
'packt.java11.example.stringsort.Sort.sort(java.lang.String[])' accessed
via instance reference

Esto se debe a que puede acceder al método sin una instancia directamente a través del
nombre de la clase Sort.sort(actualNames); sin la necesidad de la variable
sorter. Llamar a un método estático a través de una variable de instancia es posible en
Java (nuevamente, algo que parecía ser una buena idea en la génesis de Java, pero
probablemente no lo es), pero puede inducir a error al lector del código para que piense
que el método es Un método de instancia.

Haciendo el método sort()static, el método main()puede ser el siguiente:

public static void main(String[] args) {


String[] actualNames = new String[]{
"Johnson", "Wilson",
"Wilkinson", "Abraham", "Dagobert"
};
Sort.sort(actualNames);
for (final String name : actualNames) {
System.out.println(name);
}
}

Parece ser mucho más simple (lo es) y, en caso de que el método no use ningún campo,
puede pensar que no hay razón para hacer que un método no sea estático. Durante los
primeros 10 años de Java, los métodos estáticos fueron de gran uso. Incluso hay un
término, clase de utilidad, que significa una clase que solo tiene métodos estáticos y no
debe ser instanciada. Con la llegada de los contenedores de Inversión de Control ,
tendemos a usar métodos menos estáticos. Cuando se usan métodos estáticos, es más
difícil usar la inyección de dependencia , y también es más difícil crear
pruebas. Discutiremos estos temas avanzados en los próximos capítulos. Por ahora, se
le informa qué son los métodos estáticos y qué se pueden usar; sin embargo,
generalmente, a menos que haya una necesidad muy especial para ellos, los evitaremos.

Más adelante, veremos cómo se implementan las clases en la jerarquía y cómo las clases
pueden implementar interfaces y extender otras clases. Cuando se observan estas
características, veremos que existen las llamadas clases abstractas que pueden
contener métodos abstractos. Estos métodos tienen el modificador abstract y no están
definidos: solo se especifican el nombre, los tipos de argumento (y los nombres) y el
tipo de retorno. Una clase concreta (no abstracta) que extienda la clase abstracta
debería definirlos.

pág. 36
Lo opuesto al método abstracto es el método final declarado con el
modificador final. Un método final no puede ser anulado en subclases. [1]

Interfaces
Los métodos también se declaran en las interfaces. Un método declarado en una
interfaz no define el comportamiento real del método; No contienen el código. Solo
tienen la cabeza del método; en otras palabras, son abstractos implícitamente. Aunque
nadie lo hace, incluso puede usar la palabra clave abstract en una interfaz cuando
define un método.

Las interfaces se parecen mucho a las clases, pero en lugar de usar la class palabra
clave, usamos la palabra clave interface. Debido a que las interfaces se utilizan
principalmente para definir métodos, los métodos son public si no se utiliza ningún
modificador.

Las interfaces también pueden definir campos, pero como las interfaces no pueden
tener instancias (solo las clases de implementación pueden tener instancias), estos
campos son todos static y también tienen que serlo final. Este es el valor
predeterminado para los campos en las interfaces, por lo tanto, no necesitamos
escribirlos si definimos campos en las interfaces.

Era una práctica común definir solo constantes en algunas interfaces y luego usarlas en las
clases. Para hacer eso, la forma más fácil era implementar la interfaz. Dado que estas interfaces
no definen ningún método, la implementación no es más que escribir la palabra clave implements
y el nombre de la interfaz en el encabezado de la declaración de clase. Esta es una mala práctica
porque de esta manera la interfaz se convierte en parte de la declaración pública de la clase,
aunque estas constantes son necesarias dentro de la clase. Si necesita definir constantes que no
son locales para una clase pero se usan en muchas clases, defínalos en una clase e importe los
campos usando import statico simplemente use el nombre de la clase y el campo.

Las interfaces también pueden tener clases anidadas, pero no pueden tener clases
internas. La razón obvia para eso es que las instancias de clase interna tienen una
referencia a una instancia de la clase que lo encierra. En el caso de una interfaz, no hay
instancias, por lo que una clase interna no podría tener una referencia a una instancia
de una interfaz de cierre porque eso simplemente no existe. La parte alegre de esto es
que no necesitamos usar la palabra clave static en el caso de clases anidadas porque
ese es el valor predeterminado, al igual que en el caso de los campos.

Con el advenimiento de Java 8, también puede tener métodos default en las interfaces
que proporcionan la implementación predeterminada del método para las clases que
implementan la interfaz. También puede haber static y métodos private de las
interfaces de Java desde 9.

pág. 37
Los métodos se identifican por su nombre y la lista de argumentos. Puede reutilizar un
nombre para un método y tener diferentes tipos de argumentos; Java identificará qué
método utilizar en función de los tipos de argumentos reales. Esto se llama sobrecarga
de métodos. Por lo general, es fácil saber a qué método llama, pero cuando hay tipos
que se extienden entre sí, la situación se vuelve más compleja. El estándar define reglas
muy precisas para la selección real del método que sigue el compilador, por lo que no
hay ambigüedad. Sin embargo, los programadores que leen el código pueden
malinterpretar los métodos sobrecargados o, al menos, tendrán dificultades para
identificar qué método se llama realmente. La sobrecarga de métodos también puede
dificultar la compatibilidad con versiones anteriores cuando desee ampliar su clase. El
consejo general es pensar dos veces antes de crear métodos sobrecargados. Son
lucrativos, pero a veces pueden ser costosos. [1]

Argumento que pasa


En Java, los argumentos se pasan por valor. Cuando el método modifica una variable de
argumento, solo se modifica la copia del valor original. Cualquier valor primitivo se
copia durante la llamada al método. Cuando se pasa un objeto como argumento, se pasa
la copia de la referencia al objeto.

De esa manera, el objeto está disponible para ser modificado para el método. En el caso
de las clases que tienen su contraparte primitiva, y también en el caso de String y otros
tipos de clases, los objetos simplemente no proporcionan métodos o campos para
modificar el estado. Esto es importante para la integridad del lenguaje y para no
meterse en problemas cuando los objetos y los valores primitivos se convierten
automáticamente.

En otros casos, cuando el objeto es modificable, el método puede funcionar


efectivamente en el mismo objeto al que se pasó. Esta es también la forma en que
el método sort()en nuestro ejemplo funciona en la matriz. La misma matriz, que
también es un objeto en sí, se modifica.

Este argumento que pasa es mucho más simple que en otros idiomas. Otros lenguajes
permiten al desarrollador mezclar el paso por referencia y el paso por argumento de
paso. En Java, cuando usa una variable por sí misma como una expresión para pasar un
parámetro a un método, puede estar seguro de que la variable en sí misma nunca se
modifica. El objeto se refiere a esto, sin embargo, si es mutable, puede modificarse.

Un objeto es mutable si puede modificarse, alterando el valor de algunos de sus campos


directamente o mediante alguna llamada a un método. Cuando una clase se diseña de una manera
que no existe una forma normal de modificar el estado del objeto después de la creación del objeto,
el objeto es inmutable. Las clases Byte, Short, Integer, Long, Float, Double, Boolean, Character,,
así como String, están diseñados en el JDK para que los objetos son inmutables. Es posible superar
la limitación de la implementación de la inmutabilidad de ciertas clases utilizando la reflexión,

pág. 38
pero hacerlo es piratear y no codificación profesional. Esto se puede hacer con un único propósito:
obtener un mejor conocimiento y comprensión del funcionamiento interno de algunas clases de
Java, pero nada más. [1]

Campos
Los campos son variables a nivel de clase. Representan el estado de un objeto. Son
variables con un tipo definido y un posible valor inicial. Los campos pueden
ser static, final, transient, y volatile, y el acceso puede ser modificado con
el public, protected y palabras clave private.

Los campos estáticos pertenecen a la clase. Significa que hay una de ellas compartida
por todas las instancias de la clase. Los campos normales y no estáticos pertenecen a
los objetos. Si tiene un campo llamado f, entonces cada instancia de la clase tiene la
suya f. Si fse declara static, las instancias compartirán el mismo fcampo.

Los finalcampos no se pueden modificar después de que se inicializan. La inicialización


se puede hacer en la línea donde se declaran, en un bloque inicializador o en el código
del constructor. El requisito estricto es que la inicialización tenga que suceder antes de
que regrese el constructor. De esta manera, el significado de la palabra clave final es
muy diferente, en este caso, de lo que significa en el caso de una clase o un
método. Una clase final no se puede extender y un método final no se puede anular
en una clase extendida, como veremos en el próximo capítulo. Los campos final no
están inicializados u obtienen un valor durante la creación de la instancia. El
compilador también verifica que el código inicialice todos los campos final durante la
creación de la instancia del objeto o durante la carga de la clase, en caso de que el
campo final sea static, y que el código no está accediendo / leyendo
ningún finalcampo que aún no se haya inicializado.

Es un error común pensar que los campos final deben inicializarse en la declaración. Se puede
hacer en un código inicializador o en un constructor. La restricción es que no importa qué
constructor se llame en caso de que haya más, los campos final deben inicializarse exactamente
una vez.

Los campos transient no son parte del estado serializado del objeto. La serialización
es un acto de convertir el valor real de un objeto en bytes físicos. La deserialización es
lo opuesto cuando el objeto se crea a partir de los bytes. Se utiliza para guardar el
estado en algunos marcos. El código que realiza la
serialización, java.lang.io.ObjectOutputStream funciona solo con clases que
implementan la Serializableinterfaz y usa solo los campos de aquellos objetos que no
lo son transient. Obviamente, los campos transient tampoco se restauran a partir de
los bytes que representan la forma serializada del objeto porque su valor no está allí.

pág. 39
La serialización se usa generalmente en programas distribuidos. Un buen ejemplo es el
objeto de sesión de un servlet. Cuando el contenedor de servlet se ejecuta en un nodo
agrupado, algunos campos de objetos almacenados en el objeto de sesión pueden
desaparecer mágicamente entre los accesos HTTP. Esto se debe a que la serialización
guarda y vuelve a cargar la sesión para moverla entre los nodos. La serialización, en tal
situación, también puede ser un problema de rendimiento si un desarrollador no
conoce los efectos secundarios de los objetos grandes almacenados en la sesión.

La palabra clave volatile es una palabra clave que le dice al compilador que el campo
puede ser utilizado por diferentes hilos. Cuando un código volatile accede
a un campo, el compilador JIT genera código, lo que garantiza que el valor del campo al
que se accede esté actualizado.

Cuando un campo no es volátil, el código generado por el compilador puede almacenar


el valor del campo en un caché de procesador o registro para un acceso más rápido
cuando ve que el valor será necesario pronto por algún fragmento de código
posterior. En el caso de los campos volatile, esta optimización no se puede
hacer. Además, tenga en cuenta que guardar el valor en la memoria y cargar desde allí
todo el tiempo puede ser 50 o más veces más lento que acceder a un valor desde un
registro o caché. [1]

Modificadores
Los métodos, constructores, campos, interfaces y clases pueden tener modificadores de
acceso. La regla general es que en caso de que no haya un modificador, el alcance del
método, el constructor, etc., es el paquete. Cualquier código en el mismo paquete puede
acceder a él.

Cuando private se usa el modificador, el alcance está restringido a la llamada unidad


de compilación. Esto significa la clase que está en un archivo. Lo que está dentro de un
archivo puede ver y usar cualquier cosa declarada private. De esta manera, las clases
internas y anidadas pueden tener acceso a las variables private de cada una, lo que
puede no ser un buen estilo de programación, pero Java lo permite.

Private puede acceder a los miembros desde el código que está en la misma clase de nivel
superior. Si hay clases internas dentro de una clase de nivel superior, entonces el compilador
genera archivos de clase separados de estos archivos. La JVM no sabe qué es una clase
interna. Para la JVM, una clase es solo una clase. Private los miembros aún deben ser accesibles
desde una clase que es la clase de nivel superior o está dentro de la misma clase de nivel superior
donde está el miembro private (método o campo). Al mismo tiempo, otras clases no deberían
poder acceder a los campos private. Para resolver esta ambigüedad, Java generó los llamados
métodos proxy sintéticos que son visibles desde el exterior y, por lo tanto, son accesibles. Cuando
quieres llamar a un método private de la misma clase de nivel superior pero en una clase interna
diferente, entonces el compilador genera una clase proxy. Esta es la razón por la que muchas veces

pág. 40
los IDE advierten que los métodos private pueden no ser óptimos desde el punto de vista del
rendimiento.

Esto ha cambiado con Java 11, que introdujo la noción de nido. La clase de nivel superior es un
anfitrión del nido y cada clase puede decir cuáles están en su nido y quién es su anfitrión. De esta
forma, la JVM sabe si un acceso a un miembro private (leer o escribir en un campo o llamar a un
método) es permisible. Al mismo tiempo, Java 11 ya no genera métodos proxy sintéticos.

Lo contrario de private es public. Extiende la visibilidad a todo el programa Java, o al


menos a todo el módulo si el proyecto es un módulo Java.

Hay un camino intermedio: protected. Se puede acceder a cualquier cosa con este
modificador dentro del paquete y también en las clases que extienden la clase
(independientemente del paquete) en la que se encuentra el método protegido, el
campo, etc. [1]

Inicializadores y constructores de
objetos.
Cuando se crea una instancia de un objeto, se llama al constructor apropiado. La
declaración del constructor parece un método con la siguiente desviación: el
constructor no tiene un valor de retorno. Esto se debe a que los constructores trabajan
en la instancia no totalmente lista cuando new se invoca el operador de comando y no
devuelve nada. Los constructores, que tienen el mismo nombre que la clase, no se
pueden distinguir entre sí. Si se necesita más de un constructor, deben
sobrecargarse. Los constructores, por lo tanto, pueden llamarse entre sí, casi como si
fueran voidmétodos con diferentes argumentos. Sin embargo, hay una restricción:
cuando un constructor llama a otro, tiene que ser la primera instrucción en el
constructor. Tu usas sintaxis this() con una lista de argumentos apropiada, que puede
estar vacía, para invocar un constructor desde otro constructor.

La inicialización de la instancia del objeto también ejecuta bloques de


inicializador. Estos son bloques que contienen código ejecutable dentro de
los caracteres {y }fuera de los métodos y constructores. Se ejecutan antes del
constructor en el orden en que aparecen en el código, junto con la inicialización de los
campos en caso de que sus declaraciones contengan inicialización de valor.

Si ve la palabra clave static frente a un bloque inicializador, el bloque pertenece a la


clase y se ejecuta cuando se carga la clase, junto con los inicializadores de campo
estático. [1]

pág. 41
JEP 318 - Epsilon
Epsilon es un llamado recolector de basura no operativo que básicamente no hace
nada. Sus casos de uso incluyen pruebas de rendimiento, presión de memoria y la
interfaz de máquina virtual. También podría usarse para trabajos de corta duración o
trabajos que no consumen mucha memoria y no requieren recolección de basura.

Discutimos esta característica con más detalles en la receta Comprenda Epsilon,


una receta de recolección de basura de bajo costo en el Capítulo 11 , Administración de
memoria y depuración .

JEP 321 - Cliente HTTP


(estándar)
JDK 18.9 estandariza el cliente de API HTTP incubado introducido en JDK 9 y
actualizado en JDK 10. Basado en CompleteableFuture, admite solicitudes y respuestas
sin bloqueo. La nueva implementación es asíncrona y proporciona un mejor flujo de
datos rastreable.

El Capítulo 10 , Redes , explica esta característica con más detalle en varias recetas.

JEP 323 - Sintaxis de variable


local para parámetros Lambda
Una sintaxis de variable local para parámetros lambda tiene la misma sintaxis que una
declaración de variable local que usa el var tipo reservado introducido en Java
11. Consulte la receta Uso de la sintaxis de variable local para parámetros lambda en
el Capítulo 15 , La nueva forma de codificación con Java 10 y Java 11 , para más detalles.

JEP 333 - ZGC


El recolector de basura Z ( ZGC ) es un recolector de basura experimental de baja
latencia. Sus tiempos de pausa no deben exceder los 10 ms y no debe haber más de 15%
de reducción en el rendimiento de la aplicación en comparación con el uso del
pág. 42
recopilador G1. ZGC también sienta las bases para futuras funciones y
optimizaciones. Linux / x64 será la primera plataforma en obtener soporte para ZGC.

Nueva API
Hay varias adiciones a la API estándar de Java:

• Character.toString(int codePoint): Devuelve un objeto String que representa el


carácter especificado por el punto de código Unicode proporcionado:

var s = Character.toString(50);
System.out.println(s); //prints: 2

• CharSequence.compare(CharSequence s1, CharSequence s2): Compara


dos CharSequenceinstancias lexicográficamente. Devuelve la diferencia entre la
posición del segundo parámetro y la posición del primer parámetro en la lista
ordenada:

var i = CharSequence.compare("a", "b");


System.out.println(i); //prints: -1

i = CharSequence.compare("b", "a");
System.out.println(i); //prints: 1

i = CharSequence.compare("this", "that");
System.out.println(i); //prints: 8

i = CharSequence.compare("that", "this");
System.out.println(i); //prints: -8

• El método repeat(int count) de la clase String : Devuelve un valor compuesto


String por tiempos repetidos count en el valor String fuente:

String s1 = "a";
String s2 = s1.repeat(3); //prints: aaa
System.out.println(s2);

String s3 = "bar".repeat(3);
System.out.println(s3); //prints: barbarbar

• El método de la clase: devuelve si el valor está vacío o contiene solo espacios en


blanco, de lo contrario . En nuestro ejemplo, lo hemos contrastado con el método,
que devuelve si, y solo si, es
cero: isBlank() StringtrueStringfalseisEmpty()truelength()

String s1 = "a";
System.out.println(s1.isBlank()); //false
System.out.println(s1.isEmpty()); //false

pág. 43
String s2 = "";
System.out.println(s2.isBlank()); //true
System.out.println(s2.isEmpty()); //true

String s3 = " ";


System.out.println(s3.isBlank()); //true
System.out.println(s3.isEmpty()); //false

• El método de la clase: Devuelve un objeto que emite extraídos líneas de la fuente


de valor, separados por terminadores de
línea - , o : lines() StringStreamString \n\r\r\n

String s = "l1 \nl2 \rl3 \r\nl4 ";


s.lines().forEach(System.out::print); //prints: l1 l2 l3 l4

• Tres métodos de la clase que eliminan el espacio inicial, el espacio final o ambos
del valor de origen : StringString

String s = " a b ";


System.out.println("'" + s.strip() + "'"); // 'a b'
System.out.println("'" + s.stripLeading() + "'"); // 'a b '
System.out.println("'" + s.stripTrailing() + "'");// ' a b'

• Dos métodos Path.of() que construyen un objeto java.nio.file.Path:

Path filePath = Path.of("a", "b", "c.txt");


System.out.println(filePath); //prints: a/b/c.txt

try {
filePath = Path.of(new URI("file:/a/b/c.txt"));
System.out.println(filePath); //prints: /a/b/c.txt
} catch (URISyntaxException e) {
e.printStackTrace();
}

• El método asMatchPredicate() de la clase java.util.regex.Pattern , que crea un


objeto de la interfaz funcional java.util.function.Predicate, que luego nos permite
probar un Stringvalor para que coincida con el patrón compilado. En el siguiente
ejemplo, probamos si un valor String comienza con el carácter a y termina con
el carácter b:

Pattern pattern = Pattern.compile("^a.*z$");


Predicate<String> predicate = pattern.asMatchPredicate();
System.out.println(predicate.test("abbbbz")); // true
System.out.println(predicate.test("babbbz")); // false
System.out.println(predicate.test("abbbbx")); // false

pág. 44
Hay más...
Hay bastantes otros cambios introducidos en JDK 18.9:

• Se eliminan los módulos Java EE y CORBA


• JavaFX se separa y elimina de las bibliotecas estándar de Java
• Las herramientas Pack200 y Unpack200 y la API Pack200 util.jar están en desuso
• El motor JavaScript de Nashorn, junto con la herramienta JJS, están en desuso con la
intención de eliminarlos en el futuro
• El formato de archivo de clase Java se extiende para admitir un nuevo formulario de
agrupación constante, CONSTANT_Dynamic
• Los intrínsecos de Aarch64 se mejoran, con la implementación de nuevos intrínsecos
para las java.lang.Mathfunciones sin, cos y log, en los procesadores Aarch64JEP 309
— Constantes dinámicas de archivos de clase
• Flight Recorder proporciona un marco de recopilación de datos de bajo costo para
resolver problemas tanto de aplicaciones Java como de HotSpot JVM
• El iniciador de Java ahora puede ejecutar un programa suministrado como un solo
archivo de código fuente de Java, por lo que estos programas pueden ejecutarse
directamente desde la fuente
• Se puede acceder a un perfil de almacenamiento dinámico de bajo costo, que
proporciona una forma de probar las asignaciones de almacenamiento dinámico de
Java a través de la interfaz de herramientas JVM
• Transport Layer Security ( TLS ) 1.3 aumenta la seguridad y mejora el rendimiento
• Soporte de Unicode versión 10.0 en
el java.lang.Character, java.lang.String, java.awt.font.NumericShaper, ,
y clasesjava.text.Bidi,java.text.BreakIteratorjava.text.Normalizer

Lea las notas de la versión de Java 11 (JDK 18.9) para obtener más detalles y otros
cambios.

Uso de uso compartido de datos


de clase de aplicación
Esta característica ha existido en Java desde Java 5. Se extendió en Java 9 como una
característica comercial al permitir que no solo las clases de bootstrap sino también las
clases de aplicación se coloquen en el archivo compartido por las JVM. En Java 10, esta
característica se convirtió en parte del JDK abierto. Disminuye el tiempo de inicio y,
cuando se ejecutan varias JVM en la misma máquina con la misma aplicación
implementada, reduce el consumo de memoria.

pág. 45
Prepararse
Las ventajas de cargar clases desde el archivo compartido se hicieron posibles por dos
razones:

• Las clases almacenadas en el archivo están preprocesadas, lo que significa que la


asignación de memoria JVM también se almacena en el archivo. Reduce la
sobrecarga de carga de clases cuando se inicia una instancia de JVM.
• La región de memoria incluso se puede compartir entre las instancias de JVM que se
ejecutan en la misma computadora, lo que reduce el consumo general de memoria al
eliminar la necesidad de replicar la misma información en cada instancia.

La nueva funcionalidad JVM nos permite crear una lista de clases para compartir, luego
usar esta lista para crear un archivo compartido y usar el archivo compartido para
cargar rápidamente las clases archivadas en la memoria.

Cómo hacerlo...
1. Por defecto, JVM puede crear un archivo usando la lista de clases que viene con
JDK. Por ejemplo, ejecute el siguiente comando:

java -Xshare:dump

Creará el archivo compartido como un archivo classes.jsa . En un sistema Linux, este


archivo se coloca en la siguiente carpeta:

/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home/lib/server

En un sistema Windows, se coloca en la siguiente carpeta:

C:\Program Files\Java\jdk-11\bin\server

Si el administrador del sistema solo puede acceder a esta carpeta, ejecute el comando
como administrador.

Tenga en cuenta que no todas las clases se pueden compartir. Por ejemplo, los archivos
.class ubicados en el directorio en el classpath y las clases cargadas por cargadores de
clases personalizados no se pueden agregar al archivo compartido.

2. Para indicarle a la JVM que use el archivo compartido predeterminado, use el


siguiente comando:

java -Xshare: on -jar app.jar

pág. 46
El comando anterior mapea el contenido del archivo en una dirección fija. Esta
operación de asignación de memoria puede fallar ocasionalmente cuando el espacio de
direcciones requerido no está disponible. Si eso sucede cuando -Xshare:on se
usa la opción, la JVM sale con un error. Alternativamente, -Xshare:auto se puede
usar la opción, que simplemente deshabilita la característica y carga las clases desde el
classpath si el archivo compartido no se puede usar por cualquier razón.

3. La forma más sencilla de crear una lista de clases de aplicaciones cargadas es


mediante el siguiente comando:

java -XX: + UseAppCDS -XX: DumpLoadedClassList = classes.txt -jar app.jar

El comando anterior registra todas las clases cargadas en el archivo classes.txt . Si


desea que su aplicación se cargue más rápido, detenga la JVM justo después de que la
aplicación se haya iniciado. Si necesita cargar ciertas clases más rápido pero estas clases
no se cargan al inicio de la aplicación automáticamente, asegúrese de que se ejecuten
los casos de uso que requieren estas clases.

4. Alternativamente, puede editar manualmente el archivo classes.txt y agregar /


eliminar cualquier clase que necesite poner en el archivo compartido. Cree este archivo una
vez automáticamente y vea el formato. Es un archivo de texto simple que enumera una clase
en cada línea.

5. Una vez que se crea la lista, use el siguiente comando para generar el archivo
compartido:

java -XX: + UseAppCDS -Xshare: dump -XX: SharedClassListFile =


classes.txt -XX: SharedArchiveFile = app-shared.jsa --class-path
app.jar

Tenga en cuenta que el archivo de almacenamiento compartido tiene un nombre


distinto de classes.jsa, por lo que el archivo compartido predeterminado no se
sobrescribe.

6. Use el archivo creado ejecutando el siguiente comando:

java -XX: + UseAppCDS -Xshare: on -XX: SharedArchiveFile = app-


shared.jsa -jar app.jar

Nuevamente, puede usar la opción para evitar una salida inesperada de la JVM. -
Xshare:auto

El efecto del uso del archivo compartido depende del número de clases en él y otros
detalles de la aplicación. Por lo tanto, le recomendamos que experimente y pruebe
varias configuraciones antes de comprometerse con una determinada lista de clases en
producción.

pág. 47
Fast Track to OOP - Clases e
interfaces
En este capítulo, cubriremos las siguientes recetas:

• Implementación de diseño orientado a objetos ( OOD )


• Usando clases internas
• Usando herencia y agregación
• Codificación a una interfaz
• Crear interfaces con métodos predeterminados y estáticos
• Crear interfaces con métodos privados
• Una mejor manera de trabajar con nulos usando Optional
• Usando la clase de utilidad Objects

Las recetas en este capítulo no requieren ningún conocimiento previo de OOD. Sin
embargo , alguna experiencia de escribir código en Java sería beneficiosa. Los ejemplos
de código en este capítulo son totalmente funcionales y compatibles con Java 11. Para
una mejor comprensión, le recomendamos que intente ejecutar los ejemplos
presentados.

También le recomendamos que adapte los consejos y recomendaciones de este capítulo


a sus necesidades en el contexto de la experiencia de su equipo. Considere compartir su
nuevo conocimiento con sus colegas y discuta cómo los principios descritos se pueden
aplicar a su dominio y su proyecto actual.

Introducción
En este capítulo se da una breve introducción a los conceptos de o programación
orientada bject- ( POO ) y cubre algunas mejoras que se han introducido desde Java
8. También tratarán de cubrir unas buenas prácticas OOD siempre que sea aplicable y
demostrar que el uso de código específica ejemplos

Uno puede pasar muchas horas leyendo artículos y consejos prácticos sobre OOD en
libros y en Internet. Hacer esto puede ser beneficioso para algunas personas. Pero,
según nuestra experiencia, la forma más rápida de obtener OOD es probar sus
principios al principio de su propio código. Ese es exactamente el objetivo de este
capítulo: darle la oportunidad de ver y usar los principios OOD para que la definición
formal tenga sentido de inmediato.

Uno de los criterios principales del código bien escrito es la claridad de la intención. Un
diseño bien motivado y claro ayuda a lograr esto. El código lo ejecuta una computadora,
pero los humanos lo mantienen , leen y modifican. Tener esto en cuenta asegurará la
pág. 48
longevidad de su código y tal vez incluso algunas gracias y menciones con
agradecimiento de aquellos que tienen que lidiar con él más adelante.

En este capítulo, aprenderá a usar los cinco conceptos básicos de OOP:

• Objeto / clase : mantener datos y métodos juntos


• Encapsulación : Ocultar datos y / o métodos
• Herencia : extensión de datos y / o métodos de otra clase
• Interfaz : ocultar la implementación y la codificación de un tipo
• Polimorfismo : uso de la referencia de tipo de clase base que apunta a un objeto de
clase secundaria

Si busca en Internet, puede notar que muchos otros conceptos y adiciones a ellos, así
como todos los principios de OOD, pueden derivarse de los cinco conceptos
enumerados anteriormente. Esto significa que una comprensión sólida de ellos es un
requisito previo para un diseño exitoso de un sistema orientado a objetos.

Implementación de diseño
orientado a objetos (OOD)
En esta receta, aprenderá los dos primeros conceptos de OOP : objeto / clase y
encapsulación. Estos conceptos están en la base de OOD.

Prepararse
El término objeto generalmente se refiere a una entidad que combina datos y
procedimientos que pueden aplicarse a estos datos. No se requieren datos ni
procedimientos, pero uno de ellos está , y, por lo general, ambos están , siempre
presentes. Los datos se denominan campos de objetos (o propiedades), mientras que
los procedimientos se denominan métodos. Los valores de campo describen
el estado del objeto . Los métodos describen el comportamiento del objeto . Cada objeto
tiene un tipo, que se define por su clase : la plantilla utilizada para la creación del
objeto. También se dice que un objeto es una instancia de una clase.

Una clase es una colección de definiciones de campos y métodos que estarán presentes en cada
una de sus instancias : los objetos creados en base a esta clase.
La encapsulación es la ocultación de esos campos y métodos a los que otros objetos no deberían
poder acceder.

La encapsulación se logra mediante el uso de las palabras


clave public, protectedo private de Java, llamado modificadores de acceso, en la

pág. 49
declaración de los campos y métodos. También hay un nivel predeterminado de
encapsulación cuando no se especifica un modificador de acceso.

Cómo hacerlo...
1. Crea una clase Engine con el campo horsePower. Agregue el método
setHorsePower(int horsePower), que establece el valor de este campo, y el método
getSpeedMph(double timeSec, int weightPounds), que calcula la velocidad de un
vehículo en función del período de tiempo transcurrido desde que el vehículo
comenzó a moverse, el peso del vehículo y la potencia del motor:

public class Engine {


private int horsePower;
public void setHorsePower(int horsePower) {
this.horsePower = horsePower;
}
public double getSpeedMph(double timeSec, int weightPounds){
double v = 2.0 * this.horsePower * 746 * timeSec *
32.17 / weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
}

2. Crea la clase Vehicle :

public class Vehicle {


private int weightPounds;
private Engine engine;
public Vehicle(int weightPounds, Engine engine) {
this.weightPounds = weightPounds;
this.engine = engine;
}
public double getSpeedMph(double timeSec){
return this.engine.getSpeedMph(timeSec, weightPounds);
}
}

3. Cree la aplicación que usará las clases anteriores:

public static void main(String... arg) {


double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Engine engine = new Engine();
engine.setHorsePower(horsePower);
Vehicle vehicle = new Vehicle(vehicleWeight, engine);
System.out.println("Vehicle speed (" + timeSec + " sec)="
+ vehicle.getSpeedMph(timeSec) + " mph");
}

pág. 50
Como puede ver, el objeto engine se creó invocando al constructor predeterminado de
la clase Engine sin parámetros y con la palabra clave Java new que asigna memoria para
el objeto recién creado en el montón.

El segundo objeto, vehicle se creó con el constructor explícitamente definido de


la clase Vehicle con dos parámetros. El segundo parámetro del constructor es
el engine objeto, que lleva el valor establecido para usar
el método. horsePower246setHorsePower(int horsePower)

El objeto contiene el método, que puede ser llamado por cualquier objeto (porque lo
es ), como se hace en el método de la clase. engine getSpeedMph(double timeSec, int
weightPounds) public getSpeedMph(double timeSec) Vehicle.

Cómo funciona...
La aplicación anterior produce el siguiente resultado:

Vale la pena notar que el método de la clase se basa en la presencia de un valor


asignado al campo. De esta manera, el objeto de la clase delega el cálculo de
velocidad al objeto de la clase. Si este último no está configurado ( pasado en
el constructor, por
ejemplo), getSpeedMph(double timeSec)VehicleengineVehicleEngine
nullVehicle()NullPointerExceptionengineVehicle()

if(engine == null){

lanzará en el tiempo de ejecución y, si la aplicación no lo maneja, será capturado


por JVM y lo obligará a salir. Para evitar esto, podemos marcar la presencia
del valor del campo engine en el constructor Vehicle():

if (motor == nulo) {
arroja una nueva RuntimeException ("Engine" + "es un
parámetro obligatorio");
}

Alternativamente, podemos colocar un cheque en el método


getSpeedMph(double timeSec) de la clase Vehicle :

if(getEngine() == null){
throw new RuntimeException("Engine value is required.");

pág. 51
De esta forma, evitamos la ambigüedad de NullPointerException y le decimos
al usuario exactamente cuál fue el origen del problema.

Como habrás notado, el método se puede eliminar de la clase y se puede


implementar completamente en la clase:getSpeedMph(double timeSec,
int weightPounds)EngineVehicle

public double getSpeedMph(double timeSec){


double v = 2.0 * this.engine.getHorsePower() * 746 *
timeSec * 32.17 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}

Para hacer esto, necesitaríamos agregar el método público getHorsePower()a


la clase Engine para que esté disponible para su uso por el método en
la clase. Por ahora, dejamos el método
getSpeedMph(double timeSec)VehiclegetSpeedMph(double timeSec,
int weightPounds) en la clase Engine.

Esta es una de las decisiones de diseño que debe tomar. Si cree que un objeto de
la clase Engine va a ser pasado y utilizado por los objetos de diferentes clases (no
solo Vehicle), necesitaría mantener el método en la clase. De lo contrario, si cree
que solo la clase será responsable del cálculo de la velocidad (lo cual tiene sentido,
ya que es la velocidad de un vehículo, no de un motor), debe implementar este
método dentro de la clase getSpeedMph(double timeSec, int
weightPounds)EngineVehicleVehicle

Hay más...
Java proporciona la capacidad de extender una clase y permite que la subclase acceda a
todos los campos y métodos no privados de la clase base. Por ejemplo, puede decidir
que cada objeto al que se le pueda preguntar sobre su velocidad pertenece a una
subclase que se deriva de la clase Vehicle. En tal caso, la clase Car puede verse así:

public class Car extends Vehicle {


private int passengersCount;
public Car(int passengersCount, int weightPounds, Engine engine){
super(weightPounds, engine);
this.passengersCount = passengersCount;
}
public int getPassengersCount() {
return this.passengersCount;
}
}

Ahora, podemos cambiar nuestro código de prueba reemplazando el objeto de clase


Vehicle con el objeto de la clase Car:

pág. 52
public static void main(String... arg) {
double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Engine engine = new Engine();
engine.setHorsePower(horsePower);
Vehicle vehicle = new Car(4, vehicleWeight, engine);
System.out.println("Car speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
}

Cuando se ejecuta el código anterior, produce el mismo valor que con un objeto de
la clase Vehicle :

Debido a polimorfismo, una referencia al objeto de la clase Car puede ser asignado a la
referencia de su clase base, Vehicle. El objeto Car de la clase tiene dos tipos-su
propio tipo, Car y el tipo de la clase base, Vehicle.

En Java, una clase también puede implementar múltiples interfaces, y el objeto de dicha
clase también tendría un tipo de cada una de las interfaces implementadas. Hablaremos
de esto en las siguientes recetas.

Usando clases internas


En esta receta, aprenderá sobre tres tipos de clases internas:

• Clase interna : Esta es una clase definida dentro de otra clase (adjunta). Su
accesibilidad desde fuera de la clase envolvente está regulada por
las public, protectedy private modificadores de acceso. Una clase interna puede
acceder a los miembros privados de la clase envolvente, y la clase envolvente puede
acceder a los miembros privados de su clase interna, pero no se puede acceder a una
clase interna privada o miembros privados de una clase interna no privada desde fuera
de la clase envolvente .
• Clase interna de método local : esta es una clase definida dentro de un método. Su
accesibilidad está restringida a dentro del método.
• Clase interna anónima : esta es una clase sin un nombre declarado que se define
durante la creación de instancias de objetos basada solo en la interfaz o la clase
extendida.

Prepararse

pág. 53
Cuando una clase es utilizada por una, y solo una, otra clase, el diseñador puede decidir
que no hay necesidad de hacer pública dicha clase. Por ejemplo, supongamos que
la clase Engine solo la usa la clase Vehicle.

Cómo hacerlo...
1. Cree la clase Engine como una clase interna de la clase Vehicle:

public class Vehicle {


private int weightPounds;
private Engine engine;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.engine = new Engine(horsePower);
}
public double getSpeedMph(double timeSec){
return this.engine.getSpeedMph(timeSec);
}
private int getWeightPounds(){ return weightPounds; }
private class Engine {
private int horsePower;
private Engine(int horsePower) {
this.horsePower = horsePower;
}
private double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746 *
timeSec * 32.17 / getWeightPounds();
return Math.round(Math.sqrt(v) * 0.68);
}
}
}

2. Tenga en cuenta que el método getSpeedMph(double timeSec) de


la clase Vehicle puede acceder a la clase Engine, aunque esté
declarada private. Incluso puede acceder al método privado de la clase. Y la clase
interna también puede acceder a todos los elementos privados de la clase adjunta. Es
por eso que el método de la clase puede acceder al método privado de
la clase adjunta . getSpeedMph(double timeSec)EnginegetSpeedMph(double timeSec
)EnginegetWeightPounds()Vehicle

3. Mire más de cerca el uso de la clase interna Engine. Solo se usa el método de
la clase. Si el diseñador cree que también va a ser así en el futuro, podría decidir
razonablemente hacer de la clase una clase interna de método local, que es el
segundo tipo de clase interna:
getSpeedMph(double timeSec)EngineEngine

public class Vehicle {


private int weightPounds;
private int horsePower;

pág. 54
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
private int getWeightPounds() { return weightPounds; }
public double getSpeedMph(double timeSec){
class Engine {
private int horsePower;
private Engine(int horsePower) {
this.horsePower = horsePower;
}
private double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746 *
timeSec * 32.17 / getWeightPounds();
return Math.round(Math.sqrt(v) * 0.68);
}
}
Engine engine = new Engine(this.horsePower);
return engine.getSpeedMph(timeSec);
}
}

En el ejemplo de código anterior, no tiene sentido tener una clase Engine en absoluto. La
fórmula de cálculo de velocidad se puede usar directamente, sin la mediación de
la Engine clase. Pero hay casos en que esto podría no ser tan fácil de hacer. Por ejemplo,
la clase interna local del método puede necesitar extender alguna otra clase para
heredar su funcionalidad, o el objeto creado Engine puede necesitar alguna
transformación, por lo que se requiere creación. Otras consideraciones pueden
requerir una clase interna de método local.

En cualquier caso, es una buena práctica hacer que todas las funcionalidades a las que
no se requiere acceder desde fuera de la clase adjunta sean inaccesibles. La
encapsulación , que oculta el estado y el comportamiento de los objetos , ayuda a evitar
los efectos secundarios inesperados que resultan de un cambio accidental o de un
comportamiento sobresaliente. Hace que los resultados sean más predecibles. Es por
eso que un buen diseño expone solo la funcionalidad a la que se debe acceder desde el
exterior. Y, por lo general, es la funcionalidad de clase adjunta la que motivó la creación
de la clase, no la clase interna u otros detalles de implementación.

Cómo funciona...
Ya sea que la clase Engine se implemente como una clase interna o una clase interna
local de método, el código de prueba tiene el mismo aspecto:

public static void main(String arg[]) {


double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle =
new Vehicle(vehicleWeightPounds, engineHorsePower);
System.out.println("Vehicle speed (" + timeSec + " sec) = "

pág. 55
+ vehicle.getSpeedMph(timeSec) + " mph");
}

Si ejecutamos el programa anterior, obtenemos el mismo resultado:

Ahora, supongamos que necesitamos probar una implementación diferente


del método getSpeedMph():

public double getSpeedMph (double timeSec) {return -1.0d; }

Si esta fórmula de cálculo de velocidad no tiene sentido para usted, tiene razón, no lo
tiene. Lo hicimos para hacer que el resultado sea predecible y diferente del resultado
de la implementación anterior.

Hay muchas formas de presentar esta nueva implementación. Podemos cambiar el


código del método en la clase, por ejemplo. O podemos cambiar la implementación del
mismo método en la clase.getSpeedMph(double timeSec)EngineVehicle

En esta receta, haremos esto usando el tercer tipo de clase interna, llamada clase
interna anónima. Este enfoque es especialmente útil cuando desea escribir el menor
código nuevo posible, o si desea probar rápidamente el nuevo comportamiento
anulando temporalmente el anterior. El uso de una clase anónima se vería así:

public static void main(String... arg) {


double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle =
new Vehicle(vehicleWeightPounds, engineHorsePower) {
public double getSpeedMph(double timeSec){
return -1.0d;
}
};
System.out.println("Vehicle speed (" + timeSec + " sec) = "
+ vehicle.getSpeedMph(timeSec) + " mph");
}

Si ejecutamos este programa, este sería el resultado:

Como puede ver, la implementación de la clase anónima ha anulado la implementación


de la clase Vehicle. La nueva clase anónima solo tiene un
método : el método getSpeedMph() que devuelve el valor codificado. Pero

pág. 56
podríamos anular otros métodos de la clase Vehicle o agregar otros nuevos
también. Solo queríamos mantener el ejemplo simple para fines de demostración.

Por definición, una clase interna anónima tiene que ser una expresión que es parte de
una declaración que termina (como cualquier declaración) con un punto y coma. Tal
expresión se compone de las siguientes partes:

• El new operador
• El nombre de la interfaz implementada o la clase extendida seguida de
paréntesis, ()que representa el constructor predeterminado o un constructor de
la clase extendida (este último es nuestro caso, siendo la clase
extendida Vehicle)
• El cuerpo de clase con métodos

Al igual que cualquier clase interna, una clase interna anónima puede acceder a
cualquier miembro de la clase adjunta con una advertencia : para ser utilizada por una
clase anónima interna, los campos de la clase adjunta deben declararse final o
hacerse final implícitos, lo que significa que sus valores no puede ser cambiado. Un
buen IDE moderno le advertirá sobre la violación de esta restricción si intenta cambiar
dicho valor.

Usando estas características, podemos modificar nuestro código de muestra y


proporcionar más datos de entrada para el método recién implementado sin pasarlos
como parámetros del método:getSpeedMph(double timeSec)

public static void main(String... arg) {


double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle =
new Vehicle(vehicleWeightPounds, engineHorsePower){
public double getSpeedMph(double timeSec){
double v = 2.0 * engineHorsePower * 746 *
timeSec * 32.17 / vehicleWeightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
};
System.out.println("Vehicle speed (" + timeSec + " sec) = "
+ vehicle.getSpeedMph(timeSec) + " mph");
}

Observe que las timeSec, engineHorsePowery vehicleWeightPounds las


variables son accesibles por el método de la clase interna y no se pueden modificar. Si
ejecutamos el código anterior, el resultado será el mismo que
antes:getSpeedMph(double timeSec)

pág. 57
En el caso de una interfaz con un solo método abstracto (llamado interfaz funcional),
en lugar de una clase interna anónima, se puede usar otra construcción,
llamada expresión lambda . Proporciona una notación más corta. Vamos a discutir la
interfaz funcional y las expresiones lambda enCapítulo 4 , Funcionando .

Hay más...
Una clase interna es una clase anidada no estática. Java también nos permite crear una
clase anidada estática que se puede usar cuando una clase interna no requiere acceso a
campos y métodos no estáticos de la clase que los encierra. Aquí hay un ejemplo
(la palabra clave static se agrega a la clase Engine):

public class Vehicle {


private Engine engine;
public Vehicle(int weightPounds, int horsePower) {
this.engine = new Engine(horsePower, weightPounds)
}
public double getSpeedMph(double timeSec){
return this.engine.getSpeedMph(timeSec);
}
private static class Engine {
private int horsePower;
private int weightPounds;
private Engine(int horsePower, int weightPounds) {
this.horsePower = horsePower;
}
private double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746 *
timeSec * 32.17 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
}
}

Debido a que una clase estática no podía acceder a un miembro no estático, nos vimos
obligados a pasar el valor de peso a la clase Engine durante su construcción, y
eliminamos el método getWeightPounds()porque ya no es necesario.

Usando herencia y agregación


En esta receta, aprenderá más sobre dos conceptos importantes de OOP, herencia y
polimorfismo, que ya se han mencionado y utilizado en los ejemplos de las recetas
anteriores. Junto con la agregación, estos conceptos hacen que el diseño sea más
extensible.

Prepararse
pág. 58
La herencia es la capacidad de una clase para obtener la propiedad de los campos y métodos no
privados de otra clase.

La clase extendida se llama clase base, superclase o clase primaria. La nueva extensión
de la clase se llama subclase o clase secundaria.

El polimorfismo es la capacidad de usar el tipo de clase base para la referencia a un objeto de su


subclase.

Para demostrar el poder de la herencia y el polimorfismo , creemos clases que


representen automóviles y camiones, cada uno con el peso, la potencia del motor y la
velocidad que puede alcanzar (en función del tiempo) con la carga máxima. Además, un
automóvil, en este caso, se caracterizará por la cantidad de pasajeros, mientras que la
característica importante de un camión será su carga útil.

Cómo hacerlo...
1. Mira la clase Vehicle:

public class Vehicle {


private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746 *
timeSec * 32.17 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
}

La funcionalidad implementada en la clase Vehicle no es específica para un automóvil


o un camión, por lo que tiene sentido usar esta clase como clase base para
las clases Cary Truck, por lo que cada uno de ellos obtiene esta funcionalidad como
propia.

2. Crea la clase Car:

public class Car extends Vehicle {


private int passengersCount;
public Car(int passengersCount, int weightPounds,
int horsepower){
super(weightPounds, horsePower);
this.passengersCount = passengersCount;
}
public int getPassengersCount() {
return this.passengersCount;
}
}

pág. 59
3. Crea la clase Truck:

public class Truck extends Vehicle {


private int payload;
public Truck(int payloadPounds, int weightPounds,
int horsePower){
super(weightPounds, horsePower);
this.payload = payloadPounds;
}
public int getPayload() {
return this.payload;
}
}

Como la clase base Vehicle no tiene un constructor implícito ni explícito sin parámetros
(porque hemos elegido usar un constructor explícito solo con parámetros), tuvimos que
llamar al constructor de la clase base super()como la primera línea del constructor de
cada subclase de la clase Vehicle.

Cómo funciona...
Escribamos un programa de prueba:

public static void main(String... arg) {


double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle = new Car(4, vehicleWeightPounds, engineHorsePower);
System.out.println("Passengers count=" +
((Car)vehicle).getPassengersCount());
System.out.println("Car speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
vehicle = new Truck(3300, vehicleWeightPounds, engineHorsePower);
System.out.println("Payload=" +
((Truck)vehicle).getPayload() + " pounds");
System.out.println("Truck speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");
}

Observe que la referencia vehicle del tipo Vehicle apunta al objeto de


la subclase Car y más tarde al objeto de la subclase Truck . Esto es posible gracias al
polimorfismo, según el cual un objeto tiene un tipo de cada clase en su línea de herencia,
incluidas todas las interfaces.

Si necesita invocar un método que solo existe en la subclase, debe emitir dicha
referencia al tipo de subclase, como se hizo en el ejemplo anterior.

Los resultados del código anterior son los siguientes:

pág. 60
No debería sorprendernos ver la misma velocidad calculada tanto para el automóvil
como para el camión porque se usa el mismo peso y potencia del motor para calcular la
velocidad de cada uno. Pero, intuitivamente, creemos que un camión muy cargado no
debería poder alcanzar la misma velocidad que un automóvil en el mismo período de
tiempo. Para verificar esto, debemos incluir el peso total del automóvil (con los
pasajeros y su equipaje) y el del camión (con la carga útil) en los cálculos de la
velocidad. Una forma de hacerlo es anular el método getSpeedMph(double timeSec) de
la clase base Vehicle en cada una de las subclases.

Podemos agregar el método getSpeedMph(double timeSec) a la clase Car, que anulará el


método con la misma firma en la clase base. Este método utilizará el cálculo del peso
específico del automóvil:

public double getSpeedMph(double timeSec) {


int weight = this.weightPounds + this.passengersCount * 250;
double v = 2.0 * this.horsePower * 746 * timeSec * 32.17 / weight;
return Math.round(Math.sqrt(v) * 0.68);
}

En el código anterior, hemos asumido que un pasajero con equipaje pesa un total de
250 libras en promedio.

Del mismo modo, podemos agregar el método getSpeedMph(double timeSec) a la clase


Truck :

public double getSpeedMph(double timeSec) {


int weight = this.weightPounds + this.payload;
double v = 2.0 * this.horsePower * 746 * timeSec * 32.17 / weight;
return Math.round(Math.sqrt(v) * 0.68);
}

Los resultados de estas modificaciones (si ejecutamos la misma clase de prueba) serán
los siguientes:

Los resultados confirman nuestra intuición: un automóvil o camión totalmente


cargado no alcanza la misma velocidad que uno vacío.

pág. 61
Los nuevos métodos en las subclases anulan getSpeedMph(double timeSec)la clase
base Vehicle, aunque accedemos a ella a través de la referencia de clase base:

Vehicle vehicle = new Car(4, vehicleWeightPounds, engineHorsePower);


System.out.println("Car speed (" + timeSec + " sec) = " +
vehicle.getSpeedMph(timeSec) + " mph");

El método anulado está vinculado dinámicamente, lo que significa que el contexto de


la invocación del método está determinado por el tipo del objeto real al que se hace
referencia. Dado que, en nuestro ejemplo, la referencia vehicle apunta a un objeto de
la subclase Car , la construcción vehicle.getSpeedMph(double timeSec) invoca el método
de la subclase, no el método de la clase base.

Hay una redundancia de código obvia en los dos nuevos métodos, que podemos
refactorizar creando un método en la clase base Vehicle y luego usarlo en cada una de
las subclases:

doble getSpeedMph protegido (doble timeSec, int weightPounds) {


double v = 2.0 * this.horsePower * 746 *
timeSec * 32.17 / weightPounds;
return Math.round (Math.sqrt (v) * 0.68);
}

Dado que este método solo lo usan las subclases, puede ser protected y, por lo tanto,
accesible solo para las subclases.

Ahora, podemos cambiar el método getSpeedMph(double timeSec) en la clase Car , de la


siguiente manera:

public double getSpeedMph(double timeSec) {


int weightPounds = this.weightPounds + this.passengersCount * 250;
return getSpeedMph(timeSec, weightPounds);
}

En el código anterior, no había necesidad de usar la palabra clave super mientras se


llamaba al método getSpeedMph(timeSec, weightPounds) porque solo existe un método
con dicha firma en la Vehicle clase base, y no hay ambigüedad al respecto.

Se pueden hacer cambios similares en el método de la clase: getSpeedMph(double


timeSec)Truck

public double getSpeedMph(double timeSec) {


int weightPounds = this.weightPounds + this.payload;
return getSpeedMph(timeSec, weightPounds);
}

Ahora, tenemos que modificar la clase de prueba agregando conversión, de lo


contrario habrá un error de tiempo de ejecución porque el getSpeedMph(double
timeSec)método no existe en la clase Vehicle base:

pág. 62
public static void main(String... arg) {
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Vehicle vehicle = new Car(4, vehicleWeightPounds,
engineHorsePower);
System.out.println("Passengers count=" +
((Car)vehicle).getPassengersCount());
System.out.println("Car speed (" + timeSec + " sec) = " +
((Car)vehicle).getSpeedMph(timeSec) + " mph");
vehicle = new Truck(3300, vehicleWeightPounds, engineHorsePower);
System.out.println("Payload=" +
((Truck)vehicle).getPayload() + " pounds");
System.out.println("Truck speed (" + timeSec + " sec) = " +
((Truck)vehicle).getSpeedMph(timeSec) + " mph");
}
}

Como es de esperar, la clase de prueba produce los mismos valores:

Para simplificar el código de prueba, podemos eliminar la conversión y escribir lo


siguiente en su lugar:

public static void main(String... arg) {


double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
Car car = new Car(4, vehicleWeightPounds, engineHorsePower);
System.out.println("Passengers count=" + car.getPassengersCount());
System.out.println("Car speed (" + timeSec + " sec) = " +
car.getSpeedMph(timeSec) + " mph");
Truck truck =
new Truck(3300, vehicleWeightPounds, engineHorsePower);
System.out.println("Payload=" + truck.getPayload() + " pounds");
System.out.println("Truck speed (" + timeSec + " sec) = " +
truck.getSpeedMph(timeSec) + " mph");
}

Los valores de velocidad producidos por este código siguen siendo los mismos.

Sin embargo, hay una forma aún más simple de lograr el mismo efecto. Podemos
agregar el método a la clase base getMaxWeightPounds()y a cada una de las
subclases. La clase Car ahora se verá de la siguiente manera:

public class Car extends Vehicle {


private int passengersCount, weightPounds;
public Car(int passengersCount, int weightPounds, int horsePower){
super(weightPounds, horsePower);
this.passengersCount = passengersCount;

pág. 63
this.weightPounds = weightPounds;
}
public int getPassengersCount() {
return this.passengersCount;
}
public int getMaxWeightPounds() {
return this.weightPounds + this.passengersCount * 250;
}
}

Y Truck así es como se ve la nueva versión de la clase ahora:

public class Truck extends Vehicle {


private int payload, weightPounds;
public Truck(int payloadPounds, int weightPounds, int horsePower) {
super(weightPounds, horsePower);
this.payload = payloadPounds;
this.weightPounds = weightPounds;
}
public int getPayload() { return this.payload; }
public int getMaxWeightPounds() {
return this.weightPounds + this.payload;
}
}

También necesitamos agregar el método getMaxWeightPounds() a la clase base para que


pueda usarse para los cálculos de velocidad:

public abstract class Vehicle {


private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public abstract int getMaxWeightPounds();
public double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746 *
timeSec * 32.17 / getMaxWeightPounds();
return Math.round(Math.sqrt(v) * 0.68);
}
}

Agregar un método abstracto getMaxWeightPounds(), a la clase Vehicle hace que la clase


sea abstracta. Esto tiene un efecto secundario positivo: impone la implementación
del método getMaxWeightPounds() en cada subclase. De lo contrario, una subclase no se
puede instanciar y también debe declararse abstracta.

La clase de prueba permanece igual y produce los mismos resultados:

pág. 64
Pero, para ser honesto, lo hicimos solo para demostrar una posible forma de usar un
método abstracto y una clase. De hecho, una solución aún más simple sería pasar el
peso máximo como parámetro al constructor de la clase base Vehicle . Las clases
resultantes se verán así:

public class Car extends Vehicle {


private int passengersCount;
public Car(int passengersCount, int weightPounds, int horsepower){
super(weightPounds + passengersCount * 250, horsePower);
this.passengersCount = passengersCount;
}
public int getPassengersCount() {
return this.passengersCount; }
}

Agregamos el peso de los pasajeros al valor que le pasamos al constructor de la


superclase; Este es el único cambio en esta subclase. Aquí hay un cambio similar en la
clase Truck:

public class Truck extends Vehicle {


private int payload;
public Truck(int payloadPounds, int weightPounds, int horsePower) {
super(weightPounds + payloadPounds, horsePower);
this.payload = payloadPounds;
}
public int getPayload() { return this.payload; }
}

La clase base Vehicle sigue siendo la misma que la original:

public class Vehicle {


private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public double getSpeedMph(double timeSec){
double v = 2.0 * this.horsePower * 746;
v = v * timeSec * 32.174 / this.weightPounds;
return Math.round(Math.sqrt(v) * 0.68);
}
}

La clase de prueba no cambia y produce los mismos resultados:

Esta última versión - pasando el peso máximo para el constructor de la clase


base - ahora será el punto de partida para más manifestaciones de código.

pág. 65
La agregación hace que el diseño
sea más extensible
En el ejemplo anterior, el modelo de velocidad se implementó en el método
getSpeedMph(double timeSec) de la clase Vehicle . Si necesitamos usar un modelo de
velocidad diferente (que incluye más parámetros de entrada y está más ajustado a
ciertas condiciones de manejo, por ejemplo), necesitaríamos cambiar la clase Vehicle o
crear una nueva subclase para anular el método. En el caso de que necesitemos
experimentar con docenas o incluso cientos de modelos diferentes, este enfoque se
vuelve insostenible.

Además, en la vida real, el modelado basado en el aprendizaje automático y otras


técnicas avanzadas se vuelve tan complicado y especializado, que es bastante común
que el modelado de la aceleración del automóvil sea realizado por un equipo diferente,
no por el equipo que ensambla el modelo del vehículo.

Para evitar la proliferación de subclases y conflictos de combinación de código entre los


fabricantes de vehículos y los desarrolladores de modelos de velocidad, podemos crear
un diseño más extensible mediante la agregación.

La agregación es un principio OOD para implementar la funcionalidad necesaria utilizando el


comportamiento de clases que no forman parte de la jerarquía de herencia. Ese comportamiento
puede existir independientemente de la funcionalidad agregada.

Podemos encapsular los cálculos de velocidad dentro de la clase SpeedModel en el


método getSpeedMph(double timeSec):

public class SpeedModel{


private Properties conditions;
public SpeedModel(Properties drivingConditions){
this.drivingConditions = drivingConditions;
}
public double getSpeedMph(double timeSec, int weightPounds,
int horsePower){
String road =
drivingConditions.getProperty("roadCondition","Dry");
String tire =
drivingConditions.getProperty("tireCondition","New");
double v = 2.0 * horsePower * 746 * timeSec *
32.17 / weightPounds;
return Math.round(Math.sqrt(v)*0.68)-road.equals("Dry")? 2 : 5)
-(tire.equals("New")? 0 : 5);
}
}

Se puede crear un objeto de esta clase y luego establecerlo como el valor del campo
Vehicle de clase:

pág. 66
public class Vehicle {
private SpeedModel speedModel;
private int weightPounds, horsePower;
public Vehicle(int weightPounds, int horsePower) {
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public void setSpeedModel(SpeedModel speedModel){
this.speedModel = speedModel;
}
public double getSpeedMph(double timeSec){
return this.speedModel.getSpeedMph(timeSec,
this.weightPounds, this.horsePower);
}
}

La clase de prueba cambia de la siguiente manera:

public static void main(String... arg) {


double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Properties drivingConditions = new Properties();
drivingConditions.put("roadCondition", "Wet");
drivingConditions.put("tireCondition", "New");
SpeedModel speedModel = new SpeedModel(drivingConditions);
Car car = new Car(4, vehicleWeight, horsePower);
car.setSpeedModel(speedModel);
System.out.println("Car speed (" + timeSec + " sec) = " +
car.getSpeedMph(timeSec) + " mph");
}

El resultado del código anterior es el siguiente:

Aislamos la funcionalidad de cálculo de velocidad en una clase separada y ahora


podemos modificarla o extenderla sin cambiar ninguna clase de la Vehicle jerarquía de
herencia. Así es como el principio de diseño de agregación le permite cambiar el
comportamiento sin cambiar la implementación.

En la próxima receta, le mostraremos cómo el concepto OOP de interfaz desbloquea


más poder de agregación y polimorfismo, haciendo que el diseño sea más simple e
incluso más expresivo.

Codificación a una interfaz


En esta receta, aprenderá el último de los conceptos de OOP, llamado interfaz, y
practicará el uso de la agregación y el polimorfismo, así como las clases internas y la
herencia.

pág. 67
Prepararse
Una interfaz define las firmas de los métodos que uno puede esperar ver en la clase que
implementa la interfaz. Es la cara pública de la funcionalidad accesible para un cliente
y, por lo tanto, a menudo se la denomina Interfaz de programa de
aplicación ( API ). Admite polimorfismo y agregación, y facilita un diseño más flexible
y extensible.

Una interfaz es implícitamente abstracta, lo que significa que no se puede instanciar. No


se puede crear ningún objeto basado solo en una interfaz, sin implementarlo. Se utiliza
para contener métodos abstractos (sin cuerpo) solamente. Pero desde Java 8, es posible
agregar métodos predeterminados y privados a una interfaz, que es la capacidad que
discutiremos en las siguientes recetas.

Cada interfaz puede extender varias otras interfaces y, de forma similar a la herencia
de clases, heredar todos los métodos predeterminados y abstractos de las interfaces
extendidas. Los miembros estáticos no se pueden heredar porque pertenecen a una
interfaz específica.

Cómo hacerlo...
1. Cree interfaces que describan la API:

public interface SpeedModel {


double getSpeedMph(double timeSec, int weightPounds,
int horsePower);
}
public interface Vehicle {
void setSpeedModel(SpeedModel speedModel);
double getSpeedMph(double timeSec);
}
public interface Car extends Vehicle {
int getPassengersCount();
}
public interface Truck extends Vehicle {
int getPayloadPounds();
}

2. Use fábricas, que son clases que generan objetos que implementan ciertas
interfaces. Una fábrica oculta del código del cliente los detalles de la implementación, por
lo que el cliente solo trata con una interfaz. Es especialmente útil cuando la creación de una
instancia requiere un proceso complejo y / o una duplicación significativa de código. En
nuestro caso, tiene sentido tener una clase FactoryVehicle que crea objetos de clases que
implementan la Vehicle, Caro Truck interfaz. También crearemos
la clase FactorySpeedModel , que genera objetos de una clase que implementa
la interfaz SpeedModel. Tal API nos permite escribir el siguiente código:

pág. 68
public static void main(String... arg) {
double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Properties drivingConditions = new Properties();
drivingConditions.put("roadCondition", "Wet");
drivingConditions.put("tireCondition", "New");
SpeedModel speedModel = FactorySpeedModel.
generateSpeedModel(drivingConditions);
Car car = FactoryVehicle.
buildCar(4, vehicleWeight, horsePower);
car.setSpeedModel(speedModel);
System.out.println("Car speed (" + timeSec + " sec) = "
+ car.getSpeedMph(timeSec) + " mph");
}

3. Observe que el comportamiento del código es el mismo que en los ejemplos


anteriores:

Sin embargo, el diseño es mucho más extensible.

Cómo funciona...
Ya hemos visto una posible implementación de la interfaz SpeedModel. Aquí hay otra
forma de hacerlo agregando el objeto del tipo SpeedModel dentro de la clase
FactorySpeedModel:

public class FactorySpeedModel {


public static SpeedModel generateSpeedModel(
Properties drivingConditions){
//if drivingConditions includes "roadCondition"="Wet"
return new SpeedModelWet(...);
//if drivingConditions includes "roadCondition"="Dry"
return new SpeedModelDry(...);
}
private class SpeedModelWet implements SpeedModel{
public double getSpeedMph(double timeSec, int weightPounds,
int horsePower){
//method code goes here
}
}
private class SpeedModelDry implements SpeedModel{
public double getSpeedMph(double timeSec, int weightPounds,
int horsePower){
//method code goes here
}

pág. 69
}
}

Ponemos comentarios como pseudocódigo, y el ... símbolo en lugar del código real,
por brevedad.

Como puede ver, la clase de fábrica puede ocultar muchas clases privadas diferentes,
cada una con un modelo especializado para condiciones particulares de manejo. Cada
modelo produce un resultado diferente.

Una implementación de la clase FactoryVehicle puede verse así:

public class FactoryVehicle {


public static Car buildCar(int passengersCount,
int weightPounds, int horsePower){
return new CarImpl(passengersCount, weightPounds,horsePower);
}
public static Truck buildTruck(int payloadPounds,
int weightPounds, int horsePower){
return new TruckImpl(payloadPounds, weightPounds,horsePower);
}
}

La CarImpl clase privada anidada puede verse de la siguiente manera dentro de la


clase FactoryVehicle:

private static class CarImpl extends VehicleImpl implements Car {


private int passengersCount;
private CarImpl(int passengersCount, int weightPounds,
int horsePower){
super(weightPounds + passengersCount * 250, horsePower);
this.passengersCount = passengersCount;
}
public int getPassengersCount() {
return this.passengersCount;
}
}

Del mismo modo, la TruckImpl clase puede ser una clase privada anidada de la clase
FactoryImpl:

private static class TruckImpl extends VehicleImpl implements Truck {


private int payloadPounds;
private TruckImpl(int payloadPounds, int weightPounds,
int horsePower){
super(weightPounds+payloadPounds, horsePower);
this.payloadPounds = payloadPounds;
}
public int getPayloadPounds(){ return payloadPounds; }
}

pág. 70
También podemos colocar la clase VehicleImpl como una clase interna privada de
la clase FactoryVehicle, para que las clases CarImpl y TruckImpl puedan acceder a ella,
pero no a ninguna otra clase fuera de FactoryVehicle:

private static abstract class VehicleImpl implements Vehicle {


private SpeedModel speedModel;
private int weightPounds, horsePower;
private VehicleImpl(int weightPounds, int horsePower){
this.weightPounds = weightPounds;
this.horsePower = horsePower;
}
public void setSpeedModel(SpeedModel speedModel){
this.speedModel = speedModel;
}
public double getSpeedMph(double timeSec){
return this.speedModel.getSpeedMph(timeSec, weightPounds,
horsePower);
}
}

Como puede ver, una interfaz describe cómo invocar el comportamiento de un objeto,
mientras que las fábricas pueden generar diferentes implementaciones para diferentes
solicitudes sin cambiar el código de la aplicación cliente.

Hay más...
Tratemos de modelar una cabina de tripulación : un camión con múltiples asientos para
pasajeros que combina las propiedades de un automóvil y un camión. Java no permite
múltiples herencias. Este es otro caso en el que una interfaz viene al rescate.

La clase CrewCab puede verse así:

public class CrewCab extends VehicleImpl implements Car, Truck {


private int payloadPounds;
private int passengersCount;
private CrewCabImpl(int passengersCount, int payloadPounds,
int weightPounds, int horsePower) {
super(weightPounds + payloadPounds + passengersCount * 250,
horsePower);
this.payloadPounds = payloadPounds;
this. passengersCount = passengersCount;
}
public int getPayloadPounds(){ return payloadPounds; }
public int getPassengersCount() {
return this.passengersCount;
}
}

Esta clase implementa dos interfaces -Car y Truck- y pasa el peso combinado del
vehículo, la carga útil, y los pasajeros con su equipaje a la constructor de la clase base.

pág. 71
También podemos agregar el siguiente método a FactoryVehicle:

public static Vehicle buildCrewCab(int passengersCount,


int payload, int weightPounds, int horsePower){
return new CrewCabImpl(passengersCount, payload,
weightPounds, horsePower);
}

La doble naturaleza del objeto CrewCab se puede demostrar en la siguiente prueba:

public static void main(String... arg) {


double timeSec = 10.0;
int horsePower = 246;
int vehicleWeight = 4000;
Properties drivingConditions = new Properties();
drivingConditions.put("roadCondition", "Wet");
drivingConditions.put("tireCondition", "New");
SpeedModel speedModel =
FactorySpeedModel.generateSpeedModel(drivingConditions);
Vehicle vehicle = FactoryVehicle.
buildCrewCab(4, 3300, vehicleWeight, horsePower);
vehicle.setSpeedModel(speedModel);
System.out.println("Payload = " +
((Truck)vehicle).getPayloadPounds()) + " pounds");
System.out.println("Passengers count = " +
((Car)vehicle).getPassengersCount());
System.out.println("Crew cab speed (" + timeSec + " sec) = "
+ vehicle.getSpeedMph(timeSec) + " mph");
}

Como puede ver, podemos lanzar el objeto de la CrewCubclase a cada una de las
interfaces que implementa. Si ejecutamos este programa, los resultados serán los
siguientes:

Crear interfaces con métodos


predeterminados y estáticos
En esta receta, aprenderá sobre dos nuevas características que se introdujeron por
primera vez en Java 8: los métodos predeterminados y estáticos en una interfaz.

Prepararse
pág. 72
Un método predeterminado en una interfaz nos permite agregar una nueva firma de
método sin cambiar las clases que han implementado esta interfaz antes de agregar una
nueva firma de método. El método se llama predeterminado porque proporciona
funcionalidad en caso de que la clase no implemente este método. Sin embargo, si la
clase lo implementa, la implementación predeterminada de la interfaz ignora y anula la
implementación de la clase.

Un método estático en una interfaz puede proporcionar funcionalidad de la misma


manera que un método estático en una clase. De manera similar a un método estático
clase, que puede ser llamado sin instanciación de la clase, un método estático interfaz
también puede ser llamado usando un dot-operador aplica a la
interfaz, SomeInterface.someStaticMethod().

La clase que implementa esta interfaz no puede anular el método estático de una
interfaz y no puede ocultar ningún método estático de ninguna clase, incluida la clase
que implementa esta interfaz.

Por ejemplo, agreguemos alguna funcionalidad al sistema que ya hemos usado en


nuestros ejemplos. Hasta ahora, hemos creado una increíble pieza de software que
calcula la velocidad de un vehículo. Si el sistema se vuelve popular (como debería), nos
gustaría que fuera más amigable con los lectores que prefieren un sistema métrico de
unidades, en lugar de las millas y libras que hemos usado en nuestros cálculos de
velocidad. Para abordar esta necesidad después de que nuestro software de cálculo de
velocidad se haya popularizado, hemos decidido agregar más métodos a
las interfaces Cary Truck, pero no queremos romper las implementaciones existentes.

El método de interfaz predeterminado se introdujo exactamente para tal


situación. Gracias a él, podemos lanzar una nueva versión de las
interfaces Cary Truck sin la necesidad de coordinar la liberación con la correspondiente
modificación de las implementaciones existentes, es decir,
las clases CarImpl, TruckImply FactoryVehicle .

Cómo hacerlo...
Como ejemplo, cambiaremos la interfaz Truck. La interfaz Car se puede modificar de
manera similar:

1. Mejore la interfaz Truck agregando el método getPayloadKg(), que devuelve la carga


útil del camión en kilogramos. Puede hacerlo sin forzar un cambio en la clase
TruckImpl que implementa la interfaz Truck, agregando un nuevo método
predeterminado a la interfaz Truck:

public interface Truck extends Vehicle {


int getPayloadPounds();
default int getPayloadKg(){

pág. 73
return (int) Math.round(0.454 * getPayloadPounds());
}
}

Observe cómo el nuevo método getPayloadKg() usa el método


getPayloadPounds() existente como si este último también se implementara dentro de
la interfaz, aunque, de hecho, se implementa en una clase que implementa la interfaz
Truck. La magia ocurre durante el tiempo de ejecución cuando este método se vincula
dinámicamente a la instancia de la clase que implementa esta interfaz.

No pudimos hacer que el método getPayloadKg() sea estático porque no podría acceder
al método getPayloadPounds() no estático , y debemos usar la palabra default clave
porque solo el método predeterminado o estático de una interfaz puede tener un
cuerpo.

2. Escriba el código del cliente que usa el nuevo método:

public static void main(String... arg) {


Truck truck = FactoryVehicle.buildTruck(3300, 4000, 246);
System.out.println("Payload in pounds: " +
truck.getPayloadPounds());
System.out.println("Payload in kg: " +
truck.getPayloadKg());
}

3. Ejecute el programa anterior y compruebe la salida:

4. Tenga en cuenta que el nuevo método funciona incluso sin cambiar la clase que lo
implementó.

5. Cuando decida mejorar la implementación de la clase TruckImpl, puede hacerlo


agregando el método correspondiente, por ejemplo:

class TruckImpl extends VehicleImpl implements Truck {


private int payloadPounds;
private TruckImpl(int payloadPounds, int weightPounds,
int horsePower) {
super(weightPounds + payloadPounds, horsePower);
this.payloadPounds = payloadPounds;
}
public int getPayloadPounds(){ return payloadPounds; }
public int getPayloadKg(){ return -2; }
}

Hemos implementado el método getPyloadKg() return -2 para que sea obvio qué
implementación se utiliza.

pág. 74
6. Ejecute el mismo programa de demostración. Los resultados serán los siguientes:

Como puede ver, esta vez, TruckImpl se utilizó la implementación del método en
la clase. Se ha anulado la implementación predeterminada en la interfaz Truck.

7. Mejore la interfaz Truck con la capacidad de ingresar la carga útil en kilogramos sin
cambiar la implementación FactoryVehicle y la interfaz Truck . Además, no queremos
agregar un método setter. Con todas estas limitaciones, nuestro único recurso es
agregar convertKgToPounds(int kgs)a la interfaz Truck, y tiene que ser así, static ya que
vamos a usarlo antes de Truck construir el objeto que implementa la interfaz:

public interface Truck extends Vehicle {


int getPayloadPounds();
default int getPayloadKg(){
return (int) Math.round(0.454 * getPayloadPounds());
}
static int convertKgToPounds(int kgs){
return (int) Math.round(2.205 * kgs);
}
}

Cómo funciona...
Aquellos que prefieren el sistema métrico de unidades ahora pueden aprovechar el
nuevo método:

public static void main(String... arg) {


int horsePower = 246;
int payload = Truck.convertKgToPounds(1500);
int vehicleWeight = Truck.convertKgToPounds(1800);
Truck truck = FactoryVehicle.
buildTruck(payload, vehicleWeight, horsePower);
System.out.println("Payload in pounds: " +
truck.getPayloadPounds());
int kg = truck.getPayloadKg();
System.out.println("Payload converted to kg: " + kg);
System.out.println("Payload converted back to pounds: " +
Truck.convertKgToPounds(kg));
}

Los resultados serán los siguientes:

pág. 75
El valor de 1,502 está cerca de los 1,500 originales, mientras que 3,308 está cerca de
3,312. La diferencia es causada por el error de una aproximación durante la conversión.

Crear interfaces con métodos


privados
En esta receta, aprenderá acerca de una nueva característica que se introdujo en Java 9,
el método de interfaz privada, que es de dos tipos: estático y no estático.

Prepararse
Un método de interfaz privada debe tener una implementación (un cuerpo con un
código). Un método de interfaz privada no utilizado por otros métodos de la misma
interfaz no tiene sentido. El propósito de un método privado es contener la
funcionalidad que es común entre dos o más métodos con un cuerpo en la misma
interfaz o aislar una sección de código en un método separado para una mejor
estructura y legibilidad. Un método de interfaz privada no puede ser anulado, ni por un
método de ninguna otra interfaz, ni por un método en una clase que implemente la
interfaz.

Solo se puede acceder a un método de interfaz privada no estático mediante métodos


no estáticos de la misma interfaz. Se puede acceder a un método de interfaz privada
estática mediante métodos no estáticos y estáticos de la misma interfaz.

Cómo hacerlo...
1. Agregue la implementación del método getWeightKg(int pounds):

public interface Truck extends Vehicle {


int getPayloadPounds();
default int getPayloadKg(){
return (int) Math.round(0.454 * getPayloadPounds());
}
static int convertKgToPounds(int kilograms){
return (int) Math.round(2.205 * kilograms);
}
default int getWeightKg(int pounds){
return (int) Math.round(0.454 * pounds);
}
}

2. Elimine el código redundante utilizando el método de interfaz privada:

pág. 76
public interface Truck extends Vehicle {
int getPayloadPounds();
default int getPayloadKg(int pounds){
return convertPoundsToKg(pounds);
}
static int convertKgToPounds(int kilograms){
return (int) Math.round(2.205 * kilograms);
}
default int getWeightKg(int pounds){
return convertPoundsToKg(pounds);
}
private int convertPoundsToKg(int pounds){
return (int) Math.round(0.454 * pounds);
}
}

Cómo funciona...
El siguiente código demuestra la nueva adición:

public static void main(String... arg) {


int horsePower = 246;
int payload = Truck.convertKgToPounds(1500);
int vehicleWeight = Truck.convertKgToPounds(1800);
Truck truck =
FactoryVehicle.buildTruck(payload, vehicleWeight, horsePower);
System.out.println("Weight in pounds: " + vehicleWeight);
int kg = truck.getWeightKg(vehicleWeight);
System.out.println("Weight converted to kg: " + kg);
System.out.println("Weight converted back to pounds: " +
Truck.convertKgToPounds(kg));
}

Los resultados de la prueba no cambian:

Hay más...
Con el método getWeightKg(int pounds) que acepta el parámetro de entrada, el nombre
del método puede ser engañoso porque no captura la unidad de peso del parámetro de
entrada. Podríamos intentar nombrarlo, getWeightKgFromPounds(int pounds) pero no
hace que el método funcione más claramente. Después de darse cuenta de ello,
decidimos hacer que el convertPoundsToKg(int pounds) público y el método para
eliminar el método en absoluto . Dado que el método no requiere acceso a los campos
de objeto, también puede ser estático:

pág. 77
public interface Truck extends Vehicle {
int getPayloadPounds();
default int getPayloadKg(int pounds){
return convertPoundsToKg(pounds);
}
static int convertKgToPounds(int kilograms){
return (int) Math.round(2.205 * kilograms);
}
static int convertPoundsToKg(int pounds){
return (int) Math.round(0.454 * pounds);
}
}

Los fanáticos del sistema métrico aún pueden convertir libras en kilogramos y
viceversa. Además, dado que ambos métodos de conversión son estáticos, no
necesitamos crear una instancia de la clase que implemente la interfaz Truck para
realizar la conversión:

public static void main(String... arg) {


int payload = Truck.convertKgToPounds(1500);
int vehicleWeight = Truck.convertKgToPounds(1800);
System.out.println("Weight in pounds: " + vehicleWeight);
int kg = Truck.convertPoundsToKg(vehicleWeight);
System.out.println("Weight converted to kg: " + kg);
System.out.println("Weight converted back to pounds: " +
Truck.convertKgToPounds(kg));
}

Los resultados no cambian:

Una mejor manera de trabajar


con nulos usando Opcional
En esta receta, aprenderá a usar la clase java.util.Optional para representar valores
opcionales en lugar de usar referencias null. Se introdujo en Java 8 y mejora aún más
en Java 9 -donde tres más métodos eran
añadido- or(), ifPresentOrElse()y stream(). Los demostraremos todos.

Prepararse
La clase Optional es un contenedor alrededor de un valor, que puede ser null o un valor
de cualquier tipo. Estaba destinado a ayudar a evitar lo

pág. 78
temido NullPointerException. Pero, hasta ahora, la introducción de Optional ayudó a
lograrlo solo hasta cierto punto y principalmente en el área de transmisiones y
programación funcional.

La visión que motivó la creación de la clase Optional fue llamar al método


isPresent() en un objeto Optional y luego aplicar el método get() (para obtener el
valor contenido) solo cuando el método isPresent()
regrese true. Desafortunadamente, cuando uno no puede garantizar que la referencia
al objeto Optional en sí no lo es null, uno debe verificarlo para
evitarlo NullPointerException. Si es así, entonces el valor de usar Optional disminuye,
porque con una cantidad aún menor de escritura de código podríamos verificar null el
valor en sí mismo y evitar envolvernos en Optional absoluto. Escribamos el código que
ilustra de lo que hemos estado hablando.

Supongamos que nos gustaría escribir un método que verifique el resultado de la lotería
y, si gana el boleto que compró con su amigo, calcula su participación del 50%. La forma
tradicional de hacerlo sería:

void checkResultInt(int lotteryPrize){


if(lotteryPrize <= 0){
System.out.println("We've lost again...");
} else {
System.out.println("We've won! Your half is " +
Math.round(((double)lotteryPrize)/2) + "!");
}
}

Pero, para demostrar cómo usar Optional, asumiremos que el resultado es del tipo
Integer. Luego, también debemos verificar nullsi no estamos seguros de que el valor
pasado no puede ser null:

void checkResultInt(Integer lotteryPrize){


if(lotteryPrize == null || lotteryPrize <= 0){
System.out.println("We've lost again...");
} else {
System.out.println("We've won! Your half is " +
Math.round(((double)lotteryPrize)/2) + "!");
}
}

Usar la clase Optional no ayuda a evitar la verificación null. Incluso


requiere isPresent()que se agregue una verificación adicional, para que podamos
evitar NullPointerException al obtener el valor:

void checkResultOpt(Optional<Integer> lotteryPrize){


if(lotteryPrize == null || !lotteryPrize.isPresent()
|| lotteryPrize.get() <= 0){
System.out.println("We lost again...");
} else {
System.out.println("We've won! Your half is " +
Math.round(((double)lotteryPrize.get())/2) + "!");

pág. 79
}
}

Aparentemente, el uso anterior de Optional no ayuda a mejorar el código ni a facilitar


la codificación. El uso Optional de expresiones Lambda y canalizaciones de flujo tiene
más potencial porque el objeto Optional proporciona métodos que pueden invocarse a
través del operador de punto y pueden conectarse al código de procesamiento de estilo
fluido.

Cómo hacerlo...
1. Cree un objeto Optional utilizando cualquiera de los métodos que se han
demostrado, de la siguiente manera:

Optional<Integer> prize1 = Optional.empty();


System.out.println(prize1.isPresent()); //prints: false
System.out.println(prize1); //prints: Optional.empty

Optional<Integer> prize2 = Optional.of(1000000);


System.out.println(prize2.isPresent()); //prints: true
System.out.println(prize2); //prints: Optional[1000000]

//Optional<Integer> prize = Optional.of(null);


//NullPointerException

Optional<Integer> prize3 = Optional.ofNullable(null);


System.out.println(prize3.isPresent()); //prints: false
System.out.println(prize3); //prints: Optional.empty

Observe que un valor null puede ajustarse dentro de un objeto Optional utilizando
el método ofNullable().

2. Es posible comparar dos objetos Optional utilizando el método equals(), que los
compara por valor:

Optional<Integer> prize1 = Optional.empty();


System.out.println(prize1.equals(prize1)); //prints: true

Optional<Integer> prize2 = Optional.of(1000000);


System.out.println(prize1.equals(prize2)); //prints: false

Optional<Integer> prize3 = Optional.ofNullable(null);


System.out.println(prize1.equals(prize3)); //prints: true

Optional<Integer> prize4 = Optional.of(1000000);


System.out.println(prize2.equals(prize4)); //prints: true
System.out.println(prize2 == prize4); //prints: false

Optional<Integer> prize5 = Optional.of(10);


System.out.println(prize2.equals(prize5)); //prints: false

pág. 80
Optional<String> congrats1 = Optional.empty();
System.out.println(prize1.equals(congrats1));//prints: true

Optional<String> congrats2 = Optional.of("Happy for you!");


System.out.println(prize1.equals(congrats2));//prints: false

Tenga en cuenta que un objeto Optional vacío es igual a un objeto que envuelve
el valor null (los objetos prize1y prize3 en el código anterior). Los objetos prize2
y prize4 en el código anterior son iguales porque envuelven el mismo valor, aunque
son objetos diferentes y las referencias no coinciden ( prize2 != prize4). Además,
observe que los objetos vacíos que envuelven diferentes tipos son iguales
( prize1.equals(congrats1)), lo que significa que el método equals() de la clase
Optional no compara el tipo de valor.

3. Utilice el método or(Suppier<Optional<T>> supplier) de la clase Optional para


devolver de manera confiable un valor no nulo del objeto Optional. Si el objeto está
vacío y contiene null, devuelve otro valor contenido en el objeto Optional que fue
producido por la función Supplier proporcionada .

Por ejemplo, si el objeto Optional<Integer> lotteryPrize puede contener un valor null,


la siguiente construcción devolverá cero cada vez que null se encuentre el valor:

int prize = lotteryPrize.or(() -> Optional.of(0)).get();

3. Utilice el método ifPresent(Consumer<T> consumer) para ignorar el valor null y


procesar el valor no nulo utilizando la función Consumer<T> proporcionada . Por ejemplo,
aquí está el método processIfPresent(Optional<Integer>) , que procesa el objeto
Optional<Integer> lotteryPrize:

void processIfPresent(Optional<Integer> lotteryPrize){


lotteryPrize.ifPresent(prize -> {
if(prize <= 0){
System.out.println("We've lost again...");
} else {
System.out.println("We've won! Your half is " +
Math.round(((double)prize)/2) + "!");
}
});

Podemos simplificar el código anterior creando el método checkResultAndShare(int


prize):

void checkResultAndShare(int prize){


if(prize <= 0){
System.out.println("We've lost again...");
} else {
System.out.println("We've won! Your half is " +
Math.round(((double)prize)/2) + "!");
}
}

pág. 81
Ahora, el método processIfPresent() se ve mucho más simple:

void processIfPresent(Optional<Integer> lotteryPrize){


lotteryPrize.ifPresent(prize -> checkResultAndShare(prize));
}

4. Si no desea ignorar el valor null y procesarlo también, puede usar el método


ifPresentOrElse(Consumer<T> consumer, Runnable processEmpty) para aplicar la función
a un valor no nulo y usar la interfaz funcional para procesar el valor:

void processIfPresentOrElse(Optional<Integer> lotteryPrize){


Consumer<Integer> weWon =
prize -> checkResultAndShare(prize);
Runnable weLost =
() -> System.out.println("We've lost again...");
lotteryPrize.ifPresentOrElse(weWon, weLost);
}

Como puede ver, hemos reutilizado el método que acabamos de


crear. checkResultAndShare(int prize)

5. El uso del método orElseGet(Supplier<T> supplier) nos permite reemplazar


un valor null vacío o (contenido en el objeto) con el valor producido por
la función proporcionada :

void processOrGet(Optional<Integer> lotteryPrize){


int prize = lotteryPrize.orElseGet(() -> 42);
lotteryPrize.ifPresentOrElse(p -> checkResultAndShare(p),
() -> System.out.println("Better " + prize
+ " than nothing..."));
}

6. Use el método orElseThrow() si necesita lanzar una excepción en caso de que


un objeto esté vacío o contenga un valor:

void processOrThrow(Optional<Integer> lotteryPrize){


int prize = lotteryPrize.orElseThrow();
checkResultAndShare(prize);
}

Una versión sobrecargada del método orElseThrow() nos permite especificar una
excepción y el mensaje que le gustaría lanzar cuando el valor contenido en el objeto
Optional es null:

void processOrThrow(Optional<Integer> lotteryPrize){


int prize = lotteryPrize.orElseThrow(() ->
new RuntimeException("We've lost again..."));
checkResultAndShare(prize);
}
7. Usa los métodos filter(), map()y flatMap() para procesar objetos Optional en una
corriente:

pág. 82
void useFilter(List<Optional<Integer>> list){
list.stream().filter(opt -> opt.isPresent())
.forEach(opt -> checkResultAndShare(opt.get()));
}
void useMap(List<Optional<Integer>> list){
list.stream().map(opt -> opt.or(() -> Optional.of(0)))
.forEach(opt -> checkResultAndShare(opt.get()));
}
void useFlatMap(List<Optional<Integer>> list){
list.stream().flatMap(opt ->
List.of(opt.or(()->Optional.of(0))).stream())
.forEach(opt -> checkResultAndShare(opt.get()));
}

En el código anterior, el método useFilter() procesa solo aquellos elementos de flujo


que tienen valores no nulos. El método useMap() procesa todos los elementos de flujo
pero reemplaza objetos Optional sin ningún valor o ajustando el valor null con
un objeto Optional que envuelve cero. Utiliza el último método flatMap(), que requiere
devolver una secuencia de la función proporcionada. Nuestro ejemplo es bastante inútil
a este respecto porque la función que pasamos como parámetro flatMap(), produce
una secuencia de un objeto, por lo que usar map()(como en el
método useMap() anterior ) es una mejor solución aquí. Solo hicimos esto para
demostrar cómo flatMap()se puede conectar el método a la tubería de transmisión.

Cómo funciona...
El siguiente código demuestra la funcionalidad de la clase Optional
descrita . El método useFlatMap() acepta una lista de objetos Optional, crea una
secuencia y procesa cada elemento emitido:

void useFlatMap(List<Optional<Integer>> list){


Function<Optional<Integer>,
Stream<Optional<Integer>>> tryUntilWin = opt -> {
List<Optional<Integer>> opts = new ArrayList<>();
if(opt.isPresent()){
opts.add(opt);
} else {
int prize = 0;
while(prize == 0){
double d = Math.random() - 0.8;
prize = d > 0 ? (int)(1000000 * d) : 0;
opts.add(Optional.of(prize));
}
}
return opts.stream();
};
list.stream().flatMap(tryUntilWin)
.forEach(opt -> checkResultAndShare(opt.get()));
}

Cada elemento de la lista original ingresa primero el método flatMap() como entrada
en la función tryUntilWin. Esta función primero verifica si el valor del objeto Optional

pág. 83
está presente. En caso afirmativo, el objeto Optional se emite como un elemento único
de una secuencia y se procesa mediante el método checkResultAndShare(). Pero si
la función tryUntilWin determina que no hay valor en el objeto Optional o el valor
es null, genera un número doble aleatorio en el rango entre -0.8y 0.2. Si el valor es
negativo, se agrega un objeto Optional a la lista resultante con un valor de cero y se
genera un nuevo número aleatorio. Pero si el número generado es positivo, se utiliza
para el cálculo del valor del premio, que se agrega a la lista resultante que se encuentra
dentro de un objeto Optional. La lista resultante de objetos Optional se devuelve como
una secuencia, y cada elemento de la secuencia es procesado por el método
checkResultAndShare().

Ahora, ejecutemos el método anterior para la siguiente lista:

List<Optional<Integer>> list = List.of(Optional.empty(),


Optional.ofNullable(null),
Optional.of(100000));
useFlatMap(list);

Los resultados serán los siguientes:

Como puede ver, cuando Optional.empty()se procesó el primer elemento de la lista ,


la función logró obtener un valor positivo del tercer intento. El segundo objeto causó
dos intentos hasta que la función tuvo éxito. El último objeto pasó con éxito y te
otorgó a ti y a tu amigo 50,000 cada
uno. tryUntilWinprizeOptional.ofNullable(null) tryUntilWin

Hay más...
Un objeto de la Optional clase no es serializable y, por lo tanto, no puede usarse como
un campo de un objeto. Esta es otra indicación de que el diseñador de la Optional clase
pretende ser utilizado en un proceso sin estado.

pág. 84
Hace que la canalización de procesamiento de flujo sea más compacta y expresiva,
centrándose en los valores reales en lugar de verificar si hay elementos vacíos en el
flujo.

Usando la clase de utilidad


Objetos
En esta receta, aprenderá cómo la java.util.Objects clase de utilidad permite un mejor
procesamiento de los objetos relacionados con la funcionalidad relacionada con la
comparación de objetos, calculando un valor hash y comprobando null. Llevó mucho
tiempo, ya que los programadores escribieron el mismo código para verificar un
objeto nulluna y otra vez.

Prepararse
La clase Objects tiene solo 17 métodos, todos los cuales son estáticos. Para una mejor
visión general, los hemos organizado en siete grupos:

• compare(): Un método compara dos objetos usando el proporcionado Comparator


• toString(): Dos métodos que convierten un Object a un valor String.
• checkIndex(): Tres métodos que nos permiten verificar si el índice y la longitud de
una colección o una matriz son compatibles
• requireNonNull(): Cinco métodos arrojan una excepción si el objeto proporcionado
esnull
• hash(), Dos métodos que calculan un valor hash para un solo objeto o una matriz
de objetoshashCode():
• isNull(), : Dos métodos que envuelven las expresiones o nonNull() obj
== nullobj != null
• equals(), deepEquals(): Dos métodos que comparan dos objetos que pueden ser
nulos o matrices

Vamos a escribir código que use estos métodos en la secuencia anterior.

Cómo hacerlo...
1. El método int compare(T a, T b, Comparator<T> c) utiliza el comparador
proporcionado para comparar los dos objetos:
1. Devuelve 0 cuando los objetos son iguales
2. Devuelve un número negativo cuando el primer objeto es más pequeño que
el segundo

pág. 85
3. Devuelve un número positivo de lo contrario

El valor de retorno distinto de cero del método int compare(T a, T b, Comparator<T>


c) depende de la implementación. En el caso de String, más pequeño y más grande se
definen de acuerdo con su posición de pedido (más pequeño se coloca delante de más
grande en la lista ordenada), y el valor devuelto es la diferencia entre las posiciones
del primer y el segundo parámetro en la lista, ordenado de acuerdo con el comparador
proporcionado:

int res =
Objects.compare("a", "c", Comparator.naturalOrder());
System.out.println(res); //prints: -2
res = Objects.compare("a", "a", Comparator.naturalOrder());
System.out.println(res); //prints: 0
res = Objects.compare("c", "a", Comparator.naturalOrder());
System.out.println(res); //prints: 2
res = Objects.compare("c", "a", Comparator.reverseOrder());
System.out.println(res); //prints: -2

Los valores, por otro lado, devuelven solo o cuando los valores no son
iguales: Integer -11

res = Objects.compare(3, 5, Comparator.naturalOrder());


System.out.println(res); //prints: -1
res = Objects.compare(3, 3, Comparator.naturalOrder());
System.out.println(res); //prints: 0
res = Objects.compare(5, 3, Comparator.naturalOrder());
System.out.println(res); //prints: 1
res = Objects.compare(5, 3, Comparator.reverseOrder());
System.out.println(res); //prints: -1
res = Objects.compare("5", "3", Comparator.reverseOrder());
System.out.println(res); //prints: -2

Observe cómo, en la última línea del bloque de código anterior, el resultado cambia
cuando comparamos números como literales. String

Cuando ambos objetos son, el método los considera iguales: null compare()

res = Objects.compare(null,null,Comparator.naturalOrder());
System.out.println(res); //prints: 0

Pero se lanza cuando solo uno de los objetos es nulo: NullPointerException

//Objects.compare(null, "c", Comparator.naturalOrder ());


//Objects.compare("a ", null, Comparator.naturalOrder ());

Si necesita comparar un objeto con nulo, es mejor


usarlo org.apache.commons.lang3.ObjectUtils.compare(T o1, T o2).

2. El método toString(Object obj) es útil cuando una referencia obj de objeto es


el valor null:

pág. 86
1. String toString(Object obj): Devuelve el resultado de llamar toString() al
primer parámetro cuando no es null y null cuando el valor del primer parámetro
es null
2. String toString(Object obj, String nullDefault): Devuelve el resultado de
llamar toString() al primer parámetro cuando no es null y al segundo valor del
parámetro nullDefault, cuando el primer valor del parámetro es null

El uso del método toString(Object obj) es sencillo:

System.out.println (Objects.toString ("a")); // imprime: a


System.out.println (Objects.toString (null)); // imprime: nulo
System.out.println (Objects.toString ("a", "b")); // imprime: a
System.out.println (Objects.toString (null, "b")); // imprime: b

3. El método sobrecargado verifica si el índice y la longitud de una colección o una


matriz son compatibles: checkIndex()
1. int checkIndex(int index, int length):
Lanza IndexOutOfBoundsException si el proporcionado index es mayor
que length - 1, por ejemplo:

List<Integer> list = List.of(1, 2);


try {
Objects.checkIndex(3, list.size());
} catch (IndexOutOfBoundsException ex){
System.out.println(ex.getMessage());
//prints: Index 3 out-of-bounds for length 2
}

1. int checkFromIndexSize(int fromIndex, int size, int length):


Lanza IndexOutOfBoundsException si el proporcionado index + size es
mayor que length - 1, por ejemplo:

List<Integer> list = List.of(1, 2);


try {
Objects.checkFromIndexSize(1, 3, list.size());
} catch (IndexOutOfBoundsException ex){
System.out.println(ex.getMessage());
//prints:Range [1, 1 + 3) out-of-bounds for length 2
}

1. int checkFromToIndex(int fromIndex, int toIndex, int length):


Arroja IndexOutOfBoundsException si el valor proporcionado fromIndex es
mayor toIndexo toIndex mayor que length - 1, por ejemplo:

List<Integer> list = List.of(1, 2);


try {
Objects.checkFromToIndex(1, 3, list.size());
} catch (IndexOutOfBoundsException ex){
System.out.println(ex.getMessage());
//prints:Range [1, 3) out-of-bounds for length 2
}

pág. 87
4. Los cinco métodos del grupo requireNonNull () verifican el valor del primer
parámetro, obj. Si el valor es nulo, arrojan NullPointerException o devuelven el valor
predeterminado proporcionado:objnullNullPointerException
1. T requireNonNull(T obj): T busca NullPointerException sin mensaje si el
parámetro es null, por ejemplo:

String obj = null;


try {
Objects.requireNonNull(obj);
} catch (NullPointerException ex){
System.out.println(ex.getMessage());//prints: null
}

1. T requireNonNull(T obj, String message):


T coincide NullPointerException con el mensaje proporcionado si el
primer parámetro es null, por ejemplo:

String obj = null;


try {
Objects.requireNonNull(obj,
"Parameter 'obj' is null");
} catch (NullPointerException ex){
System.out.println(ex.getMessage());
//prints: Parameter 'obj' is null
}

1. T requireNonNull(T obj, Supplier<String> messageSupplier): Si el


primer parámetro es null, devuelve el mensaje que generó la función
proporcionada o, si el mensaje generado o la función en sí misma null,
arroja NullPointerException, por ejemplo:

String obj = null;


Supplier<String> supplier = () -> "Message";
try {
Objects.requireNonNull(obj, supplier);
} catch (NullPointerException ex){
System.out.println(ex.getMessage());
//prints: Message
}

1. T requireNonNullElse(T obj, T defaultObj): Devuelve el primer


parámetro (si no es nulo), el segundo parámetro (si no es nulo),
arroja NullPointerException (si ambos parámetros lo son null), por
ejemplo:

String object = null;


System.out.println(Objects
.requireNonNullElse(obj, "Default value"));
//prints: Default value

1. T requireNonNullElseGet(T obj, Supplier<T>


supplier): Devuelve el primer parámetro (si no es nulo), el objeto

pág. 88
producido por la función de proveedor proporcionada (si no es nulo
y supplier.get()no es nulo), arroja NullPointerException (si ambos
parámetros son null o el primer parámetro y proveedor.get () son null), por
ejemplo:

Integer obj = null;


Supplier<Integer> supplier = () -> 42;
try {
System.out.println(Objects
.requireNonNullElseGet(obj, supplier));
} catch (NullPointerException ex){
System.out.println(ex.getMessage()); //prints: 42
}

5. El método hash() se usa generalmente para anular


la implementación predeterminada de: hashCode()

1. int hashCode(Object value): Calcula un valor hash para un solo objeto, por
ejemplo:

System.out.println(Objects.hashCode(null));
//prints: 0
System.out.println(Objects.hashCode("abc"));
//prints: 96354

• int hash(Object... values): Calcula un valor hash para una matriz de objetos, por
ejemplo:

System.out.println(Objects.hash(null)); //prints: 0
System.out.println(Objects.hash("abc"));
//prints: 96385
String[] arr = {"abc"};
System.out.println(Objects.hash(arr));
//prints: 96385
Object[] objs = {"a", 42, "c"};
System.out.println(Objects.hash(objs));
//prints: 124409
System.out.println(Objects.hash("a", 42, "c"));
//prints: 124409

Tenga en cuenta que el método hashCode(Object value) devuelve un valor hash


diferente ( 96354) que el método Objects.hash(Object... values) ( 96385), a pesar de
que calculan el valor hash para el mismo objeto individual.

6. Los métodos isNull()y nonNull()son solo envoltorios alrededor de expresiones


booleanas:
1. boolean isNull(Object obj): Devuelve el mismo valor que obj == null,
por ejemplo:

String obj = null;


System.out.println(obj == null); //prints: true
System.out.println(Objects.isNull(obj));

pág. 89
//prints: true
obj = "";
System.out.println(obj == null); //prints: false
System.out.println(Objects.isNull(obj));
//prints: false

1. boolean nonNull(Object obj): Devuelve el mismo valor que obj != null,


por ejemplo:

String obj = null;


System.out.println(obj != null); //prints: false
System.out.println(Objects.nonNull(obj));
//prints: false
obj = "";
System.out.println(obj != null); //prints: true
System.out.println(Objects.nonNull(obj));
//prints: true

7. Los métodos equals() y deepEquals()nos permiten comparar dos objetos por su


estado:
1. boolean equals(Object a, Object b): Compara dos objetos usando
el equals(Object) método y maneja el caso cuando uno de ellos o ambos
son null, por ejemplo:

String o1 = "o";
String o2 = "o";
System.out.println(Objects.equals(o1, o2));
//prints: true
System.out.println(Objects.equals(null, null));
//prints: true
Integer[] ints1 = {1,2,3};
Integer[] ints2 = {1,2,3};
System.out.println(Objects.equals(ints1, ints2));
//prints: false

En el ejemplo anterior, Objects.equals(ints1, ints2) se devuelve false porque las


matrices no pueden anular el equals() método de la Object clase y se comparan por
referencias, no por valor.

1. boolean deepEquals(Object a, Object b): Compara dos matrices por el


valor de sus elementos, por ejemplo:

String o1 = "o";
String o2 = "o";
System.out.println(Objects.deepEquals(o1, o2));
//prints: true
System.out.println(Objects.deepEquals(null, null));
//prints: true
Integer[] ints1 = {1,2,3};
Integer[] ints2 = {1,2,3};
System.out.println(Objects.deepEquals(ints1,ints2));
//prints: true
Integer[][] iints1 = {{1,2,3},{1,2,3}};
Integer[][] iints2 = {{1,2,3},{1,2,3}};

pág. 90
System.out.println(Objects.
deepEquals(iints1, iints2)); //prints: true

Como puede ver, el método deepEquals() regresa true cuando los valores
correspondientes de las matrices son iguales. Pero si las matrices tienen valores
diferentes o un orden diferente de los mismos valores, el método devuelve false:

Integer[][] iints1 = {{1,2,3},{1,2,3}};


Integer[][] iints2 = {{1,2,3},{1,3,2}};
System.out.println(Objects.
deepEquals(iints1, iints2)); //prints: false

Cómo funciona...
Los métodos Arrays.equals(Object a, Object b) y
se Arrays.deepEquals(Object a, Object b)se comportan de la misma manera que
los métodos Objects.equals(Object a, Object
b) y Objects.deepEquals(Object a, Object b):

Integer[] ints1 = {1,2,3};


Integer[] ints2 = {1,2,3};
System.out.println(Arrays.equals(ints1, ints2));
//prints: true
System.out.println(Arrays.deepEquals(ints1, ints2));
//prints: true
System.out.println(Arrays.equals(iints1, iints2));
//prints: false
System.out.println(Arrays.deepEquals(iints1, iints2));
//prints: true

De hecho, los métodos Arrays.equals(Object a, Object


b) y Arrays.deepEquals(Object a, Object b) se utilizan en la implementación de
los métodos Objects.equals(Object a, Object
b) y Objects.deepEquals(Object a, Object b).

En resumen, si desea comparar dos objetos a y b, por los valores de sus campos,
entonces:

• Si no son matrices y a no es null, use a.equals(Object b)


• Si no son matrices y cada uno o ambos objetos pueden serlo null,
use Objects.equals(Object a, Object b)
• Si ambos pueden ser matrices y cada uno o ambos pueden ser null,
use Objects.deepEquals(Object a, Object b)

El método Objects.deepEquals(Object a, Object b) parece ser el más seguro, pero no


significa que siempre deba usarlo. La mayoría de las veces, sabrá si los objetos
comparados pueden ser null o pueden ser matrices, por lo que también puede usar
otros métodos de manera segura.

pág. 91
Programacion Modular
En este capítulo, cubriremos las siguientes recetas:

• Usando jdeps para encontrar dependencias en una aplicación Java


• Crear una aplicación modular simple
• Crear un JAR modular
• Uso de un módulo JAR con aplicaciones JDK de Jigsaw anteriores al proyecto
• Migración de abajo hacia arriba
• Migración de arriba hacia abajo
• Usar servicios para crear un acoplamiento flexible entre los módulos de consumidor
y proveedor
• Crear una imagen de tiempo de ejecución modular personalizada usando jlink
• Compilación para versiones de plataforma anteriores
• Crear JAR de lanzamiento múltiple
• Usando Maven para desarrollar una aplicación modular
• Hacer que su biblioteca sea amigable con los módulos
• Cómo abrir un módulo para reflexionar

Introducción
La programación modular le permite a uno organizar el código en módulos
independientes y cohesivos, que se pueden combinar para lograr la funcionalidad
deseada. Esto nos permite crear código que es:

• Más cohesivo, porque los módulos están construidos con un propósito específico, por
lo que el código que reside allí tiende a satisfacer ese propósito específico.
• Encapsulado, porque los módulos pueden interactuar solo con aquellas API que los
otros módulos han puesto a disposición.
• Fiable, porque la capacidad de detección se basa en los módulos y no en los tipos
individuales. Esto significa que si un módulo no está presente, el módulo dependiente
no se puede ejecutar hasta que sea detectable por el módulo dependiente. Esto ayuda
a evitar errores de tiempo de ejecución.
• Débilmente acoplado. Si utiliza interfaces de servicio, el módulo de interfaz y la
implementación de interfaz de servicio pueden ser de forma flexible.

Entonces, el proceso de pensamiento en el diseño y la organización del código ahora


implicará identificar los módulos, el código y los archivos de configuración que van al
módulo y los paquetes en los que el código está organizado dentro del módulo. Después
de eso, tenemos que decidir sobre las API públicas del módulo, de modo que estén
disponibles para su uso por módulos dependientes.

pág. 92
En el desarrollo del Sistema de Módulo de Plataforma Java , se rige por la Solicitud
de Especificación J ava ( JSR ) 376 ( https://www.jcp.org/en/jsr/detail?id=376 ). El JSR
menciona que un sistema de módulos debe abordar los siguientes problemas
fundamentales:

• Configuración confiable : brinde una alternativa al classpath para declarar la


dependencia entre componentes de manera que los desarrolladores puedan evitar que
sus aplicaciones arrojen sorpresas en el tiempo de ejecución debido a la falta de
dependencias en el classpath.
• Encapsulación fuerte : proporcione un control de acceso más estricto, de modo que
algo privado a un componente sea privado en sentido verdadero, es decir, no sea
accesible incluso a través de Reflection y permita al desarrollador exponer
selectivamente partes en el componente para su uso por otros componentes.

El JSR enumera las ventajas que resultan de abordar los problemas anteriores:

• Una plataforma escalable : la especificación en JSR 376 permitirá aprovechar los


diferentes perfiles introducidos en JSR 337 de la manera correcta al permitir la
creación de perfiles utilizando diferentes componentes / módulos creados en la nueva
plataforma. Esta plataforma modular también permitirá a otros desarrolladores
empaquetar diferentes componentes de la Plataforma Java para crear un tiempo de
ejecución personalizado, dándoles la opción de crear el tiempo de ejecución lo
suficiente para su uso.
• Mayor integridad de la plataforma : la fuerte encapsulación evitará el uso
intencionado o accidental de las API internas de Java, dando así una mayor integridad
de la plataforma.
• Rendimiento mejorado : con la clara dependencia entre los componentes, ahora es
mucho más fácil optimizar los componentes individuales en función de los
componentes que interactúan dentro de la plataforma Java SE y fuera de ella.

En este capítulo, veremos algunas recetas importantes que lo ayudarán a comenzar con
la programación modular.

Usando jdeps para encontrar


dependencias en una aplicación
Java
El primer paso para modularizar su aplicación es identificar sus
dependencias. Se jdepsintrodujo una herramienta de análisis estático llamada en JDK 8
para permitir a los desarrolladores encontrar las dependencias de sus aplicaciones. Hay
varias opciones compatibles con el comando, que permite a los desarrolladores
verificar las dependencias en las API internas de JDK, mostrar las dependencias a nivel

pág. 93
de paquete, mostrar las dependencias a nivel de clase y filtrar las dependencias, entre
otras opciones.

En esta receta, veremos cómo utilizar la jdepsherramienta explorando su funcionalidad


y utilizando las múltiples opciones de línea de comandos que admite.

Prepararse
Necesitamos una aplicación de muestra que podamos ejecutar contra el comando jdeps
para encontrar sus dependencias. Entonces, pensamos en crear una aplicación muy
simple que use la API de Jackson para consumir JSON desde la API
REST: http://jsonplaceholder.typicode.com/users .

En el código de muestra, también agregamos una llamada a la API interna JDK en


desuso, llamada sun.reflect.Reflection.getCallerClass(). De esta manera, podemos
ver cómo jdeps ayuda a encontrar dependencias para las API internas de JDK.

Los siguientes pasos lo ayudarán a configurar los requisitos previos para esta receta:

1. Puede obtener el código completo de la muestra en Chapter03/1_json-jackson-


sample. Hemos creado este código contra Java 9 y también utilizando Java 8, y se
compila bien. Por lo tanto, solo necesita instalar Java 9 para compilarlo. Si intenta
compilar con JDK 11, se encontrará con un error debido a la API interna en desuso,
que ya no está disponible.
2. Una vez que tenga el código, compílelo usando lo siguiente:

# On Linux
javac -cp 'lib/*' -d classes
-sourcepath src $(find src -name *.java)

# On Windows
javac -cp lib*;classes
-d classes src/com/packt/model/*.java
src/com/packt/*.java
src / com / packt / *. java

Nota : Si javacestá apuntando a JDK 11, puede declarar variables de entorno tales
como JAVA8_HOME o JAVA9_HOME que están apuntando a sus instalaciones JDK 8 y JDK9,
respectivamente. De esta manera, puede compilar usando:

# On Linux
"$JAVA8_HOME"/bin/javac -cp 'lib/*'
-d classes -sourcepath src $(find src -name *.java)

# On Windows
"%JAVA8_HOME%"\bin\javac -cp lib\*;classes
-d classes src\com\packt\*.java
src\com\packt\model\*.java

pág. 94
Verá una advertencia sobre el uso de una API interna, que puede ignorar de forma
segura. Agregamos esto con el propósito de demostrar la capacidad de jdeps. Ahora,
debe tener sus archivos de clase compilados en el directorio de clases.

3. Puede crear un JAR ejecutable y ejecutar el programa de ejemplo ejecutando el JAR


usando los siguientes comandos:

# On Linux:
jar cvfm sample.jar manifest.mf -C classes .
"$JAVA8_HOME"/bin/java -jar sample.jar
# On Windows:
jar cvfm sample.jar manifest.mf -C classes .
"%JAVA8_HOME%"\bin\java -jar sample.jar

4. Hemos proporcionado los run.baty run.shscripts en Chapter03/1_json-jackson-


sample. Puede compilar y ejecutar utilizando estos scripts también.

Un archivo sample.jar se crea en el directorio actual si ha usado run.bato run.sh o los


comandos anteriores para crear JAR. Si no se ha creado el JAR , puede usar
el script build-jar.bato build.-jar.sh para compilar y construir el JAR .

Entonces, tenemos una aplicación no modular de muestra que analizaremos


usando jdeps para encontrar sus dependencias, y también los nombres de los módulos
de los que posiblemente depende.

Cómo hacerlo...
1. La forma más sencilla de usar jdepses la siguiente:

# On Linux
jdeps -cp classes/:lib/* classes/com/packt/Sample.class

# On Windows
jdeps -cp "classes/;lib/*" classes/com/packt/Sample.class

El comando anterior es equivalente al siguiente:

# On Linux
jdeps -verbose:package -cp classes/:lib/*
classes/com/packt/Sample.class

# On Windows
jdeps -verbose:package -cp "classes/;lib/*" classes/com/packt/Sample.class

El resultado del código anterior es el siguiente:

pág. 95
En el comando anterior, usamos jdeps para enumerar las dependencias para el archivo
de clase Sample.class, a nivel de paquete . Tenemos que proporcionar a jdeps la ruta
para buscar las dependencias del código que se analiza. Esto se puede hacer mediante
el establecimiento de la -classpath, -cpo --class-path la opción del comando jdeps.

La opción -verbose:package enumera las dependencias a nivel de paquete.


2. Hagamos una lista de las dependencias a nivel de clase:

# On Linux
jdeps -verbose:class -cp classes/:lib/*
classes/com/packt/Sample.class

# On Windows
jdeps -verbose:class -cp "classes/;lib/*" classes/com/packt/Sample.class

El resultado del comando anterior es el siguiente:

En este caso, se hace uso de la opción -verbose:class de listar las dependencias a nivel
de clase, por lo que se puede ver que la clase com.packt.Sample depende de
com.packt.model.Company, java.lang.Exception, com.fasterxml.jackson.core.type.TypeR
eference, y así sucesivamente.

3. Veamos el resumen de las dependencias:

# On Linux
jdeps -summary -cp classes/:lib/*
classes/com/packt/Sample.class

# On Windows

pág. 96
jdeps -summary -cp "classes/;lib/*"
classes/com/packt/Sample.class

La salida es la siguiente:

4. Verifiquemos la dependencia de la API interna de JDK:

# On Linux
jdeps -jdkinternals -cp classes/:lib/*
classes/com/packt/Sample.class

# On Windows
jdeps -jdkinternals -cp "classes/;lib/*"
classes/com/packt/Sample.class

El siguiente es el resultado del comando anterior:

La API StackWalker es la nueva API para atravesar la pila de llamadas, que se introdujo
en Java 9. Este es el reemplazo del método
sun.reflect.Reflection.getCallerClass(). Discutiremos esta API en
el Capítulo 11 , Administración de memoria y depuración .

5. Vamos a ejecutar jdepsel archivo JAR sample.jar:

# On Linux and Windows


jdeps -s -cp lib/* sample.jar

El resultado que obtenemos es el siguiente:

La información anterior obtenida después de investigar el sample.jar usando jdeps es


bastante útil. Establece claramente las dependencias de nuestros archivos JAR y es muy
útil cuando intentamos migrar esta aplicación a una aplicación modular.

pág. 97
6. Veamos si hay dependencias en un nombre de paquete dado:

# On Linux and Windows


jdeps -p java.util sample.jar

La salida es la siguiente:

La -popción se utiliza para buscar dependencias en el nombre del paquete


dado. Entonces, llegamos a saber que nuestro código depende del paquete
java.util. Probemos esto con otro nombre de paquete:

jdeps -p java.util.concurrent sample.jar

No hay salida, lo que significa que nuestro código no depende del paquete
java.util.concurrent.

7. Queremos ejecutar la verificación de dependencia solo para nuestro código. Si, esto
es posible. Supongamos que corremos jdeps -cp lib/* sample.jar; verá incluso los JAR
de la biblioteca siendo analizados. No quisiéramos eso, ¿verdad? Solo incluyamos las clases
del paquete com.packt:

# On Linux
jdeps -include 'com.packt.*' -cp lib/* sample.jar

# On Windows
jdeps -include "com.packt.*" -cp lib/* sample.jar

La salida es la siguiente:

8. Verifiquemos si nuestro código depende de un paquete específico:

# On Linux
jdeps -p 'com.packt.model' sample.jar

# On Windows
jdeps -p "com.packt.model" sample.jar

La salida es la siguiente:

pág. 98
9. Podemos usar jdeps para analizar los módulos JDK. Elija el módulo
java.httpclient para el análisis:

jdeps -m java.xml

Aquí está la salida:

También podemos averiguar si un módulo determinado depende de otro módulo


utilizando la opción –require, de la siguiente manera:

# On Linux and Windows


jdeps --require java.logging -m java.sql

Aquí está la salida:

pág. 99
En el comando anterior, intentamos averiguar si el módulo java.sql depende
del módulo java.logging. El resultado que obtenemos es el resumen de dependencia
del módulo java.sql y los paquetes en el módulo java.sql, que hacen uso del código
del módulo java.logging.

Cómo funciona...
El comando jdeps es un analizador de dependencias de clase estática y se utiliza para
analizar las dependencias estáticas de la aplicación y sus
bibliotecas. El comando jdeps, de manera predeterminada, muestra las dependencias
a nivel de paquete de los archivos de entrada, que pueden ser archivos .class, un
directorio o un archivo JAR. Esto es configurable y se puede cambiar para mostrar
dependencias a nivel de clase. Hay varias opciones disponibles para filtrar las
dependencias y especificar los archivos de clase que se analizarán. Hemos visto un uso
regular de la opción -cp de línea de comandos. Esta opción se utiliza para proporcionar
las ubicaciones para buscar las dependencias del código analizado.

Hemos analizado el archivo de clase, los archivos JAR y los módulos JDK, y también
probamos diferentes opciones del comando jdeps. Hay algunas opciones, como -e, -
regex, --regex, -f, --filter, y -include, que aceptan una expresión regular (regex). Es
importante comprender la salida del comando jdeps. Hay dos partes de información
para cada clase / archivo JAR que se analiza:

1. El resumen de la dependencia para el archivo analizado (JAR o archivo de clase). Este


consiste en el nombre de la clase o el archivo JAR a la izquierda y el nombre de la
entidad dependiente a la derecha. La entidad dependiente puede ser un directorio, un
archivo JAR o un módulo JDK, de la siguiente manera:

Sample.class -> clases


Sample.class -> lib / jackson-core-2.9.6.jar
Sample.class -> lib / jackson-databind-2.9.6.jar
Sample.class -> java.base
Sample.class -> jdk.unsupported

2. Una información de dependencia más detallada del contenido del archivo analizado
a nivel de paquete o clase (según las opciones de la línea de comandos). Este consta de tres
columnas: la columna 1 contiene el nombre del paquete / clase, la columna 2 contiene el

pág. 100
nombre del paquete dependiente y la columna 3 contiene el nombre del módulo / JAR donde
se encuentra la dependencia. Una salida de muestra tiene el siguiente aspecto:

com.packt -> com.fasterxml.jackson.core.type


jackson-core-2.9.6.jar
com.packt -> com.fasterxml.jackson.databind
jackson-databind-2.9.6.jar
com.packt -> com.packt.model sample.jar

Hay más...
Hemos visto bastantes opciones del comando jdeps. Hay algunos más relacionados con
el filtrado de las dependencias y el filtrado de las clases que se analizarán. Aparte de
eso, hay algunas opciones que se ocupan de las rutas de los módulos.

Las siguientes son las opciones que se pueden probar:

• -e, -regex, --regex: Estas dependencias encontrar los correspondientes al patrón


dado.
• -f, -filter: Excluyen las dependencias que coinciden con el patrón dado.
• -filter:none: Esto no permite el filtrado que se aplica a través de filter:package
o filter:archive.
• -filter:package: Esto excluye dependencias dentro del mismo paquete. Esta es la
opción por defecto. Por ejemplo, si añadimos -filter:nonea jdeps sample.jar, sería
imprimir la dependencia del paquete a sí mismo.
• -filter:archive: Esto excluye dependencias dentro del mismo archivo.
• -filter:module: Esto excluye dependencias en el mismo módulo.
• -P, -profile: Esto se usa para mostrar el perfil del paquete, ya sea en compact1,
compact2, compact3 o JRE completo.
• -R, -recursive: Atraviesan recursivamente todas las dependencias de tiempo de
ejecución; Son equivalentes a la -filter:noneopción.

Crear una aplicación modular


simple
Debería preguntarse de qué se trata esta modularidad y cómo crear una aplicación
modular en Java. En esta receta, trataremos de aclarar la confusión en torno a la
creación de aplicaciones modulares en Java guiándole a través de un ejemplo
simple. Nuestro objetivo es mostrarle cómo crear una aplicación modular; Por lo tanto,
elegimos un ejemplo simple para centrarnos en nuestro objetivo.

pág. 101
Nuestro ejemplo es una calculadora avanzada simple, que verifica si un número es
primo, calcula la suma de los números primos, verifica si un número es par y calcula la
suma de los números pares e impares.

Prepararse
Dividiremos nuestra aplicación en dos módulos:

• El módulo math.util, que contiene las API para realizar los cálculos matemáticos.
• El módulo calculator, que lanza una calculadora avanzada.

Cómo hacerlo...
1. Implementemos las API en la clase com.packt.math.MathUtil, comenzando con
la API isPrime(Integer number):

public static Boolean isPrime(Integer number){


if ( number == 1 ) { return false; }
return IntStream.range(2,num).noneMatch(i -> num % i == 0 );
}

2. Implemente la API sumOfFirstNPrimes(Integer count:

public static Integer sumOfFirstNPrimes(Integer count){


return IntStream.iterate(1,i -> i+1)
.filter(j -> isPrime(j))
.limit(count).sum();
}

3. Escribamos una función para verificar si el número es par:

public static Boolean isEven(Integer number){


return number % 2 == 0;
}

4. La negación de isEven nos dice si el número es impar. Podemos tener funciones


para encontrar la suma de los primeros N números pares y los primeros N números impares,
como se muestra aquí:

public static Integer sumOfFirstNEvens(Integer count){


return IntStream.iterate(1,i -> i+1)
.filter(j -> isEven(j))
.limit(count).sum();
}

public static Integer sumOfFirstNOdds(Integer count){


return IntStream.iterate(1,i -> i+1)
.filter(j -> !isEven(j))

pág. 102
.limit(count).sum();
}

Podemos ver en las API anteriores que se repiten las siguientes operaciones:

• Una secuencia infinita de números a partir de 1


• Filtrar los números según alguna condición
• Limitar el flujo de números a un recuento dado
• Encontrar la suma de números así obtenidos

Según nuestra observación, podemos refactorizar las API anteriores y extraer estas
operaciones en un método, de la siguiente manera:

Integer computeFirstNSum(Integer count,


IntPredicate filter){
return IntStream.iterate(1,i -> i+1)
.filter(filter)
.limit(count).sum();
}

Aquí, count es el límite de números que necesitamos para encontrar la suma de,
y filter es la condición para elegir los números para sumar.

Reescribamos las API en función de la refactorización que acabamos de hacer:

public static Integer sumOfFirstNPrimes(Integer count){


return computeFirstNSum(count, (i -> isPrime(i)));
}

public static Integer sumOfFirstNEvens(Integer count){


return computeFirstNSum(count, (i -> isEven(i)));
}

public static Integer sumOfFirstNOdds(Integer count){


return computeFirstNSum(count, (i -> !isEven(i)));
}

Debe estar preguntándose sobre lo siguiente:

• La IntStreamclase y el encadenamiento relacionado de los métodos.


• El uso de ->en la base del código
• El uso de la IntPredicateclase.

Si realmente se está preguntando, no necesita preocuparse, ya que cubriremos estas


cosas en el Capítulo 4 , Funcionamiento funcional, yCapítulo 5 , Corrientes y tuberías .

Hasta ahora, hemos visto algunas API en torno a los cálculos matemáticos. Estas API
son parte de nuestra clase com.packt.math.MathUtil. El código completo para esta clase
se puede encontrar en Chapter03/2_simple-modular-math-
util/math.util/com/packt/math, en la base de código descargada para este libro.

pág. 103
Hagamos que esta pequeña clase de utilidad sea parte de un módulo
llamado math.util. Las siguientes son algunas convenciones que usamos para crear
un módulo:

1. Coloque todo el código relacionado con el módulo en un directorio


llamado math.util y trátelo como nuestro directorio raíz del módulo.
2. En la carpeta raíz, inserte un archivo llamado module-info.java.
3. Coloque los paquetes y los archivos de código en el directorio raíz.

¿Qué contiene module-info.java? El seguimiento:

• El nombre del módulo.


• Los paquetes que exporta, es decir, el que pone a disposición para que otros
módulos lo usen
• Los módulos de los que depende
• Los servicios que usa
• El servicio para el cual proporciona implementación

Como se mencionó en el Capítulo 1 , Instalación y un adelanto de Java 11 , el JDK viene


con muchos módulos, es decir, ¡el SDK de Java existente se ha modularizado! Uno de
esos módulos es un módulo llamado java.base. Todos los módulos definidos por el
usuario dependen implícitamente (o requieren) del módulo java.base (piense en cada
clase que extiende implícitamente la clase Object).

Nuestro módulo math.util no depende de ningún otro módulo (excepto, por supuesto,
el módulo java.base). Sin embargo, hace que su API esté disponible para otros
módulos (si no, la existencia de este módulo es cuestionable). Vamos a seguir y poner
esta declaración en el código:

module math.util{
exports com.packt.math;
}

Le estamos diciendo al compilador y al tiempo de ejecución de Java que


nuestro módulo math.util está exportando el código del paquete com.packt.math a
cualquier módulo que dependa de math.util.

El código para este módulo se puede encontrar en Chapter03/2_simple-modular-math-


util/math.util.

Ahora, creemos otra calculadora de módulo que use el módulo math.util. Este módulo
tiene una clase Calculator cuyo trabajo es aceptar la elección del usuario para qué
operación matemática ejecutar y luego la entrada requerida para ejecutar la
operación. El usuario puede elegir entre cinco operaciones matemáticas disponibles:

• Verificación de número primo


• Comprobación de número par

pág. 104
• Suma de N primos
• Suma de N pares
• Suma de N probabilidades

Veamos esto en código:

private static Integer acceptChoice(Scanner reader){


System.out.println("************Advanced Calculator************");
System.out.println("1. Prime Number check");
System.out.println("2. Even Number check");
System.out.println("3. Sum of N Primes");
System.out.println("4. Sum of N Evens");
System.out.println("5. Sum of N Odds");
System.out.println("6. Exit");
System.out.println("Enter the number to choose operation");
return reader.nextInt();
}

Luego, para cada una de las opciones, aceptamos la entrada requerida e invocamos
la API MathUtil correspondiente , de la siguiente manera:

switch(choice){
case 1:
System.out.println("Enter the number");
Integer number = reader.nextInt();
if (MathUtil.isPrime(number)){
System.out.println("The number " + number +" is prime");
}else{
System.out.println("The number " + number +" is not prime");
}
break;
case 2:
System.out.println("Enter the number");
Integer number = reader.nextInt();
if (MathUtil.isEven(number)){
System.out.println("The number " + number +" is even");
}
break;
case 3:
System.out.println("How many primes?");
Integer count = reader.nextInt();
System.out.println(String.format("Sum of %d primes is %d",
count, MathUtil.sumOfFirstNPrimes(count)));
break;
case 4:
System.out.println("How many evens?");
Integer count = reader.nextInt();
System.out.println(String.format("Sum of %d evens is %d",
count, MathUtil.sumOfFirstNEvens(count)));
break;
case 5:
System.out.println("How many odds?");
Integer count = reader.nextInt();
System.out.println(String.format("Sum of %d odds is %d",
count, MathUtil.sumOfFirstNOdds(count)));
break;
}

pág. 105
El código completo de la clase Calculator se puede encontrar en Chapter03/2_simple-
modular-math-util/calculator/com/packt/calculator/Calculator.java.

Creemos la definición de módulo para nuestro módulo calculator de la misma manera


que la creamos para el módulo math.util:

module calculator{
requires math.util;
}

En la definición de módulo anterior, mencionamos que el módulo calculator depende


del módulo math.util mediante el uso de la palabra clave required.

El código para este módulo se puede encontrar en Chapter03/2_simple-modular-math-


util/calculator.

Vamos a compilar el código:

javac -d mods --module-source-path. $ (find. -name "* .java")

El comando anterior debe ejecutarse desde Chapter03/2_simple-modular-math-util.

Además, debe tener el código compilado de ambos módulos math.util y calculator


del directorio mods. El compilador se ocupa de un solo comando y de todo, incluida la
dependencia entre los módulos. No requerimos herramientas de compilación
como ant para gestionar la compilación de módulos.

El comando --module-source-path es la nueva opción de línea de comando para javac,


que especifica la ubicación del código fuente de nuestro módulo.

Ejecutemos el código anterior:

java --module-path mods -m calculadora / com.packt.calculator.Calculator

El comando --module-path, similar a --classpath, es la nueva opción de línea de


comandos java, que especifica la ubicación de los módulos compilados.

Después de ejecutar el comando anterior, verá la calculadora en acción:

pág. 106
¡Felicidades! Con esto, tenemos una aplicación modular simple en funcionamiento.

Hemos proporcionado scripts para probar el código en plataformas Windows y


Linux. Use run.bat para Windows y run.shLinux.

Cómo funciona...

pág. 107
Ahora que ha pasado por el ejemplo, veremos cómo generalizarlo para que podamos
aplicar el mismo patrón en todos nuestros módulos. Seguimos una convención
particular para crear los módulos:

|application_root_directory
|--module1_root
|----module-info.java
|----com
|------packt
|--------sample
|----------MyClass.java
|--module2_root
|----module-info.java
|----com
|------packt
|--------test
|----------MyAnotherClass.java

Colocamos el código específico del módulo dentro de sus carpetas con


un archivo module-info.java correspondiente en la raíz de la carpeta. De esta manera,
el código está bien organizado.

Veamos qué module-info.java puede contener. De la especificación del lenguaje Java


( http://cr.openjdk.java.net/~mr/jigsaw/spec/lang-vm.html ), la declaración de un
módulo tiene la siguiente forma:

{Annotation} [open] module ModuleName { {ModuleStatement} }

Aquí está la sintaxis, explicada:

• {Annotation}: Esta es cualquier anotación del formulario @Annotation(2).


• open: Esta palabra clave es opcional. Un módulo abierto hace que todos sus
componentes sean accesibles en tiempo de ejecución a través de la reflexión. Sin
embargo, en tiempo de compilación y tiempo de ejecución, solo son accesibles
aquellos componentes que se exportan explícitamente.
• module: Esta es la palabra clave utilizada para declarar un módulo.
• ModuleName: Este es el nombre del módulo que es un identificador Java válido con un
punto ( .) permitido entre los nombres de los identificadores , similar a math.util.
• {ModuleStatement}: Esta es una colección de las declaraciones permitidas dentro de
una definición de módulo. Vamos a ampliar esto a continuación.

Una declaración de módulo tiene la siguiente forma:

ModuleStatement:
requires {RequiresModifier} ModuleName ;
exports PackageName [to ModuleName {, ModuleName}] ;
opens PackageName [to ModuleName {, ModuleName}] ;
uses TypeName ;
provides TypeName with TypeName {, TypeName} ;

pág. 108
La declaración del módulo se decodifica aquí:

• requires: Esto se utiliza para declarar una dependencia en un módulo. puede


ser transitivo , estático o ambos. Transitivo significa que cualquier módulo que
depende del módulo dado también depende implícitamente del módulo requerido por
dicho módulo de forma transitiva. Estático significa que la dependencia del módulo
es obligatoria en tiempo de compilación, pero opcional en tiempo de
ejecución. Algunos ejemplos son , y .{RequiresModifier}requires
math.utilrequires transitive math.utilrequires static math.util
• exports: Esto se utiliza para hacer que los paquetes dados sean accesibles para los
módulos dependientes. Opcionalmente, podemos forzar la accesibilidad del paquete
a módulos específicos especificando el nombre del módulo, como exports
com.package.math to claculator.
• opens: Esto se usa para abrir un paquete específico. Vimos anteriormente que
podemos abrir un módulo especificando la palabra clave open con la declaración del
módulo. Pero esto puede ser menos restrictivo. Entonces, para hacerlo más
restrictivo, podemos abrir un paquete específico para acceso reflexivo en tiempo de
ejecución mediante el uso de la palabra clave opens — opens com.packt.math.
• uses: Esto se utiliza para declarar una dependencia en una interfaz de servicio a la
que se puede acceder mediante java.util.ServiceLoader. La interfaz de servicio
puede estar en el módulo actual o en cualquier módulo del que dependa el módulo
actual.
• provides: Esto se utiliza para declarar una interfaz de servicio y proporcionarle al
menos una implementación. La interfaz de servicio se puede declarar en el módulo
actual o en cualquier otro módulo dependiente. Sin embargo, la implementación del
servicio debe proporcionarse en el mismo módulo; de lo contrario, se producirá un
error en tiempo de compilación.

Examinaremos las cláusulas uses y provides con más detalle en la sección Uso de
servicios para crear un acoplamiento flexible entre la receta de los módulos de
consumidor y proveedor .

La fuente del módulo de todos los módulos se puede compilar a la vez utilizando
la opción --module-source-path de línea de comandos. De esta manera, todos los
módulos se compilarán y colocarán en sus directorios correspondientes en el directorio
proporcionado por la -dopción. Por ejemplo, javac -d mods --module-source-path .
$(find . -name "*.java") compila el código en el directorio actual en un directorio mods.

Ejecutar el código es igualmente simple. Especificamos la ruta en la que se compilan


todos nuestros módulos utilizando la opción de línea de comandos --module-
path. Luego, mencionamos el nombre del módulo junto con el nombre completo de la
clase principal usando la opción de línea de comandos -m, por ejemplo java --module-
path mods -m calculator/com.packt.calculator.Calculator,.

Ver también
pág. 109
La Compilación y ejecución de una aplicación Java receta en el Capítulo 1 , Instalación
y un adelanto en Java 11

Crear un JAR modular


Compilar módulos en una clase es bueno, pero no es adecuado para compartir binarios
e implementación. Los JAR son mejores formatos para compartir e
implementar. Podemos empaquetar el módulo compilado en JAR, y los JAR que
contienen module-info.classen su nivel superior se denominan JAR modulares . En
esta receta, veremos cómo crear JAR modulares y también cómo ejecutar la aplicación,
que se compone de múltiples JAR modulares.

Prepararse
Hemos visto y creado una aplicación modular simple en Crear una receta
de aplicación modular más simple . Para construir un JAR modular, utilizaremos el
código de muestra disponible en Chapter03/3_modular_jar. Este código de muestra
contiene dos módulos: math.utily calculator. Crearemos JAR modulares para ambos
módulos.

Cómo hacerlo...
1. Compile el código y coloque las clases compiladas en un directorio, por
ejemplo mods:

javac -d mods --module-source-path. $ (find. -name * .java)

2. Cree un JAR modular para el módulo math.util:

jar --create --file=mlib/[email protected] --module-version 1.0 -C


mods / math.util.

No olvide el punto ( .) al final del código anterior.


3. Cree un JAR modular para el módulo calculator, especificando la clase principal
para hacer que el JAR sea ejecutable:

jar --create --file=mlib/[email protected] --module-version 1.0 --


main-class com.packt.calculator.Calculator -C mods / calculator.

pág. 110
La pieza crítica en el comando anterior es la opción --main-class. Esto nos permite
ejecutar el JAR sin proporcionar la información de la clase principal durante la
ejecución.

4. Ahora, tenemos dos JAR en


el directorio mlib : [email protected] y [email protected]. Estos JAR se denominan
JAR modulares. Si desea ejecutar el ejemplo, puede usar el siguiente comando:

java -p mlib -m calculator

5. Se ha introducido una nueva opción de línea de comandos para el comando JAR en


Java 9, llamada -d , o --describe-module. Esto imprime la información sobre el módulo
que contiene el JAR modular:

jar -d --file=mlib/[email protected]

La salida de jar -d for [email protected] es la siguiente:

[email protected]
requires mandated java.base
requires math.util
conceals com.packt.calculator
main-class com.packt.calculator.Calculator

jar -d --file=mlib/[email protected]

La salida de jar -d for [email protected] la siguiente:

[email protected]
requires mandated java.base
exports com.packt.math

Hemos proporcionado los siguientes scripts para probar el código de receta en


Windows:

• compile-math.bat
• compile-calculator.bat
• jar-math.bat
• jar-calculator.bat
• run.bat

Hemos proporcionado los siguientes scripts para probar el código de receta en Linux:

• compile.sh
• jar-math.sh
• jar-calculator.sh
• run.sh

Debe ejecutar los scripts en el orden en que se han enumerado.

pág. 111
Uso de un módulo JAR con
aplicaciones JDK de Jigsaw
anteriores al proyecto
Sería sorprendente si nuestros JAR modulares se pudieran ejecutar con aplicaciones
JDK Jigsaw anteriores al proyecto. De esta manera, no nos preocuparemos por escribir
otra versión de nuestra API para aplicaciones anteriores a JDK 9. La buena noticia es
que podemos usar nuestros JAR modulares como si fueran JAR comunes, es decir, JAR
sin ellos module-info.classen su raíz. Veremos cómo hacerlo en esta receta.

Prepararse
Para esta receta, necesitaremos un frasco modular y una aplicación no
modular. Nuestro código modular se puede encontrar
en Chapter03/4_modular_jar_with_pre_java9/math.util (este es el mismo módulo
math.util que creamos en Crear una receta de aplicación modular
simple ). Compilemos este código modular y creemos un JAR modular utilizando los
siguientes comandos:

javac -d classes --module-source-path. $ (encontrar math.util -name *


.java)
mkdir mlib
jar --create --file mlib / math.util.jar -C classes / math.util.

También proporcionamos un jar-


math.bat script en Chapter03/4_modular_jar_with_pre_java9, que puede usarse para
crear JAR modulares en Windows. Tenemos nuestro JAR modular. Vamos a verificarlo
usando la opción -d del comando jar:

jar -d --file mlib/[email protected]


[email protected]
requiere mandato java.base
exportaciones com.packt.math

Cómo hacerlo...
Ahora, creemos una aplicación simple, que no sea modular. Nuestra aplicación
consistirá en una clase nombrada NonModularCalculator, que toma prestado su código
de la clase Calculator, en Crear una receta de aplicación modular simple .

pág. 112
Puede encontrar la definición de clase NonModularCalculator en el paquete
com.packt.calculator en el directorio
Chapter03/4_modular_jar_with_pre_java9/calculator . Como no es modular, no necesita
un archivo module-info.java. Esta aplicación hace uso de nuestro JAR
modular math.util.jar para ejecutar algunos cálculos matemáticos.

En este punto, debe tener lo siguiente:

• Un JAR modular llamado [email protected]


• Una aplicación no modular que consiste en el paquete NonModularCalculator

Ahora, necesitamos compilar nuestra clase NonModularCalculator:

javac -d classes / --source-path calculator $ (find calculator -name *


.java)

Después de ejecutar el comando anterior, verá una lista de errores que dice que
el paquete com.packt.math no existe, que MathUtil no se puede
encontrar el símbolo, etc. Lo has adivinado; no proporcionamos la ubicación de
nuestro JAR modular para el compilador. Agreguemos la ubicación jar
modular usando la opción --class-path:

javac --class-path mlib / * -d classes / --source-path calculator $ (find


calculator -name * .java)

Ahora, hemos compilado con éxito nuestro código no modular, que dependía del JAR
modular. Ejecutemos el código compilado:

java -cp classes: mlib / * com.packt.calculator.NonModularCalculator

¡Felicidades! Ha utilizado con éxito su JAR modular con una aplicación no


modular. Increíble, ¿verdad?

Hemos proporcionado los siguientes scripts


en Chapter03/4_modular_jar_with_pre_java9 para ejecutar el código en la plataforma
Windows:

• compile-calculator.bat
• run.bat

Migración de abajo hacia arriba


Ahora que Java 9 está fuera de la puerta, la característica de modularidad tan esperada
ahora está disponible para ser adoptada por los desarrolladores. En algún momento u
otro, participará en la migración de su aplicación a Java 9 y, por lo tanto, intentará
modularizarla. Un cambio de tal magnitud, que involucra bibliotecas de terceros y

pág. 113
repensar la estructura del código, requeriría una planificación e implementación
adecuadas. El equipo de Java ha sugerido dos enfoques de migración:

• Migración de abajo hacia arriba


• Migración de arriba hacia abajo

Antes de comenzar a aprender sobre la migración de abajo hacia arriba, es importante


comprender qué son un módulo sin nombre y un módulo automático. Suponga que está
accediendo a un tipo que no está disponible en ninguno de los módulos; en tal caso, el
sistema de módulos buscará el tipo en el classpath, y si se encuentra, el tipo se convierte
en parte de un módulo sin nombre. Esto es similar a las clases que escribimos que no
pertenecen a ningún paquete, pero Java las agrega a un paquete sin nombre para
simplificar la creación de nuevas clases.

Por lo tanto, un módulo sin nombre es un módulo general sin nombre que contiene
todos los tipos que no forman parte de ningún módulo, pero se encuentran en el
classpath. Un módulo sin nombre puede acceder a todos los tipos exportados de todos
los módulos con nombre (módulos definidos por el usuario) y módulos integrados
(módulos de plataforma Java). Por otro lado, un módulo con nombre (módulo definido
por el usuario) no podrá acceder a los tipos en el módulo sin nombre. En otras palabras,
un módulo con nombre no puede declarar dependencia de un módulo sin nombre. Si
desea declarar una dependencia, ¿cómo lo haría? ¡Un módulo sin nombre no tiene
nombre!

Con el concepto de módulos sin nombre, puede tomar su aplicación Java 8 tal cual y
ejecutarla en Java 9 (a excepción de cualquier API interna en desuso, que podría no estar
disponible para el código de usuario en Java 9).

Es posible que haya visto esto si ha probado Usar jdeps para encontrar dependencias en
una receta de aplicación Java , donde teníamos una aplicación no modular y pudimos
ejecutarla en Java 9. Sin embargo, ejecutar como está en Java 9 sería una derrota El
propósito de introducir el sistema modular.

Si un paquete se define tanto en módulos con nombre como sin nombre, el que está en el
módulo con nombre tendrá preferencia sobre el que está en el módulo sin nombre. Esto
ayuda a evitar conflictos de paquetes cuando provienen de módulos con y sin nombre.

Los módulos automáticos son aquellos creados automáticamente por la JVM. Estos
módulos se crean cuando presentamos las clases empaquetadas en JAR en la ruta del
módulo en lugar de la ruta de clase. El nombre de este módulo se derivará del nombre
del JAR sin la extensión .jar y, por lo tanto, es diferente de los módulos sin
nombre. Alternativamente, uno puede proporcionar el nombre de estos módulos
automáticos proporcionando el nombre del módulo Automatic-Module-Name en el
archivo de manifiesto JAR. Estos módulos automáticos exportan todos los paquetes
presentes en él y también dependen de todos los módulos automáticos y con nombre
(usuario / JDK).

pág. 114
Según esta explicación, los módulos se pueden clasificar en los siguientes:

• Módulos sin nombre : el código disponible en el classpath y no disponible en la ruta


del módulo se coloca en un módulo sin nombre.
• Módulos con nombre : todos aquellos módulos que tienen un nombre
asociado ; pueden ser módulos definidos por el usuario y módulos JDK.
• Módulos automáticos : todos los módulos que JVM crea implícitamente en función
de los archivos JAR presentes en la ruta del módulo.
• Módulos implícitos : módulos que se crean implícitamente. Son lo mismo que los
módulos automáticos.
• Módulos explícitos : todos los módulos creados explícitamente por el usuario o JDK.

Pero el módulo sin nombre y el módulo automático son un buen primer paso para
comenzar su migración. ¡Entonces empecemos!

Prepararse
Necesitamos una aplicación no modular que eventualmente modularicemos. Ya hemos
creado una aplicación simple, cuyo código fuente está disponible
en Chapter03/6_bottom_up_migration_before. Esta sencilla aplicación tiene tres partes:

• Una biblioteca de utilidad matemática que contiene nuestras API matemáticas


favoritas: verificador de números primos, verificador de números pares, suma de
primos, suma de pares y suma de probabilidades. El código para esto está disponible
en Chapter03/6_bottom_up_migration_before/math_util.
• Una biblioteca de servicios bancarios que contiene API para calcular el interés simple
y el interés compuesto. El código para esto está disponible
en Chapter03/6_bottom_up_migration_before/banking_util.
• Nuestra aplicación de calculadora nos ayuda con nuestros cálculos matemáticos y
bancarios. Para hacer esto más interesante, publicaremos los resultados en JSON y
para esto, utilizaremos la API Jackson JSON. El código para esto está disponible
en Chapter03/6_bottom_up_migration_before/calculator.

Después de que haya copiado o descargado el código, compilaremos y crearemos


los JAR respectivos . Entonces, use los siguientes comandos para compilar y construir
los JAR :

#Compiling math util

javac -d math_util/out/classes/ -sourcepath math_util/src $(find


math_util/src -name *.java)
jar --create --file=math_util/out/math.util.jar
-C math_util/out/classes/ .

#Compiling banking util

javac -d banking_util/out/classes/ -sourcepath banking_util/src $(find

pág. 115
banking_util/src -name *.java)
jar --create --file=banking_util/out/banking.util.jar
-C banking_util/out/classes/ .

#Compiling calculator

javac -cp
calculator/lib/*:math_util/out/math.util.jar:banking_util/out/banking.util
.jar -d calculator/out/classes/ -sourcepath calculator/src $(find
calculator/src -name *.java)

También creemos un JAR para esto (haremos uso del JAR para construir el gráfico de
dependencia, pero no para ejecutar la aplicación):

jar --create --file=calculator/out/calculator.jar -C


calculator/out/classes/ .

Tenga en cuenta que nuestros JAR de Jackson están en la calculadora / lib, por lo que
no necesita preocuparse por descargarlos. Ejecutemos nuestra calculadora usando el
siguiente comando:

java -cp
calculator/out/classes:calculator/lib/*:math_util/out/math.util.jar:bankin
g_util/out/banking.util.jar com.packt.calculator.Calculator

Verá un menú que le pedirá la elección de la operación, y luego podrá jugar con
diferentes operaciones. ¡Ahora, modularicemos esta aplicación!

Hemos proporcionado package-*.bat y run.bat al paquete y ejecutamos la aplicación


en Windows.

Puede usar package-*.sh y run.sh para el paquete y ejecutar la aplicación en Linux.

Cómo hacerlo...
El primer paso para modularizar su aplicación es comprender su gráfico de
dependencia. Creemos un gráfico de dependencia para nuestra aplicación. Para eso,
hacemos uso de la herramienta jdeps. Si se pregunta cuál es la herramienta jdeps,
deténgase ahora y lea el uso de jdeps para encontrar dependencias en una receta
de aplicación Java . OK, entonces ejecutemos la herramienta jdeps:

jdeps -summary -R -cp calculator / lib / *: math_util / out / *:


banking_util / out / * calculator / out / calculator.jar

Estamos pidiendo que jdeps nos proporcione un resumen de las dependencias de


nuestro calculator.jar y luego hacer esto de forma recursiva para cada dependencia
de calculator.jar. La salida que obtenemos es la siguiente:

banking.util.jar -> java.base

pág. 116
calculator.jar -> banking_util / out / banking.util.jar
calculator.jar -> calculator / lib / jackson-databind-2.8.4.jar
calculator.jar -> java.base
calculator.jar -> math_util / out / math.util.jar
jackson-annotations-2.8.4.jar -> java.base
jackson-core-2.8.4.jar -> java.base
jackson-databind-2.8.4.jar -> calculadora / lib / jackson-annotations-
2.8.4.jar
jackson-databind-2.8.4.jar -> calculadora / lib / jackson-core-2.8.4.jar
jackson-databind-2.8.4.jar -> java.base
jackson-databind-2.8.4.jar -> java.logging
jackson-databind-2.8.4.jar -> java.sql
jackson-databind-2.8.4.jar -> java.xml
math.util.jar -> java.base

El resultado anterior es difícil de entender y lo mismo puede ser esquemáticamente,


de la siguiente manera:

En la migración ascendente, comenzamos modularizando los nodos hoja. En nuestro

gráfico, los nodos hoja java.xml, java.sql, java.base y java.logging ya están

modularizados. Vamos a modularizar banking.util.jar.

Todo el código para esta receta está disponible en Chapter03/6_bottom_up_migration_after.

Modularizing banking.util.jar
1. Copiar BankUtil.java
de Chapter03/6_bottom_up_migration_before/banking_util/src/com/packt/bankin
g

pág. 117
a Chapter03/6_bottom_up_migration_after/src/banking.util/com/packt/banking.
Hay dos cosas a tener en cuenta:
1. Hemos cambiado el nombre de la carpeta de banking_util
a banking.util. Esto es para seguir la convención de colocar código
relacionado con el módulo debajo de la carpeta que lleva el nombre del
módulo.
2. Hemos colocado el paquete directamente debajo de la carpeta banking.util
y no src debajo. De nuevo, esto es para seguir la convención. Colocaremos
todos nuestros módulos debajo de la carpeta src.
2. Crear el archivo de definición de módulo module-info.java
bajo Chapter03/6_bottom_up_migration_after/src/banking.util con la siguiente
definición:

module banking.util{
exports com.packt.banking;
}

3. Desde dentro de la carpeta 6_bottom_up_migration_after, compile el código Java


de los módulos ejecutando el comando:

javac -d mods --module-source-path src


$ (find src -name * .java)

4. Verá que el código Java en el módulo banking.util se compila en el directorio de


mods.

5. Creemos un JAR modular para este módulo:

jar --create --file = mlib / banking.util.jar -C mods /


banking.util.

Si se pregunta qué es un JAR modular, no dude en leer la receta Creación de un JAR


modular en este capítulo.

Ahora que nos hemos modularizado banking.util.jar, usemos este modular jar en
lugar del JAR no modular utilizado en la sección Preparativos anterior. Debe ejecutar lo
siguiente desde la carpeta 6_bottom_up_migration_before porque aún no hemos
modularizado completamente la aplicación:

java --add-modules ALL-MODULE-PATH --module-path


../6_bottom_up_migration_after/mods/banking.util -cp calculator / out /
classes: calculator / lib / *: math_util / out / math.util.jar com
.packt.calculator.Calculator

La opción --add-modules le indica el tiempo de ejecución Java para incluir los módulos, ya
sea por su nombre de módulo o por constantes predefinidas, a saber ALL-MODULE-
PATH, ALL-DEFAULT, y ALL-SYSTEM. Utilizamos ALL-MODULE-PATH para agregar el módulo que
está disponible en nuestra ruta de módulo.

pág. 118
La opción --module-path le dice al tiempo de ejecución de Java la ubicación de nuestros
módulos.

Verá que nuestra calculadora funciona como de costumbre. Pruebe un cálculo de


interés simple, un cálculo de interés compuesto, para verificar si BankUtil se
encuentra la clase. Entonces, nuestro gráfico de dependencia ahora se ve así:

Modularizing math.util.jar
1. Copiar MathUtil.java
de Chapter03/6_bottom_up_migration_before/math_util/src/com/packt/matha Cha
pter03/6_bottom_up_migration_after/src/math.util/com/packt/math.

2. Cree el archivo de definición de módulo module-info.java,


bajo Chapter03/6_bottom_up_migration_after/src/math.util con la siguiente
definición:

module math.util{
exports com.packt.math;
}

3. Desde dentro de la carpeta 6_bottom_up_migration_after, compile el código Java


de los módulos ejecutando el siguiente comando:

javac -d mods --module-source-path src $ (find src -name * .java)

4. Verá que el código Java en los módulos math.utily banking.utilse compila en el


Directorio mods.

5. Creemos un JAR modular para este módulo:


pág. 119
jar --create --file = mlib / math.util.jar -C mods / math.util.

Si se pregunta qué es un modular jar, no dude en leer la receta Creación de un JAR


modular en este capítulo.

6. Ahora que hemos modularizado math.util.jar, usemos este modular jar en lugar
del no modular jar que usamos en la sección Preparativos anterior. Debe ejecutar lo
siguiente desde la carpeta 6_bottom_up_migration_before porque todavía no hemos
modularizado completamente la aplicación:

java --add-modules ALL-MODULE-PATH --module-path


../6_bottom_up_migration_after/mods/banking.util:
../6_bottom_up_migration_after/mods/math.util -cp calculator / out /
classes: calculator / lib / * com.packt.calculator.Calculator

Nuestra aplicación funciona bien y el gráfico de dependencia se ve de la siguiente


manera:

No podemos modularizar calculator.jar porque depende de otro código no


modular jackson-databind, y no podemos modularizarlo jackson-databind porque no
lo mantenemos nosotros. Esto significa que no podemos lograr una modularidad del
100% para nuestra aplicación. Le presentamos los módulos sin nombre al comienzo de
esta receta. Todo nuestro código no modular en el classpath se agrupa en módulos sin
nombre, lo que significa que todo el código relacionado con jackson puede permanecer
en el módulo sin nombre y podemos intentar modularizar calculator.jar. Pero no
podemos hacerlo porque calculator.jar no podemos declarar una dependencia
en jackson-databind-2.8.4.jar(porque es un módulo sin nombre y los módulos con
nombre no pueden declarar dependencia en módulos sin nombre).

pág. 120
Una forma de evitar esto es hacer que el código relacionado con jackson sea un
módulo automático. Podemos hacer esto moviendo los frascos relacionados con
Jackson:

• jackson-databind-2.8.4.jar
• jackson-annotations-2.8.4.jar
• jackson-core-2.8.4.jar

Los moveremos debajo de la carpeta 6_bottom_up_migration_after usando los


siguientes comandos:

$ pwd
/ root / java9-samples / Chapter03 / 6_bottom_up_migration_after
$ cp ../6_bottom_up_migration_before/calculator/lib/*.jar mlib /
$ mv mlib / jackson-annotations-2.8.4.jar mods / jackson.annotations.jar
$ mv mlib / jackson-core-2.8.4.jar mods / jackson.core.jar
$ mv mlib / jackson-databind-2.8.4.jar mods / jackson.databind.jar

La razón para cambiar el nombre de los archivos jar es que el nombre del módulo debe
ser un identificador válido (no debe ser solo numérico, no debe contener -y otras
reglas) separado .. Como los nombres se derivan del nombre de los archivos JAR,
tuvimos que cambiar el nombre de los archivos JAR para cumplir con las reglas de
identificación de Java.

Cree un nuevo directorio mlib , si no está presente, en 6_bottom_up_migration_after.

Ahora, ejecutemos nuestro programa de calculadora nuevamente usando el siguiente


comando:

java --add-modules ALL-MODULE-PATH --module-path


../6_bottom_up_migration_after/mods:../6_bottom_up_migration_after/mlib -
cp calculator / out / classes com.packt.calculator.Calculator

La aplicación se ejecutará como siempre. Notará que el valor -cp de nuestra opción se
está reduciendo a medida que todas las bibliotecas dependientes se han movido como
módulos en la ruta del módulo. El gráfico de dependencia ahora se ve así:

pág. 121
Modularizing calculator.jar
El último paso en la migración es modularizar calculator.jar. Siga estos pasos para
modularizarlo:

1. Copie la carpeta
com de Chapter03/6_bottom_up_migration_before/calculator/srca Chapter03/6_b
ottom_up_migration_after/src/calculator.
2. Cree el archivo de definición de módulo module-info.java,
debajo Chapter03/6_bottom_up_migration_after/src/calculator, con la siguiente
definición:

module calculator{
requires math.util;
requires banking.util;
requires jackson.databind;
requires jackson.core;
requires jackson.annotations;
}

3. Desde dentro de la carpeta 6_bottom_up_migration_after, compile el código Java


de los módulos ejecutando el siguiente comando:

javac -d mods --module-path mlib: mods --module-source-path src $


(find src -name * .java)

4. Verá que el código Java en todos nuestros módulos está compilado en el directorio
de mods. Tenga en cuenta que debe tener los módulos automáticos (es decir, JAR
relacionados con jackson) ya colocados en el directorio mlib.

pág. 122
5. Creemos un JAR modular para este módulo y también mencionemos cuál es la clase
main:
jar --create --file = mlib / calculator.jar --main- class =
com.packt.calculator.Calculator -C mods / calculator.

6. Ahora, tenemos un JAR modular para nuestro módulo de calculadora, que es


nuestro módulo principal ya que contiene la clase main. Con esto, también hemos
modularizado nuestra aplicación completa. Ejecutemos el siguiente comando desde la
carpeta 6_bottom_up_migration_after:

java -p mlib: mods -m calculator

Entonces, hemos visto cómo modularizar una aplicación no modular utilizando un


enfoque de migración ascendente. El gráfico de dependencia final se parece a esto:

El código final para esta aplicación modular se puede encontrar


en Chapter03/6_bottom_up_migration_after.

Podríamos haber hecho modificaciones en línea, es decir, modularizar el código en el


mismo directorio 6_bottom_up_migration_before,. Pero preferimos hacerlo por separado
en un directorio diferente, 6_bottom_up_migration_after para mantenerlo limpio y no
perturbar la base de código existente.

Cómo funciona...
El concepto de módulos sin nombre nos ayudó a ejecutar nuestra aplicación no modular
en Java 9. El uso de la ruta del módulo y classpath nos ayudó a ejecutar la aplicación
parcialmente modular mientras realizábamos la migración. Comenzamos con la
modularización de esas bases de código que no dependían de ningún código no

pág. 123
modular, y cualquier base de código que no pudiéramos modularizar, convertimos en
módulos automáticos, lo que nos permitió modularizar el código que dependía de dicha
base de código. Finalmente, terminamos con una aplicación completamente modular.

Migración de arriba hacia abajo


La otra técnica para la migración es la migración de arriba hacia abajo. En este enfoque,
comenzamos con el JAR raíz en el gráfico de dependencia de los JAR.

Los JAR indican una base de código. Asumimos que la base de código está disponible en forma de
JAR y, por lo tanto, el gráfico de dependencia que tenemos tiene nodos, que son JAR.

La modularización de la raíz del gráfico de dependencia significaría que todos los demás
JAR de los que depende esta raíz deben ser modulares. De lo contrario, esta raíz
modular no puede declarar una dependencia en módulos sin nombre. Consideremos el
ejemplo de aplicación no modular que presentamos en nuestra receta de Migración de
abajo hacia arriba. El gráfico de dependencia se parece a esto:

Utilizamos ampliamente los módulos automáticos en la migración de arriba hacia


abajo. Los módulos automáticos son módulos que la JVM crea implícitamente. Estos se
crean en base a los JAR no modulares disponibles en la ruta del módulo.

Prepararse
Haremos uso del ejemplo de la calculadora que presentamos en la receta
anterior, Migración ascendente . Continúe y copie el código no modular

pág. 124
de Chapter03/7_top_down_migration_before. Utilice los siguientes comandos si desea
ejecutarlo y ver si funciona:

$ javac -d math_util / out / classes / -sourcepath math_util / src $


(buscar math_util / src -name * .java)

$ jar --create --file = math_util / out / math.util.jar


-C math_util / out / classes /.

$ javac -d banking_util / out / classes / -sourcepath banking_util / src $


(find banking_util / src -name * .java)

$ jar --create --file = banking_util / out / banking.util.jar


-C banking_util / out / classes /.

$ javac -cp calculator / lib / *: math_util / out / math.util.jar:


banking_util / out / banking.util.jar -d calculator / out / classes / -
sourcepath calculator / src $ (buscar calculadora / src -name *.Java)

$ java -cp calculator / out / classes: calculator / lib / *: math_util /


out / math.util.jar: banking_util / out / banking.util.jar
com.packt.calculator.Calculator

Hemos proporcionado al paquete package-*.baty run.bat y ejecutamos el código en Windows,


y utilizamos package-*.sh y en el paquete run.sh y ejecutamos el código en Linux.

Cómo hacerlo...
Estaremos modularizando la aplicación en
el Chapter03/7_top_down_migration_after directorio. Cree dos directorios srcy mlib,
debajo Chapter03/7_top_down_migration_after.

Modularizando la calculadora
1. No podemos modularizar la calculadora hasta que hayamos modularizado todas sus
dependencias. Pero modularizar sus dependencias puede ser más fácil en ocasiones y
no en otras, especialmente en los casos en que la dependencia es de un tercero. En
tales escenarios, hacemos uso de módulos automáticos. Copiamos mliblos JAR no
modulares en la carpeta y nos aseguramos de que el nombre del JAR esté en el
formulario <identifier>(.<identifier>)*, donde hay un identificador <identifier>
de Java válido:

$ cp ../7_top_down_migration_before/calculator/lib/jackson-
annotations-
2.8.4.jar mlib / jackson.annotations.jar

$ cp ../7_top_down_migration_before/calculator/lib/jackson-core-
2.8.4.jar

pág. 125
mlib / jackson .core.jar

$ cp ../7_top_down_migration_before/calculator/lib/jackson-
databind-
2.8.4.jar mlib / jackson.databind.jar

$ cp
../7_top_down_migration_before/banking_util/out/banking.util.jar
mlib /

$ cp ../7_top_down_migration_before/math_util/out/math.util.jar
mlib /
Hemos proporcionado los scripts copy-non-mod-jar.bat y copy-non-mod-jar.sh para
copiar los frascos fácilmente.

Veamos en qué copiamos mlib:

$ ls mlib
banking.util.jar jackson.annotations.jar jackson.core.jar
jackson.databind.jar math.util.jar

banking.util.ja y math.util.jar existirá solo si ha compilado y JAR-ed el código en


los directorios Chapter03/7_top_down_migration_before/banking_util y Chapter03/7_top_
down_migration_before/math_util. Hicimos esto en
la sección Preparativos anteriormente.
2. Crea una nueva carpeta calculator en src. Esto contendrá el código para el módulo
calculator.
3. Cree module-info.javabajo
el Chapter03/7_top_down_migration_after/src/calculator directorio que
contiene lo siguiente :

module calculator{
requires math.util;
requires banking.util;
requires jackson.databind;
requires jackson.core;
requires jackson.annotations;
}

4. Copie el directorio
Chapter03/7_top_down_migration_before/calculator/src/com y todo el código debajo
de Chapter03/7_top_down_migration_after/src/calculator.

5. Compile el módulo de la calculadora:

#On Linux
javac -d mods --module-path mlib --module-source-path src $(find
src -name *.java)

#On Windows
javac -d mods --module-path mlib --module-source-path src
srccalculatormodule-info.java

pág. 126
srccalculatorcompacktcalculatorCalculator.java
srccalculatorcompacktcalculatorcommands*.java

6. Cree el JAR modular para el módulo calculator:

jar --create --file = mlib / calculator.jar --main- class =


com.packt.calculator.Calculator -C mods / calculator /.

7. Ejecute el módulo calculator:


java --module-path mlib -m calculator

Veremos que nuestra calculadora se está ejecutando correctamente. Puede probar


diferentes operaciones para verificar si todas se están ejecutando correctamente.

Modularizing banking.util
Como esto no depende de otro código que no sea de módulo, podemos convertirlo
directamente en un módulo siguiendo estos pasos:

1. Crea una nueva carpeta banking.util en src. Esto contendrá el código para
el módulo banking.util.
2. Cree module-info.java bajo
el Chapter03/7_top_down_migration_after/src/banking.util directorio , que
contiene lo siguiente :

module banking.util{
exports com.packt.banking;
}

3. Copie el directorio
Chapter03/7_top_down_migration_before/banking_util/src/com y todo el código debajo
de Chapter03/7_top_down_migration_after/src/banking.util.
4. Compile los módulos:

#On Linux
javac -d mods --module-path mlib --module-source-path src $(find
src -name *.java)

#On Windows
javac -d mods --module-path mlib --module-source-path src
srcbanking.utilmodule-info.java
srcbanking.utilcompacktbankingBankUtil.java

5. Cree un JAR modular para el módulo banking.util. Esto reemplazará el archivo no


modular banking.util.jar ya presente en mlib:

pág. 127
jar --create --file = mlib / banking.util.jar -C mods / banking.util
/.
6. ¡
7. Ejecute el módulo calculator para probar si el JAR banking.util modular se ha
creado correctamente:

java --module-path mlib -m calculator

7. Deberías ver la calculadora ejecutándose. Juegue con diferentes operaciones para


asegurarse de que no haya problemas de "clase no encontrada".

Modularizing math.util
1. Crea una nueva carpeta math.util en src. Esto contendrá el código para el módulo
math.util.
2. Cree module-info.javabajo el directorio
Chapter03/7_top_down_migration_after/src/math.util , que contiene lo siguiente :

module math.util{
exports com.packt.math;
}

3. Copie el directorio Chapter03/7_top_down_migration_before/math_util/src/com y


todo el código debajo de Chapter03/7_top_down_migration_after/src/math.util.
4. Compile los módulos:

#On Linux
javac -d mods --module-path mlib --module-source-path src $(find
src -name *.java)

#On Windows
javac -d mods --module-path mlib --module-source-path src
srcmath.utilmodule-info.java
srcmath.utilcompacktmathMathUtil.java

5. Cree un JAR modular para el módulo banking.util. Esto reemplazará el archivo no


modular banking.util.jar ya presente en mlib:

jar --create --file = mlib / math.util.jar -C mods / math.util /.

6. Ejecute el calculatormódulo para probar si el math.utilJAR modular se ha creado


correctamente.

java --module-path mlib -m calculator

7. Deberías ver la calculadora ejecutándose. Juega con diferentes operaciones para


asegurarte de que no haya problemas de clase no encontrados .

pág. 128
Con esto, hemos modularizado completamente la aplicación, dejando al descubierto las
bibliotecas de Jackson que hemos convertido en módulos automáticos.

Preferimos el enfoque de arriba hacia abajo para la migración. Esto se debe a que no
tenemos que lidiar con classpath y module-path al mismo tiempo. Podemos convertir
todo en módulos automáticos y luego usar la ruta del módulo a medida que
continuamos migrando los JAR no modulares a JAR modulares.

Usar servicios para crear un


acoplamiento flexible entre los
módulos de consumidor y
proveedor
En general, en nuestras aplicaciones, tenemos algunas interfaces y múltiples
implementaciones de esas interfaces. Luego, en tiempo de ejecución, dependiendo de
ciertas condiciones, hacemos uso de implementaciones específicas. Este principio se
llama Inversión de dependencia . Este principio lo utilizan los marcos de inyección de
dependencias, como Spring, para crear objetos de implementaciones concretas y
asignar (o inyectar) en las referencias del tipo de interfaz abstracto.

Durante mucho tiempo, Java (desde Java 6) ha soportado instalaciones de carga de


proveedores de servicios a través de la clase java.util.ServiceLoader. Con Service
Loader, puede tener una interfaz de proveedor de servicios ( SPI ) y múltiples
implementaciones de SPI simplemente llamadas proveedor de servicios. Estos
proveedores de servicios se encuentran en el classpath y se cargan en tiempo de
ejecución. Cuando estos proveedores de servicios se encuentran dentro de los módulos,
y como ya no dependemos de la exploración de classpath para cargar el proveedor de
servicios, necesitamos un mecanismo para informar a nuestros módulos sobre el
proveedor de servicios y la interfaz del proveedor de servicios para los que está
proporcionando una implementación. En esta receta, veremos este mecanismo
utilizando un ejemplo simple.

Prepararse
No hay nada específico que necesitemos configurar para esta receta. En esta receta,
tomaremos un ejemplo simple. Tenemos una clase BookService abstracta, que admite
operaciones CRUD. Ahora, estas operaciones CRUD pueden funcionar en una base de
datos SQL, MongoDB, un sistema de archivos, etc. Esta flexibilidad se puede

pág. 129
proporcionar mediante el uso de la interfaz del proveedor de servicios y la clase
ServiceLoader para cargar la implementación requerida del proveedor de servicios.

Cómo hacerlo...
Tenemos cuatro módulos en esta receta:

• book.service: Este es el módulo que contiene nuestra interfaz de proveedor de


servicios, es decir, el servicio
• mongodb.book.service: Este es uno de los módulos del proveedor de servicios
• sqldb.book.service: Este es el otro módulo de proveedor de servicios
• book.manage: Este es el módulo de consumidor de servicios

Los siguientes pasos demuestran cómo utilizar ServiceLoader para lograr un


acoplamiento flojo:

1. Crea una carpeta book.service debajo del directorio


Chapter03/8_services/src. Todo nuestro código para el módulo book.service
estará debajo de esta carpeta.
2. Cree un nuevo paquete, com.packt.model y una nueva clase, Book bajo el nuevo
paquete. Esta es nuestra clase de modelo, que contiene las siguientes propiedades:

public String id;


public String title;
public String author;

3. Cree un nuevo paquete, com.packt.service y una nueva clase, BookService bajo el


nuevo paquete. Esta es nuestra interfaz de servicio principal, y los proveedores de servicios
proporcionarán una implementación para este servicio. Además de los métodos abstractos
para operaciones CRUD, un método que vale la pena mencionar es getInstance(). Este
método usa la clase ServiceLoader para cargar cualquier proveedor de servicios (el último,
para ser específico) y luego usa ese proveedor de servicios para obtener una
implementación de BookService. Veamos el siguiente código:

public static BookService getInstance(){


ServiceLoader<BookServiceProvider> sl =
ServiceLoader.load(BookServiceProvider.class);
Iterator<BookServiceProvider> iter = sl.iterator();
if (!iter.hasNext())
throw new RuntimeException("No service providers found!");

BookServiceProvider provider = null;


while(iter.hasNext()){
provider = iter.next();
System.out.println(provider.getClass());
}
return provider.getBookService();
}

pág. 130
El primer ciclo while es solo para demostrar que ServiceLoader carga a todos los
proveedores de servicios y elegimos a uno de los proveedores de servicios. También
puede devolver condicionalmente el proveedor de servicios, pero todo depende de los
requisitos.

4. La otra parte importante es la interfaz real del proveedor de servicios. La


responsabilidad de esto es devolver una instancia apropiada de la implementación del
servicio. En nuestra receta, BookServiceProvider en el paquete com.packt.spi hay una
interfaz de proveedor de servicios:

public interface BookServiceProvider{


public BookService getBookService();
}

5. Creamos module-info.javabajo el directorio


Chapter03/8_services/src/book.service , que contiene lo siguiente:

module book.service{
exports com.packt.model;
exports com.packt.service;
exports com.packt.spi;
uses com.packt.spi.BookServiceProvider;
}

La declaración uses en la definición del módulo anterior especifica la interfaz de


servicio que el módulo descubre usando ServiceLoader.

6. Ahora creemos un módulo de proveedor de servicios


llamado mongodb.book.service. Esto proporcionará una aplicación para nuestra interfaz
BookServicee BookServiceProvider en el módulo book.service. Nuestra idea es que este
proveedor de servicios implemente las operaciones CRUD utilizando el almacén de datos
MongoDB.
7. Crea una carpeta mongodb.book.service debajo del directorio
Chapter03/8_services/src.
8. Cree una clase MongoDbBookService en el paquete com.packt.mongodb.service ,
que amplía la clase BookService abstracta y proporciona una implementación de
nuestros métodos de operación CRUD abstractos:
9. public void create(Book book){
System.out.println("Mongodb Create book ... " + book.title);
}

public Book read(String id){


System.out.println("Mongodb Reading book ... " + id);
return new Book(id, "Title", "Author");
}

public void update(Book book){


System.out.println("Mongodb Updating book ... " +
book.title);
}

public void delete(String id){

pág. 131
System.out.println("Mongodb Deleting ... " + id);
}

9. Cree una clase MongoDbBookServiceProvider en el paquete com.packt.mongodb, que


implementa la interfaz BookServiceProvider. Esta es nuestra clase de descubrimiento de
servicio. Básicamente, devuelve una instancia relevante de la implementación
BookService. Reemplaza el método en la interfaz BookServiceProvider, de la siguiente
manera:

@Override
public BookService getBookService(){
return new MongoDbBookService();
}

10. La definición del módulo es bastante interesante. Tenemos que declarar en la


definición del módulo que este módulo es un proveedor de servicios para la
interfaz BookServiceProvider, y eso se puede hacer de la siguiente manera:

module mongodb.book.service{
requires book.service;
provides com.packt.spi.BookServiceProvider
with com.packt.mongodb.MongoDbBookServiceProvider;
}

La declaración provides .. with ..se utiliza para especificar la interfaz del servicio y
uno de los proveedores de servicios.

11. Ahora creemos un módulo de consumidor de servicios llamado book.manage.


12. Cree una nueva carpeta book.manage en la Chapter03/8_services/src que
contendrá el código para el módulo.
13. Cree una nueva clase llamada BookManager en el paquete com.packt.manage. El
objetivo principal de esta clase es obtener una instancia BookService y luego
ejecutar sus operaciones CRUD. La instancia devuelta la deciden los proveedores de
servicios cargados por ServiceLoader. La BookManagerclase se parece a esto:

public class BookManager{


public static void main(String[] args){
BookService service = BookService.getInstance();
System.out.println(service.getClass());
Book book = new Book("1", "Title", "Author");
service.create(book);
service.read("1");
service.update(book);
service.delete("1");
}
}

14. Vamos a compilar y ejecutar nuestro módulo principal usando los siguientes
comandos:

$ javac -d mods --module-source-path src


$(find src -name *.java)

pág. 132
$ java --module-path mods -m
book.manage/com.packt.manage.BookManager
class com.packt.mongodb.MongoDbBookServiceProvider
class com.packt.mongodb.service.MongoDbBookService
Mongodb Create book ... Title
Mongodb Reading book ... 1
Mongodb Updating book ... Title
Mongodb Deleting ... 1

En el resultado anterior, la primera línea indica los proveedores de servicios que están
disponibles y la segunda línea indica qué implementación de BookService estamos
utilizando.

15. Con un proveedor de servicios, parece simple. Avancemos y agreguemos otro


módulo sqldb.book.service, cuya definición de módulo sería la siguiente:

module sqldb.book.service{
requires book.service;
provides com.packt.spi.BookServiceProvider
with com.packt.sqldb.SqlDbBookServiceProvider;
}

16. La clase SqlDbBookServiceProvider en el paquete com.packt.sqldb es una


implementación de la interfaz BookServiceProvider , como sigue:

@Override
public BookService getBookService(){
return new SqlDbBookService();
}

17. La implementación de las operaciones CRUD es realizada por


la clase SqlDbBookService en el paquete del paquete com.packt.sqldb.service.
18. Vamos a compilar y ejecutar el módulo principal, esta vez con dos proveedores de
servicios:

$ javac -d mods --module-source-path src


$(find src -name *.java)
$ java --module-path mods -m
book.manage/com.packt.manage.BookManager
class com.packt.sqldb.SqlDbBookServiceProvider
class com.packt.mongodb.MongoDbBookServiceProvider
class com.packt.mongodb.service.MongoDbBookService
Mongodb Create book ... Title
Mongodb Reading book ... 1
Mongodb Updating book ... Title
Mongodb Deleting ... 1

Las primeras dos líneas imprimen los nombres de clase de los proveedores de
servicios disponibles y la tercera línea imprime qué implementación de BookService
estamos usando.

pág. 133
Crear una imagen de tiempo de
ejecución modular personalizada
usando jlink
Java viene en dos sabores:

• Java runtime only, también conocido como JRE: admite la ejecución de aplicaciones
Java
• Kit de desarrollo de Java con Java Runtime, también llamado JDK: esto admite el
desarrollo y la ejecución de aplicaciones Java

Aparte de esto, hay tres perfiles compactos introducidas en Java 8 con el objetivo de
proporcionar tiempos de ejecución con una huella más pequeña con el fin de funcionar
en dispositivos más pequeños incrustados y se muestran como sigue:

La imagen anterior muestra los diferentes perfiles y las funciones compatibles con ellos.

Se introdujo jlink una nueva herramienta, llamada Java 9, que permite la creación
de imágenes modulares en tiempo de ejecución. Estas imágenes de tiempo de ejecución
pág. 134
no son más que una colección de un conjunto de módulos y sus dependencias. Existe
una propuesta de mejora de Java, JEP 220, que rige la estructura de esta imagen de
tiempo de ejecución.

En esta receta, vamos a utilizar jlink para crear una imagen en tiempo de ejecución
que consiste en nuestros math.util, banking.util y calculator los módulos,
junto con los módulos automáticos Jackson.

Prepararse
En Crear una receta de aplicación modular simple , creamos una aplicación modular
simple que consta de los siguientes módulos:

• math.util
• calculator: Consiste en la clase principal

Reutilizaremos el mismo conjunto de módulos y código para demostrar el uso de


la jlinkherramienta. Para la comodidad de nuestros lectores, el código se puede
encontrar en Chapter03/9_jlink_modular_run_time_image.

Cómo hacerlo...
1. Vamos a compilar los módulos:
$ javac -d mods --module-path mlib --module-source-path
src $ (buscar src - nombre * .java)

2. Creemos el JAR modular para todos los módulos:


$ jar --create --file mlib / math.util.jar -C mods / math.util.

$ jar --create --file = mlib / calculator.jar --main-


class = com.packt.calculator.Calculator -C mods / calculator /.

3. Vamos a utilizar jlink para crear una imagen en tiempo de ejecución


que consiste en calculator y los módulos math.util y sus dependencias:
$ jlink --module-path mlib: $ JAVA_HOME / jmods --add-modules
calculator, math.util --output image --launcher
launch = calculator / com.packt.calculator.Calculator

La imagen de tiempo de ejecución se crea en la ubicación especificada con


la opción --output de línea de comandos.

pág. 135
4. La imagen de tiempo de ejecución creada bajo la imagen
del directorio bin contiene el directorio, entre otros
directorios. Este bindirectorio consta de un script de shell
llamado calculator. Esto se puede usar para iniciar nuestra aplicación:
$ ./image/bin/launch

************ Calculadora avanzada ************


1. Verificación de números primos
2. Verificación de números pares
3. Suma de N primos
4. Suma de N Evens
5. Suma de N Odds
6. Salir
Ingrese el número para elegir la operación

No podemos crear una imagen de tiempo de ejecución de los módulos que


contienen módulos automáticos. Jlink da un error si los archivos JAR no son
modulares o si no los hay module-info.class.

Compilación para versiones de


plataforma anteriores
En algún momento, hemos utilizado las opciones -sourcey -target para crear
una compilación de Java. La opción -source se usa para indicar la versión del
lenguaje Java aceptada por el compilador, y la -targetopción se usa para indicar la
versión admitida por los archivos de clase. A menudo, olvidamos usar la opción -
source y, de forma predeterminada, javac compila con la última versión de Java
disponible. Debido a esto, hay posibilidades de que se utilicen API más nuevas y, como
resultado, la compilación no se ejecuta como se esperaba en la versión de destino.

Para superar la confusión de proporcionar dos opciones de línea de comandos


diferentes, una nueva opción de línea de comandos, --release se introdujo en Java
9. Esto actúa como un sustituto para el -source, -target y -
bootclasspath opciones. -bootclasspathse utiliza para proporcionar la
ubicación de los archivos de clase de rutina de carga para una versión dada, N .

Prepararse
Hemos creado un módulo simple, llamado demo, que contiene una clase muy simple
llamada CollectionsDemo que solo pone algunos valores en el mapa y los repite
de la siguiente manera:

pág. 136
public class CollectionsDemo {
public static void main (String [] args) {
Map <String, String> map = new HashMap <> ();
map.put ("clave1", "valor1");
map.put ("clave2", "valor3");
map.put ("clave3", "valor3");
map.forEach ((k, v) -> System.out.println (k + "," +
v));
}
}

Vamos a compilarlo y ejecutarlo para ver su salida:

$ javac -d mods --module-source-path src srcdemomodule-


info.java srcdemocompacktCollectionsDemo.java
$ java --module-path mods -m demo /
com.packt.CollectionsDemo

El resultado que obtenemos es el siguiente:

clave1, valor1
clave2, valor3
clave3, valor3

Ahora compilemos esto para ejecutarlo en Java 8 y luego ejecutarlo en Java 8.

Cómo hacerlo...
1. Como las versiones anteriores de Java , Java 8 y anteriores, no admiten módulos,
por lo que tendríamos que deshacernos de module-info.java si
compiláramos una versión anterior. Es por eso que no incluimos module-
info.java durante nuestra compilación. Compilamos usando el siguiente
código:
$ javac --release 8 -d mods
srcdemocompacktCollectionsDemo.java

Puede ver que estamos usando la opción –release, apuntando a Java 8 y no


compilando module-info.java.

2. Creemos un archivo JAR porque es más fácil transportar la compilación de Java en


lugar de copiar todos los archivos de clase:

$ jar --create --file mlib / demo.jar --main-class


com.packt.CollectionsDemo -C mods /.

pág. 137
3. Ejecutemos el JAR anterior en Java 9:

$ java -version
java version "9"
Java(TM) SE Runtime Environment (build 9+179)
Java HotSpot(TM) 64-Bit Server VM (build 9+179, mixed mode)

$ java -jar mlib/demo.jar


key1, value1
key2, value3
key3, value3

4. Ejecutemos el JAR en Java 8:

$ "%JAVA8_HOME%"binjava -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

$ "%JAVA8_HOME%"binjava -jar mlibdemo.jar


key1, value1
key2, value3
key3, value3

¿Qué pasa si no usamos la -releaseopción al construir en Java 9? Probemos eso


también:

1. Compile sin usar la --releaseopción y cree un JAR a partir de los archivos de


clase resultantes:

$ javac -d mods srcdemocompacktCollectionsDemo.java


$ jar --create --file mlib/demo.jar --main-class
com.packt.CollectionsDemo -C mods/ .

2. Ejecutemos el JAR en Java 9:

$ java -jar mlib/demo.jar


key1, value1
key2, value3
key3, value3

Funciona como se esperaba.

3. Ejecutemos el JAR en Java 8:

$ "%JAVA8_HOME%"binjava -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

El resultado es el siguiente:

pág. 138
$ java -jar mlibdemo.jar

Exception in thread "main" java.lang.UnsupportedClassVersionError:

com/packt/CollectionsDemo has been compiled by a more recent version of


the Java Runtime (class file version 53.0), this version of the Java
Runtime only recognizes class file versions up to 52.0

Está indicando claramente que hay una falta de coincidencia en la versión del archivo
de clase. Como se compiló para Java 9 (versión 53.0), no se ejecuta en Java 8 (versión
52.0).

Cómo funciona...
Los datos necesarios para compilar en una versión anterior de destino se almacenan en
el archivo $JDK_ROOT/lib/ct.sym. Esta información es utilizada por la opción –
release de localizar bootclasspath. El ct.sym archivo es un archivo ZIP que
contiene archivos de clase despojados correspondientes a los archivos de clase de las
versiones de la plataforma de destino (tomado literalmente
de http://openjdk.java.net/jeps/247 ).

Crear JAR de lanzamiento


múltiple
Antes de Java 9, era difícil para los desarrolladores de una biblioteca adoptar las nuevas
características introducidas en el lenguaje sin lanzar una nueva versión de la
biblioteca. Pero en Java 9, los JAR de versiones múltiples proporcionan una
funcionalidad en la que puede agrupar ciertos archivos de clase para que se ejecuten
cuando se utiliza una versión superior de Java.

En esta receta, le mostraremos cómo crear un JAR de lanzamiento múltiple.

Cómo hacerlo...
1. Cree el código Java requerido para la plataforma Java 8. Agregaremos dos
clases CollectionUtil.javay FactoryDemo.java, en el directorio
src8compackt:

public class CollectionUtil{


public static List<String> list(String ... args){
System.out.println("Using Arrays.asList");
return Arrays.asList(args);
}

pág. 139
public static Set<String> set(String ... args){
System.out.println("Using Arrays.asList and set.addAll");
Set<String> set = new HashSet<>();
set.addAll(list(args));
return set;
}
}

public class FactoryDemo{


public static void main(String[] args){
System.out.println(CollectionUtil.list("element1",
"element2", "element3"));
System.out.println(CollectionUtil.set("element1",
"element2", "element3"));
}
}

2. Deseamos hacer uso de los Collectionmétodos de fábrica que se introdujeron en


Java 9. Por lo tanto, vamos a crear otro subdirectorio bajo srcpara colocar nuestro código
relacionado con Java-9: src9compackt. Aquí es donde agregaremos
otra CollectionUtilclase:

public class CollectionUtil{


public static List<String> list(String ... args){
System.out.println("Using factory methods");
return List.of(args);
}
public static Set<String> set(String ... args){
System.out.println("Using factory methods");
return Set.of(args);
}
}

3. El código anterior utiliza los métodos de fábrica de la colección Java 9. Compile el


código fuente usando los siguientes comandos:

javac -d mods - versión 8 src8compackt * .java


javac -d mods9 - versión 9 src9compackt * .java

Tome nota de la --releaseopción que se utiliza para compilar el código para


diferentes versiones de Java.

4. Ahora creemos el JAR de lanzamiento múltiple:


jar --create --file mr.jar --main-class=com.packt.FactoryDemo
-C mods . --release 9 -C mods9 .

Al crear el JAR, también hemos mencionado que, cuando se ejecuta en Java 9,


utilizamos el código específico de Java-9.

pág. 140
5. Vamos a correr mr.jaren Java 9:
java -jar mr.jar
[element1, element2, element3]
Using factory methods
[element2, element3, element1]

6. Vamos a correr mr.jaren Java 8:


#Linux
$ /usr/lib/jdk1.8.0_144/bin/java -version
versión java "1.8.0_144"
Java (TM) SE Runtime Environment (compilación 1.8.0_144-b01)
Java HotSpot (TM) 64-Bit Server VM (compilación 25.144-b01, modo
mixto)
$ /usr/lib/jdk1.8.0_144/bin/java -jar mr.jar
Usando Arrays.asList
[element1, element2, element3]
Usando Arrays.asList y set.addAll
Usando Arrays. asList
[element1, element2, element3]

#Windows
$ "% JAVA8_HOME%" binjava -version
versión java "1.8.0_121"
Java (TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot (TM) Servidor de 64 bits VM (compilación 25.121-b13,
modo mixto)
$ "% JAVA8_HOME%" binjava -jar mr.jar
Usando Arrays.asList
[element1, element2, element3]
Usando Arrays.asList y set.addAll
Usando Arrays.asList
[element1, element2, element3]

Cómo funciona...
Veamos el diseño del contenido en mr.jar:

jar -tvf mr.jar

El contenido del JAR es el siguiente:

pág. 141
En el diseño anterior, tenemos META-INF/versions/9, que contiene el código
específico de Java 9. Otra cosa importante a tener en cuenta es el contenido del META-
INF/MANIFEST.MFarchivo. Vamos a extraer el JAR y ver su contenido:

jar -xvf mr.jar

$ cat META-INF / MANIFEST.MF


Versión de manifiesto: 1.0
Creado por: 9 (Oracle Corporation)
Clase principal: com.packt.FactoryDemo
Multi-Release: true

El nuevo atributo Multi-Release manifiesto se usa para indicar si el JAR es un


JAR de lanzamiento múltiple.

Usando Maven para desarrollar


una aplicación modular
En esta receta, analizaremos el uso de Maven, la herramienta de compilación más
popular en el ecosistema de Java, para desarrollar una aplicación modular
simple. Reutilizaremos la idea que presentamos en la receta de Servicios en este
capítulo.

Prepararse
Tenemos los siguientes módulos en nuestro ejemplo:

• book.manage: Este es el módulo principal que interactúa con la fuente de datos


• book.service: Este es el módulo que contiene la interfaz del proveedor de
servicios
• mongodb.book.service: Este es el módulo que proporciona una
implementación a la interfaz del proveedor de servicios

pág. 142
• sqldb.book.service: Este es el módulo que proporciona otra implementación
a la interfaz del proveedor de servicios

En el curso de esta receta, crearemos un proyecto maven e incluiremos los módulos


JDK anteriores como módulos maven. Entonces empecemos.

Cómo hacerlo...
1. Cree una carpeta para contener todos los módulos. Lo hemos
llamado 12_services_using_maven con la siguiente estructura de carpetas:

12_services_using_maven
| --- book-manage
| --- book-service
| --- mongodb-book-service
| --- sqldb-book-service
| --- pom.xml

2. El pom.xmlpara el padre es el siguiente:

<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packt</groupId>
<artifactId>services_using_maven</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>book-service</module>
<module>mongodb-book-service</module>
<module>sqldb-book-service</module>
<module>book-manage</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>9</source>
<target>9</target>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
</plugins>
</build>
</project>

pág. 143
3. Creemos la estructura para el módulo book-service Maven de la siguiente
manera:
book-service
|---pom.xml
|---src
|---main
|---book.service
|---module-info.java
|---com
|---packt
|---model
|---Book.java
|---service
|---BookService.java
|---spi
|---BookServiceProvider.java

4. El contenido de módulo pom.xmlla book-service de Maven es:

<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.packt</groupId>
<artifactId>services_using_maven</artifactId>
<version>1.0</version>
</parent>
<artifactId>book-service</artifactId>
<version>1.0</version>
<build>
<sourceDirectory>src/main/book.service</sourceDirectory>
</build>
</project>

5. Aqui esta module-info.java:

module book.service{
exports com.packt.model;
exports com.packt.service;
exports com.packt.spi;
uses com.packt.spi.BookServiceProvider;
}

6. Aqui esta Book.java:

public class Book{


public Book(String id, String title, String author){
this.id = id;
this.title = title
this.author = author;
}
public String id;

pág. 144
public String title;
public String author;
}

7. Aqui esta BookService.java:

public abstract class BookService{


public abstract void create(Book book);
public abstract Book read(String id);
public abstract void update(Book book);
public abstract void delete(String id);
public static BookService getInstance(){
ServiceLoader<BookServiceProvider> sl =
ServiceLoader.load(BookServiceProvider.class);
Iterator<BookServiceProvider> iter = sl.iterator();
if (!iter.hasNext())
throw new RuntimeException("No service providers found!");
BookServiceProvider provider = null;
while(iter.hasNext()){
provider = iter.next();
System.out.println(provider.getClass());
}
return provider.getBookService();
}
}
}

8. Aqui esta BookServiceProvider.java:

public interface BookServiceProvider{


public BookService getBookService();
}

En una línea similar, definimos los otros tres módulos Maven, mongodb-book-
service, sqldb-book-service, y book-manager. El código para esto se puede
encontrar en Chapter03/12_services_using_maven.

Podemos compilar las clases y construir los archivos JAR requeridos usando el
siguiente comando:

mvn clean install

Hemos proporcionado run-with-mongo.*para usar mongodb-book-service


como la implementación del proveedor de servicios y run-with-sqldb.*para
usar sqldb-book-service como la implementación del proveedor de servicios.

El código completo de esta receta se puede encontrar


en Chapter03/12_services_using_maven.

pág. 145
Hacer que su biblioteca sea
amigable con los módulos
Para que una aplicación sea completamente modular, debería haberse modularizado a
sí misma y sus dependientes. Ahora, hacer que un tercero sea modular no está en manos
del desarrollador de la aplicación. Un enfoque es incluir el jar de terceros en la ruta
del módulo y usar el nombre jar como el nombre del módulo para declarar la
dependencia. En tales casos, se jarconvierte en un módulo automático. Esto está bien,
pero a menudo el nombre del módulo jar no es compatible con el nombre del módulo
o no se ajusta a la sintaxis de un nombre de módulo válido. En tales casos, hacemos uso
de otro soporte agregado en JDK 9 en el que se puede definir el nombre jar en
el MANIFEST.mfarchivo deljar, y el consumidor de la biblioteca puede declarar una
dependencia en el nombre definido. De esta manera, en el futuro, el desarrollador de la
biblioteca puede modularizar su biblioteca mientras usa el mismo nombre de módulo.

En esta receta, le mostraremos cómo proporcionar un nombre para el módulo


automático creado a partir del jar no modular. Primero, le mostraremos cómo lograr
esto usando Maven y luego en la sección Hay más ... , veremos cómo crear un JAR sin
usar ninguna herramienta de compilación.

Prepararse
Necesitaría al menos JDK 9 para ejecutar esta receta, pero utilizaremos JDK 11 en
el complemento de compilación de Maven . También necesitará instalar Maven para
poder usarlo. Puede buscar en Internet para encontrar el procedimiento de instalación
de Maven .

Cómo hacerlo...
1. Genere un proyecto vacío usando Maven :

mvn archetype:generate -DgroupId=com.packt.banking -


DartifactId=13_automatic_module -DarchetypeArtifactId=maven-archetype-
quickstart -DinteractiveMode=false

2. Actualice las dependencias en el pom.xmlarchivo ubicado en el directorio


13_automatic_module copiando las siguientes dependencias:

<dependencies>
<dependency>
<groupId>junit</groupId>

pág. 146
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>

3. Necesitamos configurar maven-compiler-plugin para poder compilar para


JDK 11. Entonces, agregaremos la siguiente configuración de complemento
inmediatamente después <dependencies></dependencies>:

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>11</source>
<target>11</target>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
</plugins>
</build>

4. Configure maven-jar-pluginpara proporcionar el nombre del módulo


automático proporcionando el nombre en la nueva etiqueta <Automatic-Module-
Name> , como se muestra aquí:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.packt.banking</Automatic-Module-
Name>
</manifestEntries>
</archive>
</configuration>
</plugin>

5. Agregaremos una API para calcular el interés simple en la clase


com.packt.banking.Banking, que se muestra a continuación:

public class Banking {


public static Double simpleInterest(Double principal,
Double rateOfInterest, Integer years){

pág. 147
Objects.requireNonNull(principal, "Principal cannot be null");
Objects.requireNonNull(rateOfInterest,
"Rate of interest cannot be null");
Objects.requireNonNull(years, "Years cannot be null");
return ( principal * rateOfInterest * years ) / 100;
}
}

6. También agregamos una prueba, que puede


encontrar Chapter03\13_automatic_module\src\test\java\com\packt\
banking en el código descargado para este capítulo. Ejecutemos el comando mvn
package para construir un JAR. Si todo va bien, verá lo siguiente:

7. Puede usar cualquier utilidad de compresión, como 7z, para ver el contenido del
JAR, especialmente el archivo Manifest.MF , cuyo contenido es el siguiente:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.3.9
Built-By: sanaulla
Build-Jdk: 11-ea
Automatic-Module-Name: com.packt.banking

El código para estos pasos se puede encontrar


en Chapter03\13_automatic_module.

Cómo funciona...
Hasta ahora, hemos creado una biblioteca Java JAR con un nombre de módulo
automático. Ahora, veamos cómo usar este JAR no modular como módulo automático
pág. 148
en una aplicación modular. El código completo para esto se puede encontrar
en Chapter03\13_using_automatic_module.

Copiemos el archivo jar creado en la sección Cómo hacerlo ..., que puedes
encontrar 13_automatic_module\target\13_automatic_module-
1.0.jar, en la carpeta 13_using_automatic_module\mods . Esto permite que
nuestra próxima aplicación modular haga uso del módulo com.packt.banking que
se envió con jar.

Después de copiar el jar, necesitamos crear una definición de módulo para nuestro
módulo y declarar sus dependencias module-info.java, ubicadas
en 13_using_automatic_module\src\banking.demo:

module banking.demo{
requires com.packt.banking;
}

Lo siguiente es crear la clase principal com.packt.demo.BankingDemo, que


utilizará las utilidades bancarias. Esto se creará en la ruta
13_using_automatic_module\src\banking.demo\com\packt\demo de la
siguiente manera:

package com.packt.demo;
import com.packt.banking.Banking;
public class BankingDemo{
public static void main(String[] args) {
Double principal = 1000.0;
Double rateOfInterest = 10.0;
Integer years = 2;
Double simpleInterest = Banking.simpleInterest(principal,
rateOfInterest, years);
System.out.println("The simple interest is: " +
simpleInterest);
}
}

Podemos compilar el código anterior usando el siguiente comando, ejecutado


desde 13_using_automatic_module:

javac -d mods -p mods --module-source-path src src\banking.demo\*.java


src\banking.demo\com\packt\demo\*.java

Y luego ejecute el código anterior con el siguiente comando, ejecutado desde la misma
ubicación:

java --module-path mods -m banking.demo/com.packt.demo.BankingDemo

Verá el siguiente resultado:

The simple interest is: 200.0

pág. 149
Nota: Puede utilizar los scripts run.bato run.sh para compilar y ejecutar el
código.

Entonces, con esto, tenemos:

• Creó un JAR no modular con un nombre de módulo automático.


• Utilizó el JAR no modular como un módulo automático al declarar una dependencia
de él mediante el uso de su nombre de módulo automático.

También verá que hemos eliminado por completo el uso de classpath, en lugar de
utilizar solo la ruta del módulo; Este es nuestro primer paso hacia una aplicación
completamente modular.

Hay más...
Le mostraremos cómo crear un JAR de su utilidad bancaria, junto con el nombre del
módulo automático si no utiliza Maven . El código para esto se puede encontrar
en Chapter03\13_automatic_module_no_maven. Todavía tendremos el
mismo Banking .javacopiado en el directorio
13_automatic_module_no_maven\src\com\packt\banking .

A continuación, debemos definir un archivo manifest.mf de manifiesto que


contendrá el siguiente nombre de módulo automático:

Automatic-Module-Name: com.packt.banking

Podemos compilar la clase anterior emitiendo el siguiente comando


desde Chapter03\13_automatic_module_no_maven:

javac -d classes src/com/packt/banking/*.java

Y luego construya un jaremitiendo el siguiente comando desde la misma ubicación:

jar cvfm banking-1.0.jar manifest.mf -C classes .

También hemos proporcionado scripts para crear su jar. Puede usar build-
jar.bato build-jar.sh para compilar y crear un jar. Ahora, puede
copiar banking-
1.0.jara Chapter03\13_using_automatic_module\modsy
reemplazar 13_automati_module-1.0.jar. Luego, ejecute el
código Chapter03\13_using_automatic_module usando
los scripts run.bato run.sh, dependiendo de su plataforma. Aún verá el mismo
resultado que en la sección anterior.

pág. 150
Cómo abrir un módulo para
reflexionar
El sistema de módulos introduce una encapsulación estricta de clases dentro de su
módulo y un nivel de rigor que, si la clase no está explícitamente permitida para la
reflexión, no se puede acceder a sus miembros privados a través de la reflexión. La
mayoría de las bibliotecas, como hibernate y Jackson, dependen de la reflexión para
lograr su propósito. Una encapsulación estricta ofrecida por el sistema de módulos
rompería estas bibliotecas en el nuevo JDK 9 y más adelante de inmediato.

Para admitir bibliotecas tan importantes, el equipo de Java decidió introducir


características en las que el desarrollador del módulo puede declarar algunos paquetes
o paquetes completos que están abiertos para inspección por reflexión. En esta receta,
veremos cómo exactamente lograr eso.

Prepararse
Necesita JDK 9 o posterior instalado. Usaremos la API de Jackson en esta receta, y
sus jararchivos se pueden encontrar
en Chapter03/14_open_module_for_rflxn/mods la descarga del código
para este libro. Estos jararchivos son importantes ya que crearemos una cadena
JSON a partir de un objeto Java utilizando la API de Jackson. Estas API de Jackson se
utilizarán como módulos automáticos.

Cómo hacerlo...
1. Cree una clase Person
14_open_module_for_rflxn/src/demo/com/packt/demo con la
siguiente definición:

package com.packt.demo;

import java.time.LocalDate;

public class Person{


public Person(String firstName, String lastName,
LocalDate dob, String placeOfBirth){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.placeOfBirth = placeOfBirth;
}
public final String firstName;

pág. 151
public final String lastName;
public final LocalDate dob;
public final String placeOfBirth;
}

2. Cree una clase OpenModuleDemo que cree una instancia de la clase Person y
use com.fasterxml.jackson.databind.ObjectMapper para serializarla en
JSON. La serialización de las nuevas API de fecha y hora requiere algunos cambios de
configuración en la instancia ObjectMapper, que también se ha realizado en el bloque
de inicialización estática, de la siguiente manera:

package com.packt.demo;

import java.time.LocalDate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

public class OpenModuleDemo{


final static ObjectMapper MAPPER = new ObjectMapper();
static{
MAPPER.registerModule(new JavaTimeModule());
MAPPER.configure(SerializationFeature.
WRITE_DATES_AS_TIMESTAMPS, false);
}
public static void main(String[] args)
throws Exception {
Person p = new Person("Mohamed", "Sanaulla",
LocalDate.now().minusYears(30), "India");
String json = MAPPER.writeValueAsString(p);
System.out.println("The Json for Person is: ");
System.out.println(json);
}
}

3. Crear module-info.java
en 14_open_module_for_rflxn/src/demo, que declara el nombre del módulo,
sus dependencias y otra cosa interesante llamada opens. Opens es la solución para
permitir la reflexión desde bibliotecas externas, como se muestra aquí:

module demo{
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.datatype.jsr310;
opens com.packt.demo;
}

Cómo funciona...
Hay dos formas de abrir un módulo para inspección por reflexión:

pág. 152
• Declarando abierto en el nivel del módulo:

open module demo { }

• La declaración se abre en el nivel de paquete individual:

module demo {
opens com.packt.demo;
}

• El último es más restrictivo (es decir, solo hace que un paquete esté disponible
para la reflexión) que el primero. Hay otra forma de lograr esto, y es
exportando el paquete específico al paquete Jackson correcto, de la siguiente
manera:
module demo{
exports com.packt.demo to <relevant Jackson package here>
}

Funcionando
Este capítulo presenta un paradigma de programación llamado programación
funcional y su aplicabilidad en Java 11. Cubriremos las siguientes recetas:

• Usar interfaces funcionales estándar


• Crear una interfaz funcional
• Comprender las expresiones lambda
• Usar expresiones lambda
• Usando referencias de métodos
• Aprovechando expresiones lambda en tus programas

Introducción
La programación funcional es la capacidad de tratar una determinada pieza de
funcionalidad como un objeto y pasarla como un parámetro o el valor de retorno de un
método. Esta característica está presente en muchos lenguajes de programación, y Java
la adquirió con el lanzamiento de Java 8.

Evita crear una clase, su objeto y administrar el estado del objeto. El resultado de una
función depende solo de los datos de entrada, sin importar cuántas veces se llame. Este
estilo hace que el resultado sea más predecible, que es el aspecto más atractivo de la
programación funcional.

Su introducción a Java también nos permite mejorar las capacidades de programación


paralela en Java al cambiar la responsabilidad del paralelismo del código del cliente a
la biblioteca. Antes de esto, para procesar elementos de colecciones de Java, el código

pág. 153
del cliente tenía que adquirir un iterador de la colección y organizar el procesamiento
de la colección.

Algunos de los métodos predeterminados de las colecciones Java aceptan una función
(una implementación de una interfaz funcional) como parámetro y luego la aplican a
cada elemento de la colección. Por lo tanto, es responsabilidad de la biblioteca
organizar el procesamiento. Un ejemplo es el método forEach(Consumer) que está
disponible en cada interfaz Iterable, donde hay una interfaz funcional. Otro ejemplo
es el método que está disponible para cada interfaz, donde también hay una interfaz
funcional. Además, los métodos y se agregaron a la interfaz, y el método se agregó
a . Consumer removeIf(Predicate) Collection Predicate sort(Compara
tor) replaceAll(UnaryOperator) Listcompute()Map

Las expresiones Lambda aprovechan las interfaces funcionales y simplifican


significativamente su implementación, haciendo que el código sea más corto, más claro
y más expresivo.

A lo largo de este capítulo, discutiremos las ventajas de la programación funcional,


definiremos y explicaremos las interfaces funcionales y las expresiones lambda, y
demostraremos todas las características relacionadas en ejemplos de código.

Hacer que los ciudadanos del lenguaje sean de primera clase agrega más poder a
Java. Pero aprovechar esta capacidad de lenguaje requiere , de aquellos que aún no
están expuestos a la programación funcional, una nueva forma de pensar y organizar el
código.

Explicar esta nueva característica y compartir las mejores prácticas de uso es el


propósito de este capítulo.

Usar interfaces funcionales


estándar
En esta receta, aprenderá qué es una interfaz funcional y por qué se agregó a Java, junto
con 43 interfaces funcionales listas para usar de la biblioteca Java estándar que viene
con JDK 8 en el paquete . java.util.function

Sin interfaces funcionales, la única forma de pasar una funcionalidad a un método sería
escribiendo una clase, creando su objeto y luego pasándolo como parámetro. Pero
incluso el estilo menos complicado, el uso de una clase anónima, requiere escribir
demasiado código. El uso de interfaces funcionales ayuda a evitar todo eso.

pág. 154
Prepararse
Cualquier interfaz que tenga uno y solo un método abstracto se llama interfaz
funcional. Para ayudar a evitar un error de tiempo de ejecución,
la anotación @FunctionalInterface se puede agregar frente a la interfaz. Le
informa al compilador sobre la intención, por lo que el compilador puede verificar si
realmente hay un método abstracto en esa interfaz, incluidos los heredados de otras
interfaces.

En nuestro código de demostración en los capítulos anteriores, ya hemos tenido un


ejemplo de una interfaz funcional, incluso si no la hemos anotado como funcional:

public interface SpeedModel {


double getSpeedMph(double timeSec, int weightPounds, int horsePower);
enum DrivingCondition {
ROAD_CONDITION,
TIRE_CONDITION
}
enum RoadCondition {
//...
}
enum TireCondition {
//...
}
}

La presencia de tipos enum o cualquier método implementado (predeterminado o


estático) no lo convierte en una interfaz no funcional. Solo cuentan los métodos
abstractos (no implementados). Entonces, este es un ejemplo de una interfaz
funcional también:

public interface Vehicle {


void setSpeedModel(SpeedModel speedModel);
default double getSpeedMph(double timeSec){ return -1; };
default int getWeightPounds(){ return -1; }
default int getWeightKg(){
return convertPoundsToKg(getWeightPounds());
}
private int convertPoundsToKg(int pounds){
return (int) Math.round(0.454 * pounds);
}
static int convertKgToPounds(int kilograms){
return (int) Math.round(2.205 * kilograms);
}
}

Para recapitular lo que ya aprendió sobre un método predeterminado de una interfaz


en Capítulo 2 , Fast Track to OOP - Clases e interfaces , el método
getWeightPounds() volverá -1cuando lo llame getWeightKg() o directamente,
utilizando el objeto de una clase que implementa la interfaz Vehicle. Sin embargo,

pág. 155
esto solo es cierto si el método getWeightPounds()no se implementa en una
clase. De lo contrario, se utilizará la implementación de la clase y devolverá un valor
diferente.

Además de los métodos de interfaz estáticos y predeterminados, una interfaz


funcional puede incluir cualquiera y todos los métodos abstractos de la base
java.lang.Object . En Java, cada objeto cuenta con la implementación
predeterminada de métodos java.lang.Object, por lo que el compilador y el
tiempo de ejecución de Java ignoran dichos métodos abstractos.

Por ejemplo, esta también es una interfaz funcional:

public interface SpeedModel {


double getSpeedMph(double timeSec, int weightPounds, int horsePower);
boolean equals(Object obj);
String toString();
}

Sin embargo, lo siguiente no es una interfaz funcional:

public interface Car extends Vehicle {


int getPassengersCount();
}

Esto se debe a que la interfaz Car tiene dos métodos abstractos : su propio método
getPassengersCount()y el método heredado de
la interfaz.setSpeedModel(SpeedModel speedModel)Vehicle

Podemos intentar agregar la anotación @FunctionalInterface a la interfaz


Car :

@FunctionalInterface
public interface Car extends Vehicle {
int getPassengersCount();
}

Si hacemos eso, el compilador genera el siguiente error:

El uso de la anotación @FunctionalInterface ayuda no solo a detectar errores en


tiempo de compilación, sino que también asegura una comunicación confiable de la
intención de diseño entre los programadores. Le ayuda a usted u otros programadores
recordar que esta interfaz no puede tener más de un método abstracto, lo cual es
especialmente importante cuando ya existe algún código que se basa en tal suposición.

pág. 156
Por la misma razón, las interfaces Runnabley Callable(han existido en Java desde
sus versiones anteriores) se anotaron como @FunctionalInterface en Java 8
para hacer explícita esta distinción:

@FunctionalInterface
interface Runnable { void run(); }

@FunctionalInterface
interface Callable<V> { V call() throws Exception; }

Cómo hacerlo...
Antes de crear su propia interfaz funcional, considere usar primero una de las 43
interfaces funcionales proporcionadas en el paquete java.util.function . La
mayoría de ellos son especializaciones de las interfaces
Function, Consumer, Supplier, y Predicate .

Los siguientes son los pasos que puede seguir para familiarizarse con las interfaces
funcionales:

1. Mira la interfaz Function<T,R> funcional:

Interfaz
pública @FunctionalInterface Función <T, R>

Como puede ver en los genéricos <T,R> , el único método de esta interfaz toma un
parámetro del T tipo y devuelve un valor del R tipo. De acuerdo con JavaDoc, esta
interfaz tiene el R apply(T t) método. Podemos crear una implementación de
esta interfaz usando una clase anónima:

Función <Integer, Double> ourFunc =


new Función <Integer, Double> () {
public Double apply (Integer i) {
return i * 10.0;
}
};

El método R apply(T t) en nuestra implementación acepta un valor del tipo


Integer (o el int primitivo, que se encuadrará automáticamente), lo multiplica
por 10 y devuelve el valor del tipo Double para que podamos usar nuestra nueva
función de la siguiente manera:

System.out.println (ourFunc.apply (1)); // impresiones: 10

En la receta que comprende las expresiones lambda a continuación, presentaremos una


expresión lambda y le mostraremos cómo su uso hace que la implementación sea
mucho más corta. Pero por ahora, continuaremos usando una clase anónima.

pág. 157
2. Mira la interfaz funcional Consumer<T> . El nombre nos ayuda a recordar que el
método de esta interfaz acepta un valor pero no devuelve nada , solo consume. Su único
método es void accept(T). La implementación de esta interfaz puede verse de la
siguiente manera:

Consumer <String> ourConsumer = new Consumer <String> () {


public void accept (String s) {
System.out.println ("El" + s + "se consume");
}
};

El método void accept(T t)en nuestra implementación recibe un valor del tipo
String y lo imprime. Por ejemplo, podemos usarlo de la siguiente manera:

ourConsumer.accept ("¡Hola!");
// impresiones: ¡Hola! se consume

3. Mira la interfaz funcional Supplier<T> . El nombre le ayuda a recordar que el


método de esta interfaz no acepta ningún valor pero devuelve algo, solo suministros. Su
único método es T get(). En base a esto, podemos crear una función:

Proveedor <String> ourSupplier = nuevo Proveedor <String> () {


public String get () {
String res = "Success";
// Hacer algo y devolver el resultado: éxito o error.
volver res;
}
};

El método T get()en nuestra implementación hace algo y luego devuelve un valor


del tipo String , por lo que podemos escribir lo siguiente:

System.out.println (ourSupplier.get ()); // impresiones: Éxito

4. Mira la interfaz funcional Predicate<T> . El nombre ayuda a recordar que el


método de esta interfaz devuelve un valor booleano: predica algo. Su único método
es boolean test(T t), lo que significa que podemos crear la siguiente función:

Predicate <Double> ourPredicate = new Predicate <Double> () {


prueba booleana pública (Double num) {
System.out.println ("Prueba si" + num +
"es menor que 20");
retorno num <20;
}
};

pág. 158
Su método boolean test(T t)de implementación acepta un valor del tipo
Double como parámetro y devuelve el valor del tipo boolean, por lo que podemos
usarlo de la siguiente manera:

System.out.println (ourPredicate.test (10.0)?


"10 es más pequeño": "10 es más grande");

El resultado de esto será el siguiente:

5. Mire las otras 39 interfaces funcionales en


el java.util.function paquete. Observe que son variaciones de las cuatro interfaces
que ya hemos discutido. Estas variaciones se crean por los siguientes motivos:

• Para un mejor rendimiento, evitando de auto-boxing y unboxing a través del


uso explícito de los int, double, o long primitivas
• Para aceptar dos parámetros de entrada
• Para una notación más corta

Las siguientes interfaces funcionales son solo algunos ejemplos de la lista de 39


interfaces.

La interfaz funcional IntFunction<R> tiene el método R apply(int


i) abstracto. Proporciona una notación más corta (sin genéricos para el tipo de
parámetro) y evita el auto-boxing (definiendo la primitiva intcomo el
parámetro). Aquí hay un ejemplo de su uso:

IntFunction<String> iFunc = new IntFunction<String>() {


public String apply(int i) {
return String.valueOf(i * 10);
}
};
System.out.println(iFunc.apply(1)); //prints: 10

La interfaz funcional BiFunction<T,U,R> tiene método abstracto, R


apply(T,U). Aquí hay un ejemplo de su implementación:

BiFunction<String, Integer, Double> biFunc =


new BiFunction<String, Integer, Double >() {
public Double apply(String s, Integer i) {
return (s.length() * 10d) / i;
}
};
System.out.println(biFunc.apply("abc", 2)); //prints: 15.0

pág. 159
La interfaz funcional BinaryOperator<T> tiene un método abstracto, T
apply(T,T). Proporciona una notación más corta al evitar repetir el mismo tipo tres
veces. Aquí hay un ejemplo de su uso:

BinaryOperator<Integer> function = new BinaryOperator<Integer>(){


public Integer apply(Integer i, Integer j) {
return i >= j ? i : j;
}
};
System.out.println(binfunc.apply(1, 2)); //prints: 2

La interfaz funcional IntBinaryOperator tiene el método abstracto int


applyAsInt(int,int) . Podemos usarlo para reproducir la misma funcionalidad
que en el ejemplo anterior:

IntBinaryOperator intBiFunc = new IntBinaryOperator(){


public int applyAsInt(int i, int j) {
return i >= j ? i : j;
}
};
System.out.println(intBiFunc.applyAsInt(1, 2)); //prints: 2

Se proporcionarán más ejemplos del uso de tales especializaciones en las siguientes


recetas.

Cómo funciona...
Podemos componer todo el método usando solo las funciones:

void calculate(Supplier<Integer> source,


Function<Integer, Double> process, Predicate<Double> condition,
Consumer<Double> success, Consumer<Double> failure){
int i = source.get();
double res = process.apply(i);
if(condition.test(res)){
success.accept(res);
} else {
failure.accept(res);
}
}

El código anterior obtiene el valor de la fuente, lo procesa y luego decide si el


resultado es exitoso , todo en función de las funciones proporcionadas como
parámetros. Ahora, creemos estas funciones e invoquemos el método. El parámetro
fuente decidimos ser el siguiente:

Supplier<Integer> source = new Supplier<Integer>() {


public Integer get() {
Integer res = 42;
//Do something and return result value
return res;

pág. 160
}
};

En el código de la vida real, esta función podría extraer datos de una base de datos o
de cualquier otra fuente de datos. Nos mantenemos simple - con un valor de retorno
codificada - con el fin de obtener un resultado predecible.

La función de procesamiento y el predicado permanecerán igual que antes:

Function<Integer, Double> process = new Function<Integer, Double>(){


public Double apply(Integer i){
return i * 10.0;
}
};
Predicate<Double> condition = new Predicate<Double>() {
public boolean test(Double num) {
System.out.println("Test if " + num +
" is smaller than " + 20);
return num < 20;
}
};

Y los consumidores serán casi idénticos, excepto por el prefijo diferente antes de
imprimir el resultado:

Consumer<Double> success = new Consumer<Double>() {


public void accept(Double d) {
System.out.println("Success: " + d);
}
};
Consumer<Double> failure = new Consumer<Double>() {
public void accept(Double d) {
System.out.println("Failure: " + d);
}
};

Ahora podemos invocar el método de cálculo, de la siguiente manera:

calculate(source, process, condition, success, failure);

Y el resultado será el siguiente:

Test if 420.0 is smaller than 20.0


Failure: 420.0

Si necesitamos probar rápidamente varias combinaciones del valor fuente y la


condición del predicado, podemos crear el método
testSourceAndCondition(int src, int limit) de la siguiente manera:

void testSourceAndCondition(int src, double condition) {


Supplier<Integer> source = new Supplier<Integer>() {
public Integer get() {

pág. 161
Integer res = src;
//Do something and return result value
return res;
}
};
Function<Integer, Double> process =
new Function<Integer, Double>() {
public Double apply(Integer i){
return i * 10.0;
}
};
Predicate<Double> condition = new Predicate<Double>() {
public boolean test(Double num) {
System.out.println("Test if " + num +
" is smaller than " + limit);
return num < limit;
}
};
Consumer<Double> success = new Consumer<Double>() {
public void accept(Double d) {
System.out.println("Success: " + d);
}
};
Consumer<Double> failure = new Consumer<Double>() {
public void accept(Double d) {
System.out.println("Failure: " + d);
}
};
calculate(source, process, cond, success, failure);
}

Observe cómo pasamos el valor src al proveedor source y el valor limit


al predicado condition . Ahora, podemos ejecutar el método
testSourceAndCondition(int src, int limit) con diferentes valores de
entrada en busca de la combinación del valor src y el valor limit que trae éxito:

testSourceAndCondition ( 10 , 20 ) ;
testSourceAndCondition ( 1 , 20 ) ;
testSourceAndCondition ( 10 , 200 ) ;

El resultado será el siguiente :

Test if 100.0 is smaller than 20.0


Failure: 100.0
Test if 10.0 is smaller than 20.0
Success: 10.0
Test if 100.0 is smaller than 200.0
Success: 100.0

Hay más...
pág. 162
Muchas de las interfaces funcionales en el paquete tienen métodos predeterminados
que no solo mejoran su funcionalidad, sino que también le permiten encadenar las
funciones y pasar el resultado de una como parámetro de entrada a otra. Por ejemplo,
podemos usar el método predeterminado de
la interfaz: java.util.function Function<T,V>
andThen(Function<R,V> after) Function<T,R>

Function<Integer, Double> before = new Function<Integer, Double>(){


public Double apply(Integer i){
return i * 10.0;
}
};
Function<Double, Double> after = new Function<Double, Double>(){
public Double apply(Double d){
return d + 10.0;
}
};
Function<Integer, Double> process = before.andThen(after);

Como puede ver, nuestra función process ahora es una combinación de nuestra
función original (que multiplica el valor fuente por 10.0) y una nueva función after,
que agrega 10.0 al resultado de la primera función. Si llamamos al método
testSourceAndCondition(int source, int
condition)como testSourceAndCondition(42, 20), el resultado será
el siguiente :

Test if 430.0 is smaller than 20


Failure: 430.0

La interfaz Supplier<T> no tener métodos que nos permiten a la cadena de varias


funciones, pero la interfaz tiene las y predeterminados métodos, que nos permiten
construir más expresiones booleanas complejas. La interfaz también tiene
el método predeterminado. Predicate<T> and(Predicate<T>
other) or(Predicate<T> other)Consumer<T>andThen(Consumer<T>
after)

Observe cómo el tipo del valor de entrada de la función after tiene que coincidir
con el tipo de resultado de la beforefunción:

Function<T,R> before = ...


Function<R,V> after = ...
Function<T,V> result = before.andThen(after);

La función resultante acepta un valor del T tipo y produce un valor del V tipo.

Otra forma de lograr el mismo resultado es usar el método Function<V,R>


compose(Function<V,T> before) predeterminado:

Function<Integer, Double> process = after.compose(before);

pág. 163
Cuál de los métodos -andThen() o compose()- usar depende de cuál de las
funciones está disponible para invocar el método de agregación. Entonces, uno se
considera una base, mientras que otro es un parámetro.

Si esta codificación parece un poco sobredimensionada y complicada, es porque lo


es. Lo hicimos solo con fines de demostración. La buena noticia es que las expresiones
lambda presentadas en la siguiente receta nos permiten lograr los mismos resultados
de una manera mucho más breve y clara.

Las interfaces funcionales del paquete tienen otros métodos predeterminados


útiles. El que se destaca es el método, que devuelve una función que siempre devuelve
su argumento de entrada: java.util.functionidentity()

Function<Integer, Integer> id = Function.identity();


System.out.println(id.apply(4)); //prints: 4

El método es muy útil cuando un método requiere que proporcione una determinada
función, pero no desea que esta función modifique el resultado. identity()

Otros métodos predeterminados están relacionados principalmente con la conversión,


el encajonamiento, el desempaquetado y la extracción de los valores mínimos y
máximos de dos parámetros. Lo alentamos a que recorra la API de todas las interfaces
funcionales del y se haga una idea de las posibilidades. java.util.function

Crear una interfaz funcional


En esta receta, aprenderá a crear y utilizar una interfaz funcional personalizada cuando
ninguna de las interfaces estándar del paquete cumple los
requisitos. java.util.function

Prepararse
Crear una interfaz funcional es fácil. Solo hay que asegurarse de que solo haya un
método abstracto en la interfaz, incluidos los métodos heredados de otras interfaces:

@FunctionalInterface
interface A{
void m1();
}

@FunctionalInterface
interface B extends A{
default void m2(){};
}

//@FunctionalInterface

pág. 164
interface C extends B{
void m3();
}

En el ejemplo anterior, la interfaz Cno es una interfaz funcional porque tiene dos
métodos abstractos -m1() , heredado de interfaz A, y su propio método, m3().

También ya hemos visto la interfaz funcional SpeedModel:

@FunctionalInterface
public interface SpeedModel {
double getSpeedMph(double timeSec, int weightPounds, int horsePower);
}

Lo hemos anotado para expresar la intención y ser advertido en caso de que se


agregue otro método abstracto a la interfaz SpeedModel. Y, para simplificarlo, hemos
eliminado clases enum de él. Esta interfaz se usa en la interfaz Vehicle:

public interface Vehicle {


void setSpeedModel(SpeedModel speedModel);
double getSpeedMph(double timeSec);
}

Y la razón por la que la implementación Vehicle lo necesita es porque SpeedModel es la


fuente de la funcionalidad que calcula la velocidad:

public class VehicleImpl implements Vehicle {


private SpeedModel speedModel;
private int weightPounds, hoursePower;
public VehicleImpl(int weightPounds, int hoursePower){
this.weightPounds = weightPounds;
this.hoursePower = hoursePower;
}
public void setSpeedModel(SpeedModel speedModel){
this.speedModel = speedModel;
}
public double getSpeedMph(double timeSec){
return this.speedModel.getSpeedMph(timeSec,
this.weightPounds, this.hoursePower);
};
}

Como mencionamos en Capítulo 2 , Fast Track to OOP - Clases e interfaces , este diseño
se llama agregación. Es una forma preferida de componer el comportamiento deseado,
ya que permite una mayor flexibilidad.

Con interfaces funcionales, dicho diseño se vuelve aún más flexible. Para demostrarlo,
implementemos nuestra interfaz personalizada -SpeedModel .

Cómo hacerlo...
pág. 165
El enfoque tradicional sería crear una clase que implemente la interfaz SpeedModel:

public class SpeedModelImpl implements SpeedModel {


public double getSpeedMph(double timeSec,
int weightPounds, int horsePower){
double v = 2.0 * horsePower * 746 *
timeSec * 32.17 / weightPounds;
return (double) Math.round(Math.sqrt(v) * 0.68);
}
}

Entonces, podemos usar esta implementación de la siguiente manera:

Vehicle vehicle = new VehicleImpl(3000, 200);


SpeedModel speedModel = new SpeedModelImpl();
vehicle.setSpeedModel(speedModel);
System.out.println(vehicle.getSpeedMph(10.)); //prints: 122.0

Para cambiar la forma en que se calcula la velocidad, necesitamos cambiar la clase


SpeedModelImpl.

Alternativamente, usando el hecho de que SpeedModeles una interfaz, podemos


introducir cambios más rápido e incluso evitar tener la clase SpeedModelImpl en
primer lugar:

Vehicle vehicle = new VehicleImpl(3000, 200);


SpeedModel speedModel = new SpeedModel(){
public double getSpeedMph(double timeSec,
int weightPounds, int horsePower){
double v = 2.0 * horsePower * 746 *
timeSec * 32.17 / weightPounds;
return (double) Math.round(Math.sqrt(v) * 0.68);
}
};
vehicle.setSpeedModel(speedModel);
System.out.println(vehicle.getSpeedMph(10.)); //prints: 122.0

Sin embargo, la implementación anterior no aprovecha la funcionalidad de la


interfaz. Si comentamos la anotación, podemos agregar otro método a
la interfaz: SpeedModel

//@FunctionalInterface
public interface SpeedModel {
double getSpeedMph(double timeSec,
int weightPounds, int horsePower);
void m1();
}
Vehicle vehicle = new VehicleImpl(3000, 200);
SpeedModel speedModel = new SpeedModel(){
public double getSpeedMph(double timeSec,
int weightPounds, int horsePower){
double v = 2.0 * horsePower * 746 *
timeSec * 32.17 / weightPounds;
return (double) Math.round(Math.sqrt(v) * 0.68);

pág. 166
}
public void m1(){}
public void m2(){}
};
vehicle.setSpeedModel(speedModel);
System.out.println(vehicle.getSpeedMph(10.)); //prints: 122.0

Como puede ver en el código anterior, no solo la interfaz tiene otro método
abstracto, sino que la clase anónima tiene otro método que no aparece en
la interfaz. Por lo tanto, una clase anónima no requiere que la interfaz sea
funcional. Pero la expresión lambda sí. SpeedModel m1()m2()SpeedModel

Cómo funciona...
Usando expresiones lambda, podemos reescribir el código anterior de la siguiente
manera:

Vehicle vehicle = new VehicleImpl(3000, 200);


SpeedModel speedModel = (t, wp, hp) -> {
double v = 2.0 * hp * 746 * t * 32.17 / wp;
return (double) Math.round(Math.sqrt(v) * 0.68);
};
vehicle.setSpeedModel(speedModel);
System.out.println(vehicle.getSpeedMph(10.)); //prints: 122.0

Discutiremos el formato de expresiones lambda en la próxima receta. Por ahora, solo


queremos señalar la importancia de las interfaces funcionales para una
implementación como la anterior. Como puede ver, solo se especifica el nombre de la
interfaz y ningún nombre de método. Eso es posible porque una interfaz funcional solo
tiene que implementarse un método, y así es como JVM puede resolverlo y generar una
implementación de interfaz funcional detrás de escena.

Hay más...
Es posible definir una interfaz funcional personalizada genérica que se parezca a las
interfaces funcionales estándar. Por ejemplo, podríamos crear la siguiente interfaz
funcional personalizada:

@FunctionalInterface
interface Func < T1 , T2 , T3 , R > {
R aplica ( T1 t1 , T2 t2 , T3 t3) ;
}

Permite tres parámetros de entrada, que es exactamente lo que necesitamos para


calcular la velocidad:

pág. 167
Func<Double, Integer, Integer, Double> speedModel = (t, wp, hp) -> {
double v = 2.0 * hp * 746 * t * 32.17 / wp;
return (double) Math.round(Math.sqrt(v) * 0.68);
};

Usando esta función en lugar de la interfaz SpeedModel, podríamos cambiar la interfaz


Vehicle y su implementación de la siguiente manera:

interface Vehicle {
void setSpeedModel(Func<Double, Integer, Integer,
Double> speedModel);
double getSpeedMph(double timeSec);
}
class VehicleImpl implements Vehicle {
private Func<Double, Integer, Integer, Double> speedModel;
private int weightPounds, hoursePower;
public VehicleImpl(int weightPounds, int hoursePower){
this.weightPounds = weightPounds;
this.hoursePower = hoursePower;
}
public void setSpeedModel(Func<Double, Integer,
Integer, Double> speedModel){
this.speedModel = speedModel;
}
public double getSpeedMph(double timeSec){
return this.speedModel.apply(timeSec,
weightPounds, hoursePower);
};
}

El código anterior produce el mismo resultado que antes, con la interfaz SpeedModel.

El nombre de la interfaz personalizada y el nombre de su único método pueden ser lo


que queramos. Por ejemplo:

@FunctionalInterface
interface FourParamFunction <T1, T2, T3, R> {
R caclulate (T1 t1, T2 t2, T3 t3);
}

Bueno, dado que vamos a crear una nueva interfaz de todos modos, usar el nombre
SpeedModel y el nombre getSpeedMph()del método es probablemente una mejor
solución, ya que hace que el código sea más legible. Pero hay casos en que una interfaz
funcional genérica personalizada es una mejor opción. En tales casos, puede usar la
definición anterior y mejorarla como sea necesario.

Comprender las expresiones


lambda
pág. 168
Ya hemos mencionado varias veces las expresiones lambda y declaramos que su uso en
Java justificaba la introducción de interfaces funcionales en
el java.util.function paquete. La expresión lambda nos permite simplificar la
implementación de funciones al eliminar todo el código repetitivo de las clases
anónimas, dejando solo información mínimamente necesaria. También hemos
explicado que esta simplificación es posible porque una interfaz funcional tiene solo un
método abstracto, por lo que el compilador y JVM hacen coincidir la funcionalidad
proporcionada con la firma del método y generan la implementación de la interfaz
funcional detrás de escena.

Ahora, es hora de definir la sintaxis de la expresión lambda y ver el rango de posibles


formas de expresiones lambda, antes de comenzar a usarlas para hacer que nuestro
código sea más corto y más legible que cuando usamos clases anónimas.

Prepararse
En la década de 1930, el matemático Alonzo Church, en el curso de su investigación
sobre los fundamentos de las matemáticas, introdujo el cálculo lambda , un modelo
universal de computación que se puede utilizar para simular cualquier máquina de
Turing. Bueno, en ese momento, la máquina Turing no había sido creada. Solo más
tarde, cuando Alan Turing inventó su máquina a (máquina automática), también
llamada máquina universal de Turing , él e Church unieron fuerzas y produjeron una
tesis de Church-Turing que mostró que el cálculo lambda y la máquina de Turing tenían
capacidades muy similares.

Church usó la letra griega lambda para describir funciones anónimas, y se convirtió en
un símbolo no oficial del campo de la teoría del lenguaje de programación. El primer
lenguaje de programación que aprovechó el formalismo de cálculo lambda fue
Lisp. Java agregó programación funcional a sus capacidades en 2014, con el
lanzamiento de Java 8.

Una expresión lambda es un método anónimo que nos permite omitir modificadores,
tipos de retorno y tipos de parámetros. Eso lo convierte en una notación muy
compacta. La sintaxis de una expresión lambda incluye la lista de parámetros, un
token de flecha ( ->) y un cuerpo. La lista de parámetros puede estar vacía (solo entre
paréntesis, ()), sin paréntesis (si solo hay un parámetro) o una lista de parámetros
separados por comas rodeados de paréntesis. El cuerpo puede ser una sola expresión
sin corchetes o un bloque de enunciado rodeado de corchetes.

Cómo hacerlo...
Veamos algunos ejemplos. La siguiente expresión lambda no tiene parámetros de
entrada y siempre devuelve 33:

pág. 169
() -> 33;

La siguiente expresión lambda acepta un parámetro del tipo entero, lo incrementa en


1 y devuelve el resultado:

i -> i ++;

La siguiente expresión lambda acepta dos parámetros y devuelve su suma:

(a, b) -> a + b;

La siguiente expresión lambda acepta dos parámetros, los compara y devuelve


el booleanresultado:

(a, b) -> a == b;

Y la última expresión lambda acepta dos parámetros, calcula e imprime el resultado:

(a, b) -> {
double c = a + Math.sqrt(b);
System.out.println("Result: " + c);
}

Como puede ver, una expresión lambda puede incluir un bloque de código de
cualquier tamaño, de manera similar a cualquier método. El ejemplo anterior no
devuelve ningún valor. Aquí hay otro ejemplo de un bloque de código que devuelve
el Stringvalor:

(a, b) -> {
double c = a + Math.sqrt(b);
return c > 10.0 ? "Success" : "Failure";
}

Cómo funciona...
Miremos ese último ejemplo nuevamente. Si hay un método definido en
una interfaz funcional, y si hay un método que acepta un objeto del tipo, podemos
invocarlo de la siguiente manera: String m1(double x, double y) A m2(A a) A

A a = (a, b) -> {
double c = a + Math.sqrt(b);
return c > 10.0 ? "Success" : "Failure";
}
m2(a);

El código anterior significa que el objeto pasado tiene la siguiente implementación


del método m1():

pág. 170
public String m1(double x, double y){
double c = a + Math.sqrt(b);
return c > 10.0 ? "Success" : "Failure";
}

El hecho de que m2(A a)tenga el objeto A como parámetro nos dice que el código de m2(A
a) probablemente usa al menos uno de los A métodos de interfaz (también puede haber
métodos predeterminados o estáticos en la A interfaz). Pero, en general, no hay
garantía de que el método use el objeto pasado porque el programador puede haber
decidido dejar de usarlo y dejar la firma sin cambios solo para evitar romper el código
del cliente, por ejemplo.

Sin embargo, el cliente debe pasar al método un objeto que implemente la A interfaz, lo
que significa que su único método abstracto debe implementarse. Y eso es lo que hace
la expresión lambda. Define la funcionalidad del método abstracto utilizando la
cantidad mínima de código: una lista de los parámetros de entrada y un bloque de
código de la implementación del método. Esto es todo lo que el compilador y JVM
necesitan para generar una implementación.

Escribir un código tan compacto y eficiente se hizo posible debido a la combinación de


una expresión lambda y una interfaz funcional.

Hay más...
Al igual que en una clase anónima, la variable creada fuera pero utilizada dentro de
una expresión lambda se vuelve efectivamente final y no puede modificarse. Puedes
escribir el siguiente código:

double v = 10d;
Function<Integer, Double> multiplyBy10 = i -> i * v;

Sin embargo, no puede cambiar el valor de la variable fuera de la expresión lambda: v

double v = 10d;
v = 30d; //Causes compiler error
Function<Integer, Double> multiplyBy10 = i -> i * v;

No puede cambiarlo dentro de la expresión, tampoco:

double v = 10d;
Function<Integer, Double> multiplyBy10 = i -> {
v = 30d; //Causes compiler error
return i * v;
};

La razón de esta restricción es que se puede pasar y ejecutar una función para
diferentes argumentos en diferentes contextos (diferentes hilos, por ejemplo), y el

pág. 171
intento de sincronizar estos contextos frustraría la idea original de la evaluación
distribuida de funciones.

Otra característica de expresión lambda que vale la pena mencionar es su


interpretación de la palabra clave this, que es bastante diferente de su interpretación
por una clase anónima. Dentro de una clase anónima, this se refiere a la instancia de la
clase anónima, pero dentro de la expresión lambda, se refiere a la instancia de la clase
que rodea la expresión. Vamos a demostrarlo, asumiendo que tenemos la siguiente
clase: this

class Demo{
private String prop = "DemoProperty";
public void method(){
Consumer<String> consumer = s -> {
System.out.println("Lambda accept(" + s
+ "): this.prop=" + this.prop);
};
consumer.accept(this.prop);
consumer = new Consumer<>() {
private String prop = "ConsumerProperty";
public void accept(String s) {
System.out.println("Anonymous accept(" + s
+ "): this.prop=" + this.prop);
}
};
consumer.accept(this.prop);
}
}

Como puede ver, en el código method(), la interfaz funcional Consumer se implementa


dos veces ,utilizando la expresión lambda y una clase anónima. Invoquemos este
método en el siguiente código:

Demo d = nueva Demo ();


d.method ();

El resultado será el siguiente:

La expresión lambda no es una clase interna y no se puede hacer referencia a ella. La


expresión lambda simplemente no tiene campos o propiedades. Es apátrida. Es por eso
que en una expresión lambda, la palabra clave se refiere al contexto circundante. Y esa
es otra razón para el requisito de que todas las variables del contexto circundante
utilizadas por la expresión lambda deben ser finales o efectivamente finales. thisthis

Usar expresiones lambda


pág. 172
En esta receta, aprenderá a usar expresiones lambda en la práctica.

Prepararse
Crear y usar expresiones lambda es en realidad mucho más simple que escribir un
método. Uno solo necesita enumerar los parámetros de entrada, si los hay, y el código
que hace lo que debe hacerse.

Revisemos nuestra implementación de interfaces funcionales estándar de la primera


receta de este capítulo y las reescribamos usando expresiones lambda. Así es como
hemos implementado las cuatro interfaces funcionales principales usando clases
anónimas:

Function<Integer, Double> ourFunc = new Function<Integer, Double>(){


public Double apply(Integer i){
return i * 10.0;
}
};
System.out.println(ourFunc.apply(1)); //prints: 10.0
Consumer<String> consumer = new Consumer<String>() {
public void accept(String s) {
System.out.println("The " + s + " is consumed.");
}
};
consumer.accept("Hello!"); //prints: The Hello! is consumed.
Supplier<String> supplier = new Supplier<String>() {
public String get() {
String res = "Success";
//Do something and return result—Success or Error.
return res;
}
};
System.out.println(supplier.get()); //prints: Success
Predicate<Double> pred = new Predicate<Double>() {
public boolean test(Double num) {
System.out.println("Test if " + num + " is smaller than 20");
return num < 20;
}
};
System.out.println(pred.test(10.0)? "10 is smaller":"10 is bigger");
//prints: Test if 10.0 is smaller than 20
// 10 is smaller

Y así es como se ven con expresiones lambda:

Function<Integer, Double> ourFunc = i -> i * 10.0;


System.out.println(ourFunc.apply(1)); //prints: 10.0

Consumer<String> consumer =
s -> System.out.println("The " + s + " is consumed.");
consumer.accept("Hello!"); //prints: The Hello! is consumed.

Supplier<String> supplier = () - > {

pág. 173
String res = "Success";
//Do something and return result—Success or Error.
return res;
};
System.out.println(supplier.get()); //prints: Success

Predicate<Double> pred = num -> {


System.out.println("Test if " + num + " is smaller than 20");
return num < 20;
};
System.out.println(pred.test(10.0)? "10 is smaller":"10 is bigger");
//prints: Test if 10.0 is smaller than 20
// 10 is smaller

Los ejemplos de interfaces funcionales especializadas que hemos presentado son los siguientes:

IntFunction<String> ifunc = new IntFunction<String>() {


public String apply(int i) {
return String.valueOf(i * 10);
}
};
System.out.println(ifunc.apply(1)); //prints: 10
BiFunction<String, Integer, Double> bifunc =
new BiFunction<String, Integer, Double >() {
public Double apply(String s, Integer i) {
return (s.length() * 10d) / i;
}
};

System.out.println(bifunc.apply("abc",2)); //prints: 15.0


BinaryOperator<Integer> binfunc = new BinaryOperator<Integer>(){
public Integer apply(Integer i, Integer j) {
return i >= j ? i : j;
}
};
System.out.println(binfunc.apply(1,2)); //prints: 2
IntBinaryOperator intBiFunc = new IntBinaryOperator(){
public int applyAsInt(int i, int j) {
return i >= j ? i : j;
}
};
System.out.println(intBiFunc.applyAsInt(1,2)); //prints: 2

Los ejemplos de interfaces funcionales especializadas que hemos presentado son los
siguientes:

IntFunction <String> ifunc = new IntFunction <String> () {


public String apply ( int i) {
return String. valorDe (i * 10 ) ;
}
} ;
Sistema. out .println (ifunc.apply ( 1 )) ; // imprime: 10
BiFunction <String , Integer , Double> bifunc =
new BiFunction <String , Integer , Double> () {
public Double apply (String s , Integer i) {
return (s.length () * 10d ) / i ;

pág. 174
}
} ;

Sistema. out .println (bifunc.apply ( "abc" , 2 )) ; // imprime: 15.0


BinaryOperator <Integer> binfunc = new BinaryOperator <Integer> () {
public Integer apply (Integer i , Integer j) {
return i> = j? i: j ;
}
} ;
Sistema. out .println (binfunc.apply ( 1 , 2 )) ; // impresiones: 2
IntBinaryOperator intBiFunc = new IntBinaryOperator () {
public int applyAsInt ( int i , int j) {
return i> = j? i: j ;
}
} ;
Sistema. out .println (intBiFunc.applyAsInt ( 1 , 2 )) ; // impresiones: 2

Y así es como se ven con expresiones lambda:

IntFunction<String> ifunc = i -> String.valueOf(i * 10);


System.out.println(ifunc.apply(1)); //prints: 10

BiFunction<String, Integer, Double> bifunc =


(s,i) -> (s.length() * 10d) / i;
System.out.println(bifunc.apply("abc",2)); //prints: 15.0

BinaryOperator<Integer> binfunc = (i,j) -> i >= j ? i : j;


System.out.println(binfunc.apply(1,2)); //prints: 2

IntBinaryOperator intBiFunc = (i,j) -> i >= j ? i : j;


System.out.println(intBiFunc.applyAsInt(1,2)); //prints: 2

Como puede ver, el código está menos abarrotado y es más legible.

Cómo hacerlo...
Aquellos que tienen alguna experiencia tradicional en la escritura de códigos, al
comenzar la programación funcional, equiparan las funciones con los
métodos. Ellos tratan de crear funciones primero porque era la forma en que todos
utilizan para escribir código tradicional -mediante la creación de métodos. Sin
embargo, las funciones son solo piezas más pequeñas de funcionalidad que modifican
algunos aspectos del comportamiento de los métodos o proporcionan la lógica
empresarial para el código que de otro modo no sería específico del negocio . En la
programación funcional, como en la programación tradicional, los métodos continúan
proporcionando la estructura del código, mientras que las funciones son las adiciones
agradables y útiles. Entonces, en la programación funcional, crear un método es lo
primero, antes de definir las funciones. Demostremos esto.

Los siguientes son los pasos básicos de la escritura de código. Primero, identificamos el
bloque de código bien enfocado que se puede implementar como método. Luego,

pág. 175
después de saber qué va a hacer el nuevo método, podemos convertir algunas partes de
su funcionalidad en funciones:

1. Crea el método calculate():

void calculate(){
int i = 42; //get a number from some source
double res = 42.0; //process the above number
if(res < 42){ //check the result using some criteria
//do something
} else {
//do something else
}
}

El pseudocódigo anterior describe la idea de la funcionalidad del método


calculate() . Se puede implementar en un estilo tradicional , mediante el uso de los
siguientes métodos:

int getInput(){
int result;
//getting value for result variable here
return result;
}
double process(int i){
double result;
//process input i and assign value to result variable
}
boolean checkResult(double res){
boolean result = false;
//use some criteria to validate res value
//and assign value to result
return result;
}
void processSuccess(double res){
//do something with res value
}
void processFailure(double res){
//do something else with res value
}
void calculate(){
int i = getInput();
double res = process(i);
if(checkResult(res)){
processSuccess(res);
} else {
processFailure(res);
}
}

Pero algunos de estos métodos pueden ser muy pequeños, por lo que el código se
fragmenta y es menos legible con tantas indirecciones adicionales. Esta desventaja se
vuelve especialmente evidente en el caso cuando los métodos provienen de fuera de la
clase donde se implementa el método calculate():

pág. 176
void calculate(){
SomeClass1 sc1 = new SomeClass1();
int i = sc1.getInput();
SomeClass2 sc2 = new SomeClass2();
double res = sc2.process(i);
SomeClass3 sc3 = new SomeClass3();
SomeClass4 sc4 = new SomeClass4();
if(sc3.checkResult(res)){
sc4.processSuccess(res);
} else {
sc4.processFailure(res);
}
}

Como puede ver, en el caso de que cada uno de los métodos externos sea pequeño, la
cantidad de código de plomería puede exceder sustancialmente la carga útil que
admite. Además, la implementación anterior crea muchas dependencias estrechas
entre clases.

2. Veamos cómo podemos implementar la misma funcionalidad usando funciones. La ventaja


es que las funciones pueden ser tan pequeñas como deben ser, pero el código de plomería nunca
excederá la carga útil porque no hay código de plomería. Otra razón para usar funciones es cuando
necesitamos la flexibilidad para cambiar secciones de la funcionalidad sobre la marcha, para el
propósito de investigación del algoritmo. Y si estas funcionalidades tienen que venir de fuera de la
clase, no necesitamos construir otras clases solo por el hecho de pasar un
método calculate(). Podemos pasarlos como funciones:

void calculate(Supplier<Integer> souc e, Function<Integer,


Double> process, Predicate<Double> condition,
Consumer<Double> success, Consumer<Double> failure){
int i = source.get();
double res = process.apply(i);
if(condition.test(res)){
success.accept(res);
} else {
failure.accept(res);
}
}

3. Así es como pueden verse las funciones:

Supplier<Integer> source = () -> 4;


Function<Integer, Double> before = i -> i * 10.0;
Function<Double, Double> after = d -> d + 10.0;
Function<Integer, Double> process = before.andThen(after);
Predicate<Double> condition = num -> num < 100;
Consumer<Double> success =
d -> System.out.println("Success: "+ d);
Consumer<Double> failure =
d -> System.out.println("Failure: "+ d);
calculate(source, process, condition, success, failure);

El resultado del código anterior será el siguiente:

pág. 177
Éxito: 50.0

Cómo funciona...
La expresión lambda actúa como un método regular, excepto cuando piensa en probar
cada función por separado. ¿Cómo hacerlo?

Hay dos formas de abordar este problema. Primero, dado que las funciones son
típicamente pequeñas, a menudo no es necesario probarlas por separado, y se prueban
indirectamente cuando se prueba el código que las usa. En segundo lugar, si todavía
cree que la función debe probarse, siempre es posible ajustarla en el método que
devuelve la función, por lo que puede probar ese método como cualquier otro
método. Aquí hay un ejemplo de cómo se puede hacer:

public class Demo {


Supplier<Integer> source(){ return () -> 4;}
Function<Double, Double> after(){ return d -> d + 10.0; }
Function<Integer, Double> before(){return i -> i * 10.0; }
Function<Integer, Double> process(){return before().andThen(after());}
Predicate<Double> condition

res = process.apply(i)
if(condition.test(res)){
success.accept(res);
} else {
failure.accept(res);
}
}
void someOtherMethod() {
calculate(source(), process(),
condition(), success(), failure());
}

Ahora podemos escribir las pruebas de unidad de función de la siguiente manera:

public class DemoTest {

@Test
public void source() {
int i = new Demo().source().get();
assertEquals(4, i);
}
@Test
public void after() {
double d = new Demo().after().apply(1.);
assertEquals(11., d, 0.01);
}
@Test
public void before() {
double d = new Demo().before().apply(10);
assertEquals(100., d, 0.01);
}
@Test

pág. 178
public void process() {
double d = new Demo().process().apply(1);
assertEquals(20., d, 0.01);
}
@Test
public void condition() {
boolean b = new Demo().condition().test(10.);
assertTrue;
}
}

Por lo general, las expresiones lambda (y las funciones en general) se utilizan para
especializar funcionalidades genéricas, agregando lógica empresarial a un método. Un
buen ejemplo es las operaciones de rutas, que w e van a discutir en Capítulo 5, Arroyos
y tuberías. Los autores de la biblioteca los han creado para poder trabajar en paralelo,
lo que requiere mucha experiencia. Y ahora los usuarios de la biblioteca pueden
especializar las operaciones pasándoles las expresiones lambda (funciones) que
proporcionan la lógica empresarial de la aplicación.

Hay más...
Dado que, como ya hemos mencionado, las funciones son a menudo simples líneas
simples, a menudo están en línea cuando se pasan como parámetros, por ejemplo:

Consumer<Double> success = d -> System.out.println("Success: " + d);


Consumer<Double> failure = d-> System.out.println("Failure: " + d);
calculate(() -> 4, i -> i * 10.0 + 10, n -> n < 100, success, failure);

Pero uno no debe llevarlo demasiado lejos, ya que dicha alineación puede disminuir la
legibilidad del código.

Usando referencias de métodos


En esta receta, aprenderá a usar una referencia de método, siendo la referencia del
constructor uno de los casos.

Prepararse
Cuando una expresión lambda de una línea consiste solo en una referencia a un método
existente implementado en otro lugar, es posible simplificar aún más la notación
lambda utilizando la referencia del método .

La sintaxis de la referencia del método es Location::methodName, donde Location indica


dónde (en qué objeto o clase) methodName se puede encontrar el método. Los dos puntos

pág. 179
( ::) sirven como separador entre la ubicación y el nombre del método. Si hay varios
métodos con el mismo nombre en la ubicación especificada (debido a la sobrecarga del
método), el método de referencia se identifica mediante la firma del método abstracto
de la interfaz funcional implementada por la expresión lambda.

Cómo hacerlo...
El formato exacto de la referencia del método depende de si el método referido es
estático o no estático. La referencia del método también puede
estar vinculada o no vinculada, o para ser más formal, la referencia del método puede
tener un receptor vinculado o un receptor independiente. Un receptor es un objeto o
clase que se utiliza para invocar el método. Que recibe el llamado. Puede estar
vinculado a un contexto particular o no (sin consolidar). Explicaremos lo que esto
significa durante la demostración.

La referencia del método también puede referirse a un constructor con o sin


parámetros.

Tenga en cuenta que la referencia del método es aplicable solo cuando la expresión
consta de una sola llamada al método y nada más. Por ejemplo, se puede aplicar una
referencia de método a la expresión lambda () -> SomeClass.getCount(). Se verá
así . Pero la expresión no se puede reemplazar con la referencia del método porque
hay más operaciones en esta expresión que solo una llamada al
método. SomeClass::getCount() -> 5 + SomeClass.getCount()

Referencia de método no enlazado


estático
Para demostrar una referencia de método estático, utilizaremos la clase Food con dos
métodos estáticos:

class Food{
public static String getFavorite(){ return "Donut!"; }
public static String getFavorite(int num){
return num > 1 ? String.valueOf(num) + " donuts!" : "Donut!";
}
}

Puesto que el primer método, String getFavorite(), no acepta ningún parámetro de


entrada y devuelve un valor, que puede ser implementado como una interfaz
funcional, Supplier<T>. La expresión lambda que implementa la función que consiste
en la llamada al método estático String getFavorite() se ve así:

pág. 180
Supplier<String> supplier = () -> Food.getFavorite();

Usando la referencia del método, la línea anterior cambia a la siguiente:

Supplier<String> supplier = Food::getFavorite;

Como puede ver, el formato anterior define la ubicación del método (como la clase
Food), el nombre del método y el valor del tipo de retorno (como String). El nombre de
la interfaz funcional indica que no hay parámetros de entrada, por lo que el compilador
y JVM pueden identificar el método entre los métodos de la Food clase.

Una referencia de método estático no está vinculada porque no se utiliza ningún objeto
para invocar el método. En el caso de un método estático, una clase es el receptor de la
llamada, no un objeto.

El segundo método estático , acepta un parámetro y devuelve un valor. Significa que


podemos usar la interfaz funcional para implementar la función que consiste solo en
una llamada a este método:String getFavorite(int num)Function<T,R>

Function<Integer, String> func = i -> Food.getFavorite(i);

Pero cuando se usa la referencia del método, cambia exactamente a la misma forma
que en el ejemplo anterior:

Function<Integer, String> func = Food::getFavorite;

La diferencia está en la interfaz funcional especificada. Permite que el compilador y el


tiempo de ejecución de Java identifiquen el método que se utilizará: el método se
nombra getFavorite(), acepta el valor Integer y devuelve el valor String. Y solo hay
un método de este tipo en la clase Food . En realidad, no hay necesidad de mirar qué
valor devuelve el método, porque no es posible sobrecargar un método solo por el
valor de retorno. La firma del método ( nombre y lista de tipos de parámetros ) es
suficiente para la identificación del método.

Podemos usar las funciones implementadas de la siguiente manera:

Supplier<String> supplier = Food::getFavorite;


System.out.println("supplier.get() => " + supplier.get());

Function<Integer, String> func = Food::getFavorite;


System.out.println("func.getFavorite(1) => " + func.apply(1));
System.out.println("func.getFavorite(2) => " + func.apply(2));

Si ejecutamos el código anterior, el resultado será el siguiente:

pág. 181
Referencia de método enlazado no
estático
Para demostrar una referencia de método enlazado no estático, mejoremos
la Food clase agregando un campo, dos constructores y dos métodos: nameString
sayFavorite()

class Food{
private String name;
public Food(){ this.name = "Donut"; }
public Food(String name){ this.name = name; }
public static String getFavorite(){ return "Donut!"; }
public static String getFavorite(int num){
return num > 1 ? String.valueOf(num) + " donuts!" : "Donut!";
}
public String sayFavorite(){
return this.name + (this.name.toLowerCase()
.contains("donut")?"? Yes!" : "? D'oh!");
}
public String sayFavorite(String name){
this.name = this.name + " and " + name;
return sayFavorite();
}
}

Ahora, creemos tres instancias de la clase Food:

Food food1 = new Food();


Food food2 = new Food("Carrot");
Food food3 = new Food("Carrot and Broccoli");

Lo anterior es el contexto: el código que rodea la expresión lambda que vamos a crear
ahora. Utilizamos las variables locales del contexto anterior para implementar tres
proveedores diferentes:

Supplier<String> supplier1 = () -> food1.sayFavorite();


Supplier<String> supplier2 = () -> food2.sayFavorite();
Supplier<String> supplier3 = () -> food3.sayFavorite();

Nosotros usamos Supplier<T> porque el método String, sayFavorite()no requiere


ningún parámetro y justo Producimos (suministros) del valor String. Usando la
referencia del método, podemos reescribir las expresiones lambda anteriores de la
siguiente manera:

Supplier<String> supplier1 = food1::sayFavorite;


Supplier<String> supplier2 = food2::sayFavorite;
Supplier<String> supplier3 = food3::sayFavorite;

pág. 182
El método sayFavorite()pertenece a un objeto que se creó en un determinado
contexto. En otras palabras, este objeto (el receptor de la llamada) está vinculado a un
determinado contexto, por lo que dicha referencia de método se denomina referencia
de método vinculado o referencia de método de receptor vinculado .

Podemos pasar las funciones recién creadas como cualquier otro objeto y usarlas en
cualquier lugar que necesitemos, por ejemplo:

System.out.println("new Food().sayFavorite() => " + supplier1.get());


System.out.println("new Food(Carrot).sayFavorite() => "
+ supplier2.get());
System.out.println("new Food(Carrot,Broccoli).sayFavorite() => "
+ supplier3.get());

El resultado será el siguiente:

Tenga en cuenta que el receptor permanece vinculado al contexto, por lo que su estado
puede cambiar y afectar la salida. Ese es el significado de la distinción de
ser atado. Usando tal referencia, uno debe tener cuidado de no cambiar el estado del
receptor en el contexto de su origen. De lo contrario, puede conducir a resultados
impredecibles. Esta consideración es especialmente pertinente para el procesamiento
paralelo cuando la misma función se puede utilizar en diferentes contextos.

Veamos otro caso de una referencia método vinculado con el segundo método no
estático, String sayFavorite(String name). Primero, creamos una implementación de
una interfaz funcional, UnaryOperator<T> , utilizando los mismos objetos de la Food clase
que utilizamos en el ejemplo anterior:

UnaryOperator <String> op1 = s -> food1 .sayFavorite (s) ;


UnaryOperator <String> op2 = s -> food2.sayFavorite (s);
UnaryOperator <String> op3 = s -> food3.sayFavorite (s);

La razón por la que hemos usado la interfaz funcional UnaryOperator<T> es que


el método acepta un parámetro y produce el valor del mismo tipo. Y ese es el
propósito de las interfaces funcionales con el nombre String sayFavorite(String
name)Operator en ellas, para admitir casos en los que el valor de entrada y el resultado
tienen el mismo tipo.

La referencia del método nos permite cambiar la expresión lambda de la siguiente


manera:

pág. 183
UnaryOperator <String> op1 = food1 :: sayFavorite ;
UnaryOperator <String> op2 = food2 :: sayFavorite ;
UnaryOperator <String> op3 = food3 :: sayFavorite ;

Ahora podemos usar las funciones anteriores (operadores) en cualquier parte del
código, por ejemplo:

System.out.println("new Food()
.sayFavorite(Carrot) => " + op1.apply("Carrot"));
System.out.println("new Food(Carrot)
.sayFavorite(Broccoli) => " + op2.apply("Broccoli"));
System.out.println("new Food(Carrot, Broccoli)
.sayFavorite(Donuts) => " + op3.apply("Donuts"));

El resultado del código anterior es el siguiente :

Referencia de método
independiente no estático
Para demostrar una referencia de método independiente al método String
sayFavorite(), utilizaremos la interfaz funcional Function<T,R> porque nos gustaría
utilizar un objeto de la clase Food (el receptor de la llamada) como parámetro y
recuperar un valor String:

Function<Food, String> func = f -> f.sayFavorite();

La referencia del método nos permite reescribir la expresión lambda anterior de la


siguiente manera:

Function<Food, String> func = Food::sayFavorite;

Usando los mismos objetos de la clase que creamos en los ejemplos anteriores,
usamos la función recién creada en el siguiente código, por ejemplo: Food

System.out.println ("new Food ()


.sayFavorite () =>" + func.apply (food1));
System.out.println ("new Food (Carrot)
.sayFavorite () =>" + func.apply (food2));
System.out.println ("new Food (Carrot, Broccoli)
.sayFavorite () =>" + func.apply (food3));

pág. 184
Como puede ver, el parámetro (el objeto receptor de la llamada) proviene solo del
contexto actual, como lo hace cualquier parámetro. Dondequiera que se pasa la función,
no lleva consigo el contexto. Su receptor no está vinculado al contexto que se utilizó
para la creación de la función. Es por eso que esta referencia de método se
llama independiente .

La salida del código anterior es la siguiente :

Y, para demostrar otro caso de la referencia del método independiente, usaremos el


segundo método String sayFavorite(String name), con los mismos objetos Food que
hemos usado todo el tiempo. La interfaz funcional que vamos a implementar esta vez
se llama BiFunction<T,U,R>:

BiFunction <Food , String , String> func = (f , s) -> f.sayFavorite (s) ;

La razón por la que seleccionamos esta interfaz funcional es porque acepta dos
parámetros ,exactamente lo que necesitamos en este caso , para tener el objeto y
el Stringvalor del receptor como parámetros. La versión de referencia del método de la
expresión lambda anterior tiene el siguiente aspecto:

BiFunction <Food , String , String> func = Food :: sayFavorite ;

Podemos usar la función anterior escribiendo el siguiente código, por ejemplo:

System.out.println("new Food()
.sayFavorite(Carrot) => " + func.apply(food1, "Carrot"));
System.out.println("new Food(Carrot)
.sayFavorite(Broccoli) => "
+ func2.apply(food2, "Broccoli"));
System.out.println("new Food(Carrot,Broccoli)
.sayFavorite(Donuts) => " + func2.apply(food3,"Donuts"));

El resultado es el siguiente:

Referencias de métodos de
constructor
pág. 185
Usar la referencia de método para un constructor es muy similar a una referencia de
método estático porque usa una clase como receptor de llamadas, no un objeto (aún
no se ha creado). Aquí está la expresión lambda que implementa la interfaz
Supplier<T>:

Supplier<Food> foodSupplier = () -> new Food();

Y aquí está su versión con la referencia del método:

Supplier<Food> foodSupplier = Food::new;


System.out.println("new Food()
.sayFavorite() => " + foodSupplier.get().sayFavorite());

Si ejecutamos el código anterior, obtenemos el siguiente resultado:

Ahora, agreguemos otro constructor a la clase: Food

public Food(String name){


this.name = name;
}

Una vez que hacemos esto, podemos expresar el constructor anterior usando la
referencia del método:

Function<String, Food> createFood = Food::new;


Food food = createFood.apply("Donuts");
System.out.println("new Food(Donuts).sayFavorite() => "
+ food.sayFavorite());
food = createFood.apply("Carrot");
System.out.println("new Food(Carrot).sayFavorite() => "
+ food.sayFavorite());

Aquí está la salida del código anterior:

De la misma manera, podemos agregar un constructor con dos parámetros:

public Food(String name, String anotherName) {


this.name = name + " and " + anotherName;
}

Una vez que hacemos eso, podemos expresarlo a través de : BiFunction<String,


String>

BiFunction<String, String, Food> createFood = Food::new;


Food food = createFood.apply("Donuts", "Carrots");

pág. 186
System.out.println("new Food(Donuts, Carrot)
.sayFavorite() => " + food.sayFavorite());
food = constrFood2.apply("Carrot", "Broccoli");
System.out.println("new Food(Carrot, Broccoli)
.sayFavorite() => " food.sayFavorite());

El resultado del código anterior es el siguiente:

Para expresar un constructor que acepta más de dos parámetros, podemos crear una
interfaz funcional personalizada con cualquier número de parámetros. Por ejemplo,
podemos usar la siguiente interfaz funcional personalizada, que discutimos en la receta
anterior:

@FunctionalInterface
interface Func <T1, T2, T3, R> {R apply (T1 t1, T2 t2, T3 t3);}

Asumamos que necesitamos usar la AClass clase:

class AClass {
public AClass ( int i , double d , String s) {}
public String get ( int i , double d) { return "" ; }
public String get ( int i , double d , String s) { return "" ; }
}

Podemos escribir el siguiente código utilizando la referencia del método:

Func<Integer, Double, String, AClass> func1 = AClass::new;


AClass obj = func1.apply(1, 2d, "abc");

Func<Integer, Double, String, String> func2 = obj::get; //bound


String res1 = func2.apply(42, 42., "42");

Func<AClass, Integer, Double, String> func3 = AClass::get; //unbound


String res21 = func3.apply(obj, 42, 42.);

En el fragmento de código anterior, creamos una función func1 que nos permite crear
un objeto de clase AClass. La función func2 se aplica al objeto resultante del método
obj que utiliza la referencia del método String get(int i, double d) vinculado porque
su receptor de llamada (objeto obj) proviene de un contexto particular (vinculado a
él). Por el contrario, la func3 función se implementa como una referencia de método
independiente porque obtiene su receptor de llamada (clase AClass) no desde un
contexto.

Hay más...
pág. 187
Hay varias referencias de métodos simples pero muy útiles porque obtiene su
receptor de llamadas que a menudo se usa en la práctica:

Function<String, Integer> strLength = String::length;


System.out.println(strLength.apply("3")); //prints: 1

Function<String, Integer> parseInt = Integer::parseInt;


System.out.println(parseInt.apply("3")); //prints: 3

Consumer<String> consumer = System.out::println;


consumer.accept("Hello!"); //prints: Hello!

También hay algunos métodos útiles para trabajar con matrices y listas:

Function<Integer, String[]> createArray = String[]::new;


String[] arr = createArray.apply(3);
System.out.println("Array length=" + arr.length);

int i = 0;
for(String s: arr){ arr[i++] = String.valueOf(i); }
Function<String[], List<String>> toList = Arrays::<String>asList;
List<String> l = toList.apply(arr);
System.out.println("List size=" + l.size());
for(String s: l){ System.out.println(s); }

Aquí están los resultados del código anterior:

Le dejamos a usted analizar cómo se crearon y utilizaron las expresiones lambda


anteriores.

Aprovechando expresiones
lambda en tus programas
En esta receta, aprenderá cómo aplicar una expresión lambda a su código. Volveremos
a la aplicación de demostración y la modificaremos introduciendo una expresión
lambda donde tenga sentido.

Prepararse

pág. 188
Equipados con interfaces funcionales, expresiones lambda y las mejores prácticas de un
diseño API amigable con lambda, podemos mejorar sustancialmente nuestra aplicación
de cálculo de velocidad al hacer que su diseño sea más flexible y fácil de usar. Vamos a
configurar un fondo lo más cercano posible a un problema de la vida real sin hacerlo
demasiado complejo.

Los autos sin conductor están en las noticias en estos días, y hay buenas razones para
creer que será así durante bastante tiempo. Una de las tareas en este dominio es el
análisis y la modelización del flujo de tráfico en un área urbana basada en datos
reales. Muchos de estos datos ya existen y se seguirán recopilando en el
futuro. Supongamos que tenemos acceso a dicha base de datos por fecha, hora y
ubicación geográfica. Supongamos también que los datos de tráfico de esta base de
datos vienen en unidades, cada uno capturando detalles sobre un vehículo y las
condiciones de manejo:

public interface TrafficUnit {


VehicleType getVehicleType();
int getHorsePower();
int getWeightPounds();
int getPayloadPounds();
int getPassengersCount();
double getSpeedLimitMph();
double getTraction();
RoadCondition getRoadCondition();
TireCondition getTireCondition();
int getTemperature();
}

Los tipos enum -VehicleType , RoadConditiony TireCondition- ya se construyeron


en Capítulo 2 , Fast Track to OOP - Clases e interfaces :

enum VehicleType {
CAR("Car"), TRUCK("Truck"), CAB_CREW("CabCrew");
private String type;
VehicleType(String type){ this.type = type; }
public String getType(){ return this.type;}
}
enum RoadCondition {
DRY(1.0),
WET(0.2) { public double getTraction() {
return temperature > 60 ? 0.4 : 0.2; } },
SNOW(0.04);
public static int temperature;
private double traction;
RoadCondition(double traction){ this.traction = traction; }
public double getTraction(){return this.traction;}
}
enum TireCondition {
NEW(1.0), WORN(0.2);
private double traction;
TireCondition(double traction){ this.traction = traction; }
public double getTraction(){ return this.traction;}
}

pág. 189
La interfaz de acceso a los datos de tráfico puede verse así:

TrafficUnit getOneUnit(Month month, DayOfWeek dayOfWeek,


int hour, String country, String city,
String trafficLight);
List<TrafficUnit> generateTraffic(int trafficUnitsNumber,
Month month, DayOfWeek dayOfWeek, int hour,
String country, String city, String trafficLight);

Aquí hay un ejemplo del acceso a los métodos anteriores:

TrafficUnit trafficUnit = FactoryTraffic.getOneUnit (Month.APRIL,


DayOfWeek.FRIDAY, 17, "EE. UU.", "Denver", "Main103S");

El número 17 es una hora del día (5 pm) y Main1035 es una identificación de semáforo.

La llamada al segundo método devuelve múltiples resultados:

List<TrafficUnit> trafficUnits =
FactoryTrafficModel.generateTraffic(20, Month.APRIL,
DayOfWeek.FRIDAY, 17, "USA", "Denver", "Main103S");

El primer parámetro, 20es el número de unidades de tráfico solicitadas.

Como puede ver, una fábrica de tráfico de este tipo proporciona datos sobre el tráfico
en una ubicación particular en un momento determinado (entre las 5 p.m. y las 6 p.m.
en nuestro ejemplo). Cada llamada a la fábrica produce un resultado diferente, mientras
que la lista de unidades de tráfico describe datos estadísticamente correctos (incluidas
las condiciones climáticas más probables) en la ubicación especificada.

También cambiaremos las interfaces de FactoryVehicley FactorySpeedModel para que


puedan construirse Vehicley SpeedModel basarse en la TrafficUnitinterfaz. El código de
demostración resultante es el siguiente:

double timeSec = 10.0;


TrafficUnit trafficUnit = FactoryTraffic.getOneUnit(Month.APRIL,
DayOfWeek.FRIDAY, 17, "USA", "Denver", "Main103S");
Vehicle vehicle = FactoryVehicle.build(trafficUnit);
SpeedModel speedModel =
FactorySpeedModel.generateSpeedModel(trafficUnit);
vehicle.setSpeedModel(speedModel);
printResult(trafficUnit, timeSec, vehicle.getSpeedMph(timeSec));

El printResult() método tiene el siguiente código:

void printResult(TrafficUnit tu, double timeSec, double speedMph){


System.out.println("Road " + tu.getRoadCondition()
+ ", tires " + tu.getTireCondition() + ": "
+ tu.getVehicleType().getType()
+ " speedMph (" + timeSec + " sec)="
+ speedMph + " mph");
}

pág. 190
La salida de este código puede verse así:

Dado que usamos los datos "reales" ahora, cada ejecución de este programa produce un
resultado diferente, basado en las propiedades estadísticas de los datos. En un lugar
determinado, un automóvil o clima seco aparecería con mayor frecuencia en esa fecha
y hora, mientras que en otro lugar, un camión o nieve sería más típico.

En esta carrera, la unidad de tráfico trajo una carretera mojada, llantas nuevas y Truck
con tal potencia y carga del motor que en 10 segundos pudo alcanzar una velocidad de
22 mph. La fórmula que usamos para calcular la velocidad (dentro de un objeto
de SpeedModel) le es familiar:

double weightPower = 2.0 * horsePower * 746 * 32.174 / weightPounds;


double speed = (double) Math.round(Math.sqrt(timeSec * weightPower)
* 0.68 * traction);

Aquí, el valor traction proviene TrafficUnit. En la clase que implementa la interfaz


TrafficUnit, el método getTraction() tiene el siguiente aspecto:

public double getTraction () {


double rt = getRoadCondition (). getTraction ();
tt doble = getTireCondition (). getTraction ();
devuelve rt * tt;
}

Los métodos getRoadCondition()y getTireCondition()devuelven los elementos de


los tipos enum correspondientes que acabamos de describir.

Ahora estamos listos para mejorar nuestra aplicación de cálculo de velocidad usando
las expresiones lambda discutidas en las recetas anteriores.

Cómo hacerlo...
Siga estos pasos para aprender a usar expresiones lambda:

1. Comencemos a construir una API. Vamos a llamarlo Traffic. Sin usar interfaces
funcionales, podría verse así:

public interface Traffic {


void speedAfterStart(double timeSec, int trafficUnitsNumber);
}

Su implementación puede ser la siguiente:

pág. 191
public class TrafficImpl implements Traffic {
private int hour;
private Month month;
private DayOfWeek dayOfWeek;
private String country, city, trafficLight;
public TrafficImpl(Month month, DayOfWeek dayOfWeek, int hour,
String country, String city, String trafficLight){
this.hour = hour;
this.city = city;
this.month = month;
this.country = country;
this.dayOfWeek = dayOfWeek;
this.trafficLight = trafficLight;
}
public void speedAfterStart(double timeSec,
int trafficUnitsNumber){
List<TrafficUnit> trafficUnits =
FactoryTraffic.generateTraffic(trafficUnitsNumber,
month, dayOfWeek, hour, country, city, trafficLight);
for(TrafficUnit tu: trafficUnits){
Vehicle vehicle = FactoryVehicle.build(tu);
SpeedModel speedModel =
FactorySpeedModel.generateSpeedModel(tu);
vehicle.setSpeedModel(speedModel);
double speed = vehicle.getSpeedMph(timeSec);
printResult(tu, timeSec, speed);
}
}
}

2. Escribamos un código de muestra que use la interfaz Traffic:

Traffic traffic = new TrafficImpl(Month.APRIL,


DayOfWeek.FRIDAY, 17, "USA", "Denver", "Main103S");
double timeSec = 10.0;
int trafficUnitsNumber = 10;
traffic.speedAfterStart(timeSec, trafficUnitsNumber);

Obtenemos resultados similares a los siguientes:

Como se mencionó anteriormente, dado que estamos usando datos reales, el mismo
código no produce exactamente el mismo resultado cada vez. Uno no debería esperar
ver los valores de velocidad de la captura de pantalla anterior, sino algo que se ve muy
similar.

pág. 192
3. Usemos una expresión lambda. La API anterior es bastante limitada. Por ejemplo, no
le permite probar diferentes fórmulas de cálculo de velocidad sin
cambiar FactorySpeedModel. Mientras tanto, la interfaz SpeedModel tiene solo un
método abstracto, llamado getSpeedMph() (que lo convierte en una interfaz funcional):

public interface SpeedModel {


double getSpeedMph(double timeSec,
int weightPounds, int horsePower);
}

Podemos aprovechar la ventaja de SpeedModel ser una interfaz funcional y agregar


otro método a la interfaz Traffic que pueda aceptar la implementación
SpeedModel como una expresión lambda:

public interface Traffic {


void speedAfterStart(double timeSec,
int trafficUnitsNumber);
void speedAfterStart(double timeSec,
int trafficUnitsNumber, SpeedModel speedModel);
}

Sin embargo, el problema es que el valor traction no viene como un parámetro


para el método getSpeedMph(), por lo que no podemos implementarlo como una
función pasada como un parámetro en el método speedAfterStart(). Mire más
de cerca el cálculo de
velocidad FactorySpeedModel.generateSpeedModel(TrafficUnit
trafficUnit):

double getSpeedMph(double timeSec, int weightPounds,


int horsePower) {
double traction = trafficUnit.getTraction();
double v = 2.0 * horsePower * 746 * timeSec *
32.174 / weightPounds;
return Math.round(Math.sqrt(v) * 0.68 * traction);
}

Como puede ver, el valor traction es un multiplicador del valor calculado


de speed y esa es la única dependencia de la unidad de tráfico. Podemos eliminar la
tracción del modelo de velocidad y aplicar tracción después de calcular la velocidad
utilizando el modelo de velocidad. Significa que podemos cambiar la
implementación speedAfterStart()de la clase TrafficImpl, de la siguiente
manera:

public void speedAfterStart(double timeSec,


int trafficUnitsNumber, SpeedModel speedModel) {
List<TrafficUnit> trafficUnits =
FactoryTraffic.generateTraffic(trafficUnitsNumber,
month, dayOfWeek, hour, country, city, trafficLight);
for(TrafficUnit tu: trafficUnits){
Vehicle vehicle = FactoryVehicle.build(tu);
vehicle.setSpeedModel(speedModel);

pág. 193
double speed = vehicle.getSpeedMph(timeSec);
speed = (double) Math.round(speed * tu.getTraction());
printResult(tu, timeSec, speed);
}
}

Este cambio permite a los usuarios de la API Traffic pasar SpeedModelcomo


una función:

Traffic traffic = new TrafficImpl(Month.APRIL,


DayOfWeek.FRIDAY, 17, "USA", "Denver", "Main103S");
double timeSec = 10.0;
int trafficUnitsNumber = 10;
SpeedModel speedModel = (t, wp, hp) -> {
double weightPower = 2.0 * hp * 746 * 32.174 / wp;
return (double) Math
.round(Math.sqrt(t * weightPower) * 0.68);
};
traffic.speedAfterStart(timeSec, trafficUnitsNumber,
speedModel);

4. El resultado del código anterior es el mismo que cuando SpeedModel fue generado
por FactorySpeedModel. Pero ahora los usuarios de API pueden crear su propia función
de cálculo de velocidad.
5. Podemos anotar la interfaz SpeedModel como @FunctionalInterface, por lo
que todos los que intenten agregarle otro método recibirán una advertencia y no podrán
agregar otro método abstracto sin eliminar esta anotación y ser conscientes del riesgo de
romper el código de los clientes existentes que Ya he implementado esta interfaz
funcional.
6. Podemos enriquecer la API agregando varios criterios que dividen todo el tráfico posible
en segmentos.

Por ejemplo, los usuarios de API pueden querer analizar solo automóviles, camiones,
automóviles con un motor de más de 300 caballos de fuerza o camiones con un motor
de más de 400 caballos de fuerza. La forma tradicional de lograr esto sería mediante la
creación de métodos como estos:

void speedAfterStartCarEngine(double timeSec,


int trafficUnitsNumber, int horsePower);
void speedAfterStartCarTruckOnly(double timeSec,
int trafficUnitsNumber);
void speedAfterStartEngine(double timeSec,
int trafficUnitsNumber, int carHorsePower,
int truckHorsePower);

En cambio, solo podemos agregar interfaces funcionales estándar


al método speedAfterStart() existente de la interfaz Traffic y dejar que el
usuario de la API decida qué segmento de tráfico extraer:

void speedAfterStart(double timeSec, int trafficUnitsNumber,


SpeedModel speedModel, Predicate<TrafficUnit> limitTraffic);

pág. 194
La implementación del método en la clase cambiaría de la siguiente
manera: speedAfterStart()TrafficImpl

public void speedAfterStart(double timeSec,


int trafficUnitsNumber, SpeedModel speedModel,
Predicate<TrafficUnit> limitTraffic) {
List<TrafficUnit> trafficUnits =
FactoryTraffic.generateTraffic(trafficUnitsNumber,
month, dayOfWeek, hour, country, city, trafficLight);
for(TrafficUnit tu: trafficUnits){
if(limitTraffic.test(tu){
Vehicle vehicle = FactoryVehicle.build(tu);
vehicle.setSpeedModel(speedModel);
double speed = vehicle.getSpeedMph(timeSec);
speed = (double) Math.round(speed *
tu.getTraction());
printResult(tu, timeSec, speed);
}
}
}

Los usuarios Traffic de la API pueden definir el tráfico que necesitan de la


siguiente manera:

Predicate<TrafficUnit> limit = tu ->


(tu.getHorsePower() < 250
&& tu.getVehicleType() == VehicleType.CAR) ||
(tu.getHorsePower() < 400
&& tu.getVehicleType() == VehicleType.TRUCK);
traffic.speedAfterStart(timeSec,
trafficUnitsNumber, speedModel, limit);

Los resultados ahora se limitan a automóviles con un motor menor de 250 hpy
camiones con un motor menor de 400 hp:

De hecho, un usuario Traffic de API ahora puede aplicar cualquier criterio para
limitar el tráfico siempre que sea aplicable a los valores del objeto TrafficUnit. Un
usuario puede escribir, por ejemplo, lo siguiente:

Predicate<TrafficUnit> limitTraffic =
tu -> tu.getTemperature() > 65
&& tu.getTireCondition() == TireCondition.NEW
&& tu.getRoadCondition() == RoadCondition.WET;

pág. 195
Alternativamente, pueden escribir cualquier otra combinación de límites en los valores
que provienen de TrafficUnit. Si un usuario decide eliminar el límite y analizar todo
el tráfico, este código también lo hará:

traffic.speedAfterStart (timeSec, trafficUnitsNumber,


speedModel, tu -> true );

7. Si hay una necesidad de seleccionar las unidades de tráfico por la velocidad, podemos
aplicar los criterios precedentes después de los cálculos de velocidad (cuenta de cómo hemos
reemplazado Predicate con BiPredicateya que tenemos que utilizar dos parámetros
ahora):

public void speedAfterStart(double timeSec,


int trafficUnitsNumber, SpeedModel speedModel,
BiPredicate<TrafficUnit, Double> limitSpeed){
List<TrafficUnit> trafficUnits =
FactoryTraffic.generateTraffic(trafficUnitsNumber,
month, dayOfWeek, hour, country, city, trafficLight);
for(TrafficUnit tu: trafficUnits){
Vehicle vehicle = FactoryVehicle.build(tu);
vehicle.setSpeedModel(speedModel);
double speed = vehicle.getSpeedMph(timeSec);
speed = (double) Math.round(speed*tu.getTraction());
if(limitSpeed.test(tu, speed)){
printResult(tu, timeSec, speed);
}
}
}

Los usuarios Traffic de API ahora pueden escribir el siguiente código :

BiPredicate<TrafficUnit, Double> limit = (tu, sp) ->


(sp > (tu.getSpeedLimitMph() + 8.0) &&
tu.getRoadCondition() == RoadCondition.DRY) ||
(sp > (tu.getSpeedLimitMph() + 5.0) &&
tu.getRoadCondition() == RoadCondition.WET) ||
(sp > (tu.getSpeedLimitMph() + 0.0) &&
tu.getRoadCondition() == RoadCondition.SNOW);
traffic.speedAfterStart(timeSec,
trafficUnitsNumber, speedModel, limit);

El predicado anterior selecciona las unidades de tráfico que exceden el límite de


velocidad en más de una cierta cantidad (que es diferente para diferentes
condiciones de manejo). Si es necesario, puede ignorar la velocidad por
completo y limitar el tráfico exactamente de la misma manera que lo hizo el
predicado anterior. El único inconveniente de esta implementación es que es un
poco menos eficiente porque el predicado se aplica después de los cálculos de
velocidad. Esto significa que el cálculo de la velocidad se realizará para cada
unidad de tráfico generada, no en un número limitado, como en la
implementación anterior. Si esto le preocupa, puede dejar todas las firmas
diferentes que hemos discutido en esta receta:

pág. 196
public interface Traffic {
void speedAfterStart(double timeSec, int trafficUnitsNumber);
void speedAfterStart(double timeSec, int trafficUnitsNumber,
SpeedModel speedModel);
void speedAfterStart(double timeSec,
int trafficUnitsNumber, SpeedModel speedModel,
Predicate<TrafficUnit> limitTraffic);
void speedAfterStart(double timeSec,
int trafficUnitsNumber, SpeedModel speedModel,
BiPredicate<TrafficUnit,Double> limitTraffic);
}

De esta manera, el usuario de la API decide cuál de los métodos usar, más flexible o más
eficiente, y decide si la implementación de cálculo de velocidad predeterminada es
aceptable.

Hay más...
Hasta ahora, no le hemos dado al usuario de la API una opción del formato de
salida. Actualmente, se implementa como el método printResult():

void printResult(TrafficUnit tu, double timeSec, double speedMph) {


System.out.println("Road " + tu.getRoadCondition() +
", tires " + tu.getTireCondition() + ": "
+ tu.getVehicleType().getType() + " speedMph ("
+ timeSec + " sec)=" + speedMph + " mph");
}

Para hacerlo más flexible, podemos agregar otro parámetro a nuestra API:

Traffic traffic = new TrafficImpl(Month.APRIL, DayOfWeek.FRIDAY, 17,


"USA", "Denver", "Main103S");
double timeSec = 10.0;
int trafficUnitsNumber = 10;
BiConsumer<TrafficUnit, Double> output = (tu, sp) ->
System.out.println("Road " + tu.getRoadCondition() +
", tires " + tu.getTireCondition() + ": "
+ tu.getVehicleType().getType() + " speedMph ("
+ timeSec + " sec)=" + sp + " mph");
traffic.speedAfterStart(timeSec, trafficUnitsNumber, speedModel, output);

Observe que tomamos el valor timeSec no como uno de los parámetros de la función,
sino del alcance adjunto de la función. Podemos hacer esto porque permanece
constante (y puede ser efectivamente final) a lo largo de los cálculos. De la misma
manera, podemos añadir cualquier otro objeto a la función output un nombre de
archivo u otro dispositivo de salida, por ejemplo - lo que deja todas las decisiones
relacionados con la producción para el usuario de la API. Para acomodar esta nueva
función, la implementación de la API cambia a lo siguiente:

public void speedAfterStart(double timeSec, int trafficUnitsNumber,


SpeedModel speedModel, BiConsumer<TrafficUnit, Double> output) {

pág. 197
List<TrafficUnit> trafficUnits =
FactoryTraffic.generateTraffic(trafficUnitsNumber, month,
dayOfWeek, hour, country, city, trafficLight);
for(TrafficUnit tu: trafficUnits){
Vehicle vehicle = FactoryVehicle.build(tu);
vehicle.setSpeedModel(speedModel);
double speed = vehicle.getSpeedMph(timeSec);
speed = (double) Math.round(speed * tu.getTraction());
output.accept(tu, speed);
}
}

Nos llevó un tiempo llegar a este punto, donde el poder de la programación funcional
comienza a brillar y justifica el esfuerzo de aprenderlo. Sin embargo, cuando se utilizan
para procesar secuencias, como se describe en el próximo capítulo, las expresiones
lambda producen aún más potencia.

Arroyos y tuberías
En Java 8 y 9, la API de colecciones obtuvo un importante lavado de cara con la
introducción de secuencias e iteraciones internas al aprovechar las expresiones
lambda. En Java 10 (JDK 18.3), nuevo
métodos- List.copyOf, Set.copyOfy Map.copyOf- se añadieron que nos
permiten crear una nueva colección inmutable de las instancias existentes. Además,
los nuevos métodos - toUnmodifiableList , toUnmodifiableSet
y toUnmodifiableMap- se añadieron a la clase Collectors en el paquete
java.util.stream, permitiendo que los elementos de Stream sean recogidos en
una colección inmutable. Este capítulo le muestra cómo usar los flujos y encadenar
múltiples operaciones para crear una tubería. Además, el lector aprenderá cómo se
pueden realizar estas operaciones en paralelo. La lista de recetas incluye lo siguiente:

• Cree colecciones inmutables utilizando los métodos of() y copyOf()factory


• Crear y operar en streams
• Utilice flujos numéricos para operaciones aritméticas.
• Completa transmisiones produciendo colecciones
• Completa transmisiones produciendo mapas
• Completar secuencias agrupando elementos de secuencia
• Crear una tubería de operación de flujo
• Procesando una secuencia en paralelo

Introducción
Las expresiones Lambda descritas y demostradas en el capítulo anterior se
introdujeron en Java 8. Junto con las interfaces funcionales, agregaron la capacidad de
programación funcional a Java, permitiendo el paso del comportamiento (funciones)

pág. 198
como parámetros a las bibliotecas optimizadas para el rendimiento del procesamiento
de datos. De esta manera, un programador de aplicaciones puede concentrarse en los
aspectos comerciales del sistema desarrollado, dejando los aspectos de rendimiento a
los especialistas, los autores de la biblioteca.

Un ejemplo de dicha biblioteca es el paquete java.util.stream , que será el tema


central de este capítulo. Este paquete le permite tener una presentación declarativa de
los procedimientos que pueden aplicarse posteriormente a los datos, también en
paralelo; Estos procedimientos se presentan como flujos, que son objetos de la interfaz
Stream. Para una mejor transición de las colecciones tradicionales a las
transmisiones, se agregaron dos métodos predeterminados
( stream()y parallelStream()) a la interfaz java.util.Collection, junto
con la adición de nuevos métodos de fábrica de la generación de transmisiones a
la interfaz Stream.

Este enfoque aprovecha el poder de la agregación, como se discute en Capítulo


2 , Fast Track to OOP - Clases e interfaces . Junto con otros principios de
diseño ( encapsulación, interfaz y polimorfismo ) , facilita un diseño altamente
extensible y flexible, mientras que las expresiones lambda le permiten implementarlo
de manera concisa y sucinta.

Hoy, cuando los requisitos de aprendizaje automático del procesamiento masivo de


datos y el ajuste fino de las operaciones se han vuelto omnipresentes, estas nuevas
características refuerzan la posición de Java entre algunos lenguajes de programación
modernos de elección.

Crear colecciones inmutables


utilizando los métodos de fábrica
of () y copyOf ()
En esta receta, volveremos a examinar tradicionales métodos de creación de
colecciones y compararlos con el List.of(), Set.of(), Map.of(),
y Map.ofEntries(), métodos de fábrica que vienen con Java 9, y el , y métodos
que vienen con Java 10 . List.copyOf()Set.copyOf()Map.copyOf()

Prepararse
Antes de Java 9, había varias formas de crear colecciones. Aquí está la forma más
popular que se utilizó para crear un List:

pág. 199
List<String> list = new ArrayList<>();
list.add("This ");
list.add("is ");
list.add("built ");
list.add("by ");
list.add("list.add()");
list.forEach(System.out::print);

Si ejecutamos el código anterior, obtenemos esto:

La forma más corta de crear la colección List es comenzando con una matriz:

Arrays.asList("This ", "is ", "created ", "by ",


"Arrays.asList()").forEach(System.out::print);

El resultado es el siguiente:

La colección Set solía crearse de manera similar:

Set<String> set = new HashSet<>();


set.add("This ");
set.add("is ");
set.add("built ");
set.add("by ");
set.add("set.add() ");
set.forEach(System.out::print);

Alternativamente, podemos construir Setcomenzando con una matriz:

new HashSet<>(Arrays.asList("This ", "is ", "created ", "by ",


"new HashSet(Arrays.asList()) "))
.forEach(System.out::print);

Aquí hay una ilustración de los resultados de los últimos dos ejemplos:

Tenga en cuenta que, a diferencia List, el orden de los elementos Set no se


conserva. Depende de la implementación del código hash y puede cambiar de una
computadora a otra. Pero el orden sigue siendo el mismo entre las ejecuciones en la
misma computadora. Tome nota de este último hecho, porque volveremos a ello más
tarde.

pág. 200
Y así es como solíamos crear Mapantes de Java 9:

Map<Integer, String> map = new HashMap<>();


map.put(1, "This ");
map.put(2, "is ");
map.put(3, "built ");
map.put(4, "by ");
map.put(5, "map.put() ");
map.entrySet().forEach(System.out::print);

La salida del código anterior es la siguiente:

Aunque el resultado anterior conserva el orden de los elementos, no está


garantizado Map porque se basa en las claves que se recopilan Set.

Aquellos que tuvieron que crear colecciones de esa manera a menudo apreciaron la
mejora JDK-Propuesta 269 Convenience Factory Methods for Collections (JEP 269) que
decía:

" Java a menudo es criticado por su verbosidad " y su objetivo era " Proporcionar métodos
de fábrica estáticos en las interfaces de recopilación que crearán instancias de
recopilación compactas e inmodificables ".

En respuesta a la crítica y la propuesta, Java 9 introdujo 12 métodos de fábrica


estáticas para cada una de las 3 interfaces - , y . Los siguientes son los métodos de
fábrica de : of()ListSetMapList

static <E> List<E> of()


//Returns list with zero elements
static <E> List<E> of(E
e1) //Returns list with one element
static <E> List<E> of(E
e1, E e2) //etc
static <E> List<E> of(E
e1, E e2, E e3)
static <E> List<E> of(E
e1, E e2, E e3, E e4)
static <E> List<E> of(E
e1, E e2, E e3, E e4, E e5)
static <E> List<E> of(E
e1, E e2, E e3, E e4, E e5, E e6)
static <E> List<E> of(E
e1, E e2, E e3, E e4, E e5, E e6, E e7)
static <E> List<E> of(E
e1, E e2, E e3, E e4, E e5,
E e6, E e7, E e8)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5,
E e6, E e7, E e8, E e9)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5,
E e6, E e7, E e8, E e9, E e10)
static <E> List<E> of(E... elements)

10 métodos de fábrica sobrecargados con un número fijo de elementos están


optimizados para el rendimiento y, como se indica en JEP 269
( http://openjdk.java.net/jeps/269 ), estos métodos

pág. 201
" evitar la asignación de matriz, la inicialización y la sobrecarga de recolección de
basura en la que incurren las llamadas varargs " .

El uso de los métodos of() de fábrica hace que el código sea mucho más compacto:

List.of("This ", "is ", "created ", "by ", "List.of()")


.forEach(System.out::print);
System.out.println();
Set.of("This ", "is ", "created ", "by ", "Set.of() ")
.forEach(System.out::print);
System.out.println();
Map.of(1, "This ", 2, "is ", 3, "built ", 4, "by ", 5,"Map.of() ")
.entrySet().forEach(System.out::print);

La declaración System.out.println()se agregó para inyectar un salto de línea


entre los resultados:

Uno de los 12 métodos de fábrica estáticos en la Map interfaz es diferente de los


otros métodos of():

Map <K, V> ofEntries (Map.Entry <K, V> ... entradas)

Aquí hay un ejemplo de su uso:

Map.ofEntries(
entry(1, "This "),
entry(2, "is "),
entry(3, "built "),
entry(4, "by "),
entry(5, "Map.ofEntries() ")
).entrySet().forEach(System.out::print);

Produce el siguiente resultado:

Y no existe un método Map.of()de fábrica para un número ilimitado de


elementos. Uno tiene que usar Map.ofEntries()al crear un mapa con más de 10
elementos.

En Java 10, las , y se introdujeron métodos. Nos permiten convertir cualquier


colección en una colección inmutable del tipo
correspondiente. List.copyOf()Set.copyOf()Map.copyOf()

pág. 202
Cómo hacerlo...
Como ya hemos mencionado, los
métodos Set.of(), Map.of()y Map.ofEntries()no preservan el orden de los
elementos de la colección. Esto es diferente de la anterior (antes de Java 9) instancias
del Set y Map el comportamiento de conservar el mismo orden mientras se ejecuta
en el mismo equipo. Los métodos Set.of(), Map.of()y Map.ofEntries() de
cambio de orden de los elementos entre las corridas incluso en el mismo equipo. El
orden permanece igual solo durante la misma ejecución, sin importar cuántas veces se
repita la colección. Cambiar el orden de los elementos de una ejecución a otra en la
misma computadora ayuda a los programadores a evitar la dependencia injustificada
de un cierto orden.

Otra característica de las colecciones generadas por el método of() estático del las
interfaces List, Set y Map es su inmutabilidad. ¿Qué significa esto? Considere el
siguiente código:

List<String> list = List.of("This ", "is ", "immutable");


list.add("Is it?"); //throws UnsupportedOperationException
list.set(1, "is not "); //throws UnsupportedOperationException

Como puede ver, cualquier intento de agregar un nuevo elemento o modificar un


elemento existente de una colección creada usando el método List.of() da como
resultado una excepción java.lang.UnsupportedOperationException de
tiempo de ejecución.

Además, el método List.of() no acepta un elemento null, por lo que el siguiente


código genera una excepción java.lang.NullPointerException de tiempo de
ejecución:

List<String> list = List.of("This ", "is ", "not ", "created ", null);

Las colecciones creadas por Set.of()y Map.of()tienen el mismo comportamiento


que el método List.of()descrito anteriormente:

Set<String> set = Set.of("a", "b", "c");


//set.remove("b"); //UnsupportedOperationException
//set.add("e"); //UnsupportedOperationException
//set = Set.of("a", "b", "c", null); //NullPointerException

Map<Integer, String> map = Map.of(1, "one", 2, "two", 3, "three");


//map.remove(2); //UnsupportedOperationException
//map.put(5, "five "); //UnsupportedOperationException
//map = Map.of(1, "one", 2, "two", 3, null); //NullPointerException
//map = Map.ofEntries(entry(1, "one"), null); //NullPointerException

pág. 203
Los métodos List.copyOf()y Set.copyOf(), proporcionan otra manera de crear una
colección inmutable basado en otra
colección: List.copyOf()Set.copyOf()Map.copyOf()

List<Integer> list = Arrays.asList(1,2,3);


list = List.copyOf(list);
//list.set(1, 0); //UnsupportedOperationException
//list.remove(1); //UnsupportedOperationException

Set<Integer> setInt = Set.copyOf(list);


//setInt.add(42); //UnsupportedOperationException
//setInt.remove(3); //UnsupportedOperationException

Set<String> set = new HashSet<>(Arrays.asList("a","b","c"));


set = Set.copyOf(set);
//set.add("d"); //UnsupportedOperationException
//set.remove("b"); //UnsupportedOperationException

Map<Integer, String> map = new HashMap<>();


map.put(1, "one ");
map.put(2, "two ");
map = Map.copyOf(map);
//map.remove(2); //UnsupportedOperationException
//map.put(3, "three "); //UnsupportedOperationException

Observe que el parámetro de entrada puede ser cualquier colección que tenga
elementos del mismo tipo o el tipo que extiende el tipo de los elementos de la colección
que se pasa:

class A{}
class B extends A{}

List<A> listA = Arrays.asList(new B(), new B());


Set<A> setA = new HashSet<>(listA);

List<B> listB = Arrays.asList(new B(), new B());


setA = new HashSet<>(listB);

//List<B> listB = Arrays.asList(new A(), new A()); //compiler error


//Set<B> setB = new HashSet<>(listA); //compiler error

Hay más...
No es un accidente que los valores no nulos y la inmutabilidad se aplicaron poco
después de que se introdujeran las expresiones lambda y las secuencias. Como verá en
las recetas subsiguientes, la programación funcional y las canalizaciones de flujo
fomentan un estilo fluido de codificación (utilizando el método de encadenamiento, así
como el método forEach()en los ejemplos de esta receta). El estilo fluido
proporciona un código más compacto y legible. R eliminar la necesidad de verificar

pág. 204
el valor null ayuda a mantenerlo de esta manera : compacto y enfocado en los
principales procedimientos de procesamiento.

La característica de inmutabilidad, a su vez, se alinea bien con el concepto efectivamente


final para las variables utilizadas por las expresiones lambda. Por ejemplo, una
colección mutable nos permite evitar esta limitación:

List<Integer> list = Arrays.asList(1,2,3,4,5);


list.set(2, 0);
list.forEach(System.out::print); //prints: 12045

list.forEach(i -> {
int j = list.get(2);
list.set(2, j + 1);
});
System.out.println();
list.forEach(System.out::print); //prints: 12545

En el código anterior, la expresión lambda utilizada por la segunda operación


forEach()mantiene el estado en el tercer elemento (con índice 2) de la lista
original. Hace posible , intencionalmente o no , introducir un estado en una expresión
lambda y causar diferentes resultados de la misma función en diferentes
contextos. Esto es especialmente peligroso en el procesamiento paralelo porque no se
puede predecir el estado de cada contexto posible. Es por eso que la inmutabilidad de
una colección es una adición útil que hace que el código sea más robusto y confiable.

Crear y operar en streams


En esta receta, describiremos cómo se pueden crear flujos y cómo se pueden aplicar las
operaciones a los elementos emitidos por los flujos. La discusión y los ejemplos son
aplicables para una corriente de cualquier tipo, incluyendo los arroyos numérica
especializada: , , y . El comportamiento específico de los flujos numéricos no se
presenta porque se describe en la siguiente receta, Uso de flujos numéricos para
operaciones aritméticas . IntStreamLongStreamDoubleStream

Prepararse
Hay muchas formas de crear una secuencia:

• Los métodos stream() y parallelStream() de la interfaz


java.util.Collection significa que todos los sub-interfaces,
incluyendo Set y List, tienen estos métodos también
• Dos métodos stream() sobrecargados de la clase java.util.Arrays, que
convierten matrices y submatrices en secuencias

pág. 205
• Los métodos of(), generate()y iterate() de la interfaz
java.util.stream.Stream
• Los métodos Stream<Path> list(), Stream<String>
lines()y Stream<Path> find() de la clase java.nio.file.Files
• El metodo Stream<String>lines() de la clase
java.io.BufferedReader

Después de crear una secuencia, se pueden aplicar varios métodos (llamados


operaciones) a sus elementos. Una secuencia en sí misma no almacena datos. En
cambio, adquiere datos de la fuente (y los proporciona o los emite a las operaciones)
según sea necesario. Las operaciones pueden formar una tubería utilizando el estilo
fluido, ya que muchas operaciones intermedias también pueden devolver una
secuencia. Tales operaciones se llaman operaciones intermedias . Los ejemplos de
operaciones intermedias incluyen los siguientes:

• map(): Transforma elementos según una función


• flatMap(): Transforma cada elemento en una secuencia de acuerdo con una
función
• filter(): Selecciona solo elementos que coinciden con un criterio
• limit(): Limita una secuencia al número especificado de elementos
• sorted(): Transforma una secuencia no ordenada en una secuencia ordenada
• distinct(): Elimina duplicados
• Otros métodos de la interfaz Stream que Stream también regresan

La tubería termina con una operación terminal . El procesamiento de los elementos


de flujo en realidad comienza solo cuando se ejecuta una operación de terminal. Luego,
todas las operaciones intermedias (si están presentes) comienzan a procesarse y el flujo
se cierra y no se puede volver a abrir hasta que la operación del terminal haya finalizado
con la ejecución. Ejemplos de operaciones terminales son:

• forEach()
• findFirst()
• reduce()
• collect()
• Otros métodos de la interfaz Stream que no regresan Stream

Las operaciones de terminal devuelven un resultado o producen un efecto secundario,


pero no devuelven el objeto Stream.

Todas las operaciones Stream admiten el procesamiento paralelo, lo que es


especialmente útil en el caso de una gran cantidad de datos procesados en una
computadora multinúcleo. Todas las interfaces y clases de la API Java Stream están en
el paquete java.util.stream.

pág. 206
En esta receta, vamos a demostrar secuencias secuenciales. El procesamiento de flujos
paralelos no es muy diferente. Uno solo tiene que observar que la canalización de
procesamiento no utiliza un estado de contexto que puede variar en diferentes
entornos de procesamiento. Discutiremos el procesamiento paralelo en otra receta más
adelante en este capítulo.

Cómo hacerlo...
En esta sección de la receta, presentaremos métodos para crear una secuencia . Cada
clase que implementa la interfaz Set o la interfaz List tiene el método
stream() y el método parallelStream(), que devuelve una instancia de
la interfaz Stream :

1. Considere los siguientes ejemplos de creación de secuencias:

List.of("This", "is", "created", "by", "List.of().stream()")


.stream().forEach(System.out::print);
System.out.println();
Set.of("This", "is", "created", "by", "Set.of().stream()")
.stream().forEach(System.out::print);
System.out.println();
Map.of(1, "This ", 2, "is ", 3, "built ", 4, "by ", 5,
"Map.of().entrySet().stream()")
.entrySet().stream().forEach(System.out::print);

Utilizamos el estilo fluido para hacer el código más compacto e


interpuesto System.out.println()para comenzar una nueva línea en la salida.

2. Ejecute el ejemplo anterior y debería ver el siguiente resultado:

Tenga en cuenta que List conserva el orden de los elementos, mientras que el orden
de los elementos Set cambia en cada ejecución. Este último ayuda a descubrir los
defectos basados en la dependencia de un determinado pedido cuando el pedido no está
garantizado.

3. Mira el Javadoc de la clase Arrays. Tiene dos métodos estáticos


sobrecargados: stream()

Stream<T> stream(T[] array)


Stream<T> stream(T[] array, int startInclusive, int endExclusive)

pág. 207
)

4. Escriba un ejemplo del uso de los dos últimos métodos:

String[] array = {"That ", "is ", "an ", "Arrays.stream(array)"};


Arrays.stream(array).forEach(System.out::print);
System.out.println();
String[] array1 = { "That ", "is ", "an ",
"Arrays.stream(array,0,2)" };
Arrays.stream(array1, 0, 2).forEach(System.out::print);

5. Ejecútelo y vea el resultado:

Observe que en el segundo ejemplo, solo los dos primeros elementos , con índices 0
y, 1 se seleccionaron para ser incluidos en la secuencia, como se pretendía.

6. O pen el Javadoc de la interfaz Stream y ver los métodos


of(), generate()y iterate() de fábrica estáticas:

Stream<T> of(T t) //Stream of one element


Stream<T> ofNullable(T t) //Stream of one element
// if not null. Otherwise, returns an empty Stream
Stream<T> of(T... values)
Stream<T> generate(Supplier<T> s)
Stream<T> iterate(T seed, UnaryOperator<T> f)
Stream<T> iterate(T seed, Predicate<T> hasNext,
UnaryOperator<T> next)

Los dos primeros métodos son simples, por lo que se saltan su demo y comienzan con
el tercer método, of(). Puede aceptar una matriz o elementos delimitados por comas.

7. Escribe el ejemplo de la siguiente manera:

String[] array = { "That ", "is ", "a ", "Stream.of(array)" };


Stream.of(array).forEach(System.out::print);
System.out.println();
Stream.of( "That ", "is ", "a ", "Stream.of(literals)" )
.forEach(System.out::print);

8. Ejecútelo y observe la salida:

9. Escribe los ejemplos del uso de los métodos generate()y iterate() de la


siguiente manera:

pág. 208
Stream.generate(() -> "generated ")
.limit(3).forEach(System.out::print);
System.out.println();
System.out.print("Stream.iterate().limit(10): ");
Stream.iterate(0, i -> i + 1)
.limit(10).forEach(System.out::print);
System.out.println();
System.out.print("Stream.iterate(Predicate < 10): ");
Stream.iterate(0, i -> i < 10, i -> i + 1)
.forEach(System.out::print);

Tuvimos que poner un límite al tamaño de las secuencias generadas por los dos
primeros ejemplos. De lo contrario, serían infinitos. El tercer ejemplo acepta un
predicado que proporciona el criterio para cuando la iteración tiene que detenerse.

10. Ejecute los ejemplos y observe los resultados:

11. Veamos el ejemplo del método Files.list(Path dir), que


devuelve Stream<Path> todas las entradas del directorio:

System.out.println("Files.list(dir): ");
Path dir = FileSystems.getDefault()
.getPath("src/main/java/com/packt/cookbook/ch05_streams/");
try(Stream<Path> stream = Files.list(dir)) {
stream.forEach(System.out::println);
} catch (Exception ex){
ex.printStackTrace();
}

Lo siguiente es de la API JDK:

"Este método debe usarse dentro de una declaración de prueba con recursos o una
estructura de control similar para garantizar que el directorio abierto de la secuencia se
cierre inmediatamente después de que se completen las operaciones de la secuencia ".

Y esto es lo que hicimos; utilizamos una declaración de prueba con


recursos. Alternativamente, podríamos usar una construcción try-catch-finally, cerrar
la secuencia en el bloque finalmente, y el resultado no cambiaría.

12. Ejecute los ejemplos anteriores y observe el resultado:

pág. 209
No todas las secuencias tienen que cerrarse explícitamente, aunque la interfaz
Stream se extiende AutoCloseable y uno esperaría que todas las secuencias
tengan que cerrarse automáticamente utilizando la instrucción try-with-
resources . Pero ese no es el caso. El Javadoc para la interfaz Stream
( https://docs.oracle.com/javase/8/docs/api/java/util/stream/S
tream.html ) dice:

" Las transmisiones tienen un método BaseStream.close()e


implementación AutoCloseable. La mayoría de las instancias de transmisiones no
necesitan cerrarse después del uso, ya que están respaldadas por colecciones, matrices o
funciones generadoras, que no requieren una administración especial de recursos. En
general, solo las transmisiones cuya fuente es un I / O canal, como los devueltos
por Files.lines(Path), requerirá el cierre ".

Esto significa que un programador debe conocer la fuente de la transmisión, por lo que
debe asegurarse de que la transmisión esté cerrada si la API de la fuente lo requiere.

13. Escribe un ejemplo del uso del método Files.lines():

System.out.println("Files.lines().limit(3): ");
String file = "src/main/java/com/packt/cookbook/" +
"ch05_streams/Chapter05Streams.java";
try(Stream<String> stream=Files.lines(Paths.get(file)).limit(3)){
stream.forEach(l -> {
if( l.length() > 0 ) {
System.out.println(" " + l);
}
});
} catch (Exception ex){
ex.printStackTrace();
}

La intención del ejemplo anterior era leer las primeras tres líneas del archivo
especificado e imprimir líneas no vacías con una sangría de tres espacios.

14. Ejecute el ejemplo anterior y vea el resultado:

pág. 210
15. Escribe el código que usa el método Files.find():

Stream<Path> find(Path start, int maxDepth, BiPredicate<Path,


BasicFileAttributes> matcher, FileVisitOption... options)

16. Similar al caso anterior, una secuencia generada por el método


Files.find()también debe cerrarse explícitamente. El método
Files.find()recorre el árbol de archivos enraizado en un archivo inicial dado y a la
profundidad solicitada y devuelve las rutas a los archivos que coinciden con el predicado
(que incluye los atributos del archivo). Escribe el siguiente código:

Path dir = FileSystems.getDefault()


.getPath("src/main/java/com/packt/cookbook/ch05_streams/");
BiPredicate<Path, BasicFileAttributes> select =
(p, b) -> p.getFileName().toString().contains("Factory");
try(Stream<Path> stream = Files.find(f, 2, select)){
stream.map(path -> path.getFileName())
.forEach(System.out::println);
} catch (Exception ex){
ex.printStackTrace();
}

17. Ejecute el ejemplo anterior y obtendrá el siguiente resultado:

Si es necesario, FileVisitorOption.FOLLOW_LINKS podría incluirse como el


último parámetro del método Files.find() si necesitamos realizar una búsqueda
que siga todos los enlaces simbólicos que pueda encontrar.

18. Los requisitos para usar el método BufferedReader.lines(), que


devuelve las líneas Stream<String> leídas de un archivo, son un poco diferentes. De
acuerdo con Javadoc
( https://docs.oracle.com/javase/8/docs/api/java/io/BufferedRe
ader.html ),
"El lector no debe ser operado durante la ejecución de la operación de flujo terminal. De
lo contrario, el resultado de la operación de flujo terminal no está definido".

Hay muchos otros métodos en el JDK que producen flujos. Pero son más especializados,
y no los demostraremos aquí debido a la escasez de espacio.

Cómo funciona...
pág. 211
A lo largo de los ejemplos anteriores, ya hemos demostrado varias operaciones de flujo:
métodos de la interfaz Stream . Usamos forEach() más a menudo
y limit()algunas veces. La primera es una operación terminal y la segunda es
intermedia. Veamos otros métodos de la interfaz Stream ahora.

Estas son las operaciones intermedias: métodos que regresan Stream y se pueden
conectar con un estilo fluido:

//1
Stream<T> peek(Consumer<T> action)
//2
Stream<T> distinct() //Returns stream of distinct elements
Stream<T> skip(long n) //Discards the first n elements
Stream<T> limit(long n) //Allows the first n elements to be processed
Stream<T> filter(Predicate<T> predicate)
Stream<T> dropWhile(Predicate<T> predicate)
Stream<T> takeWhile(Predicate<T> predicate)
//3
Stream<R> map(Function<T, R> mapper)
IntStream mapToInt(ToIntFunction<T> mapper)
LongStream mapToLong(ToLongFunction<T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<T> mapper)
//4
Stream<R> flatMap(Function<T, Stream<R>> mapper)
IntStream flatMapToInt(Function<T, IntStream> mapper)
LongStream flatMapToLong(Function<T, LongStream> mapper)
DoubleStream flatMapToDouble(Function<T, DoubleStream> mapper)
//5
static Stream<T> concat(Stream<T> a, Stream<T> b)
//6
Stream<T> sorted()
Stream<T> sorted(Comparator<T> comparator)

Las firmas de los métodos anteriores generalmente incluyen "? super T"un
parámetro de entrada y "? extends R"el resultado (consulte el Javadoc para la
definición formal). Los simplificamos eliminando estas anotaciones para proporcionar
una mejor visión general de la variedad y la comunidad de los métodos. Para
compensar, nos gustaría recapitular el significado de las notaciones genéricas
relacionadas, ya que se utilizan ampliamente en la API de Stream y pueden ser fuente
de confusión.

Veamos la definición formal del método flatMap()porque tiene todos ellos:

<R> Stream<R> flatMap(Function<? super T,

El símbolo <R> delante del método indica al compilador que es un método genérico
(el que tiene sus propios parámetros de tipo). Sin él, el compilador estaría buscando la
definición del tipo R . El tipo T no aparece en la lista delante del método porque está
incluido en la definición Stream<T> de la interfaz (mire en la parte superior de la
página donde se declara la interfaz). La notación ? super T significa que el tipo T o

pág. 212
su superclase está permitido aquí . La notación ? extends R significa que el tipo
R o su subclase está permitido aquí. Lo mismo se aplica a ? extends
Stream<...> : el tipo Stream o su subclase está permitido aquí.

Ahora, volvamos a nuestra lista (simplificada) de operaciones intermedias. Los hemos


dividido en varios grupos por similitud:

• El primer grupo contiene solo un método peek(), que le permite aplicar


la función Consumer a cada uno de los elementos del flujo sin afectar el
elemento porque la función Consumer no devuelve nada. Por lo general, se usa
para depurar:

int sum = Stream.of( 1,2,3,4,5,6,7,8,9 )


.filter(i -> i % 2 != 0)
.peek(i -> System.out.print(i))
.mapToInt(Integer::intValue)
.sum();
System.out.println("sum = " + sum);

Si ejecuta el código anterior, el resultado será el siguiente:

• En el segundo grupo de operaciones intermedias mencionadas anteriormente, las


tres primeras distinct() , se explican por si mismas. El método es uno de los
más utilizados. Hace lo que su nombre sugiere: elimina del flujo aquellos elementos
que no coinciden con el criterio pasado como función. Vimos un ejemplo de su uso
en el fragmento de código anterior: solo los números impares podían fluir a través
del filtro. El método descarta los elementos siempre que se cumpla el criterio (luego
permite que el resto de los elementos de flujo fluyan a la siguiente
operación). El método hace lo contrario: permite que los elementos fluyan siempre
que se cumpla el criterio (luego descarta el resto de los elementos). Aquí hay un
ejemplo del uso de estas operaciones: skip()limit()filter(Predicate
p)PredicatedropWhile()takeWhile()
• System.out.println("Files.lines().dropWhile().takeWhile():");
String file = "src/main/java/com/packt/cookbook/" +
"ch05_streams/Chapter05Streams.java";
try(Stream<String> stream = Files.lines(Paths.get(file))){
stream.dropWhile(l ->
!l.contains("dropWhile().takeWhile()"))
.takeWhile(l -> !l.contains("} catc" + "h"))
.forEach(System.out::println);
} catch (Exception ex){
ex.printStackTrace();
}

pág. 213
Este código lee el archivo donde se almacena el código anterior. Queremos que
se imprima "Files.lines().dropWhile().takeWhile():"primero,
luego imprima todas las líneas anteriores excepto las últimas tres. Entonces, el
código anterior descarta todas las primeras líneas del archivo que no tienen
la subcadena dropWhile().takeWhile(), luego permite que todas las
líneas fluyan hasta que se encuentre la subcadena } catch.

Tenga en cuenta que tuvimos que escribir en "} catc" + "h" lugar de "}
catch". De lo contrario, el código "} catch"contains(" catch")encontraría y
no iría más lejos . El resultado del código anterior de la siguiente manera:

• El grupo de operaciones map()también es bastante sencillo. Dicha operación


transforma cada elemento de la secuencia al aplicarle una función que se pasó como
parámetro. Ya hemos visto un ejemplo del uso del método mapToInt(). Aquí hay
otro ejemplo de la operación map():

Stream.of( "That ", "is ", "a ", "Stream.of(literals)" )


.map(s -> s.contains("i"))
.forEach(System.out::println);

En este ejemplo, transformamos literales Stringen boolean. El resultado es


el siguiente:

• El siguiente grupo de operaciones intermedias, denominado flatMap(),


proporciona un procesamiento más complejo. Una operación flatMap() aplica la
función pasada (que devuelve una secuencia) a cada uno de los elementos para que
la operación pueda producir una secuencia compuesta de las secuencias extraídas
de cada uno de los elementos. Aquí hay un ejemplo de uso flatMap():
• Stream.of( "That ", "is ", "a ", "Stream.of(literals)" )
.filter(s -> s.contains("Th"))
.flatMap(s -> Pattern.compile("(?!^)").splitAsStream(s))
.forEach(System.out::print);

pág. 214
El código anterior selecciona de los elementos de la secuencia solo literales que los
contienen Th y los convierte en una secuencia de caracteres, que luego
imprime forEach(). El resultado de esto es el siguiente:

• El método concat() crea una secuencia a partir de dos secuencias de entrada


para que todos los elementos de la primera secuencia sean seguidos por todos los
elementos de la segunda secuencia. Aquí hay un ejemplo de esta funcionalidad:

Stream.concat(Stream.of(4,5,6), Stream.of(1,2,3))
.forEach(System.out::print);

El resultado es el siguiente:

En el caso de que haya más de dos concatenaciones de flujo, se puede escribir lo


siguiente:

Stream.of(Stream.of(4,5,6), Stream.of(1,2,3), Stream.of(7,8,9))


.flatMap(Function.identity())
.forEach(System.out::print);

El resultado es el siguiente:

Observe que, en el código anterior, Function.identity() es una función que


devuelve su argumento de entrada. Lo usamos porque no necesitamos transformar las
secuencias de entrada, sino simplemente pasarlas como están a la secuencia
resultante. Sin usar esta operación flatMap(), la secuencia consistiría en
los objetos Stream, no en sus elementos, y la salida se
mostraría java.util.stream.ReferencePipeline$Head@548b7f67java.
util.stream.ReferencePipeline$Head@7ac7a4e4
java.util.stream.ReferencePipeline$Head@6d78f375 .

• El último grupo de operaciones intermedias se compone de


los métodos sorted()que ordenan los elementos de la secuencia en un orden
natural (si son del Comparable tipo) o de acuerdo con

pág. 215
el objeto Comparator pasado . Es una operación con estado (así
como distinct(), limit()y skip()) que produce un resultado no
determinista en el caso del procesamiento paralelo (ese es el tema de la secuencia
Procesamiento de la receta en paralelo a continuación).

Ahora, veamos las operaciones de la terminal (también simplificamos su firma al


eliminar ? super T y ? extends R):

//1
long count() //Returns total count of elements
//2
Optional<T> max(Comparator<T> c) //Returns max according to Comparator
Optional<T> min(Comparator<T> c) //Returns min according to Comparator
//3
Optional<T> findAny() //Returns any or empty Optional
Optional<T> findFirst() //Returns the first element or empty Optional
//4
boolean allMatch(Predicate<T> p) //All elements match Predicate?
boolean anyMatch(Predicate<T> p) //Any element matches Predicate?
boolean noneMatch(Predicate<T> p) //No element matches Predicate?
//5
void forEach(Consumer<T> action) //Apply action to each element
void forEachOrdered(Consumer<T> action)
//6
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<U,T,U> accumulator,
BinaryOperator<U> combiner)
//7
R collect(Collector<T,A,R> collector)
R collect(Supplier<R> supplier, BiConsumer<R,T> accumulator,
BiConsumer<R,R> combiner)
//8
Object[] toArray()
A[] toArray(IntFunction<A[]> generator)

Los primeros cuatro grupos se explican por sí mismos, pero necesitamos decir
algunas palabras al respecto Optional. El Javadoc
( https://docs.oracle.com/javase/8/docs/api/java/util/Optional
.html ) lo define como,

"Un objeto contenedor que puede o no contener un valor no nulo. Si hay un valor
presente, isPresent()devuelve truey get()devuelve el valor".

Le permite evitar NullPointerExceptiono verificar null(bueno, debe llamar


de isPresent()todos modos). Tiene su propio métodos
map(), filter()y flatMap(). Además, Optionaltiene métodos que incluyen
la isPresent()verificación implícitamente:

• ifPresent(Consumer<T> action): Realiza la acción con el valor si está


presente, de lo contrario no hace nada

pág. 216
• ifPresentOrElse(Consumer<T> action, Runnable
emptyAction): Realiza la acción proporcionada con el valor si está presente, de
lo contrario realiza la acción basada en vacío proporcionada
• or(Supplier<Optional<T>> supplier): Devuelve una Optional clase
que describe el valor si está presente; de lo contrario, devuelve
una Optional clase producida por la función proporcionada
• orElse(T other): Devuelve el valor si está presente; de lo contrario, devuelve
el objeto proporcionadoother
• orElseGet(Supplier<T> supplier): R devuelve el valor si está presente,
de lo contrario devuelve el resultado producido por la función proporcionada
• orElseThrow(Supplier<X> exceptionSupplier): Devuelve el valor si
está presente; de lo contrario, arroja una excepción producida por la función
proporcionada

Tenga en cuenta que Optional se utiliza como valor de retorno en los casos en
que nulles un posible resultado. Aquí hay un ejemplo de su uso. Reimplementamos el
código de concatenación de secuencias utilizando la operación reduce()que
devuelve Optional:

Stream.of(Stream.of(4,5,6), Stream.of(1,2,3), Stream.of(7,8,9))


.reduce(Stream::concat)
.orElseGet(Stream::empty)
.forEach(System.out::print);

El resultado es el mismo que en la implementación anterior con el método


flatMap():

El siguiente grupo de operaciones terminales se conoce como forEach(). Estas


operaciones garantizan que la función dada se aplicará a cada elemento de la
secuencia. Pero forEach() no dice nada sobre el orden, que podría modificarse para
un mejor rendimiento. Por el contrario, forEachOrdered() garantiza no solo el
procesamiento de todos los elementos de la secuencia, sino que también lo hace en el
orden especificado por su fuente, independientemente de si la secuencia es secuencial
o paralela. Aquí hay un par de ejemplos de esto:

Stream.of("3","2","1").parallel().forEach(System.out::print);
System.out.println();
Stream.of("3","2","1").parallel().forEachOrdered(System.out::print);

El resultado es el siguiente:

pág. 217
Como puede ver, en el caso del procesamiento en paralelo, forEach()no garantiza
el pedido, mientras que forEachOrdered()sí. Aquí hay otro ejemplo de uso de
ambos Optionaly forEach():

Stream.of( "That ", "is ", "a ", null, "Stream.of(literals)" )


.map(Optional::ofNullable)
.filter(Optional::isPresent)
.map(Optional::get)
.map(String::toString)
.forEach(System.out::print);

No pudimos usar Optional.of()y se utiliza Optional.ofNullable()en su


lugar porque Optional.of()sería tirar NullPointerException en null. En
tal caso, Optional.ofNullable()solo vuelve Optional vacío. El resultado es el
siguiente:

Ahora, hablemos sobre el próximo grupo de operaciones de terminal,


llamado reduce(). Cada uno de los tres métodos sobrecargados devuelve un solo
valor después de procesar todos los elementos de la secuencia. Entre los ejemplos más
simples se encuentra encontrar una suma de los elementos de la secuencia en caso de
que sean números, o max, min y similares. Pero se puede construir un resultado más
complejo para una secuencia de objetos de cualquier tipo.

El primer método, Optional<T> reduce(BinaryOperator<T>


accumulator)devuelve el objeto Optional<T> porque es responsabilidad de la
función de acumulador proporcionada calcular el resultado, y los autores de la
implementación de JDK no pueden garantizar que siempre contendrá un valor no nulo:

int sum = Stream.of(1,2,3).reduce((p,e) -> p + e).orElse(0);


System.out.println("Stream.of(1,2,3).reduce(acc): " +sum);

La función pasada recibe el resultado de la ejecución previa de la misma función (como


el primer parámetro p) y el siguiente elemento de la secuencia (como el segundo
parámetro e). Para el primer elemento, pobtiene su valor, mientras que ees el segundo
elemento. Puede imprimir el pvalor de la siguiente manera:

int sum = Stream.of(1,2,3)


.reduce((p,e) -> {
System.out.println(p); //prints: 1 3
return p + e;

pág. 218
})
.orElse(10);
System.out.println("Stream.of(1,2,3).reduce(acc): " + sum);

La salida del código anterior es la siguiente:

Para evitar el paso adicional con Optional, el segundo método, T reduce(T


identity, BinaryOperator<T> accumulator)devuelve el valor
proporcionado como primer parámetro identity, del tipo T (que es el tipo de los
elementos de Stream<T>) en caso de que la secuencia esté vacía. Este parámetro
debe cumplir con for all t, ya que accumulator.apply(identity, t)es igual
al t requisito (de Javadoc). En nuestro caso, tiene que ser 0 para que cumpla 0 + e
== e. Aquí hay un ejemplo de cómo usar el segundo método:

int sum = Stream.of(1,2,3).reduce(0, (p,e) -> p + e);


System.out.println("Stream.of(1,2,3).reduce(0, acc): " + sum);

El resultado es el mismo que con el primer método reduce().

El tercer método, U reduce(U identity, BiFunction<U,T,U>


accumulator, BinaryOperator<U> combiner)convierte el valor del tipo
T en un valor del tipo U con la ayuda de la función
BiFunction<U,T,U> . BiFunction<U,T,U> se utiliza como acumulador para
que el resultado (el U tipo) de su aplicación al elemento anterior (el T tipo) se
convierta en una entrada en la función junto con el elemento actual de la
secuencia. Aquí hay un ejemplo de código:

String sum = Stream.of(1,2,3)


.reduce("", (p,e) -> p + e.toString(), (x,y) -> x + "," + y);
System.out.println("Stream.of(1,2,3).reduce(,acc,comb): " + sum);

Naturalmente, se espera ver el resultado como 1,2,3. En cambio, vemos lo siguiente:

La razón del resultado anterior es que se utilizó el combinador porque la secuencia


era secuencial. Pero hagamos que la corriente sea paralela ahora:

pág. 219
String sum = Stream.of(1,2,3).parallel()
.reduce("", (p,e) -> p + e.toString(), (x,y) -> x + "," + y);
System.out.println("Stream.of(1,2,3).reduce(,acc,comb): " + sum);

El resultado de la ejecución del código anterior será el siguiente:

Esto significa que el combinador se llama solo para el procesamiento en paralelo con
el fin de ensamblar (combinar) los resultados de diferentes subtransmisiones
procesadas en paralelo. Esta es la única desviación que hemos notado hasta ahora de
la intención declarada de proporcionar el mismo comportamiento para flujos
secuenciales y paralelos. Pero hay muchas maneras de lograr el mismo resultado sin
usar esta tercera versión de reduce(). Por ejemplo, considere el siguiente código:

String sum = Stream.of(1,2,3)


.map(i -> i.toString() + ",")
.reduce("", (p,e) -> p + e);
System.out.println("Stream.of(1,2,3).map.reduce(,acc): "
+ sum.substring(0, sum.length()-1));

Produce el mismo resultado que el ejemplo anterior:

Ahora vamos a cambiarlo a un flujo paralelo:

String sum = Stream.of(1,2,3).parallel()


.map(i -> i.toString() + ",")
.reduce("", (p,e) -> p + e);
System.out.println("Stream.of(1,2,3).map.reduce(,acc): "
+ sum.substring(0, sum.length()-1));

El resultado sigue siendo el mismo: 1,2,3.

El siguiente grupo de operaciones intermedias, denominado collect(), consta de


dos métodos:

R collect(Collector<T,A,R> collector)
R collect(Supplier<R> supplier, BiConsumer<R,T> accumulator,
BiConsumer<R,R> combiner)

El primero acepta Collector<T,A,R>como parámetro. Es mucho más popular que


el segundo porque está respaldado por la clase Collectors, que proporciona una
amplia variedad de implementaciones de la interfaz Collector . Le recomendamos
que revise el Javadoc de la clase Collectors y vea lo que ofrece.

pág. 220
Analicemos algunos ejemplos del uso de la clase Collectors . Primero, crearemos
una pequeña clase de demostración llamada Thing:

public class Thing {


private int someInt;
public Thing(int i) { this.someInt = i; }
public int getSomeInt() { return someInt; }
public String getSomeStr() {
return Integer.toString(someInt); }
}

Ahora podemos usarlo para demostrar algunos coleccionistas:

double aa = Stream.of(1,2,3).map(Thing::new)
.collect(Collectors.averagingInt(Thing::getSomeInt));
System.out.println("stream(1,2,3).averagingInt(): " + aa);

String as = Stream.of(1,2,3).map(Thing::new).map(Thing::getSomeStr)
.collect(Collectors.joining(","));
System.out.println("stream(1,2,3).joining(,): " + as);

String ss = Stream.of(1,2,3).map(Thing::new).map(Thing::getSomeStr)
.collect(Collectors.joining(",", "[", "]"));
System.out.println("stream(1,2,3).joining(,[,]): " + ss);

El resultado será el siguiente:

El recopilador de unión es una fuente de alegría para cualquier programador que alguna
vez haya tenido que escribir código que verifique si el elemento agregado es el primero,
el último o elimina el último carácter (como hicimos en el ejemplo de la operación
reduce()). El colector producido por el método joining() hace esto detrás de
escena. Todo lo que el programador tiene que proporcionar es el delimitador, el prefijo
y el sufijo.

La mayoría de los programadores nunca necesitarán escribir un cobrador de


aduanas. Pero en el caso de que sea necesario, se puede usar el segundo
método, collect()of Stream, y proporcionar las funciones que componen el
recopilador o utilizar uno de los dos métodos estáticos Collector.of() que
generan un recopilador que se puede reutilizar.

Si compara las operaciones reduce()y collect(), notará que el propósito


principal de reduce()es operar sobre objetos inmutables y primitivas. El resultado
de reduce() es un valor que es típicamente (pero no exclusivamente) del mismo
tipo que los elementos de la secuencia. collect(), por el contrario, produce el
resultado de un tipo diferente envuelto en un recipiente mutable. El uso más popular

pág. 221
de collect()se centra alrededor de la producción de List, Seto Map objetos
utilizando el
correspondiente Collectors.toList(), Collectors.toSet()o Collectors
.toMap() colector.

El último grupo de operaciones de terminal consta de dos métodos toArray() :

Object[] toArray()
A[] toArray(IntFunction<A[]> generator)

El primero devuelve Object[], el segundo, una matriz del tipo especificado. Veamos
los ejemplos de su uso:

Object[] os = Stream.of(1,2,3).toArray();
Arrays.stream(os).forEach(System.out::print);
System.out.println();
String[] sts = Stream.of(1,2,3)
.map(i -> i.toString())
.toArray(String[]::new);
Arrays.stream(sts).forEach(System.out::print);

El resultado de estos ejemplos es el siguiente:

El primer ejemplo es bastante sencillo. Vale la pena señalar que no podemos escribir
lo siguiente:

Stream.of(1,2,3).toArray().forEach(System.out::print);

Esto se debe a que toArray()es una operación de terminal y la secuencia se cierra


automáticamente después de ella. Es por eso que tenemos que abrir una nueva
secuencia en la segunda línea de nuestro ejemplo de código anterior.

El segundo ejemplo, con el método sobrecargado A[]


toArray(IntFunction<A[]> generator), es más complicado. El Javadoc
( https://docs.oracle.com/javase/8/docs/api/java/util/stream/S
tream.html ) dice:

"La función del generador toma un número entero, que es el tamaño de la matriz
deseada, y produce una matriz del tamaño deseado".

Esto significa que la referencia del método a un constructor


toArray(String[]::new) en el último ejemplo es una versión más corta
de toArray(size -> new String[size]).

pág. 222
Usando flujos numéricos para
operaciones aritméticas
Además de la interfaz Stream , el paquete
java.util.stream también proporciona interfaces-
especializadas IntStream, y - que están optimizados para corrientes de tratamiento
de tipos primitivos correspondiente. Son muy cómodo de usar, y tiene operaciones
numéricas, tales
como DoubleStream LongStreammax() min() average() sum()

Las interfaces numéricas tienen métodos similares a los métodos de la interfaz Stream,
lo que significa que todo lo que hemos mencionado en la receta anterior, Crear y operar
en flujos , también se aplica a los flujos numéricos. Por eso, en esta sección, solo
hablaremos sobre los métodos que no están presentes en la interfaz Stream .

Prepararse
Además de los métodos descritos en la receta Crear y operar en secuencias ,
los siguientes métodos se pueden utilizar para crear una secuencia numérica:

• Los métodos range(int startInclusive, int


endInclusive) y rangeClosed(int startInclusive, int
endInclusive) de las interfaces IntStream y LongStream
• Seis métodos sobrecargados de la clase, que convierten matrices y submatrices
en flujos numéricos stream()java.util.Arrays

La lista de operaciones intermedias específicas para flujos numéricos incluye lo


siguiente:

• boxed(): Convierte una secuencia numérica de un tipo primitivo en una


secuencia del tipo de ajuste correspondiente
• mapToObj(mapper): Convierte una secuencia numérica de un tipo primitivo en
una secuencia de objetos utilizando el mapeador de funciones proporcionado
• asDoubleStream()de la interfaz LongStream :
Convierte LongStreamaDoubleStream
• asLongStream() y asDoubleStream()de la interfaz IntStream :
Convierte IntStream a la secuencia numérica correspondiente

La lista de operaciones aritméticas terminales específicas para flujos numéricos


incluye lo siguiente:

pág. 223
• sum(): Calcula una suma de los elementos de flujo numérico
• average(): Calcula un promedio de los elementos de flujo numérico
• summaryStatistics(): Crea un objeto con varios datos de resumen sobre los
elementos de la secuencia

Cómo hacerlo...
1. Experimente con los métodos y de las interfaces: range(int
startInclusive, int endInclusive)rangeClosed(int
startInclusive, int endInclusive)IntStreamLongStream

IntStream.range(1,3).forEach(System.out::print); //prints: 12
LongStream.range(1,3).forEach(System.out::print); //prints: 12
IntStream.rangeClosed(1,3).forEach(System.out::print); // 123
LongStream.rangeClosed(1,3).forEach(System.out::print); // 123

Como puede ver, la diferencia entre los métodos range()y rangeClosed()es la


exclusión o inclusión del valor pasado como segundo parámetro. Esto también conduce
a los siguientes resultados en el caso en que ambos parámetros tienen el mismo valor:

IntStream.range(3,3).forEach(System.out::print);
//prints:
LongStream.range(3,3).forEach(System.out::print);
//prints:
IntStream.rangeClosed(3,3).forEach(System.out::print);
//prints: 3
LongStream.rangeClosed(3,3).forEach(System.out::print);
//prints: 3

En los ejemplos anteriores, el método range() no emite ningún elemento, mientras


que el método emite solo un elemento. rangeClosed()

Tenga en cuenta que ninguno de estos métodos genera un error cuando el primer
parámetro es mayor que el segundo parámetro. Simplemente no emiten nada y las
siguientes declaraciones no producen ningún resultado:

IntStream.range(3,1).forEach(System.out::print);
LongStream.range(3,1).forEach(System.out::print);
IntStream.rangeClosed(3,1).forEach(System.out::print);
LongStream.rangeClosed(3,1).forEach(System.out::print);

2. Si no necesita que los valores de los elementos de secuencia sean secuenciales,


primero puede crear una matriz de valores y luego generar una secuencia utilizando uno de
los seis métodos estáticos sobrecargados de la clase: stream()java.util.Arrays

pág. 224
IntStream stream(int[] array)
IntStream stream(int[] array, int startInclusive,
int endExclusive)
LongStream stream(long[] array)
LongStream stream(long[] array, int startInclusive,
int endExclusive)
DoubleStream stream(double[] array)
DoubleStream stream(double[] array, int startInclusive,
int endExclusive)

Estos son los ejemplos del uso del método Arrays.stream() :

int[] ai = {2, 3, 1, 5, 4};


Arrays.stream(ai)
.forEach(System.out::print); //prints: 23154
Arrays.stream(ai, 1, 3)
.forEach(System.out::print); //prints: 31
long[] al = {2, 3, 1, 5, 4};
Arrays.stream(al)
.forEach(System.out::print); //prints: 23154
Arrays.stream(al, 1, 3)
.forEach(System.out::print); //prints: 31
double[] ad = {2., 3., 1., 5., 4.};
Arrays.stream(ad)
.forEach(System.out::print); //prints: 2.03.01.05.04.0
Arrays.stream(ad, 1, 3)
.forEach(System.out::print); //prints: 3.01.0

Las últimas dos canalizaciones se pueden mejorar para imprimir los


elementos DoubleStream en un formato más amigable para los humanos mediante
el uso del recopilador de unión que discutimos en la receta anterior, Creación y
operación en secuencias :

double[] ad = {2., 3., 1., 5., 4.};


String res = Arrays.stream(ad).mapToObj(String::valueOf)
.collect(Collectors.joining(" "));
System.out.println(res); //prints: 2.0 3.0 1.0 5.0 4.0
res = Arrays.stream(ad, 1, 3).mapToObj(String::valueOf)
.collect(Collectors.joining(" "));
System.out.println(res); //prints: 3.0 1.0

Puesto que el Collector<CharSequence, ?, String> colector de unirse


acepta CharSequence como un tipo de entrada, tuvimos que convertir el número
en String el uso de una operación intermedia, mapToObj().

3. Utilice la operación intermedia mapToObj(mapper) para convertir un elemento


de tipo primitivo en un tipo de referencia. Vimos un ejemplo de su uso en el paso 2. La
función de mapeador puede ser tan simple o tan compleja como sea necesario para lograr la
transformación necesaria.

También hay una operación especializada boxed(), sin parámetros que convierten
elementos de un tipo numérico primitivo al tipo de ajuste correspondiente: int valor

pág. 225
a Integer valor, long valor a Long valor y double valor a Double
valor. Podemos usarlo, por ejemplo, para lograr los mismos resultados que los dos
últimos ejemplos del uso de la operación mapToObj(mapper):

double [] ad = { 2. , 3. , 1. , 5. , 4. } ;
Cadena res = matrices. stream (ad) .boxed ()
.map (Object :: toString)
.collect (Coleccionistas. unirse ( "" ))
;
Sistema. out .println (res) ; // imprime: 2.0 3.0 1.0 5.0
4.0
res = Matrices. stream (ad , 1 , 3 ) .boxed ()
.map (Object :: toString)
.collect (Coleccionistas. unión( "" ))
;
Sistema. out .println (res) ; // impresiones: 3.0 1.0
4. También hay operaciones intermedias que convierten un elemento de una secuencia
numérica de un tipo primitivo a otro tipo primitivo
numérico: asLongStream() y asDoubleStream() en la IntStream interfaz
y asDoubleStream() en la LongStream interfaz. Veamos ejemplos de su uso:

double[] ad = {2., 3., 1., 5., 4.};


String res = Arrays.stream(ad).boxed()
.map(Object::toString)
.collect(Collectors.joining(" "));
System.out.println(res); //prints: 2.0 3.0 1.0 5.0 4.0
res = Arrays.stream(ad, 1, 3).boxed()
.map(Object::toString)
.collect(Collectors.joining(" "));
System.out.println(res); //prints: 3.0 1.0

Es posible que haya notado que estas operaciones son posibles solo para la conversión
primitiva de ampliación: del tipo int a long y double, y de long a double.

5. Las operaciones aritméticas terminales específicas para flujos numéricos son


bastante sencillas. Estos son ejemplos de
las operaciones sum()y average()con IntStream:

IntStream.range(1, 3).asLongStream()
.forEach(System.out::print); //prints: 12
IntStream.range(1, 3).asDoubleStream()
.forEach(d -> System.out.print(d + " ")); //prints: 1.0 2.0
LongStream.range(1, 3).asDoubleStream()
.forEach(d -> System.out.print(d + " ")); //prints: 1.0 2.0

Como puede ver, la operación average() vuelve OptionalDouble. Es


interesante considerar por qué los autores decidieron
volver OptionalDoublea average()pero no para sum(). Esta decisión
probablemente se tomó para asignar una secuencia vacía a una
pág. 226
vacía OptionalDouble, pero luego la decisión de regresar 0cuando se sum()aplica
a una secuencia vacía parece inconsistente.

Estas operaciones se comportan de la misma manera


para LongStreamy DoubleStream:

long suml = LongStream.range(1, 3).sum();


System.out.println(suml); //prints: 3
double avl = LongStream.range(1, 3).average().orElse(0);
System.out.println(avl); //prints: 1.5

double sumd = DoubleStream.of(1, 2).sum();


System.out.println(sumd); //prints: 3.0
double avd = DoubleStream.of(1, 2).average().orElse(0);
System.out.println(avd); //prints: 1.5

6. La operación del terminal summaryStatistics() recopila varios datos de


resumen sobre los elementos de la secuencia:

IntSummaryStatistics iss =
IntStream.empty().summaryStatistics();
System.out.println(iss); //count=0, sum=0,
//min=2147483647, average=0.000000, max=-2147483648
iss = IntStream.range(1, 3).summaryStatistics();
System.out.println(iss); //count=2, sum=3, min=1,
//average=1.500000, max=2

LongSummaryStatistics lss =
LongStream.empty().summaryStatistics();
System.out.println(lss); //count=0, sum=0,
//min=9223372036854775807,
//average=0.000000, max=-9223372036854775808
lss = LongStream.range(1, 3).summaryStatistics();
System.out.println(lss); //count=2, sum=3, min=1,
//average=1.500000, max=2

DoubleSummaryStatistics dss =
DoubleStream.empty().summaryStatistics();
System.out.println(dss); //count=0, sum=0.000000,
//min=Infinity, average=0.000000, max=-Infinity
dss = DoubleStream.of(1, 2).summaryStatistics();
System.out.println(dss); //count=2, sum=3.000000,
//min=1.000000, average=1.500000, max=2.000000

Las impresiones añadido como comentarios a las líneas de impresión anteriores


provienen del método toString()de los objetos
IntSummaryStatistics, LongSummaryStatisticso DoubleSummaryStat
istics , de manera correspondiente. Otros métodos de estos objetos
incluyen getCount(), getSum(), getMin(), getAverage(), y getMax(), lo
que permite el acceso a un aspecto particular de las estadísticas recogidas.

pág. 227
Observe que en el caso de una secuencia vacía, el valor mínimo (máximo) es el valor
más pequeño (mayor) posible del tipo Java correspondiente:

System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.MIN_VALUE); //-2147483648
System.out.println(Long.MAX_VALUE); // 9223372036854775807
System.out.println(Long.MIN_VALUE); //-9223372036854775808
System.out.println(Double.MAX_VALUE); //1.7976931348623157E308
System.out.println(Double.MIN_VALUE); //4.9E-324

Solo se muestra DoubleSummaryStatistics Infinity y -Infinity


como valores mínimo y máximo, en lugar de los números reales que se muestran
aquí. De acuerdo con el Javadoc de estos métodos, getMax()devuelve "el valor
máximo registrado, Double.NaNsi se registró algún
valor NaNo Double.NEGATIVE_INFINITYsi no se registraron valores"
y getMin()devuelve "el valor mínimo registrado, Double.NaNsi se registró algún
valor NaNo Double.POSITIVE_INFINITYsi no se registraron valores".

Además, tenga en cuenta que, en contraste con la operación average() de flujo de


terminal, el método getAverage() de cualquiera de las estadísticas de resumen
anteriores devuelve la media aritmética de los valores transmitidos, o cero si no se
emitieron valores del flujo, no el objeto Optional.

Hay más...
Los objetos IntSummaryStatistics, LongSummaryStatistics
y DoubleSummaryStatistics se pueden crear no sólo por la operación de terminal de
flujo numérico summaryStatistics(). Tal objeto también puede ser creado por
el funcionamiento del terminal de collect()que se aplica a cualquier objeto
Stream, no sólo IntStream, LongStreamo DoubleStream.

E ada de los objetos estadísticos de resumen tiene los


métodos accept()y combine(), que nos permiten crear un objeto Collector
que se puede pasar a la operación collect() y producir el correspondiente objeto
estadísticas de resumen. Demostraremos esta posibilidad creando el objeto
IntSummaryStatistics . Los objetos LongSummaryStatistics y DoubleSu
mmaryStatisticsse pueden crear de manera similar.

La clase IntSummaryStatistics tiene los siguientes dos métodos:

• aceptar nulo (valor int): incluye un nuevo valor en el resumen de estadísticas


• void combine ( IntSummaryStatistics other): agrega las estadísticas
recopiladas del objeto other proporcionado al actual

pág. 228
Estos métodos nos permiten usar la versión sobrecargada de la R
collect(Supplier<R> supplier, BiConsumer<R,? super T>
accumulator, BiConsumer<R,R> combiner) operación en cualquier objeto
Stream, de la siguiente manera:

IntSummaryStatistics iss = Stream.of(3, 1)


.collect(IntSummaryStatistics::new,
IntSummaryStatistics::accept,
IntSummaryStatistics::combine
);
System.out.println(iss); //count=2, sum=4, min=1,
//average=2.000000, max=3

Como puede ver, la secuencia no es una de las secuencias numéricas


especializadas. Solo tiene elementos numéricos del mismo tipo que el objeto de
estadísticas de resumen creado. Sin embargo, pudimos crear un objeto de la clase. Del
mismo modo, es posible crear objetos de
las clases y . IntSummaryStatisticsLongSummaryStatisticsDoubleSummaryStat
istics

Tenga en cuenta que el tercer parámetro, combiner se usa solo para el procesamiento
de flujo paralelo : combina los resultados de los flujos secundarios que se procesan en
paralelo. Para demostrar esto, podemos cambiar el ejemplo anterior de la siguiente
manera:

IntSummaryStatistics iss = Stream. de ( 3 , 1 )


.collect (IntSummaryStatistics :: new,
IntSummaryStatistics :: accept ,
(r , r1) -> {
System. out .println ( "Combinando ..." ) ; // no imprime
r.combine ( r1) ;
}
) ;
System.out.println (iss); // count = 2, sum = 4, min = 1,
//average=2.000000, max = 3

La línea Combining... no se imprime. Cambiemos el flujo a uno paralelo:


IntSummaryStatistics iss = Stream.of(3, 1)
.collect(IntSummaryStatistics::new,
IntSummaryStatistics::accept,
(r, r1) -> {
System.out.println("Combining..."); //is not printing
r.combine(r1);
}
);
System.out.println(iss); //count=2, sum=4, min=1,
//average=2.000000, max=3

Si ejecuta el código anterior ahora, verá la línea Combining... .

pág. 229
Otra forma de recopilar estadísticas es utilizar un objeto Collector creado por uno
de los siguientes métodos de la clase Collectors :

Collector<T, ?, IntSummaryStatistics>
summarizingInt (ToIntFunction<T> mapper)
Collector<T, ?, LongSummaryStatistics>
summarizingLong(ToLongFunction<T> mapper)
Collector<T, ?, DoubleSummaryStatistics>
summarizingDouble(ToDoubleFunction<T> mapper)

Nuevamente, utilizaremos el primero de los métodos anteriores para crear


el IntSummaryStatistics objeto. Supongamos que tenemos la
siguiente Person clase:

class Person {
private int age;
private String name;
public Person(int age, String name) {
this.name = name;
this.age = age;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
}

Si hay una secuencia de objetos de clase Person, podemos recopilar estadísticas de


la edad de las personas (elementos de secuencia) de la siguiente manera:

IntSummaryStatistics iss =
Stream.of(new Person(30, "John"), new Person(20, "Jill"))
.collect(Collectors.summarizingInt(Person::getAge));
System.out.println(iss); //count=2, sum=50, min=20,
//average=25.000000, max=30

Como puede ver, pudimos recopilar estadísticas solo en el campo de un objeto que
coincide con el tipo de estadísticas recopiladas. Ni la secuencia ni sus elementos son
numéricos.

Mire en el Javadoc de la clase java.util.stream.Collectors para ver qué otra


funcionalidad proporciona antes de intentar crear un
objeto personalizado Collector.

Completando transmisiones
produciendo colecciones

pág. 230
Aprenderá y practicará cómo usar la operación collect() de terminal para
reempaquetar elementos de flujo en una estructura de colección de destino.

Prepararse
Hay dos versiones sobrecargadas de la operación collect() del terminal que nos
permiten crear una colección de elementos de transmisión:

• R collect(Supplier<R> supplier, BiConsumer<R,T> accumulator,


BiConsumer<R,R> combiner): Produce el resultado R utilizando las funciones
pasadas aplicadas a los elementos de flujo del tipo T . El proveedor y el acumulador
provistos trabajan juntos de la siguiente manera:

Resultado R = supplier.get ();


para (elemento T: esta secuencia) {
acumulator.accept (resultado, elemento);
}
resultado devuelto;

El combinador proporcionado se usa solo para el procesamiento de una corriente


paralela. Combina los resultados de las subtransmisiones procesadas en paralelo.

• R collect(Collector<T, A, R> collector): Produce el resultado


R utilizando el objeto Collector pasado aplicado a los elementos de flujo
del tipo T . El tipo A es un tipo de acumulación intermedia de Collector. El
objeto Collector se puede construir utilizando el método de fábrica
Collector.of() , pero no vamos a discutirlo en esta receta porque hay muchos
métodos de fábrica disponibles en la clase java.util.stream.Collectors que
cubren la mayoría de las necesidades. Además, después de que aprenda a usar
la clase Collectors, también podrá usar el método Collector.of().

En esta receta, vamos a demostrar cómo usar los siguientes métodos de


la clase Collectors :

• Collector<T, ?, List<T>> toList(): Crea un objeto Collector que


recopila los elementos de secuencia del tipo T en un objeto List<T>
• Collector<T, ?, Set<T>> toSet(): C hace reaccionar
un objeto Collector que recoge los elementos de flujo del tipo T en
un objeto Set<T>
• Collector<T, ?, C> toCollection(Supplier<C> collectionFactory):
C hace reaccionar un objeto Collector que recoge los elementos de flujo
del tipo T en uno Collection del tipo C producido por el proveedor
collectionFactor

pág. 231
• Collector<T, ?, List<T>> toUnmodifiableList(): C crea un objeto
Collector que recopila los elementos de flujo del tipo T en
un objeto List<T>inmutable Collector<T, ?, Set<T>>
toUnmodifiableSet(): C crea un objeto Collector que recopila los elementos
de flujo del tipo T en un objeto inmutable Set<T>

Para nuestras demostraciones, vamos a utilizar la siguiente clase Person :

class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return getAge() == person.getAge() &&
Objects.equals(getName(), person.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + this.age + "}";
}
}

Cómo hacerlo...
Le guiaremos a través de la secuencia de pasos prácticos que demuestran cómo usar
los métodos y clases anteriores:

1. Escriba un ejemplo del uso de la R collect(Supplier<R> supplier,


BiConsumer<R,T> accumulator, BiConsumer<R,R>
combiner) operación de la interfaz Stream<T> que produce el objeto
List<T>:

List<Person> list =
Stream.of(new Person(30, "John"), new Person(20, "Jill"))
.collect(ArrayList::new,
List::add, //same as: (a,p)-> a.add(p),
List::addAll //same as: (r, r1)-> r.addAll(r1)
);

pág. 232
System.out.println(list);
//prints: [Person{name:John,age:30}, Person{name:Jill,age:20}]

En el ejemplo anterior, los comentarios al acumulador y al combinador demuestran


cómo estas funciones pueden presentarse como expresiones lambda en lugar de solo
referencias a métodos.

El primer parámetro, Supplier<R> devuelve el contenedor para el resultado. En


nuestro caso, lo hemos definido como un constructor de la clase
ArrayList<Person> porque implementa la interfaz List<Person>, el tipo de
objeto que nos gustaría construir.

El acumulador toma el resultado actual, a (que será del tipo List<Person> en


nuestro caso) y le agrega el siguiente elemento de flujo p (el Personobjeto en nuestro
caso). El resultado del ejemplo se muestra como la última línea de comentario.

El combinador combina los resultados de las subtransmisiones procesadas en


paralelo. Toma el primer resultado r (de cualquier subtransmisión que haya
terminado de procesar primero) y le agrega otro resultado r1, y así
sucesivamente. Esto significa que el combinador se usa solo para procesamiento
paralelo. Para demostrar esto, modifiquemos el código anterior de la siguiente manera:

List<Person> list =
Stream.of(new Person(30, "John"), new Person(20, "Jill"))
.collect(ArrayList::new,
ArrayList::add,
(r, r1)-> {
System.out.println("Combining...");
r.addAll(r1);
}
);
System.out.println(list1);
//prints: [Person{name:John,age:30}, Person{name:Jill,age:20}]

Si ejecuta el ejemplo anterior, no verá


la línea Combining... impresa porque combiner no se utiliza para el
procesamiento secuencial de secuencias.

Ahora, convierta el flujo en uno paralelo:

List<Person> list =
Stream.of(new Person(30, "John"), new Person(20, "Jill"))
.parallel()
.collect(ArrayList::new,
ArrayList::add,
(r, r1)-> {
System.out.println("Combining...");
r.addAll(r1);
}
);

pág. 233
System.out.println(list1);
//prints: [Person{name:John,age:30}, Person{name:Jill,age:20}]

Si ejecuta el código anterior, se mostrará la línea Combining...

Nada le impide modificar las funciones proporcionadas de la manera que lo necesite,


siempre que los tipos de entrada y retorno de cada función sigan siendo los mismos.

El objeto Set<Person> se puede crear de la misma manera:

Set<Person> set =
Stream.of(new Person(30, "John"), new Person(20, "Jill"))
.collect(HashSet::new,
Set::add, //same as: (a,p)-> a.add(p),
Set::addAll //same as: (r, r1)-> r.addAll(r1)
);
System.out.println(set);
//prints: [Person{name:John,age:30}, Person{name:Jill,age:20}]

El objeto creado List o Set se puede modificar en cualquier momento:

list.add(new Person(30, "Bob"));


System.out.println(list); //prints: [Person{name:John,age:30},
// Person{name:Jill,age:20},
// Person{name:Bob,age:30}]
list.set(1, new Person(15, "Bob"));
System.out.println(list); //prints: [Person{name:John,age:30},
// Person{name:Bob,age:15},
// Person{name:Bob,age:30}]
set.add(new Person(30, "Bob"));
System.out.println(set); //prints: [Person{name:John,age:30},
// Person{name:Jill,age:20},
// Person{name:Bob,age:30}]

Lo hemos mencionado para contrastar este comportamiento con el comportamiento


de colecciones inmutables, que discutiremos en breve.

2. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
los métodos y : Stream<T>Collector<T, ?, List<T>>
Collectors.toList()Collector<T, ?, Set<T>> Collectors.toSet()

List<Person> list = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toList());
System.out.println(list); //prints: [Person{name:John,age:30},
// Person{name:Jill,age:20}]

Set<Person> set1 = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toSet());
System.out.println(set1); //prints: [Person{name:John,age:30},

pág. 234
Person{name:Jill,age:20}]

Set<Person> set2 = Stream.of(new Person(30, "John"),


new Person(20, "Jill"),
new Person(30, "John"))
.collect(Collectors.toSet());
System.out.println(set2); //prints: [Person{name:John,age:30},
Person{name:Jill,age:20}]
set2.add(new Person(30, "Bob"));
System.out.println(set2); //prints: [Person{name:John,age:30},
Person{name:Jill,age:20},
Person{name:Bob,age:30}]

Como se esperaba, Setno permite elementos duplicados definidos por


la implementación del método equals(). En el caso de la clase Person , el método
equals() compara la edad y el nombre, por lo que una diferencia en cualquiera de
estas propiedades hace que dos objetos Person no sean iguales.

3. Escriba un ejemplo del uso de la operación R collect(Collector<T, A, R>


collector) de la interfaz Stream<T> con el recopilador creado por
el Collector<T, ?, C> Collectors.toCollection(Supplier<C>
collectionFactory) método. La ventaja de este recopilador es que recopila
elementos de flujo no solo en List o Set, sino en cualquier objeto que implemente una
interfaz Collection . El proveedor T produce el objeto de destino que recopila los
elementos de flujo del tipo : collectionFactor

LinkedList<Person> list = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toCollection(LinkedList::new));
System.out.println(list); //prints: [Person{name:John,age:30},
// Person{name:Jill,age:20}]

LinkedHashSet<Person> set = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toCollection(LinkedHashSet::new));
System.out.println(set); //prints: [Person{name:John,age:30},
Person{name:Jill,age:20}]

4. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
los métodos : Stream<T>Collector<T, ?, List<T>>
Collectors.toUnmodifiableList()Collector<T, ?, Set<T>>
Collectors.toUnmodifiableSet()

List<Person> list = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toUnmodifiableList());
System.out.println(list); //prints: [Person{name:John,age:30},
// Person{name:Jill,age:20}]

pág. 235
list.add(new Person(30, "Bob")); //UnsupportedOperationException
list.set(1, new Person(15, "Bob")); //UnsupportedOperationException
list.remove(new Person(30, "John")); //UnsupportedOperationException

Set<Person> set = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toUnmodifiableSet());
System.out.println(set); //prints: [Person{name:John,age:30},
// Person{name:Jill,age:20}]

set.add(new Person(30, "Bob")); //UnsupportedOperationException

Como puede ver en los comentarios en el código anterior, los objetos creados usando
colectores generados por los métodos Collector<T, ?, List<T>>
Collectors.toUnmodifiableList() y Collector<T, ?, Set<T>>
Collectors.toUnmodifiableSet()crean objetos inmutables. Dichos objetos
son muy útiles cuando se usan en expresiones lambda porque de esta manera se
garantiza que no se pueden modificar, por lo que la misma expresión, incluso si se pasa
y se ejecuta en diferentes contextos, producirá el resultado que depende solo de sus
parámetros de entrada y no tendrá inesperados efectos secundarios causados por la
modificación de los objetos List o Set que utiliza.

Por ejemplo:

Set<Person> set = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toUnmodifiableSet());

Predicate<Person> filter = p -> set.contains(p);

El filtro que hemos creado en el ejemplo anterior se puede usar en cualquier lugar para
seleccionar objetos Person que pertenecen al conjunto proporcionado.

Completando transmisiones
produciendo mapas
Aprenderá y practicará cómo usar la operación de terminal para reempaquetar
elementos de flujo para apuntar a la estructura. Al hablar sobre los coleccionistas, no
incluiremos a los coleccionistas que usan la agrupación porque se presentarán en la
próxima receta. collect()Map.

Prepararse

pág. 236
Como mencionamos en la receta anterior, hay dos versiones sobrecargadas de
la operación del terminal, que nos permiten crear una colección de elementos de
transmisión: collect()

• R collect(Supplier<R> supplier, BiConsumer<R,T>


accumulator, BiConsumer<R,R> combiner): Produce el resultado
utilizando las funciones pasadas aplicadas a los elementos de flujo del tipo R T
• R collect(Collector<T, A, R> collector): Produce el resultado
R utilizando el objeto Collector pasado aplicado a los elementos de flujo del
tipo T .

Estas operaciones también se pueden usar para crear un objeto Map, y en esta receta,
vamos a demostrar cómo hacerlo.

En apoyo de la segunda de las versiones anteriores de la operación, la clase


proporciona cuatro grupos de métodos de fábrica que crean el objeto. El primer grupo
incluye los métodos de fábrica muy similares a los que crean el objeto para recopilar
elementos de flujo o discutidos y demostrados en la receta
anterior: collect() CollectorsCollectorCollectorListSet

• Collector<T,?,Map<K,U>> toMap(Function<T,K> keyMapper,


Function<T,U> valueMapper): Crea un objeto Collector que recopila
los elementos de secuencia del tipo T en un objeto Map<K,U> utilizando las
funciones proporcionadas (asignadores) que producen una clave y un valor de un
elemento de secuencia como parámetro de entrada.
• Collector<T,?,Map<K,U>> toMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U>
mergeFunction): C hace reaccionar un objeto Collector que recopila los
elementos de flujo del tipo T en un objeto Map<K,U> utilizando las funciones
proporcionadas (mapeadores) que producen una clave y un valor de un elemento de
flujo como parámetro de entrada. Lo proporcionado mergeFunction se usa solo
para el procesamiento de flujo paralelo; fusiona los resultados de las
subtransmisiones en el único resultado final: el objeto Map<K,U>.
• Collector<T,?,M> toMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U>
mergeFunction, Supplier<M> mapFactory): C hace reaccionar
un objeto Collector que recopila los elementos de flujo del T tipo en
un Map<K,U> objeto utilizando las funciones proporcionadas (mapeadores) que
producen una clave y un valor de un elemento de flujo como parámetro de
entrada. Lo proporcionado mergeFunctionse usa solo para el procesamiento de
flujo paralelo; fusiona los resultados de las subtransmisiones en el único resultado
final: el objeto Map<K,U> . El proveedor proporcionado mapFactory crea
un objeto Map<K,U> vacío en el que se insertarán los resultados.
• Collector<T,?,Map<K,U>> toUnmodifiableMap(Function<T,K>
keyMapper, Function<T,U> valueMapper): Crea un objeto

pág. 237
Collector que recopila los elementos de secuencia del tipo T en
un objeto inmutable Map<K,U> utilizando las funciones proporcionadas
(asignadores) que producen una clave y un valor de un elemento de secuencia como
parámetro de entrada.
• Collector<T,?,Map<K,U>> toUnmodifiableMap(Function<T,K>
keyMapper, Function<T,U> valueMapper, BinaryOperator<U>
mergeFunction): C hace reaccionar un objeto Collector que recolecta los
elementos de flujo del T tipo en un objeto inmutable utilizando las funciones
proporcionadas (mapeadores) que producen una clave y un valor de un elemento de
flujo como parámetro de entrada. Lo proporcionado se usa solo para el
procesamiento de flujo paralelo; fusiona los resultados de las subtransmisiones en el
único resultado final:
un objeto inmutable . Map<K,U>mergeFunctionMap<K,U>

El segundo grupo incluye tres métodos de fábrica similares a los tres métodos
toMap() que acabamos de enumerar. La única diferencia es que los recopiladores
creados por los métodos recopilan elementos de secuencia en
un objeto : toConcurrentMap()ConcurrentMap

• Collector<T,?,ConcurrentMap<K,U>>
toConcurrentMap(Function<T,K> keyMapper, Function<T,U>
valueMapper): C hace reaccionar un objeto Collector que recopila los
elementos de flujo del tipo T en un objeto ConcurrentMap<K,U> utilizando
las funciones proporcionadas (mapeadores) que producen una clave y un valor de un
elemento de flujo como parámetro de entrada.
• Collector<T,?,ConcurrentMap<K,U>>
toConcurrentMap(Function<T,K> keyMapper, Function<T,U>
valueMapper, BinaryOperator<U> mergeFunction): C
hace reaccionar un objeto Collector que recopila los elementos de flujo
del tipo T en un objeto ConcurrentMap<K,U> utilizando las funciones
proporcionadas (mapeadores) que producen una clave y un valor de un elemento de
flujo como parámetro de entrada. La función mergeFunction se usa solo para el
procesamiento de flujo paralelo; fusiona los resultados de las subtransmisiones en el
único resultado final: el objeto ConcurrentMap<K,U> .
• Collector<T,?,M> toConcurrentMap(Function<T,K>
keyMapper, Function<T,U> valueMapper, BinaryOperator<U>
mergeFunction, Supplier<M> mapFactory): C hace reaccionar
un objeto Collector que recopila los elementos de flujo del T tipo en un objeto
ConcurrentMap<K,U> utilizando las funciones proporcionadas (mapeadores)
que producen una clave y un valor de un elemento de flujo como parámetro de
entrada. La función mergeFunction se usa solo para el procesamiento de flujo
paralelo; fusiona los resultados de las subtransmisiones en el único resultado final:
el objeto
ConcurrentMap<K,U> . El mapFactory proveedor proporcionado crea
un objeto ConcurrentMap<K,U> vacío en el que se insertarán los resultados.

pág. 238
La necesidad de este segundo grupo de métodos de fábrica surge del hecho de
que, para un flujo paralelo, los resultados de fusión de diferentes flujos secundarios
son una operación costosa. Es especialmente pesado cuando los resultados tienen que
fusionarse en el resultado Map en el orden encontrado; eso es lo que hacen los
recolectores creados por los métodos de fábrica toMap(). Estos recolectores crean
múltiples resultados intermedios y luego los fusionan llamando al proveedor y al
combinador del recolector varias veces.

Cuando el orden de la fusión de resultados no es importante, los recolectores


creados por los métodos toConcurrentMap() pueden usarse como menos
pesados porque llaman al proveedor solo una vez, insertan los elementos en
el contenedor resultante compartido y nunca llaman al combinador.

Entonces, la diferencia entre los colectores toMap()y


se toConcurrentMap()manifiesta solo durante el procesamiento de flujo
paralelo. Es por eso que a menudo se recomienda usar los recolectores toMap()para
el procesamiento de flujo en serie y los recolectores toConcurrentMap()para el
procesamiento de flujo en paralelo (si el orden de recolección de los elementos de
flujo no es importante).

El tercer grupo incluye tres métodos de fábrica groupingBy(), que discutiremos en


la próxima receta.

El cuarto grupo incluye tres métodos de fábrica groupingByConcurrent(), que


también discutiremos en la próxima receta.

Para nuestras demostraciones, vamos a usar la misma clase Person que usamos
para crear colecciones en la receta anterior:

class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return getAge() == person.getAge() &&
Objects.equals(getName(), person.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}

pág. 239
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + this.age + "}";
}
}

Cómo hacerlo...
Le guiaremos a través de la secuencia de pasos prácticos que demuestran cómo usar
los métodos y clases anteriores:

1. Escriba un ejemplo del uso de la operación R collect(Supplier<R>


supplier, BiConsumer<R,T> accumulator, BiConsumer<R,R>
combiner) de la interfaz que produce el objeto. Cree con el nombre de una
persona como clave: Stream<T>MapMap<String, Person>

Map<String, Person> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(HashMap::new,
(m,p) -> m.put(p.getName(), p),
Map::putAll
);
System.out.println(map); //prints: {John=Person{name:John,age:30},
// Jill=Person{name:Jill,age:20}}

O, para evitar datos redundantes en el resultado Map, podemos usar el campo de edad
como el Mapvalor:

Map<String, Integer> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(HashMap::new,
(m,p) -> m.put(p.getName(), p.getAge()),
Map::putAll
);
System.out.println(map); //prints: {John=30, Jill=20}

El combinador se llama solo para una corriente paralela, ya que se usa para combinar
los resultados de diferentes procesamientos de subtransmisión. Para probarlo, hemos
reemplazado la referencia del método Map::putAllcon el bloque de código que
imprime el mensaje Combining...:

Map<String, Integer> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
//.parallel() //conversion to a parallel stream
.collect(HashMap::new,
(m,p) -> m.put(p.getName(), p.getAge()),
(m,m1) -> {
System.out.println("Combining...");
m.putAll(m1);
}

pág. 240
);
System.out.println(map); //prints: {John=30, Jill=20}

El mensaje Combining... se mostrará solo si la conversión a una secuencia


paralela no está comentada.

Si agregamos otro objeto Person con el mismo nombre, uno de ellos se


sobrescribirá en el resultado Map:

Map<String, Integer> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"),
new Person(15, "John"))
.collect(HashMap::new,
(m,p) -> m.put(p.getName(), p.getAge()),
Map::putAll
);
System.out.println(map); //prints: {John=15, Jill=20}

Si tal comportamiento no es deseable y necesitamos ver todos los valores de todas las
claves duplicadas, podemos cambiar el resultado Mappara tener un objeto List
como valor, para que en esta lista podamos recopilar todos los valores que tienen la
misma clave:

BiConsumer<Map<String, List<Integer>>, Person> consumer =


(m,p) -> {
List<Integer> list = m.get(p.getName());
if(list == null) {
list = new ArrayList<>();
m.put(p.getName(), list);
}
list.add(p.getAge());
};
Map<String, List<Integer>> map =
Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(HashMap::new, consumer, Map::putAll);
System.out.println(map);
//prints: {John=[30, 15], Jill=[20]}

Como puede ver, no alineamos la función BiConsumer en la operación


collect() como parámetro porque ahora es un código multilínea y es más fácil de
leer de esta manera.

Otra forma de recopilar varios valores para la misma clave, en este caso, sería
crear Map con un valor String de la siguiente manera:

BiConsumer<Map<String, String>, Person> consumer2 = (m,p) -> {


if(m.keySet().contains(p.getName())) {
m.put(p.getName(), m.get(p.getName()) + "," + p.getAge());
} else {
m.put(p.getName(), String.valueOf(p.getAge()));
}

pág. 241
};
Map<String, String> map = Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(HashMap::new, consumer, Map::putAll);
System.out.println(map); //prints: {John=30,15, Jill=20}

2. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, Map<K,U>>
Collectors.toMap(Function<T,K> keyMapper,
Function<T,U> valueMapper)

Map<String, Integer> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))
.collect(Collectors.toMap(Person::getName, Person::getAge));
System.out.println(map); //prints: {John=30, Jill=20}

La solución anterior funciona bien siempre que no se encuentre una clave duplicada,
como en el siguiente caso:

Map<String, Integer> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toMap(Person::getName, Person::getAge));

El código anterior lanza IllegalStateException con el Duplicate key


John mensaje (intento de fusión valores de 30 y 15) y no hay manera de que
añadimos un cheque por una clave duplicada, como lo hemos hecho antes. Entonces, si
existe la posibilidad de una clave duplicada, uno tiene que usar la versión
sobrecargada del método toMap().

3. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, Map<K,U>>
Collectors.toMap(Function<T,K> keyMapper, Function<T,U>
valueMapper, BinaryOperator<U> mergeFunction)

Function<Person, List<Integer>> valueMapper = p -> {


List<Integer> list = new ArrayList<>();
list.add(p.getAge());
return list;
};
BinaryOperator<List<Integer>> mergeFunction = (l1, l2) -> {
l1.addAll(l2);
return l1;
};
Map<String, List<Integer>> map =
Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toMap(Person::getName,

pág. 242
valueMapper, mergeFunction));
System.out.println(map);
//prints: {John=[30, 15], Jill=[20]}

Ese es el propósito de combinar valores mergeFunction- para una clave


duplicada. En lugar de List<Integer>, también podemos recopilar los valores de
una clave duplicada en un objeto: String

Function<Person, String> valueMapper =


p -> String.valueOf(p.getAge());
BinaryOperator<String> mergeFunction =
(s1, s2) -> s1 + "," + s2;
Map<String, String> map =
Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toMap(Person::getName,
valueMapper, mergeFunction));
System.out.println(map3);//prints: {John=30,15, Jill=20}

4. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, M>
Collectors.toMap(Function<T,K> keyMapper, Function<T,U>
valueMapper, BinaryOperator<U> mergeFunction, Supplier<M>
mapFactory)

Function<Person, String> valueMapper =


p -> String.valueOf(p.getAge());
BinaryOperator<String> mergeFunction =
(s1, s2) -> s1 + "," + s2;
LinkedHashMap<String, String> map =
Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toMap(Person::getName,
valueMapper, mergeFunction, LinkedHashMap::new));
System.out.println(map3); //prints: {John=30,15, Jill=20}

Como puede ver, esta versión del método toMap() nos permite especificar
la Mapimplementación de interfaz deseada (la LinkedHashMap clase, en este caso)
en lugar de utilizar la predeterminada.

5. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, Map<K,U>>
Collectors.toUnmodifiableMap(Function<T,K> keyMapper,
Function<T,U> valueMapper)

Map<String, Integer> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"))

pág. 243
.collect(Collectors.toUnmodifiableMap(Person::getName,
Person::getAge));
System.out.println(map); //prints: {John=30, Jill=20}

map.put("N", new Person(42, "N")); //UnsupportedOperationExc


map.remove("John"); //UnsupportedOperationExc

Map<String, Integer> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toUnmodifiableMap(Person::getName,
Person::getAge)); //IllegalStateExc: Duplicate key John

Como puede ver, el colector creado por el método toUnmpdifiableMap()se


comporta igual que el colector creado por el método Collector<T, ?,
Map<K,U>> Collectors.toMap(Function<T,K> keyMapper,
Function<T,U> valueMapper) , excepto que produce un Mapobjeto inmutable .

6. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, Map<K,U>>
Collectors.toUnmodifiableMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U> mergeFunction)

Function<Person, List<Integer>> valueMapper = p -> {


List<Integer> list = new ArrayList<>();
list.add(p.getAge());
return list;
};
BinaryOperator<List<Integer>> mergeFunction = (l1, l2) -> {
l1.addAll(l2);
return l1;
};
Map<String, List<Integer>> map =
Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toUnmodifiableMap(Person::getName,
valueMapper, mergeFunction));
System.out.println(map); //prints: {John=[30, 15], Jill=[20]}

El colector creado por el método toUnmpdifiableMap() se comporta igual que el


colector creado por el método Collector<T, ?, Map<K,U>>
Collectors.toMap(Function<T,K> keyMapper, Function<T,U>
valueMapper, BinaryOperator<U> mergeFunction) , excepto que produce
un objeto inmutable Map. Su propósito es manejar el caso de claves duplicadas. La
siguiente es otra forma de combinar los valores de claves duplicadas:

Function<Person, String> valueMapper =


p -> String.valueOf(p.getAge());
BinaryOperator<String> mergeFunction =
(s1, s2) -> s1 + "," + s2;
Map<String, String> map = Stream.of(new Person(30, "John"),

pág. 244
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toUnmodifiableMap(Person::getName,
valueMapper, mergeFunction));
System.out.println(map); //prints: {John=30,15, Jill=20}

7. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ? ,ConcurrentMap<K,U>>
Collectors.toConcurrentMap(Function<T,K> keyMapper,
Function<T,U> valueMapper)

ConcurrentMap<String, Integer> map =


Stream.of(new Person(30, "John"),
new Person(20, "Jill"))
.collect(Collectors.toConcurrentMap(Person::getName,
Person::getAge));
System.out.println(map); /prints: {John=30, Jill=20}

map.put("N", new Person(42, "N")); //UnsupportedOperationExc


map.remove("John"); //UnsupportedOperationExc

ConcurrentMap<String, Integer> map =


Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toConcurrentMap(Person::getName,
Person::getAge)); //IllegalStateExc: Duplicate key John

Como puede ver, el recopilador creado por el método se comporta igual que el
recopilador creado por los métodos y , excepto que produce un objeto mutable y,
cuando la secuencia es paralela, comparte entre subtransmisiones el
resultado .toConcurrentMap()Collector<T, ?, Map<K,U>>
Collectors.toMap(Function<T,K> keyMapper,
Function<T,U> valueMapper)Collector<T, ?, Map<K,U>>
Collectors.toUnmodifiableMap(Function<T,K> keyMapper,
Function<T,U> valueMapper)MapMap

8. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, ConcurrentMap<K,U>>
Collectors.toConcurrentMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U> mergeFunction)

Function<Person, List<Integer>> valueMapper = p -> {


List<Integer> list = new ArrayList<>();
list.add(p.getAge());
return list;
};
BinaryOperator<List<Integer>> mergeFunction = (l1, l2) -> {
l1.addAll(l2);
return l1;

pág. 245
};
ConcurrentMap<String, List<Integer>> map =
Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toConcurrentMap(Person::getName,
valueMapper, mergeFunction));
System.out.println(map);
//prints: {John=[30, 15], Jill=[20]}

Como puede ver, el recopilador creado por el método se comporta igual que el
recopilador creado por los métodos y , excepto que produce un objeto mutable y,
cuando la secuencia es paralela, comparte el resultado entre subtransmisiones . La
siguiente es otra forma de combinar los valores de claves
duplicadas:toConcurrentMap()Collector<T, ?, Map<K,U>>
Collectors.toMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U>
mergeFunction)Collector<T, ?, Map<K,U>>
Collectors.toUnmodifiableMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U>
mergeFunction)MapMap

Function<Person, String> valueMapper =


p -> String.valueOf(p.getAge());
BinaryOperator<String> mergeFunction =
(s1, s2) -> s1 + "," + s2;
ConcurrentMap<String, String> map =
Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toConcurrentMap(Person::getName,
valueMapper, mergeFunction));
System.out.println(map); //prints: {John=30,15, Jill=20}

9. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, M>
Collectors.toConcurrentMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U> mergeFunction,
Supplier<M> mapFactory)

ConcurrentSkipListMap<String, String> map =


Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(15, "John"))
.collect(Collectors.toConcurrentMap(Person::getName,
valueMapper, mergeFunction, ConcurrentSkipListMap::new));
System.out.println(map4); //prints: {Jill=20, John=30,15}

Como puede ver, esta versión del método nos permite especificar
la implementación de interfaz deseada (la clase, en este caso) en lugar de utilizar la
predeterminada.toConcurrentMap()MapConcurrentSkipListMap

pág. 246
El recopilador creado por el método se comporta igual que el recopilador creado por
el método, pero cuando la secuencia es paralela, comparte entre las secuencias
secundarias el resultado .toConcurrentMap()Collector<T, ?, Map<K,U>>
Collectors.toMap(Function<T,K> keyMapper,
Function<T,U> valueMapper, BinaryOperator<U> mergeFunction,
Supplier<M> mapFactory) Map

Completar transmisiones
produciendo mapas con
recopiladores de agrupación
En esta receta, aprenderá y practicará cómo usar la operación collect() de terminal
para agrupar elementos por una propiedad y almacenar el resultado en
una Mapinstancia usando un recopilador.

Prepararse
Hay dos conjuntos de recopiladores que utilizan la agrupación, similar al grupo por
la funcionalidad de las declaraciones SQL, para presentar los datos de la secuencia
como un Mapobjeto. El primer conjunto incluye tres métodos groupingBy() de
fábrica sobrecargados :

• Collector<T, ?, Map<K,List<T>>> groupingBy(Function<T,K>


classifier): Crea un Collector objeto que recopila los elementos de flujo
del tipo T en un objeto Map<K,List<T>> utilizando la función classifier
proporcionada para asignar el elemento actual a la clave en el mapa resultante.
• Collector<T,?,Map<K,D>> groupingBy(Function<T,K>
classifier, Collector<T,A,D> downstream): Crea un objeto
Collector que recopila los elementos de flujo del tipo T en
un objeto Map<K,D> utilizando la función proporcionada classifier para
asignar el elemento actual a la clave en el mapa intermedio Map<K,List<T>>. A
continuación, utiliza el colector downstream para convertir los valores del mapa
intermedio en los valores del mapa resultante, Map<K,D .
• Collector<T, ?, M> groupingBy(Function<T,K> classifier,
Supplier<M> mapFactory, Collector<T,A,D> downstream): C
hace reaccionar un objeto Collector que recoge los elementos de flujo del tipo
T en el objeto M de mapa utilizando la función classifier proporcionada para
asignar el elemento actual a la clave en
el mapa Map<K,List<T>> intermedio. Luego utiliza el recopilador

pág. 247
downstream para convertir los valores del mapa intermedio en los valores del
mapa resultante del tipo proporcionado por el proveedor mapFactory .

El segundo conjunto de recopiladores incluye tres métodos de


fábrica groupingByConcurrent() , que se crean para el manejo de concurrencia
durante el procesamiento de flujo paralelo. Estos recopiladores toman los mismos
argumentos que las versiones sobrecargadas correspondientes de los recopiladores
groupingBy() enumerados anteriormente. La única diferencia es que el tipo de
retorno de los recopiladores groupingByConcurrent()son las instancias de
la ConcurrentHashMapclase o su subclase:

• Collector<T, ?, ConcurrentMap<K,List<T>>>
groupingByConcurrent(Function<T,K> classifier) : C
hace reaccionar un objeto Collector que recopila los elementos de flujo
del tipo T en un objeto ConcurrentMap<K,List<T>> utilizando
la función proporcionada classifier para asignar el elemento actual a la clave
en el mapa resultante.
• Collector<T, ?, ConcurrentMap<K,D>>
groupingByConcurrent(Function<T,K> classifier,
Collector<T,A,D> downstream): Crea un objeto Collector que
recopila los elementos de flujo del tipo T en
un objeto ConcurrentMap<K,D> utilizando
la función classifier proporcionada para asignar el elemento actual a la
clave en el mapa intermedio ConcurrentMap<K,List<T>> . A continuación,
utiliza el colector downstream para convertir los valores del mapa intermedio en
los valores del mapa resultante, ConcurrentMap<K,D>.
• Collector<T, ?, M> groupingByConcurrent(Function<T,K>
classifier, Supplier<M> mapFactory, Collector<T,A,D>
downstream): C hace reaccionar un objeto Collector que recoge los
elementos de flujo del tipo T en el objeto M de mapa utilizando
la función classifier proporcionada para asignar el elemento actual a la
clave en el mapa intermedio ConcurrentMap<K,List<T>> . Luego utiliza
el recopilador downstream para convertir los valores del mapa intermedio en los
valores del mapa resultante del tipo proporcionado por
el mapFactory proveedor.

Para nuestras demostraciones, vamos a usar la misma clase que usamos para crear
mapas en la receta anterior: Person

class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;

pág. 248
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return getAge() == person.getAge() &&
Objects.equals(getName(), person.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + this.age + "}";
}
}

También usaremos la clase Person2 :

class Person2 {
private int age;
private String name, city;
public Person2(int age, String name, String city) {
this.age = age;
this.name = name;
this.city = city;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
public String getCity() { return this.city; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person2 person = (Person2) o;
return getAge() == person.getAge() &&
Objects.equals(getName(), person.getName()) &&
Objects.equals(getCity(), person.getCity());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge(), getCity());
}
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + this.age +
",city:" + this.city + "}";
}
}

La clase Person2 es diferente de la clase Person ya que tiene un campo


adicional : ciudad. Se utilizará para demostrar el poder de la funcionalidad de
agrupación. Y la variación de clase Person2 , la clase Person3 , se utilizará para
pág. 249
demostrar cómo crear el objeto EnumMap . La clase Person3 utiliza enum City
como tipo de valor para su propiedad city :

enum City{
Chicago, Denver, Seattle
}

class Person3 {
private int age;
private String name;
private City city;
public Person3(int age, String name, City city) {
this.age = age;
this.name = name;
this.city = city;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
public City getCity() { return this.city; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person3 person = (Person3) o;
return getAge() == person.getAge() &&
Objects.equals(getName(), person.getName()) &&
Objects.equals(getCity(), person.getCity());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge(), getCity());
}
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + this.age +
",city:" + this.city + "}";
}
}

Para que los ejemplos sean menos detallados, vamos a utilizar los siguientes métodos
para generar flujos de prueba:

Stream<Person> getStreamPerson() {
return Stream.of(new Person(30, "John"),
new Person(20, "Jill"),
new Person(20, "John"));
}
Stream<Person2> getStreamPerson2(){
return Stream.of(new Person2(30, "John", "Denver"),
new Person2(30, "John", "Seattle"),
new Person2(20, "Jill", "Seattle"),
new Person2(20, "Jill", "Chicago"),
new Person2(20, "John", "Denver"),
new Person2(20, "John", "Chicago"));
}
Stream<Person3> getStreamPerson3(){
return Stream.of(new Person3(30, "John", City.Denver),

pág. 250
new Person3(30, "John", City.Seattle),
new Person3(20, "Jill", City.Seattle),
new Person3(20, "Jill", City.Chicago),
new Person3(20, "John", City.Denver),
new Person3(20, "John", City.Chicago));
}

Cómo hacerlo...
Le guiaremos a través de la secuencia de pasos prácticos que demuestran cómo usar
los métodos y clases anteriores:

1. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?,
Map<K,List<T>>> groupingBy(Function<T,K> classifier)

Map<String, List<Person>> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getName));
System.out.println(map);
//prints: {John=[Person{name:John,age:30},
// Person{name:John,age:20}],
// Jill=[Person{name:Jill,age:20}]}

Esta es la versión más simple del objeto Collector. Simplemente defina cuál será la
clave del mapa resultante, y el recopilador agregará todos los elementos de flujo que
tengan el mismo valor clave a la lista de elementos asociados con esa clave en el mapa
resultante.

Aquí hay otro ejemplo:

Map<Integer, List<Person>> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getAge));
System.out.println(map);
//prints: {20=[Person{name:Jill,age:20},
// Person{name:John,age:20}],
// 30=[Person{name:John,age:30}]}

Si los elementos de la secuencia deben agruparse por una combinación de propiedades,


puede crear una clase que pueda contener la combinación necesaria. El objeto de esta
clase servirá como una clave compleja. Por ejemplo, leamos la secuencia de
los elementos Person2 y agrúpelos por edad y nombre. Esto significa que necesita
una clase que pueda llevar dos valores. Por ejemplo, aquí hay una clase de este tipo,
llamada TwoStrings:

class TwoStrings {
private String one, two;
public TwoStrings(String one, String two) {

pág. 251
this.one = one;
this.two = two;
}
public String getOne() { return this.one; }
public String getTwo() { return this.two; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TwoStrings)) return false;
TwoStrings twoStrings = (TwoStrings) o;
return Objects.equals(getOne(), twoStrings.getOne())
&& Objects.equals(getTwo(), twoStrings.getTwo());
}
@Override
public int hashCode() {
return Objects.hash(getOne(), getTwo());
}
@Override
public String toString() {
return "(" + this.one + "," + this.two + ")";
}
}

Tuvimos que implementar los métodos equals()y hashCode()porque un objeto


de la clase TwoStrings se usará como clave y su valor debe ser específico para
cada combinación de los dos valores. Podemos usarlo ahora de la siguiente manera:

Map<TwoStrings, List<Person2>> map = getStreamPerson2()


.collect(Collectors.groupingBy(p ->
new TwoStrings(String.valueOf(p.getAge()),
p.getName())));
System.out.println(map);
//prints:
// {(20,Jill)=[Person{name:Jill,age:20,city:Seattle},
// Person{name:Jill,age:20,city:Chicago}],
// (20,John)=[Person{name:John,age:20,city:Denver},
// Person{name:John,age:20,city:Chicago}],
// (30,John)=[Person{name:John,age:30,city:Denver},
// Person{name:John,age:30,city:Seattle}]}

3. Escriba un ejemplo del uso de las operaciones R collect(Collector<T,


A, R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T,?,Map<K,D>> groupingBy(Function<
T,K> classifier, Collector<T,A,D> downstream)

Map<String, Set<Person>> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getName,
Collectors.toSet()));
System.out.println(map);
//prints: {John=[Person{name:John,age:30},
// Person{name:John,age:20}],
// Jill=[Person{name:Jill,age:20}]}

Como puede ver, los valores List<Person> del mapa producido por
el recopilador Collectors.groupingBy(Person::getName) se cambiaron

pág. 252
posteriormente (aguas abajo) a un conjunto por
el recopilador Collectors.toSet() .

Alternativamente, cada valor List<Person> se puede convertir a solo un recuento


de los elementos de la lista, de la siguiente manera:

Map<String, Long> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getName,
Collectors.counting()));
System.out.println(map); //prints: {John=2, Jill=1}

Para contar cuántos de los mismos objetos Person (los que son iguales según
el método equals() ) están en la secuencia, podemos usar la función de identidad,
que se define como devolver la entrada sin cambios. Por ejemplo:

Stream.of("a","b","c")
.map(s -> Function.identity()
.apply(s))
.forEach(System.out::print); //prints: abc

Usando esta función, podemos contar el número de mismas personas, de la siguiente


manera:

Map<Person, Long> map = Stream.of(new Person(30, "John"),


new Person(20, "Jill"),
new Person(30, "John"))
.collect(Collectors.groupingBy(Function.identity(),
Collectors.counting()));
System.out.println(map); //prints: {Person{name:Jill,age:20}=1,
// Person{name:John,age:30}=2}

También podemos calcular una edad promedio en cada grupo de personas (un grupo
se define como el que tiene el mismo valor clave resultante):

Map<String, Double> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getName,
Collectors.averagingInt(Person::getAge)));
System.out.println(map); //prints: {John=25.0, Jill=20.0}

Para enumerar todos los valores de la edad de las personas con el mismo nombre,
podemos usar el recopilador posterior creado por el método:Collector<T, ?,
R> Collectors.mapping (Function<T,U> mapper, Collector<U,A,R>
downstream)

Map<String, List<Integer>> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getName,
Collectors.mapping(Person::getAge,
Collectors.toList())));
System.out.println(map);
//prints: {John=[30, 20], Jill=[20]}

pág. 253
Otra variación de esta solución es el siguiente ejemplo, donde para cada edad, se crea
una lista de nombres delimitados por comas:

Map<Integer, String> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getAge,
Collectors.mapping(Person::getName,
Collectors.joining(","))));
System.out.println(map);
//prints: {20=Jill, John, 30=John}

Y, finalmente, para demostrar otra técnica, podemos usar


los groupingBy()recolectores anidados para crear un mapa que contenga la edad
como clave y un mapa de los nombres de las personas a sus ciudades como valores:

Map<Integer, Map<String, String>> map = getStreamPerson2()


.collect(Collectors.groupingBy(Person2::getAge,
Collectors.groupingBy(Person2::getName,
Collectors.mapping(Person2::getCity,
Collectors.joining(",")))));
System.out.println(map); //prints:
// {20={John=Denver,Chicago,
// Jill=Seattle,Chicago},
// 30={John=Denver,Seattle}}

Tenga en cuenta que utilizamos la secuencia Person2 en el ejemplo anterior.

4. Escriba un ejemplo del uso de la operación R collect(Collector<T, A,


R> collector) de la interfaz con el recopilador creado por
el método: Stream<T>Collector<T, ?, M> groupingBy(Function<T,K>
classifier, Supplier<M> mapFactory, Collector<T,A,D>
downstream)

LinkedHashMap<String, Long> map = getStreamPerson()


.collect(Collectors.groupingBy(Person::getName,
LinkedHashMap::new,
Collectors.counting()));
System.out.println(map); //prints: {John=2, Jill=1}

El código del ejemplo anterior cuenta cuántas veces se encuentra cada nombre en la
secuencia de los objetos Person y coloca el resultado en el contenedor
( LinkedHashMap en este caso) definido por la función mapFactory (el segundo
parámetro del método groupingBy()).

Los siguientes ejemplos demuestran cómo decir el colector de manejar


en EnumMap basado en enum City como un contenedor del resultado final:

EnumMap<City, List<Person3>> map = getStreamPerson3()


.collect(Collectors.groupingBy(Person3::getCity,
() -> new EnumMap<>(City.class),
Collectors.toList()));
System.out.println(map);

pág. 254
//prints: {Chicago=[Person{name:Jill,age:20,city:Chicago},
// Person{name:John,age:20,city:Chicago}],
// Denver=[Person{name:John,age:30,city:Denver},
// Person{name:John,age:20,city:Denver}],
// Seattle=[Person{name:Jill,age:20,city:Seattle},
// Person{name:John,age:30,city:Seattle}]}

Tenga en cuenta que usamos la secuencia Person3 en los ejemplos anteriores. Para
simplificar el resultado (para evitar mostrar una ciudad dos veces para el mismo
resultado) y agrupar a las personas por edad (para cada ciudad), podemos usar
el recolector groupingBy()anidado nuevamente:

EnumMap<City, Map<Integer, String>> map = getStreamPerson3()


.collect(Collectors.groupingBy(Person3::getCity,
() -> new EnumMap<>(City.class),
Collectors.groupingBy(Person3::getAge,
Collectors.mapping(Person3::getName,
Collectors.joining(",")))));
System.out.println(map);
//prints: {Chicago={20=Jill,John},
// Denver={20=John, 30=John},
// Seattle={20=Jill, 30=John}}

5. Como ejemplos del segundo conjunto de recopiladores, los creados por los métodos
groupingByConcurrent() , todos los fragmentos de código anteriores (excepto los
dos últimos con EnumMap) se pueden usar simplemente
reemplazando groupingBy() con groupingByConcurrent() y el
resultante Map con la clase o su subclase ConcurrentMap . Por ejemplo:

ConcurrentMap<String, List<Person>> map1 =


getStreamPerson().parallel()
.collect(Collectors.groupingByConcurrent(Person::getName));
System.out.println(map1);
//prints: {John=[Person{name:John,age:30},
// Person{name:John,age:20}],
// Jill=[Person{name:Jill,age:20}]}

ConcurrentMap<String, Double> map2 =


getStreamPerson().parallel()
.collect(Collectors.groupingByConcurrent(Person::getName,
Collectors.averagingInt(Person::getAge)));
System.out.println(map2); //prints: {John=25.0, Jill=20.0}

ConcurrentSkipListMap<String, Long> map3 =


getStreamPerson().parallel()
.collect(Collectors.groupingByConcurrent(Person::getName,
ConcurrentSkipListMap::new, Collectors.counting()));
System.out.println(map3); //prints: {Jill=1, John=2}

Como hemos mencionado antes, los recolectores también pueden procesar secuencias
secuenciales, pero están diseñadas para ser utilizadas para procesar datos de
secuencias paralelas, por lo que hemos convertido las secuencias anteriores en

pág. 255
paralelas. El resultado devuelto es del tipo o una subclase del
mismo. groupingByConcurrent() ConcurrentHashMap

Hay más...
La clase Collectors también proporciona dos recopiladores generados por
el método partitioningBy(), que son versiones especializadas de
los recopiladores groupingBy():

• Collector<T, ?, Map<Boolean,List<T>>>
partitioningBy(Predicate<T> predicate): C hace reaccionar
un objeto Collector que recopila los elementos de flujo del tipo T en
un objeto Map<Boolean,List<T>> utilizando
la función proporcionada predicate para asignar el elemento actual a la clave
en el mapa resultante.
• Collector<T, ?, Map<Boolean,D>>
partitioningBy(Predicate<T> predicate, Collector<T,A,D>
downstream) : C hace reaccionar un objeto Collector que recolecta los
elementos de flujo del tipo T en un objeto Map<Boolean,D> usando la
función predicate provista para asignar el elemento actual a la clave en
el mapa intermedio Map<K,List<T>> . A continuación, utiliza
el colector downstream para convertir los valores del mapa intermedio en los
valores del mapa resultante, Map<Boolean,D> .

Veamos algunos ejemplos. Así es como se puede usar el primero de los métodos
anteriores para recopilar los Personelementos de la secuencia en dos grupos : uno
con nombres que contienen la letra i y otro con nombres que no contienen la letra i:

Map<Boolean, List<Person>> map = getStreamPerson()


.collect(Collectors.partitioningBy(p-> p.getName().contains("i")));
System.out.println(map); //prints: {false=[Person{name:John,age:30},
// Person{name:John,age:20}],
// true=[Person{name:Jill,age:20}]}

Para demostrar el uso del segundo método, podemos convertir


cada valor List<Person> del mapa creado en el ejemplo anterior al tamaño de la
lista:

Map<Boolean, Long> map = getStreamPerson()


.collect(Collectors.partitioningBy(p-> p.getName().contains("i"),
Collectors.counting()));
System.out.println(map); //prints: {false=2, true=1}

pág. 256
Se puede lograr el mismo resultado utilizando colectores creados por los métodos
groupingBy() :

Map<Boolean, List<Person>> map1 = getStreamPerson()


.collect(Collectors.groupingBy(p-> p.getName().contains("i")));
System.out.println(map); //prints: {false=[Person{name:John,age:30},
// Person{name:John,age:20}],
// true=[Person{name:Jill,age:20}]}

Map<Boolean, Long> map2 = getStreamPerson()


.collect(Collectors.groupingBy(p-> p.getName().contains("i"),
Collectors.counting()));
System.out.println(map2); //prints: {false=2, true=1}

Los recopiladores creados por los métodos partitioningBy()se consideran una


especialización de los recopiladores creados por los métodos groupingBy(), y se
espera que nos permitan escribir menos código cuando los elementos de secuencia se
dividen en dos grupos y se almacenan en un mapa con claves booleanas. Pero, como
puede ver en el código anterior, ese no es siempre el caso. Los recopiladores en
nuestros ejemplos requieren que escribamos exactamente la misma cantidad de código
que los recopiladores. partitioningBy()groupingBy()

Crear canalización de operación


de flujo
En esta receta, aprenderá a construir una tubería a partir de las operaciones Stream.

Prepararse
En el capitulo anterior, Capítulo 4 , Funcionando , al crear una API compatible con
lambda, terminamos con el siguiente método de API:

public interface Traffic {


void speedAfterStart(double timeSec,
int trafficUnitsNumber, SpeedModel speedModel,
BiPredicate<TrafficUnit, Double> limitTraffic,
BiConsumer<TrafficUnit, Double> printResult);
}

El número especificado de instancias TrafficUnit se produjo dentro del método


speedAfterStart(). Estaban limitados por la función limitTrafficAndSpeed
y se procesaron de acuerdo con la función speedModel dentro del método
speedAfterStart(). Los resultados fueron formateados por la
función printResults.

pág. 257
Es un diseño muy flexible que permite una amplia gama de experimentación mediante
la modificación de las funciones que se pasan a la API. Sin embargo, en realidad,
especialmente durante las primeras etapas del análisis de datos, la creación de una API
requiere más escritura de código. Paga solo a largo plazo y solo si la flexibilidad de
diseño nos permite acomodar nuevos requisitos con cero o muy pocos cambios de
código.

La situación cambia radicalmente durante la fase de investigación. Cuando se


desarrollan nuevos algoritmos o cuando la necesidad de procesar una gran cantidad de
datos presenta sus propios desafíos, la transparencia en todas las capas del sistema
desarrollado se convierte en un requisito fundamental. Sin ella, muchos de los éxitos
actuales en el análisis de big data serían imposibles.

Los flujos y las tuberías abordan el problema de la transparencia y minimizan la


sobrecarga de escribir código de infraestructura.

Cómo hacerlo...
Recordemos cómo un usuario llamó a la API compatible con lambda:

double timeSec = 10.0;


int trafficUnitsNumber = 10;

SpeedModel speedModel = (t, wp, hp) -> ...;


BiConsumer<TrafficUnit, Double> printResults = (tu, sp) -> ...;
BiPredicate<TrafficUnit, Double> limitSpeed = (tu, sp) -> ...;

Traffic api = new TrafficImpl(Month.APRIL, DayOfWeek.FRIDAY, 17,


"USA", "Denver", "Main103S");
api.speedAfterStart(timeSec, trafficUnitsNumber, speedModel,
limitSpeed, printResults);

Como ya hemos notado, tal API puede no cubrir todas las formas posibles en que el
modelo puede evolucionar, pero es un buen punto de partida que nos permite construir
el flujo y la tubería de operaciones con más transparencia y flexibilidad de
experimentación.

Ahora, veamos la implementación de la API:

double timeSec = 10.0;


int trafficUnitsNumber = 10;

SpeedModel speedModel = (t, wp, hp) -> ...;


BiConsumer<TrafficUnit, Double> printResults = (tu, sp) -> ...;
BiPredicate<TrafficUnit, Double> limitSpeed = (tu, sp) -> ...;
List<TrafficUnit> trafficUnits = FactoryTraffic
.generateTraffic(trafficUnitsNumber, Month.APRIL,
DayOfWeek.FRIDAY, 17, "USA", "Denver",
"Main103S");

pág. 258
for(TrafficUnit tu: trafficUnits){
Vehicle vehicle = FactoryVehicle.build(tu);
vehicle.setSpeedModel(speedModel);
double speed = vehicle.getSpeedMph(timeSec);
speed = Math.round(speed * tu.getTraction());
if(limitSpeed.test(tu, speed)){
printResults.accept(tu, speed);
}
}

Podemos convertir el bucle for en una secuencia de unidades de tráfico y aplicar las
mismas funciones directamente a los elementos de la secuencia. Pero primero,
podemos solicitar al sistema generador de tráfico que nos proporcione datos
en lugar Stream de datos List. Nos permite evitar almacenar todos los datos en
la memoria:

Stream<TrafficUnit> stream = FactoryTraffic


.getTrafficUnitStream(trafficUnitsNumber, Month.APRIL,
DayOfWeek.FRIDAY, 17, "USA", "Denver", "Main103S");

Ahora podemos procesar un número interminable de unidades de tráfico sin


almacenar en la memoria más de una unidad a la vez. En el código de demostración,
todavía lo usamos List, por lo que la transmisión no nos ahorra memoria. Pero en
sistemas reales, como los que recopilan datos de varios sensores, el uso de flujos ayuda
a disminuir o evitar por completo las preocupaciones por el uso de la memoria.

También crearemos un método conveniente:

Stream<TrafficUnit>getTrafficUnitStream(int trafficUnitsNumber){
return FactoryTraffic.getTrafficUnitStream(trafficUnitsNumber,
Month.APRIL, DayOfWeek.FRIDAY, 17, "USA",
"Denver", "Main103S");
}

Con esto, podemos escribir lo siguiente:

getTrafficUnitStream(trafficUnitsNumber).map(tu -> {
Vehicle vehicle = FactoryVehicle.build(tu);
vehicle.setSpeedModel(speedModel);
return vehicle;
})
.map(v -> {
double speed = v.getSpeedMph(timeSec);
return Math.round(speed * tu.getTraction());
})
.filter(s -> limitSpeed.test(tu, s))
.forEach(tuw -> printResults.accept(tu, s));

Hemos trazado (transformar) TrafficUnit para Vehicle, a continuación,


mapeado Vehiclea speed, y luego se usa la corriente instancia TrafficUnit y el
calcularon speed para limitar los resultados de tráfico y de impresión. Si tiene este
código en un editor moderno, notará que no se compila porque, después del primer

pág. 259
mapa, el elemento TrafficUnit actual ya no es accesible, se reemplaza
por Vehicle. Esto significa que necesitamos llevar los elementos originales y agregar
nuevos valores en el camino. Para lograr esto, necesitamos un contenedor, algún tipo
de envoltorio de unidad de tráfico. Vamos a crear uno:

class TrafficUnitWrapper {
private double speed;
private Vehicle vehicle;
private TrafficUnit trafficUnit;
public TrafficUnitWrapper(TrafficUnit trafficUnit){
this.trafficUnit = trafficUnit;
}
public TrafficUnit getTrafficUnit(){ return this.trafficUnit; }
public Vehicle getVehicle() { return vehicle; }
public void setVehicle(Vehicle vehicle) {
this.vehicle = vehicle;
}
public double getSpeed() { return speed; }
public void setSpeed(double speed) { this.speed = speed; }
}

Ahora, podemos construir una tubería que funcione:

getTrafficUnitStream(trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> {
Vehicle vehicle = FactoryVehicle.build(tuw.getTrafficUnit());
vehicle.setSpeedModel(speedModel);
tuw.setVehicle(vehicle);
return tuw;
})
.map(tuw -> {
double speed = tuw.getVehicle().getSpeedMph(timeSec);
speed = Math.round(speed * tuw.getTrafficUnit().getTraction());
tuw.setSpeed(speed);
return tuw;
})
.filter(tuw -> limitSpeed.test(tuw.getTrafficUnit(),tuw.getSpeed()))
.forEach(tuw -> printResults.accept(tuw.getTrafficUnit(),
tuw.getSpeed()));

El código se ve un poco detallado, especialmente la configuración Vehicle


y SpeedModel. Podemos ocultar esta plomería moviéndolas a la clase
TrafficUntiWrapper ¡:

class TrafficUnitWrapper {
private double speed;
private Vehicle vehicle;
private TrafficUnit trafficUnit;
public TrafficUnitWrapper(TrafficUnit trafficUnit){
this.trafficUnit = trafficUnit;
this.vehicle = FactoryVehicle.build(trafficUnit);
}
public TrafficUnitWrapper setSpeedModel(SpeedModel speedModel) {
this.vehicle.setSpeedModel(speedModel);
return this;

pág. 260
}
pubic TrafficUnit getTrafficUnit(){ return this.trafficUnit; }
public Vehicle getVehicle() { return vehicle; }
public double getSpeed() { return speed; }
public TrafficUnitWrapper setSpeed(double speed) {
this.speed = speed;
return this;
}
}

Observe cómo regresamos this de


los métodos setSpeedModel() y setSpeed(). Esto nos permite preservar el
estilo fluido. Ahora, la tubería se ve mucho más limpia:

getTrafficUnitStream(trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(speedModel))
.map(tuw -> {
double speed = tuw.getVehicle().getSpeedMph(timeSec);
speed = Math.round(speed * tuw.getTrafficUnit().getTraction());
return tuw.setSpeed(speed);
})
.filter(tuw -> limitSpeed.test(tuw.getTrafficUnit(),tuw.getSpeed()))
.forEach(tuw -> printResults.accept(tuw.getTrafficUnit(),
tuw.getSpeed()));

Si no es necesario mantener fácilmente accesible la fórmula para los cálculos de


velocidad, podemos moverla a la clase TrafficUnitWrapper cambiando
el setSpeed()método a calcSpeed():

TrafficUnitWrapper calcSpeed(double timeSec) {


double speed = this.vehicle.getSpeedMph(timeSec);
this.speed = Math.round(speed * this.trafficUnit.getTraction());
return this;
}

Entonces, la tubería se vuelve aún menos detallada:

getTrafficUnitStream(trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(speedModel))
.map(tuw -> tuw.calcSpeed(timeSec))
.filter(tuw -> limitSpeed.test(tuw.getTrafficUnit(),
tuw.getSpeed()))
.forEach(tuw -> printResults.accept(tuw.getTrafficUnit(),
tuw.getSpeed()));

En base a esta técnica, ahora podemos crear un método que calcule la densidad del
tráfico : el recuento de vehículos en cada carril de una carretera de varios carriles para
el límite de velocidad dado en cada uno de los carriles:

Integer[] trafficByLane(Stream<TrafficUnit> stream,


int trafficUnitsNumber, double timeSec,
SpeedModel speedModel, double[] speedLimitByLane) {

pág. 261
int lanesCount = speedLimitByLane.length;
Map<Integer, Integer> trafficByLane = stream
.limit(trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(speedModel))
.map(tuw -> tuw.calcSpeed(timeSec))
.map(speed -> countByLane(lanesCount,
speedLimitByLane, speed))
.collect(Collectors.groupingBy(CountByLane::getLane,
Collectors.summingInt(CountByLane::getCount)));
for(int i = 1; i <= lanesCount; i++){
trafficByLane.putIfAbsent(i, 0);
}
return trafficByLane.values()
.toArray(new Integer[lanesCount]);
}

La clase privada CountByLane utilizada por el método anterior tiene el siguiente


aspecto:

private class CountByLane {


int count, lane;
private CountByLane(int count, int lane){
this.count = count;
this.lane = lane;
}
public int getLane() { return lane; }
public int getCount() { return count; }
}

Y así es como se ve la clase privada TrafficUnitWrapper :

private static class TrafficUnitWrapper {


private Vehicle vehicle;
private TrafficUnit trafficUnit;
public TrafficUnitWrapper(TrafficUnit trafficUnit){
this.vehicle = FactoryVehicle.build(trafficUnit);
this.trafficUnit = trafficUnit;
}
public TrafficUnitWrapper setSpeedModel(SpeedModel speedModel) {
this.vehicle.setSpeedModel(speedModel);
return this;
}
public double calcSpeed(double timeSec) {
double speed = this.vehicle.getSpeedMph(timeSec);
return Math.round(speed * this.trafficUnit.getTraction());
}
}

El código del método privado countByLane() es el siguiente:

private CountByLane countByLane(int lanesNumber,


double[] speedLimit, double speed){
for(int i = 1; i <= lanesNumber; i++){
if(speed <= speedLimit[i - 1]){
return new CountByLane(1, i);
}

pág. 262
}
return new CountByLane(1, lanesNumber);
}

En Capítulo 14 , Pruebas , discutiremos este método de la clase


TrafficDensity con más detalle y revisaremos esta implementación para permitir
mejores pruebas unitarias. Esta es la razón por la cual escribir una prueba unitaria
paralela al desarrollo del código brinda mayor productividad; elimina la necesidad de
cambiar el código después. También da como resultado un código más comprobable
(de mejor calidad).

Hay más...
La tubería permite agregar fácilmente otro filtro, o cualquier otra operación para el
caso:

Predicate<TrafficUnit> limitTraffic = tu ->


tu.getVehicleType() == Vehicle.VehicleType.CAR
|| tu.getVehicleType() == Vehicle.VehicleType.TRUCK;

getTrafficUnitStream(trafficUnitsNumber)
.filter(limitTraffic)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(speedModel))
.map(tuw -> tuw.calcSpeed(timeSec))
.filter(tuw -> limitSpeed.test(tuw.getTrafficUnit(),
tuw.getSpeed()))
.forEach(tuw -> printResults.accept(tuw.getTrafficUnit(),
tuw.getSpeed()));

Es especialmente importante cuando se deben procesar muchos tipos de datos. Vale la


pena mencionar que tener un filtro antes de los cálculos es la mejor manera de mejorar
el rendimiento porque le permite evitar cálculos innecesarios.

Otra ventaja importante de usar una secuencia es que el proceso puede hacerse paralelo
sin codificación adicional. Todo lo que necesita hacer es cambiar la primera línea de la
tubería
a getTrafficUnitStream(trafficUnitsNumber).parallel() (suponiend
o que la fuente no genera la secuencia paralela, que puede ser identificada por la
operación .isParallel()). Hablaremos sobre el procesamiento paralelo en más
detalle en la próxima receta.

Procesando flujos en paralelo

pág. 263
En las recetas anteriores, demostramos algunas de las técnicas de procesamiento de
flujo paralelo. En esta receta, discutiremos el procesamiento con mayor detalle y
compartiremos las mejores prácticas y soluciones para problemas comunes.

Prepararse
Es tentador configurar todas las transmisiones para que sean paralelas y no pensar en
ello nuevamente. Desafortunadamente, el paralelismo no siempre proporciona una
ventaja. De hecho, incurre en una sobrecarga debido a la coordinación de los hilos de
trabajo. Además, algunas fuentes de flujo son de naturaleza secuencial y algunas
operaciones pueden compartir el mismo recurso (sincronizado). Peor aún, el uso de
una operación con estado en procesamiento paralelo puede conducir a un resultado
impredecible. No significa que uno no pueda usar una operación con estado para una
secuencia paralela, pero requiere una planificación cuidadosa y una comprensión clara
de cómo se comparte el estado entre las subcorrientes de procesamiento paralelo.

Cómo hacerlo...
Como se mencionó en la receta anterior, se puede crear una secuencia paralela
mediante el método parallelStream() de una colección o el método
parallel() aplicado a una secuencia. Por el contrario, la corriente paralela
existente se puede convertir en secuencial utilizando el método sequential().

Como primera práctica recomendada, uno debería usar una secuencia secuencial por
defecto y comenzar a pensar en la secuencia paralela solo si es necesario y posible. La
necesidad generalmente surge si el rendimiento no es lo suficientemente bueno y se
debe procesar una gran cantidad de datos. Las posibilidades están limitadas por la
naturaleza de la fuente de flujo y las operaciones. Por ejemplo, la lectura de un archivo
es secuencial y una secuencia basada en archivos no funciona mejor en
paralelo. Cualquier operación de bloqueo también niega la mejora del rendimiento en
paralelo.

Una de las áreas donde las secuencias secuenciales y paralelas son diferentes es el
ordenamiento. Aquí hay un ejemplo:

List.of("This ", "is ", "created ", "by ",


"List.of().stream()").stream().forEach(System.out::print);
System.out.println();
List.of("This ", "is ", "created ", "by ",
"List.of().parallelStream()")
.parallelStream().forEach(System.out::print);

El resultado es el siguiente:

pág. 264
Como puede ver, List conserva el orden de los elementos pero no lo mantiene en el
caso de procesamiento paralelo.

En la receta Creación y operación de flujos , demostramos que con


las operaciones reduce()y collect(), se llama a un combinador solo para un flujo
paralelo. Por lo tanto, el combinador no es necesario para un procesamiento de flujo
secuencial, pero debe estar presente mientras opera en paralelo. Sin ella, los resultados
de múltiples trabajadores no se agregan correctamente.

También hemos demostrado que las operaciones


sorted(), distinct(), limit(), y skip() con estado dió resultados no
deterministas en el caso de procesamiento en paralelo.

Si el pedido es importante, hemos demostrado que puede confiar en la operación


forEachOrdered(). Se garantiza no sólo el procesamiento de todos los elementos
de la corriente, sino también hacerlo en el orden especificado por su origen,
independientemente de si la corriente es secuencial o en paralelo.

Se puede crear una secuencia paralela por el método parallelStream() o por


el método parallel(). Una vez creado, utiliza un marco ForkJoin durante el
procesamiento: la secuencia original se divide en segmentos (subtransmisiones) que
luego se entregan a diferentes subprocesos de trabajo para su procesamiento, luego
todos los resultados (de cada procesamiento de subtransmisión) se agregan y
presentan como Los resultados finales del procesamiento original de la secuencia. En
una computadora con un solo procesador, dicha implementación no tiene una ventaja
porque el procesador es compartido. Pero en una computadora multinúcleo, los
subprocesos de trabajo pueden ser ejecutados por diferentes procesadores. Aún más,
si un trabajador queda inactivo, puede robaruna parte del trabajo de uno
ocupado. Luego, los resultados se recopilan de todos los trabajadores y se agregan para
la finalización de la operación del terminal (es decir, cuando un combinador de una
operación de recopilación se ocupa).

En términos generales, si hay un recurso que no es seguro para el acceso concurrente,


tampoco es seguro usarlo durante el procesamiento de flujo paralelo. Considere estos
dos ejemplos ( ArrayList no se sabe que sea seguro para subprocesos):

List<String> wordsWithI = new ArrayList<>();


Stream.of("That ", "is ", "a ", "Stream.of(literals)")
.parallel()
.filter(w -> w.contains("i"))
.forEach(wordsWithI::add);
System.out.println(wordsWithI);
System.out.println();

pág. 265
wordsWithI = Stream.of("That ", "is ", "a ", "Stream.of(literals)" )
.parallel()
.filter(w -> w.contains("i"))
.collect(Collectors.toList());
System.out.println(wordsWithI);

Si se ejecuta varias veces, este código puede producir el siguiente resultado:

El método Collectors.toList() siempre genera la misma lista, que consiste


en isy Stream.of(literals), aunque forEach() falla, iso
de Stream.of(literals)vez en cuando.

Si es posible, intente usar primero Collectors construidos por la clase y evite los
recursos compartidos durante los cálculos paralelos.

En general, el uso de funciones sin estado es su mejor opción para las tuberías de flujo
paralelo. En caso de duda, pruebe su código y, lo más importante, ejecute la misma
prueba muchas veces para verificar si el resultado es estable.

Programación de bases de datos


Este capítulo cubre las interacciones básicas y de uso común entre una aplicación Java
y una base de datos ( DB ), desde conectarse a la DB y realizar operaciones CRUD
hasta crear transacciones, almacenar procedimientos y trabajar con objetos
grandes ( LOB ). Cubriremos las siguientes recetas:

• Conexión a una base de datos utilizando JDBC


• Configurar las tablas necesarias para las interacciones de DB
• Realizando operaciones CRUD usando JDBC
• Uso del conjunto de conexiones de Hikari ( HikariCP )
• Usar declaraciones preparadas
• Usar transacciones
• Trabajando con objetos grandes
• Ejecutar procedimientos almacenados
• Uso de operaciones por lotes para un gran conjunto de datos
• Usando MyBatis para operaciones CRUD
• Uso de la API de persistencia de Java e Hibernate

pág. 266
Introducción
Es difícil imaginar una aplicación de software compleja que no utilice algún tipo de
almacenamiento de datos estructurado y accesible llamado base de datos. Es por eso
que cualquier implementación de lenguaje moderno incluye un marco que le permite
acceder a la base de datos y crear, leer, actualizar y eliminar datos ( CRUD ) en
ella. En Java, la API de Java Database Connectivity ( JDBC ) proporciona acceso a
cualquier fuente de datos, desde bases de datos relacionales hasta hojas de cálculo y
archivos planos.

En base a este acceso, una aplicación puede manipular datos en la base de datos
directamente, utilizando lenguaje de base de datos (SQL, por ejemplo), o
indirectamente, utilizando un marco de Mapeo Relacional de Objetos (ORM) , que
permite la asignación de objetos en memoria al tablas en la base de datos. La API de
persistencia de Java (JPA) es la especificación ORM para Java. Cuando se utiliza un
marco ORM, las operaciones CRUD en los objetos Java asignados se traducen al lenguaje
de la base de datos automáticamente. La lista de los marcos ORM más populares incluye
Apache Cayenne, Apache OpenJPA, EclipseLink, jOOQ, MyBatis e Hibernate, por
nombrar algunos.

Los paquetes java.sqly javax.sql que componen la API JDBC se incluyen


en Java Platform Standard Edition ( Java SE ). El paquete java.sql proporciona la
API para acceder y procesar datos almacenados en una fuente de datos (generalmente
una base de datos relacional) . El paquete javax.sql proporciona la API para el
acceso y el procesamiento de la fuente de datos del lado del servidor. Específicamente,
proporciona la DataSource interfaz para establecer una conexión con una base de
datos, agrupación de conexiones y declaraciones, transacciones distribuidas y
conjuntos de filas. El paquete javax.persistence que contiene interfaces que
cumplen con JPA no está incluido en Java SE y debe agregarse como una dependencia al
archivo de configuración de Mavenpom.xml. La implementación específica de JPA, el
marco ORM preferido, también debe incluirse como una dependencia de
Maven. Discutiremos el uso de los marcos JDBC, JPA y dos ORM, Hibernate y MyBatis,
en las recetas de este capítulo.

Para conectarse realmente DataSource a una base de datos física, también necesita
un controlador específico de la base de datos (proporcionado por un proveedor de
bases de datos, como MySQL, Oracle, PostgreSQL o la base de datos del servidor SQL,
por ejemplo). Puede estar escrito en Java o en una combinación de métodos nativos de
Java y la Interfaz nativa de Java ( JNI ). Este controlador implementa la API JDBC.

Trabajar con una base de datos implica ocho pasos:

1. Instalación de la base de datos siguiendo las instrucciones del proveedor.

pág. 267
2. Agregar la dependencia de a .jar a la aplicación con el controlador específico de
la base de datos.
3. Crear un usuario, una base de datos y un esquema de base de datos: tablas, vistas,
procedimientos almacenados, etc.
4. Conexión a la base de datos desde la aplicación.
5. Construir una declaración SQL directamente usando JDBC o indirectamente usando
JPA.
6. Ejecutando la declaración SQL directamente usando JDBC o confirmando cambios
de datos usando JPA.
7. Usando el resultado de la ejecución.
8. Cerrar la conexión de la base de datos y otros recursos.

Los pasos 1 a 3 se realizan solo una vez en la etapa de configuración de la base de datos,
antes de ejecutar la aplicación.

La aplicación realiza los pasos 4 a 8 repetidamente, según sea necesario.

Los pasos 5 a 7 se pueden repetir varias veces con la misma conexión de base de datos.

Conexión a una base de datos


utilizando JDBC
En esta receta, aprenderá cómo conectarse a una base de datos.

Cómo hacerlo...
1. Seleccione la base de datos con la que le gustaría trabajar. Hay buenas bases de datos
comerciales y buenas bases de datos de código abierto. Lo único que vamos a suponer
es que la base de datos de su elección es compatible con el Lenguaje de consulta
estructurado ( SQL ), que es un lenguaje estandarizado que le permite
realizar operaciones CRUD en una base de datos. En nuestras recetas, utilizaremos
el SQL estándar y evitaremos construcciones y procedimientos específicos para un
tipo de base de datos en particular.
2. Si la base de datos aún no está instalada, siga las instrucciones del proveedor e
instálela. Luego, descargue el controlador de la base de datos. Los más populares son
de los tipos 4 y 5, escritos en Java. Son muy eficientes y hablan con el servidor de la
base de datos a través de una conexión de socket. Si un archivo.jar con dicho
controlador se coloca en el classpath, se carga automáticamente. Los controladores
de tipo 4 y 5 son específicos de la base de datos porque utilizan un protocolo nativo
de base de datos para acceder a la base de datos. Vamos a suponer que está utilizando
un controlador de ese tipo.

pág. 268
Si su aplicación tiene que acceder a varios tipos de bases de datos, entonces necesita un
controlador del tipo 3. Dicho controlador puede comunicarse con diferentes bases de
datos a través de un servidor de aplicaciones de middleware.

Utilice controladores de tipo 1 y 2 solo cuando no haya otros tipos de controladores


disponibles para su base de datos.

3. Establezca el archivo .jar descargado con el controlador en el classpath de su


aplicación. Ahora, su aplicación puede acceder a la base de datos.
4. Su base de datos puede tener una consola, una GUI o alguna otra forma de
interactuar con ella. Lea las instrucciones y primero cree un usuario, es decir cook,
y luego una base de datos, a saber cookbook.

Por ejemplo, aquí están los comandos para PostgreSQL:

CREATE USER cook SUPERUSER;


CREATE DATABASE cookbook OWNER cook;

Seleccionamos el rol SUPERUSER para nuestro usuario; sin embargo, una buena
práctica de seguridad es asignar un rol tan poderoso a un administrador y crear otro
usuario específico de la aplicación que pueda administrar datos pero no pueda cambiar
la estructura de la base de datos. Es una buena práctica crear otra capa lógica,
llamada esquema, que pueda tener su propio conjunto de usuarios y permisos. De esta
manera, podrían aislarse varios esquemas en la misma base de datos, y cada usuario
(uno de ellos es su aplicación) tendrá acceso solo a un determinado esquema.

5. Además, a nivel empresarial, la práctica común es crear sinónimos para el esquema


de la base de datos para que ninguna aplicación pueda acceder a la estructura original
directamente. Incluso puede crear una contraseña para cada usuario, pero, una vez más, para
el propósito de este libro, esto no es necesario. Por lo tanto, dejamos que los administradores
de la base de datos establezcan las reglas y pautas adecuadas para las condiciones de trabajo
particulares de cada empresa.

Ahora, conectamos nuestra aplicación a la base de datos. En el siguiente código de


demostración, utilizaremos, como probablemente ya haya adivinado, la base de datos
de código abierto PostgreSQL.

Cómo funciona...
Aquí está el fragmento de código que crea una conexión con la base de datos local de
PostgreSQL:

String URL = "jdbc:postgresql://localhost/cookbook";


Properties prop = new Properties( );
//prop.put( "user", "cook" );

pág. 269
//prop.put( "password", "secretPass123" );
Connection conn = DriverManager.getConnection(URL, prop);

Las líneas comentadas muestran cómo puede establecer un usuario y una contraseña
para su conexión. Dado que, para esta demostración, mantenemos la base de datos
abierta y accesible para cualquier persona, podríamos utilizar un
método sobrecargado DriverManager.getConnection(String url). Sin
embargo, mostraremos la implementación más general que permitiría a cualquiera leer
un archivo de propiedades y pasar otros valores útiles ( sslcomo verdadero /
falso, autoReconnectcomo verdadero / falso, connectTimeouten segundos, etc.)
al método de creación de conexión. Muchas claves para las propiedades pasadas son las
mismas para todos los tipos de bases de datos principales, pero algunas de ellas son
específicas de la base de datos.

Alternativamente, para pasar solo un usuario y una contraseña, podríamos usar la


tercera versión sobrecargada, a saber DriverManager.getConnection(String
url, String user, String password). Vale la pena mencionar que es una
buena práctica mantener la contraseña encriptada. No le mostraremos cómo hacerlo,
pero hay muchas guías disponibles en línea.

Además, el método getConnection()arroja SQLException, por lo que debemos


envolverlo en un try...catchbloque.

Para ocultar toda esta plomería, es una buena idea mantener el código de
establecimiento de conexión dentro de un método:

Connection getDbConnection(){
String url = "jdbc:postgresql://localhost/cookbook";
try {
return DriverManager.getConnection(url);
}
catch(Exception ex) {
ex.printStackTrace();
return null;
}
}

Otra forma de conectarse a una base de datos es usar la interfaz DataSource. Su


implementación generalmente se incluye en el mismo archivo.jar que el
controlador de la base de datos. En el caso de PostgreSQL, hay dos clases que
implementan la interfaz
DataSource : org.postgresql.ds.PGSimpleDataSourcey org.postgres
ql.ds.PGPoolingDataSource. Podemos usarlos en lugar
de DriverManager. Aquí hay un ejemplo del uso de PGSimpleDataSource:

Connection getDbConnection(){
PGSimpleDataSource source = new PGSimpleDataSource();
source.setServerName("localhost");
source.setDatabaseName("cookbook");

pág. 270
source.setLoginTimeout(10);
try {
return source.getConnection();
}
catch(Exception ex) {
ex.printStackTrace();
return null;
}
}

Y el siguiente es un ejemplo del uso de PGPoolingDataSource:

Connection getDbConnection(){
PGPoolingDataSource source = new PGPoolingDataSource();
source.setServerName("localhost");
source.setDatabaseName("cookbook");
source.setInitialConnections(3);
source.setMaxConnections(10);
source.setLoginTimeout(10);
try {
return source.getConnection();
}
catch(Exception ex) {
ex.printStackTrace();
return null;
}
}

La última versión del método getDbConnection()suele ser la forma preferida de


conexión porque le permite utilizar la agrupación de conexiones y algunas otras
funciones, además de las disponibles al conectarse a través de DriverManager. Sin
embargo, tenga en cuenta que la clase está en desuso, ya que la versión estaba a favor
del software de agrupación de conexiones de terceros. Uno de estos marcos, HikariCP,
que hemos mencionado anteriormente, se discutirá y demostrará en la receta Uso del
conjunto de conexiones de Hikari . PGPoolingDataSource42.0.0

Cualquiera sea la versión de la implementación getDbConnection() que elija,


puede usarla en todos los ejemplos de código de la misma manera.

Hay más...
Es una buena práctica cerrar la conexión tan pronto como no la necesite. La forma de
hacerlo es mediante el uso de la construcción try-with-resources, que
garantiza que el recurso se cierre al final del bloque try...catch:

try (Connection conn = getDbConnection ()) {


// código que usa la conexión para acceder al DB
}
catch (Exception ex) {
ex.printStackTrace () ;
}

pág. 271
Tal construcción se puede usar con cualquier objeto que
implemente java.lang.AutoCloseable la interfaz java.io.Closeable .

Configurar las tablas necesarias


para las interacciones de DB
En esta receta, aprenderá cómo crear, cambiar y eliminar tablas y otras
construcciones de bases de datos lógicas que componen un esquema de base de datos.

Prepararse
La instrucción SQL estándar para la creación de tablas tiene el siguiente aspecto:

CREATE TABLE
table_name ( column1_name data_type (size),
column2_name data_type (size),
column3_name data_type (size),
....
);

Aquí, table_namey column_name deben ser identificadores alfanuméricos y


únicos (dentro del esquema). Las limitaciones para los nombres y los posibles tipos de
datos son específicos de la base de datos. Por ejemplo, Oracle permite que el nombre de
la tabla tenga 128 caracteres, mientras que en PostgreSQL, la longitud máxima del
nombre de la tabla y la columna es de 63 caracteres. También hay diferencias en los
tipos de datos, así que lea la documentación de la base de datos.

Cómo funciona...
Aquí hay un ejemplo de un comando que crea la tabla traffic_unit en
PostgreSQL:

CREATE TABLE traffic_unit (


id SERIAL PRIMARY KEY,
vehicle_type VARCHAR NOT NULL,
horse_power integer NOT NULL,
weight_pounds integer NOT NULL,
payload_pounds integer NOT NULL,
passenger_count integer NOT NULL,
speed_limit_mph double precision NOT NULL,
traction double precision NOT NULL,
road_condition VARCHAR NOT NULL ,
tire_condition VARCHAR NOT NULL,
temperature integer NOT NULL

pág. 272
);

El parámetro size es opcional. Si no se establece, como en el código de ejemplo


anterior, significa que la columna puede almacenar valores de cualquier
longitud. El tipo integer, en este caso, le permite almacenar números
desde Integer.MIN_VALUE(que es -2147483648)
hasta Integer.MAX_VALUE (que es +2147483647). El tipo NOT NULL se agregó
porque, de forma predeterminada, la columna sería anulable y queríamos asegurarnos
de que todas las columnas se llenen.

También identificamos la columna id como PRIMARY KEY, lo que indica que la


columna (o la combinación de columnas) identifica de forma exclusiva el registro. Por
ejemplo, si hay una tabla que contiene información sobre todas las personas de todos
los países, la combinación única probablemente sería su nombre completo, dirección y
fecha de nacimiento. Bueno, es plausible imaginar que en algunos hogares, los gemelos
nacen y reciben el mismo nombre, por lo que dijimos probablemente . Si la posibilidad
de tal ocasión es alta, necesitaríamos agregar otra columna a la combinación de teclas
principal, que es un orden de nacimiento, con el valor predeterminado de 1. Así es como
podemos hacer esto en PostgreSQL:

CREATE TABLE person (


name VARCHAR NOT NULL,
address VARCHAR NOT NULL,
dob date NOT NULL,
order integer DEFAULT 1 NOT NULL,
PRIMARY KEY (name,address,dob,order)
);

En el caso de la tabla traffic_unit, no existe una combinación de columnas que


puedan servir como clave principal. Muchos automóviles tienen los mismos valores en
cualquier combinación de columnas. Pero necesitamos referirnos a un
registro traffic_unit para poder saber, por ejemplo, qué unidades han sido
seleccionadas y procesadas y cuáles no. Es por eso que agregamos una columna id
para llenarla con un número único generado, y nos gustaría que la base de datos genere
esta clave primaria automáticamente.

Si emite el comando \d traffic_unit para mostrar la descripción de la tabla,


verá la función nextval('traffic_unit_id_seq'::regclass) asignada a
la columna id . Esta función genera números secuencialmente, comenzando con 1. Si
necesita un comportamiento diferente, cree el generador de números de secuencia
manualmente. Aquí hay un ejemplo de cómo hacer esto:

CREATE SEQUENCE traffic_unit_id_seq


START WITH 1000 INCREMENT BY 1
NO MINVALUE NO MAXVALUE CACHE 10;
ALTER TABLE ONLY traffic_unit ALTER COLUMN id SET DEFAULT
nextval('traffic_unit_id_seq':

pág. 273
Esta secuencia comienza desde 1,000 y almacena en caché 10 números para un mejor
rendimiento, si es necesario generar números en rápida sucesión.

De acuerdo con los ejemplos de código dados en los capítulos anteriores, los valores
de vehicle_type, road_conditiony tire_condition están limitados por
el enumtipo. Es por eso que cuando traffic_unitse llena la tabla, nos gustaría
asegurarnos de que solo los valores del tipo enum correspondiente se puedan
establecer en la columna. Para lograr esto, crearemos una tabla de búsqueda
llamada enums y la llenaremos con los valores de nuestros enumtipos:

CREATE TABLE enums (


id integer PRIMARY KEY,
type VARCHAR NOT NULL,
value VARCHAR NOT NULL
);

insert into enums (id, type, value) values


(1, 'vehicle', 'car'),
(2, 'vehicle', 'truck'),
(3, 'vehicle', 'crewcab'),
(4, 'road_condition', 'dry'),
(5, 'road_condition', 'wet'),
(6, 'road_condition', 'snow'),
(7, 'tire_condition', 'new'),
(8, 'tire_condition', 'worn');

PostgreSQL tiene un tipo enum de datos, pero incurre en una sobrecarga si la lista de
valores posibles no es fija y debe cambiarse con el tiempo. Creemos que es muy posible
que la lista de valores en nuestra aplicación se expanda. Entonces, decidimos no usar
un tipo enum de base de datos, sino crear la tabla de búsqueda nosotros mismos.

Ahora, podemos referirnos a los valores de la tabla enums desde la tabla


traffic_unit usando su ID como clave foránea. Primero, eliminamos la tabla:

soltar tabla traffic_unit;

Luego, lo recreamos con un comando SQL ligeramente diferente:

CREATE TABLE traffic_unit (


id SERIAL PRIMARY KEY,
vehicle_type integer REFERENCES enums (id) ,
horse_power integer NOT NULL,
weight_pounds integer NOT NULL,
payload_pounds integer NOT NULL,
passenger_count integer NOT NULL,
speed_limit_mph double precision NOT NULL,
traction double precision NOT NULL,
road_condition enteros REFERENCES enums (id) ,
tire_condition integer REFERENCES enums (id) ,
temperatura integer NOT NULL
);

pág. 274
Las columnas vehicle_type, road_conditiony tire_condition ahora
deben rellenarse con los valores de una clave primaria del registro correspondiente
de la enums tabla. De esta forma, podemos asegurarnos de que nuestro código de
análisis de tráfico podrá hacer coincidir los valores en estas columnas con los valores
de los enumtipos en el código.

Hay más...
También nos gustaría asegurarnos de que la enumstabla no contenga un tipo y valor
de combinación duplicado. Para garantizar esto, podemos agregar una restricción
única a la tabla enums:

ALTER TABLE enums ADD CONSTRAINT enums_unique_type_value


UNIQUE (type, value);

Ahora, si intentamos agregar un duplicado, la base de datos no lo permitirá.

Otra consideración importante de la creación de la tabla de base de datos es si se debe


agregar un índice. Un índice es una estructura de datos que ayuda a acelerar las
búsquedas de datos en la tabla sin tener que verificar cada registro de la tabla. Puede
incluir una o más columnas de una tabla. Por ejemplo, se crea automáticamente un
índice para una clave primaria. Si aparece la descripción de la tabla que ya hemos
creado, verá lo siguiente:

Indexes: "traffic_unit_pkey" PRIMARY KEY, btree (id)

También podemos agregar un índice nosotros mismos si pensamos (y lo hemos


probado por experimentación) que ayudará al rendimiento de la aplicación. En el caso
de traffic_unit, descubrimos que nuestro código a menudo busca en esta tabla
por vehicle_typey passengers_count. Por lo tanto, medimos el rendimiento de
nuestro código durante la búsqueda y agregamos estas dos columnas al índice:

CREATE INDEX idx_traffic_unit_vehicle_type_passengers_count


ON traffic_unit USING btree (vehicle_type,passengers_count);

Luego, medimos el rendimiento nuevamente. Si mejora, dejaríamos el índice en su


lugar, pero, en nuestro caso, lo hemos eliminado:

drop index idx_traffic_unit_vehicle_type_passengers_count;

El índice no mejoró significativamente el rendimiento, probablemente porque un


índice tiene una sobrecarga de escrituras adicionales y espacio de almacenamiento.

En nuestros ejemplos de clave primaria, restricciones e índices, seguimos la convención


de nomenclatura de PostgreSQL. Si usa una base de datos diferente, le sugerimos que

pág. 275
busque su convención de nomenclatura y la siga, para que su nomenclatura se alinee
con los nombres creados automáticamente.

Realizando operaciones CRUD


usando JDBC
En esta receta, aprenderá a rellenar, leer, cambiar y eliminar datos en el uso de JDBC.

Prepararse
Ya hemos visto ejemplos de declaraciones SQL que crean (rellenan) datos en la base
de datos:

INSERT INTO table_name (column1,column2,column3,...)


VALUES (value1,value2,value3,...);

También hemos visto ejemplos de instancias en las que se deben agregar varios
registros de tabla:

INSERT INTO table_name (column1,column2,column3,...)


VALUES (value1,value2,value3, ... ),
(value21,value22,value23, ...),
( ... );

Si una columna tiene un valor predeterminado especificado, no es necesario incluirlo


en la declaración INSERT INTO, a menos que se deba insertar un valor diferente.

La lectura de los datos de la base de datos se realiza mediante una


declaración SELECT:

SELECT column_name,column_name
FROM table_name WHERE some_column=some_value;

Aquí hay una definición general de la cláusula WHERE:

WHERE column_name operator value


Operator:
= Equal
<> Not equal. In some versions of SQL, !=
> Greater than
< Less than
>= Greater than or equal
<= Less than or equal
BETWEEN Between an inclusive range
LIKE Search for a pattern
IN To specify multiple possible values for a column

pág. 276
El constructor column_name operator value se puede combinar con
operadores lógicos AND y OR y se agrupan con los soportes (y ).

Los valores seleccionados se pueden devolver en un cierto orden:

SELECT * FROM table_name WHERE-clause by some_other_column;

El orden se puede marcar como ascendente (predeterminado) o descendente:

SELECT * FROM table_name WHERE-clause by some_other_column desc;

Los datos se pueden cambiar con la declaración UPDATE:

UPDATE table_name SET column1=value1,column2=value2,... WHERE-clause;

Alternativamente, se puede eliminar con la declaración DELETE:

DELETE FROM table_name WHERE-clause;

Sin la cláusula WHERE , todos los registros de la tabla se verán afectados por
la instrucción UPDATEo DELETE.

Cómo hacerlo...
Ya hemos visto una declaración INSERT. Aquí hay un ejemplo de otros tipos de
declaraciones:

La declaración SELECT anterior muestra los valores de todas las columnas de todas
las filas de la tabla. Para limitar la cantidad de datos devueltos, WHERE se puede
agregar una cláusula:

pág. 277
La siguiente captura de pantalla captura tres declaraciones:

La primera es una declaración UPDATE que cambia los valores en la valuecolumna


a NEW, pero solo en las filas donde la valuecolumna contiene
valor new(aparentemente, el valor distingue entre mayúsculas y minúsculas). La
segunda instrucción elimina todas las filas que no tienen el valor NEW en la columna
value . La tercera declaración ( SELECT) recupera valores de todas las filas de todas
las columnas.

Vale la pena señalar que no podríamos eliminar los registros de la tabla enums
si la tabla hiciera referencia a estos registros (como claves
externas) traffic_unit . Solo después de eliminar los registros correspondientes
de la tabla traffic_unit es posible eliminar los registros de la enums tabla .

Para ejecutar cualquiera de las operaciones CRUD en el código, primero debe adquirir
una conexión JDBC, luego crear y ejecutar una declaración:

try (Connection conn = getDbConnection()) {


try (Statement st = conn.createStatement()) {
boolean res = st.execute("select id, type, value from enums");
if (res) {
ResultSet rs = st.getResultSet();
while (rs.next()) {
int id = rs.getInt(1);
String type = rs.getString(2);
String value = rs.getString(3);
System.out.println("id = " + id + ", type = "
+ type + ", value = " + value);
}
} else {
int count = st.getUpdateCount();
System.out.println("Update count = " + count);
}
}
} catch (Exception ex) { ex.printStackTrace(); }

pág. 278
Es una buena práctica usar la construcción try-with-resources para el objeto
Statement. La pérdida de c del objeto Connection cerraría el objeto Statement
automáticamente. Sin embargo, cuando cierra el Statementobjeto explícitamente, la
limpieza ocurre de inmediato, en lugar de tener que esperar las comprobaciones y
acciones necesarias para propagarse a través de las capas del marco.

El método execute() es el más genérico entre los tres métodos que pueden ejecutar
una declaración. Los otros dos incluyen executeQuery()(por SELECT sólo
declaraciones) y executeUpdate()(a declaraciones UPDATE, DELETE, CREATE,
o ALTER). Como puede ver en el ejemplo anterior, el método
execute()regresa boolean, lo que indica si el resultado es un objeto ResultSet
o solo un conteo. Esto significa que execute()actúa como executeQuery() para
la declaración SELECT y executeUpdate()para las otras declaraciones que hemos
enumerado.

Podemos demostrar esto ejecutando el código anterior para la siguiente secuencia de


declaraciones:

"select id, type, value from enums"


"insert into enums (id, type, value)" + " values(1,'vehicle','car')"
"select id, type, value from enums"
"update enums set value = 'bus' where value = 'car'"
"select id, type, value from enums"
"delete from enums where value = 'bus'"
"select id, type, value from enums"

El resultado será el siguiente:

Usamos la extracción posicional de los valores ResultSet porque es más eficiente


que usar el nombre de la columna (como
en rs.getInt("id")o rs.getInt("type") ). Sin embargo, la diferencia en el
rendimiento es muy pequeña y se vuelve importante solo cuando la operación ocurre

pág. 279
muchas veces. Solo la medición y las pruebas reales pueden decirle si esta diferencia
es significativa para su aplicación. Tenga en cuenta que obtener valores por nombre
proporciona una mejor legibilidad del código, que paga bien a largo plazo durante el
mantenimiento de la aplicación.

Utilizamos el método execute()con fines de demostración. En la práctica,


el método executeQuery() se usa para SELECTdeclaraciones:

try (Connection conn = getDbConnection()) {


try (Statement st = conn.createStatement()) {
boolean res = st.execute("select id, type, value from enums");
ResultSet rs = st.getResultSet();
while (rs.next()) {
int id = rs.getInt(1);
String type = rs.getString(2);
String value = rs.getString(3);
System.out.println("id = " + id + ", type = "
+ type + ", value = " + value);
}
}
} catch (Exception ex) { ex.printStackTrace(); }

Como puede ver, el código anterior no se puede generalizar como un método que recibe
la instrucción SQL como parámetro. El código que extrae los datos es específico de la
instrucción SQL ejecutada. Por el contrario, la llamada a executeUpdate()se puede
envolver en un método genérico:

void executeUpdate(String sql){


try (Connection conn = getDbConnection()) {
try (Statement st = conn.createStatement()) {
int count = st.executeUpdate(sql);
System.out.println("Update count = " + count);
}
} catch (Exception ex) { ex.printStackTrace(); }
}

Hay más...
SQL es un lenguaje rico y no tenemos suficiente espacio para cubrir todas sus
características. Pero nos gustaría enumerar algunos de los más populares para que
conozca su existencia y pueda buscarlos cuando sea necesario:

• La SELECTdeclaración permite el uso de la palabra clave DISTINCT, para


deshacerse de todos los valores duplicados
• La palabra clave LIKE permite establecer el patrón de búsqueda en la cláusula
WHERE
• El patrón de búsqueda puede utilizar varios wildcards- %,
_, [charlist], [^charlist], o [!charlist].
• Los valores coincidentes se pueden enumerar con la palabra clave IN

pág. 280
• La declaración SELECT puede incluir varias tablas usando la cláusula JOIN
• SELECT * INTO table_2 from table_1 crea table_2 y copia datos
de table_1
• TRUNCATE es más rápido y usa menos recursos al eliminar todas las filas de una
tabla

Hay muchos otros métodos útiles en la interfaz ResultSet también. Aquí hay un
ejemplo de cómo algunos de sus métodos pueden usarse para escribir código genérico
que atraviese el resultado devuelto y use metadatos para imprimir el nombre de la
columna y el valor devuelto:

void traverseRS(String sql){


System.out.println("traverseRS(" + sql + "):");
try (Connection conn = getDbConnection()) {
try (Statement st = conn.createStatement()) {
try(ResultSet rs = st.executeQuery(sql)){
int cCount = 0;
Map<Integer, String> cName = new HashMap<>();
while (rs.next()) {
if (cCount == 0) {
ResultSetMetaData rsmd = rs.getMetaData();
cCount = rsmd.getColumnCount();
for (int i = 1; i <= cCount; i++) {
cName.put(i, rsmd.getColumnLabel(i));
}
}
List<String> l = new ArrayList<>();
for (int i = 1; i <= cCount; i++) {
l.add(cName.get(i) + " = " + rs.getString(i));
}
System.out.println(l.stream()
.collect(Collectors.joining(", ")));
}
}
}
} catch (Exception ex) { ex.printStackTrace(); }
}

Solo utilizamos ResultSetMetaData una vez para recopilar los nombres de


columna devueltos y la longitud (número de columnas) de una fila. Luego, extrajimos
los valores de cada fila por posición y creamos elementos List<String> con los
nombres de columna correspondientes. Para imprimir, utilizamos algo con lo que ya
está familiarizado: el deleite de un programador : el coleccionista que se une (lo
discutimos en el capítulo anterior). Si llamamos al método traverseRS("select
* from enums"), el resultado será el siguiente:

pág. 281
Uso del conjunto de conexiones de
Hikari (HikariCP)
En esta receta, aprenderá cómo configurar y usar el HikariCP de alto rendimiento.

Prepararse
El marco HikariCP fue creado por Brett Wooldridge, que vive en Japón. Hikari en
japonés significa luz . Es una API ligera y relativamente pequeña que está altamente
optimizada y permite el ajuste a través de muchas propiedades, algunas de las cuales
no están disponibles en otros grupos. Además de usuario estándar, la contraseña, el
tamaño máximo de la piscina, varios ajustes de tiempo de espera, y las propiedades de
configuración de caché, también expone propiedades tales
como allowPoolSuspension, connectionInitSql , connectionTestQuery
, y muchos otros, incluso incluyendo una propiedad que se ocupa de las conexiones no-
tiempo cerrados, leakDetectionThreshold.

Para usar la última versión (al momento de escribir este libro) del grupo Hikari, agregue
la siguiente dependencia al proyecto:

<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.2.0</version>
</dependency>

Para fines de demostración, utilizaremos la base de datos creada en la receta anterior


de este capítulo, Conectando a una base de datos usando JDBC . También asumiremos
que ha estudiado esa receta y que no hay necesidad de repetir lo que se dijo sobre la
base de datos, JDBC y cómo funcionan juntos.

Cómo hacerlo...
Hay varias formas de configurar el grupo de conexiones de Hikari. Todos ellos se
basan en el uso de la interfaz javax.sql.DataSource:

1. El método más obvio y directo es establecer las propiedades del grupo en


el DataSourceobjeto directamente:

HikariDataSource ds = new HikariDataSource();


ds.setPoolName("cookpool");
ds.setDriverClassName("org.postgresql.Driver");

pág. 282
ds.setJdbcUrl("jdbc:postgresql://localhost/cookbook");
ds.setUsername( "cook");
//ds.setPassword("123Secret");
ds.setMaximumPoolSize(10);
ds.setMinimumIdle(2);
ds.addDataSourceProperty("cachePrepStmts", Boolean.TRUE);
ds.addDataSourceProperty("prepStmtCacheSize", 256);
ds.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
ds.addDataSourceProperty("useServerPrepStmts", Boolean.TRUE);

Hemos comentado la contraseña porque no configuramos una para nuestra base de


datos. Entre las propiedades jdbcUrl y dataSourceClassName, solo una de ellas
se puede usar a la vez, excepto cuando se usan algunos controladores más antiguos que
pueden requerir la configuración de ambas propiedades. Además, tenga en cuenta
cómo hemos utilizado el método general addDataSourceProperty()cuando no
hay un configurador dedicado para la propiedad en particular.

Para cambiar de PostgreSQL a otra base de datos relacional, todo lo que necesita hacer
es cambiar el nombre de la clase del controlador y la URL de la base de datos. También
hay muchas otras propiedades; algunos de ellos son específicos de la base de datos,
pero no vamos a profundizar en esos detalles, porque esta receta demuestra cómo usar
HikariCP. Lea la documentación de la base de datos sobre las propiedades de
configuración del grupo específico de la base de datos y cómo usarlas para ajustar el
grupo para obtener el mejor rendimiento, lo que también depende en gran medida de
cómo interactúa la aplicación particular con la base de datos.

2. Otra forma de configurar el grupo de Hikari es usar la clase HikariConfig para


recopilar todas las propiedades y luego establecer el HikariConfig objeto en
el constructor HikariDataSource:

HikariConfig config = new HikariConfig();


config.setPoolName("cookpool");
config.setDriverClassName("org.postgresql.Driver");
config.setJdbcUrl("jdbc:postgresql://localhost/cookbook");
config.setUsername("cook");
//conf.setPassword("123Secret");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.addDataSourceProperty("cachePrepStmts", true);
config.addDataSourceProperty("prepStmtCacheSize", 256);
config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
config.addDataSourceProperty("useServerPrepStmts", true);

HikariDataSource ds = new HikariDataSource(config);

Como puede ver, hemos utilizado el método


general addDataSourceProperty() nuevamente porque tampoco hay
definidores dedicados para esas propiedades en la clase HikariConfig .

3. El objeto HikariConfig, a su vez, puede rellenarse con datos utilizando la


clase java.util.Properties:

pág. 283
4. Properties props = new Properties();
props.setProperty("poolName", "cookpool");
props.setProperty("driverClassName", "org.postgresql.Driver");
props.setProperty("jdbcUrl", "jdbc:postgresql://localhost/cookbook");
props.setProperty("username", "cook");
//props.setProperty("password", "123Secret");
props.setProperty("maximumPoolSize", "10");
props.setProperty("minimumIdle", "2");
props.setProperty("dataSource.cachePrepStmts","true");
props.setProperty("dataSource.prepStmtCacheSize", "256");
props.setProperty("dataSource.prepStmtCacheSqlLimit", "2048");
props.setProperty("dataSource.useServerPrepStmts","true");

HikariConfig config = new HikariConfig(props);


HikariDataSource ds = new HikariDataSource(config);

Tenga en cuenta que hemos utilizado el prefijo dataSource para las propiedades
que no tienen setters dedicados en la clase HikariConfig .

4. Para facilitar aún más la carga de la configuración, la clase HikariConfig tiene


un constructor que acepta un archivo con las propiedades. Por ejemplo, creemos un archivo
llamado database.properties en la carpeta resources con el siguiente
contenido:

poolName=cookpool
driverClassName=org.postgresql.Driver
jdbcUrl=jdbc:postgresql://localhost/cookbook
username=cook
password=
maximumPoolSize=10
minimumIdle=2
dataSource.cachePrepStmts=true
dataSource.useServerPrepStmts=true
dataSource.prepStmtCacheSize=256
dataSource.prepStmtCacheSqlLimit=2048

Observe cómo usamos el prefijo dataSource con las mismas propiedades


nuevamente. Ahora, podemos cargar el archivo anterior directamente en
el constructor HikariConfig :

ClassLoader loader = getClass().getClassLoader();


File file =
new File(loader.getResource("database.properties").getFile());
HikariConfig config = new HikariConfig(file.getAbsolutePath());
HikariDataSource ds = new HikariDataSource(config);

Detrás de escena, como puedes adivinar, solo carga propiedades:

public HikariConfig(String propertyFileName) {


this();
this.loadProperties(propertyFileName);
}

pág. 284
5. Alternativamente, podemos usar la siguiente funcionalidad que se incluye en
el HikariConfigconstructor predeterminado:

String systemProp =
System.getProperty("hikaricp.configurationFile");
if (systemProp != null) {
this.loadProperties(systemProp);
}

Significa que podemos establecer la propiedad del sistema de la siguiente manera:

-Dhikaricp.configurationFile=src/main/resources/database.properties

Entonces, podemos configurar HikariCP de la siguiente manera:

HikariConfig config = new HikariConfig();


HikariDataSource ds = new HikariDataSource(config);

Todos los métodos anteriores de la configuración del grupo producen el mismo


resultado, por lo que solo depende del estilo, la convención o simplemente su
preferencia personal decidir cuál de ellos usar.

Cómo funciona...
El siguiente método está utilizando el objeto DataSource creado para acceder a la
base de datos y seleccionar todos los valores de la tabla enums, que se crearon en la
receta Conexión a una base de datos utilizando JDBC :

void readData(DataSource ds) {


try(Connection conn = ds.getConnection();
PreparedStatement pst =
conn.prepareStatement("select id, type, value from enums");
ResultSet rs = pst.executeQuery()){
while (rs.next()) {
int id = rs.getInt(1);
String type = rs.getString(2);
String value = rs.getString(3);
System.out.println("id = " + id + ", type = " +
type + ", value = " + value);
}
} catch (SQLException ex){
ex.printStackTrace();
}
}

Si ejecutamos el código anterior, el resultado será el siguiente:

pág. 285
Hay más...
Puede leer más sobre las características de HikariCP en GitHub
( https://github.com/brettwooldridge/HikariCP ).

Usar declaraciones preparadas


En esta receta, aprenderá a usar una declaración preparada, una plantilla de
declaración que se puede almacenar en la base de datos y ejecutar de manera eficiente
con diferentes valores de entrada.

Prepararse
Un objeto de una subinterfaz PreparedStatement - de Statement - puede
precompilarse y almacenarse en la base de datos y luego usarse para ejecutar
eficientemente la instrucción SQL varias veces para diferentes valores de
entrada. Similar a un objeto de Statement (creado por
el método createStatement()), puede ser creado por
el prepareStatement()método del mismo objeto Connection.

La misma instrucción SQL que se utilizó para generar también Statement se puede
utilizar para generar PreparedStatement. De hecho, es una buena idea considerar
el uso PrepdaredStatement de cualquier instrucción SQL que se llame varias veces,
ya que funciona mejor que Statement. Para hacer esto, todo lo que necesitamos
cambiar son estas dos líneas en el código de muestra de la sección anterior:

try (Statement st = conn.createStatement()) {


boolean res = st.execute("select * from enums");

Cambiamos estas líneas a lo siguiente:

try (PreparedStatement st =
conn.prepareStatement ( "select * from enums" )) {
boolean res = st.execute () ;

pág. 286
Cómo hacerlo...
La verdadera utilidad de PreparedStatement brilla debido a su capacidad para
aceptar parámetros: los valores de entrada que sustituyen (en orden de aparición)
el ? símbolo. Aquí hay un ejemplo de esto:

traverseRS("select * from enums");


System.out.println();
try (Connection conn = getDbConnection()) {
String[][] values = {{"1", "vehicle", "car"},
{"2", "vehicle", "truck"}};
String sql = "insert into enums (id, type, value) values(?, ?, ?)");
try (PreparedStatement st = conn.prepareStatement(sql) {
for(String[] v: values){
st.setInt(1, Integer.parseInt(v[0]));
st.setString(2, v[1]);
st.setString(3, v[2]);
int count = st.executeUpdate();
System.out.println("Update count = " + count);
}
}
} catch (Exception ex) { ex.printStackTrace(); }
System.out.println();
traverseRS("select * from enums");

El resultado de esto es el siguiente:

Hay más...
No es una mala idea usar siempre declaraciones preparadas para operaciones
CRUD. Pueden ser más lentos si se ejecutan solo una vez, pero puede probar y ver si
este es el precio que está dispuesto a pagar. Mediante el uso sistemático de
declaraciones preparadas, producirá un código coherente (mejor legible) que
proporciona más seguridad (las declaraciones preparadas no son vulnerables a la
inyección de SQL).

pág. 287
Usar transacciones
En esta receta, aprenderá qué es una transacción de base de datos y cómo se puede
usar en código Java.

Prepararse
Una transacción es una unidad de trabajo que incluye una o muchas operaciones que
cambian datos. Si tiene éxito, todos los cambios de datos se confirman (se aplican a la
base de datos). Si uno de los errores de operaciones fuera, la transacción se deshace , y
ninguno de los cambios incluidos en la transacción se aplicará a la base de datos.

Las propiedades de transacción se configuran en el objeto Connection. Se pueden


cambiar sin cerrar la conexión, por lo que diferentes transacciones pueden reutilizar el
mismo objeto Connection.

JDBC permite el control de transacciones solo para operaciones CRUD. Tabla de


modificación ( CREATE TABLE, ALTER TABLEy así sucesivamente) se compromete
automáticamente y no se puede controlar desde el código Java.

De forma predeterminada, una transacción de operación CRUD está configurada


para autocomprometirse . Esto significa que cada cambio de datos introducido por
una declaración SQL se aplica a la base de datos tan pronto como se completa la
ejecución de esta declaración. Todos los ejemplos anteriores en este capítulo usan este
comportamiento predeterminado.

Para cambiar este comportamiento, debe usar el


método setAutoCommit(boolean) del objeto Connection . Si se establece
en false, los cambios de datos no se aplicarán a la base de datos hasta que se
invoque el commit()método en el Connectionobjeto. Además, si rollback()se
llama al método en su lugar, todos los datos cambian desde el comienzo de la
transacción o desde la última llamada a commit()se descartarán.

La gestión explícita de transacciones programáticas mejora el rendimiento, pero es


insignificante en el caso de operaciones atómicas cortas que se llaman una vez y no muy
a menudo. Asumir el control de las transacciones se vuelve crucial cuando varias
operaciones introducen cambios que tienen que aplicarse ya sea todos juntos o ninguno
de ellos. Permite los cambios de bases de datos grupales en unidades atómicas y, por lo
tanto, evita la violación accidental de la integridad de los datos.

Cómo hacerlo...
pág. 288
Primero, agreguemos una salida al método traverseRS():

void traverseRS(String sql){


System.out.println("traverseRS(" + sql + "):");
try (Connection conn = getDbConnection()) {
...
}
}

Esto lo ayudará a analizar la salida cuando se ejecutan muchas declaraciones SQL


diferentes en el mismo ejemplo de demostración.

Ahora, ejecutemos el siguiente código que lee los datos de la tabla enums, luego
inserta una fila y luego lee todos los datos de la tabla nuevamente:

traverseRS("select * from enums");


System.out.println();
try (Connection conn = getDbConnection()) {
conn.setAutoCommit(false);
String sql = "insert into enums (id, type, value) "
+ " values(1,'vehicle','car')";
try (PreparedStatement st = conn.prepareStatement(sql)) {
System.out.println(sql);
System.out.println("Update count = " + st.executeUpdate());
}
//conn.commit();
} catch (Exception ex) { ex.printStackTrace(); }
System.out.println();
traverseRS("select * from enums");

Tenga en cuenta que asumimos el control de las transacciones


llamando conn.setAutoCommit(false). El resultado es el siguiente:

Como puede ver, los cambios no se aplicaron porque la llamada a commit() fue
comentada. Cuando lo descomentamos, el resultado cambia:

pág. 289
Ahora, ejecutemos dos inserciones, pero introduzca un error de ortografía en la
segunda inserción:

traverseRS("select * from enums");


System.out.println();
try (Connection conn = getDbConnection()) {
conn.setAutoCommit(false);
String sql = "insert into enums (id, type, value) "
+ " values(1,'vehicle','car')";
try (PreparedStatement st = conn.prepareStatement(sql)) {
System.out.println(sql);
System.out.println("Update count = " + st.executeUpdate());
}
conn.commit();
sql = "inst into enums (id, type, value) "
+ " values(2,'vehicle','truck')";
try (PreparedStatement st = conn.prepareStatement(sql)) {
System.out.println(sql);
System.out.println("Update count = " + st.executeUpdate());
}
conn.commit();
} catch (Exception ex) { ex.printStackTrace(); } //get exception here
System.out.println();
traverseRS("select * from enums");

Obtenemos un seguimiento de pila de excepción (no lo mostramos para ahorrar


espacio) y este mensaje:

org.postgresql.util.PSQLException: ERROR: syntax error at or near "inst"

Sin embargo, la primera inserción se ejecutó con éxito:

La segunda fila no se insertó. Si no existiera conn.commit()después de la primera


instrucción INSERT INTO , la primera inserción tampoco se aplicaría. Esta es la
ventaja del control programático de transacciones en el caso de muchos cambios de
datos independientes : si uno falla, podemos omitirlo y continuar aplicando otros
cambios.

pág. 290
Ahora, intentemos insertar tres filas con un error (estableciendo una letra en lugar de
un número como idvalor) en la segunda fila:

traverseRS("select * from enums");


System.out.println();
try (Connection conn = getDbConnection()) {
conn.setAutoCommit(false);
String[][] values = { {"1", "vehicle", "car"},
{"b", "vehicle", "truck"},
{"3", "vehicle", "crewcab"} };
String sql = "insert into enums (id, type, value) "
+ " values(?, ?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
for (String[] v: values){
try {
System.out.print("id=" + v[0] + ": ");
st.setInt(1, Integer.parseInt(v[0]));
st.setString(2, v[1]);
st.setString(3, v[2]);
int count = st.executeUpdate();
conn.commit();
System.out.println("Update count = "+count);
} catch(Exception ex){
//conn.rollback();
System.out.println(ex.getMessage());
}
}
}
} catch (Exception ex) { ex.printStackTrace(); }
System.out.println();
traverseRS("select * from enums");

Ponemos cada ejecución de inserción en el bloque try...catch y confirmamos los


cambios antes de imprimir el resultado (recuento de actualizaciones o mensaje de
error). El resultado es el siguiente:

Como puede ver, la segunda fila no se insertó, aunque conn.rollback()se


comentó. ¿Por qué? Esto se debe a que la única instrucción SQL incluida en esta
transacción falló, por lo que no había nada que revertir.

pág. 291
Ahora, creemos una tabla test con solo una columna nameusando la consola de la
base de datos:

Insertaremos en la tabla test el tipo de vehículo antes de insertar un registro en


la tabla enums:

traverseRS("select * from enums");


System.out.println();
try (Connection conn = getDbConnection()) {
conn.setAutoCommit(false);
String[][] values = { {"1", "vehicle", "car"},
{"b", "vehicle", "truck"},
{"3", "vehicle", "crewcab"} };
String sql = "insert into enums (id, type, value) " +
" values(?, ?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
for (String[] v: values){
try(Statement stm = conn.createStatement()) {
System.out.print("id=" + v[0] + ": ");
stm.execute("insert into test values('"+ v[2] + "')");
st.setInt(1, Integer.parseInt(v[0]));
st.setString(2, v[1]);
st.setString(3, v[2]);
int count = st.executeUpdate();
conn.commit();
System.out.println("Update count = " + count);
} catch(Exception ex){
//conn.rollback();
System.out.println(ex.getMessage());
}
}
}
} catch (Exception ex) { ex.printStackTrace(); }
System.out.println();
traverseRS("select * from enums");
System.out.println();
traverseRS("select * from test");

Como puede ver, el código anterior confirma los cambios después de la segunda
inserción, que, como en el ejemplo anterior, no tiene éxito para el segundo elemento de
la matriz values. Con conn.rollback()comentado, el resultado será el siguiente:

pág. 292
La fila con truckno se insertó en la tabla enums, sino que se agregó a la tabla
test . Es decir, cuando se demostró la utilidad de una reversión. Si
descomentamos conn.rollback(), el resultado será el siguiente:

Esto demuestra que conn.rollback() revierte todos los cambios aún no


confirmados.

Hay más...
Otra propiedad importante de una transacción es el nivel de aislamiento
de la transacción . Define los límites entre los usuarios de la base de datos. Por
ejemplo, ¿pueden otros usuarios ver los cambios en su base de datos antes de
comprometerse? Cuanto mayor sea el aislamiento (el más alto es serializable ), más
tiempo lleva completar una transacción en el caso de acceso concurrente a los mismos
registros. Cuanto menos restrictivo sea el aislamiento (lo menos restrictivo se lee sin

pág. 293
confirmar ), más sucios son los datos, lo que significa que otros usuarios pueden
obtener los valores que aún no ha confirmado (y tal vez nunca lo harán).

Por lo general, es suficiente usar el nivel predeterminado, que normalmente


es TRANSACTION_READ_COMMITTED, aunque puede ser diferente para diferentes
bases de datos. JDBC le permite obtener el nivel de aislamiento de transacción actual
llamando al método getTransactionIsolation() en el objeto
Connection. El método setTransactionIsolation() del objeto Connection
le permite establecer cualquier nivel de aislamiento según sea necesario.

En el caso de una lógica compleja de toma de decisiones sobre qué cambios deben
confirmarse y cuáles deben revertirse, uno puede usar otros dos métodos
Connection para crear y eliminar puntos de guardado . El
método setSavepoint(String savepointName) crea un nuevo punto de
rescate y devuelve un objeto Savepoint, que luego se puede utilizar para revertir
todos los cambios hasta este punto utilizando el método rollback (Savepoint
savepoint). Un punto de rescate se puede eliminar
llamando releaseSavepoint(Savepoint savepoint).

Los tipos más complejos de transacciones de bases de datos son transacciones


distribuidas . A veces se denominan transacciones
globales , transacciones XA o transacciones JTA (esta última es una API Java que
consta de dos paquetes Java, a
saber, javax.transactiony javax.transaction.xa). Permiten la creación y
ejecución de una transacción que abarca operaciones en dos bases de datos
diferentes. Proporcionar una descripción detallada de las transacciones distribuidas
está fuera del alcance de este libro.

Trabajando con objetos grandes


En esta receta, aprenderá cómo almacenar y recuperar un LOB que puede ser uno de
tres tipos: Objeto grande binario ( BLOB ), Objeto grande de carácter ( CLOB )
y Objeto grande de carácter nacional ( NCLOB ).

Prepararse
El procesamiento real de los objetos LOB dentro de una base de datos es específica del
proveedor, pero JDBC API ocultar estos detalles de implementación de la aplicación
mediante la representación de los tres tipos LOB como interfaces -
java.sql.Blob , java.sql.Cloby java.sql.NClob.

pág. 294
Blobgeneralmente se usa para almacenar imágenes u otros datos no alfanuméricos. En
el camino a la base de datos, una imagen se puede convertir en una secuencia de bytes
y almacenarse utilizando la instrucción INSERT INTO. La Blobinterfaz le permite
encontrar la longitud del objeto y convertirlo en una matriz de bytes que Java puede
procesar con el fin de mostrar la imagen, por ejemplo.

Clob le permite almacenar datos de personajes. NClob almacena datos de caracteres


Unicode como una forma de apoyar la internacionalización. Extiende la interfaz
Clob y proporciona los mismos métodos. Ambas interfaces le permiten encontrar la
longitud de LOB y obtener una subcadena dentro del valor.

Los métodos en ResultSet, CallableStatement (discutiremos esto en la


próxima receta), y las interfaces PreparedStatement permiten que una aplicación
almacene y acceda al valor almacenado de varias maneras, algunas de ellas a través de
setters y getters de los objetos correspondientes, mientras que otras como bytes[],
o como una secuencia binaria, de caracteres o ASCII.

Cómo hacerlo...
Cada base de datos tiene su forma específica de almacenar un LOB. En el caso de
PostgreSQL, Blob está generalmente asignado al tipo de datos OIDo BYTEA,
mientras que Cloby NClob se asignan al tipo TEXT . Para demostrar cómo hacer esto,
creemos tablas que puedan almacenar cada uno de los tipos de objetos
grandes. Escribiremos un nuevo método que cree tablas mediante programación:

void execute (String sql) {


try (Connection conn = getDbConnection ()) {
try (PreparedStatement st = conn.prepareStatement (sql)) {
st.execute () ;
}
} catch (Exception ex) {
ex.printStackTrace () ;
}
}

Ahora, podemos crear tres tablas:

execute("create table images (id integer, image bytea)");


execute("create table lobs (id integer, lob oid)");
execute("create table texts (id integer, text text)");

Consulte las interfaces JDBC PreparedStatement y ResultSet y se dará cuenta


de los setters y getters para los objetos -
get/setBlob() , get/setClob(), get/setNClob(), get/setBytes()- y lo
s métodos
que utilizan InputStreamyReader - get/setBinaryStream(), get/setAsci

pág. 295
iStream()o get/setCharacterStream(). La gran ventaja de los métodos de
transmisión es que mueven datos entre la base de datos y la fuente sin almacenar todo
el LOB en la memoria.

Sin embargo, los establecedores y captadores del objeto están más cerca de que nuestro
corazón esté en línea con la codificación orientada a objetos. Entonces comenzaremos
con ellos, usando objetos que no son demasiado grandes, con fines de
demostración. Esperamos que el siguiente código funcione bien:

try (Connection conn = getDbConnection()) {


String sql = "insert into images (id, image) values(?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
File file =
new File("src/main/java/com/packt/cookbook/ch06_db/image1.png");
FileInputStream fis = new FileInputStream(file);
Blob blob = conn.createBlob();
OutputStream out = blob.setBinaryStream(1);
int i = -1;
while ((i = fis.read()) != -1) {
out.write(i);
}
st.setBlob(2, blob);
int count = st.executeUpdate();
System.out.println("Update count = " + count);
}
} catch (Exception ex) { ex.printStackTrace(); }

Alternativamente, en el caso de Clob, escribimos este código:

try (Connection conn = getDbConnection()) {


String sql = "insert into texts (id, text) values(?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
File file = new File("src/main/java/com/packt/cookbook/" +
"ch06_db/Chapter06Database.java");
Reader reader = new FileReader(file);
st.setClob(2, reader);
int count = st.executeUpdate();
System.out.println("Update count = " + count);
}
} catch (Exception ex) { ex.printStackTrace(); }

Resulta que no todos los métodos que están disponibles en la API JDBC son
implementados por los controladores de todas las bases de datos. Por
ejemplo, createBlob()parece funcionar bien para Oracle y MySQL, pero en el caso
de PostgreSQL, obtenemos esto:

Para Clob, obtenemos lo siguiente:

pág. 296
También podemos intentar recuperar un objeto a ResultSet través del getter:

String sql = "select image from images";


try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
try(ResultSet rs = st.executeQuery()){
while (rs.next()){
Blob blob = rs.getBlob(1);
System.out.println("blob length = " + blob.length());
}
}
}

El resultado será el siguiente:

Aparentemente, conocer la API JDBC no es suficiente; También debe leer la


documentación de la base de datos. Esto es lo que la documentación para PostgreSQL
( https://jdbc.postgresql.org/documentation/80/binary-
data.html ) tiene que decir sobre el manejo de LOB:

"Para utilizar el tipo de datos BYTEA se debe utilizar simplemente


las getBytes(), setBytes(), getBinaryStream(),
o setBinaryStream()métodos.
Para utilizar la funcionalidad de objeto grande puede utilizar la LargeObjectclase
proporcionada por el controlador JDBC de PostgreSQL, o mediante el uso de
la getBLOB()y setBLOB()métodos".

Además, debe acceder a objetos grandes dentro de un bloque de transacciones


SQL. Puede iniciar un bloqueo de transacción llamando setAutoCommit(false).

Sin conocer tales detalles, descubrir una forma de manejar los LOB requeriría mucho
tiempo y causaría mucha frustración.

Cuando se trata de LOB, se prefieren los métodos de transmisión porque transfieren


datos directamente desde la fuente a la base de datos (o al revés) y no consumen
memoria tanto como los configuradores y captadores (que tienen que cargar todo el
LOB en memoria primero). Aquí está el código que se transmite Blob en y desde la
base de datos PostgreSQL:

pág. 297
traverseRS("select * from images");
System.out.println();
try (Connection conn = getDbConnection()) {
String sql = "insert into images (id, image) values(?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
File file =
new File("src/main/java/com/packt/cookbook/ch06_db/image1.png");
FileInputStream fis = new FileInputStream(file);
st.setBinaryStream(2, fis);
int count = st.executeUpdate();
System.out.println("Update count = " + count);
}
sql = "select image from images where id = ?";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
try(ResultSet rs = st.executeQuery()){
while (rs.next()){
try(InputStream is = rs.getBinaryStream(1)){
int i;
System.out.print("ints = ");
while ((i = is.read()) != -1) {
System.out.print(i);
}
}
}
}
}
} catch (Exception ex) { ex.printStackTrace(); }
System.out.println();
traverseRS("select * from images");

Veamos el resultado. Hemos cortado la captura de pantalla


arbitrariamente en el lado derecho; de lo contrario, es demasiado largo
horizontalmente:

Otra forma de procesar la imagen recuperada es usar byte[]:


try (Connection conn = getDbConnection()) {
String sql = "insert into images (id, image) values(?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
File file =
new File("src/main/java/com/packt/cookbook/ch06_db/image1.png");
FileInputStream fis = new FileInputStream(file);
byte[] bytes = fis.readAllBytes();
st.setBytes(2, bytes);
int count = st.executeUpdate();

pág. 298
System.out.println("Update count = " + count);
}
sql = "select image from images where id = ?";
System.out.println();
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
try(ResultSet rs = st.executeQuery()){
while (rs.next()){
byte[] bytes = rs.getBytes(1);
System.out.println("bytes = " + bytes);
}
}
}
} catch (Exception ex) { ex.printStackTrace(); }

PostgreSQL limita el tamaño BYTEA a 1 GB. Los objetos binarios más grandes se
pueden almacenar como el tipo de datos del identificador de objeto ( OID ):

traverseRS("select * from lobs");


System.out.println();
try (Connection conn = getDbConnection()) {
conn.setAutoCommit(false);
LargeObjectManager lobm =
conn.unwrap(org.postgresql.PGConnection.class)
.getLargeObjectAPI();
long lob = lobm.createLO(LargeObjectManager.READ
| LargeObjectManager.WRITE);
LargeObject obj = lobm.open(lob, LargeObjectManager.WRITE);
File file =
new File("src/main/java/com/packt/cookbook/ch06_db/image1.png");
try (FileInputStream fis = new FileInputStream(file)){
int size = 2048;
byte[] bytes = new byte[size];
int len = 0;
while ((len = fis.read(bytes, 0, size)) > 0) {
obj.write(bytes, 0, len);
}
obj.close();
String sql = "insert into lobs (id, lob) values(?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
st.setLong(2, lob);
st.executeUpdate();
}
}
conn.commit();
} catch (Exception ex) { ex.printStackTrace(); }
System.out.println();
traverseRS("select * from lobs");

El resultado será el siguiente:

pág. 299
Tenga en cuenta que la selectdeclaración devuelve un valor largo de
la lob columna. Esto se debe a que la OIDcolumna no almacena el valor en sí como
lo BYTEAhace. En su lugar, almacena la referencia al objeto que se almacena en otro
lugar de la base de datos. Tal disposición hace que eliminar la fila con el tipo OID no
sea tan sencillo como esto:

execute("delete from lobs where id = 100");

Si hace exactamente eso, deja al objeto real como un huérfano que continúa
consumiendo espacio en disco. Para evitar este problema, unlink primero debe ir al
LOB ejecutando el siguiente comando:

execute("select lo_unlink((select lob from lobs " + " where id=100))");

Solo después de esto puede ejecutar los delete from lobs where id =
100 comandos de forma segura .

Si olvidó hacerlo unlinkprimero, o si creó un LOB huérfano accidentalmente (debido


a un error en el código, por ejemplo), hay una manera de encontrar huérfanos en las
tablas del sistema. Nuevamente, la documentación de la base de datos debe
proporcionarle instrucciones sobre cómo hacer esto. En el caso de PostgreSQL v.9.3 o
posterior, puede verificar si tiene un LOB huérfano ejecutando el select count(*)
from pg_largeobject comando. Si devuelve un recuento que es mayor que 0,
puede eliminar todos los huérfanos con la siguiente unión (suponiendo que
la lobstabla sea la única que puede contener una referencia a un LOB):

SELECT lo_unlink(pgl.oid) FROM pg_largeobject_metadata pgl


WHERE (NOT EXISTS (SELECT 1 FROM lobs ls" + "WHERE ls.lob = pgl.oid));

Este es el precio que hay que pagar por almacenar un LOB en una base de datos.

Vale la pena señalar que, aunque BYTEAno requiere tanta complejidad durante la
operación de eliminación, tiene un tipo diferente de sobrecarga. De acuerdo con la
documentación de PostgreSQL, cuando cerca de 1 GB, que w ould requiere una enorme
cantidad de memoria para procesar un valor tan grande.

Para leer datos de LOB, puede usar el siguiente código:

try (Connection conn = getDbConnection()) {


conn.setAutoCommit(false);

pág. 300
LargeObjectManager lobm =
conn.unwrap(org.postgresql.PGConnection.class)
.getLargeObjectAPI();
String sql = "select lob from lobs where id = ?";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
try(ResultSet rs = st.executeQuery()){
while (rs.next()){
long lob = rs.getLong(1);
LargeObject obj = lobm.open(lob, LargeObjectManager.READ);
byte[] bytes = new byte[obj.size()];
obj.read(bytes, 0, obj.size());
System.out.println("bytes = " + bytes);
obj.close();
}
}
}
conn.commit();
} catch (Exception ex) { ex.printStackTrace(); }

Alternativamente, es posible usar un código más simple al obtener Blob


directamente del objeto ResultSet si el LOB no es demasiado grande:

while (rs.next()){
Blob blob = rs.getBlob(1);
byte[] bytes = blob.getBytes(1, (int)blob.length());
System.out.println("bytes = " + bytes);
}

Para almacenar Clob en PostgreSQL, puede usar el mismo código que el


anterior. Mientras lee desde la base de datos, puede convertir bytes en un
tipo String de datos o algo similar (nuevamente, si el LOB no es demasiado grande):

String str = new String(bytes, Charset.forName("UTF-8"));


System.out.println("bytes = " + str);

Sin embargo, Clob en PostgreSQL se puede almacenar directamente como un tipo de


datos TEXT de tamaño ilimitado. Este código lee el archivo donde se escribe este
código y lo almacena / recupera en / de la base de datos:

traverseRS("select * from texts");


System.out.println();
try (Connection conn = getDbConnection()) {
String sql = "insert into texts (id, text) values(?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
File file = new File("src/main/java/com/packt/cookbook/ch06_db/"
+ "Chapter06Database.java");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] bytes = fis.readAllBytes();
st.setString(2, new String(bytes, Charset.forName("UTF-8")));
}
int count = st.executeUpdate();
System.out.println("Update count = " + count);
}

pág. 301
sql = "select text from texts where id = ?";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
try(ResultSet rs = st.executeQuery()){
while (rs.next()) {
String str = rs.getString(1);
System.out.println(str);
}
}
}
} catch (Exception ex) { ex.printStackTrace(); }

El resultado será el siguiente ( hemos mostrado solo las primeras líneas de la salida) :

Para objetos más grandes, los métodos de transmisión serían una mejor opción (si no
la única):

traverseRS("select * from texts");


System.out.println();
try (Connection conn = getDbConnection()) {
String sql = "insert into texts (id, text) values(?, ?)";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
File file = new File("src/main/java/com/packt/cookbook/ch06_db/"
+ "Chapter06Database.java");
//This is not implemented:
//st.setCharacterStream(2, reader, file.length());
st.setCharacterStream(2, reader, (int)file.length());

int count = st.executeUpdate();


System.out.println("Update count = " + count);
}
} catch (Exception ex) { ex.printStackTrace(); }
System.out.println();
traverseRS("select * from texts");

Tenga en cuenta que, al momento de escribir este


libro, setCharacterStream(int, Reader, long)no está implementado,
aunque setCharacterStream(int, Reader, int)funciona bien.

También podemos leer el archivo de la textstabla como una secuencia de caracteres


y limitarlo a los primeros 160 caracteres:

pág. 302
String sql = "select text from texts where id = ?";
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.setInt(1, 100);
try(ResultSet rs = st.executeQuery()){
while (rs.next()) {
try(Reader reader = rs.getCharacterStream(1)) {
char[] chars = new char[160];
reader.read(chars);
System.out.println(chars);
}
}
}
}

El resultado será el siguiente:

Hay más...
Aquí hay otra recomendación de la documentación de PostgreSQL (puede acceder a ella
en https://jdbc.postgresql.org/documentation/80/binary-data.html ):

"El tipo de datos BYTEA no se adapta bien para el almacenamiento de cantidades muy
grandes de datos binarios. Mientras que una columna de tipo BYTEA puede contener
hasta 1 GB de datos binaria, que requeriría una enorme cantidad de memoria para
procesar un valor tan grande.
La El método de objetos grandes para almacenar datos binarios es más adecuado para
almacenar valores muy grandes, pero tiene sus propias limitaciones. Eliminar
específicamente una fila que contiene una referencia de objeto grande no elimina el
objeto grande. Eliminar el objeto grande es una operación separada que necesita "Los
objetos grandes también tienen algunos problemas de seguridad, ya que cualquier
persona conectada a la base de datos puede ver y / o modificar cualquier objeto grande,
incluso si no tienen permisos para ver / actualizar la fila que contiene la referencia del
objeto grande".

Al decidir almacenar LOB en una base de datos, debe recordar que cuanto más grande
es la base de datos, más difícil es mantenerla. La velocidad de acceso, la principal
ventaja de elegir una base de datos como instalación de almacenamiento, también
disminuye y no es posible crear índices para los tipos de LOB para mejorar la

pág. 303
búsqueda. Además, no puede usar columnas LOB en una cláusula WHERE, excepto en
algunos casos CLOB, ni usar columnas LOB en varias filas INSERT o UPDATE.

Entonces, antes de pensar en una base de datos para un LOB, siempre debe considerar
si almacenar el nombre de un archivo, palabras clave y algunas otras propiedades de
contenido en la base de datos sería suficiente para la solución.

Ejecutar procedimientos
almacenados
En esta receta, aprenderá cómo ejecutar un procedimiento almacenado en la base de
datos desde un programa Java.

Prepararse
De vez en cuando, un programador de Java encuentra la necesidad de manipular y / o
seleccionar datos en / de varias tablas, por lo que el programador presenta un conjunto
de complejas declaraciones SQL que no son prácticas para implementar en Java o se
sospecha que la implementación de Java podría no producir un rendimiento
adecuado. Esto es cuando el conjunto de instrucciones SQL se puede incluir en un
procedimiento almacenado que se compila y almacena en la base de datos y luego se
invoca a través de la interfaz JDBC. O, en otro giro del destino, un programador de Java
podría encontrar la necesidad de incorporar una llamada a un procedimiento
almacenado existente en el programa. Para lograr esto, se puede usar la
interfaz CallableStatement (que extiende la PreparedStatementinterfaz),
aunque algunas bases de datos le permiten llamar a un procedimiento almacenado
usando una interfaz Statemento a PreparedStatement.

CallableStatement puede tener parámetros de tres tipos: para un valor de


entrada IN, OUT para el resultado y IN OUT para un valor de entrada o de
salida. Los parámetros OUT deben registrarse por el método
registerOutParameter() de CallableStatement. Los parámetros IN se
pueden configurar de la misma manera que los parámetros
de PreparedStatement.

Tenga en cuenta que ejecutar un procedimiento almacenado desde Java mediante


programación es una de las áreas menos estandarizadas. PostgreSQL, por ejemplo, no
admite procedimientos almacenados directamente, pero se pueden invocar como
funciones, que se han modificado para este fin al interpretar los parámetros OUT como
valores de retorno. Oracle, por otro lado, también permite parámetros OUT para
funciones.

pág. 304
Es por eso que las siguientes diferencias entre las funciones de la base de datos y los
procedimientos almacenados solo pueden servir como una guía general y no como una
definición formal:

• Una función tiene un valor de retorno, pero no permite parámetros OUT (excepto
para algunas bases de datos) y puede usarse en una declaración SQL.
• Un procedimiento almacenado no tiene un valor de retorno (a excepción de algunas
bases de datos); permite parámetros OUT(para la mayoría de las bases de datos) y
puede ejecutarse utilizando la interfaz JDBC CallableStatement.

Es por eso que es muy importante leer la documentación de la base de datos para
aprender cómo ejecutar un procedimiento almacenado.

Como los procedimientos almacenados se compilan y almacenan en el servidor de la


base de datos, el execute() método de CallableStatement funciona mejor para
la misma instrucción SQL que el método correspondiente
de Statemento PreparedStatement. Esta es una de las razones por las que una
gran cantidad de código Java a veces se reemplaza por uno o varios procedimientos
almacenados que incluyen incluso la lógica empresarial. Pero no hay una respuesta
correcta para cada caso y problema, por lo que nos abstendremos de hacer
recomendaciones específicas, excepto para repetir el mantra familiar sobre el valor de
las pruebas y la claridad del código que está escribiendo.

Cómo hacerlo...
Como en la receta anterior, continuaremos usando la base de datos PostgreSQL con
fines de demostración. Antes de escribir instrucciones SQL personalizadas, funciones y
procedimientos almacenados, primero debe mirar la lista de funciones ya
existentes. Por lo general, proporcionan una gran cantidad de funcionalidades.

Aquí hay un ejemplo de llamar a la función replace(string text, from text,


to text) que busca el primer parámetro ( string text) y la reemplaza con todas
las subcadenas que coinciden con el segundo parámetro ( from text ) con la
subcadena proporcionada por el tercer parámetro ( string text):

String sql = "{ ? = call replace(?, ?, ? ) }";


try (CallableStatement st = conn.prepareCall(sql)) {
st.registerOutParameter(1, Types.VARCHAR);
st.setString(2, "Hello, World! Hello!");
st.setString(3, "llo");
st.setString(4, "y");
st.execute();
String res = st.getString(1);
System.out.println(res);
}

pág. 305
El resultado es el siguiente:

Incorporaremos esta función a nuestras funciones personalizadas y procedimientos


almacenados para mostrarle cómo se puede hacer esto.

Un procedimiento almacenado puede estar sin ningún parámetro, solo IN con OUT
parámetros, solo con parámetros o con ambos. El resultado puede ser uno o varios
valores, o un objeto ResultSet. Aquí hay un ejemplo de cómo crear un
procedimiento almacenado sin ningún parámetro en PostgreSQL:

execute("create or replace function createTableTexts() "


+ " returns void as "
+ "$$ drop table if exists texts; "
+ " create table texts (id integer, text text); "
+ "$$ language sql");

En el código anterior, usamos el método execute(), con el que ya estamos


familiarizados:

void execute(String sql){


try (Connection conn = getDbConnection()) {
try (PreparedStatement st = conn.prepareStatement(sql)) {
st.execute();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}

Este procedimiento almacenado (siempre es una función en PostgreSQL) crea la tabla


texts (después de descartarla si la tabla ya existía). Puede encontrar la sintaxis del
SQL para la creación de funciones en la documentación de la base de datos. Lo único
que nos gustaría comentar aquí es que, en lugar del símbolo $$que denota el cuerpo de
la función, puede usar comillas simples. Sin embargo, preferimos , porque ayuda a
evitar el escape de comillas simples si necesitamos incluirlas en el cuerpo de
la función .

Después de ser creado y almacenado en la base de datos, el procedimiento puede ser


invocado por CallableStatement:

String sql = "{ call createTableTexts() }";


try (CallableStatement st = conn.prepareCall(sql)) {
st.execute();
}

Alternativamente, se puede invocar con la instrucción SQL select


createTableTexts() o select * from createTableTexts(). Ambas

pág. 306
declaraciones devuelven un objeto ResultSet (que es nullen el caso de
la función createTableTexts()), por lo que podemos atravesarlo por nuestro
método:

void traverseRS(String sql){


System.out.println("traverseRS(" + sql + "):");
try (Connection conn = getDbConnection()) {
try (Statement st = conn.createStatement()) {
try(ResultSet rs = st.executeQuery(sql)){
int cCount = 0;
Map<Integer, String> cName = new HashMap<>();
while (rs.next()) {
if (cCount == 0) {
ResultSetMetaData rsmd = rs.getMetaData();
cCount = rsmd.getColumnCount();
for (int i = 1; i <= cCount; i++) {
cName.put(i, rsmd.getColumnLabel(i));
}
}
List<String> l = new ArrayList<>();
for (int i = 1; i <= cCount; i++) {
l.add(cName.get(i) + " = " + rs.getString(i));
}
System.out.println(l.stream()
.collect(Collectors.joining(", ")));
}
}
}
} catch (Exception ex) { ex.printStackTrace(); }
}

Ya hemos usado este método en las recetas anteriores.

La función se puede eliminar mediante la siguiente instrucción:

drop function if exists createTableTexts();

Ahora, juntemos todo esto en código Java, creemos una función e invoquémosla en
tres estilos diferentes:

execute("create or replace function createTableTexts() "


+ "returns void as "
+ "$$ drop table if exists texts; "
+ " create table texts (id integer, text text); "
+ "$$ language sql");
String sql = "{ call createTableTexts() }";
try (Connection conn = getDbConnection()) {
try (CallableStatement st = conn.prepareCall(sql)) {
st.execute();
}
}
traverseRS("select createTableTexts()");
traverseRS("select * from createTableTexts()");
execute("drop function if exists createTableTexts()");

pág. 307
El resultado es el siguiente:

Como se esperaba, el ResultSetobjeto devuelto es null. Tenga en cuenta que el


nombre de la función no distingue entre mayúsculas y minúsculas. Lo mantenemos en
estilo de camello solo para legibilidad humana.

Ahora, creemos y llamemos a otro procedimiento almacenado (función) con dos


parámetros de entrada:

execute("create or replace function insertText(int,varchar)"


+ " returns void "
+ " as $$ insert into texts (id, text) "
+ " values($1, replace($2,'XX','ext'));"
+ " $$ language sql");
String sql = "{ call insertText(?, ?) }";
try (Connection conn = getDbConnection()) {
try (CallableStatement st = conn.prepareCall(sql)) {
st.setInt(1, 1);
st.setString(2, "TXX 1");
st.execute();
}
}
execute("select insertText(2, 'TXX 2')");
traverseRS("select * from texts");
execute("drop function if exists insertText()");

En el cuerpo de la función, los parámetros de entrada se denominan por su posición


como $1y $2. Como se mencionó anteriormente, también utilizamos la función
replace()incorporada para manipular los valores del segundo parámetro de entrada
antes de insertarlo en la tabla. El procedimiento almacenado recién creado se llama dos
veces: primero a través CallableStatment y luego a través
del execute() método, con diferentes valores de entrada. Luego, miramos dentro de
la tabla usando traverseRS("select * from texts")y soltamos la función
recién creada para realizar una limpieza. Eliminamos la función solo para esta
demostración. En el código de la vida real, la función, una vez creada, permanece y
aprovecha estar allí, compilada y lista para ejecutarse.

Si ejecutamos el código anterior, obtendremos el siguiente resultado:

pág. 308
Ahora agreguemos dos filas a la tabla texts y luego investiguemos y creemos un
procedimiento almacenado (función) que cuente el número de filas en la tabla y
devuelva el resultado:

execute("insert into texts (id, text) "


+ "values(3,'Text 3'),(4,'Text 4')");
traverseRS("select * from texts");
execute("create or replace function countTexts() "
+ "returns bigint as "
+ "$$ select count(*) from texts; "
+ "$$ language sql");
String sql = "{ ? = call countTexts() }";
try (Connection conn = getDbConnection()) {
try (CallableStatement st = conn.prepareCall(sql)) {
st.registerOutParameter(1, Types.BIGINT);
st.execute();
System.out.println("Result of countTexts() = " + st.getLong(1));
}
}
traverseRS("select countTexts()");
traverseRS("select * from countTexts()");
execute("drop function if exists countTexts()");

Tenga en cuenta el valor bigint del valor devuelto y el tipo de coincidencia para
el parámetro OUT Types.BIGINT. El procedimiento almacenado recién creado se
ejecuta tres veces y luego se elimina. El resultado es el siguiente:

Ahora, veamos un ejemplo de un procedimiento almacenado con un parámetro de


entrada del tipo int que devuelve el objeto ResultSet :

execute("create or replace function selectText(int) "


+ "returns setof texts as
+ "$$ select * from texts where id=$1; "
+ "$$ language sql");
traverseRS("select selectText(1)");
traverseRS("select * from selectText(1)");
execute("drop function if exists selectText(int)");

Tenga en cuenta el tipo de retorno definido como setof texts, donde texts es
el nombre de la tabla. Si ejecutamos el código anterior, el resultado será el siguiente:

pág. 309
Vale la pena analizar la diferencia en el contenido ResultSetde dos llamadas
diferentes al procedimiento almacenado. Sin select *, contiene el nombre del
procedimiento y el objeto devuelto (del ResultSet tipo). Pero con select *,
devuelve el ResultSet contenido real de la última instrucción SQL en el
procedimiento.

Naturalmente, surge la pregunta de por qué no podríamos llamar a este


procedimiento almacenado de la CallableStatement siguiente manera:

String sql = "{ ? = call selectText(?) }";


try (CallableStatement st = conn.prepareCall(sql)) {
st.registerOutParameter(1, Types.OTHER);
st.setInt(2, 1);
st.execute();
traverseRS((ResultSet)st.getObject(1));
}

Lo intentamos, pero no funcionó. Esto es lo que la documentación de PostgreSQL tiene


que decir al respecto:

"Las funciones que devuelven datos como un conjunto no deberían llamarse a través de la
interfaz CallableStatement, sino que deberían usar las interfaces normales de Statement
o PreparedStatement".

Sin embargo, hay una forma de evitar esta limitación. La misma documentación de la
base de datos describe cómo recuperar un valor refcursor (una característica
específica de PostgreSQL) que luego se puede convertir a ResultSet:

execute("create or replace function selectText(int) "


+ "returns refcursor " +
+ "as $$ declare curs refcursor; "
+ " begin "
+ " open curs for select * from texts where id=$1;"
+ " return curs; "
+ " end; "
+ "$$ language plpgsql");
String sql = "{ ? = call selectText(?) }";
try (Connection conn = getDbConnection()) {
conn.setAutoCommit(false);
try(CallableStatement st = conn.prepareCall(sql)){
st.registerOutParameter(1, Types.OTHER);
st.setInt(2, 2);
st.execute();
try(ResultSet rs = (ResultSet) st.getObject(1)){
System.out.println("traverseRS(refcursor()=>rs):");
traverseRS(rs);
}

pág. 310
}
}
traverseRS("select selectText(2)");
traverseRS("select * from selectText(2)");
execute("drop function if exists selectText(int)");

Algunos comentarios sobre el código anterior probablemente lo ayudarán a


comprender cómo se hizo esto:

• La confirmación automática debe estar desactivada.


• Dentro de la función, se $1refiere al primer parámetro IN (sin contar el parámetro
OUT).
• El lenguaje está configurado para plpgsql acceder a la funcionalidad
refcursor (PL / pgSQL es un lenguaje de procedimiento cargable de la base de
datos PostgreSQL).
• Para atravesar ResultSet, escribimos un nuevo método, como sigue:

void traverseRS(ResultSet rs) throws Exception {


int cCount = 0;
Map<Integer, String> cName = new HashMap<>();
while (rs.next()) {
if (cCount == 0) {
ResultSetMetaData rsmd = rs.getMetaData();
cCount = rsmd.getColumnCount();
for (int i = 1; i <= cCount; i++) {
cName.put(i, rsmd.getColumnLabel(i));
}
}
List<String> l = new ArrayList<>();
for (int i = 1; i <= cCount; i++) {
l.add(cName.get(i) + " = " + rs.getString(i));
}
System.out.println(l.stream()
.collect(Collectors.joining(", ")));
}
}

• Entonces, nuestro viejo amigo, el método traverseRS(String


sql) ahora se puede refactorizar en la siguiente forma:

void traverseRS(String sql){


System.out.println("traverseRS(" + sql + "):");
try (Connection conn = getDbConnection()) {
try (Statement st = conn.createStatement()) {
try(ResultSet rs = st.executeQuery(sql)){
traverseRS(rs);
}
}
} catch (Exception ex) { ex.printStackTrace(); }
}

Si ejecutamos el último ejemplo, el resultado será el siguiente:

pág. 311
Puede ver que los métodos de recorrido de resultados que no extraen un objeto y lo
convierten para ResultSet que no muestren los datos correctos en este caso.

Hay más...
Cubrimos los casos más populares de llamar a procedimientos almacenados desde
código Java. El alcance de este libro no nos permite presentar formas más complejas y
potencialmente útiles de procedimientos almacenados en PostgreSQL y otras bases de
datos. Sin embargo, nos gustaría mencionarlos aquí, para que tenga una idea de otras
posibilidades:

• Funciones en tipos compuestos


• Funciones con nombres de parámetros.
• Funciones con números variables de argumentos.
• Funciones con valores predeterminados para argumentos
• Funciona como fuentes de tabla.
• Funciones que devuelven tablas
• Funciones polimórficas de SQL
• Funciones con colaciones

Uso de operaciones por lotes para


un gran conjunto de datos
En esta receta, aprenderá a crear y ejecutar muchos lotes de instrucciones SQL con
una sola llamada a una base de datos.

Prepararse
El procesamiento por lotes es necesario cuando se deben ejecutar muchas instrucciones
SQL al mismo tiempo para insertar, actualizar o leer registros de la base de datos. La
ejecución de varias instrucciones SQL se puede realizar iterando sobre ellas y enviando
cada una a la base de datos una por una, pero incurre en una sobrecarga de red que se
puede evitar enviando todas las consultas a la base de datos al mismo tiempo.

pág. 312
Para evitar esta sobrecarga de la red, todas las declaraciones SQL se pueden combinar
en un String valor, y cada declaración está separada por un punto y coma, por lo que
todas se pueden enviar a la base de datos en una sola llamada. El resultado devuelto, si
está presente, también se devuelve como una colección de conjuntos de resultados
generados por cada instrucción. Tal procesamiento generalmente se denomina
procesamiento masivo para distinguirlo de un procesamiento por lotes que está
disponible solo para declaraciones INSERT y UPDATE . El procesamiento por lotes le
permite combinar muchas instrucciones SQL utilizando el método addBatch()de la
interfaz java.sql.Statemento java.sql.PreparedStatement.

Utilizaremos la base de datos PostgreSQL y la siguiente tabla person, para insertar,


actualizar y leer datos de ella:

create table person (


name VARCHAR NOT NULL,
age INTEGER NOT NULL
)

Como puede ver, cada registro de la tabla puede contener dos atributos de una
persona : nombre y edad.

Cómo hacerlo...
Vamos a demostrar tanto un procesamiento a granel y el procesamiento por
lotes . Para lograrlo, sigamos estos pasos:

1. Un ejemplo de procesamiento masivo es una sola INSERT declaración con


múltiples cláusulas VALUES:

INSERT into <table_name> (column1, column2, ...) VALUES


( value1, value2, ...),
( value1, value2, ...),
...
( value1, value2, ...)

El código que construye dicha declaración se ve de la siguiente manera:

int n = 100000; //number of records to insert


StringBuilder sb =
new StringBuilder("insert into person (name,age) values ");
for(int i = 0; i < n; i++){
sb.append("(")
.append("'Name").append(String.valueOf(i)).append("',")
.append(String.valueOf((int)(Math.random() * 100)))
.append(")");
if(i < n - 1) {
sb.append(",");
}
}

pág. 313
try(Connection conn = getConnection();
Statement st = conn.createStatement()){
st.execute(sb.toString());
} catch (SQLException ex){
ex.printStackTrace();
}

Como puede ver, el código anterior construye una declaración con


100,000 VALUEScláusulas, lo que significa que inserta 100,000 registros en un viaje a
una base de datos. En nuestro experimento, tomó 1,082 milisegundos completar este
trabajo. Como resultado, la tabla personahora contiene 100,000 registros de personas
con nombres de Name0 hasta Name99999y edad como un número aleatorio del 1 al 99
inclusive.

Hay dos desventajas de este método de procesamiento masivo : es susceptible al ataque


de inyección SQL y puede consumir demasiada memoria. La inyección de SQL se puede
abordar mediante el uso PreparedStatement, pero está limitada por el número de
variables de enlace. En el caso de PostgreSQL, no puede ser más que 32767. Esto
significa que necesitaríamos dividir el single PreparedStatement en varios más
pequeños, cada uno de los cuales no tiene más que 32767variables de enlace. Por
cierto, también abordará el problema del consumo de memoria, ya que cada declaración
es ahora mucho más pequeña que la grande. La declaración única anterior, por ejemplo,
incluye 200,000 valores.

2. El siguiente código soluciona ambos problemas al dividir la única instrucción SQL


en objetos PreparedStatement más pequeños , cada uno con no más
que 32766variables de enlace:

int n = 100000, limit = 32766, l = 0;


List<String> queries = new ArrayList<>();
List<Integer> bindVariablesCount = new ArrayList<>();
String insert = "insert into person (name, age) values ";
StringBuilder sb = new StringBuilder(insert);
for(int i = 0; i < n; i++){
sb.append("(?, ?)");
l = l + 2;
if(i == n - 1) {
queries.add(sb.toString());
bindVariablesCount.add(l % limit);
}
if(l % limit == 0) {
queries.add(sb.toString());
bindVariablesCount.add(limit);
sb = new StringBuilder(insert);
} else {
sb.append(",");
}
}
try(Connection conn = getConnection()) {
int i = 0, q = 0;
for(String query: queries){
try(PreparedStatement pst = conn.prepareStatement(query)) {

pág. 314
int j = 0;
while (j < bindVariablesCount.get(q)) {
pst.setString(++j, "Name" + String.valueOf(i++));
pst.setInt(++j, (int)(Math.random() * 100));
}
pst.executeUpdate();
q++;
}
}
} catch (SQLException ex){
ex.printStackTrace();
}

El código anterior se ejecuta tan rápido como nuestro ejemplo anterior. Se necesitaron
1.175 milisegundos para completar este trabajo. Pero ejecutamos este código en la
misma computadora donde está instalada la base de datos, por lo que no hay sobrecarga
de la red de los siete viajes a la base de datos (esa fue la cantidad de consultas que se
agregaron List queries). Pero, como puede ver, el código es bastante complejo. Se
puede simplificar sustancialmente mediante el procesamiento por lotes.

3. El procesamiento por lotes se basa en el uso de los


métodos addBatch()y executeBatch() , que están disponibles tanto
en interfaces Statement como en interfaces PreparedStatement. Para nuestra
demostración, vamos a utilizar PreparedStatementpor dos razones: no es susceptible a
la inyección de SQL y funciona mejor cuando se ejecuta muchas veces (que es el objetivo
principal de PreparedStatement - para aprovechar las múltiples ejecuciones de la
misma declaración con valores diferentes):

int n = 100000;
String insert =
"insert into person (name, age) values (?, ?)";
try (Connection conn = getConnection();
PreparedStatement pst = conn.prepareStatement(insert)) {
for (int i = 0; i < n; i++) {
pst.setString(1, "Name" + String.valueOf(i));
pst.setInt(2, (int)(Math.random() * 100));
pst.addBatch();
}
pst.executeBatch();
} catch (SQLException ex) {
ex.printStackTrace();
}

Se necesitaron 2.299 milisegundos para insertar 100.000 registros en la tabla


person, que es casi el doble de tiempo en comparación con el uso de una sola
declaración con múltiples cláusulas VALUES (el primer ejemplo) o el uso de
múltiples objetos PreparedStatement (el segundo ejemplo). Aunque su ejecución
lleva más tiempo, este código tiene la ventaja obvia de ser mucho más simple. Y envía
el lote de declaraciones a la base de datos en un solo viaje, lo que significa que la
brecha en el rendimiento entre esta implementación y la anterior (con siete viajes a la
base de datos) será menor cuando la base de datos no esté colocada con la aplicación.

pág. 315
Pero esta implementación también se puede mejorar.

4. Para mejorar el procesamiento por lotes, agreguemos


la reWriteBatchedInserts propiedad al objeto DataSource y configurémoslo
en true:

DataSource createDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setPoolName("cookpool");
ds.setDriverClassName("org.postgresql.Driver");
ds.setJdbcUrl("jdbc:postgresql://localhost/cookbook");
ds.setUsername( "cook");
//ds.setPassword("123Secret");
ds.setMaximumPoolSize(2);
ds.setMinimumIdle(2);
ds.addDataSourceProperty("reWriteBatchedInserts",
Boolean.TRUE);
return ds;
}

Ahora, si ejecutamos el mismo código de procesamiento por lotes usando la


conexión createDataSource().getConnection(), el tiempo que lleva insertar
los mismos 100,000 registros cae a 750 milisegundos, que es un 25% mejor que
cualquiera de las implementaciones que hemos probado hasta ahora. Y el código sigue
siendo mucho más simple que cualquiera de las implementaciones anteriores.

¿Pero qué pasa con el consumo de memoria?

5. A medida que crece el tamaño del lote, en algún momento, JVM puede quedarse sin
memoria. En tal caso, el procesamiento por lotes puede dividirse en varios lotes, cada uno
entregado a la base de datos en un viaje separado:

int n = 100000;
int batchSize = 30000;
boolean execute = false;
String insert =
"insert into person (name, age) values (?, ?)";
try (Connection conn = getConnection();
PreparedStatement pst = conn.prepareStatement(insert)) {
for (int i = 0; i < n; i++) {
pst.setString(1, "Name" + String.valueOf(i));
pst.setInt(2, (int)(Math.random() * 100));
pst.addBatch();
if((i > 0 && i % batchSize == 0) ||
(i == n - 1 && execute)) {
pst.executeBatch();
System.out.print(" " + i);
//prints: 30000 60000 90000 99999
if(n - 1 - i < batchSize && !execute){
execute = true;
}
}
}
pst.executeBatch();

pág. 316
} catch (SQLException ex) {
ex.printStackTrace();
}

Usamos la variable the execute como el indicador que indica que necesitamos
llamar executeBatch()una vez más cuando se agrega la última instrucción al lote si
este último lote es menor que el valor batchSize. Como puede ver en el comentario
al código anterior, executeBatch() se llamó cuatro veces, incluso cuando se agregó
la última instrucción (cuándo i=99999). El rendimiento de este código en nuestras
ejecuciones fue el mismo que sin generar múltiples lotes, porque nuestra base de datos
se encuentra en la misma computadora que la aplicación. De lo contrario, la entrega de
cada lote a través de la red aumentaría el tiempo que llevó ejecutar este código.

Cómo funciona...
El último ejemplo (paso 5) de la subsección anterior es una implementación final de un
proceso por lotes que puede usarse para insertar y actualizar registros en una base de
datos. El método executeBatch()devuelve una matriz de int, que, en caso de éxito,
indica cuántas filas se actualizaron por cada una de las declaraciones en el lote. En el
caso de una INSERTdeclaración, este valor es igual a -2 (negativo dos), que es el valor
de la constante estática Statement.SUCCESS_NO_INFO. El valor de -3 (negativo
tres), que es el valor de la constante Statement.EXECUTE_FAILED, indica un fallo
en la declaración.

Si se espera que el recuento de filas actualizado devuelto sea mayor


que Integer.MAX_VALUE, use el método long[] executeLargeBatch()para
ejecutar el lote.

No hay procesamiento por lotes para leer datos de la base de datos. Para leer datos en
masa, puede enviar muchas declaraciones separadas por un punto y coma como una
cadena a la base de datos y luego iterar sobre los conjuntos de resultados múltiples
devueltos. Por ejemplo, enviemos declaraciones SELECT que cuenten el número de
registros para cada uno de los valores de edad del 1 al 99 inclusive:

int minAge = 0, maxAge = 0, minCount = n, maxCount = 0;


StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append("select count(*) from person where age = ")
.append(i).append(";");
}
try (Connection conn = getConnection();
PreparedStatement pst = conn.prepareStatement(sb.toString())) {
boolean hasResult = pst.execute();
int i = 0;
while (hasResult){
try (ResultSet rs = pst.getResultSet()) {
rs.next();
int c = rs.getInt(1);

pág. 317
if(c < minCount) {
minAge = i;
minCount = c;
}
if(c > maxCount) {
maxAge = i;
maxCount = c;
}
i++;
hasResult = pst.getMoreResults();
}
}
} catch (SQLException ex) {
ex.printStackTrace();
}
System.out.println("least popular age=" + minAge + "(" + minCount +
"), most popular age=" + maxAge + "(" + maxCount + ")");

En nuestra ejecución de prueba, tomó 2,162 milisegundos para ejecutar el código


anterior y mostrar el siguiente mensaje:

edad menos popular = 14 (929), edad más popular = 10 (1080)

Hay más...
Mover grandes conjuntos de datos hacia y desde la base de datos PostgreSQL también
se puede hacer usando el comando COPY, que copia datos hacia y desde un
archivo. Puede leer más al respecto en la documentación de la base de datos
( https://www.postgresql.org/docs/current/static/sql-
copy.html ).

Usando MyBatis para operaciones


CRUD
En las recetas anteriores, mientras usábamos JDBC, teníamos el código de escritura, que
extrae los resultados de la consulta de un ResultSet objeto devuelto por la
consulta. La desventaja de este enfoque es que tiene que escribir un poco de código
repetitivo para crear y completar objetos de dominio que representan registros en la
base de datos. Como ya hemos mencionado en la introducción de este capítulo, Existen
varios marcos ORM que pueden hacer esto por usted y crear los objetos de dominio
correspondientes automáticamente (o, en otras palabras, para asignar registros de la
base de datos a los objetos de dominio correspondientes). Naturalmente, cada uno de
esos marcos elimina parte del control y la flexibilidad en la construcción de sentencias
SQL. Entonces, antes de comprometerse con un marco ORM particular, debe investigar
y experimentar con diferentes marcos para encontrar el que proporcione todo lo que la

pág. 318
aplicación necesita con respecto a la base de datos y no genere demasiados gastos
generales.

En esta receta, aprenderá sobre la herramienta MyBatis de Mapper de SQL, que


simplifica la programación de la base de datos en comparación con el uso de JDBC
directamente.

Prepararse
MyBatis es un marco ORM liviano que permite no solo mapear los resultados a objetos
Java sino también ejecutar una sentencia SQL arbitraria. Existen principalmente dos
formas de describir el mapeo:

• Usando anotaciones Java


• Usando la configuración XML

En esta receta, vamos a utilizar la configuración XML. Pero, sea cual sea el estilo que
prefiera, debe crear un objeto del
tipo org.apache.ibatis.session.SqlSessionFactory y luego usarlo para
iniciar una sesión de MyBatis creando un objeto del
tipo org.apache.ibatis.session.SqlSession:

InputStream inputStream = Resources.getResourceAsStream(configuration);


SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();

El SqlSessionFactoryBuilderobjeto tiene nueve


métodos build()sobrecargados que crean el objeto SqlSession. Estos métodos
permiten configurar el entorno de ejecución de SQL. Utilizándolos, puede definir lo
siguiente:

• Si prefiere realizar cambios automáticos en la base de datos o hacerlos


explícitamente (usamos esto último en nuestros ejemplos)
• Si usará la fuente de datos configurada (como en nuestros ejemplos) o usará la
conexión de base de datos proporcionada externamente
• Si utilizará el nivel de aislamiento de transacción específico de la base de datos
predeterminado (como en nuestro ejemplo) o si desea establecer su propio
• ¿Cuál de los siguientes valores ExecutorType usará? SIMPLE
(Predeterminado, crea uno nuevo PreparedStatementpara cada ejecución de
una declaración), REUSE (reutiliza PreparedStatements) o BATCH (agrupa
todas las declaraciones de actualización y las demarca según sea necesario
si SELECT se ejecutan entre ellas)

pág. 319
• En qué entorno ( development, testo production, por ejemplo) se
implementa este código, por lo que se utilizará la sección correspondiente de la
configuración (lo discutiremos en breve)
• El Propertiesobjeto que contiene la configuración del origen de datos.

El objeto SqlSession proporciona métodos que permiten


ejecutar SELECT, INSERT, UPDATE, y DELETE las declaraciones que se definen en
los archivos XML de mapeo SQL. También le permite confirmar o revertir la
transacción actual.

La dependencia de Maven que utilizamos para esta receta es la siguiente:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

Al momento de escribir este libro, la última documentación de MyBatis se puede


encontrar aquí: http://www.mybatis.org/mybatis-3/index.html

Cómo hacerlo...
Comenzaremos con las operaciones CRUD utilizando la base de datos PostgreSQL y la
clase Person1:

public class Person1 {


private int id;
private int age;
private String name;
public Person1(){} //Must be present, used by the framework
public Person1(int age, String name){
this.age = age;
this.name = name;
}
public int getId() { return id; }
public void setName(String name) { this.name = name; }
@Override
public String toString() {
return "Person1{id=" + id + ", age=" + age +
", name='" + name + "'}";
}
}

Necesitamos el método getId()anterior para obtener un valor de ID (para


demostrar cómo encontrar un registro de base de datos por ID). El
método setName()se usará para actualizar el registro de la base de datos y el
método toString()se usará para mostrar los resultados. Usamos el
nombre Person1para distinguirlo de otra versión de la misma clase, Person2 que

pág. 320
usaremos para demostrar cómo implementar la relación entre las clases y las tablas
correspondientes.

La tabla de base de datos coincidente se puede crear utilizando la siguiente


instrucción SQL:

create table person1 (


id SERIAL PRIMARY KEY,
age INTEGER NOT NULL,
name VARCHAR NOT NULL
);

Luego ejecute los siguientes pasos:

1. Comience creando un archivo de configuración XML. Lo llamaremos mb-


config1.xml y lo colocaremos en la carpeta mybatis debajo resources. De
esta manera, Maven lo pondrá en un classpath. Otra opción sería colocar el archivo
en cualquier otra carpeta junto con el código Java y modificarlo pom.xml para
decirle a Maven que también coloque los .xmlarchivos de esa carpeta en el
classpath. El contenido del archivo tiene el siguiente aspecto: mb-config1.xml

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="useGeneratedKeys" value="true"/>
</settings>
<typeAliases>
<typeAlias type="com.packt.cookbook.ch06_db.mybatis.Person1"
alias="Person"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.postgresql.Driver"/>
<property name="url"
value="jdbc:postgresql://localhost/cookbook"/>
<property name="username" value="cook"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/Person1Mapper.xml"/>
</mappers>
</configuration>

La etiqueta <settings> permite definir algunos comportamientos de forma global:


carga lenta de los valores, habilitar / deshabilitar la memoria caché, establecer el
comportamiento de mapeo automático (para llenar datos anidados o no) y más. Hemos

pág. 321
elegido establecer el uso de claves autogeneradas a nivel mundial porque necesitamos
que los objetos insertados se llenen con ID generados en la base de datos.

La etiqueta <typeAiases> contiene alias de los nombres de clase totalmente


calificados, que funcionan de manera similar a la instrucción IMPORT. La única
diferencia es que un alias puede ser cualquier palabra, no solo un nombre de
clase. Después de declarar el alias, en cualquier otro lugar de
los .xmlarchivos MyBatis , la clase puede ser referida solo por este alias. Veremos
cómo hacerlo mientras revisamos el contenido del archivo
en Person1Mapper.xmlbreve.

La etiqueta <environments> contiene configuración para diferentes entornos. Por


ejemplo, podríamos tener una configuración para el entorno env42 (cualquier cadena
funcionaría). Luego, al crear un objeto, puede pasar este nombre como parámetro del
método y se utilizará la configuración incluida en las etiquetas . Define el
administrador de transacciones que se utilizará y la fuente de
datos. SqlSessionSqlSessionFactory.build()<environment
id="env42"></environment>

El TransactionManager puede ser uno de dos tipos: JDBC, que utiliza la conexión
proporcionada por el origen de datos para confirmar, revertir y administrar el alcance
de la transacción, y MANAGEDque no hace nada y permite que el contenedor administre
el ciclo de vida de la transacción, bueno, cierra la conexión por defecto, pero ese
comportamiento se puede cambiar configurando la siguiente propiedad:

< <transactionManager type="MANAGED">


<property name="closeConnection" value="false"/>
</transactionManager>

La etiqueta <mappers>contiene referencias a todos los .xmlarchivos que contienen


sentencias SQL que mapean registros de bases de datos y objetos Java, que en nuestro
caso es el archivoPerson1Mapper.xml .

2. Cree el archivo Person1Mapper.xml y colóquelo en la misma carpeta que


el archivo mb-config1.xml. Este archivo puede tener cualquier nombre que desee, pero
contiene todas las declaraciones SQL que mapean los registros de la base de datos y los
objetos de la clase Person1, por lo que lo hemos nombrado Person1Mapper.xml solo
por razones de claridad:

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatis.Person1Mapper">
<insert id="insertPerson" keyProperty="id" keyColumn="id"
parameterType="Person">

pág. 322
insert into Person1 (age, name) values(#{age}, #{name})
</insert>
<select id="selectPersonById" parameterType="int"
resultType="Person">
select * from Person1 where id = #{id}
</select>
<select id="selectPersonsByName" parameterType="string"
resultType="Person">
select * from Person1 where name = #{name}
</select>
<select id="selectPersons" resultType="Person">
select * from Person1
</select>
<update id="updatePersonById" parameterType="Person">
update Person1 set age = #{age}, name = #{name}
where id = #{id}
</update>
<delete id="deletePersons">
delete from Person1
</delete>
</mapper>

Como puede ver, la etiqueta <mapper> tiene un atributo namespace que se utiliza
para resolver archivos con el mismo nombre en diferentes ubicaciones. Se puede o no
coincidir con la ubicación del archivo mapeador. La ubicación del archivo del mapeador
se especifica en el archivo de configuración mb-config1.xml como el recurso de
atributo de la etiqueta <mapper>(consulte el paso anterior).

Los atributos de las etiquetas <insert>, <select>, <update> y <delete>son


fáciles de entender en su mayor parte. Los
atributos keyProperty, keyColumny useGeneratedKeys (en la
configuración <settings>) se agregan para llenar el objeto insertado con el valor
generado por la base de datos. Si no lo necesita globalmente, el
atributo useGeneratedKeyspuede eliminarse de la configuración en la
configuración y agregarse solo a las instrucciones de inserción en las que le gustaría
aprovechar la generación automática de algún valor. Hicimos esto porque queríamos
obtener la ID generada y usarla en el código más adelante para demostrar cómo el ID
puede recuperar el registro.

El atributo de ID <select> y etiquetas similares se utilizan para invocarlos junto con


el valor del espacio de nombres del asignador. Le mostraremos cómo se hace esto en
breve. La construcción se #{id}refiere al valor pasado como parámetro si el valor es
de tipo primitivo. De lo contrario, se espera que el objeto pasado tenga dicho campo. No
es necesario tener un captador sobre el objeto. Si hay un captador, debe cumplir con el
formato del método JavaBean.

Para el valor de retorno, de forma predeterminada, el nombre de una columna coincide


con el nombre del campo de objeto o definidor (debe ser compatible con el formato del
método JavaBean). Si el campo (o el nombre del emisor) y el nombre de la columna son
diferentes, puede proporcionar la asignación utilizando la etiqueta <resultMap>. Por

pág. 323
ejemplo, si la tabla persontiene las columnas person_idy person_name, mientras
que el objeto de dominio Persontiene los campos idy name, podemos crear un mapa:

<resultMap id="personResultMap" type="Person">


<id property="id" column="person_id" />
<result property="name" column="person_name"/>
</resultMap>

Este resultMap puede usarse para llenar el objeto de dominio de


la Person siguiente manera:

<select id="selectPersonById" parameterType="int"


resultMap="personResultMap">
select person_id, person_name from Person where id = #{id}
</select>

Alternativamente, es posible usar los alias de la cláusula select estándar:

<select id="selectPersonById" parameterType="int"


resultType="Person">
select person_id as "id", person_name as "name" from Person
where id = #{id}
</select>
resultType =
"Person">
seleccione person_id como "id", person_name como "name"
de Person
donde id = #
{id}
</select>

3. Escriba código que inserte un registro en la tabla person1y luego lea este registro
de la siguiente manera id:

String resource = "mybatis/mb-config1.xml";


String mapperNamespace = "mybatis.Person1Mapper";
try {
InputStream inputStream =
Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
try(SqlSession session = sqlSessionFactory.openSession()){
Person1 p = new Person1(10, "John");
session.insert(mapperNamespace + ".insertPerson", p);
session.commit();
p = session.selectOne(mapperNamespace +
".selectPersonById", p.getId());
System.out.println("By id " + p.getId() + ": " + p);
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (Exception ex){

pág. 324
ex.printStackTrace();
}

El código anterior producirá una salida:

By id 1: Person1{id=1, age=10, name='John'}

La utilidad Resources tiene diez métodos sobrecargados para leer el archivo de


configuración. Ya hemos descrito cómo asegurarse de que Maven coloca los archivos de
configuración y mapeador en el classpath.

El objeto SqlSession implementa la interfaz AutoCloseable, por lo que


podemos usar el bloque de prueba con recursos y no preocuparnos por la pérdida de
recursos. La interfaz proporciona muchos métodos de ejecución, incluidos los métodos
sobrecargados , , , , , , y , por citar los más frecuentemente utilizados y directas. También
hemos usado y . Este último se asegura de que solo se devuelva un resultado. De lo
contrario, arroja una excepción. También arroja una excepción cuando la columna
utilizada para identificar un solo registro por un valor no tiene una restricción única. Es
por eso que hemos agregado la calificación a la ID de la columna. Alternativamente,
podríamos agregar una restricción única (marcándola
como SqlSessioninsert()select()selectList()selectMap()selectO
ne()update()delete()insert()selectOne()PRIMARY KEYPRIMARY
KEY hace esto implícitamente).
El método selectList(, por otro lado, produce un objeto List, incluso cuando
solo se devuelve un resultado. Vamos a demostrar esto ahora.

4. Escriba un código que lea todos los registros de la tabla person1:

List<Person1> list = session.selectList(mapperNamespace


+ ".selectPersons");
for(Person1 p1: list) {
System.out.println("All: " + p1);
}

El código anterior producirá el siguiente resultado:

All: Person1{id=1, age=10, name='John'}

5. Para demostrar una actualización, cambiemos el nombre de "John"a "Bill"y


leamos todos los registros de person1 nuevo:

List<Person1> list = session.selectList(mapperNamespace


+ ".selectPersonsByName", "John");
for(Person1 p1: list) {
p1.setName("Bill");
int c = session.update(mapperNamespace +
".updatePersonById", p1);
System.out.println("Updated " + c + " records");
}
session.commit();

pág. 325
list =
session.selectList(mapperNamespace + ".selectPersons");
for(Person1 p1: list) {
System.out.println("All: " + p1);
}

El código anterior producirá el siguiente resultado:

Updated 1 records
All: Person1{id=1, age=10, name='Bill'}

Observe cómo se cometió el cambio: session.commit(). Sin esta línea, el resultado


es el mismo, pero el cambio no persiste porque la transacción no se autocompita de
forma predeterminada. Se puede cambiar configurando la confirmación automática
en true al abrir la sesión:

SqlSession session = sqlSessionFactory.openSession(true);

6. Finalmente, llame a la DELETEdeclaración y elimine todos los registros de la


tabla person1:

int c = session.delete(mapperNamespace + ".deletePersons");


System.out.println("Deleted " + c + " persons");
session.commit();

List<Person1> list = session.selectList(mapperNamespace +


".selectPersons");
System.out.println("Total records: " + list.size());

El código anterior producirá el siguiente resultado:

Deleted 0 persons
Total records: 0

7. Para demostrar cómo MyBatis apoya las relaciones, cree la tabla family y la
tabla person2:

create table family (


id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL
);
create table person2 (
id SERIAL PRIMARY KEY,
age INTEGER NOT NULL,
name VARCHAR NOT NULL,
family_id INTEGER references family(id)
ON DELETE CASCADE
);

Como puede ver, los registros en las tablas family y person2 tienen relaciones uno
a muchos. Cada registro de la tabla person2puede pertenecer a una familia (consulte
un family registro) o no. Varias personas pueden pertenecer a la misma

pág. 326
familia. También hemos agregado la cláusula ON DELETE CASCADE para que
los registros person2 se puedan eliminar automáticamente cuando se elimina la
familia a la que pertenecen.

Las clases Java correspondientes tienen el siguiente aspecto:

class Family {
private int id;
private String name;
private final List<Person2> members = new ArrayList<>();
public Family(){} //Used by the framework
public Family(String name){ this.name = name; }
public int getId() { return id; }
public String getName() { return name; }
public List<Person2> getMembers(){ return this.members; }
}

Como puede ver, la clase Family tiene una colección de objetos Person2. Para los
métodos getId()y getMembers(), necesitamos establecer la relación con la clase
Person2. Will utilizará el método getName() para el código de demostración.

La clase Person2 tiene el siguiente aspecto:

class Person2 {
private int id;
private int age;
private String name;
private Family family;
public Person2(){} //Used by the framework
public Person2(int age, String name, Family family){
this.age = age;
this.name = name;
this.family = family;
}
@Override
public String toString() {
return "Person2{id=" + id + ", age=" + age +
", name='" + name + "', family='" +
(family == null ? "" : family.getName())+ "'}";
}
}

8. Cree un nuevo archivo de configuración llamado mb-config2.xml:

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="useGeneratedKeys" value="true"/>
</settings>
<typeAliases>
<typeAlias type="com.packt.cookbook.ch06_db.mybatis.Family"

pág. 327
alias="Family"/>
<typeAlias type="com.packt.cookbook.ch06_db.mybatis.Person2"
alias="Person"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.postgresql.Driver"/>
<property name="url"
value="jdbc:postgresql://localhost/cookbook"/>
<property name="username" value="cook"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/FamilyMapper.xml"/>
<mapper resource="mybatis/Person2Mapper.xml"/>
</mappers>
</configuration>

Tenga en cuenta que ahora tenemos dos alias y dos .xmlarchivos de mapeador .

9. El contenido del Person2Mapper.xmlarchivo es mucho más pequeño que el


contenido del Person1Mapper.xmlarchivo que usamos antes:

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatis.Person2Mapper">
<insert id="insertPerson" keyProperty="id" keyColumn="id"
parameterType="Person">
insert into Person2 (age, name, family_id)
values(#{age}, #{name}, #{family.id})
</insert>
<select id="selectPersonsCount" resultType="int">
select count(*) from Person2
</select>
</mapper>

Esto se debe a que no vamos a actualizar o administrar a estas personas


directamente. Vamos a hacer esto a través de las familias a las que pertenecen. Hemos
agregado una nueva consulta que devuelve el recuento de los registros en la tabla
person2.

10. El contenido del archivo FamilyMapper.xml es el siguiente:

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatis.FamilyMapper">
<insert id="insertFamily" keyProperty="id" keyColumn="id"

pág. 328
parameterType="Family">
insert into Family (name) values(#{name})
</insert>
<select id="selectMembersOfFamily" parameterType="int"
resultMap="personMap">
select * from Person2 where family_id = #{id}
</select>
<resultMap id="personMap" type="Person">
<association property="family" column="family_id"
select="selectFamilyById"/>
</resultMap>
<select id="selectFamilyById" parameterType="int"
resultType="Family">
select * from Family where id = #{id}
</select>
<select id="selectFamilies" resultMap="familyMap">
select * from Family
</select>
<resultMap id="familyMap" type="Family">
<collection property="members" column="id" ofType="Person"
select="selectMembersOfFamily"/>
</resultMap>
<select id="selectFamiliesCount" resultType="int">
select count(*) from Family
</select>
<delete id="deleteFamilies">
delete from Family
</delete>

El mapeador familiar está mucho más involucrado porque manejamos la relación en


él. Primero, mira la consulta selectMembersOfFamily. Si no desea llenar el
campo family en el objeto de Person2, el SQL sería mucho más simple, de la
siguiente manera:

<select id="selectMembersOfFamily" parameterType="int"


resultType="Person">
select * from Person2 where family_id = #{id}
</select>

Pero queríamos establecer el valor Family del objeto correspondiente en el objeto


Person2, por lo que utilizamos el ResultMap personMap que describe solo la
asignación que no se puede hacer de forma predeterminada: utilizamos la etiqueta
<association> para asociar el campo family con la columna family_id a
utilizando la consulta selectFamilyById. Esta última consulta no completará el
campo members del objeto Family, pero decidimos que no es necesaria para nuestra
demostración.

Reutilizamos la consulta selectMembersOfFamily en la


consulta selectFamilies. Para llenar el campo members del objeto Family,
creamos un ResultMap familyMap que usa selectMembersOfFamily para
hacer eso.

pág. 329
Cómo funciona...
Escribamos el código que muestra las operaciones CRUD en la tabla family. Primero,
así es como family se puede crear un registro y asociarlo con dos registros
person2:

String resource = "mybatis/mb-config2.xml";


String familyMapperNamespace = "mybatis.FamilyMapper";
String personMapperNamespace = "mybatis.Person2Mapper";
try {
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
try(SqlSession session = sqlSessionFactory.openSession()){
Family f = new Family("The Jones");
session.insert(familyMapperNamespace + ".insertFamily", f);
System.out.println("Family id=" + f.getId()); //Family id=1

Person2 p = new Person2(25, "Jill", f);


session.insert(personMapperNamespace + ".insertPerson", p);
System.out.println(p);
//Person2{id=1, age=25, name='Jill', family='The Jones'}

p = new Person2(30, "John", f);


session.insert(personMapperNamespace + ".insertPerson", p);
System.out.println(p);
//Person2{id=2, age=30, name='John', family='The Jones'}

session.commit();
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (Exception ex){
ex.printStackTrace();
}

Ahora, podemos leer los registros creados usando el siguiente código:

List<Family> fList =
session.selectList(familyMapperNamespace + ".selectFamilies");
for (Family f1: fList) {
System.out.println("Family " + f1.getName() + " has " +
f1.getMembers().size() + " members:");
for(Person2 p1: f1.getMembers()){
System.out.println(" " + p1);
}
}

El fragmento de código anterior produce el siguiente resultado:

Family The Jones has 2 members:


Person2{id=1, age=25, name='Jill', family='The Jones'}
Person2{id=2, age=30, name='John', family='The Jones'}

pág. 330
Ahora, podemos eliminar todos los familyregistros y verificar si alguna de las
tablas contiene family y person2 contiene registros posteriores:

int c = session.delete(familyMapperNamespace + ".deleteFamilies");


System.out.println("Deleted " + c + " families");
session.commit();

c = session.selectOne(familyMapperNamespace + ".selectFamiliesCount");
System.out.println("Total family records: " + c);

c = session.selectOne(personMapperNamespace + ".selectPersonsCount");
System.out.println("Total person records: " + c);

El resultado del fragmento de código anterior es el siguiente:

Deleted 1 families
Total family records: 0
Total person records: 0

La tabla person2 ahora también está vacía porque agregamos la cláusula ON


DELETE CASCADE al crear la tabla.

Hay más...
MyBatis también proporciona instalaciones para construir un SQL dinámico, una clase
SqlBuilder y muchas otras formas de construir y ejecutar SQL de cualquier complejidad
o procedimiento almacenado. Para los detalles, lea la documentación
en http://www.mybatis.org/mybatis-3 .

Uso de la API de persistencia de


Java e Hibernate
En esta receta, aprenderá a rellenar, leer, cambiar y eliminar datos en la base de datos
utilizando una implementación de Java Persistence API ( JPA ) llamada marco
de Hibernate Object-Relational Mapping ( ORM ).

Prepararse
JPA es una especificación que define una posible solución para ORM. Puede encontrar
la versión 2.2 de JPA en el siguiente enlace:

http://download.oracle.com/otn-pub/jcp/persistence-2_2-mrel-
spec/JavaPersistence.pdf

pág. 331
Las interfaces, enumeraciones, anotaciones y excepciones descritas en la
especificación pertenecen al
paquete javax.persistence ( https://javaee.github.io/javaee-
spec/javadocs ) que se incluye en Java Enterprise Edition ( EE ). La JPA
se implementa mediante varios marcos, siendo el más popular:

• Hibernate ORM ( http://hibernate.org/orm )


• EclipseLink ( http://www.eclipse.org/eclipselink )
• Oracle TopLink
( http://www.oracle.com/technetwork/middleware/toplink/ov
erview/index.html )
• jOOQ ( https://www.jooq.org )

JPA está diseñado en torno a entidades: los beans Java que se asignan a las tablas de la
base de datos mediante anotaciones. Alternativamente, el mapeo se puede definir
usando XML o una combinación de ambos. La asignación definida por XML reemplaza a
la definida por las anotaciones. La especificación también define un lenguaje de
consulta similar a SQL para consultas de datos estáticos y dinámicos.

La mayoría de las implementaciones de JPA permiten la creación de un esquema de


base de datos utilizando la asignación definida por anotaciones y XML.

Cómo hacerlo...
1. Comencemos agregando la dependencia javax.persistence del paquete
al archivo de configuración de Maven pom.xml:

<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>

Todavía no necesitamos ninguna de las implementaciones de JPA. De esta manera,


podemos asegurarnos de que nuestro código no use ningún código específico del marco
y use solo interfaces JPA.

2. Crea la clase Person1:

public class Person1 {


private int age;
private String name;
public Person1(int age, String name){
this.age = age;
this.name = name;
}

pág. 332
@Override
public String toString() {
return "Person1{id=" + id + ", age=" + age +
", name='" + name + "'}";
}
}

No agregamos getters, setters ni ningún otro método; Esto es para que podamos
mantener nuestro código breve y simple. Para convertir esta clase en una entidad,
necesitamos, de acuerdo con la especificación JPA, agregar la anotación @Entity a la
declaración de la clase ( requiere importación java.persistence.Entity ). Esto
significa que nos gustaría que esta clase represente un registro en una tabla de base de
datos llamada person. Por defecto, el nombre de la clase de la entidad coincide con el
nombre de la tabla. Pero es posible asignar la clase a una tabla con otro nombre usando
la anotación @Table(name="<another table name>"). Del mismo modo, cada
propiedad de clase se asigna a una columna con el mismo nombre, y es posible cambiar
el nombre predeterminado mediante la anotación @Column (name="<another
column name>").

Además, una clase de entidad debe tener una clave primaria, un campo representado
por la anotación @Id. Una clave compuesta que combina varios campos también se
puede definir utilizando la anotación @IdClass (no se utiliza en nuestros ejemplos). Si
la clave principal se genera automáticamente en la base de datos, la anotación
@GeneratedValue se puede colocar delante de ese campo.

Y, finalmente, una clase de entidad debe tener un constructor sin argumentos. Con
todas estas anotaciones, la clase de entidad Person ahora tiene el siguiente aspecto:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Person1 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
public int age;
private String name;
public Person1(){}
public Person1(int age, String name){
this.age = age;
this.name = name;
}
}

Ni la clase ni ninguna de sus variables de instancia persistentes pueden declararse


finales. De esta manera, los marcos de implementación pueden ampliar las clases de
entidad e implementar la funcionalidad requerida.

pág. 333
Alternativamente, las anotaciones de persistencia se pueden agregar a los captadores y
definidores, en lugar de los campos de instancia (si los nombres de los métodos siguen
las convenciones de Java Bean). Pero mezclar anotaciones de campos y métodos no está
permitido y puede tener consecuencias inesperadas.

También es posible usar un archivo XML en lugar de una anotación para definir el
mapeo entre una clase Java y una tabla y columnas de la base de datos, pero nos
quedaremos con las anotaciones a nivel de campo para proporcionar el método más
compacto y claro para expresar la intención

3. Cree una tabla de base de datos llamada person1 utilizando el siguiente script
SQL:

create table person1 (


id SERIAL PRIMARY KEY,
age INTEGER NOT NULL,
name VARCHAR NOT NULL
);

4. Hemos definido la columna id como SERIAL, lo que significa que le pedimos


a la base de datos que genere el siguiente valor entero automáticamente cada vez que
se inserta una nueva fila en la tabla person1. Coincide con las anotaciones de la
propiedad id de la clase Person1.

4. Ahora, escriba un código que inserte un registro en la tabla person1 y luego lea
todos los registros de él. Para crear, actualizar y eliminar una entidad (y el registro
correspondiente en la tabla correspondiente), debe usar un administrador de entidades
como javax.persistence.EntityManager:

EntityManagerFactory emf =
Persistence.createEntityManagerFactory("jpa-demo");
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
Person1 p = new Person1(10, "Name10");
em.persist(p);
em.getTransaction().commit();

Query q = em.createQuery("select p from Person1 p");


List<Person1> pList = q.getResultList();
for (Person1 p : pList) {
System.out.println(p);
}
System.out.println("Size: " + pList.size());
} catch (Exception ex){
em.getTransaction().rollback();
} finally {
em.close();
emf.close();
}

pág. 334
Como se puede ver, un objeto del EntityManagerFactory que se crea utilizando
algún tipo de configuración, es decir, jpa-demo. Hablaremos de ello en breve. La
fábrica permite la creación de un objeto EntityManager, que controla el proceso de
persistencia: crea, confirma y revierte una transacción, almacena un nuevo objeto
Person1(insertando así un nuevo registro en la tabla person1), admite la lectura de
datos utilizando Java Persistence Query Language ( JPQL ), y muchas otras
operaciones de bases de datos y procesos de gestión de transacciones.

Después de cerrar el administrador de entidades, las entidades administradas se


encuentran en un estado separado. Para sincronizarlos nuevamente con la base de
datos, se puede usar el método merge()de EntityManager.

En el ejemplo anterior, tenemos JPQL para consultar la base de


datos. Alternativamente, también podríamos usar el Criteria API definido por la
especificación JPA:

CriteriaQuery<Person1> cq =
em.getCriteriaBuilder().createQuery(Person1.class);
cq.select(cq.from(Person1.class));
List<Person1> pList = em.createQuery(cq).getResultList();
System.out.println("Size: " + pList.size());

Pero parece que JPQL es menos detallado y admite la intuición de aquellos


programadores que conocen SQL, por lo que vamos a usar JPQL.

5. Defina la configuración de persistencia en el


archivo persistence.xml ubicado en la carpeta resources/META-INF. La
etiqueta <persistence-unit> tiene un nombre de atributo. Hemos establecido el
valor del atributo en , pero puede usar cualquier otro nombre que desee. Esta
configuración especifica la implementación JPA (proveedor), las propiedades de conexión
de la base de datos y muchas otras propiedades relacionadas con la persistencia en un
formato XML:jpa-demo

<?xml version="1.0" encoding="UTF-8"?>


<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="jpa-demo"
transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="javax.persistence.jdbc.url"
value="jdbc:postgresql://localhost/cookbook"/>
<property name="javax.persistence.jdbc.driver"
value="org.postgresql.Driver"/>
<property name="javax.persistence.jdbc.user" value="cook"/>
<property name="javax.persistence.jdbc.password" value=""/>
</properties>

pág. 335
</persistence-unit>
</persistence>

Consulte la documentación de Oracle


( https://docs.oracle.com/cd/E16439_01/doc.1013/e13981/cfgdepd
s005.htm ) sobre la configuración del archivo persistence.xml. Para esta
receta, utilizamos el ORM de Hibernate y, por lo tanto, lo
especificamos org.hibernate.jpa.HibernatePersistenceProvider
como proveedor.

6. Finalmente, necesitamos agregar la implementación JPA (Hibernate ORM) como


una dependencia en pom.xml:

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.3.1.Final</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>

Como habrás notado, hemos marcado las dependencias de Hibernate como de


alcance runtime. Hicimos esto para evitar el uso de funciones específicas de
Hibernate al escribir el código. También hemos agregado la dependencia jaxb-api,
que es utilizada por Hibernate, pero esta biblioteca no es específica de Hibernate, por
lo que no la utilizamos solo en tiempo de ejecución.

7. Para una mejor presentación de resultados, agregaremos el siguiente método


toString()a la clase Person1:

@Override
public String toString () {
return "Person1 {id =" + id + ", age =" + age +
", name = '" + name + "'}" ;
}

8. Ahora, podemos ejecutar nuestro ejemplo de código JPA y observar el resultado:

Person1{id=1, age=10, name='Name10'}


Size: 1
Size: 1

Las dos primeras líneas de la salida anterior provienen del uso de JPQL, y la última
línea proviene del fragmento de uso de Criteria API de nuestro ejemplo de código.

pág. 336
9. JPA también tiene una disposición para establecer relaciones entre clases. Una clase
de entidad (y la tabla de base de datos correspondiente) puede tener relaciones uno a uno,
uno a muchos, muchos a uno y muchos a muchos con otra clase de entidad (y su tabla). La
relación puede ser bidireccional o unidireccional. Esta especificación define las siguientes
reglas para una relación bidireccional:

• El lado inverso debe referirse a su lado propietario mediante el mappedBy atributo


de la anotación @OneToOne, @OneToManyo @ManyToMany.
• El lado múltiple de las relaciones uno a muchos y muchos a uno debe ser el
propietario de esta relación, por lo que el mappedBy atributo no puede
especificarse en la @ManyToOneanotación.
• En las relaciones uno a uno, el lado propietario es el lado que contiene la clave
externa.
• En las relaciones de muchos a muchos, cualquiera de las partes puede ser la parte
propietaria.

Con una relación unidireccional, solo una clase tiene una referencia a la otra clase.

Para ilustrar estas reglas, creemos una clase llamada Family que tenga una relación
de uno a muchos con la Person2 clase:

@Entity
public class Family {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
public Family(){}
public Family(String name){ this.name = name;}

@OneToMany(mappedBy = "family")
private final List<Person2> members = new ArrayList<>();

public List<Person2> getMembers(){ return this.members; }


public String getName() { return name; }
}

El script SQL que crea la tabla family es sencillo:

create table family (


id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL
);

También necesitamos agregar el campo Family a la clase Person2:

@Entity
public class Person2 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

pág. 337
private int id;
private int age;
private String name;

@ManyToOne
private Family family;

public Person2(){}
public Person2(int age, String name, Family family){
this.age = age;
this.name = name;
this.family = family;
}
@Override
public String toString() {
return "Person2{id=" + id + ", age=" + age +
", name='" + name + "', family='" +
(family == null ? "" : family.getName())+ "'}";
}
}

La clase Person2 es un lado "muchos", por lo que, de acuerdo con esta regla, posee
la relación, por lo que la tabla person2 debe tener una clave externa que apunte al
registro de la tabla family:

create table person2 (


id SERIAL PRIMARY KEY,
age INTEGER NOT NULL,
name VARCHAR NOT NULL,
family_id INTEGER references family(id)
ON DELETE CASCADE
);

La referencia a una columna requiere que este valor de columna sea único. Por eso
hemos marcado la columna idde la tabla person2 como PRIMARY KEY. De lo
contrario, se ERROR: 42830: there is no unique constraint matching
given keys for referenced table generaría un error .

10. Ahora, podemos usar las clases Familyy Person2crear los registros en las tablas
correspondientes y leer también de estas tablas:

EntityManagerFactory emf =
Persistence.createEntityManagerFactory("jpa-demo");
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
Family f = new Family("The Jones");
em.persist(f);

Person2 p = new Person2(10, "Name10", f);


em.persist(p);

f.getMembers().add(p);
em.getTransaction().commit();

pág. 338
Query q = em.createQuery("select f from Family f");
List<Family> fList = q.getResultList();
for (Family f1 : fList) {
System.out.println("Family " + f1.getName() + ": "
+ f1.getMembers().size() + " members:");
for(Person2 p1: f1.getMembers()){
System.out.println(" " + p1);
}
}
q = em.createQuery("select p from Person2 p");
List<Person2> pList = q.getResultList();
for (Person2 p1 : pList) {
System.out.println(p1);
}
} catch (Exception ex){
ex.printStackTrace();
em.getTransaction().rollback();
} finally {
em.close();
emf.close();
}
}

En el código anterior, creamos un objeto de la clase Family y lo persistimos. De esta


manera, el objeto adquiere un valor id de la base de datos. Luego lo pasamos al
constructor de la clase Person2 y establecimos la relación en muchos lados. Luego,
persistimos en el objeto Person2 (para que también adquiriera un id de la base de
datos) y lo agregamos a la colección members del objeto Family , de modo que
también se establece un lado de la relación. Para preservar los datos, la transacción
debe confirmarse. Cuando se confirma la transacción, todos los objetos de entidad
asociados con un EntityManager (se dice que dichos objetos están en
un estado administrado ) se conservan automáticamente.

Cómo funciona...
Si ejecutamos el código anterior, el resultado será el siguiente:

Family The Jones: 1 members:


Person2{id=1, age=10, name='Name10', family='The Jones'}
Person2{id=1, age=10, name='Name10', family='The Jones'}

Como puede ver, eso es exactamente lo que esperábamos: un objeto de


clase Family The Jones tiene un miembro, un objeto de clase Person2, y el
registro en la tabla se person2 refiere al registro correspondiente de la
tabla family.

pág. 339
Programación concurrente y
multiproceso
La programación concurrente siempre ha sido una tarea difícil. Es una fuente de
muchos problemas difíciles de resolver. En este capítulo, le mostraremos diferentes
formas de incorporar la concurrencia y algunas mejores prácticas, como la
inmutabilidad, que ayuda a crear un procesamiento multiproceso. También
discutiremos la implementación de algunos patrones de uso común, como dividir y
conquistar y publicar-suscribir, utilizando las construcciones proporcionadas por
Java. Cubriremos las siguientes recetas:

• Uso del elemento básico de concurrencia: hilo


• Diferentes enfoques de sincronización
• La inmutabilidad como un medio para lograr la concurrencia
• Usar colecciones concurrentes
• Usar el servicio ejecutor para ejecutar tareas asíncronas
• Usando fork / join para implementar divide-and-conquer
• Uso del flujo para implementar el patrón de publicación-suscripción

Introducción
La concurrencia, la capacidad de ejecutar varios procedimientos en paralelo, se vuelve
cada vez más importante a medida que el análisis de big data se traslada a la corriente
principal de aplicaciones modernas. Tener CPU o varios núcleos en una CPU ayuda a
aumentar el rendimiento, pero la tasa de crecimiento del volumen de datos siempre
superará los avances de hardware. Además, incluso en un sistema de múltiples CPU,
uno todavía tiene que estructurar el código y pensar en compartir recursos para
aprovechar la potencia computacional disponible.

En los capítulos anteriores, demostramos cómo l ambdas con interfaces funcionales y


flujos paralelos hicieron que el procesamiento concurrente formara parte del conjunto
de herramientas de cada programador Java. Uno puede aprovechar fácilmente esta
funcionalidad con una orientación mínima, si la hay.

En este capítulo, describiremos algunas otras características y API de Java (antiguas


(antes de Java 9) y nuevas) que permiten un mayor control sobre la concurrencia. La
API Java de concurrencia de alto nivel ha existido desde Java 5. La Propuesta de
Mejora JDK (JEP) 266, Más Actualizaciones de
Concurrencia ( http://openjdk.java.net/jeps/266 ), presentada, a Java 9 en
el java.util.concurrent paquete .

pág. 340
"un marco interoperable de publicación-suscripción, mejoras en la API
CompletableFuture y varias otras mejoras"

Pero antes de sumergirnos en los detalles de las últimas incorporaciones, repasemos


los conceptos básicos de la programación concurrente en Java y veamos cómo usarlos.

Java tiene dos unidades de ejecución: proceso e hilo. Un proceso generalmente


representa la JVM completa, aunque una aplicación puede crear otro proceso
usando ProcessBuilder. Pero dado que el caso de multiproceso está fuera del
alcance de este libro, nos centraremos en la segunda unidad de ejecución, es decir, un
hilo, que es similar a un proceso pero menos aislado de otros hilos y requiere menos
recursos para la ejecución.

Un proceso puede tener muchos hilos en ejecución y al menos un hilo


llamado hilo principal. Los subprocesos pueden compartir recursos, incluida la
memoria y los archivos abiertos, lo que permite una mejor eficiencia. Pero viene con un
precio de mayor riesgo de interferencia mutua involuntaria e incluso bloqueo de la
ejecución. Ahí es donde se requieren habilidades de programación y una comprensión
de las técnicas de concurrencia. Y eso es lo que vamos a discutir en este capítulo.

Usando el elemento básico de


concurrencia - hilo
En este capítulo, veremos la clase java.lang.Thread y veremos qué puede hacer
para la concurrencia y el rendimiento del programa en general.

Prepararse
Una aplicación Java comienza como el subproceso principal (sin contar los subprocesos
del sistema que admiten el proceso). A continuación, puede crear otros subprocesos y
dejar que se ejecuten en paralelo, compartiendo el mismo núcleo a través del corte de
tiempo o teniendo una CPU dedicada para cada subproceso. Esto se puede hacer
utilizando la clase java.lang.Thread que implementa la interfaz Runnable
funcional con sólo un método abstracto, run().

Hay dos formas de crear un nuevo hilo: creando una subclase de Thread, o
implementando la interfaz Runnable y pasando el objeto de la clase de
implementación al constructor Thread. Podemos invocar el nuevo hilo llamando
al start()método de la Threadclase que, a su vez, llama al método run()que se
implementó.

pág. 341
Luego, podemos dejar que el nuevo hilo se ejecute hasta su finalización o pausarlo y
dejar que continúe nuevamente. También podemos acceder a sus propiedades o
resultados intermedios si es necesario.

Cómo hacerlo...
Primero, creamos una clase llamada AThread que extiende Thread y anula
su método run() :

class AThread extends Thread {


int i1,i2;
AThread(int i1, int i2){
this.i1 = i1;
this.i2 = i2;
}
public void run() {
IntStream.range(i1, i2)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);
}
}

En este ejemplo, queremos que el hilo genere una secuencia de enteros en un cierto
rango. Luego, usamos la operación peek() para invocar el método
doSomething() estático de la clase principal para cada elemento de flujo con el fin
de ocupar el hilo durante algún tiempo. Consulte el siguiente código:

int doSomething(int i){


IntStream.range(i, 100000).asDoubleStream().map(Math::sqrt).average();
return i;
}

Como puede ver, el método doSomething()genera una secuencia de enteros en el


rango de i a 99999; luego convierte la secuencia en una secuencia de dobles, calcula
la raíz cuadrada de cada uno de los elementos de la secuencia y finalmente calcula un
promedio de los elementos de la secuencia. Descartamos el resultado y devolvemos el
parámetro pasado como una conveniencia que nos permite mantener el estilo fluido en
la tubería de flujo del hilo, que termina imprimiendo cada elemento. Usando esta nueva
clase, podemos demostrar la ejecución concurrente de los tres hilos, de la siguiente
manera:

Thread thr1 = new AThread(1, 4);


thr1.start();

Thread thr2 = new AThread(11, 14);


thr2.start();

IntStream.range(21, 24)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);

pág. 342
El primer hilo genera los números enteros 1, 2y 3, el segundo genera los números
enteros 11, 12y 13, y el tercer hilo (uno principal) genera 21, 22y 23.

Como se mencionó anteriormente, podemos reescribir el mismo programa creando y


usando una clase que podría implementar la interfaz Runnable:

class ARunnable implements Runnable {


int i1,i2;
ARunnable(int i1, int i2){
this.i1 = i1;
this.i2 = i2;
}
public void run() {
IntStream.range(i1, i2)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);
}
}

Uno puede ejecutar los mismos tres hilos como este:

Thread thr1 = new Thread(new ARunnable(1, 4));


thr1.start();

Thread thr2 = new Thread(new ARunnable(11, 14));


thr2.start();

IntStream.range(21, 24)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);

También podemos aprovechar la ventaja de Runnableser una interfaz funcional y


evitar crear una clase intermedia al pasar una expresión lambda en su lugar:

Thread thr1 = new Thread(() -> IntStream.range(1, 4)


.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println));
thr1.start();

Thread thr2 = new Thread(() -> IntStream.range(11, 14)


.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println));
thr2.start();

IntStream.range(21, 24)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);

Qué implementación es mejor depende de su objetivo y estilo. La


implementación Runnable tiene una ventaja (y en algunos casos, es la única opción
posible) que permite que la implementación extienda otra clase. Es particularmente útil
cuando desea agregar un comportamiento similar a un hilo a una clase

pág. 343
existente. Incluso puede invocar el método run() directamente, sin pasar el objeto
al constructor Thread.

El uso de una expresión lambda gana a la implementación Runnable cuando


solo run() se necesita la implementación del método, sin importar cuán grande
sea. Si es demasiado grande, puede aislarlo en un método separado:

public static void main(String arg[]) {


Thread thr1 = new Thread(() -> runImpl(1, 4));
thr1.start();

Thread thr2 = new Thread(() -> runImpl(11, 14));


thr2.start();

runImpl(21, 24);
}

private static void runImpl(int i1, int i2){


IntStream.range(i1, i2)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);
}

Sería difícil encontrar una implementación más corta de la funcionalidad anterior.

Si ejecutamos alguna de las versiones anteriores, obtendremos un resultado similar a


este:

Como puede ver, los tres hilos imprimen sus números simultáneamente, pero la
secuencia depende de la implementación particular de JVM y del sistema operativo
subyacente. Entonces, probablemente obtendrá un resultado diferente. Además,
también puede cambiar de una carrera a otra.

La clase Thread tiene varios constructores que permiten configurar el nombre del
hilo y el grupo al que pertenece. Agrupar hilos ayuda a gestionarlos si hay muchos hilos
ejecutándose en paralelo. La clase también tiene varios métodos que proporcionan
información sobre el estado y las propiedades del hilo, y nos permiten controlar el
comportamiento del hilo. Agregue estas dos líneas al ejemplo anterior:

pág. 344
System.out.println("Id=" + thr1.getId() + ", " + thr1.getName() + ",
priority=" + thr1.getPriority() + ",
state=" + thr1.getState());
System.out.println("Id=" + thr2.getId() + ", " + thr2.getName() + ",
priority=" + thr2.getPriority() + ",
state=" + thr2.getState());

El resultado del código anterior se verá más o menos así:

A continuación, supongamos que agrega un nombre a cada hilo:

Thread thr1 = new Thread(() -> runImpl(1, 4), "First Thread");


thr1.start();

Thread thr2 = new Thread(() -> runImpl(11, 14), "Second Thread");


thr2.start();

En este caso, la salida mostrará lo siguiente:

El subproceso id se genera automáticamente y no se puede cambiar, pero se puede


reutilizar después de que se termina el subproceso. Varios hilos, por otro lado, se
pueden configurar con el mismo nombre. La prioridad de ejecución se puede establecer
mediante programación con un valor
entre Thread.MIN_PRIORITYy Thread.MAX_PRIORITY. Cuanto más pequeño es
el valor, más tiempo se permite ejecutar el subproceso, lo que significa que tiene mayor
prioridad. Si no se establece, el valor de prioridad predeterminado
es Thread.NORM_PRIORITY.

El estado de un hilo puede tener uno de los siguientes valores:

• NEW: Cuando un hilo aún no ha comenzado


• RUNNABLE: Cuando se ejecuta un hilo
• BLOCKED: Cuando un hilo está bloqueado y está esperando un bloqueo del
monitor
• WAITING: Cuando un hilo espera indefinidamente a que otro hilo realice una
acción particular
• TIMED_WAITING: Cuando un subproceso está esperando que otro subproceso
realice una acción durante un tiempo de espera especificado
• TERMINATED: Cuando un hilo ha salido

pág. 345
El método sleep()se puede utilizar para suspender la ejecución de subprocesos
durante un período de tiempo especificado (en
milisegundos). El método interrupt() complementario envía InterruptedEx
ception al hilo que puede usarse para despertar el hilo dormido . Vamos a resolver
esto en el código y crear una nueva clase:

class BRunnable implements Runnable {


int i1, result;
BRunnable(int i1){ this.i1 = i1; }
public int getCurrentResult(){ return this.result; }
public void run() {
for(int i = i1; i < i1 + 6; i++){
//Do something useful here
this.result = i;
try{ Thread.sleep(1000);
} catch(InterruptedException ex){}
}
}
}

El código anterior produce resultados intermedios, que se almacenan en la propiedad


result . Cada vez que se produce un nuevo resultado, el hilo se detiene (duerme)
durante un segundo. En este ejemplo específico, escrito solo con fines demostrativos, el
código no hace nada particularmente útil. Simplemente itera sobre un conjunto de
valores y considera cada uno de ellos un resultado. En el código del mundo real, haría
algunos cálculos basados en el estado actual del sistema y asignaría el valor calculado a
la propiedad result. Ahora usemos esta clase:

BRunnable r1 = new BRunnable(1);


Thread thr1 = new Thread(r1);
thr1.start();

IntStream.range(21, 29)
.peek(i -> thr1.interrupt())
.filter(i -> {
int res = r1.getCurrentResult();
System.out.print(res + " => ");
return res % 2 == 0;
})
.forEach(System.out::println);

El fragmento de código anterior genera una secuencia de enteros: 21, 22, ..., 28. Después
de que se genera cada entero, el hilo principal lo interrumpe thr1 y le permite
generar el siguiente resultado, al que luego se accede a través
del método getCurrentResult(). Si el resultado actual es un número par, el filtro
permite imprimir el flujo del número generado. Si no, se omite. Aquí hay un posible
resultado:

pág. 346
La salida puede verse diferente en diferentes computadoras, pero usted tiene la idea:
de esta manera, un hilo puede controlar la salida de otro hilo.

Hay más...
Hay otros dos métodos importantes que apoyan la cooperación de hilos. El primero es
el método join(), que permite que el hilo actual espere hasta que se termine otro
hilo. Las versiones sobrecargadas de join() aceptan los parámetros que definen
cuánto tiempo debe esperar el subproceso para poder hacer otra cosa.

El método setDaemon() se puede utilizar para hacer que el subproceso finalice


automáticamente después de que todos los subprocesos no daemon se terminen. Por lo
general, los hilos de daemon se usan para procesos de fondo y de apoyo.

Diferentes enfoques de
sincronización
En esta receta, aprenderá sobre los dos métodos más populares para administrar el
acceso concurrente a recursos comunes en Java: synchronized
method y synchronized block.

Prepararse
Dos o más hilos modificando el mismo valor mientras otros hilos lo leen es la
descripción más general de uno de los problemas de acceso concurrente. Los problemas
más sutiles incluyen interferencia de hilo y errores de consistencia de memoria ,
que producen resultados inesperados en fragmentos de código aparentemente
benignos. Vamos a demostrar tales casos y formas de evitarlos.

A primera vista, parece bastante sencillo: solo permita un subproceso a la vez para
modificar / acceder al recurso y listo. Pero si el acceso lleva mucho tiempo, crea un
cuello de botella que podría eliminar la ventaja de tener muchos hilos trabajando en
paralelo. O, si un hilo bloquea el acceso a un recurso mientras espera el acceso a otro

pág. 347
recurso y el segundo hilo bloquea el acceso al segundo recurso mientras espera el
acceso al primero, crea un problema llamado punto muerto . Estos son dos ejemplos
muy simples de los posibles desafíos que un programador debe enfrentar cuando se
trata de múltiples hilos.

Cómo hacerlo...
Primero, reproduciremos un problema causado por la modificación concurrente del
mismo valor. Creemos una clase Calculator que tenga el método
calculate():

class Calculator {
private double prop;
public double calculate(int i){
DoubleStream.generate(new Random()::nextDouble).limit(50);
this.prop = 2.0 * i;
DoubleStream.generate(new Random()::nextDouble).limit(100);
return Math.sqrt(this.prop);
}
}

Este método asigna un valor de entrada a una propiedad y luego calcula su raíz
cuadrada. También insertamos dos líneas de código que generan flujos de 50 y 100
valores. Hicimos esto para mantener el método ocupado durante algún tiempo. De lo
contrario, todo se hace tan rápido que habrá pocas posibilidades de que ocurra
concurrencia. Al agregar el código de generación de 100 valores le da a otro subproceso
la oportunidad de asignar al campo prop otro valor antes de que el subproceso actual
calcule la raíz cuadrada del valor que el subproceso actual acaba de asignar.

Ahora vamos a usar el método calculate() en el siguiente fragmento de código:

Calculator c = new Calculator();


Runnable runnable = () -> System.out.println(IntStream.range(1, 40)
.mapToDouble(c::calculate).sum());
Thread thr1 = new Thread(runnable);
thr1.start();
Thread thr2 = new Thread(runnable);
thr2.start();

Los dos hilos anteriores modifican la misma propiedad del


mismo objeto Calculator al mismo tiempo. Aquí está el resultado que obtuvimos
de una de nuestras carreras:

231.69407148192175
237.44481627598856

I f ejecutar estos ejemplos en su ordenador y no ve el efecto de la concurrencia, tratar


de aumentar el número de dobles generados en la ralentización línea

pág. 348
reemplazando 100 con 1000, por ejemplo. Cuando los resultados de los hilos son
diferentes, significa que en el período entre establecer el valor del campo prop y
devolver su raíz cuadrada en el método calculate(), el otro hilo logró asignar un
valor diferente prop. Este es el caso de interferencia de hilo.

Hay dos formas de proteger el código de tal problema: usando synchronized


method o synchronized block, ambas ayudan a ejecutar el código como una
operación atómica sin ninguna interferencia de otro hilo.

Hacer synchronized method es fácil y directo:

class Calculator{
private double prop;
synchronized public double calculate(int i){
DoubleStream.generate(new Random()::nextDouble).limit(50);
this.prop = 2.0 * i;
DoubleStream.generate(new Random()::nextDouble).limit(100);
return Math.sqrt(this.prop);
}
}

Simplemente agregamos la palabra clave synchronized frente a la definición del


método. Ahora, los resultados de ambos hilos serán siempre los mismos:

233.75710300331153
233.75710300331153

Esto se debe a que otro hilo no puede ingresar al método sincronizado hasta que el hilo
actual (el que ya ha ingresado al método) lo haya salido. Este enfoque puede causar
degradación del rendimiento si el método tarda mucho en ejecutarse. En tales
casos, synchronized block se puede usar, lo que envuelve no todo el método, sino
solo varias líneas de código en una operación atómica. En nuestro caso, podemos
mover la línea de código de desaceleración que genera 50 valores fuera del bloque
sincronizado:

class Calculator{
private double prop;
public double calculate(int i){
DoubleStream.generate(new Random()::nextDouble).limit(50);
synchronized (this) {
this.prop = 2.0 * i;
DoubleStream.generate(new Random()::nextDouble).limit(100);
return Math.sqrt(this.prop);
}
}

De esta manera, la parte sincronizada es mucho más pequeña, por lo que tiene menos
posibilidades de convertirse en un cuello de botella.

pág. 349
synchronized block adquiere un bloqueo en un objeto , cualquier objeto, para el
caso. En una clase enorme, es posible que no note que el objeto actual (esto) se usa
como un bloqueo para varios bloques. Y un bloqueo adquirido en una clase es aún más
susceptible a un intercambio inesperado. Por lo tanto, es mejor usar un bloqueo
dedicado:

class Calculator{
private double prop;
private Object calculateLock = new Object();
public double calculate(int i){
DoubleStream.generate(new Random()::nextDouble).limit(50);
synchronized (calculateLock) {
this.prop = 2.0 * i;
DoubleStream.generate(new Random()::nextDouble).limit(100);
return Math.sqrt(this.prop);
}
}
}

Un bloqueo dedicado tiene un mayor nivel de seguridad de que el bloqueo se utilizará


para acceder solo a un bloque en particular.

Hicimos todos estos ejemplos solo para demostrar los enfoques de sincronización. Si
fueran código real, dejaríamos que cada hilo creara su propio objeto Calculator:

Runnable runnable = () -> {


Calculator c = new Calculator();
System.out.println(IntStream.range(1, 40)
.mapToDouble(c::calculate).sum());
};
Thread thr1 = new Thread(runnable);
thr1.start();
Thread thr2 = new Thread(runnable);
thr2.start();

Esto estaría en línea con la idea general de hacer que las expresiones lambda sean
independientes del contexto en el que se crean. Esto se debe a que, en un entorno
multiproceso, uno nunca sabe cómo se verá el contexto durante su ejecución. El costo
de crear un nuevo objeto cada vez es insignificante a menos que se tenga que procesar
una gran cantidad de datos, y las pruebas aseguran que la sobrecarga de creación de
objetos sea notable.

Los errores de coherencia de memoria pueden tener muchas formas y causas en un


entorno multiproceso. Se discuten bien en el Javadoc del paquete
java.util.concurrent . Aquí, mencionaremos solo el caso más común, que es
causado por la falta de visibilidad. Cuando un subproceso cambia el valor de una
propiedad, el otro puede no ver el cambio inmediatamente y no puede usar la palabra
synchronized clave para un tipo primitivo. En tal situación, considere usar
la palabra volatile clave para la propiedad; garantiza su visibilidad de lectura /
escritura entre diferentes hilos.

pág. 350
Hay más...
En el paquete java.util.concurrent.locks se ensamblan diferentes tipos de
cerraduras para diferentes necesidades y con diferentes comportamientos .

El paquete java.util.concurrent.atomic proporciona soporte para la


programación sin bloqueo y segura para subprocesos en variables individuales.

Las siguientes clases también proporcionan soporte de sincronización:

• Semaphore: Esto restringe el número de subprocesos que pueden acceder a un


recurso
• CountDownLatch: Esto permite que uno o más subprocesos esperen hasta que
se complete un conjunto de operaciones que se realizan en otros subprocesos
• CyclicBarrier: Esto permite que un conjunto de subprocesos esperen entre sí
para alcanzar un punto de barrera común
• Phaser: Esto proporciona una forma más flexible de barrera que puede usarse
para controlar el cálculo por fases entre múltiples hilos
• Exchanger: Esto permite que dos hilos intercambien objetos en un punto de
encuentro y es útil en varios diseños de tuberías

Cada objeto de Java hereda los métodos wait(), notify()y notifyAll() desde el
objeto base. Estos métodos también se pueden usar para controlar el comportamiento
de los hilos y su acceso a los bloqueos.

La clase Collections tiene métodos que sincronizan varias colecciones. Sin


embargo, esto significa que solo las modificaciones de la colección podrían volverse
seguras para subprocesos, no los cambios en los miembros de la colección. Además, al
atravesar la colección a través de su iterador, también debe protegerse porque un
iterador no es seguro para subprocesos. Aquí hay un ejemplo de Javadoc del uso
correcto de un mapa sincronizado:

Map m = Collections.synchronizedMap(new HashMap());


...
Set s = m.keySet(); // Needn't be in synchronized block
...
synchronized (m) { // Synchronizing on m, not s!
Iterator i = s.iterator(); //Must be synchronized block
while (i.hasNext())
foo(i.next());
}

Para agregar más a su placa como programador, debe darse cuenta de que el
siguiente código no es seguro para subprocesos:

pág. 351
List<String> l = Collections.synchronizedList(new ArrayList<>());
l.add("first");
//... code that adds more elements to the list
int i = l.size();
//... some other code
l.add(i, "last");

Esto se debe a que, aunque List l está sincronizado, en el procesamiento


multiproceso, es muy posible que algún otro código agregue más elementos a la lista o
elimine un elemento.

Los problemas de concurrencia no son fáciles de resolver. Es por eso que no es


sorprendente que cada vez más desarrolladores adopten un enfoque más radical. En
lugar de administrar un estado de objeto, prefieren procesar datos en un conjunto de
operaciones sin estado. Vimos ejemplos de dicho código en el Capítulo 5 , Streams y
Pipelines . Parece que Java y muchos lenguajes y sistemas informáticos modernos están
evolucionando en esta dirección.

La inmutabilidad como un medio


para lograr la concurrencia
En esta receta, aprenderá cómo usar la inmutabilidad contra los problemas causados
por la concurrencia.

Prepararse
Un problema de concurrencia ocurre con mayor frecuencia cuando diferentes hilos
modifican y leen datos del mismo recurso compartido. Disminuir el número de
operaciones de modificación disminuye el riesgo de problemas de concurrencia. Aquí
es donde la inmutabilidad, la condición de los valores de solo lectura, entra en escena.

La inmutabilidad del objeto significa la ausencia de medios para cambiar su estado


después de que el objeto ha sido creado. No garantiza la seguridad del subproceso, pero
ayuda a aumentarlo significativamente y proporciona protección suficiente contra
problemas de concurrencia en muchas aplicaciones prácticas.

La creación de un nuevo objeto en lugar de reutilizar uno existente (cambiando su


estado a través de setters y getters) a menudo se percibe como un enfoque
costoso. Pero con el poder de las computadoras modernas, tiene que haber una gran
cantidad de creaciones de objetos para que el rendimiento se vea afectado de manera
significativa. E incluso si ese es el caso, los programadores a menudo eligen cierta
degradación del rendimiento como precio para obtener resultados predecibles.

pág. 352
Cómo hacerlo...
Aquí hay un ejemplo de una clase que produce objetos mutables:

class MutableClass{
private int prop;
public MutableClass(int prop){
this.prop = prop;
}
public int getProp(){
return this.prop;
}
public void setProp(int prop){
this.prop = prop;
}
}

Para hacerlo inmutable, necesitamos eliminar el configurador y agregar


la final palabra clave a su única propiedad y a la clase misma:

final class ImmutableClass{


final private int prop;
public ImmutableClass(int prop){
this.prop = prop;
}
public int getProp(){
return this.prop;
}
}

Agregar la palabra final clave a una clase evita que se extienda, por lo que no se
pueden anular sus métodos. Agregar final a una propiedad privada no es tan
obvio. La motivación es algo compleja y tiene que ver con la forma en que el compilador
reordena los campos durante la construcción del objeto. Si el campo se declara final,
el compilador lo trata como sincronizado. Por eso final es necesario agregar a una
propiedad privada para hacer que el objeto sea completamente inmutable.

El desafío se eleva si la clase está compuesta de otras clases, especialmente las


mutables. Cuando esto sucede, la clase inyectada podría traer código que afectaría a la
clase que lo contiene. Además, la clase interna (mutable), que se recupera mediante
referencias a través del captador, podría modificarse y propagar el cambio dentro de la
clase que lo contiene. La forma de cerrar dichos agujeros es generar nuevos objetos
durante la composición de la recuperación de objetos. Aquí hay un ejemplo de esto:

final class ImmutableClass{


private final double prop;
private final MutableClass mutableClass;
public ImmutableClass(double prop, MutableClass mc){
this.prop = prop;
this.mutableClass = new MutableClass(mc.getProp());
}

pág. 353
public double getProp(){
return this.prop;
}
public MutableClass getMutableClass(){
return new MutableClass(mutableClass.getProp());
}
}

Hay más...
En nuestros ejemplos, utilizamos código muy simple. Si se agrega más complejidad a
cualquiera de los métodos, especialmente con los parámetros (y especialmente cuando
algunos de los parámetros son objetos), es posible que vuelva a tener problemas de
concurrencia:

int getSomething(AnotherMutableClass amc, String whatever){


//... code is here that generates a value "whatever"
amc.setProperty(whatever);
//...some other code that generates another value "val"
amc.setAnotherProperty(val);
return amc.getIntValue();
}

Incluso si este método pertenece ImmutableClass y no afecta el estado del objeto


ImmutableClass, sigue siendo un tema de la raza del hilo y debe analizarse y
protegerse según sea necesario.

La clase Collections tiene métodos que hacen que varias colecciones no se puedan
modificar. Significa que la modificación de la colección en sí se convierte en solo lectura,
no en los miembros de la colección.

Usar colecciones concurrentes


En esta receta, aprenderá sobre las colecciones seguras para subprocesos del paquete
java.util.concurrent .

Prepararse
Una colección se puede sincronizar si le aplica uno de los métodos
Collections.synchronizeXYZ(); Aquí, hemos utilizado XYZ como un marcador
de posición que representa o bien Set, List, Map, o uno de los varios tipos de
colección (consulte la API de la Collections clase ). Ya hemos mencionado que la
sincronización se aplica a la colección en sí, no a su iterador o los miembros de la
colección.

pág. 354
Dichas colecciones sincronizadas también se denominan contenedores porque toda la
funcionalidad sigue siendo proporcionada por las colecciones que se pasan como
parámetros a los métodos, mientras que los contenedores solo proporcionan acceso
seguro para subprocesos. El mismo efecto podría lograrse mediante la adquisición de
un bloqueo en la colección original. Obviamente, tal sincronización incurre en una
sobrecarga de rendimiento en un entorno de subprocesos múltiples, lo que hace que
cada subproceso espere su turno para acceder a la
colección. Collections.synchronizeXYZ()

El paquete java.util.concurrent proporciona una aplicación bien ajustada para


la implementación de rendimiento de colecciones seguras para subprocesos .

Cómo hacerlo...
Cada una de las colecciones concurrentes de los implementos del paquete
java.util.concurrent (o se extiende, si se trata de una interfaz) una de las
cuatro interfaces del paquete java.util: List, Set, Map, o Queue:

1. La interfaz List tiene solo una implementación:


la CopyOnWriteArrayList clase. Lo siguiente está tomado del Javadoc de
esta clase:
"todas las operaciones mutativas (agregar, establecer, etc.) se implementan haciendo
una copia nueva de la matriz subyacente ... El método de iterador de estilo de"
instantánea "utiliza una referencia al estado de la matriz en el punto en que el iterador
era creado. Esta matriz nunca cambia durante la vida útil del iterador, por lo que la
interferencia es imposible y se garantiza que el iterador no
arroje ConcurrentModificationException. El iterador no reflejará adiciones,
eliminaciones o cambios en la lista desde que se creó el iterador. Operaciones de cambio
de elementos en los propios iteradores (eliminar, establecer y agregar) no son
compatibles. Estos métodos arrojan UnsupportedOperationException".

2. Para demostrar el comportamiento de la clase CopyOnWriteArrayList ,


comparémosla con java.util.ArrayList(que no es una implementación segura
para subprocesos de List). Aquí está el método que agrega un elemento a la lista
mientras itera en la misma lista:

void demoListAdd(List<String> list) {


System.out.println("list: " + list);
try {
for (String e : list) {
System.out.println(e);
if (!list.contains("Four")) {
System.out.println("Calling list.add(Four)...");
list.add("Four");
}

pág. 355
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
System.out.println("list: " + list);
}

Considere el siguiente código:

System.out.println("***** ArrayList add():");


demoListAdd(new ArrayList<>(Arrays
.asList("One", "Two", "Three")));

System.out.println();
System.out.println("***** CopyOnWriteArrayList add():");
demoListAdd(new CopyOnWriteArrayList<>(Arrays.asList("One",
"Two", "Three")));

Si ejecutamos este código, el resultado será el siguiente:

Como puede ver, se ArrayList lanza ConcurrentModificationException


cuando la lista se modifica mientras se itera (usamos el mismo hilo por simplicidad y
porque conduce al mismo efecto, como en el caso de otro hilo que modifica la
lista). Sin embargo, la especificación no garantiza que se lanzará la excepción o se
aplicará la modificación de la lista (como en nuestro caso), por lo que un programador
no debe basar la lógica de la aplicación en dicho comportamiento.

La clase CopyOnWriteArrayList , por otro lado, tolera la misma intervención; sin


embargo, tenga en cuenta que no agrega un nuevo elemento a la lista actual porque el
iterador se creó a partir de una instantánea de la copia nueva de la matriz subyacente.

Ahora intentemos eliminar un elemento de la lista simultáneamente mientras


recorremos la lista, usando este método:

void demoListRemove(List<String> list) {


System.out.println("list: " + list);

pág. 356
try {
for (String e : list) {
System.out.println(e);
if (list.contains("Two")) {
System.out.println("Calling list.remove(Two)...");
list.remove("Two");
}
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
System.out.println("list: " + list);
}

Considere el siguiente código:

System.out.println("***** ArrayList remove():");


demoListRemove(new ArrayList<>(Arrays.asList("One",
"Two", "Three")));
System.out.println();
System.out.println("***** CopyOnWriteArrayList remove():");
demoListRemove(new CopyOnWriteArrayList<>(Arrays
.asList("One", "Two", "Three")));

Si ejecutamos esto, obtendremos lo siguiente:

El comportamiento es similar al del ejemplo anterior. La clase


CopyOnWriteArrayList permite el acceso concurrente a la lista pero no permite
modificar la copia de la lista actual.

Sabíamos ArrayList que no sería seguro para subprocesos durante mucho tiempo,
por lo que utilizamos una técnica diferente para eliminar un elemento de la lista al
recorrerlo. Así es como se hizo esto antes del lanzamiento de Java 8:

void demoListIterRemove(List<String> list) {


System.out.println("list: " + list);
try {
Iterator iter = list.iterator();

pág. 357
while (iter.hasNext()) {
String e = (String) iter.next();
System.out.println(e);
if ("Two".equals(e)) {
System.out.println("Calling iter.remove()...");
iter.remove();
}
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
System.out.println("list: " + list);
}

Probemos esto y ejecutemos el código:

System.out.println("***** ArrayList iter.remove():");


demoListIterRemove(new ArrayList<>(Arrays
.asList("One", "Two", "Three")));
System.out.println();
System.out.println("*****"
+ " CopyOnWriteArrayList iter.remove():");
demoListIterRemove(new CopyOnWriteArrayList<>(Arrays
.asList("One", "Two", "Three")));

El resultado será el siguiente:

Esto es exactamente lo que advirtió el Javadoc


( https://docs.oracle.com/cd/E17802_01/j2se/j2se/1.5.0/jcp/bet
a2/apidiffs/java/util/concurrent/CopyOnWriteArrayList.html ):

"Las operaciones de cambio de elementos en los propios iteradores (eliminar, establecer


y agregar) no son compatibles. Estos métodos arrojan UnsupportedOperationException".

Deberíamos recordar esto al actualizar una aplicación para que funcione en un


entorno multiproceso; simplemente cambiar

pág. 358
de ArrayList()a CopyOnWriteArrayList no sería suficiente si utilizamos un
iterador para eliminar un elemento de la lista.

Desde Java 8, hay una mejor manera de eliminar un elemento de una colección usando
una lambda, que debe usarse ya que deja los detalles de la plomería en el código de la
biblioteca:

void demoRemoveIf(Collection<String> collection) {


System.out.println("collection: " + collection);
System.out.println("Calling list.removeIf(e ->"
+ " Two.equals(e))...");
collection.removeIf(e -> "Two".equals(e));
System.out.println("collection: " + collection);
}

Entonces hagamos esto:

System.out.println("***** ArrayList list.removeIf():");


demoRemoveIf(new ArrayList<>(Arrays
.asList("One", "Two", "Three")));
System.out.println();
System.out.println("*****"
+ " CopyOnWriteArrayList list.removeIf():");
demoRemoveIf(new CopyOnWriteArrayList<>(Arrays
.asList("One", "Two", "Three")));

El resultado del código anterior es el siguiente:

Es breve y no tiene problemas con ninguna de las colecciones y está en línea con la
tendencia general de tener un cómputo paralelo sin estado que utiliza flujos con
lambdas e interfaces funcionales.

Además, después de actualizar una aplicación para usar la clase


CopyOnWriteArrayList , podemos aprovechar una forma más simple de agregar
un nuevo elemento a la lista (sin verificar primero si ya está allí):

CopyOnWriteArrayList<String> list =
new CopyOnWriteArrayList<>(Arrays.asList("Five","Six","Seven"));
list.addIfAbsent("One");

pág. 359
Con , esto se puede hacer como una operación atómica, por lo que no es necesario
sincronizar el bloque de código if-not-present-then-add. CopyOnWriteArrayList

2. Vamos a revisar las colecciones concurrentes del paquete


java.util.concurrent que implementa el Set i Nterface. Hay tres de
estos
implementations- ConcurrentHashMap.KeySetView, CopyOnWriteArra
ySety ConcurrentSkipListSet.

El primero es solo una vista de las teclas de ConcurrentHashMap. Está respaldado


por ConcurrentHashMap (que puede ser recuperado por
elgetMap() método ). Revisaremos el comportamiento
de ConcurrentHashMap más adelante.

La segunda implementación de Seten el paquete java.util.concurrent es


la CopyOnWriteArraySet clase. Su comportamiento es similar al de
la CopyOnWriteArrayList clase. De hecho, utiliza
la CopyOnWriteArrayList implementación de la clase bajo el capó. La única
diferencia es que no permite elementos duplicados en la colección.

La tercera (y última) implementación de Set en el paquete


java.util.concurrent es ConcurrentSkipListSet ; implementa una
subinterfaz de Set llamado NavigableSet. De acuerdo con el Javadoc de la clase
ConcurrentSkipListSet , las operaciones de inserción, eliminación y acceso se
ejecutan de manera simultánea de manera segura por varios subprocesos . También
hay algunas limitaciones descritas en el Javadoc:

• No permite el uso de elementos null.


• El tamaño del conjunto se calcula dinámicamente recorriendo la colección,
por lo que puede informar resultados inexactos si esta colección se
modifica durante la operación.
• Las operaciones addAll(), removeIf()y forEach() no están
garantizados para ser realizado de forma atómica. La operación
forEach(), si concurre con una operación addAll(), por ejemplo,
"podría observar solo algunos de los elementos agregados".

La implementación de la clase ConcurrentSkipListSet se basa en la clase


ConcurrentSkipListMap , que discutiremos en breve. Para demostrar el
comportamiento de la clase ConcurrentSkipListSet , comparémosla
con la clase java.util.TreeSet (implementación no concurrente
de NavigableSet). Comenzamos con la eliminación de un elemento:

void demoNavigableSetRemove(NavigableSet<Integer> set) {


System.out.println("set: " + set);

pág. 360
try {
for (int i : set) {
System.out.println(i);
System.out.println("Calling set.remove(2)...");
set.remove(2);
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
System.out.println("set: " + set);
}

Por supuesto, este código no es muy eficiente; Hemos eliminado el mismo elemento
muchas veces sin verificar su presencia. Lo hemos hecho solo con fines
demostrativos. Además, desde Java 8, el mismo método removeIf() funciona bien
Set. Pero nos gustaría mostrar el comportamiento de la nueva clase, así que
ejecutemos este código: ConcurrentSkipListSet

System.out.println("***** TreeSet set.remove(2):");


demoNavigableSetRemove(new TreeSet<>(Arrays
.asList(0, 1, 2, 3)));
System.out.println();
System.out.println("*****"
+ " ConcurrentSkipListSet set.remove(2):");
demoNavigableSetRemove(new ConcurrentSkipListSet<>(Arrays
.asList(0, 1, 2, 3)));

El resultado será el siguiente:

Como se esperaba, la clase maneja la concurrencia e incluso elimina un elemento del


conjunto actual, lo cual es útil. También elimina un elemento a través de un iterador sin
excepción. Considere el siguiente código:ConcurrentSkipListSet

void demoNavigableSetIterRemove(NavigableSet<Integer> set){


System.out.println("set: " + set);
try {

pág. 361
Iterator iter = set.iterator();
while (iter.hasNext()) {
Integer e = (Integer) iter.next();
System.out.println(e);
if (e == 2) {
System.out.println("Calling iter.remove()...");
iter.remove();
}
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
System.out.println("set: " + set);
}

Ejecute esto para TreeSety ConcurrentSkipListSet:

System.out.println("***** TreeSet iter.remove():");


demoNavigableSetIterRemove(new TreeSet<>(Arrays
.asList(0, 1, 2, 3)));

System.out.println();
System.out.println("*****"
+ " ConcurrentSkipListSet iter.remove():");
demoNavigableSetIterRemove(new ConcurrentSkipListSet<>
(Arrays.asList(0, 1, 2, 3)));

No obtendremos ninguna excepción:

Esto se debe a que, según el Javadoc, el iterador de ConcurrentSkipListSetes


débilmente consistente, lo que significa lo siguiente:

pág. 362
• Pueden proceder simultáneamente con otras operaciones
• Nunca tirarán ConcurrentModificationException
• Se garantiza que atravesarán elementos tal como existieron durante la
construcción exactamente una vez, y pueden (pero no se garantiza) reflejar
cualquier modificación posterior a la construcción (del Javadoc)

Esta parte "no garantizada" es algo decepcionante, pero es mejor que obtener una
excepción, como con CopyOnWriteArrayList.

Agregar a una Set clase no es tan problemático como a una clase List porque Set
no permite duplicados y maneja las verificaciones necesarias internamente:

void demoNavigableSetAdd(NavigableSet<Integer> set) {


System.out.println("set: " + set);
try {
int m = set.stream().max(Comparator.naturalOrder())
.get() + 1;
for (int i : set) {
System.out.println(i);
System.out.println("Calling set.add(" + m + ")");
set.add(m++);
if (m > 6) {
break;
}
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
System.out.println("set: " + set);
}

Considere el siguiente código:

System.out.println("***** TreeSet set.add():");


demoNavigableSetAdd(new TreeSet<>(Arrays
.asList(0, 1, 2, 3)));

System.out.println();
System.out.println("*****"
+ " ConcurrentSkipListSet set.add():");
demoNavigableSetAdd(new ConcurrentSkipListSet<>(Arrays
.asList(0,1,2,3)));

Si ejecutamos esto, obtendremos el siguiente resultado:

pág. 363
Como antes, observamos que la Setversión concurrente maneja mejor la
concurrencia.

3. Pasemos a la interfaz Map , que tiene dos implementaciones en el paquete


java.util.concurrent : ConcurrentHashMap y ConcurrentSkipListMap.

La clase ConcurrentHashMap del Javadoc.

" admite concurrencia total de recuperaciones y alta concurrencia para


actualizaciones"

Es una versión segura para subprocesos java.util.HashMap y es análoga


a java.util.Hashtableeste respecto. De hecho,
la clase ConcurrentHashMap cumple los requisitos de la misma especificación
funcional que java.util.Hashtable, aunque su implementación es "algo
diferente en detalles de sincronización" (del Javadoc) .

A diferencia java.util.HashMap
y java.util.Hashtable, ConcurrentHashMap soportes, de acuerdo con su
Javadoc
( https://docs.oracle.com/javase/9/docs/api/java/util/concurre
nt/ConcurrentHashMap.html ),

"un conjunto de operaciones masivas secuenciales y paralelas que, a diferencia de la


mayoría de los métodos de Stream, están diseñadas para aplicarse de manera segura y, a
menudo, sensata, incluso con mapas que están siendo actualizados por otros hilos"

pág. 364
• forEach(): Esto realiza una acción dada en cada elemento
• search(): Esto devuelve el primer resultado no nulo disponible de
aplicar una función dada a cada elemento
• reduce(): Esto acumula cada elemento (hay cinco versiones
sobrecargadas)

Estas operaciones masivas aceptan un argumento parallelismThreshold que


permite diferir la paralelización hasta que el tamaño del mapa alcance el umbral
especificado. Naturalmente, cuando el umbral se establece en Long.MAX_VALUE, no
habrá paralelismo alguno.

Hay muchos otros métodos en la clase API, por lo tanto, consulte su Javadoc para
obtener una descripción general.

A diferencia de (y similar a ), ni tampoco permite nula para ser utilizado como una
clave o
valor. java.util.HashMapjava.util.HashtableConcurrentHashMapCon
currentSkipListMap

La segunda implementación de Map—la ConcurrentSkipListSet clase — se


basa, como mencionamos anteriormente, en la clase ConcurrentSkipListMap ,
por lo que todas las limitaciones de la clase ConcurrentSkipListSet que
acabamos de describir se aplican también a
la ConcurrentSkipListMap clase. La clase ConcurrentSkipListSet es
prácticamente una versión segura de subprocesos
de java.util.TreeMap. SkipList es una estructura de datos ordenada que
permite búsquedas rápidas al mismo tiempo. Todos los elementos se ordenan según
su orden de clasificación natural de claves. La funcionalidad NavigableSet que
demostramos para la clase ConcurrentSkipListSet también está presente en
la clase ConcurrentSkipListMap . Para muchos otros métodos en la clase API,
consulte su Javadoc.

Ahora vamos a demostrar la diferencia en el comportamiento en respuesta a la


concurrencia entre las clases java.util.HashMap, ConcurrentHashMap
y ConcurrentSkipListMap. Primero, escribiremos el método que genera
un objeto Map de prueba :

Map createhMap() {
Map<Integer, String> map = new HashMap<>();
map.put(0, "Zero");
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
return map;
}

pág. 365
Aquí está el código que agrega un elemento a un Mapobjeto simultáneamente:

void demoMapPut(Map<Integer, String> map) {


System.out.println("map: " + map);
try {
Set<Integer> keys = map.keySet();
for (int i : keys) {
System.out.println(i);
System.out.println("Calling map.put(8, Eight)...");
map.put(8, "Eight");

System.out.println("map: " + map);


System.out.println("Calling map.put(8, Eight)...");
map.put(8, "Eight");

System.out.println("map: " + map);


System.out.println("Calling"
+ " map.putIfAbsent(9, Nine)...");
map.putIfAbsent(9, "Nine");

System.out.println("map: " + map);


System.out.println("Calling"
+ " map.putIfAbsent(9, Nine)...");
map.putIfAbsent(9, "Nine");

System.out.println("keys.size(): " + keys.size());


System.out.println("map: " + map);
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
}

Ejecute esto para las tres implementaciones de Map:

System.out.println("***** HashMap map.put():");


demoMapPut(createhMap());

System.out.println();
System.out.println("***** ConcurrentHashMap map.put():");
demoMapPut(new ConcurrentHashMap(createhMap()));

System.out.println();
System.out.println("*****"
+ " ConcurrentSkipListMap map.put():");
demoMapPut(new ConcurrentSkipListMap(createhMap()));

Si hacemos esto, obtenemos una salida solo HashMappara la primera clave:

pág. 366
También obtenemos una salida
para ConcurrentHashMap y ConcurrentSkipListMap para todas las claves,
incluidas las recién agregadas. Aquí está la última sección de
la ConcurrentHashMap salida:

Como ya se mencionó, la apariencia de ConcurrentModificationException no


está garantizada. Ahora vemos que el momento en que se lanza (si se lanza) es el
momento en que el código descubre que la modificación ha tenido lugar. En el caso de
nuestro ejemplo, sucedió en la siguiente iteración. Otro punto que vale la pena señalar
es que el conjunto actual de claves cambia incluso cuando aislamos el conjunto en una
variable separada:

Set<Integer> keys = map.keySet();

Esto nos recuerda que no descartemos los cambios propagados a través de los objetos
a través de sus referencias.

Para ahorrarnos espacio y tiempo, no mostraremos el código para la eliminación


simultánea y solo resumiremos los resultados. Como se esperaba, HashMap lanza
la excepción ConcurrentModificationException cuando un elemento se
elimina de alguna de las siguientes maneras:

pág. 367
String result = map.remove(2);
boolean success = map.remove(2, "Two");

La eliminación simultánea se puede hacer usando Iterator una de las siguientes


formas:

iter.remove();
boolean result = map.keySet().remove(2);
boolean result = map.keySet().removeIf(e -> e == 2);

Por el contrario, las dos Map implementaciones simultáneas permiten la eliminación


de un elemento concurrente no solo el uso Iterator.

Un comportamiento similar también se exhibe por todas las implementaciones


simultáneas de la
interfaz Queue : LinkedTransferQueue, LinkedBlockingQueue,LinkedBloc
kingDequeue , ArrayBlockingQueue, PriorityBlockingQueue, DelayQue
ue, SynchronousQueue, ConcurrentLinkedQueue,
y ConcurrentLinkedDequeue, todo ello en el paquete
java.util.concurrent. Pero demostrarlos todos requeriría un volumen
separado, por lo que le dejamos que explore el Javadoc y proporcionemos un
ejemplo ArrayBlockingQueue únicamente. La cola estará representada por
la clase QueueElement:

class QueueElement {
private String value;
public QueueElement(String value){
this.value = value;
}
public String getValue() {
return value;
}
}

El productor de la cola será el siguiente:

class QueueProducer implements Runnable {


int intervalMs, consumersCount;
private BlockingQueue<QueueElement> queue;
public QueueProducer(int intervalMs, int consumersCount,
BlockingQueue<QueueElement> queue) {
this.consumersCount = consumersCount;
this.intervalMs = intervalMs;
this.queue = queue;
}
public void run() {
List<String> list =
List.of("One","Two","Three","Four","Five");
try {
for (String e : list) {
Thread.sleep(intervalMs);
queue.put(new QueueElement(e));

pág. 368
System.out.println(e + " produced" );
}
for(int i = 0; i < consumersCount; i++){
queue.put(new QueueElement("Stop"));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

El siguiente será el consumidor de la cola:

class QueueConsumer implementa Runnable {


private String name ;
intervalo int privado Ms ; privado BlockingQueue
<QueueElement> queue ; public QueueConsumer (nombre de cadena ,
int intervaloMs , cola BlockingQueue
<QueueElement>) { esto . IntervaloMs = IntervaloMs ; esta .
cola = cola ; esta . nombre = nombre ;

}
public void run () {
try {
while ( true ) {
Valor de cadena = queue .take (). getValue () ;
if ( "Stop" .equals (value)) {
break;
}
Sistema. out .println (valor + "consumido por" + nombre )
;
Hilo. dormir ( intervaloMs ) ;
}
} catch (InterruptedException e) {
e.printStackTrace () ;
}
}
} class QueueConsumer implements Runnable{
private String name;
private int intervalMs;
private BlockingQueue<QueueElement> queue;
public QueueConsumer(String name, int intervalMs,
BlockingQueue<QueueElement> queue){
this.intervalMs = intervalMs;
this.queue = queue;
this.name = name;
}
public void run() {
try {
while(true){
String value = queue.take().getValue();
if("Stop".equals(value)){
break;
}

pág. 369
System.out.println(value + " consumed by " + name);
Thread.sleep(intervalMs);
}
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}

Ejecute el siguiente código:

BlockingQueue<QueueElement> queue =
new ArrayBlockingQueue<>(5);
QueueProducer producer = new QueueProducer(queue);
QueueConsumer consumer = new QueueConsumer(queue);
new Thread(producer).start();
new Thread(consumer).start();

Sus resultados pueden verse así:

Cómo funciona...
Antes de seleccionar qué colecciones usar, lea el Javadoc para ver si las limitaciones de
la colección son aceptables para su aplicación.

Por ejemplo, según el Javadoc, la clase CopyOnWriteArrayList

"normalmente es demasiado costoso, pero puede ser más eficiente que las alternativas
cuando las operaciones de desplazamiento superan ampliamente las mutaciones, y es útil
cuando no puede o no desea sincronizar los recorridos, pero debe evitar la interferencia
entre hilos concurrentes".

Úselo cuando no necesite agregar nuevos elementos en diferentes posiciones y no


necesite ordenarlos. De lo contrario, use ConcurrentSkipListSet.

Las clases ConcurrentSkipListSet y ConcurrentSkipListMap, según el


Javadoc,

pág. 370
"proporciona el costo de tiempo promedio esperado de registro (n) para las operaciones
de contener, agregar y eliminar y sus variantes. Las vistas ordenadas ascendentes y sus
iteradores son más rápidos que los descendentes".

Úselos cuando necesite iterar rápidamente a través de los elementos en un cierto orden.

U tilice ConcurrentHashMapcuando los requisitos de concurrencia son muy


exigentes y hay que permitir el bloqueo en la operación de escritura, pero no es
necesario para bloquear el elemento.

ConcurrentLinkedQueque y ConcurrentLinkedDeque son una opción


apropiada cuando muchos hilos comparten acceso a una colección
común. ConcurrentLinkedQueque emplea un algoritmo eficiente sin bloqueo.

PriorityBlockingQueue es una mejor opción cuando un orden natural es


aceptable y necesita agregar elementos rápidamente a la cola y eliminar elementos
rápidamente del encabezado de la cola. El bloqueo significa que la cola espera volverse
no vacía cuando se recupera un elemento y espera que haya espacio disponible en la
cola cuando se almacena un elemento.

ArrayBlockingQueue, LinkedBlockingQueuey LinkedBlockingDeque


tienen un tamaño fijo (están delimitados). Las otras colas son ilimitadas.

Use estas y similares características y recomendaciones como pautas, pero ejecute


pruebas exhaustivas y mediciones de rendimiento antes y después de implementar su
funcionalidad.

Usar el servicio ejecutor para


ejecutar tareas asíncronas
En esta receta, aprenderá a usar ExecutorService para implementar la ejecución
de subprocesos controlable.

Prepararse
En una receta anterior, demostramos cómo crear y ejecutar hilos usando
la Threadclase directamente. Es un mecanismo aceptable para un pequeño número
de subprocesos que se ejecutan y producen resultados previsiblemente rápidos. Para
aplicaciones a gran escala con subprocesos de ejecución más larga con lógica compleja
(que podría mantenerlos con vida durante un tiempo impredecible) y / o una cantidad
de subprocesos que crecen también de manera impredecible, podría resultar un

pág. 371
enfoque simple de crear y ejecutar hasta salir en caso de OutOfMemoryerror o
requiere un complejo sistema personalizado de mantenimiento y gestión del estado de
los hilos. Para tales casos, ExecutorService y las clases relacionadas
del java.util.concurrentpaquete proporcionan una solución lista para usar que
libera al programador de la necesidad de escribir y mantener una gran cantidad de
código de infraestructura.

En la base del Executor Framework se encuentra una Executorinterfaz que tiene un


solo método void execute(Runnable command) que ejecuta el comando dado
en algún momento en el futuro.

Su subinterfaz, ExecutorService agrega métodos que le permiten administrar el


ejecutor:

• Los métodos invokeAny(), invokeAll(),


y awaitTermination() y submit() permiten definir cómo se ejecutarán las
roscas y si se espera que regresen algunos valores
• Los métodos shutdown()y le shutdownNow()permiten cerrar el ejecutor
• Los métodos isShutdown()y isTerminated()proporcionan el estado del
ejecutor

Los objetos de ExecutorServicese pueden crear con los métodos de fábrica


estáticos de la clase java.util.concurrent.Executors :

• newSingleThreadExecutor(): Crea un Executor método que utiliza un


único subproceso de trabajo que funciona desde una cola sin límites. Tiene una
versión sobrecargada con ThreadFactorycomo parámetro.
• newCachedThreadPool(): Crea un grupo de subprocesos que crea nuevos
subprocesos según sea necesario, pero reutiliza los subprocesos construidos
previamente cuando están disponibles. Tiene una versión sobrecargada
con ThreadFactory como parámetro.
• newFixedThreadPool(int nThreads): Crea un grupo de subprocesos que
reutiliza un número fijo de subprocesos que funcionan desde una cola compartida
no acotada. Tiene una versión sobrecargada con ThreadFactory como
parámetro .

La implementación ThreadFactory le permite anular el proceso de creación de


nuevos subprocesos, permitiendo que las aplicaciones utilicen subclases de
subprocesos especiales, prioridades, etc. Una demostración de su uso está fuera del
alcance de este libro.

Cómo hacerlo...
pág. 372
1. Un aspecto importante del comportamiento de la interfaz Executor que debe
recordar es que una vez creada, sigue ejecutándose (esperando que se ejecuten
nuevas tareas) hasta que se detenga el proceso de Java. Entonces, si desea liberar
memoria, la interfaz debe detenerse explícitamente. Si no se apaga, los ejecutores
olvidados crearán una pérdida de memoria. Aquí hay una forma posible de
asegurarse de que ningún ejecutor se quede atrás: Executor

int shutdownDelaySec = 1;
ExecutorService execService =
Executors.newSingleThreadExecutor();
Runnable runnable = () -> System.out.println("Worker One did
the job.");
execService.execute(runnable);
runnable = () -> System.out.println("Worker Two did the
job.");
Future future = execService.submit(runnable);
try {
execService.shutdown();
execService.awaitTermination(shutdownDelaySec,
TimeUnit.SECONDS);
} catch (Exception ex) {
System.out.println("Caught around"
+ " execService.awaitTermination(): "
+ ex.getClass().getName());
} finally {
if (!execService.isTerminated()) {
if (future != null && !future.isDone()
&& !future.isCancelled()){
System.out.println("Cancelling the task...");
future.cancel(true);
}
}
List<Runnable> l = execService.shutdownNow();
System.out.println(l.size()
+ " tasks were waiting to be executed."
+ " Service stopped.");
}

Puede pasar a un trabajador (una implementación de la interfaz


funcional Runnable o) para su ejecución de varias maneras, que veremos en breve. En
este ejemplo, ejecutamos dos hilos: uno usando el método y otro usando
el método. Ambos métodos aceptan o, pero solo los usamos en este
ejemplo. El método devuelve, que representa el resultado de un cálculo
asincrónico. CallableExecutorServiceexecute()submit()RunnableCall
ableRunnablesubmit()Future

El método shutdown() inicia un cierre ordenado de las tareas enviadas


previamente y evita que se acepten nuevas tareas. Este método no espera a que la
tarea complete la ejecución. El método awaitTermination() hace eso. Pero
después shutdownDelaySec, deja de bloquear y el flujo de código ingresa al bloque
finally, donde el método isTerminated() regresa true si todas las tareas se
completan después del apagado. En este ejemplo, tenemos dos tareas ejecutadas en

pág. 373
dos declaraciones diferentes. Pero tenga en cuenta que otros métodos
de ExecutorService aceptar una colección de tareas.

En tal caso, cuando el servicio se está cerrando, iteramos sobre la colección de objetos
Future. Llamamos a cada tarea y la cancelamos si aún no se ha completado,
posiblemente haciendo otra cosa antes de cancelar la tarea. Cuánto tiempo de espera
(el valor de shutdownDelaySec) debe probarse para cada aplicación y las posibles
tareas en ejecución.

Finalmente, el shutdownNow()método dice que

"intenta detener todas las tareas que se ejecutan activamente, detiene el procesamiento
de las tareas en espera y devuelve una lista de las tareas que estaban en espera de
ejecución"

(según el Javadoc).

2. Recoge y evalúa los resultados. En una aplicación real, normalmente no queremos


cerrar un servicio a menudo. Simplemente verificamos el estado de las tareas y recopilamos
los resultados de aquellos que devuelven verdadero del método isDone() . En el
ejemplo de código anterior, solo mostramos cómo asegurarnos de que cuando detengamos
el servicio, lo hagamos de manera controlada, sin dejar ningún proceso fuera de
control. Si ejecutamos ese código de ejemplo, obtendremos lo siguiente:

3. Generalice el código anterior y cree un método que cierre un servicio y la tarea que
ha devuelto Future:

void shutdownAndCancelTask(ExecutorService execService,


int shutdownDelaySec, String name, Future future) {
try {
execService.shutdown();
System.out.println("Waiting for " + shutdownDelaySec
+ " sec before shutting down service...");
execService.awaitTermination(shutdownDelaySec,
TimeUnit.SECONDS);
} catch (Exception ex) {
System.out.println("Caught around"
+ " execService.awaitTermination():"
+ ex.getClass().getName());
} finally {
if (!execService.isTerminated()) {
System.out.println("Terminating remaining tasks...");
if (future != null && !future.isDone()
&& !future.isCancelled()) {
System.out.println("Cancelling task "
+ name + "...");

pág. 374
future.cancel(true);
}
}
System.out.println("Calling execService.shutdownNow("
+ name + ")...");
List<Runnable> l = execService.shutdownNow();
System.out.println(l.size() + " tasks were waiting"
+ " to be executed. Service stopped.");
}
}

4. Mejore el ejemplo haciendo que Runnable (usando la expresión lambda)


duerma durante un tiempo (simulando un trabajo útil por hacer):

void executeAndSubmit(ExecutorService execService,


int shutdownDelaySec, int threadSleepsSec) {
System.out.println("shutdownDelaySec = "
+ shutdownDelaySec + ", threadSleepsSec = "
+ threadSleepsSec);
Runnable runnable = () -> {
try {
Thread.sleep(threadSleepsSec * 1000);
System.out.println("Worker One did the job.");
} catch (Exception ex) {
System.out.println("Caught around One Thread.sleep(): "
+ ex.getClass().getName());
}
};
execService.execute(runnable);
runnable = () -> {
try {
Thread.sleep(threadSleepsSec * 1000);
System.out.println("Worker Two did the job.");
} catch (Exception ex) {
System.out.println("Caught around Two Thread.sleep(): "
+ ex.getClass().getName());
}
};
Future future = execService.submit(runnable);
shutdownAndCancelTask(execService, shutdownDelaySec,
"Two", future);
}

Tenga en cuenta los dos parámetros, shutdownDelaySec(define cuánto tiempo


esperará el servicio sin permitir que se envíen nuevas tareas antes de continuar y
cerrarse, eventualmente) y threadSleepSec(define cuánto tiempo está durmiendo
el trabajador, lo que indica que el proceso de simulación está haciendo su trabajo) .

5. Ejecute el nuevo código para diferentes implementaciones


de ExecutorService y los valores shutdownDelaySec y threadSleepSec:

System.out.println("Executors.newSingleThreadExecutor():");
ExecutorService execService =
Executors.newSingleThreadExecutor();
executeAndSubmit(execService, 3, 1);

pág. 375
System.out.println();
System.out.println("Executors.newCachedThreadPool():");
execService = Executors.newCachedThreadPool();
executeAndSubmit(execService, 3, 1);

System.out.println();
int poolSize = 3;
System.out.println("Executors.newFixedThreadPool("
+ poolSize + "):");
execService = Executors.newFixedThreadPool(poolSize);
executeAndSubmit(execService, 3, 1);

Así es como puede verse la salida (puede ser ligeramente diferente en su computadora,
dependiendo del momento exacto de los eventos controlados por el sistema operativo):

6. Analiza los resultados. En el primer ejemplo, no encontramos sorpresa debido a la


siguiente línea:

execService.awaitTermination(shutdownDelaySec,
TimeUnit.SECONDS);

Se bloquea durante tres segundos, mientras que cada trabajador trabaja solo durante
un segundo. Por lo tanto, es suficiente tiempo para que cada trabajador complete su
trabajo incluso para un ejecutor de subproceso único.

Hagamos que el servicio espere solo un segundo:

Cuando haga esto, notará que ninguna de las tareas se completará. En este caso, el
trabajador Onefue interrumpido (ver la última línea de la salida), mientras que la
tarea Twofue cancelada.

Hagamos que el servicio espere tres segundos:

pág. 376
Ahora vemos que el trabajador Onepudo completar su tarea, mientras que el
trabajador Twofue interrumpido.

La interfaz ExecutorService producida


por newCachedThreadPool()o newFixedThreadPool()funciona de manera
similar en una computadora de un solo núcleo. La única diferencia significativa es que
si el valor shutdownDelaySec es igual al threadSleepSecvalor, ambos le
permiten completar los hilos:

Este fue el resultado del uso newCachedThreadPool(). La salida del ejemplo


usando se newFixedThreadPool() ve exactamente igual en una computadora de
un núcleo.

7. Para tener más control sobre la tarea, verifique el valor devuelto del objeto
Future , no solo envíe una tarea y espere esperando que se complete según sea
necesario. Hay otro método, llamado submit(), en la interfaz ExecutorService
que le permite no solo devolver un objeto Future sino también incluir el resultado que
se pasa al método como un segundo parámetro en el objeto de retorno. Veamos un
ejemplo de esto:
8. Future<Integer> future = execService.submit(() ->
System.out.println("Worker 42 did the job."), 42);
int result = future.get();

El valor de result es 42. Este método puede ser útil cuando ha enviado muchos
trabajadores ( nWorkers) y necesita saber cuál se ha completado:

Set<Integer> set = new HashSet<>();


while (set.size() < nWorkers){
for (Future<Integer> future : futures) {
if (future.isDone()){

pág. 377
try {
String id = future.get(1, TimeUnit.SECONDS);
if(!set.contains(id)){
System.out.println("Task " + id + " is done.");
set.add(id);
}
} catch (Exception ex) {
System.out.println("Caught around future.get(): "
+ ex.getClass().getName());
}
}
}
}

Bueno, la trampa es que future.get()es un método de bloqueo. Es por eso que


usamos una versión del método get()que nos permite establecer
el delaySec tiempo de espera. De lo contrario, get()bloquea la iteración.

Cómo funciona...
Avancemos un paso más hacia el código de la vida real y creemos una clase que
implemente Callable y le permita devolver un resultado de un trabajador como un
objeto de la clase Result :

class Result {
private int sleepSec, result;
private String workerName;
public Result(String workerName, int sleptSec, int result) {
this.workerName = workerName;
this.sleepSec = sleptSec;
this.result = result;
}
public String getWorkerName() { return this.workerName; }
public int getSleepSec() { return this.sleepSec; }
public int getResult() { return this.result; }
}

El método getResult() devuelve un resultado numérico real . Aquí, también


incluimos el nombre del trabajador y cuánto tiempo se espera que el subproceso
duerma (funcione) solo por conveniencia y para ilustrar mejor el resultado.

El trabajador en sí será una instancia de la clase CallableWorkerImpl :

class CallableWorkerImpl implements CallableWorker<Result>{


private int sleepSec;
private String name;
public CallableWorkerImpl(String name, int sleepSec) {
this.name = name;
this.sleepSec = sleepSec;
}
public String getName() { return this.name; }
public int getSleepSec() { return this.sleepSec; }

pág. 378
public Result call() {
try {
Thread.sleep(sleepSec * 1000);
} catch (Exception ex) {
System.out.println("Caught in CallableWorker: "
+ ex.getClass().getName());
}
return new Result(name, sleepSec, 42);
}
}

Aquí, el número 42 es un resultado numérico real, que un trabajador supuestamente


calculó (mientras dormía). La clase CallableWorkerImpl implementó
la interfaz CallableWorker:

interface CallableWorker<Result> extends Callable<Result> {


default String getName() { return "Anonymous"; }
default int getSleepSec() { return 1; }
}

Tuvimos que hacer que los métodos sean predeterminados y devolver algunos datos
(de todos modos serán anulados por la implementación de la clase) para preservar
su estado functional interface. De lo contrario, no podríamos usarlo en
expresiones lambda.

También crearemos una fábrica que generará una lista de trabajadores:

List<CallableWorker<Result>> createListOfCallables(int nSec){


return List.of(new CallableWorkerImpl("One", nSec),
new CallableWorkerImpl("Two", 2 * nSec),
new CallableWorkerImpl("Three", 3 * nSec));
}

Ahora podemos usar todas estas nuevas clases y métodos para demostrar el método
invokeAll():

void invokeAllCallables(ExecutorService execService,


int shutdownDelaySec, List<CallableWorker<Result>> callables) {
List<Future<Result>> futures = new ArrayList<>();
try {
futures = execService.invokeAll(callables, shutdownDelaySec,
TimeUnit.SECONDS);
} catch (Exception ex) {
System.out.println("Caught around execService.invokeAll(): "
+ ex.getClass().getName());
}
try {
execService.shutdown();
System.out.println("Waiting for " + shutdownDelaySec
+ " sec before terminating all tasks...");
execService.awaitTermination(shutdownDelaySec,
TimeUnit.SECONDS);
} catch (Exception ex) {
System.out.println("Caught around awaitTermination(): "

pág. 379
+ ex.getClass().getName());
} finally {
if (!execService.isTerminated()) {
System.out.println("Terminating remaining tasks...");
for (Future<Result> future : futures) {
if (!future.isDone() && !future.isCancelled()) {
try {
System.out.println("Cancelling task "
+ future.get(shutdownDelaySec,
TimeUnit.SECONDS).getWorkerName());
future.cancel(true);
} catch (Exception ex) {
System.out.println("Caught at cancelling task: "
+ ex.getClass().getName());
}
}
}
}
System.out.println("Calling execService.shutdownNow()...");
execService.shutdownNow();
}
printResults(futures, shutdownDelaySec);
}

El método printResults()genera los resultados recibidos de los trabajadores:

void printResults(List<Future<Result>> futures, int timeoutSec) {


System.out.println("Results from futures:");
if (futures == null || futures.size() == 0) {
System.out.println("No results. Futures"
+ (futures == null ? " = null" : ".size()=0"));
} else {
for (Future<Result> future : futures) {
try {
if (future.isCancelled()) {
System.out.println("Worker is cancelled.");
} else {
Result result = future.get(timeoutSec, TimeUnit.SECONDS);
System.out.println("Worker "+ result.getWorkerName() +
" slept " + result.getSleepSec() +
" sec. Result = " + result.getResult());
}
} catch (Exception ex) {
System.out.println("Caught while getting result: "
+ ex.getClass().getName());
}
}
}
}

Para obtener los resultados, nuevamente usamos una versión del método get()con
la configuración de tiempo de espera. Ejecute el siguiente código:

List<CallableWorker<Result>> callables = createListOfCallables(1);


System.out.println("Executors.newSingleThreadExecutor():");
ExecutorService execService = Executors.newSingleThreadExecutor();
invokeAllCallables(execService, 1, callables);

pág. 380
Su salida será la siguiente:

Probablemente vale la pena mencionar que los tres trabajadores fueron creados con
tiempos de sueño de uno, dos y tres segundos, mientras que el tiempo de espera antes
de que el servicio se cierre es de un segundo. Es por eso que todos los trabajadores
fueron cancelados.

Ahora, si establecemos el tiempo de espera en seis segundos, la salida del ejecutor de


un solo hilo será la siguiente:

Naturalmente, si aumentamos nuevamente el tiempo de espera, todos los


trabajadores podrán completar sus tareas.

La interfaz ExecutorService producida


por newCachedThreadPool() o newFixedThreadPool() funciona mucho
mejor incluso en una computadora de un núcleo:

Como puede ver, todos los hilos pudieron completarse incluso con tres segundos de
tiempo de espera.

Como alternativa, en lugar de establecer un tiempo de espera durante el cierre del


servicio, puede configurarlo en la versión sobrecargada del método invokeAll():

pág. 381
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)

Hay un aspecto particular invokeAll() del comportamiento del método que a


menudo se pasa por alto y causa sorpresas para los usuarios nuevos: regresa solo
después de completar todas las tareas (ya sea normalmente o lanzando una
excepción). Lea el Javadoc y experimente hasta que reconozca que este
comportamiento es aceptable para su aplicación.

Por el contrario, el método invokeAny()bloquea solo hasta que al menos una tarea
es

"completado exitosamente (sin lanzar una excepción), si alguno lo hace. Tras un retorno
normal o excepcional, las tareas que no se han completado se cancelan"

La cita anterior es del Javadoc


( https://docs.oracle.com/javase/7/docs/api/java/util/concurre
nt/ExecutorService.html ). Aquí hay un ejemplo del código que hace esto:

void invokeAnyCallables(ExecutorService execService,


int shutdownDelaySec, List<CallableWorker<Result>> callables) {
Result result = null;
try {
result = execService.invokeAny(callables, shutdownDelaySec,
TimeUnit.SECONDS);
} catch (Exception ex) {
System.out.println("Caught around execService.invokeAny(): "
+ ex.getClass().getName());
}
shutdownAndCancelTasks(execService, shutdownDelaySec,
new ArrayList<>());
if (result == null) {
System.out.println("No result from execService.invokeAny()");
} else {
System.out.println("Worker " + result.getWorkerName() +
" slept " + result.getSleepSec() +
" sec. Result = " + result.getResult());
}
}

Puede experimentar con él, estableciendo diferentes valores para el tiempo de espera
( shutdownDelaySec) y el tiempo de suspensión de los subprocesos hasta que se
sienta cómodo con el comportamiento de este método. Como puede ver, hemos
reutilizado el método shutdownAndCancelTasks()pasando una lista vacía
de objetos Future, ya que no los tenemos aquí.

Hay más...
pág. 382
Hay dos métodos de fábrica más estáticos en la Executors clase que crean instancias
de ExecutorService:

• newWorkStealingPool(): Esto crea un grupo de subprocesos que roba el


trabajo utilizando el número de procesadores disponibles como su nivel de
paralelismo objetivo. Tiene una versión sobrecargada con un nivel de paralelismo
como parámetro.
• unconfigurableExecutorService(ExecutorService executor):
Esto devuelve un objeto que delega todos
los métodos definidos ExecutorService al ejecutor dado, excepto aquellos
métodos a los que de otra manera se podría acceder usando conversiones.

Además, una subinterfaz de la interfaz


ExecutorService , llamada ScheduledExecutorService, mejora la API con la
capacidad de programar una ejecución de subprocesos en el futuro y / o su ejecución
periódica.

Los objetos de ScheduledExecutorService se pueden crear utilizando los


métodos de fábrica estáticos de la java.util.concurrent.Executors clase:

• newSingleThreadScheduledExecutor(): Crea un ejecutor de subproceso


único que puede programar comandos para que se ejecuten después de un retraso
determinado o para ejecutarlos periódicamente. Tiene una versión sobrecargada
con ThreadFactory como parámetro .
• newScheduledThreadPool(int corePoolSize): Crea un grupo de
subprocesos que puede programar comandos para que se ejecuten después de un
retraso determinado o para ejecutarlos periódicamente. Tiene una versión
sobrecargada con ThreadFactory como parámetro .
• unconfigurableScheduledExecutorService(
ScheduledExecutorService executor ): Devuelve un objeto que delega
todos los ScheduledExecutorServicemétodos definidos al ejecutor dado,
pero no cualquier otro método al que de otra manera se podría acceder mediante
conversiones.

La clase Executors también tiene varios métodos sobrecargados que aceptan,


ejecutan y devuelven Callable (que, en contraste con Runnable, contiene el
resultado).

El paquete java.util.concurrent también incluye clases que


implementan ExecutorService:

• ThreadPoolExecutor: Esta clase ejecuta cada tarea enviada usando uno de


los varios hilos agrupados, normalmente configurados usando los métodos de
fábrica Executors.

pág. 383
• ScheduledThreadPoolExecutor: Esta clase extiende
la clase ThreadPoolExecutor e implementa
la interfaz ScheduledExecutorService .
• ForkJoinPool: Gestiona la ejecución de trabajadores
(los ForkJoinTaskprocesos) utilizando un algoritmo de robo de trabajo. Lo
discutiremos en la próxima receta.

Las instancias de estas clases se pueden crear a través de constructores de clases que
aceptan más parámetros, incluida la cola que contiene los resultados, para
proporcionar una gestión más refinada de agrupaciones de hebras.

Usando fork / join para


implementar divide-and-conquer
En esta receta, aprenderá a usar el marco de fork / join para el patrón de cálculo divide
y vencerás .

Prepararse
Como se mencionó en la receta anterior, la clase ForkJoinPool es una
implementación de la interfaz ExecutorService que gestiona la ejecución de los
trabajadores, los procesos ForkJoinTask , utilizando el algoritmo de robo de
trabajo . Que se aprovecha de múltiples procesadores, si está disponible, y funciona
mejor en las tareas que se pueden desglosar en tareas más pequeñas de forma
recursiva, que también se llama un divide y vencerás estrategia.

Cada subproceso en el grupo tiene una cola (deque) de doble extremo dedicada que
almacena tareas, y el subproceso recoge la siguiente tarea (desde el encabezado de la
cola) tan pronto como se completa la tarea actual. Cuando otro hilo termina de ejecutar
todas las tareas en su cola, puede tomar una tarea (robarla) de la cola de una cola no
vacía de otro hilo.

Al igual que con cualquier implementación ExecutorService, el framework fork /


join distribuye tareas a subprocesos de trabajo en un grupo de subprocesos. Este marco
es distinto porque utiliza un algoritmo de robo de trabajo. Los subprocesos de trabajo
que se quedan sin tareas pueden robar tareas de otros subprocesos que todavía están
ocupados.

Tal diseño equilibra la carga y permite un uso eficiente de los recursos.

pág. 384
Con fines demostrativos, vamos a utilizar la API creada en el capítulo 3 , la
programación modular , el TrafficUnit, SpeedModel y Vehicle las interfaces y
las clases TrafficUnitWrapper, FactoryTraffic, FactoryVehicle,
y FactorySpeedModel . También confiaremos en las transmisiones y las
canalizaciones de transmisiones descritas en el Capítulo 3 , Programación
modular .

Solo para refrescar tu memoria, aquí está la TrafficUnitWrapper clase:

class TrafficUnitWrapper {
private double speed;
private Vehicle vehicle;
private TrafficUnit trafficUnit;
public TrafficUnitWrapper(TrafficUnit trafficUnit){
this.trafficUnit = trafficUnit;
this.vehicle = FactoryVehicle.build(trafficUnit);
}
public TrafficUnitWrapper setSpeedModel(SpeedModel speedModel) {
this.vehicle.setSpeedModel(speedModel);
return this;
}
TrafficUnit getTrafficUnit(){ return this.trafficUnit;}
public double getSpeed() { return speed; }

public TrafficUnitWrapper calcSpeed(double timeSec) {


double speed = this.vehicle.getSpeedMph(timeSec);
this.speed = Math.round(speed * this.trafficUnit.getTraction());
return this;
}
}

También modificaremos ligeramente la interfaz API existente y la haremos un poco


más compacta mediante la introducción de una nueva clase DateLocation :

class DateLocation {
private int hour;
private Month month;
private DayOfWeek dayOfWeek;
private String country, city, trafficLight;

public DateLocation(Month month, DayOfWeek dayOfWeek,


int hour, String country, String city,
String trafficLight) {
this.hour = hour;
this.month = month;
this.dayOfWeek = dayOfWeek;
this.country = country;
this.city = city;
this.trafficLight = trafficLight;
}
public int getHour() { return hour; }
public Month getMonth() { return month; }
public DayOfWeek getDayOfWeek() { return dayOfWeek; }
public String getCountry() { return country; }
public String getCity() { return city; }

pág. 385
public String getTrafficLight() { return trafficLight;}
}

También le permitirá ocultar los detalles y le ayudará a ver los aspectos importantes
de esta receta.

Cómo hacerlo...
Todos los cálculos se encapsulan dentro de una subclase de una de las dos subclases
( RecursiveAction o RecursiveTask<T>) de la clase
abstracta ForkJoinTask claseRecursiveActionvoid compute(). Puede
ampliar (e implementar el método ) o RecursiveTask<T> (e implementar
el método T compute() ). Como habrás notado, puedes elegir extender la clase
RecursiveAction para tareas que no devuelven ningún valor, y
extender RecursiveTask<T> cuando necesites que tus tareas devuelvan un
valor. En nuestra demostración, vamos a utilizar este último porque es un poco más
complejo.

Digamos que nos gustaría calcular la velocidad promedio del tráfico en una ubicación
determinada en una fecha y hora determinadas y condiciones de manejo (todos estos
parámetros están definidos por el objeto DateLocation de propiedad). Otros
parámetros serán los siguientes:

• timeSec: La cantidad de segundos durante los cuales los vehículos tienen la


posibilidad de acelerar después de detenerse en el semáforo
• trafficUnitsNumber: La cantidad de vehículos a incluir en el cálculo de la
velocidad promedio

Naturalmente, cuantos más vehículos se incluyan en los cálculos, mejor será la


predicción. Pero a medida que aumenta este número, también aumenta el número de
cálculos. Esto da lugar a la necesidad de dividir el número de vehículos en grupos más
pequeños y calcular la velocidad promedio de cada grupo en paralelo con los
demás. Sin embargo, hay un cierto número mínimo de cálculos que no vale la pena
dividir entre dos hilos. Esto es lo que Javadoc
( https://docs.oracle.com/javase/8/docs/api/java/util/concurre
nt/ForkJoinTask.html ) tiene que decir al respecto:

"Como regla general, una tarea debe realizar más de 100 y menos de 10000 pasos
computacionales básicos, y debe evitar bucles indefinidos. Si las tareas son demasiado
grandes, el paralelismo no puede mejorar el rendimiento. Si es demasiado pequeño,
entonces la memoria y la memoria interna la sobrecarga de mantenimiento de tareas
puede abrumar el procesamiento ".

pág. 386
Sin embargo, como siempre, la determinación del número óptimo de cálculos sin
dividirlos entre hilos paralelos debe basarse en pruebas. Es por eso que le
recomendamos que lo pase como parámetro. Llamaremos a este
parámetro threshold. Tenga en cuenta que también sirve como criterio para salir de
la recursividad.

Llamaremos a nuestra clase (tarea) AverageSpeed y


ampliaremos RecursiveTask<Double>porque nos gustaría tener como resultado
del valor de velocidad promedio del tipo double:

class AverageSpeed extends RecursiveTask<Double> {


private double timeSec;
private DateLocation dateLocation;
private int threshold, trafficUnitsNumber;
public AverageSpeed(DateLocation dateLocation,
double timeSec, int trafficUnitsNumber,
int threshold) {
this.timeSec = timeSec;
this.threshold = threshold;
this.dateLocation = dateLocation;
this.trafficUnitsNumber = trafficUnitsNumber;
}
protected Double compute() {
if (trafficUnitsNumber < threshold) {
//... write the code here that calculates
//... average speed trafficUnitsNumber vehicles
return averageSpeed;
} else{
int tun = trafficUnitsNumber / 2;
//write the code that creates two tasks, each
//for calculating average speed of tun vehicles
//then calculates an average of the two results
double avrgSpeed1 = ...;
double avrgSpeed2 = ...;
return (double) Math.round((avrgSpeed1 + avrgSpeed2) / 2);
}
}
}

Antes de que terminemos de escribir el código para el método compute() ,


escribamos el código que ejecutará esta tarea. Hay varias formas de hacerlo. Podemos
usar fork()y join(), por ejemplo:

void demo1_ForkJoin_fork_join() {
AverageSpeed averageSpeed = createTask();
averageSpeed.fork();
double result = averageSpeed.join();
System.out.println("result = " + result);
}

Esta técnica proporcionó el nombre para el marco. El método fork() , según Javadoc,

pág. 387
"hace los arreglos para ejecutar asincrónicamente esta tarea en el grupo en el que se está
ejecutando la tarea actual, si corresponde, o ForkJoinPool.commonPool()si no
está en ForkJoinPool()".

En nuestro caso, todavía no hemos utilizado ningún grupo, por lo que fork()lo vamos
a utilizar ForkJoinPool.commonPool()de forma predeterminada. Coloca la tarea
en la cola de un hilo en el grupo. El método join() devuelve el resultado del cálculo
cuando se realiza.

El método createTask()contiene lo siguiente:

AverageSpeed createTask() {
DateLocation dateLocation = new DateLocation(Month.APRIL,
DayOfWeek.FRIDAY, 17, "USA", "Denver", "Main103S");
double timeSec = 10d;
int trafficUnitsNumber = 1001;
int threshold = 100;
return new AverageSpeed(dateLocation, timeSec,
trafficUnitsNumber, threshold);
}

Tenga en cuenta los valores de los parámetros trafficUnitsNumber


y threshold. Esto será importante para analizar los resultados.

Otra forma de lograr esto es usar el método execute() o , cada uno con la misma
funcionalidad , para la ejecución de la tarea. El resultado de la ejecución se puede
recuperar mediante el método (el mismo que en el ejemplo
anterior): submit()join()

void demo2_ForkJoin_execute_join() {
AverageSpeed averageSpeed = createTask();
ForkJoinPool commonPool = ForkJoinPool.commonPool();
commonPool.execute(averageSpeed);
double result = averageSpeed.join();
System.out.println("result = " + result);
}

El último método que vamos a revisar es invoke(), lo que equivale a llamar


al fork()método seguido por el método join() :

void demo3_ForkJoin_invoke() {
AverageSpeed averageSpeed = createTask();
ForkJoinPool commonPool = ForkJoinPool.commonPool();
double result = commonPool.invoke(averageSpeed);
System.out.println("result = " + result);
}

Naturalmente, esta es la forma más popular de comenzar el proceso de divide y


vencerás.

pág. 388
Ahora volvamos al método compute() y veamos cómo se puede
implementar. Primero, implementemos el ifbloque (calcula la velocidad promedio de
menos que los thresholdvehículos). Utilizaremos la técnica y el código que
describimos en el Capítulo 3 , Programación modular :

double speed =
FactoryTraffic.getTrafficUnitStream(dateLocation,
trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(FactorySpeedModel.
generateSpeedModel(tuw.getTrafficUnit())))
.map(tuw -> tuw.calcSpeed(timeSec))
.mapToDouble(TrafficUnitWrapper::getSpeed)
.average()
.getAsDouble();
System.out.println("speed(" + trafficUnitsNumber + ") = " + speed);
return (double) Math.round(speed);

Sacamos los vehículos trafficUnitsNumber de FactoryTraffic. Creamos un


objeto TrafficUnitWrapper para cada elemento emitido y llamamos al método
setSpeedModel()en él (pasando el SpeedModelobjeto recién generado , basado
en el objeto TrafficUnit emitido ). Luego calculamos la velocidad, obtenemos un
promedio de todas las velocidades en la secuencia y obtenemos el resultado a double
partir del objeto Optional (el tipo de retorno de la operación average()). Luego
imprimimos el resultado y lo redondeamos para obtener un formato más presentable.

También es posible lograr el mismo resultado utilizando un bucle


for tradicional . Pero, como se mencionó anteriormente, parece que Java sigue la
tendencia general de un estilo más fluido y similar al flujo, orientado al procesamiento
de una gran cantidad de datos. Por lo tanto, le recomendamos que se acostumbre.

En el Capítulo 14 , Prueba , verá otra versión de la misma funcionalidad que permite


una mejor prueba unitaria de cada paso de forma aislada, lo que nuevamente respalda
la opinión de que la prueba unitaria, junto con la escritura del código, lo ayuda a hacer
que su código sea más verificable y disminuye la Necesito reescribirlo más tarde.

Ahora, revisemos las opciones de implementación else del bloque. Las primeras
líneas siempre serán las mismas:

int tun = trafficUnitsNumber / 2;


System.out.println("tun = " + tun);
AverageSpeed as1 =
new AverageSpeed(dateLocation, timeSec, tun, threshold);
AverageSpeed as2 =
new AverageSpeed(dateLocation, timeSec, tun, threshold);

Dividimos el número trafficUnitsNumber por 2 (no nos preocupa la posible


pérdida de una unidad en el caso de un promedio en un conjunto grande) y creamos
dos tareas.

pág. 389
Lo siguiente , el código de ejecución de la tarea real , se puede escribir de varias
maneras diferentes. Aquí está la primera solución posible, que ya nos es familiar, que
viene a la mente:

as1.fork(); //add to the queue


double res1 = as1.join(); //wait until completed
as2.fork();
double res2 = as2.join();
return (double) Math.round((res1 + res2) / 2);

Ejecute el siguiente código:

demo1_ForkJoin_fork_join () ;
demo2_ForkJoin_execute_join () ;
demo3_ForkJoin_invoke () ;

Si hacemos esto, veremos la misma salida (pero con diferentes valores de velocidad)
tres veces:

pág. 390
Usted ve cómo la tarea original de calcular la velocidad promedio sobre 1,001 unidades
(vehículos) se dividió primero por 2 varias veces hasta que el número de un grupo (62)
cayó por debajo del umbral de 100. Luego, la velocidad promedio de los dos últimos
grupos fue calculado y combinado (unido) con los resultados de otros grupos.

Otra forma de implementar un bloque else del método compute()podría ser la


siguiente:

as1.fork(); //add to the queue


double res1 = as2.compute(); //get the result recursively
double res2 = as1.join(); //wait until the queued task ends
return (double) Math.round((res1 + res2) / 2);

Así es como se verá el resultado:

Se puede ver cómo, en este caso, el método compute() (de la segunda tarea) se llama
de forma recursiva muchas veces hasta que se alcanza el umbral por el número de
elementos, a continuación, sus resultados se unieron con los resultados de la llamada a
l fork()y los métodos join() métodos de la primera tarea.

pág. 391
Como se mencionó anteriormente, toda esta complejidad puede ser reemplazada por
una llamada al método invoke() :

doble res1 = as1.invoke () ;


doble res2 = as2.invoke () ;
volver ( doble ) Matemáticas. ronda ((res1 + res2) / 2 ) ;

Produce un resultado similar al producido al invocar fork()y join()en cada una de


las tareas:

Sin embargo, hay una forma aún mejor de implementar un bloque else del método
compute():

double res1 = as1.invoke();


double res2 = as2.invoke();
return (double) Math.round((res1 + res2) / 2);

pág. 392
Si esto le parece complejo, solo tenga en cuenta que es solo una forma de flujo para
iterar sobre los resultados de invokeAll():

<T extends ForkJoinTask> Collection<T> invokeAll(Collection<T> tasks)

También es iterar sobre los resultados de invocar join() cada una de las tareas
devueltas (y combinar los resultados en promedio). La ventaja es que cedemos al marco
para decidir cómo optimizar la distribución de la carga. El resultado es el siguiente:

Puede ver que difiere de cualquiera de los resultados anteriores y puede cambiar
dependiendo de la disponibilidad y carga de las CPU en su computadora.

Uso del flujo para implementar el


patrón de publicación-suscripción
pág. 393
En esta receta, aprenderá sobre la nueva capacidad de publicación-suscripción
introducida en Java 9.

Prepararse
Entre muchas otras características, Java 9 introdujo estas cuatro interfaces en la clase
java.util.concurrent.Flow :

Flow.Publisher<T> - producer of items (messages) of type T


Flow.Subscriber<T> - receiver of messages of type T
Flow.Subscription - links producer and receiver
Flow.Processor<T,R> - acts as both producer and receiver

Con esto, Java entró en el mundo de la programación reactiva : programación con


el procesamiento asincrónico de flujos de datos.

Discutimos los flujos en el Capítulo 3 , Programación modular, y señalamos que


no son estructuras de datos, ya que no guardan datos en la memoria. La tubería de flujo
no hace nada hasta que se emite un elemento. Dicho modelo permite una asignación
mínima de recursos y usa recursos solo según sea necesario. La aplicación se
comporta en respuesta a la apariencia de los datos a los que reacciona, de ahí el nombre.

En un patrón de publicación-suscripción, los dos actores principales son Publisher,


que transmite datos (publica) y Subscriber, que escucha datos (se suscribe).

La interfaz Flow.Publisher<T> es una interfaz funcional. Solo tiene un método


abstracto:

void subscribe(Flow.Subscriber<? super T> subscriber)

De acuerdo con Javadoc


( https://docs.oracle.com/javase/10/docs/api/java/util/concurr
ent/SubmissionPublisher.html ), este método,

"agrega lo dado Flow.Subscriber<T>si es posible. Si ya está suscrito, o el intento de


suscripción falla, el método onError()de Flow.Subscriber<T>s e invoca con
un IllegalStateException. De lo contrario, el método
onSubscribe()de Flow.Subscriber<T> se invoca con un
nuevo Flow.Subscription.Suscriptor puede habilitar la recepción de elementos
invocando el método request()de esto Flow.Subscriptiony puede darse de baja
invocando su cancel()método ".

La interfaz Flow.Subscriber<T> tiene cuatro métodos:

pág. 394
• void onSubscribe(Flow.Subscription subscription): Invocado
antes de invocar cualquier otro método Subscriber para el dado
Subscription
• void onError(Throwable throwable): Invocado ante un error
irrecuperable encontrado por un Publishero Subscription, después del
cual no Subscriber se invocan otros métodos Subscription
• void onNext(T item): Invocado con el siguiente elemento
de Subscription
• void onComplete(): Se invoca cuando se sabe que no Subscriber se
realizarán invocaciones de métodos adicionales para Subscription

La interfaz Flow.Subscription tiene dos métodos:

• void cancel(): Hace Subscriberque (eventualmente) deje de recibir


mensajes
• void request(long n): Agrega el número n dado de elementos a la demanda
actual no satisfecha de esta suscripción

La interfaz Flow.Processor<T,R> está fuera del alcance de este libro.

Cómo hacerlo...
Para ahorrar tiempo y espacio, en lugar de crear nuestra propia implementación de
la interfaz Flow.Publisher<T>, podemos usar la clase
SubmissionPublisher<T> del paquete java.util.concurrent . Pero
crearemos nuestra propia implementación de la interfaz Flow.Subscriber<T> :

class DemoSubscriber<T> implements Flow.Subscriber<T> {


private String name;
private Flow.Subscription subscription;
public DemoSubscriber(String name){ this.name = name; }
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
this.subscription.request(0);
}
public void onNext(T item) {
System.out.println(name + " received: " + item);
this.subscription.request(1);
}
public void onError(Throwable ex){ ex.printStackTrace();}
public void onComplete() { System.out.println("Completed"); }
}

También implementaremos la interfaz Flow.Subscription :

class DemoSubscription<T> implements Flow.Subscription {


private final Flow.Subscriber<T> subscriber;

pág. 395
private final ExecutorService executor;
private Future<?> future;
private T item;
public DemoSubscription(Flow.Subscriber subscriber,
ExecutorService executor) {
this.subscriber = subscriber;
this.executor = executor;
}
public void request(long n) {
future = executor.submit(() -> {
this.subscriber.onNext(item );
});
}
public synchronized void cancel() {
if (future != null && !future.isCancelled()) {
this.future.cancel(true);
}
}
}

Como puede ver, acabamos de seguir las recomendaciones de Javadoc y


esperamos onSubscribe()que se llame al método de un suscriptor cuando el
suscriptor se agrega a un editor.

Otro detalle a tener en cuenta es que la clase SubmissionPublisher<T> tiene


el método submit(T item) que, de acuerdo con Javadoc
( https://docs.oracle.com/javase/10/docs/api/java/util/concurr
ent/SubmissionPublisher.html ):

"publica el elemento dado a cada suscriptor actual invocando asincrónicamente


su onNext()método, bloqueando ininterrumpidamente mientras los recursos para
cualquier suscriptor no están disponibles".

De esta manera, la clase SubmissionPublisher<T> envía elementos a los


suscriptores actuales hasta que se cierra. Esto permite que los generadores de
elementos actúen como publicadores de flujos reactivos.

Para demostrar esto, creemos varios suscriptores y suscripciones utilizando el método


demoSubscribe():

void demoSubscribe(SubmissionPublisher<Integer> publisher,


ExecutorService execService, String subscriberName){
DemoSubscriber<Integer> subscriber =
new DemoSubscriber<>(subscriberName);
DemoSubscription subscription =
new DemoSubscription(subscriber, execService);
subscriber.onSubscribe(subscription);
publisher.subscribe(subscriber);
}

Luego úselos en el siguiente código:

pág. 396
ExecutorService execService = ForkJoinPool.commonPool();
try (SubmissionPublisher<Integer> publisher =
new SubmissionPublisher<>()){
demoSubscribe(publisher, execService, "One");
demoSubscribe(publisher, execService, "Two");
demoSubscribe(publisher, execService, "Three");
IntStream.range(1, 5).forEach(publisher::submit);
} finally {
//...make sure that execService is shut down
}

El código anterior crea tres suscriptores, conectados al mismo editor con una
suscripción dedicada. La última línea genera una secuencia de números, 1, 2, 3 y 4, y
envía cada uno de ellos al editor. Esperamos que cada suscriptor obtenga cada uno de
los números generados como el parámetro del método onNext().

En el bloque finally, incluimos el código con el que ya está familiarizado en la


receta anterior:

try {
execService.shutdown();
int shutdownDelaySec = 1;
System.out.println("Waiting for " + shutdownDelaySec
+ " sec before shutting down service...");
execService.awaitTermination(shutdownDelaySec, TimeUnit.SECONDS);
} catch (Exception ex) {
System.out.println("Caught around execService.awaitTermination(): "
+ ex.getClass().getName());
} finally {
System.out.println("Calling execService.shutdownNow()...");
List<Runnable> l = execService.shutdownNow();
System.out.println(l.size()
+" tasks were waiting to be executed. Service stopped.");
}

Si ejecutamos el código anterior, la salida puede ser similar a la siguiente:

pág. 397
Como puede ver, debido al procesamiento asincrónico, el control llega al bloque
finally muy rápidamente y espera un segundo antes de cerrar el servicio. Este
período de espera es suficiente para que los elementos se generen y pasen a los
suscriptores. También confirmamos que cada elemento generado se envió a cada uno
de los suscriptores. Los tres nullvalores se generaron cada vez
que onSubscribe()se llamó al método de cada uno de los suscriptores.

Es razonable esperar que, en futuras versiones de Java, habrá más soporte agregado
para la funcionalidad reactiva (asíncrona y sin bloqueo).

Mejor gestión del proceso del


sistema operativo
En este capítulo, cubriremos las siguientes recetas:

• Engendrando un nuevo proceso


• Redirigir la salida del proceso y las secuencias de error al archivo
• Cambiar el directorio de trabajo de un subproceso
• Establecer la variable de entorno para un subproceso
• Ejecutar scripts de shell
• Obtención de la información del proceso de la JVM actual
• Obtención de la información del proceso del proceso generado
• Gestionar el proceso generado

pág. 398
• Enumerar procesos en vivo en el sistema
• Conectando múltiples procesos usando tubería
• Administrar subprocesos

Introducción
¿Con qué frecuencia terminaste escribiendo código que genera un nuevo proceso? No a
menudo. Sin embargo, puede haber situaciones que requirieron la escritura de dicho
código. En tales casos, tenía que recurrir al uso de una API de terceros como Apache
Commons Exec ( https://commons.apache.org/proper/commons-exec/ ),
entre otros. ¿Por qué fue esto? ¿No era suficiente la API de Java? No, no lo fue; al menos
no hasta Java 9. Ahora, con Java 9 y superior, tenemos muchas más funciones agregadas
a la API de proceso.

Hasta Java 7, la redirección de los flujos de entrada, salida y error no era trivial. Con
Java 7, se introdujeron nuevas API, que permitieron la redirección de la entrada, la
salida y el error a otros procesos (canalización), a un archivo o a una entrada / salida
estándar. Luego, en Java 8, se introdujeron algunas API más. En Java 9, ahora hay nuevas
API para las siguientes áreas:

• Obtener la información del proceso, como la ID del proceso ( PID ), el usuario que
inició el proceso, el tiempo durante el que se ha estado ejecutando, etc.
• Enumerar los procesos que se ejecutan en el sistema.
• Administrar los subprocesos y obtener acceso al árbol de procesos navegando hacia
arriba en la jerarquía de procesos.

En este capítulo, veremos algunas recetas que lo ayudarán a explorar todo lo que es
nuevo en la API del proceso, y también conocerá los cambios que se han introducido
desde el momento de Runtime.getRuntime().exec(). Y todos ustedes saben
que usar eso fue un crimen.

Todas estas recetas solo se pueden ejecutar en la plataforma Linux porque usaremos
comandos específicos de Linux al generar un nuevo proceso a partir del código Java. Hay
dos formas de ejecutar el script run.shen Linux:

• sh run.sh
• chmod +x run.sh && ./run.sh

Aquellos que usan Windows 10 no deben preocuparse, ya que Microsoft lanzó el


Subsistema de Windows para Linux, que le permite ejecutar sus distribuciones favoritas
de Linux, como Ubuntu, OpenSuse y otras, en Windows. Para obtener más detalles,
consulte este enlace: https://docs.microsoft.com/en-
in/windows/wsl/install-win10 .

pág. 399
Engendrando un nuevo proceso
En esta receta, veremos cómo generar un nuevo proceso
usando ProcessBuilder. También veremos cómo hacer uso de los flujos de entrada,
salida y error. Esta debería ser una receta muy sencilla y común. Sin embargo, el
objetivo de presentar esto es hacer que este capítulo sea un poco más completo y no
solo centrarse en las características de Java 9.

Prepararse
Hay un comando en Linux llamado free, que muestra la cantidad de RAM que está libre
y la cantidad que está utilizando el sistema. Acepta una opción, -mpara mostrar la
salida en megabytes. Entonces, simplemente correr gratis -mnos da el siguiente
resultado:

Ejecutaremos el código anterior desde el programa Java.

Cómo hacerlo...
Sigue estos pasos:

1. Cree una instancia ProcessBuilder proporcionando el comando requerido y


sus opciones:

ProcessBuilder pBuilder = new ProcessBuilder("free", "-m");

Una forma alternativa de especificar el comando y las opciones es la siguiente:

pBuilder.command("free", "-m");

2. Configure las secuencias de entrada y salida para el generador de procesos y otras


propiedades, como el directorio de ejecución y las variables de entorno. Después de eso,
invoque start()en la instancia ProcessBuilder para generar el proceso y obtener
una referencia al objeto Process:

Process p = pBuilder.inheritIO().start();

pág. 400
La función inheritIO() establece que la E / S estándar del subproceso generado
sea la misma que la del proceso Java actual.

3. Luego esperamos la finalización del proceso, o por un segundo (lo que sea antes),
como se muestra en el siguiente código:

if(p.waitFor(1, TimeUnit.SECONDS)){
System.out.println("process completed successfully");
}else{
System.out.println("waiting time elapsed, process did
not complete");
System.out.println("destroying process forcibly");
p.destroyForcibly();
}

Si esto no se completa en el tiempo especificado, entonces eliminamos el proceso


invocando el método destroyForcibly().

4. Compile y ejecute el código utilizando los siguientes comandos:

$ javac -d mods --module-source-path src


$(find src -name *.java)
$ java -p mods -m process/com.packt.process.NewProcessDemo

5. La salida que obtenemos es la siguiente:

El código para esta receta se puede encontrar


en Chapter08/1_spawn_new_process.

Cómo funciona...
Hay dos formas de informar ProcessBuilderqué comando ejecutar:

• Al pasar el comando y sus opciones al constructor mientras crea el objeto


ProcessBuilder
• Al pasar el comando y sus opciones como parámetros al command()método
del objeto ProcessBuilder

Antes de generar el proceso, podemos hacer lo siguiente:

• Podemos cambiar el directorio de ejecución utilizando el método directory()

pág. 401
• Podemos redirigir la secuencia de entrada, la secuencia de salida y las secuencias
de error a un archivo u otro proceso.
• Podemos proporcionar las variables de entorno requeridas para el subproceso.

Veremos todas estas actividades en sus respectivas recetas en este capítulo.

Se genera un nuevo proceso cuando start()se invoca el método y la persona que


llama obtiene una referencia a este subproceso en forma de una instancia de la
clase Process. Con este objeto Process, podemos hacer muchas cosas, como las
siguientes:

• Obtenga información sobre el proceso, incluido su PID


• Obtenga los flujos de salida y error
• Verifique la finalización del proceso
• Destruye el proceso
• Asociar las tareas a realizar una vez que se complete el proceso
• Verifique los subprocesos generados por el proceso
• Encuentre el proceso padre del proceso, si existe

En nuestra receta, tenemos waitFor un segundo, o para que se complete el proceso


(lo que ocurra primero). Si el proceso se ha completado,
entonces waitForregresa true; de lo contrario, vuelve false. Si el proceso no se
completa, podemos eliminar el proceso invocando el
método destroyForcibly()en el objeto Process.

Redirigir la salida del proceso y


las secuencias de error al archivo
En esta receta, veremos cómo lidiar con los flujos de salida y error de un proceso
generado a partir del código Java. Escribiremos la salida o error producido por el
proceso generado en un archivo.

Prepararse
En esta receta, haremos uso del comando iostat. Este comando se utiliza para
informar las estadísticas de CPU y E / S para diferentes dispositivos y
particiones. Ejecutemos el comando y veamos qué informa:

$ iostat

pág. 402
En algunas distribuciones de Linux, como Ubuntu, iostat no está instalado por
defecto. Puede instalar la utilidad ejecutando sudo apt-get install sysstat.

El resultado del comando anterior es el siguiente:

Cómo hacerlo...
Sigue estos pasos:

1. Cree un nuevo objeto ProcessBuilder especificando el comando que se


ejecutará:

ProcessBuilder pb = new ProcessBuilder("iostat");

2. Redireccione las secuencias de salida y error a la salida y error del archivo,


respectivamente:

pb.redirectError(new File("error"))
.redirectOutput(new File("output"));

3. Inicie el proceso y espere a que se complete:

Process p = pb.start();
int exitValue = p.waitFor();

4. Lea el contenido del archivo de salida:

Files.lines(Paths.get("output"))
.forEach(l -> System.out.println(l));

5. Lea el contenido del archivo de error. Esto se crea solo si hay un error en el
comando:

Files.lines(Paths.get("error"))
.forEach(l -> System.out.println(l));

pág. 403
Los pasos 4 y 5 son para nuestra referencia. Esto no tiene nada que
ver ProcessBuildero el proceso se generó. Usando estas dos líneas de código,
podemos inspeccionar lo que el proceso escribió en los archivos de salida y error.

El código completo se puede encontrar en Chapter08/2_redirect_to_file.

6. Compile el código con el siguiente comando:

$ javac -d mods --module-source-path src $(find src -name


*.java)

7. Ejecute el código con el siguiente comando:

$ java -p mods -m process/com.packt.process.RedirectFileDemo

Obtendremos el siguiente resultado:

Podemos ver que cuando el comando se ejecutó con éxito, no hay nada en el archivo
de error.

Hay más...
Puede proporcionar un comando erróneo ProcessBuilder y luego ver que el error
se escribe en el archivo de error y nada en el archivo de salida. Puede hacer esto
cambiando la creación ProcessBuilder de la instancia de la siguiente manera:

ProcessBuilder pb = nuevo ProcessBuilder ("iostat", "-Z");

Compile y ejecute usando los comandos dados anteriormente en la sección Cómo


hacerlo ...

Verá que hay un error reportado en el archivo de error pero nada en el archivo de
salida:

pág. 404
Cambiar el directorio de trabajo
de un subproceso
A menudo, querrá que se ejecute un proceso en el contexto de una ruta, como enumerar
los archivos en un directorio. Para hacerlo, tendremos que
indicarle ProcessBuilder que inicie el proceso en el contexto de una ubicación
determinada. Podemos lograr esto usando el método directory() . Este método
tiene dos propósitos:

• Se devuelve el directorio actual de la ejecución cuando no pasamos ningún


parámetro.
• Establece el directorio actual de ejecución en el valor pasado cuando pasamos un
parámetro.

En esta receta, veremos cómo ejecutar el comando tree para recorrer recursivamente
todos los directorios del directorio actual e imprimirlo en forma de árbol.

Prepararse
En general, el treecomando no viene preinstalado, por lo que deberá instalar el
paquete que contiene el comando. Para instalar en un sistema basado en Ubuntu /
Debian, ejecute el siguiente comando:

$ sudo apt-get install tree

Para instalar en Linux, que admite el yumadministrador de paquetes, ejecute el


siguiente comando:

$ yum install tree

Para verificar su instalación, simplemente ejecute el treecomando, y debería poder


ver la estructura de directorios actual impresa. Para mí, es algo como esto:

pág. 405
Hay varias opciones compatibles con el comando tree. Es para que lo explores.

Cómo hacerlo...
Sigue estos pasos:

pág. 406
1. Crea un nuevo objeto ProcessBuilder:

ProcessBuilder pb = new ProcessBuilder();

2. Establezca el comando treey la salida y el error al mismo que el del proceso Java
actual:

pb.command("tree").inheritIO();

3. Establezca el directorio en el directorio que desee. Lo configuré como la carpeta


raíz:

pb.directory(new File("/root"));

4. Inicie el proceso y espere a que salga:

Process p = pb.start();
int exitValue = p.waitFor();

5. Compila y ejecuta usando los siguientes comandos:

$ javac -d mods --module-source-path src $(find src -name *.java)


$ java -p mods -m process/com.packt.process.ChangeWorkDirectoryDemo

6. La salida será el contenido recursivo del directorio, especificado en el método


directory()del objeto ProcessBuilder, impreso en un formato de árbol.

El código completo se puede encontrar


en Chapter08/3_change_work_directory.

Cómo funciona...
El método directory()acepta la ruta del directorio de trabajo para Process. La
ruta se especifica como una instancia de File.

Establecer la variable de entorno


para un subproceso
Las variables de entorno son como cualquier otra variable que tengamos en nuestros
lenguajes de programación. Tienen un nombre y tienen algún valor, que puede ser
variado. Estos son utilizados por los comandos de Linux / Windows o los scripts de shell
/ lote para realizar diferentes operaciones. Estas se denominan variables de

pág. 407
entorno porque están presentes en el entorno del proceso / comando / script que se
está ejecutando. Generalmente, el proceso hereda las variables de entorno del proceso
padre.

Se accede a ellos de diferentes maneras en diferentes sistemas operativos. En


Windows, se accede a ellos como %ENVIRONMENT_VARIABLE_NAME%, y en los
sistemas operativos basados en Unix, se accede a ellos
como $ENVIRONMENT_VARIABLE_NAME.

En los sistemas basados en Unix, puede usar el comando printenv para imprimir
todas las variables de entorno disponibles para el proceso, y en los sistemas basados
en Windows, puede usar el SETcomando.

En esta receta, pasaremos algunas variables de entorno a nuestro subproceso y


utilizaremos el comando printenv para imprimir todas las variables de entorno
disponibles.

Cómo hacerlo...
Sigue estos pasos:

1. Crear una instancia de ProcessBuilder:

ProcessBuilder pb = new ProcessBuilder();

2. Establezca el comando printenvy los flujos de salida y error en el mismo que el


del proceso Java actual:

pb.command("printenv").inheritIO();

3. Proporcione a las variables de entorno COOKBOOK_VAR1 el valor First


variable, COOKBOOK_VAR2 el valor Second variable y COOKBOOK_VAR3el
valor Third variable:

Map<String, String> environment = pb.environment();


environment.put("COOKBOOK_VAR1", "First variable");
environment.put("COOKBOOK_VAR2", "Second variable");
environment.put("COOKBOOK_VAR3", "Third variable");

4. Inicie el proceso y espere a que se complete:

Process p = pb.start();
int exitValue = p.waitFor();

pág. 408
El código completo de esta receta se puede encontrar
en Chapter08/4_environment_variables.

5. Compile y ejecute el código utilizando los siguientes comandos:

$ javac -d mods --module-source-path src $(find src -name


*.java)
$ java -p mods -m
process/com.packt.process.EnvironmentVariableDemo

La salida que obtienes es la siguiente:

Puede ver las tres variables impresas entre otras variables.

Cómo funciona...
Cuando invoca el método environment()en la instancia de ProcessBuilder,
copia las variables de entorno del proceso actual, las llena en una instancia
de HashMapy lo devuelve al código de la persona que llama.

Todo el trabajo de cargar las variables de entorno se realiza mediante un paquete privado
de clase final ProcessEnvironment, que en realidad se extiende HashMap.

Luego hacemos uso de este mapa para llenar nuestras propias variables de entorno,
pero no necesitamos volver a configurar el mapa ProcessBuilder porque
tendremos una referencia al objeto del mapa y no una copia. Cualquier cambio
realizado en el objeto de mapa se reflejará en el objeto de mapa real en poder de
la instancia ProcessBuilder.

Ejecutar scripts de shell


Por lo general, recopilamos un conjunto de comandos utilizados para realizar una
operación en un archivo, denominado script de shell en el mundo Unix y un archivo
por lotes en Windows. Los comandos presentes en estos archivos se ejecutan

pág. 409
secuencialmente, con la excepción de que tiene bloques o bucles condicionales en los
scripts.

Estos scripts de shell son evaluados por el shell en el que se ejecutan. Los diferentes
tipos de proyectiles disponibles son bash, csh, ksh, y así
sucesivamente. La bashconcha es la concha más utilizada.

En esta receta, escribiremos un script de shell simple y luego invocaremos el mismo


desde el código Java utilizando los objetos ProcessBuildery Process.

Prepararse
Primero, escribamos nuestro script de shell. Este script hace lo siguiente:

1. Imprime el valor de la variable de entorno, MY_VARIABLE


2. Ejecuta el comando tree
3. Ejecuta el comando iostat

Creemos un archivo de script de shell por el nombre script.sh, con los siguientes
comandos:

echo $MY_VARIABLE;
echo "Running tree command";
tree;
echo "Running iostat command"
iostat;

Puede colocar el script.shen su carpeta de inicio; es decir, en


el /home/<username>. Ahora veamos cómo podemos ejecutar esto desde Java.

Cómo hacerlo...
Sigue estos pasos:

1. Cree una nueva instancia de ProcessBuilder:

ProcessBuilder pb = new ProcessBuilder ();

2. Establezca el directorio de ejecución para que apunte al directorio del archivo de


script de shell:

pb.directory (nuevo archivo ("/ root"));

pág. 410
Tenga en cuenta que la ruta anterior pasó, mientras que la creación del objeto File
dependerá de dónde haya colocado su script script.sh. En nuestro caso, lo tuvimos
colocado /root. Es posible que haya copiado el script /home/yournamey, en
consecuencia, el objeto File se creará como newFile("/home/yourname").

3. Establezca una variable de entorno que sería utilizada por el script de shell:

Map<String, String> environment = pb.environment();


environment.put("MY_VARIABLE", "Set by Java process");

4. Establezca el comando que se ejecutará y también los argumentos que se pasarán


al comando. Además, configure los flujos de salida y error para el proceso al mismo que el
del proceso Java actual:

pb.command("/bin/bash", "script.sh").inheritIO();

5. Inicie el proceso y espere a que se ejecute por completo:

Process p = pb.start();
int exitValue = p.waitFor();

Puede obtener el código completo de Chapter08/5_running_shell_script.

Puede compilar y ejecutar el código utilizando los siguientes comandos:

$ javac -d mods --module-source-path src $(find src -name *.java)


$ java -p mods -m process/com.packt.process.RunningShellScriptDemo

La salida que obtenemos es la siguiente:

pág. 411
Cómo funciona...
Debe anotar dos cosas en esta receta:

• Cambie el directorio de trabajo del proceso a la ubicación del script de shell.


• Se usa /bin/bash para ejecutar el script de shell.

Si no toma nota del paso 1, tendrá que usar la ruta absoluta para el archivo de script de
shell. Sin embargo, en esta receta, hicimos esto y, por lo tanto, solo usamos el nombre
del script de shell para el comando /bin/bash.

El paso 2 es básicamente cómo desea ejecutar el script de shell. La forma de hacerlo es


pasar el script de shell al intérprete, que interpretará y ejecutará el script. Eso es lo que
hace la siguiente línea de código:

pb.command ("/ bin / bash", "script.sh")

Obtención de la información del


proceso de la JVM actual
pág. 412
Un proceso en ejecución tiene un conjunto de atributos asociados, como los siguientes:

• PID : identifica de forma exclusiva el proceso


• Propietario : este es el nombre del usuario que inició el proceso
• Comando : este es el comando que se ejecuta bajo el proceso
• Tiempo de CPU : indica el tiempo durante el cual el proceso ha estado activo
• Hora de inicio : indica la hora en que se inició el proceso

Estos son algunos de los atributos que generalmente nos interesan. Quizás también nos
interesaría el uso de la CPU o el uso de la memoria. Ahora, obtener esta información
desde Java no era posible antes de Java 9. Sin embargo, en Java 9, se introdujo un nuevo
conjunto de API, que nos permite obtener la información básica sobre el proceso.

En esta receta, veremos cómo obtener la información del proceso para el proceso Java
actual; es decir, el proceso que ejecuta su código.

Cómo hacerlo...
Sigue estos pasos:

1. Cree una clase simple y úsela ProcessHandle.current()para obtener el


proceso Java actual ProcessHandle:

ProcessHandle handle = ProcessHandle.current();

2. Hemos agregado un código, que agregará algo de tiempo de ejecución al código:

for ( int i = 0 ; i < 100; i++){


Thread.sleep(1000);
}

3. Utilice el método info()en la instancia de ProcessHandlepara obtener una


instancia de ProcessHandle.Info:

ProcessHandle.Info info = handle.info();

4. Use la instancia de ProcessHandle.Info para obtener toda la información


disponible por la interfaz:

System.out.println("Command line: " +


info.commandLine().get());
System.out.println("Command: " + info.command().get());
System.out.println("Arguments: " +
String.join(" ", info.arguments().get()));
System.out.println("User: " + info.user().get());
System.out.println("Start: " + info.startInstant().get());

pág. 413
System.out.println("Total CPU Duration: " +
info.totalCpuDuration().get().toMillis() +"ms");

5. Utilice el método pid()de ProcessHandlepara obtener el ID de proceso del


proceso Java actual:

System.out.println("PID: " + handle.pid());

6. También imprimiremos la hora de finalización utilizando la hora en que el código


está a punto de finalizar. Esto nos dará una idea del tiempo de ejecución del proceso:

Instant end = Instant.now();


System.out.println("End: " + end);

Puede obtener el código completo de Chapter08/6_current_process_info.

Compile y ejecute el código utilizando los siguientes comandos:

$ javac -d mods --module-source-path src $(find src -name *.java)


$ java -p mods -m process/com.packt.process.CurrentProcessInfoDemo

El resultado que verá será algo como esto:

Tomará algún tiempo hasta que el programa complete la ejecución.


Una observación que debe hacerse es que incluso si el programa se ejecutó durante unos
dos minutos, la duración total de la CPU fue de 350 milisegundos. Este es el período de
tiempo durante el cual la CPU estuvo ocupada.

Cómo funciona...
Para dar más control a los procesos nativos y obtener su
información, ProcessHandlese ha agregado una nueva interfaz llamada a la API de
Java. Utilizando ProcessHandle, puede controlar la ejecución del proceso, así como
obtener información sobre el proceso. La interfaz tiene otra interfaz interna
llamada ProcessHandle.Info. Esta interfaz proporciona API para obtener
información sobre el proceso.

pág. 414
Hay varias formas de obtener el objeto ProcessHandle para un proceso. Algunas de
las formas son las siguientes:

• ProcessHandle.current(): Esto se usa para obtener


la ProcessHandle instancia para el proceso Java actual.
• Process.toHandle(): Esto se usa para obtener objeto ProcessHandle
Processo dado .
• ProcessHandle.of(pid): Esto se utiliza para obtener ProcessHandle un
proceso identificado por el PID dado.

En nuestra receta, utilizamos el primer enfoque, es decir,


utilizamos ProcessHandle.current(). Esto nos da una idea del proceso actual de
Java. Invocar el info()método en la instancia ProcessHandle nos dará una
instancia de la implementación de la interfaz ProcessHandle.Info, que podemos
utilizar para obtener la información del proceso, como se muestra en el código de la
receta.

ProcessHandle y ProcessHandle.Infoson interfaces. El JDK proporciona Oracle


JDK o Open JDK, proporcionará implementaciones para estas interfaces. Oracle JDK tiene
una clase llamada ProcessHandleImpl, que implementa ProcessHandle y otra
clase interna ProcessHandleImpl llamada Info, que implementa
la interfaz ProcessHandle.Info. Entonces, cada vez que llame a uno de los métodos
antes mencionados para obtener un objeto ProcessHandle, ProcessHandleImpl
se devuelve una instancia.
Lo mismo ocurre con la clase Process también. Es una clase abstracta y Oracle JDK
proporciona una implementación llamada ProcessImpl, que implementa los métodos
abstractos en la clase Process.
En todas las recetas de este capítulo, cualquier mención de la
instancia ProcessHandle o del objeto ProcessHandle se referirá a la instancia
u objeto deProcessHandleImplo cualquier otra clase de implementación
proporcionada por el JDK que está utilizando.
Además, cualquier mención de la instancia ProcessHandle.Info o
del objeto ProcessHandle.Info se referirá a la instancia u objeto
de ProcessHandleImpl.Info o cualquier otra clase de implementación
proporcionada por el JDK que está utilizando.

Obtención de la información del


proceso del proceso generado
pág. 415
En nuestra receta anterior, vimos cómo obtener la información del proceso para el
proceso Java actual. En esta receta, veremos cómo obtener la información del proceso
para un proceso generado por el código Java; es decir, por el proceso actual de Java. Las
API utilizadas serán las mismas que vimos en la receta anterior, excepto por la forma
en que se implementa la instancia de ProcessHandle .

Prepararse
En esta receta, haremos uso de un comando Unix sleep, que se usa para pausar la
ejecución por un período de tiempo en segundos.

Cómo hacerlo...
Sigue estos pasos:

1. Genera un nuevo proceso desde el código Java, que ejecuta el sleepcomando:

ProcessBuilder pBuilder = new ProcessBuilder("sleep", "20");


Process p = pBuilder.inheritIO().start();

2. Obtenga la instancia ProcessHandle para este proceso generado:

ProcessHandle handle = p.toHandle();

3. Espere a que el proceso generado complete la ejecución:

int exitValue = p.waitFor ();

4. Use ProcessHandle para obtener la instancia ProcessHandle.Info y


use sus API para obtener la información requerida. Alternativamente, incluso podemos
usar el objeto Process directamente para obtener ProcessHandle.Info usando
el método info()en la clase Process:

ProcessHandle.Info info = handle.info();


System.out.println("Command line: " +
info.commandLine().get());
System.out.println("Command: " + info.command().get());
System.out.println("Arguments: " + String.join(" ",
info.arguments().get()));
System.out.println("User: " + info.user().get());
System.out.println("Start: " + info.startInstant().get());
System.out.println("Total CPU time(ms): " +
info.totalCpuDuration().get().toMillis());
System.out.println("PID: " + handle.pid());

pág. 416
Puede obtener el código completo de Chapter08/7_spawned_process_info.

Compile y ejecute el código utilizando los siguientes comandos:

$ javac -d mods --module-source-path src $(find src -name *.java)


$ java -p mods -m process/com.packt.process.SpawnedProcessInfoDemo

Alternativamente, hay una run.shsecuencia de


comandos Chapter08/7_spawned_process_info, que puede ejecutar desde
cualquier sistema basado en Unix como /bin/bash run.sh.

El resultado que verá será algo como esto:

Gestionar el proceso generado


Hay algunos métodos, tales como destroy(), destroyForcibly()(añadido en
Java 8), isAlive()(añadido en Java 8),
y supportsNormalTermination()(añadido en Java 9), que pueden ser utilizados
para controlar el proceso dio lugar. Estos métodos están disponibles tanto en
el Processobjeto como en el ProcessHandleobjeto. Aquí, controlar sería solo para
verificar si el proceso está vivo y, si lo está, destruir el proceso.

En esta receta, generaremos un proceso de larga duración y haremos lo siguiente:

• Comprueba su vivacidad
• Compruebe si se puede detener normalmente; es decir, dependiendo de la
plataforma, el proceso tiene que ser detenido simplemente usando destruir o
usando la fuerza de destrucción
• Detener el proceso

Cómo hacerlo...
pág. 417
1. Genera un nuevo proceso desde el código Java, que ejecuta el sleepcomando
durante, por ejemplo, un minuto o 60 segundos:

ProcessBuilder pBuilder = new ProcessBuilder("sleep", "60");


Process p = pBuilder.inheritIO().start();

2. Espere, digamos, 10 segundos:

p.waitFor (10, TimeUnit.SECONDS);

3. Comprueba si el proceso está vivo:

boolean isAlive = p.isAlive();


System.out.println("Process alive? " + isAlive);

4. Compruebe si el proceso se puede detener normalmente:

boolean normalTermination = p.supportsNormalTermination();


System.out.println("Normal Termination? " + normalTermination);

5. Detenga el proceso y verifique su vivacidad:

p.destroy();
isAlive = p.isAlive();
System.out.println("Process alive? " + isAlive);

Puede obtener el código completo de Chapter08/8_manage_spawned_process.

Hemos proporcionado un script de utilidad llamado run.sh, que puede usar para
compilar y ejecutar el código— sh run.sh.

La salida que obtenemos es la siguiente:

Si ejecutamos el programa en
Windows, supportsNormalTermination()regresa false, pero
en supportsNormalTermination()retornos de Unix true(como se ve en la salida
anterior también).

pág. 418
Enumerar procesos en vivo en el
sistema
En Windows, abre el Administrador de tareas de Windows para ver los procesos
actualmente activos, y en Linux usa el pscomando con sus variadas opciones para ver
los procesos junto con otros detalles, como usuario, tiempo empleado, comando, etc.

En Java 9, se agregó una nueva API, llamada ProcessHandle, que se ocupa de


controlar y obtener información sobre los procesos. Uno de los métodos de la API
es allProcesses(), que devuelve una instantánea de todos los procesos visibles
para el proceso actual. En esta receta, veremos cómo funciona el método y qué
información podemos extraer de la API.

Cómo hacerlo...
Sigue estos pasos:

1. Use el método allProcesses()en la ProcessHandleinterfaz para obtener


una secuencia de los procesos actualmente activos:

Stream<ProcessHandle> liveProcesses =
ProcessHandle.allProcesses();

2. Itere sobre la secuencia usando forEach()y pase una expresión lambda para
imprimir los detalles disponibles:

liveProcesses.forEach(ph -> {
ProcessHandle.Info phInfo = ph.info();
System.out.println(phInfo.command().orElse("") +" " +
phInfo.user().orElse(""));
});

Puede obtener el código completo


de Chapter08/9_enumerate_all_processes.

Hemos proporcionado un script de utilidad llamado run.sh, que puede usar para
compilar y ejecutar el código— sh run.sh.

La salida que obtenemos es la siguiente:

pág. 419
En la salida anterior, imprimimos el nombre del comando y el usuario del
proceso. Hemos mostrado una pequeña parte de la salida.

Conectando múltiples procesos


usando tubería
En Unix, es común canalizar un conjunto de comandos usando el |símbolo para crear
una canalización de actividades, donde la entrada para el comando es la salida del
comando anterior. De esta manera, podemos procesar la entrada para obtener la salida
deseada.

Un escenario común es cuando desea buscar algo o un patrón en los archivos de


registro, o una aparición de algún texto en el archivo de registro. En tales situaciones,
puede crear una tubería, en el que se pasa los datos de archivo de registro
correspondiente a través de una serie de comandos, es decir, cat, grep, wc -l, y así
sucesivamente.

En esta receta, haremos uso del conjunto de datos de Iris del repositorio de
aprendizaje automático de UCI disponible
en https://archive.ics.uci.edu/ml/datasets/Iris para crear una
tubería, en la que contaremos el número de ocurrencias de cada tipo de flor.

pág. 420
Prepararse
Ya hemos descargado el conjunto de datos de Iris Flower
( https://archive.ics.uci.edu/ml/datasets/iris ), que se puede encontrar
en Chapter08/10_connecting_process_pipe/iris.data la descarga del
código para este libro.

Si observa los datos Iris, verá que hay 150 filas en el siguiente formato:

4.7,3.2,1.3,0.2, Iris-setosa

Aquí, hay varios atributos separados por una coma ( ,), y los atributos son los
siguientes:

• Longitud del sepal en cm


• Ancho sepal en cm
• Longitud del pétalo en cm
• Ancho del pétalo en cm
• Clase:
• Iris setosa
• Iris versicolour
• Iris virginica

En esta receta, encontraremos el número total de flores en cada clase, a saber, s etosa,
v ersicolour y v irginica.

Haremos uso de una tubería con los siguientes comandos (usando un sistema
operativo basado en Unix):

$ cat iris.data.txt | cortar -d ',' -f5 | uniq -c

La salida que obtenemos es la siguiente:

50 Iris-setosa
50 Iris-versicolor
50 Iris-virginica
1

El 1 al final es para la nueva línea disponible al final del archivo. Entonces, hay 50
flores de cada clase. Analicemos la canalización de comandos de shell anterior y
comprendamos la función de cada uno de ellos:

• cat: Este comando lee el archivo dado como argumento.

pág. 421
• cut: Esto divide cada línea utilizando el carácter dado en la -dopción y devuelve
el valor en la columna identificada por la -fopción.
• uniq: Esto devuelve una lista única de los valores dados, y cuando -cse
usa la opción, devuelve cuántas veces cada valor único está presente en la lista.

Cómo hacerlo...
1. Cree una lista de objetos ProcessBuilder, que contendrá las instancias
ProcessBuilder que participan en nuestra canalización. Además, redirija la
salida del último proceso en la tubería a la salida estándar del proceso Java actual:

List<ProcessBuilder> pipeline = List.of(


new ProcessBuilder("cat", "iris.data.txt"),
new ProcessBuilder("cut", "-d", ",", "-f", "5"),
new ProcessBuilder("uniq", "-c")
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
);

2. Use el método startPipeline()de ProcessBuilder y pase la lista


de objetos ProcessBuilder para comenzar la canalización. Devolverá una lista
de Processobjetos, cada uno representando un bjeto ProcessBuildero en la lista:

List<Process> processes = ProcessBuilder.startPipeline(pipeline);

3. Obtenga el último proceso en la lista y waitFor complete:

int exitValue = processes.get(processes.size() - 1).waitFor();

Puede obtener el código completo


de Chapter08/10_connecting_process_pipe.

Hemos proporcionado un script de utilidad llamado run.sh, que puede usar para
compilar y ejecutar el código— sh run.sh.

La salida que obtenemos es la siguiente:

Cómo funciona...
pág. 422
El método startPipeline()inicia un Process para cada objeto
ProcessBuilder en la lista. Excepto por el primero y el último proceso, redirige la
salida de un proceso a la entrada de otro proceso
mediante ProcessBuilder.Redirect.PIPE. Si ha
proporcionado redirectOutput algún proceso intermedio como algo
diferente ProcessBuilder.Redirect.PIPE, se generará un error; algo similar a
lo siguiente:

Exception in thread "main" java.lang.IllegalArgumentException: builder


redirectOutput() must be PIPE except for the last builder: INHERIT.

Establece que cualquier generador, excepto el último, debe redirigir su salida al


siguiente proceso. Lo mismo es aplicable para redirectInput.

Administrar subprocesos
Cuando un proceso inicia otro proceso, el proceso lanzado se convierte en el subproceso
del proceso de lanzamiento. El proceso lanzado, a su vez, puede lanzar otro proceso, y
esta cadena puede continuar. Esto da como resultado un árbol de procesos. A menudo,
tendríamos que lidiar con un subproceso defectuoso y podríamos querer eliminar ese
subproceso, o podríamos querer conocer los subprocesos que se inician y podríamos
querer obtener información sobre ellos.

En Java 9, Processse agregaron dos nuevas API en


la clase, children()y descendants(). La API children()le permite obtener
una lista de la instantánea de los procesos que son los elementos secundarios
inmediatos del proceso actual, y la API descendants()proporciona una instantánea
de los procesos que son recursivos children()del proceso actual; es decir, están
invocando children()recursivamente en cada proceso secundario.

En esta receta, veremos tanto las API children()como las API descendants()y
veremos qué información podemos recopilar de la instantánea del proceso.

Prepararse
Creemos un script de shell simple, que usaremos en la receta. Este script se puede
encontrar en Chapter08/11_managing_sub_process/script.sh:

echo "Running tree command";


tree;
sleep 60;
echo "Running iostat command";
iostat;

pág. 423
En el script anterior, estamos ejecutando los comandos treey iostat, separados
por un tiempo de suspensión de un minuto. Si desea conocer estos comandos, consulte
la receta Ejecutar scripts de shell de este capítulo. El comando de suspensión, cuando
se ejecuta desde el shell bash, crea un nuevo subproceso cada vez que se invoca.

Crearemos, digamos, 10 instancias ProcessBuilder para ejecutar el script de shell


anterior y ejecutarlas simultáneamente.

Cómo hacerlo...
1. Crearemos 10 instancias de ProcessBuilder para ejecutar nuestro script de
shell (disponible
en Chapter08/11_managing_sub_process/script.sh). No nos
preocupa su salida, así que descartemos la salida de los comandos redirigiendo la
salida a una redirección predefinida
llamada ProcessHandle.Redirect.DISCARD:

for ( int i = 0; i < 10; i++){


new ProcessBuilder("/bin/bash", "script.sh")
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.start();
}

2. Obtenga el identificador del proceso actual:

ProcessHandle currentProcess = ProcessHandle.current();

3. Use el proceso actual para que sus hijos utilicen la children()API e itere sobre
cada uno de sus hijos para imprimir su información. Una vez que tenemos una instancia
de ProcessHandle, podemos hacer varias cosas, como destruir el proceso, obtener su
información de proceso, etc.

System.out.println("Obtaining children");
currentProcess.children().forEach(pHandle -> {
System.out.println(pHandle.info());
});

4. Utilice el proceso actual para obtener todos los subprocesos que son sus
descendientes mediante el uso de la descendants()API e iterar sobre cada uno de
ellos para imprimir su información:

currentProcess.descendants().forEach(pHandle -> {
System.out.println(pHandle.info());
});

Puede obtener el código completo de Chapter08/11_managing_sub_process.

pág. 424
Hemos proporcionado un script de utilidad llamado run.sh, que puede usar para
compilar y ejecutar el código— sh run.sh.

La salida que obtenemos es la siguiente:

Cómo funciona...
Las API children() y descendants() devuelven
los Streamde ProcessHandler cada uno de los procesos, que son hijos directos o
descendientes del proceso actual. Usando la instancia de ProcessHandler,
podemos realizar las siguientes operaciones:

• Obtenga la información del proceso


• Verificar el estado del proceso
• Detener el proceso

Servicios web RESTful usando


Spring Boot
En este capítulo, vamos a cubrir las siguientes recetas:

• Crear una aplicación Spring Boot simple


• Interactuando con la base de datos
• Crear un servicio web RESTful
pág. 425
• Crear múltiples perfiles para Spring Boot
• Implementación de servicios web RESTful en Heroku
• Contenedor del servicio web RESTful usando Docker
• Monitoreo de la aplicación Spring Boot 2 usando Micrometer y Prometheus

Introducción
En los últimos años, la unidad para arquitecturas basadas en microservicios ha ganado
una amplia adopción, gracias a la simplicidad y facilidad de mantenimiento que
proporciona cuando se hace de la manera correcta. Muchas compañías, como Netflix y
Amazon, han pasado de sistemas monolíticos a sistemas más enfocados y más livianos,
todos hablando entre sí a través de servicios web RESTful. La llegada de los servicios
web RESTful y su enfoque directo para crear servicios web utilizando el protocolo HTTP
conocido ha facilitado la comunicación entre aplicaciones que los servicios web basados
en SOAP más antiguos.

En este capítulo, veremos el marco Spring Boot , que proporciona una manera
conveniente de crear microservicios listos para la producción utilizando las bibliotecas
Spring. Usando Spring Boot, desarrollaremos un servicio web RESTful simple y lo
implementaremos en la nube.

Crear una aplicación Spring Boot


simple
Spring Boot ayuda a crear fácilmente aplicaciones basadas en Spring listas para
producción. Proporciona soporte para trabajar con casi todas las bibliotecas de Spring,
sin necesidad de configurarlas explícitamente. Se proporcionan clases de configuración
automática para una fácil integración con las bibliotecas, bases de datos y colas de
mensajes más utilizadas.

En esta receta, veremos cómo crear una aplicación Spring Boot simple con un
controlador que imprima un mensaje cuando se abre en el navegador.

Prepararse
Spring Boot es compatible con Maven y Gradle como herramientas de construcción, y
utilizaremos Maven en nuestras recetas. La siguiente URL, http://start.spring.io/ ,
proporciona una manera conveniente de crear un proyecto vacío con las dependencias
requeridas. Lo usaremos para descargar un proyecto vacío. Siga estos pasos para crear
y descargar un proyecto vacío basado en Spring Boot:

pág. 426
1. Navegue a http://start.spring.io/ para ver algo similar a la siguiente captura de
pantalla:

2. Puede seleccionar la herramienta de compilación y gestión de dependencias,


seleccionando la opción adecuada en el menú desplegable después de Generar
un texto.
3. Spring Boot es compatible con Java, Kotlin y Groovy. Se puede elegir el idioma en el
menú desplegable cambiando después de que el con texto.
4. Seleccione la versión Spring Boot eligiendo su valor en el menú desplegable después
del texto y Spring Boot . Para esta receta, utilizaremos la última edición estable de
Spring Boot 2, que es 2.0.4.
5. En el lado izquierdo, bajo Metadatos del proyecto , tenemos que proporcionar
información relacionada con Maven, es decir, la identificación del grupo y la
identificación del artefacto. Utilizaremos Agrupar como com.packt
y Artefacto como boot_demo.
6. En el lado derecho, en Dependencias , puede buscar las dependencias que desea
agregar. Para esta receta, necesitamos dependencias web y Thymeleaf. Esto
significa que queremos crear una aplicación web que use plantillas de interfaz de
usuario Thymeleaf y que todas las dependencias, como Spring MVC y Embedded
Tomcat, formen parte de la aplicación.
7. Haga clic en el botón Generar proyecto para descargar el proyecto vacío. Puede
cargar este proyecto vacío en cualquier IDE de su elección, como cualquier otro
proyecto de Maven.

En este punto, tendrá su proyecto vacío cargado en un IDE de su elección y estará listo
para explorar más. En esta receta, haremos uso del motor de plantillas Thymeleaf para
definir nuestras páginas web y crear un controlador simple para representar la página
web.

El código completo de esta receta se puede encontrar


en Chapter09/1_boot_demo.

pág. 427
Cómo hacerlo...
1. Si ha seguido la convención de nomenclatura de ID de grupo e ID de artefacto
como se menciona en la sección Preparativos , tendrá una estructura de
paquete com.packt.boot_demo y una clase
BootDemoApplication.java principal ya creada para usted. Habrá una
estructura de paquete equivalente y una clase
BootDemoApplicationTests.java principal debajo de la testscarpeta.
2. Cree una nueva clase, SimpleViewController debajo
del com.packt.boot_demopaquete, con el siguiente código:

@Controller
public class SimpleViewController{
@GetMapping("/message")
public String message(){
return "message";
}
}

3. Cree una página web message.html,


debajo src/main/resources/templates, con el siguiente código:

<h1>Hello, this is a message from the Controller</h1>


<h2>The time now is [[${#dates.createNow()}]]</h2>

4. Desde el símbolo del sistema, navegue a la carpeta raíz del proyecto y emita el mvn
spring-boot:run comando; verá la aplicación que se está iniciando. Una vez que se
completa la inicialización y comienza, se ejecuta en el puerto por defecto, 8080. Navega
hasta http://localhost:8080/messagepara ver el mensaje.

Estamos utilizando el complemento Spring Boot's Maven, que nos proporciona


herramientas convenientes para iniciar la aplicación durante el desarrollo. Pero para la
producción, crearemos un JAR grueso, es decir, un JAR que comprenda todas las
dependencias, y lo implementaremos como un servicio de Linux o Windows. Incluso
podemos ejecutar el JAR gordo usando el comando java -jar.

Cómo funciona...
No entraremos en el funcionamiento de Spring Boot u otras bibliotecas de
Spring. Primavera de arranque crea un Tomcat integrado que se ejecuta en el puerto
por defecto, es decir, 8080. Luego registra todos los controladores, componentes y
servicios que están disponibles en los paquetes y subpaquetes de la clase con
la anotación @SpringBootApplication .

pág. 428
En nuestra receta, la clase BootDemoApplication en el paquete
com.packt.boot_demo se anota con @SpringBootApplication. Por lo tanto,
todas las clases que están anotados
con @Controller, @Service, @Configuration, y @Component quedan
registrados en el marco de la primavera como los beans y son administrados por el
mismo. Ahora, estos pueden inyectarse en el código utilizando la anotación
@Autowired.

Hay dos formas de crear un controlador web:

• Anotando con @Controller


• Anotando con @RestController

En el primer enfoque, creamos un controlador que puede servir tanto datos sin
procesar como datos HTML (generados por motores de plantillas como Thymeleaf,
Freemarker y JSP). En el segundo enfoque, el controlador admite puntos finales que
solo pueden servir datos sin formato en forma de JSON o XML. En nuestra receta,
utilizamos el enfoque anterior, de la siguiente manera:

@Controller
public class SimpleViewController{
@GetMapping("/message")
public String message(){
return "message";
}
}

Podemos anotar la clase con @RequestMapping , por


ejemplo, @RequestMapping("/api"). En este caso, cualquier punto final HTTP
expuesto en el controlador está precedido por /api. Hay un mapeo anotación
especializado para los HTTP GET, POST, DELETE, y PUTmétodos, que
son @GetMapping, @PostMapping, @DeleteMapping, y @PutMapping,
respectivamente. También podemos reescribir nuestra clase de controlador de la
siguiente manera:

@Controller
@RequestMapping("/message")
public class SimpleViewController{
@GetMapping
public String message(){
return "message";
}
}

Podemos modificar el puerto proporcionando server.port = 9090 en el archivo


application.properties. Este archivo se puede encontrar
en src/main/resources/application.properties. Hay un conjunto
completo de propiedades ( http://docs.spring.io/spring-

pág. 429
boot/docs/current/reference/html/common-application-
properties.html ) que podemos usar para personalizar y conectar con diferentes
componentes.

Interactuando con la base de


datos
En esta receta, veremos cómo integrarse con una base de datos para crear, leer,
modificar y eliminar los datos. Para esto, configuraremos una base de datos MySQL con
la tabla requerida. Posteriormente, actualizaremos los datos en una tabla desde nuestra
aplicación Spring Boot.

Usaremos Windows como plataforma de desarrollo para esta receta. También puede
realizar una acción similar en Linux, pero primero tendría que configurar su base de
datos MySQL.

Prepararse
Antes de comenzar a integrar nuestra aplicación con la base de datos, necesitamos
configurar la base de datos localmente en nuestras máquinas de desarrollo. En las
secciones siguientes, descargaremos e instalaremos herramientas MySQL y luego
crearemos una tabla de muestra con algunos datos, que utilizaremos con nuestra
aplicación.

Instalar herramientas MySQL


Primero, descargue el instalador MySQL
desde https://dev.mysql.com/downloads/windows/installer/5.7.html . Este paquete MySQL
es solo para Windows. Siga las instrucciones en pantalla para instalar con éxito MySQL
junto con otras herramientas como MySQL Workbench. Para confirmar que MySQL
daemon ( mysqld) se está ejecutando, abra el administrador de tareas y debería
poder ver un proceso similar al siguiente:

Debe recordar la contraseña que configuró para el usuario root.

pág. 430
Ejecutemos el banco de trabajo MySQL; al iniciar, debería poder ver algo similar a la
siguiente captura de pantalla, entre otras cosas proporcionadas por la herramienta:

Si no encuentra una conexión como en la imagen anterior, puede agregar una utilizando
el signo ( + ). Cuando haga clic en ( + ), verá el siguiente cuadro de diálogo. Rellene y
haga clic en Probar conexión para obtener un mensaje de éxito:

Una conexión de prueba exitosa dará como resultado el siguiente mensaje:

pág. 431
Haga doble clic en la conexión para conectarse a la base de datos, y debería ver una lista
de bases de datos en el lado izquierdo, un área vacía en el lado derecho y un menú y
barras de herramientas en la parte superior. En el menú Archivo, haga clic en Nueva
pestaña de consulta o presione Ctrl + T para obtener una nueva ventana de
consulta. Aquí, escribiremos nuestras consultas para crear una base de datos y crear
una tabla dentro de esa base de datos.

El instalador incluido descargado


de https://dev.mysql.com/downloads/windows/installer/5.7.html es solo para
Windows. Los usuarios de Linux deben descargar MySQL Server y MySQL Workbench
(GUI para interactuar con DB) por separado.
El servidor MySQL se puede descargar desde https://dev.mysql.com/downloads/mysql/ .
MySQL Workbench se puede descargar
desde https://dev.mysql.com/downloads/workbench/ .

Crear una base de datos de


muestra
Ejecute la siguiente instrucción SQL para crear una base de datos:

create database sample;

Crear una tabla de persona


Ejecute las siguientes instrucciones SQL para usar la base de datos recién creada y
cree una tabla de persona simple:

pág. 432
create table person(
id int not null auto_increment,
first_name varchar(255),
last_name varchar(255),
place varchar(255),
primary key(id)
);

Completar datos de muestra


Avancemos e inserte algunos datos de muestra en la tabla que acabamos de crear:

insert into person(first_name, last_name, place)


values('Raj', 'Singh', 'Bangalore');

insert into person(first_name, last_name, place)


values('David', 'John', 'Delhi');

Ahora que tenemos nuestra base de datos lista, continuaremos y descargaremos el


proyecto Spring Boot vacío de http://start.spring.io/ con las siguientes
opciones:

Cómo hacerlo...
1. Cree una clase modelo com.packt.boot_db_demo.Person para
representar a una persona. Haremos uso de las anotaciones de Lombok para
generar los captadores y establecedores para nosotros:

@Data
public class Person{
private Integer id;
private String firstName;
private String lastName;
private String place;
}

pág. 433
2. Cree com.packt.boot_db_demo.PersonMapperpara asignar los datos de
la base de datos a nuestra clase de modelo Person:

@Mapper
public interface PersonMapper {
}

3. Agreguemos un método para obtener todas las filas de la tabla. Tenga en cuenta
que los siguientes métodos se escribirán dentro de la interfaz PersonMapper:

@Select("SELECT * FROM person")


public List<Person> getPersons();

4. Otro método para obtener los detalles de una sola persona identificada por ID es el
siguiente:

@Select("SELECT * FROM person WHERE id = #{id}")


public Person getPerson(Integer id);

5. El método para crear una nueva fila en la tabla es el siguiente:

@Insert("INSERT INTO person(first_name, last_name, place) "


+ " VALUES (#{firstName}, #{lastName}, #{place})")
@Options(useGeneratedKeys = true)
public void insert(Person person);

6. El método para actualizar una fila existente en la tabla, identificada por la ID, es la
siguiente:

@Update("UPDATE person SET first_name = #{firstName},last_name =


#{lastName}, "+ "place = #{place} WHERE id = #{id} ")
public void save(Person person);

7. El método para eliminar una fila de la tabla, identificado por la ID, es el siguiente:

@Delete("DELETE FROM person WHERE id = #{id}")


public void delete(Integer id);

8. Creemos una com.packt.boot_db_demo.PersonControllerclase, que


usaremos para escribir nuestros puntos finales web:

@Controller
@RequestMapping("/persons")
public class PersonContoller {
@Autowired PersonMapper personMapper;
}

9. Creemos un punto final para enumerar todas las entradas de la tabla person:
@GetMapping
public String list(ModelMap model){

pág. 434
List<Person> persons = personMapper.getPersons();
model.put("persons", persons);
return "list";
}

10. Creemos un punto final para agregar una nueva fila en la tabla person:

@GetMapping("/{id}")
public String detail(ModelMap model, @PathVariable Integer id){
System.out.println("Detail id: " + id);
Person person = personMapper.getPerson(id);
model.put("person", person);
return "detail";
}

11. Creemos un punto final para agregar una nueva fila o editar una fila existente en
la tabla person:

@PostMapping("/form")
public String submitForm(Person person){
System.out.println("Submiting form person id: " +
person.getId());
if ( person.getId() != null ){
personMapper.save(person);
}else{
personMapper.insert(person);
}
return "redirect:/persons/";
}

12. Creemos un punto final para eliminar una fila de la persontabla:

@GetMapping("/{id}/delete")
public String deletePerson(@PathVariable Integer id){
personMapper.delete(id);
return "redirect:/persons";
}

13. Actualice el archivo src/main/resources/application.properties


para proporcionar la configuración relacionada con nuestra fuente de datos, es decir,
nuestra base de datos MySQL:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/sample?useSSL=false
spring.datasource.username=root
spring.datasource.password=mohamed
mybatis.configuration.map-underscore-to-camel-case=true

Puede ejecutar la aplicación desde la línea de comandos usando mvn spring-


boot:run. Esta aplicación se inicia en el puerto por defecto, es decir, 8080. Navega
hasta http://localhost:8080/persons en tu navegador.

pág. 435
El código completo de esta receta se puede encontrar
en Chapter09/2_boot_db_demo.

Al visitar http://localhost:8080/persons, esto es lo que encontrará:

Al hacer clic en Nueva persona , obtendrá lo siguiente:

Al hacer clic en Editar , obtendrá lo siguiente:

Cómo funciona...
pág. 436
En primer lugar, com.packt.boot_db_demo.PersonMapper con la
anotación org.apache.ibatis.annotations.Mappersabe cómo ejecutar la
consulta proporcionada dentro de los @Select, @Updatey @Deletelas
anotaciones y volver resultados relevantes. Todo esto es administrado por las
bibliotecas MyBatis y Spring Data.

Debe preguntarse cómo se logró la conexión a la base de datos. Una de las clases de
configuración automática de Spring Boot DataSourceAutoConfiguration hace
el trabajo de configuración haciendo uso de
las spring.datasource.*propiedades definidas en su archivo
application.properties para darnos una instancia
de javax.sql.DataSource. javax.sql.DataSource Luego, la biblioteca
MyBatis usa este objeto para proporcionarle una instancia
de SqlSessionTemplate, que es lo que usamos PersonMapper bajo el capó.

Luego, utilizamos com.packt.boot_db_demo.PersonMapper inyectándolo en


la clase com.packt.boot_db_demo.PersonController
mediante @AutoWired. La anotación @AutoWired busca cualquier bean
administrado por Spring, que son instancias del tipo exacto o su implementación. Eche
un vistazo a Crear una receta simple de aplicación Spring Boot en este capítulo para
comprender la anotación @Controller.

Con muy poca configuración, hemos podido configurar rápidamente operaciones CRUD
simples. ¡Esta es la flexibilidad y agilidad que Spring Boot proporciona a los
desarrolladores!

Crear un servicio web RESTful


En nuestra receta anterior, interactuamos con los datos mediante formularios web. En
esta receta, veremos cómo interactuar con los datos utilizando los servicios web
RESTful. Estos servicios web son un medio para interactuar con otras aplicaciones
utilizando el protocolo HTTP conocido y sus métodos, a saber, GET, POST y PUT. Los
datos se pueden intercambiar en forma de XML, JSON o incluso texto sin
formato. Usaremos JSON en nuestra receta.

Por lo tanto, crearemos API RESTful para admitir la recuperación de datos, la creación
de nuevos datos, la edición de datos y la eliminación de datos.

Prepararse

pág. 437
Como de costumbre, descargue el proyecto de inicio
desde http://start.spring.io/ seleccionando las dependencias que se muestran en la
siguiente captura de pantalla:

Cómo hacerlo...
1. Copie la clase Person de la receta anterior:

public class Person {


private Integer id;
private String firstName;
private String lastName;
private String place;
//required getters and setters
}

2. Haremos la parte PersonMapper de una manera diferente. Escribiremos todas


nuestras consultas SQL en un archivo XML de mapeador y luego nos referiremos a ellas
desde la PersonMapperinterfaz. Colocaremos el mapeador XML debajo de
la carpeta src/main/resources/mappers. Estableceremos el valor de la propiedad
mybatis.mapper-locations en classpath*:mappers/*.xml. De esta
manera, la interfaz PersonMapper puede descubrir las consultas SQL correspondientes
a sus métodos.

3. Crea la interfaz com.packt.boot_rest_demo.PersonMapper:

@Mapper
public interface PersonMapper {
public List<Person> getPersons();
public Person getPerson(Integer id);
public void save(Person person);
public void insert(Person person);
public void delete(Integer id);
}

pág. 438
4. Crea el SQL en PersonMapper.xml. Asegúrese de que el namespaceatributo
de la <mapper>etiqueta sea el mismo que el nombre completo de la interfaz
PersonMapper del asignador:

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"


"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.packt.boot_rest_demo.PersonMapper">
<select id="getPersons"
resultType="com.packt.boot_rest_demo.Person">
SELECT id, first_name firstname, last_name lastname, place
FROM person
</select>

<select id="getPerson"
resultType="com.packt.boot_rest_demo.Person"
parameterType="long">
SELECT id, first_name firstname, last_name lastname, place
FROM person
WHERE id = #{id}
</select>

<update id="save"
parameterType="com.packt.boot_rest_demo.Person">
UPDATE person SET
first_name = #{firstName},
last_name = #{lastName},
place = #{place}
WHERE id = #{id}
</update>

<insert id="insert"
parameterType="com.packt.boot_rest_demo.Person"
useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO person(first_name, last_name, place)
VALUES (#{firstName}, #{lastName}, #{place})
</insert>

<delete id="delete" parameterType="long">


DELETE FROM person WHERE id = #{id}
</delete>
</mapper>

5. Defina las propiedades de la aplicación en el archivo


src/main/resources/application.properties:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/sample?
useSSL=false
spring.datasource.username=root
spring.datasource.password=mohamed
mybatis.mapper-locations=classpath*:mappers/*.xml

6. Cree un controlador vacío para las API REST. Este controlador se marcará con
la anotación @RestController porque todas las API en él se ocuparán únicamente de
los datos:

pág. 439
@RestController
@RequestMapping("/api/persons")
public class PersonApiController {
@Autowired PersonMapper personMapper;
}

7. Agregue una API para enumerar todas las filas de la persontabla:

@GetMapping
public ResponseEntity<List<Person>> getPersons(){
return new ResponseEntity<>(personMapper.getPersons(),
HttpStatus.OK);
}

8. Agregue una API para obtener los detalles de una sola persona:

@GetMapping("/{id}")
public ResponseEntity<Person> getPerson(@PathVariable Integer id){
return new ResponseEntity<>(personMapper.getPerson(id),
HttpStatus.OK);
}

9. Agregue una API para agregar nuevos datos a la tabla:

@PostMapping
public ResponseEntity<Person> newPerson
(@RequestBody Person person){
personMapper.insert(person);
return new ResponseEntity<>(person, HttpStatus.OK);
}

10. Agregue una API para editar los datos en la tabla:

@PostMapping("/{id}")
public ResponseEntity<Person> updatePerson
(@RequestBody Person person,
@PathVariable Integer id){
person.setId(id);
personMapper.save(person);
return new ResponseEntity<>(person, HttpStatus.OK);
}

11. Agregue una API para eliminar los datos en la tabla:

@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePerson
(@PathVariable Integer id){
personMapper.delete(id);
return new ResponseEntity<>(HttpStatus.OK);
}

pág. 440
Puede encontrar el código completo en Chapter09/3_boot_rest_demo. Puede
iniciar la aplicación utilizando mvn spring-boot:run desde la carpeta del
proyecto. Una vez que la aplicación ha comenzado,
navegue http://localhost:8080/api/persons para ver todos los datos en la
tabla de personas.

Para probar las otras API, haremos uso de la aplicación cliente REST Postman para
Google Chrome.

Esto es lo que parece agregar una nueva persona. Mire el cuerpo de la solicitud, es
decir, el detalle de la persona especificado en JSON:

Así es como editamos los detalles de una persona:

pág. 441
Así es como se ve eliminar a una persona:

Cómo funciona...
pág. 442
Primero, veamos cómo la interfaz PersonMapper descubre las instrucciones SQL
para ejecutar. Si observa src/main/resources/mappers/PersonMapper.xml,
encontrará que el atributo <mapper> namespace
es org.packt.boot_rest_demo.PersonMapper. Este es un requisito para que
el valor del atributo namespace sea el nombre completo de la interfaz del
asignador, que, en nuestro caso,
es org.packt.boot_rest_demo.PersonMapper.

A continuación, los atributos id de las sentencias SQL individuales definidas dentro


de <select>, <insert>, <update>, y <delete>debe coincidir con el nombre del
método en la interfaz del asignador. Por ejemplo, el método getPersons()en
la interfaz PersonMapper busca una instrucción SQL con id="getPersons".

Ahora, la biblioteca MyBatis descubre la ubicación de este mapeador XML leyendo el


valor de la propiedad mybatis.mapper-locations .

Viniendo al controlador, hemos introducido una nueva


anotación, @RestController. Esta anotación especial indica, además de ser un
controlador web, que todos los métodos definidos en la clase devuelven una respuesta
que se envía a través del cuerpo de respuesta HTTP; también lo hacen todas las API
REST. Simplemente trabajan con los datos.

Como de costumbre, puede iniciar su aplicación Spring Boot utilizando el


complemento Maven Spring Boot mvn spring-boot:run o ejecutando el JAR
creado por el paquete Maven java -jar my_jar_name.jar.

Crear múltiples perfiles para


Spring Boot
En general, las aplicaciones web se implementan en diferentes entornos: primero, se
ejecutan localmente en la máquina de un desarrollador, luego se implementan en
servidores de prueba y finalmente se implementan en servidores de
producción. Tendríamos la aplicación interactuando con componentes ubicados en
diferentes lugares para cada entorno. El mejor enfoque para esto es mantener
diferentes perfiles para cada entorno. Una forma de hacerlo es creando diferentes
versiones del archivo application.properties, es decir, diferentes versiones del
archivo que almacena las propiedades de nivel de aplicación. Estos archivos de
propiedades en Spring Boot también pueden ser archivos YML,
como application.yml. Incluso si crea diferentes versiones, necesita un
mecanismo para indicar a sus aplicaciones que seleccionen la versión relevante del
archivo, en función del entorno en el que se ha implementado.

pág. 443
Spring Boot proporciona un soporte increíble para tal característica. Le permite tener
múltiples archivos de configuración, cada uno de los cuales representa un perfil
específico, y luego, puede iniciar su aplicación en diferentes perfiles, dependiendo del
entorno en el que se esté implementando. Veamos esto en acción, y luego explicaremos
cómo funciona.

Prepararse
Para esta receta, hay dos opciones para alojar otra instancia de su base de datos
MySQL:

1. Use un proveedor de la nube como AWS y use su Servicio de base de datos


relacional de Amazon ( RDS ) ( https://aws.amazon.com/rds/ ). Tienen
un cierto límite de uso gratuito.
2. Use un proveedor de la nube como DigitalOcean
( https://www.digitalocean.com/ ) para comprar una gotita (es decir, un
servidor) por tan solo $ 5 por mes. Instale el servidor MySQL en él.
3. Use VirtualBox para instalar Linux en su máquina, suponiendo que estamos usando
Windows, o viceversa si está usando Linux. Instale el servidor MySQL en él.

Las opciones son mucho más, desde los servicios de bases de datos alojadas hasta los
servidores, que le brindan acceso completo a la raíz para instalar el servidor
MySQL. Para esta receta, hicimos lo siguiente:

1. Compramos una gota básica de DigitalOcean.


2. Instalamos MySQL usando sudo apt-get install mysql-server-
5.7una contraseña para el usuario root.
3. Creamos otro usuario, springboot para que podamos usar este usuario para
conectarse desde nuestra aplicación de servicio web RESTful:

$ mysql -uroot -p
Enter password:
mysql> create user 'springboot'@'%' identified by 'springboot';

4. Modificamos el archivo de configuración de MySQL para que MySQL permita


conexiones remotas. Esto se puede hacer editando la bind-addresspropiedad en
el /etc/mysql/mysql.conf.d/mysqld.cnfarchivo para la IP del servidor.

5. Desde el banco de trabajo de MySQL, hemos añadido la nueva conexión de MySQL


utilizando IP = <Digital Ocean droplet IP>, username =
springbooty password = springboot.

La ubicación del archivo de configuración de MySQL en Ubuntu OS


es /etc/mysql/mysql.conf.d/mysqld.cnf. Una forma de averiguar la

pág. 444
ubicación de un archivo de configuración específico para su sistema operativo es hacer
lo siguiente:

1. Ejecutar mysql --help.


2. En la salida, busque Default options are read from the
following files in the given order:. Lo que sigue son las posibles
ubicaciones para el archivo de configuración de MySQL.

Crearemos la tabla requerida y rellenaremos algunos datos. Pero antes de eso,


crearemos la samplebase de datos como rooty le otorgaremos todos los privilegios
al springbootusuario:

mysql -uroot
Enter password:

mysql> create database sample;

mysql> GRANT ALL ON sample.* TO 'springboot'@'%';


Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;

Ahora, conectemos a la base de datos como el usuario springboot, cree la tabla


requerida y complete con algunos datos de muestra:

mysql -uspringboot -pspringboot

mysql> use sample


Database changed
mysql> create table person(
-> id int not null auto_increment,
-> first_name varchar(255),
-> last_name varchar(255),
-> place varchar(255),
-> primary key(id)
-> );
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO person(first_name, last_name, place) VALUES('Mohamed',


'Sanaulla', 'Bangalore');
mysql> INSERT INTO person(first_name, last_name, place) VALUES('Nick',
'Samoylov', 'USA');

mysql> SELECT * FROM person;


+----+------------+-----------+-----------+
| id | first_name | last_name | place |
+----+------------+-----------+-----------+
| 1 | Mohamed | Sanaulla | Bangalore |
| 2 | Nick | Samoylov | USA |
+----+------------+-----------+-----------+
2 rows in set (0.00 sec)

pág. 445
Ahora, tenemos lista nuestra instancia de MySQL DB en la nube. Veamos cómo
administrar la información de dos conexiones diferentes según el perfil en el que se
ejecuta la aplicación.

La aplicación de muestra inicial requerida para esta receta se puede encontrar


en Chapter09/4_boot_multi_profile_incomplete. Convertiremos esta
aplicación para que se ejecute en diferentes entornos.

Cómo hacerlo...
1. En el archivo src/main/resources/application.properties,
agregue una nueva propiedad springboot spring.profiles.active
= local,.
2. Cree un nuevo archivo, application-
local.propertiesen src/main/resources/.
3. Agregue las siguientes propiedades application-local.propertiesy
elimínelas del archivo application.properties:

spring.datasource.url=jdbc:mysql://localhost/sample?useSSL=false
spring.datasource.username=root
spring.datasource.password=mohamed

4. Cree otro archivo application-cloud.properties,


en src/main/resources/.

5. Agregue las siguientes propiedades a application-cloud.properties:

spring.datasource.url=
jdbc:mysql://<digital_ocean_ip>/sample?useSSL=false
spring.datasource.username=springboot
spring.datasource.password=springboot

El código completo para la aplicación completa se puede encontrar


en Chapter09/4_boot_multi_profile_incomplete. Puede ejecutar la
aplicación con el mvn spring-boot:run comando Spring Boot lee
la spring.profiles.activepropiedad del application.properties
archivo y ejecuta la aplicación en un perfil local. Abra
la http://localhost:8080/api/persons URL en el navegador para encontrar
los siguientes datos:

[
{
"id": 1,
"firstName": "David ",
"lastName": "John",
"place": "Delhi"

pág. 446
},
{
"id": 2,
"firstName": "Raj",
"lastName": "Singh",
"place": "Bangalore"
}
]

Ahora, ejecute la aplicación en el perfil de la nube con el comando mvn spring-


boot:run -Dspring.profiles.active=cloud Luego,
abra http://localhost:8080/api/persons en el navegador para encontrar
los siguientes datos:

[
{
"id": 1,
"firstName": "Mohamed",
"lastName": "Sanaulla",
"place": "Bangalore"
},
{
"id": 2,
"firstName": "Nick",
"lastName": "Samoylov",
"place": "USA"
}
]

Puede ver que hay un conjunto diferente de datos devueltos por la misma API y los
datos anteriores se insertaron en nuestra base de datos MySQL que se ejecuta en la
nube. Por lo tanto, hemos podido ejecutar con éxito la aplicación en dos perfiles
diferentes: local y en la nube.

Cómo funciona...
Hay varias formas en que Spring Boot puede leer la configuración de la
aplicación. Algunos importantes se enumeran aquí en el orden de su relevancia (la
propiedad definida en la fuente anterior anula la propiedad definida en las fuentes
posteriores):

• Desde la línea de comando. Las propiedades se especifican mediante la -Dopción,


como lo hicimos durante el lanzamiento de la aplicación en el perfil de nubes, mvn
spring-boot:run -Dspring.profiles.active=cloud. O, si está
utilizando JAR, sería java -Dspring.profiles.active=cloud -jar
myappjar.jar.
• Desde las propiedades del sistema Java, usando System.getProperties().
• Variables de entorno del sistema operativo.

pág. 447
• Propiedades de aplicación específicas del perfil application-
{profile}.properties, o los archivos application-{profile}.yml,
fuera del JAR empaquetado.
• Propiedades de aplicación específicas del perfil, los archivos application-
{profile}.propertieso application-{profile}.yml,
empaquetados dentro del JAR.
• Propiedades de la aplicación application.properties,
o application.yml definidas fuera del JAR empaquetado.
• Propiedades de la aplicación application.properties, o empaquetado
application.yml dentro del JAR.
• Las clases de configuración (es decir, anotadas con @Configuration) sirven
como fuentes de propiedad (anotadas con @PropertySource).
• Propiedades predeterminadas de Spring Boot.

En nuestra receta, especificamos todas las propiedades genéricas, como las siguientes,
en el archivo application.properties, y todas las propiedades específicas del
perfil se especificaron en el archivo de propiedades de la aplicación específica del perfil:

spring.profiles.active=local
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

mybatis.mapper-locations=classpath*:mappers/*.xml
mybatis.configuration.map-underscore-to-camel-case=true

De la lista anterior, podemos encontrar que


el archivo application.propertieso application-
{profile}.properties se puede definir fuera de la aplicación JAR. Hay
ubicaciones predeterminadas donde Spring Boot buscará el archivo de propiedades, y
una de esas rutas es el subdirectorio config del directorio actual desde el que se
ejecuta la aplicación.

La lista completa de propiedades de aplicaciones compatibles con Spring Boot se


puede encontrar en http://docs.spring.io/spring-
boot/docs/current/reference/html/common-application-
properties.html . Además de estos, podemos crear nuestras propias propiedades,
que serán necesarias para nuestra aplicación.

El código completo de esta receta se puede encontrar


en Chapter09/4_boot_multi_profile_complete.

Hay más...
Podemos crear un servidor de configuración usando Spring Boot, que actuará como un
repositorio para todas las propiedades de todas las aplicaciones en todos los

pág. 448
perfiles. Las aplicaciones cliente pueden conectarse con el servidor de configuración
para leer las propiedades relevantes en función del nombre de la aplicación y el perfil
de la aplicación.

En el servidor de configuración, las propiedades de la aplicación se pueden leer desde


el sistema de archivos usando classpath o un repositorio de GitHub. La ventaja de usar
un repositorio de GitHub es que los archivos de propiedades se pueden versionar. Los
archivos de propiedades en el servidor de configuración se pueden actualizar, y estas
actualizaciones se pueden enviar a las aplicaciones del cliente configurando una cola de
mensajes para retransmitir los cambios en sentido descendente. Otra forma es usar
los beans @RefreshScope y luego invocar la API /refresh siempre que
necesitemos que las aplicaciones del cliente realicen los cambios de configuración.

Implementación de servicios web


RESTful en Heroku
Platform as a Service ( Paas ) es uno de los modelos de computación en la nube (los
otros dos son Software as a Service ( SaaS ) e Infrastructure as a Service ( IaaS ))
donde el proveedor de computación en la nube proporciona plataformas informáticas
administradas, que incluyen SO, programación Language Runtime, base de datos y
otros complementos como colas, administración de registros y alertas. También le
proporcionan herramientas para facilitar la implementación y paneles para monitorear
sus aplicaciones.

Heroku es uno de los primeros jugadores en el campo de los proveedores de PaaS. Es


compatible con los siguientes lenguajes de programación: Ruby, Node.js, Java, Python,
Clojure, Scala, Go y PHP. Heroku admite múltiples almacenes de datos, como MySQL,
MongoDB, Redis y Elastic search. Proporciona integración con herramientas de
registro, utilidades de red, servicios de correo electrónico y herramientas de
monitoreo.

Heroku proporciona una herramienta de línea de comandos llamada heroku-cli


( cli.heroku.com ), que se puede usar para crear aplicaciones de Heroku,
implementar, monitorear, agregar recursos y más. La CLI también admite la
funcionalidad proporcionada por su panel web. Utiliza Git para almacenar el código
fuente de la aplicación. Por lo tanto, cuando inserta el código de la aplicación en el
repositorio Git de Heroku, desencadena una compilación, en función del paquete de
compilación que está utilizando. Luego, utiliza la forma predeterminada para generar
la aplicación o ejecutar su aplicación ProcFile.

En esta receta, implementaremos nuestro servicio web RESTful basado en Spring Boot
en Heroku. Continuaremos usando la base de datos que creamos en otro proveedor de
la nube en la receta anterior, Creación de múltiples perfiles para Spring Boot .

pág. 449
Prepararse
Antes de proceder con la implementación de nuestra aplicación de muestra en Heroku,
debemos registrarnos para obtener una cuenta de Heroku e instalar sus herramientas,
lo que nos permitirá trabajar desde la línea de comandos. En las secciones siguientes,
lo guiaremos a través del proceso de registro, creando una aplicación de muestra a
través de la interfaz de usuario web y a través de la interfaz de línea de
comandos ( CLI ) de Heroku .

Configurar una cuenta de Heroku


Visite http://www.heroku.com y regístrese si no tiene una cuenta. Si tiene una cuenta,
puede iniciar sesión. Para registrarse, visite https://signup.heroku.com :

pág. 450
Para iniciar sesión, la URL es https://id.heroku.com/login :

pág. 451
Una vez que inicie sesión correctamente, verá un panel con la lista de aplicaciones, si
tiene alguna:

Crear una nueva aplicación desde


la interfaz de usuario

pág. 452
Haga clic en nuevo | Cree una nueva aplicación , complete los detalles y haga clic
en Crear aplicación :

Crear una nueva aplicación desde


la CLI
Realice los siguientes pasos para crear una nueva aplicación desde la CLI:

1. Instale la CLI de Heroku desde https://cli.heroku.com .


2. Una vez instalado, Heroku debería estar en la PATHvariable de su sistema .
3. Abra un símbolo del sistema y ejecútelo heroku create. Verá una salida similar
a la siguiente:

Creating app... done, glacial-beyond-27911


https://glacial-beyond-27911.herokuapp.com/ |
https://git.heroku.com/glacial-beyond-27911.git

pág. 453
4. El nombre de la aplicación se genera dinámicamente y se crea un repositorio Git
remoto. Puede especificar el nombre y la región de la aplicación (como se hace a través de
la interfaz de usuario) ejecutando el siguiente comando:

$ heroku create test-app-9812 --region us


Creating test-app-9812... done, region is us
https://test-app-9812.herokuapp.com/ |
https://git.heroku.com/test-app-9812.git

La implementación en Heroku se realiza a través git push del repositorio Git


remoto creado en Heroku. Lo veremos en la siguiente sección.

Tenemos el código fuente de la aplicación


en Chapter09/5_boot_on_heroku. Entonces, copie esta aplicación y continúe e
implemente en Heroku.

Debe iniciar sesión en la cuenta de Heroku antes de ejecutar cualquiera de los comandos
en el cli de Heroku. Puede iniciar sesión ejecutando el comando heroku login .

Cómo hacerlo...
1. Ejecute el siguiente comando para crear una aplicación Heroku:

$ heroku create <app_name> -region us

2. Inicialice el repositorio de Git en la carpeta del proyecto:

$ git init

3. Agregue el repositorio Heroku Git como control remoto a su repositorio local de


Git:

$ heroku git:remote -a <app_name_you_chose>

4. Inserte el código fuente, es decir, la rama maestra, en el repositorio Heroku Git:

$ git add .
$ git commit -m "deploying to heroku"
$ git push heroku master

5. Cuando el código se inserta en el repositorio Heroku Git, desencadena una


compilación. Como estamos usando Maven, ejecuta el siguiente comando:

./mvnw -DskipTests clean dependency:list install

pág. 454
6. Una vez que el código ha completado la compilación y desplegado, puede abrir la
aplicación mediante el comando heroku open . Esto abrirá la aplicación en un
navegador.

7. Puede controlar los registros de la aplicación con el comando heroku logs -


-tail

Una vez que la aplicación se haya implementado correctamente y después de ejecutar


el comando heroku open , debería ver la URL que está cargando el navegador:

Al hacer clic en el enlace Personas, se mostrará la siguiente información:

[
{
"id":1,
"firstName":"Mohamed",
"lastName":"Sanaulla",
"place":"Bangalore"
},
{
"id":2,
"firstName":"Nick",
"lastName":"Samoylov",
"place":"USA"
}
]

Lo interesante aquí es que tenemos nuestra aplicación ejecutándose en Heroku, que se


conecta a una base de datos MySQL en un servidor DigitalOcean. Incluso podemos
aprovisionar una base de datos junto con la aplicación Heroku y conectarnos a esa
base de datos. Vea cómo hacer esto en la sección Hay más ...

Hay más...
1. Agregue un nuevo complemento de base de datos a la aplicación:

$ heroku addons:create jawsdb:kitefin

pág. 455
Aquí, addons:createtoma el nombre del complemento y el nombre del plan de
servicio, ambos separados por dos puntos ( :). Puede obtener más información sobre
los detalles y planes del complemento
en https://elements.heroku.com/addons/jawsdb-maria . Además, el
comando Heroku CLI para agregar el complemento a su aplicación se da hacia el final
de la página de detalles del complemento para todos los complementos.

2. Abra el panel de la base de datos para ver los detalles de la conexión, como URL,
nombre de usuario, contraseña y el nombre de la base de datos:

$ heroku addons:open jawsdb

El tablero jawsdb de instrumentos tiene un aspecto similar al siguiente:

3. Incluso puede obtener la cadena de conexión MySQL desde


la JAWSDB_URLpropiedad de configuración. Puede enumerar la configuración de su
aplicación con el siguiente comando:

$ heroku config
=== rest-demo-on-cloud Config Vars
JAWSDB_URL: <URL>

4. Copie los detalles de la conexión, cree una nueva conexión en MySQL Workbench y
conéctese a esta conexión. El complemento también crea el nombre de la base de
datos. Ejecute las siguientes instrucciones SQL después de conectarse a la base de datos:

pág. 456
use x81mhi5jwesjewjg;
create table person(
id int not null auto_increment,
first_name varchar(255),
last_name varchar(255),
place varchar(255),
primary key(id)
);

INSERT INTO person(first_name, last_name, place)


VALUES('Heroku First', 'Heroku Last', 'USA');

INSERT INTO person(first_name, last_name, place)


VALUES('Jaws First', 'Jaws Last', 'UK');

5. Cree un nuevo archivo de propiedades para el perfil de Heroku,, application-


heroku.propertiesat src/main/resources, con las siguientes propiedades:

spring.datasource.url=jdbc:mysql://
<URL DB>:3306/x81mhi5jwesjewjg?useSSL=false
spring.datasource.username=zzu08pc38j33h89q
spring.datasource.password=<DB password>

Puede encontrar los detalles relacionados con la conexión en el panel de


complementos.

6. Actualice el archivo src/main/resources/application.properties


para reemplazar el valor de la propiedad heroku spring.profiles.active.

7. Compromete y empuja los cambios al control remoto Heroku:

$ git commit -am"using heroky mysql addon"


$ git push heroku master

8. Una vez que la implementación tenga éxito, ejecute el comando heroku


open . Una vez que la página se carga en el navegador, haga clic en
el enlace Personas . Esta vez, verá un conjunto de datos diferente, el que ingresamos en
nuestro complemento Heroku:
9. [
{
"id":1,
"firstName":"Heroku First",
"lastName":"Heroku Last",
"place":"USA"
},
{
"id":2,
"firstName":"Jaws First",
"lastName":"Jaws Last",
"place":"UK"
}
]

pág. 457
10. Con esto, nos hemos integrado con una base de datos que creamos en Heroku.

Contenedor del servicio web


RESTful usando Docker
Hemos avanzado mucho desde el momento en que se instalaba una aplicación en todos
los servidores, hasta que cada servidor se virtualizaba y luego la aplicación se instalaba
en estas máquinas virtuales más pequeñas. Los problemas de escalabilidad para las
aplicaciones se resolvieron agregando más máquinas virtuales, con la aplicación
ejecutándose en el equilibrador de carga.

En la virtualización, un servidor grande se divide en múltiples máquinas virtuales al


asignar la potencia informática, la memoria y el almacenamiento entre las múltiples
máquinas virtuales. De esta manera, cada una de las máquinas virtuales es capaz de
todas las cosas que era un servidor, aunque a menor escala. Con esta virtualización nos
ha ayudado mucho a utilizar juiciosamente los recursos informáticos, de memoria y de
almacenamiento del servidor.

Sin embargo, la virtualización necesita cierta configuración, es decir, debe crear la


máquina virtual, instalar las dependencias requeridas y luego ejecutar la
aplicación. Además, es posible que no esté 100% seguro de que la aplicación se ejecute
correctamente. El motivo de la falla puede deberse a las versiones incompatibles del
sistema operativo o incluso a alguna configuración perdida durante la configuración o
alguna dependencia faltante. Esta configuración también conlleva algunas dificultades
en el escalado horizontal porque se pasa algo de tiempo aprovisionando la máquina
virtual y luego implementando la aplicación.

El uso de herramientas como Puppet y Chef ayuda en el aprovisionamiento, pero la


configuración de la aplicación a menudo puede dar lugar a problemas que podrían
deberse a una configuración incorrecta o faltante. Esto condujo a la introducción de
otro concepto, llamado contenedorización.

En el mundo de la virtualización, tenemos el sistema operativo host y luego el software


de virtualización, es decir, el hipervisor. Luego terminamos creando múltiples
máquinas, donde cada máquina tiene su propio sistema operativo en el que se
implementan las aplicaciones. Sin embargo, en la contenedorización, no dividimos los
recursos del servidor. En cambio, tenemos el servidor con su sistema operativo host, y
por encima de eso, tenemos una capa de contenedorización que es una capa de
abstracción de software. Empaquetamos aplicaciones como contenedores, donde un
contenedor está empaquetado con las funciones de sistema operativo necesarias para
ejecutar la aplicación, las dependencias de software para la aplicación y luego la

pág. 458
aplicación misma. La siguiente imagen, tomada de https://docs.docker.com/get-
started/#container-diagram , representa mejor esto:

La imagen anterior ilustra una arquitectura típica de los sistemas de virtualización. La


siguiente imagen ilustra una arquitectura típica de los sistemas de contenedorización:

pág. 459
La mayor ventaja de la contenedorización es que agrupa todas las dependencias de la
aplicación en una imagen de contenedor. Esta imagen se ejecuta en la plataforma de
contenedorización, lo que lleva a la creación de un contenedor. Podemos tener
múltiples contenedores ejecutándose simultáneamente en el servidor. Si es necesario
agregar más instancias, simplemente podemos implementar la imagen, y esta
implementación se puede automatizar para admitir una alta escalabilidad de una
manera fácil.

Docker es una de las plataformas de software de contenedores más populares. En esta


receta, empaquetaremos nuestra aplicación de muestra que se encuentra en la
ubicación Chapter09/6_boot_with_docker en una imagen Docker y
ejecutaremos la imagen Docker para iniciar nuestra aplicación.

Prepararse
pág. 460
Para esta receta, utilizaremos un servidor Linux con Ubuntu 16.04.2 x64:

1. Descargue el último archivo.deb


de https://download.docker.com/linux/ubuntu/dists/xenial
/pool/stable/amd64/ . Para otras distribuciones de Linux, puede encontrar
los paquetes en https://download.docker.com/linux/ :

$ wget https://download.docker.com/linux/ubuntu/dists/xenial
/pool/stable/amd64/docker-ce_17.03.2~ce-0~ubuntu-xenial_amd64.deb

2. Instale el paquete Docker usando el administrador de paquetes dpkg:

$ sudo dpkg -i docker-ce_17.03.2~ce-0~ubuntu-xenial_amd64.deb

El nombre del paquete variará según la versión que haya descargado.

3. Después de una instalación exitosa, el servicio Docker comienza a


ejecutarse. Puede verificar esto usando el comando service:

$ service docker status


docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled;
vendor preset: enabled)
Active: active (running) since Fri 2017-07-28 13:46:50 UTC;
2min 3s ago
Docs: https://docs.docker.com
Main PID: 22427 (dockerd)

La aplicación para dockerizar está disponible


en Chapter09/6_boot_with_docker, en el código fuente descargado para este
libro.

Cómo hacerlo...
1. Cree Dockerfile en la raíz de la aplicación con el siguiente contenido:

FROM ubuntu:17.10
FROM openjdk:9-b177-jdk
VOLUME /tmp
ADD target/boot_docker-1.0.jar restapp.jar
ENV JAVA_OPTS="-Dspring.profiles.active=cloud"
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar /restapp.jar" ]

2. Ejecute el siguiente comando para crear una imagen de Docker utilizando


la Dockerfile que creamos en el paso anterior:

$ docker build --tag restapp-image .

pág. 461
Sending build context to Docker daemon 18.45 MB
Step 1/6 : FROM ubuntu:17.10
---> c8cdcb3740f8
Step 2/6 : FROM openjdk:9-b177-jdk
---> 38d822ff5025
Step 3/6 : VOLUME /tmp
---> Using cache
---> 38367613d375
Step 4/6 : ADD target/boot_docker-1.0.jar restapp.jar
---> Using cache
---> 54ad359f53f7
Step 5/6 : ENV JAVA_OPTS "-Dspring.profiles.active=cloud"
---> Using cache
---> dfa324259fb1
Step 6/6 : ENTRYPOINT sh -c java $JAVA_OPTS -jar /restapp.jar
---> Using cache
---> 6af62bd40afe
Successfully built 6af62bd40afe

3. Puede ver las imágenes que se instalaron con el siguiente comando:


4. $ docker images

REPOSITORY TAG IMAGE ID CREATED SIZE


restapp-image latest 6af62bd40afe 4 hours ago 606 MB
openjdk 9-b177-jdk 38d822ff5025 6 days ago 588 MB
ubuntu 17.10 c8cdcb3740f8 8 days ago 93.9 MB

Verá que también hay imágenes de OpenJDK y Ubuntu. Estos se descargaron para
crear la imagen de nuestra aplicación, que se enumera primero.

4. Ejecute la imagen para crear un contenedor que contenga nuestra aplicación en


ejecución:

docker run -p 8090:8080 -d --name restapp restapp-image


d521b9927cec105d8b69995ef6d917121931c1d1f0b1f4398594bd1f1fcbee55

La cadena grande impresa después del runcomando es el identificador del


contenedor. Puede usar los pocos caracteres iniciales para identificar de forma
exclusiva el contenedor. Como alternativa, puede utilizar el nombre del
contenedor restapp.

5. La aplicación ya habrá comenzado. Puede ver los registros ejecutando el siguiente


comando:

docker logs restapp

6. Puede ver los contenedores Docker creados mediante el siguiente comando:

docker ps

El resultado del comando anterior es similar al siguiente:

pág. 462
7. Puede administrar el contenedor con el siguiente comando:

$ docker stop restapp


$ docker start restapp

Una vez que la aplicación se esté ejecutando,


ábrala http://<hostname>:8090/api/persons.

Cómo funciona...
Defina la estructura del contenedor y su contenido
definiendo Dockerfile. Dockerfile sigue una estructura, donde cada línea es de
la INSTRUCTION arguments forma. Hay un conjunto predefinido de instrucciones,
es decir FROM, RUN, CMD, LABEL, ENV, ADD, y COPY. Se puede encontrar una lista
completa
en https://docs.docker.com/engine/reference/builder/#from . Veam
os nuestro Dockerfile definido :

FROM ubuntu:17.10
FROM openjdk:9-b177-jdk
VOLUME /tmp
ADD target/boot_docker-1.0.jar restapp.jar
ENV JAVA_OPTS="-Dspring.profiles.active=cloud"
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar /restapp.jar" ]

Las primeras dos líneas, usando la instrucción FROM, especificaron la imagen base
para nuestra imagen Docker. Usamos la imagen del sistema operativo Ubuntu como
imagen base y luego la combinamos con la imagen OpenJDK 9. La instrucción VOLUME
se utiliza para especificar el punto de montaje para la imagen. Esta suele ser una ruta
en el sistema operativo host.

La instrucción ADD se utiliza para copiar el archivo desde el origen al directorio de


destino en el directorio de trabajo. La instrucción ENV se usa para definir las variables
de entorno.

La instrucción ENTRYPOINT se usa para configurar el contenedor para que se ejecute


como un ejecutable. Para esta instrucción, pasamos una serie de argumentos, que de
otro modo hubiéramos ejecutado directamente desde la línea de comandos. En nuestro
escenario, estamos utilizando el shell bash para ejecutar java -$JAVA_OPTS -jar
<jar name> .

pág. 463
Una vez que hemos definido Dockerfile, instruimos a la herramienta Docker para
construir una imagen usando Dockerfile. También proporcionamos un nombre
para la imagen usando la --tagopción. Al crear la imagen de nuestra aplicación,
descargará las imágenes base requeridas, que, en nuestro caso, son las imágenes de
Ubuntu y OpenJDK. Entonces, si enumera las imágenes de Docker, verá las imágenes
base junto con la imagen de nuestra aplicación.

Esta imagen de Docker es una entidad reutilizable. Si necesitamos más instancias de la


aplicación, generamos un nuevo contenedor utilizando el comando docker
run. Cuando ejecutamos la imagen Docker, tenemos varias opciones, donde una de
ellas es una -popción, que asigna los puertos desde el contenedor al sistema operativo
host. En nuestro caso, asignamos el puerto 8080 de nuestra aplicación Spring Boot
al sistema operativo host 8090.

Ahora, para verificar el estado de nuestra aplicación en ejecución, podemos verificar


los registros usando docker logs restapp. Aparte de esto, la herramienta
Docker admite múltiples comandos. Es muy recomendable ejecutar docker help
y explorar los comandos que son compatibles.

Docker, la compañía detrás de la plataforma de contenedorización de Docker, ha


creado un conjunto de imágenes base, que se pueden utilizar para crear
contenedores. Hay imágenes para MySQL DB, Couchbase, Ubuntu y otros sistemas
operativos. Puede explorar los paquetes en https://store.docker.com/ .

Monitoreo de la aplicación Spring


Boot 2 usando Micrometer y
Prometheus
Monitorear y recopilar métricas de rendimiento es una parte importante del desarrollo
y mantenimiento de aplicaciones. Uno estaría interesado en métricas como el uso de
memoria, el tiempo de respuesta de los diversos puntos finales, el uso de la CPU, la carga
en la máquina, la frecuencia de recolección de basura y las pausas. Hay diferentes
formas de habilitar la captura de métricas, como usar las métricas de Dropwizard
( https://metrics.dropwizard.io/4.0.0/ ) o el marco de métricas de Spring Boot.

La instrumentación del código en Spring Boot 2 en adelante se realiza utilizando una


biblioteca llamada Micrometer ( https://micrometer.io/ ). Micrometer proporciona una
instrumentación de código neutral para el vendedor para que pueda usar cualquier
herramienta de monitoreo y hacer que Micrometer proporcione los datos de las
métricas en el formato comprendido por la herramienta. Esto es como SLF4J para

pág. 464
iniciar sesión. Es una fachada sobre los puntos finales de métrica que produce
resultados de forma neutral para el proveedor.

Micrometer admite herramientas como Prometheus ( https://prometheus.io/ ), Netflix


Atlas ( https://github.com/Netflix/atlas ), Datadog ( https://www.datadoghq.com/ ) y
próxima asistencia para InfluxDB ( https://www.influxdata.com/ ), statsd
( https://github.com/etsy/statsd ) y Graphite ( https://graphiteapp.org/ ). Las aplicaciones
que usan una versión anterior de Spring Boot, como 1.5, también pueden hacer uso de
esta nueva biblioteca de instrumentación, como se muestra en la sección Hay más ...

En esta receta, utilizaremos Micrometer para instrumentar nuestro código y enviar las
métricas a Prometheus. Entonces, primero, comenzaremos configurando Prometheus
en la sección Preparativos .

Prepararse
Prometheus ( https://prometheus.io/ ) es un sistema de monitoreo y una base de datos de
series de tiempo que nos permite almacenar datos de series de tiempo, que incluyen las
métricas de una aplicación a lo largo del tiempo, una manera simple de visualizar las
métricas o la configuración alertas de diferentes métricas.

Realicemos los siguientes pasos para que Prometheus se ejecute en nuestras máquinas
(en nuestro caso, ejecutaremos en Windows. Se aplicarán pasos similares también para
Linux):

1. Descargue la distribución de Prometheus


desde https://github.com/prometheus/prometheus/releases/download/v2.3.2/pr
ometheus-2.3.2.windows-amd64.tar.gz .
2. Extraerlo usando 7-Zip ( https://www.7-zip.org/ ) en Windows a una ubicación que
llamaremos PROMETHEUS_HOME.
3. Agregue %PROMETHEUS_HOME%a sus variables PATH (en Linux,
sería $PROMETHEUS_HOMEa la variable PATH).
4. Ejecute Prometheus con el prometheus --config
"%PROMETHEUS_HOME%/prometheus.yml" comando Verá el siguiente
resultado:

5. Abra http://localhost:9090en su navegador para ver la consola


Prometheus. Ingrese go_gc_duration_seconds en el cuadro de texto vacío y haga

pág. 465
clic en el botón Ejecutar para mostrar las métricas capturadas. Puede cambiar la pestaña a
una versión Graph para visualizar los datos:

Las métricas anteriores son para Prometeo mismo. Puede navegar


para http://localhost:9090/targetsencontrar los objetivos monitoreados
por las Promethues que se muestran a continuación:

Cuando abra el http://localhost:9090/metrics en su navegador, verá el


valor de la métrica en el instante de tiempo actual. Es difícil de entender sin
visualización. Dichas métricas son útiles cuando se recopilan con el tiempo y se
visualizan mediante gráficos.

Ahora, tenemos Prometheus en funcionamiento. Habilitemos la publicación de


micrómetros y métricas en el formato comprendido por Prometheus. Para esto,
reutilizaremos el código utilizado en la receta de Interactuar con la base de datos en este
capítulo. Esta receta está disponible en Chapter09/2_boot_db_demo. Entonces,
simplemente copiaremos el mismo código Chapter09/7_boot_micrometery
luego mejoraremos las partes para agregar soporte para Micrometer
y Prometheus, como se ve en la siguiente sección.

Cómo hacerlo...
pág. 466
1. Actualice pom.xmlpara incluir el actuador de arranque Spring y las dependencias
de registro Micrometer Prometheus:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.0.6</version>
</dependency>

En Spring Boot 2 en adelante, Micrometer viene configurado con actuador, por lo que
solo necesitamos agregar el actuador como la dependencia y luego la dependencia
micrometer-registry-prometheus produce una representación métrica que
Prometheus entiende.

2. Cuando ejecutamos la aplicación (una de las formas es ejecutarla mvn spring-


boot:run) y abrimos el punto final del actuador, por defecto lo
será <root_url>/actuator. Encontraremos que hay pocos puntos finales del
actuador disponibles de forma predeterminada, pero el punto final de las métricas de
Prometheus no forma parte de él:

3. Para habilitar el punto final de Prometheus en el actuador, debemos agregar la


siguiente propiedad en el archivo
src/main/resources/application.properties:
management.endpoints.web.exposure.include = prometheus

4. Reinicie la aplicación y navegue


hasta http://localhost:8080/actuator/. Ahora, verá que solo el punto final de
Prometheus está disponible:

pág. 467
5. Abra http://localhost:8080/actuator/prometheus para ver las
métricas en un formato entendido por Prometheus:

6. Configure Prometheus para


llamar http://localhost:8080/actuator/prometheus a una frecuencia
específica, que se puede configurar. Esto se puede hacer actualizando el
archivo %PROMETHEUS_HOME%/prometheus.yml de configuración con un nuevo
trabajo bajo la propiedad scrape_configs:

- job_name: 'spring_apps'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']

Verá que, de forma predeterminada, hay un trabajo para eliminar las métricas de
Prometheus.

7. Reinicie el servidor Prometheus y


visite http://localhost:9090/targets. Verá una nueva
sección spring_apps, con el objetivo que hemos agregado:

pág. 468
8. Podemos trazar una métrica de las métricas capturadas
visitando http://localhost:9090/graph,
escribiendo jvm_memory_max_bytesen el cuadro de texto y haciendo clic
en Ejecutar para obtener un gráfico:

Entonces, finalmente hemos configurado la ingesta de métricas en Prometheus y


creando gráficos en Prometheus a partir de los valores de las métricas.

Cómo funciona...
Spring Boot proporciona una biblioteca llamada actuador con características para
ayudarlo a monitorear y administrar la aplicación cuando se implementa en
producción. Esta funcionalidad lista para usar no requiere ninguna configuración por
parte de los desarrolladores. Por lo tanto, obtiene auditorías, controles de estado y
recopilación de métricas, todo sin ningún trabajo.

Como se mencionó anteriormente, el actuador utiliza un micrómetro para instrumentar


y capturar diferentes métricas del código, como:

• Uso de memoria JVM


• Información de agrupación de conexiones
• Tiempo de respuesta de diferentes puntos finales HTTP en la aplicación
• Frecuencia de invocación de diferentes puntos finales HTTP

pág. 469
Para permitir que su aplicación tenga estas características listas para producción, debe
agregar la siguiente dependencia pom.xml si está utilizando Maven (hay un
equivalente para Gradle):

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

De manera predeterminada, el actuador está disponible en el /actuator punto


final, pero esto se puede configurar anulando
la management.endpoints.web.base-pathpropiedad en el archivo
src/main/resources/application.properties con un valor diferente,
como se muestra aquí:

management.endpoints.web.base-path=/metrics

Todos los puntos finales disponibles para supervisar y auditar la aplicación están
habilitados de forma predeterminada, excepto el /shutdown punto final, que está
deshabilitado de forma predeterminada. Este punto final se utiliza para cerrar la
aplicación. Estos son algunos de los puntos finales disponibles:

auditevents Expone información de eventos de auditoría para la aplicación


actual

beans Muestra una lista completa de todos los frijoles Spring en su


aplicación

env Expone propiedades de


Spring's ConfigurableEnvironment

health Muestra información sobre el estado de la aplicación.

info Muestra información de aplicación arbitraria

metrics Muestra información de métricas para la aplicación actual

mappings Muestra una lista clasificada de todas


las @RequestMappingrutas

prometheus Expone métricas en un formato que puede ser raspado por un


servidor Prometheus

pág. 470
Puede ver que estos son puntos finales muy sensibles que deben protegerse. Lo bueno
es que el actuador Spring Boot se integra bien con Spring Security para asegurar estos
puntos finales. Entonces, si Spring Security está en el classpath, asegura estos puntos
finales por defecto.

JMX o la web pueden acceder a estos puntos finales. No todos los puntos finales del
actuador están habilitados para el acceso por la web de forma predeterminada, sino que
están habilitados de forma predeterminada para el acceso utilizando JMX. Solo las
siguientes propiedades están habilitadas para acceder de forma predeterminada desde
la web:

• health
• info

Y esta es la razón por la que tuvimos que agregar la siguiente propiedad de


configuración para que el punto final de Prometheus, junto con el estado, la información
y las métricas, estén disponibles en la web:

management.endpoints.web.exposure.include=prometheus,health,info,metrics

Incluso si habilitamos Prometheus, necesitamos tener la biblioteca micrometer-


registry-prometheus en nuestro classpath. Solo así podremos ver las métricas
en el formato Prometheus . Entonces, agregamos la siguiente dependencia a nuestro
pom:

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.0.6</version>
</dependency>

El formato de salida procesado por Prometheus es simple: se


adapta <property_name value> a cada propiedad en una nueva línea. El actuador
Spring Boot no empuja las métricas a Prometheus; en cambio, configuramos
Prometheus para extraer las métricas de una URL dada a una frecuencia definida en su
configuración. La configuración predeterminada de Prometheus, que está disponible en
su directorio de inicio, es la siguiente:

# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds.
Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default
is every 1 minute.
# scrape_timeout is set to the global default (10s).

pág. 471
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093

# Load rules once and periodically evaluate them according to the global
'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:


# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries
scraped from this config.
- job_name: 'prometheus'

# metrics_path defaults to '/metrics'


# scheme defaults to 'http'.

static_configs:
- targets: ['localhost:9090']

Por lo tanto, está configurado con valores predeterminados para intervalos en los que
Prometheus buscará las métricas y para intervalos en los que evaluará las reglas
definidas en rule_files. Scrape es la actividad de extraer las métricas de diferentes
objetivos definidos en la scrape_configsopción, y evaluar es el acto de evaluar
diferentes reglas definidas en rule_files. Para permitir que Prometheus elimine las
métricas de nuestra aplicación Spring Boot, agregamos un nuevo
trabajo scrape_configs al proporcionar el nombre del trabajo, la ruta de las
métricas relativas a la URL de la aplicación y la URL de la aplicación:

- job_name: 'spring_apps'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']

También vimos cómo podemos ver los valores de estas


métricas http://localhost:9090/graphy cómo se pueden visualizar
utilizando el sencillo soporte gráfico proporcionado por Prometheus.

Hay más
Las alertas se pueden habilitar en Prometheus configurando otro servicio, llamado
Alertmanager ( https://prometheus.io/docs/alerting/alertmanager/ ). Este servicio se puede
usar para enviar alertas a correos electrónicos, buscapersonas, etc.

pág. 472
El soporte gráfico en Prometeo es ingenuo. Puede usar Grafana ( https://grafana.com/ ),
que es uno de los principales software de código abierto en el análisis de datos de series
temporales, como el almacenado en Prometheus. De esta forma, puede configurar
Grafana para leer los datos de series temporales de Prometheus y crear paneles con
métricas predefinidas trazadas en diferentes tipos de gráficos.

Redes
En este capítulo, cubriremos las siguientes recetas:

• Hacer una solicitud HTTP GET


• Hacer una solicitud HTTP POST
• Realizar una solicitud HTTP para un recurso protegido
• Hacer una solicitud HTTP asincrónica
• Realizar una solicitud HTTP utilizando Apache HttpClient
• Realizar una solicitud HTTP utilizando la biblioteca de cliente HTTP Unirest

Introducción
El soporte de Java para interactuar con características específicas de HTTP ha sido muy
primitivo. La clase HttpURLConnection , disponible desde JDK 1.1, proporciona API
para interactuar con URL con características específicas de HTTP. Dado que esta API ha
estado allí incluso antes de HTTP / 1.1, carecía de funciones avanzadas y era difícil de
usar. Esta es la razón por la cual los desarrolladores recurrieron principalmente al uso
de bibliotecas de terceros, como Apache HttpClient , Spring framework y HTTP API.

En JDK 9, se introdujo una nueva API de cliente HTTP bajo JEP 110
( http://openjdk.java.net/jeps/110 ) como un módulo de
incubadora (http://openjdk.java.net/jeps/11 ). El mismo módulo de
incubadora se ha promocionado como módulo estándar con el nombre
de java.net.httpJEP 321 ( http://openjdk.java.net/jeps/321 ), que
forma parte de la última versión de JDK 11.

Una nota sobre los módulos de incubadora: un módulo de incubadora contiene API no
finales, que son significativamente más grandes y no lo suficientemente maduras como
para ser incluidas en Java SE. Esta es una forma de lanzamiento beta de la API para que
los desarrolladores puedan usar las API mucho antes. Pero el problema es que no hay
soporte de compatibilidad con versiones anteriores para estas API en las versiones más
recientes de JDK. Esto significa que el código que depende de los módulos de la incubadora
podría romperse con las versiones más nuevas de JDK. Esto podría deberse a que el módulo
de la incubadora se promocionó a Java SE o se dejó caer silenciosamente desde los
módulos de la incubadora.

pág. 473
En este capítulo, cubriremos algunas recetas que muestran cómo usar las API de cliente
HTTP en JDK 11, y luego algunas otras API, que hacen uso del Apache HttpClient
( http://hc.apache.org/httpcomponents-client -ga / ) API y la
biblioteca HTTP Unirest Java ( http://unirest.io/java.html ).

Hacer una solicitud HTTP GET


En esta receta, analizaremos el uso de la API de cliente HTTP JDK 11 para realizar
una solicitud GET a http://httpbin.org/get .

Cómo hacerlo...
1. Cree una instancia java.net.http.HttpClient de uso de su
generador java.net.http.HttpClient.Builder:

HttpClient client = HttpClient.newBuilder().build();

2. Crear una instancia del uso java.net.http.HttpRequest de su


constructor, java.net.http.HttpRequest.Builder. La URL solicitada debe
proporcionarse como una instancia de java.net.URI:

HttpRequest request = HttpRequest


.newBuilder(new URI("http://httpbin.org/get"))
.GET()
.version(HttpClient.Version.HTTP_1_1)
.build();

3. Envíe la solicitud HTTP utilizando la API send


de java.net.http.HttpClient. Esta API toma una
instancia java.net.http.HttpRequest y una implementación
de java.net.http.HttpResponse.BodyHandler:

HttpResponse<String> response = client.send(request,


HttpResponse.BodyHandlers.ofString());

4. Imprima el código java.net.http.HttpResponse de estado y el cuerpo de


respuesta:

System.out.println("Status code: " + response.statusCode());


System.out.println("Response Body: " + response.body());

El código completo para esto se puede encontrar


en Chapter10/1_making_http_get. Puede utilizar los scripts de
ejecución run.bato run.sh, para compilar y ejecutar el código:

pág. 474
Cómo funciona...
Hay dos pasos principales para hacer una llamada HTTP a una URL:

• Crear un cliente HTTP para iniciar la llamada


• Configuración de la URL de destino, las cabeceras HTTP requeridos, y el tipo de
método HTTP, es decir, GET, POST, oPUT

La API de cliente HTTP de Java proporciona una clase de


generador java.net.http.HttpClient.Builder, que se puede utilizar para
crear una instancia java.net.http.HttpClient al mismo tiempo, haciendo uso
de las API de generador para configurar java.net.http.HttpClient. El
siguiente fragmento de código muestra cómo obtener una
instancia java.net.http.HttpClient con la configuración predeterminada:

HttpClient client = HttpClient.newHttpClient();

El siguiente fragmento de código utiliza el generador para configurar y luego crear


una instancia de java.net.http.HttpClient:

HttpClient client = HttpClient


.newBuilder()
//redirect policy for the client. Default is NEVER
.followRedirects(HttpClient.Redirect.ALWAYS)
//HTTP client version. Defabult is HTTP_2
.version(HttpClient.Version.HTTP_1_1)
//few more APIs for more configuration
.build();

Hay más API en el generador, como para configurar la autenticación, el proxy y


proporcionar contexto SSL, que veremos en diferentes recetas.

pág. 475
Configurar la URL de destino no es más que crear una instancia
de java.net.http.HttpRequestuso de su generador y sus API para
configurarlo . El siguiente fragmento de código muestra cómo crear una instancia
de java.net.http.HttpRequest:

HttpRequest request = HttpRequest


.newBuilder()
.uri(new URI("http://httpbin.org/get")
.headers("Header 1", "Value 1", "Header 2", "Value 2")
.timeout(Duration.ofMinutes(5))
.version(HttpClient.Version.HTTP_1_1)
.GET()
.build();

El objeto java.net.http.HttpClient proporciona dos API para realizar una


llamada HTTP:

• Puedes enviar sincrónicamente usando el método HttpClient#send()


• Puedes enviar asincrónicamente usando
el método HttpClient#sendAsync()

El método send()toma dos parámetros: la solicitud HTTP y el controlador para la


respuesta HTTP. El controlador de la respuesta está representado por la
implementación de la interfaz
java.net.http.HttpResponse.BodyHandlers . Hay algunas
implementaciones disponibles, como ofString(), que lee el cuerpo de respuesta
como String, y ofByteArray(), que lee el cuerpo de respuesta como una matriz
de bytes. Usaremos el método ofString(), que devuelve la respuesta Body como
una cadena:

HttpResponse<String> response = client.send(request,


HttpResponse.BodyHandlers.ofString());

La instancia de java.net.http.HttpResponse representa la respuesta del


servidor HTTP. Proporciona API para lo siguiente:

• Obteniendo el cuerpo de respuesta ( body())


• Encabezados HTTP ( headers())
• La solicitud HTTP inicial ( request())
• El código de estado de respuesta ( statusCode())
• La URL utilizada para la solicitud ( uri())

La HttpResponse.BodyHandlersimplementación que se pasa al send()método


ayuda a convertir la respuesta HTTP a un formato compatible, como String o
una byte matriz.

pág. 476
Hacer una solicitud HTTP POST
En esta receta, veremos cómo publicar algunos datos en un servicio HTTP a través del
cuerpo de la solicitud. Publicaremos los datos en
la http://httpbin.org/post URL.

Omitiremos el prefijo del paquete para las clases, como se supone que
es java.net.http.

Cómo hacerlo...
1. Cree una instancia de HttpClient usando su
constructor HttpClient.Builder:

HttpClient client = HttpClient.newBuilder().build();

2. Cree los datos necesarios para pasar al cuerpo de la solicitud:

Map<String, String> requestBody =


Map.of("key1", "value1", "key2", "value2");

3. Cree un objeto HttpRequest con el método de solicitud como POST y


proporcionando los datos del cuerpo de la solicitud como String. Utilizaremos
Jackson's ObjectMapper para convertir el cuerpo de la solicitud Map<String,
String>, en un JSON simple String y luego
utilizaremos HttpRequest.BodyPublishers para procesar el String cuerpo
de la solicitud:

ObjectMapper mapper = new ObjectMapper();


HttpRequest request = HttpRequest
.newBuilder(new URI("http://httpbin.org/post"))
.POST(
HttpRequest.BodyPublishers.ofString(
mapper.writeValueAsString(requestBody)
)
)
.version(HttpClient.Version.HTTP_1_1)
.build();

4. La solicitud se envía y la respuesta se obtiene utilizando el método


send(HttpRequest, HttpRequest.BodyHandlers):

HttpResponse<String> response = client.send(request,


HttpResponse.BodyHandlers.ofString());

pág. 477
5. Luego imprimimos el código de estado de respuesta y el cuerpo de respuesta
enviado por el servidor:

System.out.println("Status code: " + response.statusCode());


System.out.println("Response Body: " + response.body());

El código completo para esto se puede encontrar


en Chapter10/2_making_http_post. Asegúrese de que haya los siguientes JAR
de Jackson en Chapter10/2_making_http_post/mods:

• jackson.databind.jar
• jackson.core.jar
• jackson.annotations.jar

Además, tome nota de la definición del módulo module-info.java, disponible


en Chapter10/2_making_http_post/src/http.client.demo.

Para comprender cómo se utilizan los JAR de Jackson en este código modular, consulte
las recetas de migración de abajo hacia arriba y de arriba hacia abajo en el Capítulo
3 , Programación modular .

Ejecute scripts run.bat y run.sh se proporcionan para facilitar la compilación y


ejecución del código:

pág. 478
Realizar una solicitud HTTP para
un recurso protegido
En esta receta, veremos cómo invocar un recurso HTTP que ha sido protegido por
credenciales de usuario. http://httpbin.org/basic-auth/user/passwd ha sido protegido por
autenticación básica HTTP. La autenticación básica requiere que se proporcione un
nombre de usuario y una contraseña en texto plano, que luego utilizan los recursos
HTTP para decidir si la autenticación del usuario es exitosa.

Si abre http://httpbin.org/basic-auth/user/passwd en el navegador, le pedirá el nombre


de usuario y la contraseña:

Ingrese el nombre de usuario como user y la contraseña como passwd, y se


autenticará para que se muestre una respuesta JSON:

{
"authenticated": true,
"user": "user"
}

Hagamos lo mismo usando la API HttpClient.

Cómo hacerlo...
1. Necesitamos extender java.net.Authenticator y anular su método
getPasswordAuthentication(). Este método debería devolver una
instancia de java.net.PasswordAuthentication. Creemos una

pág. 479
clase UsernamePasswordAuthenticator, que se
extiende java.net.Authenticator:

public class UsernamePasswordAuthenticator


extends Authenticator{
}

2. Crearemos dos variables de instancia en la clase


UsernamePasswordAuthenticator para almacenar el nombre de usuario y la
contraseña, y proporcionaremos un constructor para inicializarlo:

private String username;


private String password;

public UsernamePasswordAuthenticator(){}
public UsernamePasswordAuthenticator ( String username,
String password){
this.username = username;
this.password = password;
}

3. Luego anularemos el método getPasswordAuthentication()para


devolver una instancia java.net.PasswordAuthentication, inicializada con el
nombre de usuario y la contraseña:

@Override
protected PasswordAuthentication getPasswordAuthentication(){
return new PasswordAuthentication(username,
password.toCharArray());
}

4. Luego crearemos una instancia de UsernamePasswordAuthenticator:

String username = "user";


String password = "passwd";
UsernamePasswordAuthenticator authenticator =
new UsernamePasswordAuthenticator(username, password);

5. Proporcionamos la instancia de UsernamePasswordAuthenticator al


inicializar HttpClient:

HttpClient client = HttpClient.newBuilder()


.authenticator(authenticator)
.build();

6. Se HttpRequestcrea un objeto correspondiente para llamar al recurso HTTP


protegido, http://httpbin.org/basic-auth/user/passwd :

HttpRequest request = HttpRequest.newBuilder(new URI(


"http://httpbin.org/basic-auth/user/passwd"
))

pág. 480
.GET()
.version(HttpClient.Version.HTTP_1_1)
.build();

7. Obtenemos el HttpResponse mediante la ejecución de la solicitud, e


imprimimos el código de estado y el cuerpo de la solicitud:

HttpResponse<String> response = client.send(request,


HttpResponse.BodyHandlers.ofString());

System.out.println("Status code: " + response.statusCode());


System.out.println("Response Body: " + response.body());

El código completo para esto está disponible


en Chapter10/3_making_http_request_protected_res. Puede ejecutar el
código utilizando los scripts de ejecución run.bato run.sh:

Cómo funciona...
Las Authenticatorllamadas de red utilizan el objeto para obtener la información
de autenticación. Los desarrolladores generalmente extienden la clase
java.net.Authenticator y anulan su
método getPasswordAuthentication() . El nombre de usuario y la contraseña
se leen desde la entrada del usuario o desde la configuración y son utilizados por la
clase extendida para crear una instancia
de java.net.PasswordAuthentication.

En la receta, creamos una extensión de la java.net.Authenticatorsiguiente


manera:

public class UsernamePasswordAuthenticator


extends Authenticator{
private String username;
private String password;

public UsernamePasswordAuthenticator(){}

public UsernamePasswordAuthenticator ( String username,


String password){
this.username = username;

pág. 481
this.password = password;
}

@Override
protected PasswordAuthentication getPasswordAuthentication(){
return new PasswordAuthentication(username,
password.toCharArray());
}
}

La instancia de UsernamePasswordAuthenticator se proporciona a la API


HttpClient.Builder. La HttpClient instancia utiliza este autenticador para
obtener el nombre de usuario y la contraseña al invocar la solicitud HTTP protegida.

Hacer una solicitud HTTP


asincrónica
En esta receta, veremos cómo hacer una solicitud asincrónica GET. En una solicitud
asincrónica, no esperamos la respuesta; en su lugar, manejamos la respuesta cada vez
que la recibe el cliente. En jQuery, haremos una solicitud asincrónica y
proporcionaremos una devolución de llamada que se encargará de procesar la
respuesta, mientras que en el caso de Java, obtenemos una instancia
de java.util.concurrent.CompletableFuture, y luego invocamos
el método thenApply para procesar la respuesta. Veamos esto en acción.

Cómo hacerlo...
1. Cree una instancia de HttpClientuso de su constructor HttpClient.Builder:

HttpClient client = HttpClient.newBuilder().build();

2. Cree una instancia de HttpRequest usando su constructor


HttpRequest.Builder , que represente la URL y el método HTTP correspondiente
que se utilizará:

HttpRequest request = HttpRequest


.newBuilder(new URI("http://httpbin.org/get"))
.GET()
.version(HttpClient.Version.HTTP_1_1)
.build();

3. Use el método sendAsync para hacer una solicitud HTTP asincrónica y


mantenga una referencia al objeto

pág. 482
CompletableFuture<HttpResponse<String>> que obtuvimos. Usaremos esto
para procesar la respuesta:

CompletableFuture<HttpResponse<String>> responseFuture =
client.sendAsync(request,
HttpResponse.BodyHandlers.ofString());

4. Proporcionamos CompletionStage procesar la respuesta una vez que se


completa la etapa anterior. Para esto, utilizamos el método thenAccept, que toma una
expresión lambda:

CompletableFuture<Void> processedFuture =
responseFuture.thenAccept(response -> {
System.out.println("Status code: " + response.statusCode());
System.out.println("Response Body: " + response.body());
});

5. Espera a que se complete el futuro:

CompletableFuture.allOf(processedFuture).join();

El código completo para esta receta se puede encontrar


en Chapter10/4_async_http_request. Hemos proporcionado
los scripts run.baty run.shpara compilar y ejecutar la receta:

Realizar una solicitud HTTP


utilizando Apache HttpClient
En esta receta, haremos uso de la biblioteca Apache HttpClient
( https://hc.apache.org/httpcomponents-client-4.5.x/index.html ) para hacer
una GETsolicitud HTTP simple . Como estamos usando Java 9, queremos hacer uso de

pág. 483
la ruta del módulo y no de la ruta de clase. Por lo tanto, necesitamos modularizar la
biblioteca Apache HttpClient. Una forma de lograr esto es utilizar el concepto de
módulos automáticos. Veamos cómo configurar las dependencias para la receta.

Prepararse
Todos los JAR requeridos ya están presentes
en Chapter10/5_apache_http_demo/mods:

Una vez que estos JAR están en la ruta del módulo, podemos declarar una dependencia
en estos JAR module-info.java, que está presente
en Chapter10/5_apache_http_demo/src/http.client.demo, como se
muestra en el siguiente fragmento de código:

module http.client.demo{
requires httpclient;
requires httpcore;
requires commons.logging;
requires commons.codec;
}

Cómo hacerlo...
1. Cree una instancia predeterminada de org.http.client.HttpClient usando
su constructor org.apache.http.impl.client.HttpClients :

CloseableHttpClient client = HttpClients.createDefault();

2. Cree una instancia org.apache.http.client.methods.HttpGet junto


con la URL requerida. Esto representa tanto el tipo de método HTTP como la URL
solicitada:

HttpGet request = new HttpGet("http://httpbin.org/get");

pág. 484
3. Ejecute la solicitud HTTP utilizando la HttpClient instancia para obtener una
instancia de CloseableHttpResponse:

CloseableHttpResponse response = client.execute(request);

La instancia CloseableHttpResponse devuelta después de ejecutar la solicitud


HTTP se puede usar para obtener detalles como el código de estado de respuesta y otros
contenidos de la respuesta incorporada dentro de la instancia de una implementación
de HttpEntity.

4. Hacemos uso de EntityUtils.toString() para obtener el cuerpo de


respuesta incrustado dentro de la instancia de una implementación de HttpEntity, e
imprimimos tanto el código de estado como el cuerpo de respuesta:

int statusCode = response.getStatusLine().getStatusCode();


String responseBody =
EntityUtils.toString(response.getEntity());
System.out.println("Status code: " + statusCode);
System.out.println("Response Body: " + responseBody);

El código completo para esta receta se puede encontrar


en Chapter10/5_apache_http_demo. Hemos
proporcionado run.baty run.sh para compilar y ejecutar el código de la receta:

Hay más...
Podemos proporcionar un controlador de respuesta personalizado al invocar el método
HttpClient.execute, de la siguiente manera:

String responseBody = client.execute(request, response -> {


int status = response.getStatusLine().getStatusCode();

pág. 485
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity) : null;
});

En este caso, el manejador de respuestas procesa la respuesta y devuelve la cadena del


cuerpo de la respuesta. El código completo para esto se puede encontrar
en Chapter10/5_1_apache_http_demo_response_handler.

Realizar una solicitud HTTP


utilizando la biblioteca de cliente
HTTP Unirest
En esta receta, haremos uso de la biblioteca Java Unirest HTTP
( http://unirest.io/java.html ) para acceder a los servicios HTTP. Unirest Java es una
biblioteca basada en la biblioteca de cliente HTTP de Apache y proporciona una API
fluida para realizar solicitudes HTTP.

Prepararse
Como la biblioteca Java no es modular, utilizaremos el concepto de módulos
automáticos, como se explica en el Capítulo 3 , Programación modular . Los JAR que
pertenecen a la biblioteca se colocan en la ruta del módulo de la aplicación, y la
aplicación declara una dependencia de los JAR utilizando el nombre del JAR como su
nombre de módulo. De esta manera, un archivo JAR se convierte automáticamente en
un módulo y, por lo tanto, se denomina módulo automático.

La dependencia de Maven para la biblioteca Java es la siguiente:

<dependency>
<groupId>com.mashape.unirest</groupId>
<artifactId>unirest-java</artifactId>
<version>1.4.9</version>
</dependency>

Como no estamos utilizando Maven en nuestras muestras, hemos descargado los JAR
en la carpeta Chapter10/6_unirest_http_demo/mods .

La definición del módulo es la siguiente:

module http.client.demo{
requires httpasyncclient;

pág. 486
requires httpclient;
requires httpmime;
requires json;
requires unirest.java;
requires httpcore;
requires httpcore.nio;
requires commons.logging;
requires commons.codec;
}

Cómo hacerlo...
Unirest proporciona una API muy fluida para realizar solicitudes HTTP. Podemos
hacer una GETsolicitud de la siguiente manera:

HttpResponse<JsonNode> jsonResponse =
Unirest.get("http://httpbin.org/get")
.asJson();

El estado de respuesta y el cuerpo de respuesta se pueden obtener del objeto


jsonResponse, de la siguiente manera:

int statusCode = jsonResponse.getStatus();


JsonNode jsonBody = jsonResponse.getBody();

Podemos hacer una solicitud POST y pasar algunos datos, de la siguiente manera:

jsonResponse = Unirest.post("http://httpbin.org/post")
.field("key1", "val1")
.field("key2", "val2")
.asJson();

Podemos hacer una llamada a un recurso HTTP protegido, de la siguiente manera:

jsonResponse = Unirest.get("http://httpbin.org/basic-auth/user/passwd")
.basicAuth("user", "passwd")
.asJson();

El código para esto se puede encontrar en Chapter10/6_unirest_http_demo.

Hemos proporcionado los scripts run.baty run.shpara ejecutar el código.

Hay más...
La biblioteca Java Unirest proporciona una funcionalidad mucho más avanzada, como
realizar solicitudes asíncronas, cargar archivos y usar proxies. Es recomendable que
pruebe estas diferentes características de la biblioteca.

pág. 487
Gestión de memoria y depuración
En este capítulo, cubriremos las siguientes recetas:

• Comprender el recolector de basura G1


• Registro unificado para JVM
• Usando el jcmdcomando para JVM
• Prueba con recursos para un mejor manejo de recursos
• Apilamiento para mejorar la depuración
• Usando el estilo de codificación de memoria
• Mejores prácticas para un mejor uso de la memoria.
• Comprenda Epsilon, un recolector de basura de bajo costo

Introducción
La gestión de la memoria es el proceso de asignación de memoria para la ejecución del
programa y la reutilización de la memoria después de que parte de la memoria asignada
ya no se utiliza. En Java, este proceso se llama recolección de basura ( GC ). La
efectividad de GC afecta a dos características principales de la aplicación: capacidad de
respuesta y rendimiento.

La capacidad de respuesta se mide por la rapidez con que una aplicación responde a la
solicitud. Por ejemplo, qué tan rápido un sitio web devuelve una página o qué tan rápido
una aplicación de escritorio responde a un evento. Naturalmente, cuanto menor sea el
tiempo de respuesta, mejor será la experiencia del usuario, que es el objetivo para
muchas aplicaciones.

El rendimiento indica la cantidad de trabajo que una aplicación puede hacer en una
unidad de tiempo. Por ejemplo, cuántas solicitudes puede atender una aplicación web
o cuántas transacciones puede admitir una base de datos. Cuanto mayor sea el número,
más valor puede generar potencialmente la aplicación y mayor número de usuarios
puede acomodar.

No todas las aplicaciones deben tener la capacidad de respuesta mínima posible y el


rendimiento máximo alcanzable. Una aplicación puede ser un envío asincrónico de
enviar y hacer algo más, que no requiere mucha interacción del usuario. También puede
haber algunos usuarios potenciales de aplicaciones, por lo que un rendimiento inferior
al promedio podría ser más que suficiente. Sin embargo, hay aplicaciones que tienen
altos requisitos para una o ambas de estas características y no pueden tolerar largas
pausas impuestas por el proceso de GC.

GC, por otro lado, debe detener la ejecución de cualquier aplicación de vez en cuando
para volver a evaluar el uso de la memoria y liberarlo de los datos que ya no se
pág. 488
usan. Dichos períodos de actividad de GC se llaman detener el mundo. Cuanto más
largos sean, más rápido hará su trabajo el GC y más durará la congelación de una
aplicación, lo que eventualmente puede crecer lo suficiente como para afectar tanto la
capacidad de respuesta de la aplicación como el rendimiento. Si ese es el caso, la
optimización de GC y la optimización de JVM se vuelven importantes y requieren una
comprensión de los principios de GC y sus implementaciones modernas.

Lamentablemente, los programadores suelen pasar por alto este paso. Intentando
mejorar la capacidad de respuesta y / o el rendimiento, simplemente agregan memoria
y otras capacidades informáticas, proporcionando así el problema existente
originalmente pequeño con el espacio para crecer. La infraestructura ampliada, además
de los costos de hardware y software, requiere que más personas la mantengan y
eventualmente justifica la construcción de una organización completamente nueva
dedicada a mantener el sistema. Para entonces, el problema alcanza la escala de
volverse prácticamente insoluble y se alimenta de quienes lo crearon al obligarlos a
hacer el trabajo de rutina, casi servil, por el resto de sus vidas profesionales.

En este capítulo, nos centraremos en el recolector de basura Garbage-First ( G1 ), que


es el predeterminado desde Java 9. Sin embargo, también nos referiremos a algunas
otras implementaciones de GC disponibles para contrastar y explicar algunas
decisiones de diseño que tienen trajo G1 a la vida. Además, podrían ser más apropiados
que G1 para algunas de las aplicaciones.

La organización y gestión de la memoria son áreas muy especializadas y complejas de


experiencia en el desarrollo de JVM. Este libro no pretende abordar los detalles de
implementación en ese nivel. Nuestro enfoque se centra en aquellos aspectos de GC que
pueden ayudar a un desarrollador de aplicaciones a ajustarlo a las necesidades de la
aplicación al establecer los parámetros correspondientes del tiempo de ejecución de
JVM.

GC utiliza dos áreas de memoria: el montón y la pila. JVM utiliza el primero para asignar
memoria y almacenar objetos creados por el programa. Cuando se crea un objeto con
la newpalabra clave, se ubica en el montón y la referencia se almacena en la pila. La pila
también almacena variables primitivas y referencias a objetos de almacenamiento
dinámico que utiliza el método o subproceso actual. La pila funciona
en Último en entrar, primero en salir ( LIFO ). La pila es mucho más pequeña que el
montón.

La vista de alto nivel ligeramente simplista, pero lo suficientemente buena para nuestro
propósito, de la actividad principal de cualquier GC es la siguiente: caminar a través de
los objetos en el montón y eliminar aquellos que no tienen referencias en la pila.

pág. 489
Comprender el recolector de
basura G1
Las implementaciones de GC anteriores incluyen el GC serial, el GC paralelo y
el colector concurrente Mark-Sweep ( CMS ). Dividen el montón en tres secciones:
generación joven, generación antigua o con tenencia, y regiones enormes para sostener
los objetos que son 50% del tamaño de una región estándar o más grande. La
generación joven contiene la mayoría de los objetos recién creados; esta es el área más
dinámica porque la mayoría de los objetos son de corta duración y pronto (a medida
que envejecen) se vuelven elegibles para la recolección. El término edad se refiere al
número de ciclos de recolección que el objeto ha sobrevivido. La generación joven tiene
tres ciclos de recolección: un espacio Eden y dos espacios sobrevivientes, como el
sobreviviente 0 (S0) y sobreviviente 1 (S1). Los objetos se mueven a través de ellos
(según su edad y algunas otras características) hasta que finalmente se descartan o se
colocan en la generación anterior.

La generación anterior contiene objetos que tienen más de cierta edad. Esta área es más
grande que la generación joven, y debido a esto, la recolección de basura aquí es más
costosa y ocurre con menos frecuencia que en la generación joven.

La generación permanente contiene metadatos que describen las clases y los métodos
utilizados en las aplicaciones. También almacena cadenas, clases de biblioteca y
métodos.

Cuando JVM comienza, el montón está vacío y luego los objetos son empujados hacia el
Edén. Cuando se está llenando, comienza un proceso menor de GC. Elimina los objetos
referidos circulares y sin referencia y mueve los demás al área S0.

El siguiente proceso de GC menor migra los objetos referenciados a S1 e incrementa la


edad de aquellos que sobrevivieron a la colección menor anterior. Después de que
todos los objetos supervivientes (de diferentes edades) se mueven a S1, tanto S0 como
Eden se vuelven vacíos.

En la siguiente colección menor, S0 y S1 cambian sus roles. Los objetos referenciados se


mueven de Eden a S1 y de S1 a S0.

En cada una de las colecciones menores, los objetos que han alcanzado cierta edad se
trasladan a la generación anterior. Como mencionamos anteriormente, la generación
anterior se verifica eventualmente (después de varias colecciones menores), los objetos
sin referencia se eliminan de allí y la memoria se desfragmenta. Esta limpieza de la vieja
generación se considera una colección importante.

pág. 490
La generación permanente se limpia en diferentes momentos mediante diferentes
algoritmos de GC.

El G1 GC lo hace de manera algo diferente. Divide el montón en regiones del mismo


tamaño y les asigna a cada una una de las mismas funciones: Edén, sobreviviente o
antiguo, pero cambia el número de regiones con la misma función dinámicamente,
según la necesidad. Hace que el proceso de limpieza de la memoria y la
desfragmentación de la memoria sean más predecibles.

Prepararse
El GC en serie limpia las generaciones jóvenes y viejas en el mismo ciclo (en serie, de
ahí el nombre). Durante la tarea, detiene el mundo. Es por eso que se utiliza para
aplicaciones que no son de servidor con una CPU y un tamaño de almacenamiento
dinámico de unos pocos cientos de MB.

El GC paralelo funciona en paralelo en todos los núcleos disponibles, aunque se puede


configurar el número de subprocesos. También detiene el mundo y es apropiado solo
para aplicaciones que pueden tolerar largos tiempos de congelación.

El recopilador de CMS fue diseñado para abordar este problema de pausas largas. Lo
hace a expensas de no desfragmentar la generación anterior y hacer algunos análisis en
paralelo a la ejecución de la aplicación (generalmente usando el 25% de la CPU). La
recopilación de la generación anterior comienza cuando está llena en un 68% (de forma
predeterminada, pero este valor se puede configurar).

El algoritmo G1 GC es similar al colector CMS. Primero, identifica simultáneamente


todos los objetos referenciados en el montón y los ¡marca de manera
correspondiente. Luego recoge primero las regiones más vacías, liberando así mucho
espacio libre. Por eso se llama Garbage-First . Debido a que usa muchas regiones
dedicadas pequeñas, tiene una mejor oportunidad de predecir la cantidad de tiempo
que necesita para limpiar una de ellas y de ajustar un tiempo de pausa definido por el
usuario (G1 puede excederlo ocasionalmente, pero está bastante cerca la mayor parte
del tiempo). veces).

Los principales beneficiarios de G1 son las aplicaciones que requieren grandes


cantidades (6 GB o más) y no toleran pausas largas (0,5 segundos o menos). Si una
aplicación encuentra un problema de pausas demasiadas y / o demasiado largas, puede
beneficiarse al cambiar del CMS o GC paralelo (especialmente el GC paralelo de la
generación anterior) al GC G1. Si ese no es el caso, cambiar al colector G1 no es un
requisito cuando se utiliza JDK 9 o superior.

El G1 GC comienza con la colección de la generación joven que utiliza pausas para


evacuar el mundo (mover objetos dentro de la generación joven y fuera de la generación

pág. 491
anterior). Después de que la ocupación de la generación anterior alcanza un cierto
umbral, también se recoge. La recopilación de algunos de los objetos de la generación
anterior se realiza de forma simultánea y algunos objetos se recopilan mediante pausas
para detener el mundo. Los pasos incluyen lo siguiente:

• El marcado inicial de las regiones sobrevivientes (regiones raíz), que pueden tener
referencias a objetos en la generación anterior, realizado mediante pausas de
detención del mundo
• El escaneo de las regiones sobrevivientes en busca de referencias a la generación
anterior, realizado simultáneamente mientras la aplicación continúa ejecutándose
• El marcado concurrente de objetos vivos en todo el montón, realizado
simultáneamente mientras la aplicación continúa ejecutándose
• El paso de observación completa el marcado de objetos vivos, realizado mediante
pausas para detener el mundo
• El proceso de limpieza calcula la edad de los objetos vivos, libera las regiones
(mediante pausas para detener el mundo) y las devuelve a la lista libre (al mismo
tiempo)

La secuencia anterior podría estar intercalada con evacuaciones de generaciones


jóvenes porque la mayoría de los objetos son de corta duración y es más fácil liberar
mucha memoria escaneando la generación joven con más frecuencia.

También hay una fase mixta cuando G1 recoge las regiones ya marcadas como basura
en su mayoría en las generaciones jóvenes y viejas, y la asignación de grandes
cantidades cuando los objetos grandes son trasladados o evacuados de regiones
gigantescas.

Hay algunas ocasiones en las que se realiza un GC completo, utilizando pausas para
detener el mundo:

• Falla concurrente: esto sucede si la generación anterior se llena durante la fase de


marcado
• Fracaso de la promoción: esto sucede si la generación anterior se queda sin espacio
durante la fase mixta
• Falla de evacuación: esto sucede cuando el recolector no puede promocionar
objetos al espacio del sobreviviente y la generación anterior
• Asignación enorme: esto sucede cuando una aplicación intenta asignar un objeto
muy grande

Si se ajusta correctamente, sus aplicaciones deben evitar el GC completo.

Para ayudar con el ajuste de GC, la documentación de JVM


( https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gc
tuning/ergonomics.html ) describe la ergonomía de la siguiente manera:

pág. 492
"La ergonomía es el proceso mediante el cual la optimización de JVM y recolección de
basura, como la optimización basada en el comportamiento, mejora el rendimiento de la
aplicación. La JVM proporciona selecciones predeterminadas dependientes de la
plataforma para el recolector de basura, el tamaño de almacenamiento dinámico y el
compilador de tiempo de ejecución. Estas selecciones satisfacen las necesidades de
diferentes tipos de aplicaciones a la vez que requieren menos ajustes de la línea de
comandos. Además, el ajuste basado en el comportamiento ajusta dinámicamente los
tamaños del montón para cumplir con un comportamiento específico de la aplicación " .

Cómo hacerlo...
1. Para ver cómo funciona GC, escriba el siguiente programa:

public class Chapter11Memory {


public static void main(String... args) {
int max = 99_888_999;
System.out.println("Chapter11Memory.main() for "
+ max + " is running...");
List<AnObject> list = new ArrayList<>();
IntStream.range(0, max)
.forEach(i -> list.add(new AnObject(i)));
}

private static class AnObject {


private int prop;
AnObject(int i){ this.prop = i; }
}
}

Como puede ver, crea 99,888,999 objetos y los agrega a la colección


List<AnObject> list . Puede ajustarlo disminuyendo el número máximo de
objetos ( max) para que coincida con la configuración de su computadora.

2. El GC G1 es el recopilador predeterminado desde Java 9, por lo que no tiene que


configurar nada si es lo suficientemente bueno para su aplicación. Sin embargo, puede
habilitar explícitamente G1 proporcionando -XX:+UseG1GC en la línea de comando :

java -XX:+UseG1GC -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

Tenga en cuenta que suponemos que puede construir un .jar archivo ejecutable y
comprender el comando básico de ejecución de Java. De lo contrario, consulte la
documentación de JVM.

Se pueden usar otros GC disponibles configurando una de las siguientes opciones:

pág. 493
• -XX:+UseSerialGC para usar un colector en serie.
• -XX:+UseParallelGC para usar un colector paralelo con
compactación paralela (que permite que el colector paralelo realice
colecciones principales en paralelo). Sin compactación paralela, las
principales colecciones se realizan utilizando un solo hilo, lo que puede
limitar significativamente la escalabilidad. La compactación en paralelo está
deshabilitada por la -XX:+UseParallelOldGC opción.
• -XX:+UseConcMarkSweepGC para usar el colector de CMS.

3. Para ver los mensajes de registro de GC, configure -Xlog:gc. También


puede usar la utilidad Unix time, para medir el tiempo que tardó en hacer
el trabajo (la utilidad publica las últimas tres líneas de la salida, por lo que
no necesita usarla si no puede o no quiere hacerlo) :

time java -Xlog:gc -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

4. Ejecute el comando anterior. La salida puede verse de la siguiente manera (los


valores reales pueden ser diferentes en su computadora):

Como puede ver, el GC realizó la mayoría de los pasos que hemos descrito. Ha
comenzado con la recolección de la generación joven. Luego, cuando el objeto
List<AnObject> list (vea el código anterior) se vuelve demasiado grande (más
del 50% de una región de generación joven), la memoria para él se asigna en
la región enorme . También puede ver el paso de la marca inicial, el siguiente
comentario y otros pasos descritos anteriormente.

Cada línea comienza con el tiempo (en segundos) durante el cual se estaba ejecutando
la JVM y termina con el tiempo (en milisegundos) que tomó cada paso. En la parte
inferior de la captura de pantalla, vemos tres líneas impresas por la utilidad time:

pág. 494
• reales la cantidad de tiempo de reloj de pared empleado: todo el tiempo
transcurrido (debe alinearse con la primera columna del valor de tiempo de
actividad de JVM) desde que se ejecutó el comando
• user es la cantidad de tiempo que todas las CPU pasaron en el código de
modo de usuario (fuera del núcleo) dentro del proceso; es más grande
porque GC trabajó simultáneamente con la aplicación
• sys es la cantidad de tiempo que la CPU pasó en el núcleo dentro del
proceso
• user+ sys es la cantidad de tiempo de CPU que utilizó el proceso

5. Configure la -XX:+PrintGCDetailsopción (o simplemente agregue *a la


opción de registro ) para ver más detalles sobre la actividad de GC. En la siguiente
captura de pantalla, proporcionamos solo el comienzo del registro relacionado con
el paso 0 del GC: -Xlog:gc*

Ahora el registro tiene más de una docena de entradas para cada uno de los pasos de
GC y termina con el registro de la User, Sysy Realcantidad de tiempo (las cantidades
acumuladas por la time utilidad) de cada paso tomó. Puede modificar el programa
agregando más objetos de corta duración, por ejemplo, y ver cómo cambia la actividad
del GC.

6. Obtenga aún más información con la opción. Lo siguiente es solo un fragmento de


una salida: -Xlog:gc*=debug

pág. 495
Por lo tanto, depende de usted elegir cuánta información necesita para el análisis.

Discutiremos más detalles sobre el formato de registro y otras opciones de registro en


el registro unificado para la receta JVM .

Cómo funciona...
Como hemos mencionado anteriormente, el GC G1 utiliza valores ergonómicos
predeterminados que probablemente serían lo suficientemente buenos para la mayoría
de las aplicaciones. Aquí está la lista de los más importantes ( <ergo>significa que el
valor real se determina ergonómicamente según el entorno):

• -XX:MaxGCPauseMillis=200: Contiene el valor del tiempo de pausa máximo


• -XX:GCPauseTimeInterval=<ergo>: Mantiene el tiempo de pausa máximo
entre los pasos del GC (no se establece de manera predeterminada, lo que permite
que G1 realice recolecciones de basura de forma consecutiva si es necesario)
• -XX:ParallelGCThreads=<ergo>: Contiene el número máximo de
subprocesos utilizados para el trabajo paralelo durante las pausas de recolección de
basura (por defecto, derivado del número de subprocesos disponibles; si el número
de subprocesos de CPU disponibles para el proceso es menor o igual a ocho, usa este
número; de lo contrario, agrega cinco octavos de los subprocesos mayores que ocho
al número final de subprocesos)
• -XX:ConcGCThreads=<ergo>: Contiene el número máximo de subprocesos
utilizados para el trabajo concurrente (establecido de forma predeterminada
como -XX:ParallelGCThreads dividido entre cuatro).
• -XX:+G1UseAdaptiveIHOP: Indica que la ocupación inicial del montón debe
ser adaptativa
• -XX:InitiatingHeapOccupancyPercent=45: Establece los primeros
ciclos de recolección; G1 utilizará una ocupación del 45% de la generación anterior
como umbral de inicio de marca
• -XX:G1HeapRegionSize=<ergo>: Mantiene el tamaño de la región de
almacenamiento dinámico en función de los tamaños de almacenamiento dinámico

pág. 496
iniciales y máximos (de forma predeterminada, dado que el almacenamiento
dinámico contiene aproximadamente 2.048 regiones de almacenamiento dinámico,
el tamaño de una región de almacenamiento dinámico puede variar de 1 a 32 MB y
debe ser una potencia de 2)
• -XX:G1NewSizePercent=5y -XX:XX:G1MaxNewSizePercent=60:
Definir el tamaño de la generación joven en total, que varía entre estos dos valores
como porcentajes del montón de JVM actual en uso
• -XX:G1HeapWastePercent=5: Mantiene el espacio no reclamado permitido
en los candidatos del conjunto de recopilación como un porcentaje (G1 detiene la
recuperación de espacio si el espacio libre en los candidatos del conjunto de
recopilación es menor que eso)
• -XX:G1MixedGCCountTarget=8: Contiene la duración esperada de la fase de
recuperación de espacio en varias colecciones)
• -XX:G1MixedGCLiveThresholdPercent=85: Contiene el porcentaje de
ocupación de objetos vivos de las regiones de la generación anterior, después de lo
cual no se recolectará una región en esta fase de recuperación de espacio

En general, los objetivos de G1 en la configuración predeterminada son "proporcionar


pausas relativamente pequeñas y uniformes con un alto rendimiento" (de la
documentación de G1). Si esta configuración predeterminada no se ajusta a su
aplicación, puede cambiar el tiempo de pausa (usando ) y el tamaño máximo de
almacenamiento dinámico de Java (usando la opción). Sin embargo, tenga en cuenta
que el tiempo de pausa real no será una coincidencia exacta en el tiempo de ejecución,
pero G1 hará todo lo posible para cumplir el objetivo. -XX:MaxGCPauseMillis-
Xmx

Si desea aumentar el rendimiento, disminuya el objetivo del tiempo de pausa o solicite


un montón más grande. Para aumentar la capacidad de respuesta, cambie el valor del
tiempo de pausa. Sin embargo, tenga en cuenta que la limitación del tamaño de la
generación joven (usando -Xmn, -XX:NewRatiou otras opciones) puede impedir el
control del tiempo de pausa porque "el tamaño de la generación joven es el medio
principal para que G1 le permita cumplir con el tiempo de pausa" (de la documentación
de G1).

Una de las primeras causas posibles de un rendimiento deficiente puede ser el GC


completo provocado por una ocupación de pila demasiado alta en la generación
anterior. Esta situación se puede detectar por la presencia de Pausa completa (error de
asignación) en el registro. Por lo general, ocurre cuando se crean demasiados objetos
en una sucesión rápida (y no se pueden recopilar con la suficiente rapidez) o muchos
objetos grandes (enormes) no se pueden asignar de manera oportuna. Hay varias
formas recomendadas para manejar esta condición:

• En el caso de un número excesivo de objetos enormes, intente reducir su recuento


aumentando el tamaño de la región, utilizando la -

pág. 497
XX:G1HeapRegionSize opción (el tamaño de la región del montón
actualmente seleccionado se imprime al comienzo del registro).
• Aumenta el tamaño del montón.
• Aumente el número de hilos de marcado concurrentes mediante la configuración . -
XX:ConcGCThreads
• Facilite el comienzo del marcado anterior (utilizando el hecho de que G1 toma las
decisiones basadas en el comportamiento anterior de la aplicación). Aumente el
búfer utilizado en un cálculo IHOP adaptativo modificando -
XX:G1ReservePercento deshabilite el cálculo adaptativo del IHOP
configurándolo manualmente con -XX:-G1UseAdaptiveIHOPy -
XX:InitiatingHeapOccupancyPercent.

Solo después de abordar el GC completo se puede comenzar a ajustar la JVM para una
mejor capacidad de respuesta y / o rendimiento. La documentación de JVM identifica
los siguientes casos para el ajuste de la capacidad de respuesta:

• Sistema inusual o uso en tiempo real


• El procesamiento de referencia lleva demasiado tiempo
• Las colecciones solo para jóvenes tardan demasiado
• Las colecciones mixtas tardan demasiado
• Alta actualización de RS y escaneo de tiempos de RS

Se puede lograr un mejor rendimiento al disminuir los tiempos de pausa generales y la


frecuencia de las pausas. Consulte la documentación de JVM para la identificación y las
recomendaciones para mitigar los problemas.

Registro unificado para JVM


Los componentes principales de JVM incluyen los siguientes:

• Cargador de clases
• Memoria JVM donde se almacenan los datos de tiempo de ejecución; se divide en
las siguientes áreas:
• Área de la pila
• Área de método
• Área del montón
• Registros de PC
• Pila de métodos nativos
• Motor de ejecución, que consta de las siguientes partes:
• Interprete
• El compilador JIT
• Recolección de basura
• Interfaz de método nativo JNI

pág. 498
• Biblioteca de métodos nativos

El mensaje de registro de todos estos componentes ahora se puede capturar y analizar


mediante el registro unificado, activado por la opción -Xlog .

Las características principales del nuevo sistema de registro son las siguientes:

• Uso del registro niveles- trace, debug, info, warning,error


• Etiquetas de mensaje que identifican el componente, la acción o el mensaje de
JVM de un interés específico
• Tres tipos-salida stdout, stderryfile
• La aplicación del límite de un mensaje por línea

Prepararse
Para ver todas las posibilidades de registro de un vistazo, puede ejecutar el siguiente
comando:

java -Xlog:help

Aquí está la salida:

Como puede ver, el formato de la -Xlog opción se define de la siguiente manera:

pág. 499
-Xlog[:[what][:[output][:[decorators][:output-options]]]]

Vamos a explicar la opción en detalle:

• whates una combinación de etiquetas y niveles


del formulario tag1[+tag2...][*][=level][,...] . Ya hemos
demostrado cómo funciona esta construcción cuando usamos la etiqueta gc en
la -Xlog:gc*=debug opción. El comodín ( *) indica que le gustaría ver todos
los mensajes que tienen la gcetiqueta (tal vez entre otras etiquetas). La ausencia
del comodín indica que le gustaría ver los mensajes marcados con una etiqueta ( en
este caso) solamente. Si solo se usa, el registro mostrará todos los mensajes en
el nivel. -Xlog:gc=debuggc-Xloginfo
• Los conjuntos output del tipo de salida (el valor predeterminado es stdout).
• El indican decorators lo que se coloca al principio de cada línea del registro
(antes de que el mensaje de registro real proviene de un
componente). Decoradores por defecto son uptime, levely tags, cada uno
incluido en corchetes.
• output_options puede incluir filecount=file count y /
o filesize=file size con sufijo K, M o G opcional.

Para resumir, la configuración de registro predeterminada es la siguiente:

-Xlog:all=info:stdout:uptime,level,tags

Cómo hacerlo...
Ejecutemos algunas de las configuraciones de registro:

1. Ejecute el siguiente comando:

java -Xlog:cpu -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

No hay mensajes porque la JVM no registra mensajes solo con la etiqueta cpu. La
etiqueta se usa en combinación con otras etiquetas.

2. Agregue un * signo y ejecute el comando nuevamente:

java -Xlog:cpu* -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

El resultado será el siguiente:

pág. 500
Como puede ver, la etiqueta cpu solo brinda mensajes sobre cuánto tiempo tardó en
ejecutarse una recolección de basura. Incluso si establecemos el nivel de registro
en trace o debug( -Xlog:cpu*=debug por ejemplo), no se mostrarán otros
mensajes.

3. Ejecute el comando con la etiqueta heap :

java -Xlog:heap* -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

Solo recibirá mensajes relacionados con el montón:

Pero veamos más de cerca la primera línea. Se inicia con tres decoradores -
uptime , log levely tags- y luego con el propio mensaje, que comienza con el
número de ciclos de recogida (0 en este caso) y la información de que el número de
regiones Eden se redujo de 24 de 0 (y su recuento actual es: 9) Sucedió porque (como
vemos en la siguiente línea) el recuento de regiones sobrevivientes aumentó de 0 a 3 y
el recuento de la generación anterior (la tercera línea) creció a 18, mientras que el
recuento de regiones gigantescas (23) no cambió . Estos son todos los mensajes
relacionados con el montón en el primer ciclo de recopilación. Entonces, comienza el
segundo ciclo de recolección.

4. Agregue la etiqueta cpu nuevamente y ejecute:

pág. 501
java -Xlog:heap*,cpu* -cp ./cookbook-1.0.jar
com.packt.cookbook.ch11_memory.Chapter11Memory

Como puede ver, el cpumensaje muestra cuánto tiempo tomó cada ciclo:

5. Intente utilizar dos etiquetas combinadas mediante el + signo ( -


Xlog:gc+heap por ejemplo). Solo muestra los mensajes que tienen ambas etiquetas
(similar a la ANDoperación binaria ). Tenga en cuenta que un comodín no funcionará junto
con el + signo ( -Xlog:gc*+heap por ejemplo, no funciona).

6. También puede seleccionar el tipo de salida y los decoradores. En la práctica, el


nivel de decorador no parece muy informativo y puede omitirse fácilmente
enumerando explícitamente solo los decoradores que se necesitan. Considere el
siguiente ejemplo:

java -Xlog:heap*,cpu*::uptime,tags -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

Observe cómo ::se insertaron los dos puntos ( ) para preservar la configuración
predeterminada del tipo de salida. También podríamos mostrarlo explícitamente:

java -Xlog:heap*,cpu*:stdout:uptime,tags -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

Para eliminar cualquier decoración, se pueden configurar para none:

java -Xlog:heap*,cpu*::none -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

El aspecto más útil de un nuevo sistema de registro es la selección de etiquetas. Permite


un mejor análisis de la evolución de la memoria de cada componente JVM y sus
subsistemas o para encontrar el cuello de botella en el rendimiento, analizando el
tiempo empleado en cada fase de recopilación ; ambos son críticos para la JVM y el
ajuste de la aplicación.

Usando el comando jcmd para la


JVM
pág. 502
Si abre la bincarpeta de la instalación de Java, puede encontrar bastantes utilidades
de línea de comandos que pueden usarse para diagnosticar problemas y monitorear
una aplicación implementada con Java Runtime
Environment ( JRE ). Utilizan diferentes mecanismos para obtener los datos que
informan. Los mecanismos son específicos de la implementación de la máquina
virtual ( VM ), los sistemas operativos y la versión. Por lo general, solo un
subconjunto de herramientas es aplicable a un problema determinado.

En esta receta, nos centraremos en el comando de diagnóstico introducido con Java 9


como una utilidad de línea de comandos, jcmd. I f la carpeta bin está en el camino,
y ou puede invocar escribiendo en la línea de comandos. De lo contrario, debe ir
al directorio o anteponer el en nuestros ejemplos con la ruta completa o relativa (a la
ubicación de su ventana de línea de comando) a la carpeta. jcmdbinjcmdbin

Si lo escribe y no hay ningún proceso de Java ejecutándose actualmente en la máquina,


obtendrá solo una línea, que se ve de la siguiente manera:

87863 jdk.jcmd/sun.tools.jcmd.JCmd

Muestra que actualmente solo se está ejecutando un proceso Java (la jcmd propia
utilidad) y tiene el identificador de proceso ( PID ) de 87863 (que será diferente con
cada ejecución).

Ejecutemos un programa Java, por ejemplo:

java -cp ./cookbook-1.0.jar


com.packt.cookbook.ch11_memory.Chapter11Memory

La salida de jcmd mostrará (con diferentes PID) lo siguiente:

87864 jdk.jcmd/sun.tools.jcmd.JCmd
87785 com.packt.cookbook.ch11_memory.Chapter11Memory

Como puede ver, si se ingresa sin ninguna opción, la utilidad jcmd informa los PID de
todos los procesos Java actualmente en ejecución. Después de obtener el PID, puede
utilizar jcmd para solicitar datos de la JVM que ejecuta el proceso:

jcmd 88749 VM.version

Alternativamente, puede evitar el uso de PID (y llamar jcmd sin parámetros) haciendo
referencia al proceso por la clase principal de la aplicación:

jcmd Chapter11Memory VM.version

Puede leer la documentación de JVM para obtener más detalles sobre la utilidad
jcmd y cómo usarla.

pág. 503
Cómo hacerlo...
jcmd es una utilidad que nos permite emitir comandos para el proceso Java
especificado:

1. Obtenga la lista completa de los comandos jcmd disponibles para un proceso


Java en particular ejecutando la siguiente línea:

jcmd PID/main-class-name help

En lugar de PID/main-class, coloque el identificador de proceso o el nombre de la


clase principal. La lista es específica de JVM, por lo que cada comando de la lista
solicita los datos del proceso específico.

2. En JDK 8, los siguientes comandos jcmd estaban disponibles:


JFR.stop
JFR.start
JFR.dump
JFR.check
VM.native_memory
VM.check_commercial_features
VM.unlock_commercial_features
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
GC.rotate_log
Thread.print
GC.class_stats
GC.class_histogram
GC.heap_dump
GC.run_finalization
GC.run
VM.uptime
VM.flags
VM.system_properties
VM.command_line
VM.version

3. El JDK 9 introdujo los siguientes comandos jcmd (JDK 18.3 y JDK 18.9 no
agregaron nuevos comandos):

• Compiler.queue: Imprime los métodos en cola para la compilación con


C1 o C2 (colas separadas)
• Compiler.codelist: Imprime métodos n (compilados) con firma
completa, rango de direcciones y estado (vivo, no entrante y zombie), y
permite la selección de impresión en stdoutun archivo, XML o impresión
de texto

pág. 504
• Compiler.codecache: Imprime el contenido de la memoria caché de
código, donde el compilador JIT almacena el código nativo generado para
mejorar el rendimiento
• Compiler.directives_add file: Agrega directivas del compilador
de un archivo a la parte superior de la pila de directivas
• Compiler.directives_clear: Borra la pila de directivas del
compilador (deja solo las directivas predeterminadas)
• Compiler.directives_print: Imprime todas las directivas en la pila
de directivas del compilador de arriba a abajo
• Compiler.directives_remove: Elimina la directiva superior de la
pila de directivas del compilador
• GC.heap_info: Imprime los parámetros y el estado del montón actual
• GC.finalizer_info: Muestra el estado del subproceso finalizador,
que recopila objetos con un finalizador (es decir, un método
finalize())
• JFR.configure: Nos permite configurar Java Flight Recorder
• JVMTI.data_dump: Imprime el volcado de datos de la interfaz de la
máquina virtual Java Tool
• JVMTI.agent_load: Carga (adjunta) el agente de Java Virtual Machine
Tool Interface
• ManagementAgent.status: Imprime el estado del agente JMX
remoto
• Thread.print: Imprime todos los hilos con trazas de pila
• VM.log [option]: Nos permite establecer la configuración del registro
JVM (que describimos en la receta anterior) en tiempo de ejecución,
después de que la JVM ha comenzado (la disponibilidad se puede ver
usando VM.log list)
• VM.info: Imprime la información unificada de JVM (versión y
configuración), una lista de todos los subprocesos y su estado (sin volcado
de subprocesos y volcado de montón), resumen de montón, eventos
internos de JVM (GC, JIT, punto seguro, etc.), mapa de memoria con
bibliotecas nativas cargadas, argumentos de VM y variables de entorno, y
detalles del sistema operativo y hardware
• VM.dynlibs: Imprime información sobre bibliotecas dinámicas
• VM.set_flag: Nos permite configurar las marcas JVM
de escritura (también llamadas manejables ) (consulte la documentación
de JVM para obtener una lista de las banderas)
• VM.stringtabley VM.symboltable: Imprime todas las constantes
de cadena UTF-8
• VM.class_hierarchy [full-class-name]: Imprime todas las
clases cargadas o solo una jerarquía de clases especificada
• VM.classloader_stats: Imprime información sobre el cargador de
clases

pág. 505
• VM.print_touched_methods: Imprime todos los métodos que se han
tocado (se han leído al menos) en tiempo de ejecución

Como puede ver, estos nuevos comandos pertenecen a varios grupos, denotados por
el compilador de prefijos, recolector de basura ( GC ), Java Flight
Recorder ( JFR ), Java Virtual Machine Tool Interface ( JVMTI ), Agente de
administración (relacionado con el agente JMX remoto) , hilo y VM . En este libro, no
tenemos suficiente espacio para revisar cada comando en detalle. Solo
demostraremos el uso de unos pocos prácticos.

Cómo funciona...
1. Para obtener ayuda para la utilidad jcmd , ejecute el siguiente comando:
jcmd -h

Aquí está el resultado del comando:

Nos dice que los comandos también se pueden leer desde el archivo especificado
después -f y que hay un comando PerfCounter.print que imprime todos los
contadores de rendimiento (estadísticas) del proceso.

2. Ejecute el siguiente comando:

jcmd Chapter11Memory GC.heap_info

El resultado puede verse como esta captura de pantalla:

pág. 506
Muestra el tamaño total del almacenamiento dinámico y la cantidad utilizada, el
tamaño de una región en la generación joven y cuántas regiones se asignan, y los
parámetros de Metaspacey class space.

3. El siguiente comando es muy útil en caso de que esté buscando hilos desbocados o
le gustaría saber qué más está sucediendo detrás de escena:

jcmd Chapter11Memory Thread.print

Aquí hay un fragmento de la salida posible:

4. Este comando probablemente se usa con mayor frecuencia, ya que produce una
gran cantidad de información sobre el hardware, el proceso JVM en su conjunto y el
estado actual de sus componentes:

jcmd Chapter11Memory VM.info

Comienza con un resumen, como sigue:

La descripción general del proceso sigue:

pág. 507
Luego vienen los detalles del montón (esto es solo un pequeño fragmento de él):

Luego imprime los eventos de compilación, el historial de almacenamiento dinámico


del GC, los eventos de desoptimización, las excepciones internas, los eventos, las
bibliotecas dinámicas, las opciones de registro, las variables de entorno, los argumentos
de VM y muchos parámetros del sistema que ejecuta el proceso.

Los comandos jcmd brindan una visión profunda del proceso JVM, que ayuda a
depurar y ajustar el proceso para obtener el mejor rendimiento y el uso óptimo de los
recursos.

Prueba con recursos para un


mejor manejo de recursos
Administrar los recursos es importante. Cualquier mal manejo (no liberación) de los
recursos (por ejemplo, conexiones de bases de datos y descriptores de archivos
abiertos) puede agotar la capacidad del sistema para operar. Es por eso que, en JDK
7, se introdujo la declaración de prueba con recursos . Lo hemos usado en los ejemplos
del Capítulo 6 , Programación de bases de datos :

try (Connection conn = getDbConnection();


Statement st = createStatement(conn)) {
st.execute(sql);
} catch (Exception ex) {
ex.printStackTrace();
}

Como recordatorio, aquí está el método getDbConnection():

pág. 508
Connection getDbConnection() {
PGPoolingDataSource source = new PGPoolingDataSource();
source.setServerName("localhost");
source.setDatabaseName("cookbook");
try {
return source.getConnection();
} catch(Exception ex) {
ex.printStackTrace();
return null;
}
}

Y aquí está el método createStatement():

Statement createStatement(Connection conn) {


try {
return conn.createStatement();
} catch(Exception ex) {
ex.printStackTrace();
return null;
}
}

Esto fue muy útil, pero en algunos casos, todavía teníamos que escribir código adicional
en el estilo antiguo, por ejemplo, si hay un método execute()que acepta un objeto
Statement como parámetro, y nos gustaría liberarlo (cerrarlo) como Tan pronto
como fue utilizado. En tal caso, el código tendrá el siguiente aspecto:

void execute(Statement st, String sql){


try {
st.execute(sql);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if(st != null) {
try{
st.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

Como puede ver, la mayor parte es solo un código repetitivo de copiar y pegar.

La nueva declaración de prueba con recursos , introducida con Java 9, aborda este caso
mediante una reducción efectiva de las variables finales que se utilizarán como
recursos.

Cómo hacerlo...
pág. 509
1. Vuelva a escribir el ejemplo anterior utilizando la nueva declaración de prueba con
recursos :
void execute(Statement st, String sql){
try (st) {
st.execute(sql);
} catch (Exception ex) {
ex.printStackTrace();
}
}

2. Como puede ver, es mucho más conciso y enfocado, sin la necesidad de escribir
repetidamente código trivial que cierre el recurso. No más finally y
adicional try...catch en él.

2. Si la conexión también se pasa, también se puede poner en el mismo bloque de


prueba y cerrar tan pronto como ya no sea necesaria :

void execute(Connection conn, Statement st, String sql) {


try (conn; st) {
st.execute(sql);
} catch (Exception ex) {
ex.printStackTrace();
}
}

Puede o no adaptarse al manejo de conexión de su aplicación, pero a menudo, esta


capacidad es útil.

3. Pruebe con una combinación diferente, como la siguiente:

Conexión conn = getDbConnection ();


Sentencia st = conn.createStatement ();
try (conn ; st) {
st.execute (sql) ;
} catch (Exception ex) {
ex.printStackTrace () ;
}

Y esta combinación también está permitida:

Conexión conn = getDbConnection ();


try (conn ; Sentencia st = conn.createStatement () ) {
st.execute (sql) ;
} catch (Exception ex) {
ex.printStackTrace () ;
}

La nueva declaración proporciona más flexibilidad para escribir código que se ajuste a
las necesidades sin escribir las líneas que cierran el recurso.

Los únicos requisitos son los siguientes:

pág. 510
• La variable incluida en la declaración try tiene que ser final o
efectivamente final
• El recurso tiene que implementar la interfaz AutoCloseable , que
incluye solo un método:

void close() throws Exception;

Cómo funciona...
Para demostrar cómo funciona la nueva declaración, creemos nuestros propios
recursos que los implementen AutoCloseable y los usen de manera similar a los
recursos de los ejemplos anteriores.

Aquí hay un recurso:

class MyResource1 implements AutoCloseable {


public MyResource1(){
System.out.println("MyResource1 is acquired");
}
public void close() throws Exception {
//Do what has to be done to release this resource
System.out.println("MyResource1 is closed");
}
}

Aquí está el segundo recurso:

class MyResource2 implements AutoCloseable {


public MyResource2(){
System.out.println("MyResource2 is acquired");
}
public void close() throws Exception {
//Do what has to be done to release this resource
System.out.println("MyResource2 is closed");
}
}

Vamos a usarlos en el ejemplo de código:

MyResource1 res1 = new MyResource1();


MyResource2 res2 = new MyResource2();
try (res1; res2) {
System.out.println("res1 and res2 are used");
} catch (Exception ex) {
ex.printStackTrace();
}

Si lo ejecutamos, el resultado será el siguiente:

pág. 511
Tenga en cuenta que el recurso enumerado primero en la trydeclaración se cierra en
último lugar. Hagamos solo un cambio y cambiemos el orden de las referencias en la
declaración try:

MyResource1 res1 = new MyResource1 () ;


MyResource2 res2 = new MyResource2 () ;
try (res2 ; res1) {
Sistema. out .println ( "se utilizan res1 y res2" ) ;
} catch (Exception ex) {
ex.printStackTrace () ;
}

La salida confirma que la secuencia del cierre de referencias también cambia:

Esta regla de cerrar los recursos en el orden inverso aborda el problema más
importante posible de dependencia entre recursos, pero depende del programador
definir la secuencia de cierre de los recursos (enumerándolos en la declaración try en
el orden correcto). Afortunadamente, la JVM maneja el cierre de la mayoría de los
recursos estándar, y el código no se rompe si los recursos se enumeran en un orden
incorrecto. Aún así, es una buena idea enumerarlos en la misma secuencia en que
fueron creados.

Apilamiento para mejorar la


depuración
El seguimiento de la pila puede ser muy útil para descubrir la fuente de un
problema. W uando una corrección automática es posible , surge la necesidad de
leerlo mediante programación.

pág. 512
Desde Java 1.4, se puede acceder al seguimiento de la pila actual a través de
las clases java.lang.Thready java.lang.Throwable. Puede agregar la
siguiente línea a cualquier método de su código:

Thread.currentThread().dumpStack();

También puede agregar la siguiente línea:

new Throwable().printStackTrace();

Imprimirá el seguimiento de la pila a la salida estándar. Alternativamente, desde Java


8, puede usar cualquiera de las siguientes líneas para el mismo efecto:

Arrays.stream(Thread.currentThread().getStackTrace())
.forEach(System.out::println);

Arrays.stream(new Throwable().getStackTrace())
.forEach(System.out::println);

O puede extraer el nombre completo de la clase de llamante, utilizando una de estas


líneas:

System.out.println("This method is called by " + Thread.currentThread()


.getStackTrace()[1].getClassName());

System.out.println("This method is called by " + new Throwable()


.getStackTrace()[0].getClassName());

Todas las soluciones antes mencionadas son posibles debido a la clase


java.lang.StackTraceElement , que representa un marco de pila en una traza
de pila. Esta clase proporciona otros métodos que describen el punto de ejecución
representado por este elemento de seguimiento de pila, que permite el acceso
programático a la información de seguimiento de pila. Por ejemplo, puede ejecutar este
fragmento de código desde cualquier parte de su programa:

Arrays.stream(Thread.currentThread().getStackTrace())
.forEach(e -> {
System.out.println();
System.out.println("e="+e);
System.out.println("e.getFileName()="+ e.getFileName());
System.out.println("e.getMethodName()="+ e.getMethodName());
System.out.println("e.getLineNumber()="+ e.getLineNumber());
});

O puede ejecutar lo siguiente desde cualquier parte del programa:

Arrays.stream(new Throwable().getStackTrace())
.forEach(x -> {
System.out.println();
System.out.println("x="+x);
System.out.println("x.getFileName()="+ x.getFileName());
System.out.println("x.getMethodName()="+ x.getMethodName());

pág. 513
System.out.println("x.getLineNumber()="+ x.getLineNumber());
});

Desafortunadamente, esta gran cantidad de datos tiene un precio. La JVM captura toda
la pila (excepto para marcos de pila ocultos), y - en aquellos casos cuando está
incrustado el análisis programático de la traza de la pila en el flujo principal de la
aplicación - que puede afectar el rendimiento de la aplicación. Mientras tanto, solo
necesita una fracción de estos datos para tomar una decisión.

Aquí es donde resulta útil la nueva clase Java 9 java.lang.StackWalker, con


su Option clase e interfaz StackFrame anidadas .

Prepararse
La StackWalker clase tiene cuatro métodos de fábrica
estáticos sobrecargados getInstance():

• StackWalker getInstance(): Está configurado para omitir todos los


marcos ocultos y no se retiene ninguna referencia de clase de llamante. Los marcos
ocultos contienen información específica de implementación interna de JVM. No
retener la referencia de la clase llamante significa que se llama al método
getCallerClass() en los objetos StackWalker
arrojados UnsupportedOperationException .
• StackWalker getInstance(StackWalker.Option option): Esto
crea una instancia con la opción dada, especificando la información del marco de la
pila a la que puede acceder.
• StackWalker getInstance(Set<StackWalker.Option>
options): Esto crea una instancia con el conjunto de opciones dado,
especificando la información del marco de la pila a la que puede acceder. Si el
conjunto dado está vacío, la instancia se configura exactamente como la instancia
creada por StackWalker getInstance().
• StackWalker getInstance(Set<StackWalker.Option> options,
int estimatedDepth): Esto crea una instancia similar a la anterior y acepta
el parámetro estimatedDepth, lo que nos permite estimar el tamaño del búfer
que podría necesitar.

Los siguientes son los valores de enum StackWalker.Option:

• StackWalker.Option.RETAIN_CLASS_REFERENCE: Configura
la StackWalkerinstancia para admitir el método getCallerClass() y
la StackFrame para admitir el método getDeclaringClass()

pág. 514
• StackWalker.Option.SHOW_HIDDEN_FRAMES: Configura la instancia
StackWalker para mostrar todos los marcos de reflexión y marcos específicos
de implementación
• StackWalker.Option.SHOW_REFLECT_FRAMES: Configura
la instancia StackWalker para mostrar todos los marcos de reflexión

La clase StackWalker también tiene tres métodos:

• T walk(Function<Stream<StackWalker.StackFrame>, T>
function): Esto aplica la función dada a la secuencia de StackFrames para
el hilo actual, atravesando los fotogramas desde la parte superior de la pila. El
marco superior contiene el método que ha llamado a este walk()método.
• void forEach(Consumer<StackWalker.StackFrame> action):
Esto realiza la acción dada en cada elemento de la secuencia StackFrame del
hilo actual, atravesando desde el marco superior de la pila, que es el método que
llama al método forEach. Este método es equivalente a llamar walk(s -> {
s.forEach(action); return null; }).
• Class<?> getCallerClass(): Esto obtiene el objeto Class del llamador
que invocó el método que llamó getCallerClass(). Este método se
lanza UnsupportedOperationException si esta instancia StackWalker
no está configurada con la opción RETAIN_CLASS_REFERENCE

Cómo hacerlo...
Cree varias clases y métodos que se llamarán entre sí, para que pueda realizar el
procesamiento de seguimiento de pila:

1. Crea una clase Clazz01:

public class Clazz01 {


public void method(){
new Clazz03().method("Do something");
new Clazz02().method();
}
}

2. Crea una clase Clazz02:

public class Clazz02 {


public void method(){
new Clazz03().method(null);
}
}

3. Crea una clase Clazz03 :

pág. 515
public class Clazz03 {
public void method(String action){
if(action != null){
System.out.println(action);
return;
}
System.out.println("Throw the exception:");
action.toString();
}
}

4. Escribe un método demo4_StackWalk():

private static void demo4_StackWalk(){


new Clazz01().method();
}

Llame a este método desde el método principal de la clase Chapter11Memory:

public class Chapter11Memory {


public static void main(String... args) {
demo4_StackWalk();
}
}

Si ahora ejecutamos la clase Chapter11Memory, el resultado será el siguiente:

El Do something mensaje se pasa Clazz01 y se imprime


en Clazz03. Luego Clazz02pasa nulo a Clazz03, y el Throw the
exception mensaje se imprime antes del seguimiento de pila causado
por NullPointerException la línea action.toString() .

Cómo funciona...
Para una comprensión más profunda de los conceptos aquí, modifiquemos Clazz03:

public class Clazz03 {


public void method(String action){
if(action != null){
System.out.println(action);

pág. 516
return;
}
System.out.println("Print the stack trace:");
Thread.currentThread().dumpStack();
}
}

El resultado será el siguiente:

Alternativamente, podemos obtener una salida similar usando en Throwable lugar


de Thread:

new Throwable().printStackTrace();

La línea anterior produce esta salida:

Un resultado similar producirá cada una de las siguientes dos líneas:

Arrays.stream(Thread.currentThread().getStackTrace())
.forEach(System.out::println);
Arrays.stream(new Throwable().getStackTrace())
.forEach(System.out::println);

Desde Java 9, se puede lograr el mismo resultado utilizando


la StackWalker clase. Veamos qué sucede si modificamos Clazz03 lo siguiente:

public class Clazz03 {


public void method(String action){
if(action != null){
System.out.println(action);
return;

pág. 517
}
StackWalker stackWalker = StackWalker.getInstance();
stackWalker.forEach(System.out::println);
}
}

El resultado es este:

Contiene toda la información que producen los métodos tradicionales. Sin embargo, a
diferencia de la traza de pila completa generada y almacenada como una matriz en la
memoria, la StackWalker clase solo trae los elementos solicitados. Esto ya es una
gran ventaja. Sin embargo, la mayor ventaja StackWalkeres que, cuando solo
necesitamos el nombre de la clase de la persona que llama, en lugar de obtener toda la
matriz y usar solo un elemento, ahora podemos obtener la información que
necesitamos usando las siguientes dos líneas:

System.out.println("Print the caller class name:");


System.out.println(StackWalker.getInstance(StackWalker
.Option.RETAIN_CLASS_REFERENCE)
.getCallerClass().getSimpleName());

El resultado del fragmento de código anterior es el siguiente:

Usando el estilo de codificación de


memoria
Al escribir código, un programador tiene dos objetivos principales en mente:

• Para implementar la funcionalidad requerida


• Para escribir código que sea fácil de leer y entender

pág. 518
Sin embargo, al hacerlo, también tienen que tomar muchas otras decisiones, una de ellas
es cuál de las clases y métodos de biblioteca estándar con una funcionalidad similar
para usar. En esta receta, lo guiaremos a través de algunas consideraciones que ayudan
a evitar el desperdicio de memoria y hacer que su estilo de código sea consciente de la
memoria:

• Presta atención al objeto creado dentro del bucle


• Use la inicialización diferida y cree un objeto justo antes del uso, especialmente si
hay una buena posibilidad de que esta necesidad nunca se materialice
• No olvides limpiar el caché y eliminar las entradas innecesarias.
• Usar en lugar del operador StringBuilder +
• Use ArrayList si se ajusta a sus necesidades, antes de usar HashSet (el uso de
memoria aumenta de ArrayList a LinkedList, HashTable, HashMap y HashSet, en
esta secuencia)

Cómo hacerlo...
1. Presta atención al objeto creado dentro del bucle.

Esta recomendación es bastante obvia. Crear y descartar muchos objetos en rápida


sucesión puede consumir demasiada memoria antes de que el recolector de basura se
ponga al día con la reutilización del espacio. Considere reutilizar objetos en lugar de
crear uno nuevo cada vez. Aquí hay un ejemplo:

class Calculator {
public double calculate(int i) {
return Math.sqrt(2.0 * i);
}
}

class SomeOtherClass {
void reuseObject() {
Calculator calculator = new Calculator();
for(int i = 0; i < 100; i++ ){
double r = calculator.calculate(i);
//use result r
}
}
}

El código anterior se puede mejorar haciendo que el método calculate() sea


estático. Otra solución sería crear una propiedad estática Calculator calculator = new
Calculator(), de la clase SomeOtherClass . Pero la propiedad estática se inicializa tan
pronto como se carga la clase la primera vez. Si calculator no se utiliza la propiedad,
su inicialización sería una sobrecarga innecesaria. En tales casos, se debe agregar una
inicialización diferida.

pág. 519
2. Utilice la inicialización diferida y cree un objeto justo antes del uso, especialmente
si hay una buena posibilidad de que esta necesidad nunca se materialice para
algunas solicitudes.

En el paso anterior, hablamos sobre la inicialización diferida de


la propiedad calculator:

class Calculator {
public double calculate(int i) {
return Math.sqrt(2.0 * i);
}
}

class SomeOtherClass {
private static Calculator calculator;
private static Calculator getCalculator(){
if(this.calculator == null){
this.calculator = new Calculator();
}
return this.calculator;
}
void reuseObject() {
for(int i = 0; i < 100; i++ ){
double r = getCalculator().calculate(i);
//use result r
}
}
}

En el ejemplo anterior, el Calculatorobjeto es un singleton : una vez creado, solo existe


una instancia de este en la aplicación. Si sabemos que la propiedad de la calculadora
siempre se usará, entonces no hay necesidad de una inicialización diferida. En Java,
podemos aprovechar la inicialización de la propiedad estática la primera vez que
cualquiera de los hilos de la aplicación carga la clase:

class SomeOtherClass {
private static Calculator calculator = new Calculator();
void reuseObject() {
for(int i = 0; i < 100; i++ ){
double r = calculator.calculate(i);
//use result r
}
}
}

Pero si hay una buena posibilidad de que el objeto inicializado nunca se use, volvemos
a la inicialización diferida que se puede implementar como se discutió anteriormente
(usando el método getCalculator()) en un solo hilo o cuando el objeto compartido no
tiene estado y su inicialización sí lo hace. No consume muchos recursos.

En el caso de una aplicación de subprocesos múltiples y una inicialización de objetos


complejos con un consumo sustancial de recursos, se deben tomar algunas medidas

pág. 520
adicionales para evitar el conflicto de un acceso concurrente y asegurarse de que solo
se cree una instancia. Por ejemplo, considere la siguiente clase:

class ExpensiveInitClass {
private Object data;
public ExpensiveInitClass() {
//code that consumes resources
//and assignes value to this.data
}

public Object getData(){


return this.data;
}
}

Si el constructor anterior requiere un tiempo extenso para completar la creación del


objeto, existe la posibilidad de que el segundo hilo entre en el constructor antes de
que el primer hilo haya completado la creación del objeto. Para evitar esta creación
concurrente del segundo objeto, necesitamos sincronizar el proceso de inicialización:

class LazyInitExample {
public ExpensiveInitClass expensiveInitClass
public Object getData(){ //can synchrnonize here
if(this.expensiveInitClass == null){
synchronized (LazyInitExample.class) {
if (this.expensiveInitClass == null) {
this.expensiveInitClass = new ExpensiveInitClass();
}
}
}
return expensiveInitClass.getData();
}
}

Como puede ver, podríamos sincronizar el acceso al método getData(), pero esta
sincronización no es necesaria después de crear el objeto y puede causar un cuello de
botella en un entorno multiproceso altamente concurrente. Del mismo modo,
podríamos tener solo una comprobación de nulo , dentro del bloque sincronizado , pero
esta sincronización no es necesaria después de que se inicialice el objeto, por lo que la
rodeamos con otra comprobación de nulo para disminuir la posibilidad de que se
produzca un cuello de botella.

3. No olvides limpiar el caché y eliminar las entradas innecesarias.

El almacenamiento en caché ayuda a disminuir el tiempo de acceso a los datos. Pero el


caché consume memoria, por lo que tiene sentido mantenerlo lo más pequeño posible,
sin dejar de ser útil. Cómo hacerlo depende en gran medida del patrón de uso de datos
en caché. Por ejemplo, si sabe que, una vez utilizado, el objeto almacenado en la
memoria caché no se volverá a utilizar, puede colocarlo en la memoria caché en el
momento de inicio de la aplicación (o periódicamente, de acuerdo con el patrón de uso)
y eliminarlo de la caché después de que se usó:

pág. 521
static HashMap<String, Object> cache = new HashMap<>();
static {
//populate the cache here
}
public Object getSomeData(String someKey) {
Object obj = cache.get(someKey);
cache.remove(someKey);
return obj;
}

Alternativamente, si espera un alto nivel de reutilización para cada objeto, puede


ponerlo en la memoria caché después de que se solicitó por primera vez:

static HashMap<String, Object> cache = new HashMap<>();


public Object getSomeData(String someKey) {
Object obj = cache.get(someKey);
if(obj == null){
obj = getDataFromSomeSource();
cache.put(someKey, obj);
}
return obj;
}

El caso anterior puede conducir a un crecimiento incontrolable de la memoria caché


que consume demasiada memoria y eventualmente causa la condición
OutOfMemoryError. Para evitarlo, puede implementar un algoritmo que mantenga
limitado el tamaño de la caché : después de un cierto tamaño, cada vez que se agrega
un nuevo objeto, se elimina algún otro objeto (que se usa más, por ejemplo, o se usa
menos). El siguiente es un ejemplo de limitar el tamaño de caché a 10 eliminando el
objeto en caché más utilizado:

static HashMap<String, Object> cache = new HashMap<>();


static HashMap<String, Integer> count = new HashMap<>();
public static Object getSomeData(String someKey) {
Object obj = cache.get(someKey);
if(obj == null){
obj = getDataFromSomeSource();
cache.put(someKey, obj);
count.put(someKey, 1);
if(cache.size() > 10){
Map.Entry<String, Integer> max =
count.entrySet().stream()
.max(Map.Entry.comparingByValue(Integer::compareTo))
.get();
cache.remove(max.getKey());
count.remove(max.getKey());
}
} else {
count.put(someKey, count.get(someKey) + 1);
}
return obj;
}

Alternativamente, uno puede usar la clase java.util.WeakHashMap para implementar el


caché:

pág. 522
private static WeakHashMap<Integer, Double> cache
= new WeakHashMap<>();
void weakHashMap() {
int last = 0;
int cacheSize = 0;
for(int i = 0; i < 100_000_000; i++) {
cache.put(i, Double.valueOf(i));
cacheSize = cache.size();
if(cacheSize < last){
System.out.println("Used memory=" +
usedMemoryMB()+" MB, cache=" + cacheSize);
}
last = cacheSize;
}
}

Si ejecuta el ejemplo anterior, verá que el uso de memoria y el tamaño de la memoria


caché aumentan primero, luego se despliegan, luego aumentan y vuelven a
desplegarse. Aquí hay un extracto de una salida:

Used memory=1895 MB, cache=2100931


Used memory=189 MB, cache=95658
Used memory=296 MB, cache=271
Used memory=408 MB, cache=153
Used memory=519 MB, cache=350
Used memory=631 MB, cache=129
Used memory=745 MB, cache=2079710
Used memory=750 MB, cache=69590
Used memory=858 MB, cache=213

El cálculo del uso de memoria que utilizamos fue el siguiente:

long usedMemoryMB() {
return Math.round(
Double.valueOf(Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory())/1024/1024
);
}

La clase es una implementación de Mapa con claves del tipo. Los objetos a los que se
hace referencia solo por referencias débiles se recogen en la basura cada vez que el
recolector de basura decide que se necesita más memoria. Esto significa que
una entrada n en un objeto se eliminará cuando no haya referencia a esa
clave. Cuando la recolección de basura elimina la clave de la memoria, el valor
correspondiente también se elimina del
mapa. java.util.WeakHashMapjava.lang.ref.WeakReferenceWeakHashMap

En nuestro ejemplo anterior, ninguna de las claves de caché se usó fuera del mapa, por
lo que el recolector de basura las eliminó a su discreción. El código se comporta de la
misma manera incluso cuando agregamos una referencia explícita a una clave fuera del
mapa:

pág. 523
private static WeakHashMap<Integer, Double> cache
= new WeakHashMap<>();
void weakHashMap() {
int last = 0;
int cacheSize = 0;
for(int i = 0; i < 100_000_000; i++) {
Integer iObj = i;
cache.put(iObj, Double.valueOf(i));
cacheSize = cache.size();
if(cacheSize < last){
System.out.println("Used memory=" +
usedMemoryMB()+" MB, cache=" + cacheSize);
}
last = cacheSize;
}
}

Esto se debe a que la referencia iObj que se muestra en el bloque de código anterior
se abandona después de cada iteración y se recopila, por lo que la clave correspondiente
en el caché se deja sin referencia externa, y el recolector de basura también la
elimina. Para probar este punto, modifiquemos nuevamente el código anterior:

void weakHashMap() {
int last = 0;
int cacheSize = 0;
List<Integer> list = new ArrayList<>();
for(int i = 0; i < 100_000_000; i++) {
Integer iObj = i;
cache.put(iObj, Double.valueOf(i));
list.add(iObj);
cacheSize = cache.size();
if(cacheSize < last){
System.out.println("Used memory=" +
usedMemoryMB()+" MB, cache=" + cacheSize);
}
last = cacheSize;
}
}

Hemos creado una lista y le hemos agregado cada una de las claves del mapa. Si
ejecutamos el código anterior, eventualmente obtendremos OutOfMemoryError porque
las claves de la caché tenían referencias fuertes fuera del mapa. También podemos
debilitar las referencias externas:

private static WeakHashMap<Integer, Double> cache


= new WeakHashMap<>();
void weakHashMap() {
int last = 0;
int cacheSize = 0;
List<WeakReference<Integer>> list = new ArrayList<>();
for(int i = 0; i < 100_000_000; i++) {
Integer iObj = i;
cache.put(iObj, Double.valueOf(i));
list.add(new WeakReference(iObj));
cacheSize = cache.size();
if(cacheSize < last){

pág. 524
System.out.println("Used memory=" +
usedMemoryMB()+" MB, cache=" + cacheSize +
", list size=" + list.size());
}
last = cacheSize;
}
}

El código anterior ahora se ejecuta como si las claves de caché no tuvieran referencias
externas. La memoria usada y el tamaño de caché crecen y vuelven a caer. Pero el
tamaño de la lista no se despliega, porque el recolector de basura no elimina valores de
la lista. Entonces, eventualmente, la aplicación puede quedarse sin memoria.

Sin embargo, ya sea que limite el tamaño del caché o lo deje crecer sin control, puede
haber una situación en la que la aplicación necesite tanta memoria como sea
posible. Entonces, si hay objetos grandes que no son críticos para la funcionalidad
principal de la aplicación, a veces tiene sentido eliminarlos de la memoria para que la
aplicación sobreviva y no entre en la condición OutOfMemoryError.

Si hay un caché, generalmente es un buen candidato para eliminar y liberar la


memoria, por lo que podemos envolver el caché con la clase WeakReference:

private static WeakReference<Map<Integer, Double[]>> cache;


void weakReference() {
Map<Integer, Double[]> map = new HashMap<>();
cache = new WeakReference<>(map);
map = null;
int cacheSize = 0;
List<Double[]> list = new ArrayList<>();
for(int i = 0; i < 10_000_000; i++) {
Double[] d = new Double[1024];
list.add(d);
if (cache.get() != null) {
cache.get().put(i, d);
cacheSize = cache.get().size();
System.out.println("Cache="+cacheSize +
", used memory=" + usedMemoryMB()+" MB");
} else {
System.out.println(i +": cache.get()=="+cache.get());
break;
}
}
}

En el código anterior , hemos envuelto el mapa (caché) dentro de la clase WeakReference,


lo que significa que le decimos a la JVM que puede recopilar este objeto tan pronto como
no haya referencia a él. Luego, en cada iteración del ciclo for, creamos un objeto new
Double[1024] y lo guardamos en la lista. Lo hacemos para utilizar toda la memoria
disponible más rápido. Luego ponemos el mismo objeto en el caché. Cuando ejecutamos
este código, rápidamente termina con el siguiente resultado:

Cache=4582, used memory=25 MB


4582: cache.get()==null

pág. 525
Esto significa que el recolector de basura decidió recolectar el objeto de caché
después de usar 25 MB de memoria. Si cree que este enfoque es demasiado agresivo y
no necesita renovar el caché con frecuencia, puede envolverlo en
la clase java.lang.ref.SoftReference. Si lo hace, la memoria caché se recopilará solo
cuando se agote toda la memoria , justo al borde del
lanzamiento OutOfMemoryError. Aquí está el fragmento de código que lo demuestra:

private static SoftReference<Map<Integer, Double[]>> cache;


void weakReference() {
Map<Integer, Double[]> map = new HashMap<>();
cache = new SoftReference<>(map);
map = null;
int cacheSize = 0;
List<Double[]> list = new ArrayList<>();
for(int i = 0; i < 10_000_000; i++) {
Double[] d = new Double[1024];
list.add(d);
if (cache.get() != null) {
cache.get().put(i, d);
cacheSize = cache.get().size();
System.out.println("Cache="+cacheSize +
", used memory=" + usedMemoryMB()+" MB");
} else {
System.out.println(i +": cache.get()=="+cache.get());
break;
}
}
}

Si lo ejecutamos, el resultado será el siguiente:

Cache=1004737, used memory=4096 MB


1004737: cache.get()==null

Así es, en nuestra computadora de prueba, hay 4 GB de RAM, por lo que el caché se
eliminó solo cuando se usó casi todo.

3. Usar en lugar del operador. StringBuilder +

Puede encontrar muchas de esas recomendaciones en Internet. También hay bastantes


declaraciones que dicen que esta recomendación es obsoleta porque Java moderno
usa StringBuilder para implementar el + operador para cadenas. Aquí está el
resultado de nuestra experimentación. Primero, hemos ejecutado el siguiente código:

long um = usedMemoryMB();
String s = "";
for(int i = 1000; i < 10_1000; i++ ){
s += Integer.toString(i);
s += " ";
}
System.out.println("Used memory: "
+ (usedMemoryMB() - um) + " MB"); //prints: 71 MB

pág. 526
La implementación de usedMemoryMB() :

long usedMemoryMB() {
return Math.round(
Double.valueOf(Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory())/1024/1024
);
}

Luego usamos StringBuilder para el mismo propósito:

long um = usedMemoryMB();
StringBuilder sb = new StringBuilder();
for(int i = 1000; i < 10_1000; i++ ){
sb.append(Integer.toString(i)).append(" ");
}
System.out.println("Used memory: "
+ (usedMemoryMB() - um) + " MB"); //prints: 1 MB

Como puede ver, el uso del + operador consumió 71 MB de memoria, mientras


que StringBuilder solo usó 1 MB para la misma tarea. También lo hemos
probado StringBuffer. Consumió 1 MB también, pero se desempeñó un poco más lento
que StringBuilder, porque es seguro para subprocesos, mientras que
solo StringBuilder se puede usar en un entorno de un solo subproceso.

Todo esto no se aplica al valor String largo que se dividió en varias subcadenas con el
signo más para una mejor legibilidad. El compilador recopila la subcadena de nuevo en
un valor largo. Por ejemplo, las cadenas s1y s2ocupan la misma cantidad de memoria:

String s1 = "this " +


"string " +
"takes " +
"as much memory as another one";
String s2 = "this string takes as much memory as another one";

4. Si necesita usar una colección, seleccione ArrayList si se ajusta a sus necesidades. El


uso de memoria aumenta de ArrayList a LinkedList, HashTable, HashMap y HashSet,
en esta secuencia.

El ArrayList objeto almacena sus elementos en una matriz Object[] y utiliza


un intcampo para rastrear el tamaño de la lista (además de array.length). Debido a
este diseño, no se recomienda asignar una ArrayList de gran capacidad al declararla, si
existe la posibilidad de que esta capacidad no se utilice por completo. A medida que se
agregan nuevos elementos a la lista, la capacidad de la matriz de back-end se
incrementa en bloques de 10 elementos, que es una posible fuente de memoria
desperdiciada. Si es significativo para la aplicación, es posible reducir
la ArrayListcapacidad a la utilizada actualmente al invocar el método
trimToSize(). Tenga en cuenta que los métodos clear() y remove()no afectan
la ArrayListcapacidad, solo cambian su tamaño.

pág. 527
Otras colecciones tienen más gastos generales porque brindan más servicio. Los
elementos LinkedList llevan referencias a los elementos anteriores y siguientes, así
como una referencia al valor de los datos. La mayoría de las implementaciones de
colecciones basadas en hash se centran en un mejor rendimiento, que a menudo se
produce a expensas de la huella de memoria.

La elección de la clase de colección Java puede ser irrelevante si su tamaño va a ser


pequeño. Sin embargo, los programadores usualmente usan el mismo patrón de
codificación, y uno puede identificar al autor del código por su estilo. Es por eso que, a
la larga, vale la pena descubrir las construcciones más eficientes y usarlas
rutinariamente. Sin embargo, trate de evitar que su código sea difícil de entender; La
legibilidad es un aspecto importante de la calidad del código.

Mejores prácticas para un mejor


uso de la memoria.
Es posible que la administración de memoria nunca se convierta en un problema para
usted, puede ser su momento de vigilia, o puede encontrarse entre estas dos
polaridades. La mayoría de las veces, no es un problema para la mayoría de los
programadores, especialmente con la mejora constante de los algoritmos de
recolección de basura. El recolector de basura G1 (predeterminado en JVM 9) es
definitivamente un paso en la dirección correcta. Pero también existe la posibilidad de
que lo llamen (o se den cuenta de sí mismo) sobre el rendimiento degradante de la
aplicación, y ahí es cuando aprenderá qué tan bien está equipado para enfrentar el
desafío.

Esta receta es un intento de ayudarlo a evitar tal situación o salir de ella con éxito.

Cómo hacerlo...
La primera línea de defensa es el código en sí. En las recetas anteriores, discutimos la
necesidad de liberar recursos tan pronto como ya no sean necesarios y el uso
de StackWalker consumir menos memoria. Hay muchas recomendaciones en
Internet, pero es posible que no se apliquen a su aplicación. Tendrá que controlar el
consumo de memoria y probar sus decisiones de diseño, especialmente si su código
maneja una gran cantidad de datos, antes de decidir dónde concentrar su atención.

Pruebe y perfile su código tan pronto como comience a hacer lo que se suponía que
debía hacer. Es posible que deba cambiar su diseño o algunos detalles de
implementación. También informará sus decisiones futuras. Hay muchos perfiladores

pág. 528
y herramientas de diagnóstico disponibles para cualquier entorno. Describimos uno de
ellos, jcmden el uso del comando jcmd para la receta JVM .

Aprenda cómo funciona su recolector de basura (consulte la descripción de la receta


del recolector de basura G1 ) y no olvide usar el registro JVM (descrito en
la receta Registro unificado para JVM ).

Después de eso, es posible que deba ajustar la JVM y el recolector de basura. Aquí hay
algunos javaparámetros de línea de comandos de uso frecuente (el tamaño se
especifica en bytes de manera predeterminada, pero puede agregar la letra k o K para
indicar kilobytes, m o M para indicar megabytes, g o G para indicar gigabytes) :

• -Xms size: Esta opción nos permite establecer el tamaño de almacenamiento


dinámico inicial (debe ser mayor que 1 MB y un múltiplo de 1,024).
• -Xmx size: Esta opción nos permite establecer el tamaño máximo de
almacenamiento dinámico (debe ser mayor que 2 MB y un múltiplo de 1,024).
• -Xmn size o una combinación de y : esta opción nos permite establecer el
tamaño inicial y máximo de la generación joven. Para GC eficiente, tiene que ser
inferior a . Oracle recomienda establecerlo en más del 25% y menos del 50% del
tamaño de almacenamiento dinámico. -XX:NewSize=size-
XX:MaxNewSize=size-Xmx size
• -XX:NewRatio=ratio: Esta opción nos permite establecer la relación entre las
generaciones jóvenes y viejas (dos, por defecto).
• -Xss size: Esta opción nos permite establecer el tamaño de la pila de hilos. Los
siguientes son los valores predeterminados para diferentes plataformas:
• Linux / ARM (32 bits): 320 KB
• Linux / ARM (64 bits): 1,024 KB
• Linux / x64 (64 bits): 1,024 KB
• macOS (64 bits): 1,024 KB
• Oracle Solaris / i386 (32 bits): 320 KB
• Oracle Solaris / x64 (64 bits): 1,024 KB
• Windows: depende de la memoria virtual

• -XX:MaxMetaspaceSize=size: Esta opción nos permite establecer el límite


superior del área de metadatos de la clase (sin límite, por defecto).

El signo revelador de una pérdida de memoria es el crecimiento de la generación


anterior que hace que el GC completo se ejecute con más frecuencia. Para investigar,
puede usar los parámetros de JVM que vuelcan la memoria de almacenamiento
dinámico en un archivo:

• -XX:+HeapDumpOnOutOfMemoryError: Nos permite guardar el contenido


del montón JVM en un archivo, pero solo cuando
se java.lang.OutOfMemoryError produce una excepción. De forma
predeterminada, el volcado de almacenamiento dinámico se guarda en el directorio
actual con el nombre donde está la ID del proceso . Use la opción
pág. 529
para personalizar la ubicación del archivo de volcado. El valor debe incluir el
nombre del archivo. java_pid<pid>.hprof<pid>-
XX:HeapDumpPath=<path><path>
• -XX:OnOutOfMemoryError="<cmd args>;<cmd args>": Nos permite
proporcionar un conjunto de comandos (separados por punto y coma) que se
ejecutarán cuando se produzca una OutOfMemoryErrorexcepción.
• -XX:+UseGCOverheadLimit: Regula el tamaño de la proporción de tiempo que
tarda GC antes de que se produzca una excepción OutOfMemoryError. Por
ejemplo, el GC paralelo arrojará una OutOfMemoryError excepción cuando el
GC tome más del 98% del tiempo y recupere menos del 2% del montón. Esta opción
es particularmente útil cuando el montón es pequeño porque evita que JVM se ejecute
con poco o ningún progreso. Está activado por defecto. Para deshabilitarlo, use -
XX:-UseGCOverheadLimit.

Entendiendo Epsilon, un
recolector de basura de bajo costo
Una de las preguntas populares de la entrevista Java es: ¿puede hacer cumplir la
recolección de basura? La gestión de memoria en tiempo de ejecución de Java
permanece fuera del control de un programador y, a veces, actúa como un Joker
impredecible , interrumpe la aplicación que de otro modo funcionaría bien e inicia una
exploración de memoria completa que detiene el mundo. Suele ocurrir en el peor
momento posible . Es especialmente molesto cuando intenta medir el rendimiento de su
aplicación bajo carga usando una ejecución corta y luego se da cuenta de que se dedicó
mucho tiempo y recursos al proceso de recolección de basura y que el patrón de la
recolección de basura, después de cambiar el código , se volvió diferente que antes del
cambio de código.

En este capítulo, describimos algunos trucos y soluciones de programación que ayudan


a aliviar la presión sobre el recolector de basura. Sin embargo, sigue siendo un
contribuyente (o detractor) independiente e impredecible del rendimiento de la
aplicación. ¿No sería bueno si el recolector de basura estuviera mejor controlado, al
menos con fines de prueba, o pudiera apagarse? En Java 11 , se introdujo un recolector
de basura, Epsilon, llamado recolector de basura no operativo.

A primera vista, parece extraño : un recolector de basura que no recolecta nada. Pero
es predecible (eso es seguro) porque no hace nada, y esa característica nos permite
probar algoritmos en tiradas cortas sin preocuparnos por pausas
impredecibles. Además, hay una categoría completa de pequeñas aplicaciones de corta
duración que necesitan todos los recursos que puedan reunir durante un breve período
de tiempo y es preferible reiniciar la JVM y dejar que el equilibrador de carga realice la
conmutación por error que intentar tener en cuenta un Joker impredecible del proceso
de recolección de basura.
pág. 530
También fue concebido como un proceso de referencia que nos permite estimar los
gastos generales de un recolector de basura regular.

Cómo hacerlo...
Para invocar al recolector de basura no operativo, use la opción -
XX:+UseEpsilonGC Al momento de escribir, se requiere una opción -
XX:+UnlockExperimentalVMOptions para acceder a la nueva capacidad.

Utilizaremos el siguiente programa para la demostración:

package com.packt.cookbook.ch11_memory;
import java.util.ArrayList;
import java.util.List;
public class Epsilon {
public static void main(String... args) {
List<byte[]> list = new ArrayList<>();
int n = 4 * 1024 * 1024;
for(int i=0; i < n; i++){
list.add(new byte[1024]);
byte[] arr = new byte[1024];
}
}
}

Como puede ver, en este programa, estamos tratando de asignar 4 GB de memoria


agregando una matriz de 1 KB a la lista en cada iteración. Al mismo tiempo, también
creamos una matriz de 1 K arr, en cada iteración, pero no utilizamos la referencia, por
lo que el recolector de basura tradicional puede recolectarla.

Primero, ejecutaremos el programa anterior con el recolector de basura


predeterminado:

time java -cp cookbook-1.0.jar -Xms4G -Xmx4G -Xlog:gc


com.packt.cookbook.ch11_memory.Epsilon

Tenga en cuenta que hemos limitado la memoria de almacenamiento dinámico JVM a


4 GB porque, con fines demostrativos, nos gustaría que salga el
programa OutOfMemoryError. Y hemos terminado la llamada con el
comando time para capturar tres valores:

• Tiempo real : cuánto tiempo estuvo ejecutándose el programa


• Tiempo de usuario : cuánto tiempo usó la CPU el programa
• Tiempo del sistema : cuánto tiempo trabajó el sistema operativo para el programa

Utilizamos JDK 11:

pág. 531
java -version
java version "11-ea" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11-ea+22)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11-ea+22, mixed mode)

La salida de los comandos anteriores puede ser diferente en su computadora. Durante


nuestra ejecución de prueba, cuando ejecutamos el programa anterior con
los javaparámetros de comando especificados , la salida comenzó con las siguientes
cuatro líneas:

Using G1
GC(0) Pause Young (Normal) (G1 Evacuation Pause) 204M->101M(4096M)
GC(1) Pause Young (Normal) (G1 Evacuation Pause) 279M->191M(4096M)
GC(2) Pause Young (Normal) (G1 Evacuation Pause) 371M->280M(4096M)

Como puede ver, el recolector de basura G1 es el predeterminado en JDK 11, y


comenzó a recopilar arr objetos sin referencia de inmediato. Como esperábamos, el
programa salió después de : OutOfMemoryError

GC(50) Pause Full (G1 Evacuation Pause) 4090M->4083M(4096M)


GC(51) Concurrent Cycle 401.931ms
GC(52) To-space exhausted
GC(52) Pause Young (Concurrent Start) (G1 Humongous Allocation)
GC(53) Concurrent Cycle
GC(54) Pause Young (Normal) (G1 Humongous Allocation) 4088M->4088M(4096M)
GC(55) Pause Full (G1 Humongous Allocation) 4088M->4085M(4096M)
GC(56) Pause Full (G1 Humongous Allocation) 4085M->4085M(4096M)
GC(53) Concurrent Cycle 875.061ms
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3720)
at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:242)
at java.base/java.util.ArrayList.add(ArrayList.java:485)
at java.base/java.util.ArrayList.add(ArrayList.java:498)
at com.packt.cookbook.ch11_memory.Epsilon.main(Epsilon.java:12)

La utilidad de tiempo produjo los siguientes resultados:

real 0m11.549s //How long the program ran


user 0m35.301s //How much time the CPU was used by the program
sys 0m19.125s //How much time the OS worked for the program

Nuestra computadora es multinúcleo, por lo que JVM pudo utilizar varios núcleos en
paralelo, probablemente para la recolección de basura. Es por eso que el tiempo del
usuario es mayor que el tiempo real, y el tiempo del sistema es mayor que el tiempo
real por la misma razón.

Ahora ejecutemos el mismo programa con el siguiente comando:

time java -cp cookbook-1.0.jar -XX:+UnlockExperimentalVMOptions -


XX:+UseEpsilonGC -Xms4G -Xmx4G -Xlog:gc
com.packt.cookbook.ch11_memory.Epsilon

pág. 532
Tenga en cuenta que hemos agregado las opciones -
XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC , que requiere el
recolector de basura Epsilon. El resultado es el siguiente:

Non-resizeable heap; start/max: 4096M


Using TLAB allocation; max: 4096K
Elastic TLABs enabled; elasticity: 1.10x
Elastic TLABs decay enabled; decay time: 1000ms
Using Epsilon
Heap: 4096M reserved, 4096M (100.00%) committed, 205M (5.01%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 410M (10.01%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 614M (15.01%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 820M (20.02%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 1025M (25.02%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 1230M (30.03%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 1435M (35.04%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 1640M (40.04%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 1845M (45.05%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 2050M (50.05%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 2255M (55.06%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 2460M (60.06%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 2665M (65.07%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 2870M (70.07%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 3075M (75.08%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 3280M (80.08%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 3485M (85.09%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 3690M (90.09%) used
Heap: 4096M reserved, 4096M (100.00%) committed, 3895M (95.10%) used
Terminating due to java.lang.OutOfMemoryError: Java heap space

Como puede ver, el recolector de basura ni siquiera intentó recoger los objetos
abandonados. El uso del espacio de almacenamiento dinámico creció de manera
constante hasta que se consumió por completo, y la JVM salió OutOfMemoryError. El
uso de la utilidad time nos permitió medir tres parámetros de tiempo siguientes:

real 0m4.239s
user 0m1.861s
sys 0m2.132s

Naturalmente, tomó mucho menos tiempo agotar toda la memoria de almacenamiento


dinámico, y el tiempo del usuario es mucho menor que el tiempo real. Es por eso que,
como ya hemos mencionado, el recolector de basura Epsilon no operativo puede ser útil
para los programas que tienen que ser lo más rápidos posible pero no consumen toda
la memoria de almacenamiento dinámico o pueden detenerse en cualquier
momento. Probablemente hay otros casos de uso en los que el recolector de basura que
no hace nada puede ser útil.

El bucle de lectura-evaluación-
impresión (REPL) usando JShell
pág. 533
En este capítulo, cubriremos las siguientes recetas:

• Familiarizarse con REPL


• Navegando por JShell y sus comandos
• Evaluación de fragmentos de código
• Programación orientada a objetos en JShell
• Guardar y restaurar el historial de comandos de JShell
• Usando la API Java JShell

Introducción
REPL significa Read-Evaluate-Print Loop y, como su nombre lo indica, lee el
comando ingresado en la línea de comando, lo evalúa, imprime el resultado de la
evaluación y continúa este proceso en cualquier comando ingresado.

Todos los idiomas principales, como Ruby, Scala, Python, JavaScript y Groovy, tienen
herramientas REPL. A Java le faltaba el muy necesario REPL. Si tuviéramos que probar
un código de muestra, por ejemplo, usando SimpleDateFormat para analizar una
cadena, teníamos que escribir un programa completo con todas las ceremonias, incluida
la creación de una clase, agregar un método principal y luego la única línea de código
que queremos experimentar con. Luego, tenemos que compilar y ejecutar el
código. Estas ceremonias hacen que sea más difícil experimentar y aprender las
características del idioma.

Con un REPL, puede escribir solo la línea de código con la que está interesado en
experimentar y obtendrá comentarios inmediatos sobre si la expresión es
sintácticamente correcta y si da los resultados deseados. REPL es una herramienta muy
poderosa, especialmente para las personas que llegan al idioma por primera
vez. Suponga que desea mostrar cómo imprimir Hello World en Java; para esto, tendrías
que comenzar a escribir la definición de la clase, luego el método public static
void main(String [] args), y al final, habrías explicado o tratado de explicar
muchos conceptos que de otra manera serían difíciles de comprender para un novato.

De todos modos, con Java 9 en adelante, los desarrolladores de Java ahora pueden dejar
de quejarse por la ausencia de una herramienta REPL. Un nuevo REPL,
llamado JShell, se incluye con la instalación de JDK. Entonces, ahora podemos escribir
con orgullo Hello World como nuestro primer código Hello World .

En este capítulo, exploraremos las características de JShell y escribiremos código que


realmente nos sorprenderá y apreciará el poder de REPL. También veremos cómo
podemos crear nuestras propias REPL utilizando la API JShell Java.

Familiarizarse con REPL


pág. 534
En esta receta, veremos algunas operaciones básicas para ayudarnos a familiarizarnos
con la herramienta JShell .

Prepararse
Asegúrese de tener instalada la última versión de JDK, que tiene JShell . JShell está
disponible desde JDK 9 en adelante.

Cómo hacerlo...
1. Debe tener %JAVA_HOME%/bin(en Windows) o $JAVA_HOME/bin(en Linux)
agregado a su PATH variable. De lo contrario, visite Instalar JDK 18.9 en Windows y
configure la variable PATH e Instale JDK 18.9 en Linux (Ubuntu, x64) y configure
las recetas de la variable PATH en el Capítulo 1 , Instalación y un adelanto en Java
11 .
2. En la línea de comando, escriba jshelly presione Entrar .
3. Verá un mensaje y luego un jshell>mensaje:

4. Barra inclinada ( /) , seguido por el JShell -apoyado comandos, que ayudan en la


interacción con JShell . Al igual que intentamos /help introobtener lo siguiente:

5. Imprimamos un Hello Worldmensaje:

pág. 535
6. Imprimamos un Hello Worldmensaje personalizado :

7. Puede navegar por los comandos ejecutados con las teclas de flecha arriba y abajo.

Cómo funciona...
Los fragmentos de código ingresados en la solicitud jshell se envuelven con
suficiente código para ejecutarlos. Entonces, las declaraciones de variables, métodos y
clases se ajustan dentro de una clase, y las expresiones se ajustan dentro de un método
que a su vez se ajusta dentro de la clase. Otras cosas, como las importaciones y las
definiciones de clase, permanecen como están porque son entidades de nivel superior,
es decir, no es necesario ajustar una definición de clase dentro de otra clase, ya que una
definición de clase es una entidad de nivel superior que puede existir por sí misma . Del
mismo modo, en Java, las declaraciones de importación pueden ocurrir por sí mismas y
ocurren fuera de una declaración de clase y, por lo tanto, no es necesario que se
envuelvan dentro de una clase.

En las siguientes recetas, veremos cómo definir un método, importar paquetes


adicionales y definir clases.

En la receta anterior, vimos $1 ==> "Hello World". Si tenemos algún valor sin
ninguna variable asociada, jshellle da un nombre de variable, como $1 o $2.

Navegando por JShell y sus


comandos
Para aprovechar una herramienta, debemos estar familiarizados con cómo usarla, los
comandos que proporciona y las diversas teclas de acceso directo que podemos usar
para ser productivos. En esta receta, veremos las diferentes formas en que podemos

pág. 536
navegar a través de JShell y también los diferentes atajos de teclado que proporciona
para ser productivo mientras lo usa.

Cómo hacerlo...
1. Desovar JShell escribiendo jshell en la línea de comandos. Será
recibido con un mensaje de bienvenida que contiene las instrucciones
para comenzar.
2. Escriba /help intropara obtener una breve introducción a JShell :

3. Escriba /helppara obtener una lista de los comandos admitidos:

4. Para obtener más información sobre un comando, escriba /help


<command>. Por ejemplo, para obtener información sobre /edit, escriba /help
/edit:

pág. 537
5. Hay soporte de autocompletado en JShell . Esto hace que los
desarrolladores de Java se sientan como en casa. Puede invocar la finalización
automática con la tecla Tab :

pág. 538
6. Puede usar /!para ejecutar un comando ejecutado previamente
y /line_numbervolver a ejecutar una expresión en el número de línea.

7. Para navegar el cursor por la línea de comando, use Ctrl + A para llegar
al principio de la línea y Ctrl + E para llegar al final de la línea.

Evaluación de fragmentos de
código
En esta receta, analizaremos la ejecución de los siguientes fragmentos de código:

• Declaraciones de importación
• Declaraciones de clase
• Declaraciones de interfaz
• Declaraciones de métodos
• Declaraciones de campo
• Declaraciones

pág. 539
Cómo hacerlo...
1. Abra la línea de comando e inicie JShell .
2. Por defecto, JShell importa algunas bibliotecas. Podemos verificar eso emitiendo
el comando /imports:

3. Vamos a importar java.text.SimpleDateForm emitiendo el comando


import java.text.SimpleDateFormat. Esto importa la clase
SimpleDateFormat.

4. Declaremos una clase Employee. Emitiremos una declaración en cada línea para
que sea una declaración incompleta, y procederemos de la misma manera que lo
hacemos en cualquier editor ordinario. La siguiente ilustración aclarará esto:

class Employee{
private String empId;
public String getEmpId() {
return empId;
}
public void setEmpId ( String empId ) {
this.empId = empId;
}
}

Obtendrá el siguiente resultado:

pág. 540
5. Declaremos una interfaz Employability, que define un
método employable(), como se muestra en el siguiente fragmento de código:

interface Employability {
public boolean employable();
}

La interfaz anterior, cuando se crea a través de jshell, se muestra en la siguiente


captura de pantalla:

6. Declaremos un método newEmployee(String empId) que construya


un objeto Employee con el empId dado :

public Employee newEmployee(String empId ) {


Employee emp = new Employee();
emp.setEmpId(empId);
return emp;
}

El método anterior definido en JShell se muestra aquí:

7. Usaremos el método definido en el paso anterior para crear una declaración que
declare una variable Employee:

Employee e = newEmployee("1234");

pág. 541
La declaración anterior y su salida cuando se ejecuta desde JShell se muestran en la
siguiente captura de pantalla. La clave e.get + Tab de fragmento genera
autocompletado según lo admiten los IDE:

Hay más...
Podemos invocar un método indefinido. Eche un vistazo al siguiente ejemplo:

public void newMethod(){


System.out.println("New Method");
undefinedMethod();
}

La siguiente imagen muestra la definición


de newMethod()invocando undefinedMethod():

Sin embargo, el método no puede invocarse antes de que se haya definido el método
utilizado:

public void undefinedMethod(){


System.out.println("Now defined");
}

La siguiente imagen muestra el método que se undefinedMethod()está definiendo


y luego newMethod()se puede invocar con éxito:

pág. 542
Podemos invocar newMethod() solo después de haberlo
definido undefinedMethod().

Programación orientada a objetos


en JShell
En esta receta, haremos uso de archivos de definición de clase Java predefinidos y los
importaremos a JShell . Luego, jugaremos con esas clases en JShell .

Cómo hacerlo...
1. Los archivos de definición de clase que utilizaremos en esta receta están
disponibles Chapter12/4_oo_programming en las descargas de código de
este libro.
2. Hay tres archivos de definición de clase: Engine.java, Dimensions.java,
y Car.java.
3. Navegue al directorio donde están disponibles estos tres archivos de definición de
clase.
4. El comando /open nos permite cargar el código desde un archivo.
5. Cargue la Enginedefinición de clase y cree un objeto Engine:

6. Cargue la definición Dimensions de clase y cree un objeto Dimensions:

pág. 543
7. Cargue la definición Car de clase y cree un objeto Car:

Guardar y restaurar el historial


de comandos de JShell
Queremos probar algunos fragmentos de código jshell como un medio para explicar
la programación de Java a alguien que sea nuevo en él. Además, alguna forma de
registro de qué fragmentos de código se ejecutaron será útil para la persona que está
aprendiendo el idioma.

En esta receta, ejecutaremos algunos fragmentos de código y los guardaremos en un


archivo. Luego cargaremos los fragmentos de código del archivo guardado.

Cómo hacerlo...
1. Ejecutemos una serie de fragmentos de código, de la siguiente manera:

"Hello World"
String msg = "Hello, %s. Good Morning"
System.out.println(String.format(msg, "Friend"))
int someInt = 10
boolean someBool = false
if ( someBool ) {
System.out.println("True block executed");
}
if ( someBool ) {
System.out.println("True block executed");
}else{
System.out.println("False block executed");
}
for ( int i = 0; i < 10; i++ ){

pág. 544
System.out.println("I is : " + i );
}

Obtendrá el siguiente resultado:

2. Guarde los fragmentos de código ejecutados en un archivo llamado history


usando el /save historycomando.

3. Salga del shell utilizando /exit y enumere los archivos en el directorio


utilizando diro ls, según el sistema operativo. Habrá un historyarchivo en la lista.

pág. 545
4. Abra jshelly compruebe el historial de fragmentos de código ejecutados
con /list. Verá que no hay fragmentos de código ejecutados.

5. Cargue el archivo history usando /open historyy luego verifique el


historial de los fragmentos de código ejecutados usando /list. Verá que todos los
fragmentos de código anteriores se ejecutan y se agregan al historial:

Usando la API Java JShell


JDK 11 proporciona la API de Java para crear herramientas como, jshell
por ejemplo, para evaluar fragmentos de código de Java. Esta API de Java está presente
en el módulo
jdk.jshell ( http://cr.openjdk.java.net/~rfield/arch/doc/jdk/j

pág. 546
shell/package-summary.html ). Por lo tanto, si desea usar la API en su
aplicación, debe declarar una dependencia en el módulo jdk.jshell.

En esta receta, usaremos la API JDK de JShell para evaluar fragmentos de código
simples, y también verá diferentes API para obtener el estado de JShell . La idea no es
recrear JShell sino mostrar cómo usar su API JDK.

Para esta receta, no usaremos JShell ; en su lugar, seguiremos la forma habitual de


compilar usando javac y ejecutar usando java.

Cómo hacerlo...
1. Nuestro módulo dependerá del módulo jdk.jshell. Entonces, la definición del
módulo será similar a la siguiente:

module jshell{
requires jdk.jshell;
}

2. Cree una instancia de la clase jdk.jshell.JShell utilizando su método


create()o la API del generador en jdk.jshell.JShell.Builder:

JShell myShell = JShell.create();

3. Lea el fragmento de código de System.in usando java.util.Scanner:

try(Scanner reader = new Scanner(System.in)){


while(true){
String snippet = reader.nextLine();
if ( "EXIT".equals(snippet)){
break;
}
//TODO: Code here for evaluating the snippet using JShell API
}
}

4. Use el método jdk.jshell.JShell#eval(String snippet) para


evaluar la entrada. La evaluación dará como resultado una lista
de jdk.jshell.SnippetEvent, que contiene el estado y la salida de la
evaluación. El TODOfragmento de código anterior se reemplazará por las siguientes líneas:

List<SnippetEvent> events = myShell.eval(snippet);


events.stream().forEach(se -> {
System.out.print("Evaluation status: " + se.status());
System.out.println(" Evaluation result: " + se.value());
});

pág. 547
5. Cuando se complete la evaluación, vamos a imprimir los fragmentos procesados
utilizando el método jdk.jshell.JShell.snippets(), que devolverá Stream
de Snippet procesado.

System.out.println("Snippets processed: ");


myShell.snippets().forEach(s -> {
String msg = String.format("%s -> %s", s.kind(), s.source());
System.out.println(msg);
});

6. Del mismo modo, podemos imprimir el método activo y las variables, de la


siguiente manera:

System.out.println("Methods: ");
myShell.methods().forEach(m ->
System.out.println(m.name() + " " + m.signature()));

System.out.println("Variables: ");
myShell.variables().forEach(v ->
System.out.println(v.typeName() + " " + v.name()));

7. Antes de que la aplicación salga, cerramos la instancia JShell invocando


su método close():

myShell.close();

El código para esta receta se puede encontrar


en Chapter12/6_jshell_api. Puede ejecutar la muestra utilizando
los scripts run.bato run.shdisponibles en el mismo directorio. La ejecución y la
salida de muestra se muestran aquí:

pág. 548
Cómo funciona...
La clase central en la API es la clase jdk.jshell.JShell. Esta clase es el motor de
estado de evaluación, cuyo estado se modifica con cada evaluación del fragmento. Como
vimos anteriormente, los fragmentos se evalúan utilizando el método eval(String
snippet) Incluso podemos soltar el fragmento evaluado previamente utilizando
el método drop(Snippet snippet) Ambos métodos resultan en un cambio del
estado interno mantenido por jdk.jshell.JShell.

Los fragmentos de código pasados al motor JShell de evaluación se clasifican de la


siguiente manera:

• Erróneo : entrada sintácticamente incorrecta


• Expresiones : una entrada que puede o no generar alguna salida
• Importar : una declaración de importación
• Método : una declaración de método
• Declaración : una declaración
• Declaración de tipo : un tipo, es decir, declaración de clase / interfaz
• Declaración variable : una declaración variable

Todas estas categorías se capturan en la enumeración


jdk.jshell.Snippet.Kind .

También vimos diferentes API para ejecutar los fragmentos evaluados, los métodos
creados, las declaraciones de variables y otros tipos de fragmentos específicos. Cada
tipo de fragmento está respaldado por una clase que extiende la clase
jdk.jshell.Snippet.

Trabajar con nuevas API de fecha


y hora
En este capítulo, cubriremos las siguientes recetas:

• Cómo construir instancias de fecha y hora independientes de la zona horaria


• Cómo construir instancias horarias dependientes de la zona horaria
• Cómo crear un período basado en fechas entre instancias de fechas
• Cómo crear un período basado en el tiempo entre instancias de tiempo
• Cómo representar el tiempo de época
• Cómo manipular instancias de fecha y hora
• Cómo comparar fecha y hora
• Cómo trabajar con diferentes sistemas de calendario.

pág. 549
• Cómo formatear fechas usando DateTimeFormatter

Introducción
Trabajar con java.util.Datey java.util.Calendar fue un dolor para los
desarrolladores de Java hasta que Stephen Colebourne ( http://www.joda.org/ )
presentó Joda-Time ( http://www.joda.org/joda-time/ ), una biblioteca para
trabajar con fecha y hora en Java. Joda-Time proporcionó las siguientes ventajas sobre
la API JDK:

• API más rica para obtener componentes de fecha, como el día de un mes, el día de
una semana, el mes y el año, y componentes de tiempo, como la hora, los minutos
y los segundos.
• Facilidad de manipulación y comparación de fechas y horas.
• Tanto las API independientes de la zona horaria como las dependientes de la zona
horaria están disponibles. La mayoría de las veces, utilizaremos API independientes
de la zona horaria, lo que facilita el uso de la API.
• Increíbles API para calcular la duración entre fechas y horas.
• El formato de fecha y el cálculo de la duración siguen los estándares ISO por defecto.
• Admite múltiples calendarios como gregoriano, budista e islámico.

Joda-Time inspiró JSR-310 ( https://jcp.org/en/jsr/detail?id=310 ), que


portó la API a JDK bajo el java.timepaquete y se lanzó como parte de Java 8. Como
la nueva Fecha / Time API se basa en los estándares ISO, hace que sea muy sencillo
integrar bibliotecas de fecha / hora en diferentes capas de su aplicación. Por ejemplo,
en la capa de JavaScript, podemos usar moment.js
( https://momentjs.com/docs/ ) para trabajar con fecha y hora y usar su estilo
de formato predeterminado (que cumple con ISO) para enviar datos al servidor. En la
capa del servidor, podemos usar la nueva API de fecha / hora para obtener instancias
de fecha y hora según sea necesario. Por lo tanto, estamos interactuando entre el cliente
y el servidor utilizando representaciones de fechas estándar.

En este capítulo, veremos las diferentes formas en que podemos aprovechar la nueva
API de fecha / hora.

Cómo trabajar con instancias de


fecha y hora independientes de la
zona horaria
pág. 550
Antes de JSR-310, no era sencillo crear instancias de fecha y hora para ningún momento
o día en un calendario. La única forma era usar el objeto java.util.Calendar
para establecer las fechas y horas requeridas, y luego invocar el getTime()método
para obtener una instancia de java.util.Date. Y esas instancias de fecha y hora
también contenían información de zona horaria, lo que a veces provocaba errores en la
aplicación.

En las nuevas API, es mucho más sencillo obtener instancias de fecha y hora, y estas
instancias de fecha y hora no tienen ninguna información de zona horaria asociada a
ellas. En esta receta, le mostraremos cómo trabajar con instancias de solo fecha
representadas por java.time.LocalDate, instancias de solo tiempo representadas
por java.time.LocalTime e instancias de fecha / hora representadas
por java.time.LocalDateTime. Estas instancias de fecha y hora son
independientes de la zona horaria y representan la información en la zona horaria
actual de la máquina.

Prepararse
Debe tener al menos JDK 8 instalado para poder usar estas bibliotecas más nuevas, y las
muestras en este capítulo usan la sintaxis que es compatible con Java 10 y
posteriores. Si lo desea, puede ejecutar estos fragmentos de código directamente en
JShell. Puede visitar el Capítulo 12 , El ciclo Leer-Evaluar-Imprimir (REPL) usando
JShell, para obtener más información sobre JShell.

Cómo hacerlo…
1. La fecha actual envuelto en java.time.LocalDate puede obtenerse utilizando
el now()método, como sigue :

var date = LocalDate.now();

2. Podemos obtener campos individuales de la instancia java.time.LocalDate


utilizando el método o métodos específicos get(fieldName), tales
como getDayOfMonth(), getDayOfYear(), getDayOfWeek(), getMonth(),
y getYear(), como sigue :

var dayOfWeek = date.getDayOfWeek();


var dayOfMonth = date.getDayOfMonth();
var month = date.getMonth();
var year = date.getYear();

3. Podemos obtener una instancia de java.time.LocalDate para cualquier


fecha en el calendario utilizando el método of(), como sigue :

pág. 551
var date1 = LocalDate.of(2018, 4, 12);
var date2 = LocalDate.of(2018, Month.APRIL, 12);
date2 = LocalDate.ofYearDay(2018, 102);
date2 = LocalDate.parse("2018-04-12");

4. Existe la clase java.time.LocalTime, que se utiliza para representar


cualquier instancia de tiempo independientemente de la fecha. La hora actual se puede
obtener utilizando lo siguiente:

var time = LocalTime.now();

5. La clase java.time.LocalTime también viene con el método of() de


fábrica, que puede usarse para crear una instancia que represente en cualquier
momento. Del mismo modo, existen métodos para obtener los diferentes componentes de la
época, como sigue :

time = LocalTime.of(23, 11, 11, 11);


time = LocalTime.ofSecondOfDay(3600);

var hour = time.getHour();


var minutes = time.getMinute();
var seconds = time.get(ChronoField.SECOND_OF_MINUTE);

6. java.time.LocalDateTime se usa para representar una entidad que contiene


tanto la hora como la fecha. Está compuesto
por java.time.LocalDatey java.time.LocalTime para representar la fecha y
la hora, respectivamente. Su ejemplo puede ser creado usando now()y diferentes sabores
del método of() de fábrica, como sigue :

var dateTime1 = LocalDateTime.of(2018, 04, 12, 13, 30, 22);


var dateTime2 = LocalDateTime.of(2018, Month.APRIL, 12, 13, 30, 22);
dateTime2 = LocalDateTime.of(date2, LocalTime.of(13, 30, 22));

Cómo funciona…
Las siguientes tres clases en el paquete java.time representan valores de fecha y
hora en la zona horaria predeterminada (la zona horaria del sistema):

• java.time.LocalDate: Contiene solo información de fecha


• java.time.LocalTime: Contiene solo información de tiempo
• java.time.LocalDateTime: Contiene información de fecha y hora

Cada una de las clases se compone de campos, a saber, los siguientes:

• Día
• Mes
• Año

pág. 552
• Hora
• Minutos
• Segundos
• Milisegundos

Todas las clases contienen el método now() , que devuelve los valores actuales de
fecha y hora. Se proporcionan métodos of() de fábrica para construir las instancias
de fecha y hora a partir de sus campos, como día, mes, año, hora y
minuto. java.time.LocalDateTime está formado
por java.time.LocalDatey java.time.LocalTime, por lo que se puede
construir java.time.LocalDateTimedesde java.time.LocalDatey java.t
ime.LocalTime.

Las API importantes aprendidas de esta receta son las siguientes:

• now(): Esto proporciona la fecha y hora actuales


• of(): Este método de fábrica se utiliza para construir la fecha, la hora y las
instancias de fecha / hora requeridas

Hay más…
En Java 9, hay una nueva API, datesUntil que toma la fecha de finalización y
devuelve una secuencia de fechas secuenciales (en otras
palabras java.time.LocalDate) desde la fecha del objeto actual hasta la fecha de
finalización (pero excluyéndola). El uso de esta API agrupa todas las fechas del mes y
año en sus respectivos días de la semana, a saber, lunes, martes, miércoles, etc.

Aceptemos el mes y el año y almacénelo en las variables month y respectivamente. El


inicio del rango será el primer día del mes y año, de la siguiente manera: year

var startDate = LocalDate.of(year, month, 1);

La fecha de finalización del rango será la cantidad de días del mes, como se muestra en
el siguiente fragmento:

var endDate = startDate.plusDays(startDate.lengthOfMonth());

Estamos haciendo uso del método lengthOfMonth para obtener el número de días
en el mes. Luego usamos el método datesUntil para obtener una
secuencia java.time.LocalDate y luego realizamos algunas operaciones de
secuencia:

• Agrupación de instancias java.time.LocalDate por día de la semana.

pág. 553
• Recopilando las instancias agrupadas en java.util.ArrayList. Pero antes de
eso, estamos aplicando una transformación para convertir las
instancias java.time.LocalDate en un día simple del mes, lo que nos da una
lista de enteros que representan el día del mes.

Las dos operaciones anteriores en el código se muestran en el siguiente fragmento:

var dayBuckets = startDate.datesUntil(endDate).collect(

Collectors.groupingBy(date -> date.getDayOfWeek(),


Collectors.mapping(LocalDate::getDayOfMonth,
Collectors.toList())
));

El código para esto se puede encontrar en Chapter13/1_2_print_calendar el


código descargado.

Cómo construir instancias


horarias dependientes de la zona
horaria
En la receta anterior, Cómo construir instancias de fecha y hora independientes de la
zona horaria, construimos objetos de fecha y hora que no contenían ninguna
información de zona horaria. Representaban implícitamente los valores en la zona
horaria del sistema; estas clases
eran java.time.LocalDate, java.time.LocalTimey java.time.LocalDa
teTime.

A menudo necesitaríamos representar la hora con respecto a alguna zona horaria; en


tales escenarios haremos uso de java.time.ZonedDateTime, que contiene
información de zona horaria junto con java.time.LocalDateTime. La
información de zona horaria se incrusta usando java.time.ZoneId o instancias
java.time.ZoneOffset. Hay otras dos clases, java.time.OffsetTime
y java.time.OffsetDateTime, que también son variantes específicas de zona
horaria para java.time.LocalTimey java.time.LocalDateTime.

En esta receta, le mostrará cómo hacer uso


de java.time.ZonedDateTime, java.time.ZoneId, java.time.ZoneOffs
et, java.time.OffsetTime, y java.time.OffsetDateTime.

Prepararse
pág. 554
Haremos uso de la sintaxis de Java 10 que se utiliza varpara las declaraciones y
módulos de variables locales. Aparte de Java 10 y superior, no hay otro requisito previo.

Cómo hacerlo…
1. Vamos a hacer uso del método now()de fábrica para obtener la fecha actual, la hora y
la información de zona horaria en función de la zona horaria del sistema, tal
como sigue :

var dateTime = ZonedDateTime.now();

2. Haremos uso de java.time.ZoneId para obtener la información de fecha y


hora actual en función de cualquier zona horaria dada:

var indianTz = ZoneId.of("Asia/Kolkata");


var istDateTime = ZonedDateTime.now(indianTz);

3. java.time.ZoneOffset también se puede utilizar para proporcionar


información de zona horaria para la fecha y la hora, como sigue :

var indianTzOffset = ZoneOffset.ofHoursMinutes(5, 30);


istDateTime = ZonedDateTime.now(indianTzOffset);

4. Hacemos uso del método of() de fábrica para construir una instancia
de java.time.ZonedDateTime:

ZonedDateTime dateTimeOf = ZonedDateTime.of(2018, 4, 22, 14, 30, 11,


33, indianTz);

5. Incluso podemos extraer java.time.LocalDateTime


de java.time.ZonedDateTime:

var localDateTime = dateTimeOf.toLocalDateTime();

Cómo funciona…
Primero, veamos cómo se captura la información de zona horaria. Se captura en función
del número de horas y minutos desde la hora del meridiano de Greenwich (GMT) ,
también conocida como hora universal coordinada (UTC). Por ejemplo, la hora estándar
de la India (IST), también conocida como Asia / Kolkata, es 5:30 horas antes de GMT.

Java proporciona java.time.ZoneId y java.time.ZoneOffset representa


información de zona horaria. java.time.ZoneId captura información de zona
horaria basada en el nombre de la zona horaria, como Asia / Kolkata, EE. UU. /

pág. 555
Pacífico y EE. UU. / Montaña. Hay alrededor de 599 ID de zona. Esto se ha calculado
utilizando la siguiente línea de código:

jshell> ZoneId.getAvailableZoneIds().stream().count()
$16 ==> 599

Imprimiremos 10 de las ID de zona:

jshell>
ZoneId.getAvailableZoneIds().stream().limit(10).forEach(System.out::printl
n)
Asia/Aden
America/Cuiaba
Etc/GMT+9
Etc/GMT+8
Africa/Nairobi
America/Marigot
Asia/Aqtau
Pacific/Kwajalein
America/El_Salvador
Asia/Pontianak

Los nombres de zona horaria, como Asia / Kolkata, África / Nairobi y América / Cuiabá,
se basan en la base de datos de zonas horarias publicada por la Autoridad Internacional
de Números Asignados (IANA). Los nombres de región de zona horaria proporcionados
por IANA son los predeterminados para Java.

A veces, los nombres de región de zona horaria también se representan como GMT + 02:
30 o simplemente +02: 30, lo que indica el desplazamiento (adelante o atrás) de la zona
horaria actual desde la zona GMT.

Este java.time.ZoneId captura java.time.zone.ZoneRules, que contiene


reglas para obtener las transiciones de desplazamiento de zona horaria y otra
información, como el horario de verano. Investiguemos las reglas de zona para EE. UU.
/ Pacífico:

jshell>
ZoneId.of("US/Pacific").getRules().getDaylightSavings(Instant.now())
$31 ==> PT1H

jshell> ZoneId.of("US/Pacific").getRules().getOffset(LocalDateTime.now())
$32 ==> -07:00

jshell>
ZoneId.of("US/Pacific").getRules().getStandardOffset(Instant.now())
$33 ==> -08:00

El método getDaylightSavings() devuelve un java.time.Duration, que


representa cierta duración en términos de horas, minutos y

pág. 556
segundos. La implementación toString()predeterminada devuelve la duración
representada utilizando la representación basada en ISO 8601 segundos donde una
duración de 1 hora, 20 minutos y 20 segundos se representa como PT1H20M20S. Se
explicará más sobre esto en la receta Cómo crear un período basado en el tiempo entre
instancias de tiempo en este capítulo.
No vamos a entrar en detalles sobre cómo se ha calculado. Para aquellos interesados en
conocer más java.time.zone.ZoneRulesy java.time.ZoneId visitar la
documentación
en https://docs.oracle.com/javase/10/docs/api/java/time/zone/Z
oneRules.html y https://docs.oracle.com
/javase/10/docs/api/java/time/ZoneId.html respectivamente.

La clase java.time.ZoneOffset captura la información de la zona horaria en


términos de la cantidad de horas y minutos que la zona horaria está delante o detrás de
GMT. Creemos una instancia de la clase java.time.ZoneOffsetclase usando
el método of*() factory:

jshell> ZoneOffset.ofHoursMinutes(5,30)
$27 ==> +05:30

La clase java.time.ZoneOffset se extiende desde java.time.ZoneId y


agrega algunos métodos nuevos. Lo importante que debe recordar es construir la
instancia correcta java.time.ZoneOffset y en función java.time.ZoneId de
la zona horaria requerida que se utilizará en sus aplicaciones.

Ahora que tenemos una comprensión de la representación de zona


horaria, java.time.ZonedDateTime no es más
que java.time.LocalDateTime junto
con java.time.ZoneIdo java.time.ZoneOffset. Hay otras dos
clases, java.time.OffsetTimey java.time.OffsetDateTime, que
envuelve java.time.LocalTimey java.time.LocalDateTime
respectivamente, junto con java.time.ZoneOffset.

Veamos algunas formas de construir instancias de java.time.ZonedDateTime.

La primera forma es usar now():

Signatures:
ZonedDateTime ZonedDateTime.now()
ZonedDateTime ZonedDateTime.now(ZoneId zone)
ZonedDateTime ZonedDateTime.now(Clock clock)

jshell> ZonedDateTime.now()
jshell> ZonedDateTime.now(ZoneId.of("Asia/Kolkata"))
$36 ==> 2018-05-04T21:58:24.453113900+05:30[Asia/Kolkata]
jshell> ZonedDateTime.now(Clock.fixed(Instant.ofEpochSecond(1525452037),

pág. 557
ZoneId.of("Asia/Kolkata")))
$54 ==> 2018-05-04T22:10:37+05:30[Asia/Kolkata]

El primer uso de now()utiliza el reloj del sistema, así como la zona horaria del sistema
para imprimir la fecha y hora actuales. El segundo uso de now()utiliza el reloj del
sistema, pero la zona horaria es proporcionada por java.time.ZoneId, que en este
caso es Asia / Kolkata. El tercer uso de now()utiliza el reloj fijo proporcionado y la zona
horaria proporcionada por java.time.ZoneId.

El reloj fijo se crea utilizando la clase java.time.Clock y su método


estático fixed(), que toma una instancia
de java.time.Instanty java.time.ZoneId. La instancia
de java.time.Instant se ha creado utilizando un número estático de segundos
después de la época. java.time.Clock se usa para representar un reloj que puede
usar la nueva API de fecha / hora para determinar la hora actual. El reloj se puede
arreglar, como hemos visto antes, luego podemos crear un reloj que esté una hora por
delante de la hora actual del sistema en la zona horaria de Asia / Calcuta, de la
siguiente manera:

var hourAheadClock = Clock.offset(Clock.system(ZoneId.of("Asia/Kolkata")),


Duration.ofHours(1));

Podemos usar este nuevo reloj para construir instancias


de java.time.LocalDateTimey java.time.ZonedDateTime, de la siguiente
manera:

jshell> LocalDateTime.now(hourAheadClock)
$64 ==> 2018-05-04T23:29:58.759973700
jshell> ZonedDateTime.now(hourAheadClock)
$65 ==> 2018-05-04T23:30:11.421913800+05:30[Asia/Kolkata]

Los valores de fecha y hora se basan en la misma zona horaria, es


decir, Asia/Kolkatapero como ya hemos
aprendido, java.time.LocalDateTimeno tiene ninguna información de zona
horaria y basa los valores en la zona horaria del sistema o
la java.time.Clockproporcionada en este caso . Por otro
lado, java.time.ZonedDateTimecontiene y muestra la información de zona
horaria como [Asia/Kolkata].

El otro enfoque para crear una instancia java.time.ZonedDateTime es usar


su método of() de fábrica :

Signatures:
ZonedDateTime ZonedDateTime.of(LocalDate date, LocalTime time, ZoneId
zone)
ZonedDateTime ZonedDateTime.of(LocalDateTime localDateTime, ZoneId zone)
ZonedDateTime ZonedDateTime.of(int year, int month, int dayOfMonth, int
hour, int minute, int second, int nanoOfSecond, ZoneId zone)

pág. 558
jshell> ZonedDateTime.of(LocalDateTime.of(2018, 1, 1, 13, 44, 44),
ZoneId.of("Asia/Kolkata"))
$70 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]

jshell> ZonedDateTime.of(LocalDate.of(2018,1,1), LocalTime.of(13, 44, 44),


ZoneId.of("Asia/Kolkata"))
$71 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]

jshell> ZonedDateTime.of(LocalDate.of(2018,1,1), LocalTime.of(13, 44, 44),


ZoneId.of("Asia/Kolkata"))
$72 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]

jshell> ZonedDateTime.of(2018, 1, 1, 13, 44, 44, 0,


ZoneId.of("Asia/Kolkata"))
$73 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]

Hay más…
Mencionamos las clases java.time.OffsetTime y . Ambos contienen valores
horarios específicos de la zona horaria. Juguemos con esas clases antes de terminar esta
receta:java.time.OffsetDateTime

• Usando el método de fábrica of():

jshell> OffsetTime.of(LocalTime.of(14,12,34), ZoneOffset.ofHoursMinutes(5,


30))
$74 ==> 14:12:34+05:30

jshell> OffsetTime.of(14, 34, 12, 11, ZoneOffset.ofHoursMinutes(5, 30))


$75 ==> 14:34:12.000000011+05:30

• Usando el método de fábrica now() :


• Signatures:
OffsetTime OffsetTime.now()
OffsetTime OffsetTime.now(ZoneId zone)
OffsetTime OffsetTime.now(Clock clock)

jshell> OffsetTime.now()
$76 ==> 21:49:16.895192800+03:00

jshell> OffsetTime.now(ZoneId.of("Asia/Kolkata"))

jshell> OffsetTime.now(ZoneId.of("Asia/Kolkata"))
$77 ==> 00:21:04.685836900+05:30

jshell> OffsetTime.now(Clock.offset(Clock.systemUTC(),
Duration.ofMinutes(330)))
$78 ==> 00:22:00.395463800Z

• Vale la pena señalar la forma en que creamos una instancia
java.time.Clock, que es 330 minutos (5 horas y 30 minutos) antes del
reloj UTC. La otra clase, java.time.OffsetDateTime es la misma

pág. 559
que java.time.OffsetTime, excepto que
usa java.time.LocalDateTime. Por lo que se le pasa la información de
fecha, es decir, año, mes y día, junto con la información de tiempo a su método
de fábrica of().

Cómo crear un período basado en


fechas entre instancias de fechas
Hay momentos en el pasado cuando tratamos de medir el período entre dos instancias
de fechas pero, debido a la falta de una API anterior a Java 8 y también a la falta de
soporte adecuado para capturar esta información, recurrimos a diferentes
medios. Recordamos usar enfoques basados en SQL para procesar dicha
información. Pero a partir de Java 8 en adelante, tenemos una nueva
clase, java.time.Period que se puede usar para capturar un período entre dos
instancias de fecha en términos de años, meses y días.

Además, esta clase admite el análisis de cadenas basadas en el estándar ISO 8601 para
representar el período. El estándar establece que cualquier período puede
representarse en forma de PnYnMnD, donde P es un carácter fijo para representar el
período, nY representa el número de años, nM para el número de meses y nD para el
número de días. Por ejemplo, un período de 2 años, 4 meses y 10 días se representa
como P2Y4M10D.

Prepararse
Necesita al menos JDK8 para jugar java.time.Period, JDK 9 para poder usar JShell
y al menos JDK 10 para hacer uso de los ejemplos utilizados en esta receta.

Cómo hacerlo…
1. Creemos una instancia de java.time.Period uso de su método de fábrica
of(), que tiene la firma Period.of(int years, int months, int
days):

jshell> Period.of(2,4,30)
$2 ==> P2Y4M30D

2. Hay variantes específicas del método of*(), a saber, ofDays(), ofMonths(),


y ofYears(), que puede ser utilizado, así:

pág. 560
1. jshell> Period.ofDays(10)
$3 ==> P10D
jshell> Period.ofMonths(4)
$4 ==> P4M
jshell> Period.ofWeeks(3)
$5 ==> P21D
jshell> Period.ofYears(3)
$6 ==> P3Y

3. Tenga en cuenta que el método ofWeeks()es un método auxiliar para


construir java.time.Period basado en días al aceptar el número de
semanas.

3. El período también se puede construir usando la cadena de período, que


generalmente es de la forma P<x>Y<y>M<z>D donde x, yy z representan el número de
años, meses y días, respectivamente:

jshell> Period.parse("P2Y4M23D").getDays()
$8 ==> 23

4. También podemos calcular el período entre dos instancias


de java.time.ChronoLocalDate(una de sus implementaciones
es java.time.LocalDate):

jshell> Period.between(LocalDate.now(), LocalDate.of(2018, 8, 23))


$9 ==> P2M2D
jshell> Period.between(LocalDate.now(), LocalDate.of(2018, 2, 23))
$10 ==> P-3M-26D

Estas son las formas más útiles para crear una instancia de java.time.Period. La
fecha de inicio es inclusiva y la fecha de finalización es exclusiva.

Cómo funciona…
Hacemos uso de los métodos de fábrica java.time.Period para crear su
instancia. El java.time.Period tiene tres campos para contener los valores de año,
mes y día, respectivamente, como se indica a continuación:

/**
* The number of years.
*/
private final int years;
/**
* The number of months.
*/
private final int months;
/**
* The number of days.

pág. 561
*/
private final int days;

Hay un interesante conjunto de métodos, a saber, withDays(), withMonths(),


y withYears(). Estos métodos devuelven la misma instancia si el campo que está
tratando de actualizar tiene el mismo valor; de lo contrario, devuelve una nueva
instancia con valores actualizados, como se indica a continuación:

jshell> Period period1 = Period.ofWeeks(2)


period1 ==> P14D

jshell> Period period2 = period1.withDays(15)


period2 ==> P15D

jshell> period1 == period2


$19 ==> false

jshell> Period period3 = period1.withDays(14)


period3 ==> P14D

jshell> period1 == period3


$21 ==> true

Hay más…
Incluso podemos calcular java.time.Period entre las dos instancias de fecha
utilizando el método until()presente en java.time.ChronoLocalDate:

jshell> LocalDate.now().until(LocalDate.of(2018, 2, 23))


$11 ==> P-3M-26D

jshell> LocalDate.now().until(LocalDate.of(2018, 8, 23))


$12 ==> P2M2D

Dada una instancia de java.time.Period, podemos usarla para manipular una


instancia de fecha dada. Hay dos formas posibles:

• Usando el método addTo o subtractFrom del objeto período


• Usando el método plus o minus del objeto de fecha

Ambos enfoques se muestran en los siguientes fragmentos :

jshell> Period period1 = Period.ofWeeks(2)


period1 ==> P14D

jshell> LocalDate date = LocalDate.now()


date ==> 2018-06-21

jshell> period1.addTo(date)
$24 ==> 2018-07-05

pág. 562
jshell> date.plus(period1)
$25 ==> 2018-07-05

En líneas similares, puede probar los métodos subtractFrom y minus. Hay otro
conjunto de métodos utilizados para manipular la instancia java.time.Period, a
saber, los siguientes:

• minus, minusDays, minusMonths, Y minusYears: Restar el valor dado de


la época.
• plus, plusDays, plusMonths, Y plusYears: Añadir el valor dado para el
período.
• negated: Devuelve el nuevo período con cada uno de sus valores negados.
• normalized: Devuelve un nuevo período normalizando sus campos de orden
superior, como meses y días. Por ejemplo, 15 meses se normaliza a 1 año y 3 meses.

Le mostraremos estos métodos en acción de la siguiente manera, comenzando con


los métodos minus:

jshell> period1.minus(Period.of(1,3,4))
$28 ==> P2Y12M25D

jshell> period1.minusDays(4)
$29 ==> P3Y15M25D

jshell> period1.minusMonths(3)
$30 ==> P3Y12M29D

jshell> period1.minusYears(1)
$31 ==> P2Y15M29D

Luego, veremos los métodos plus:

jshell> Period period1 = Period.of(3, 15, 29)


period1 ==> P3Y15M29D

jshell> period1.plus(Period.of(1, 3, 4))


$33 ==> P4Y18M33D

jshell> period1.plusDays(4)
$34 ==> P3Y15M33D

jshell> period1.plusMonths(3)
$35 ==> P3Y18M29D

jshell> period1.plusYears(1)
$36 ==> P4Y15M29D

Finalmente, aquí están los métodos negated()y normalized():

jshell> Period period1 = Period.of(3, 15, 29)


period1 ==> P3Y15M29D

jshell> period1.negated()

pág. 563
$38 ==> P-3Y-15M-29D

jshell> period1
period1 ==> P3Y15M29D

jshell> period1.normalized()
$40 ==> P4Y3M29D

jshell> period1
period1 ==> P3Y15M29D

Observe que, en los dos casos anteriores, no está mutando el período existente, en
lugar de devolver una nueva instancia.

Cómo crear un período basado en


el tiempo entre instancias de
tiempo
En nuestra receta anterior, creamos un período basado en la fecha, que está
representado por java.time.Period. En esta receta, veremos cómo crear una
diferencia basada en el tiempo entre instancias de tiempo en términos de segundos y
nanosegundos usando la clase java.time.Duration.

Examinaremos diferentes formas de crear una instancia java.time.Duration,


manipular la instancia de duración y obtener la duración en términos de diferentes
unidades, como horas y minutos. El estándar ISO 8601 especifica uno de los posibles
patrones para representar la duración PnYnMnDTnHnMnS, donde se aplica lo
siguiente:

• Y, My Drepresentan los campos del componente de fecha, a saber, año, mes y día.
• T separa la fecha con la información de la hora
• H, My Srepresentan los campos del componente de tiempo, a saber, hora, minutos y
segundos.

La implementación de la representación de cadenas java.time.Duration se basa


libremente en la ISO 8601. Hay más sobre esto en la sección Cómo funciona .

Prepararse
Necesita al menos JDK 8 para jugar java.time.Durationy JDK 9 para poder
utilizar JShell.

pág. 564
Cómo hacerlo…
1. Las instancias java.time.Duration se pueden crear utilizando los métodos de
fábrica of*(). Mostraremos usando algunos de ellos, de la siguiente manera:

jshell> Duration.of(56, ChronoUnit.MINUTES)


$66 ==> PT56M
jshell> Duration.of(56, ChronoUnit.DAYS)
$67 ==> PT1344H
jshell> Duration.ofSeconds(87)
$68 ==> PT1M27S
jshell> Duration.ofHours(7)
$69 ==> PT7H

2. También se pueden crear mediante el análisis de la cadena de


duración, como sigue :

jshell> Duration.parse("P12D")
$70 ==> PT288H
jshell> Duration.parse("P12DT7H5M8.009S")
$71 ==> PT295H5M8.009S
jshell> Duration.parse("PT7H5M8.009S")
$72 ==> PT7H5M8.009S

3. Pueden ser construidos mediante la búsqueda de la luz entre dos casos


java.time.Temporal, que soportan la información de tiempo (es decir, los casos
de java.time.LocalDateTime y los gustos), como sigue:

jshell> LocalDateTime time1 = LocalDateTime.now()


time1 ==> 2018-06-23T10:51:21.038073800
jshell> LocalDateTime time2 = LocalDateTime.of(2018, 6, 22, 11, 00)
time2 ==> 2018-06-22T11:00
jshell> Duration.between(time1, time2)
$77 ==> PT-23H-51M-21.0380738S
jshell> ZonedDateTime time1 = ZonedDateTime.now()
time1 ==> 2018-06-23T10:56:57.965606200+03:00[Asia/Riyadh]
jshell> ZonedDateTime time2 = ZonedDateTime.of(LocalDateTime.now(),
ZoneOffset.ofHoursMinutes(5, 30))
time2 ==> 2018-06-23T10:56:59.878712600+05:30
jshell> Duration.between(time1, time2)
$82 ==> PT-2H-29M-58.0868936S

Cómo funciona…
Los datos necesarios para java.time.Duration se almacenan en dos campos que
representan segundos y nanosegundos, respectivamente. Hay métodos de
conveniencia previstas para conseguir la duración en términos de minutos, horas y
días, a saber, toMinutes(), toHours(), y toDays().

pág. 565
Analicemos la implementación de la representación de
cadena. java.time.Duration admite el análisis de la representación de cadena
ISO que contiene solo el componente de día en la parte de fecha y las horas, minutos,
segundos y nanosegundos en la parte de tiempo. Por ejemplo, P2DT3 Mes aceptable,
mientras que el
análisis P3M2DT3M resulta java.time.format.DateTimeParseException
porque la cadena contiene el componente del mes en la parte de la fecha.

El método toString()de java.time.Duration siempre devuelve una cadena de


la PTxHyMz.nS forma, donde xrepresenta la cantidad de horas, yrepresenta la
cantidad de minutos y z.nrepresenta la cantidad de segundos a una precisión de
nanosegundos. Veamos algunos ejemplos:

jshell> Duration.parse("P2DT3M")
$2 ==> PT48H3M

jshell> Duration.parse("P3M2DT3M")
| Exception java.time.format.DateTimeParseException: Text cannot be parsed
to a Duration
| at Duration.parse (Duration.java:417)
| at (#3:1)

jshell> Duration.ofHours(4)
$4 ==> PT4H

jshell> Duration.parse("PT3H4M5.6S")
$5 ==> PT3H4M5.6S

jshell> Duration d = Duration.parse("PT3H4M5.6S")


d ==> PT3H4M5.6S

jshell> d.toDays()
$7 ==> 0

jshell> d.toHours()
$9 ==> 3

Hay más…
Veamos los métodos de manipulación proporcionados, que permiten sumar / restar un
valor de la unidad de tiempo específica, como días, horas, minutos, segundos o
nanosegundos. Cada uno de estos métodos es inmutable, por lo que una nueva instancia
se devuelve cada vez, como sigue:

jshell> Duration d = Duration.parse("PT1H5M4S")


d ==> PT1H5M4S

jshell> d.plusDays(3)
$14 ==> PT73H5M4S

jshell> d

pág. 566
d ==> PT1H5M4S

jshell> d.plusDays(3)
$16 ==> PT73H5M4S

jshell> d.plusHours(3)
$17 ==> PT4H5M4S

jshell> d.plusMillis(4)
$18 ==> PT1H5M4.004S

jshell> d.plusMinutes(40)
$19 ==> PT1H45M4S

Del mismo modo, puede probar los métodos minus*() , que restan. Luego están
los métodos que manipulan los casos
de java.time.LocalDateTime, java.time.ZonedDateTime y sus
semejantes. Estos métodos suman / restan la duración a / de la información de fecha /
hora. Veamos algunos ejemplos:

jshell> Duration d = Duration.parse("PT1H5M4S")


d ==> PT1H5M4S

jshell> d.addTo(LocalDateTime.now())
$21 ==> 2018-06-25T21:15:53.725373600

jshell> d.addTo(ZonedDateTime.now())
$22 ==> 2018-06-25T21:16:03.396595600+03:00[Asia/Riyadh]

jshell> d.addTo(LocalDate.now())
| Exception java.time.temporal.UnsupportedTemporalTypeException:
Unsupported unit: Seconds
| at LocalDate.plus (LocalDate.java:1272)
| at LocalDate.plus (LocalDate.java:139)
| at Duration.addTo (Duration.java:1102)
| at (#23:1)

Puede observar en el ejemplo anterior que obtuvimos una excepción cuando


intentamos agregar la duración a la entidad que contiene solo información de fecha.

Cómo representar el tiempo de


época
En esta receta, veremos el uso java.time.Instant para representar un punto en
el tiempo, así como convertir ese punto en el tiempo a una época de segundos /
milisegundos. La época de Java se utiliza para referirse al instante de tiempo 1970-01-
01 a 0: 00: 00Z y java.time.Instant almacena el número de segundos desde la
época de Java. Un valor positivo indica que el tiempo está por delante de la época y

pág. 567
negativo indica que el tiempo está por detrás de la época. Utiliza el reloj del sistema en
UTC para calcular el valor instantáneo de la hora actual.

Prepararse
Debe tener JDK compatible con las nuevas API de fecha / hora y JShell instalado para
poder probar la solución provista.

Cómo hacerlo…
1. Simplemente crearemos una instancia java.time.Instant e imprimiremos los
segundos de la época, lo que dará el tiempo en UTC después de la época de Java:

jshell> Instant.now()
$40 ==> 2018-07-06T07:56:40.651529300Z

jshell> Instant.now().getEpochSecond()
$41 ==> 1530863807

2. También podemos imprimir los milisegundos de la época, que muestra el número de


milisegundos después de la época. Esto es un poco más preciso que solo unos segundos:

jshell> Instant.now().toEpochMilli()
$42 ==> 1530863845158

Cómo funciona…
La clase java.time.Instant almacena la información de tiempo en sus dos
campos:

• Segundos, que es del tipo long: almacena el número de segundos desde la época de
1970-01-01T00: 00: 00Z.
• Nanos, que es del tipo int: almacena la cantidad de nanosegundos

Cuando invoca el método now(), java.time.Instant utiliza el reloj del sistema


en UTC para representar ese instante de tiempo. Y luego podemos
usar atZone()o atOffset()convertirlo a la zona horaria requerida, como veremos
en la siguiente sección.

Use esta clase si solo desea representar la línea de tiempo de las acciones en UTC; de
esa manera, la marca de tiempo almacenada para diferentes eventos se basará en UTC
y luego podrá convertirla a su zona horaria requerida cuando sea necesario.

pág. 568
Hay más…
Podemos manipular al sumar / restar nanosegundos , milisegundos y segundos, de
la siguiente manera :

jshell> Instant.now().plusMillis(1000)
$43 ==> 2018-07-06T07:57:57.092259400Z

jshell> Instant.now().plusNanos(1991999)
$44 ==> 2018-07-06T07:58:06.097966099Z

jshell> Instant.now().plusSeconds(180)
$45 ==> 2018-07-06T08:01:15.824141500Z

Del mismo modo, puede probar los métodos minus*(). También podemos obtener
la fecha y hora dependiente de la zona horaria utilizando
los métodos java.time.Instant atOffset() y atZone(), de la siguiente
manera :

jshell> Instant.now().atZone(ZoneId.of("Asia/Kolkata"))
$36 ==> 2018-07-06T13:15:13.820694500+05:30[Asia/Kolkata]

jshell> Instant.now().atOffset(ZoneOffset.ofHoursMinutes(2,30))
$37 ==> 2018-07-06T10:15:19.712039+02:30

Cómo manipular instancias de


fecha y hora
Las clases de fecha y
hora, java.time.LocalDate, java.time.LocalTime, java.time.LocalDa
teTime, y java.time.ZonedDateTime, proporcionar métodos para sumar y
restar los valores de sus componentes, es decir, días, horas, minutos, segundos,
semanas, meses, años, y otros.

En esta receta, veremos algunos de estos métodos, que pueden usarse para manipular
instancias de fecha y hora al sumar y restar diferentes valores.

Prepararse
Necesitará una instalación JDK que admita las nuevas API de fecha / hora y la consola
JShell.

pág. 569
Cómo hacerlo…
1. Manipulemos java.time.LocalDate:

jshell> LocalDate d = LocalDate.now()


d ==> 2018-07-27

jshell> d.plusDays(3)
$5 ==> 2018-07-30

jshell> d.minusYears(4)
$6 ==> 2014-07-27

2. Manipulemos la instancia de fecha y hora java.time.LocalDateTime:

jshell> LocalDateTime dt = LocalDateTime.now()


dt ==> 2018-07-27T15:27:40.733389700

jshell> dt.plusMinutes(45)
$8 ==> 2018-07-27T16:12:40.733389700

jshell> dt.minusHours(4)
$9 ==> 2018-07-27T11:27:40.733389700

3. Manipulemos la fecha y hora que dependen de la zona


horaria java.time.ZonedDateTime:

jshell> ZonedDateTime zdt = ZonedDateTime.now()


zdt ==> 2018-07-27T15:28:28.309915200+03:00[Asia/Riyadh]

jshell> zdt.plusDays(4)
$11 ==> 2018-07-31T15:28:28.309915200+03:00[Asia/Riyadh]

jshell> zdt.minusHours(3)
$12 ==> 2018-07-27T12:28:28.309915200+03:00[Asia/Riyadh]

Hay más…
Acabamos de ver algunas de las API de suma y resta representadas por plus*()
y minus*(). Se proporcionan diferentes métodos para manipular diferentes
componentes de fecha y hora, como años, días, meses, horas, minutos, segundos y
nanosegundos. Puede probar esas API como ejercicio.

Cómo comparar fecha y hora


A menudo, nos gustaría comparar instancias de fecha y hora con otras para verificar si
son anteriores, posteriores o iguales a las del otro. Para lograr esto, JDK

pág. 570
proporciona isBefore(), isAfter() y isEqual()en
los java.time.LocalDate, java.time.LocalDateTime
y java.time.ZonedDateTimeclases. En esta receta, veremos el uso de estos
métodos para comparar instancias de fecha y hora.

Prepararse
Necesitará una instalación de JDK que tenga las nuevas API de fecha / hora y sea
compatible con JShell.

Cómo hacerlo…
1. Probemos comparando dos instancias java.time.LocalDate:

jshell> LocalDate d = LocalDate.now()


d ==> 2018-07-28

jshell> LocalDate d2 = LocalDate.of(2018, 7, 27)


d2 ==> 2018-07-27

jshell> d.isBefore(d2)
$4 ==> false

jshell> d.isAfter(d2)
$5 ==> true

jshell> LocalDate d3 = LocalDate.of(2018, 7, 28)


d3 ==> 2018-07-28

jshell> d.isEqual(d3)
$7 ==> true

jshell> d.isEqual(d2)
$8 ==> false

2. También podemos comparar las instancias de fecha y hora que dependen de la zona
horaria:

jshell> ZonedDateTime zdt1 = ZonedDateTime.now();


zdt1 ==> 2018-07-28T14:49:34.778006400+03:00[Asia/Riyadh]

jshell> ZonedDateTime zdt2 = zdt1.plusHours(4)


zdt2 ==> 2018-07-28T18:49:34.778006400+03:00[Asia/Riyadh]

jshell> zdt1.isBefore(zdt2)
$11 ==> true

jshell> zdt1.isAfter(zdt2)
$12 ==> false

pág. 571
jshell> zdt1.isEqual(zdt2)
$13 ==> false

Hay más…
La comparación se puede realizar
en java.time.LocalTimey java.time.LocalDateTime. Esto se deja al lector
para explorar.

Cómo trabajar con diferentes


sistemas de calendario.
Hasta ahora en nuestras recetas, trabajamos con el sistema de calendario ISO, que es el
sistema de calendario de facto seguido en el mundo. Hay otros sistemas de calendario
regional seguidos en el mundo, como Hijrah, japonés y tailandés. JDK también brinda
soporte para dichos sistemas de calendario.

En esta receta, veremos cómo trabajar con dos sistemas de calendario: japonés y el
Hijri.

Prepararse
Debe tener un JDK instalado que admita las nuevas API de fecha / hora y la herramienta
JShell.

Cómo hacerlo…
1. Imprimamos la fecha actual en los diferentes sistemas de calendario
compatibles con JDK:

jshell> Chronology.getAvailableChronologies().forEach(chrono ->


System.out.println(chrono.dateNow()))
2018-07-30
Minguo ROC 107-07-30
Japanese Heisei 30-07-30
ThaiBuddhist BE 2561-07-30
Hijrah-umalqura AH 1439-11-17

2. Juguemos con la fecha representada en el sistema de calendario japonés:

pág. 572
jshell> JapaneseDate jd = JapaneseDate.now()
jd ==> Japanese Heisei 30-07-30

jshell> jd.getChronology()
$7 ==> Japanese

jshell> jd.getEra()
$8 ==> Heisei

jshell> jd.lengthOfYear()
$9 ==> 365

jshell> jd.lengthOfMonth()
$10 ==> 31

3. Se pueden enumerar diferentes eras compatibles con el calendario japonés


mediante java.time.chrono.JapeneseEra:

jshell> JapaneseEra.values()
$42 ==> JapaneseEra[5] { Meiji, Taisho, Showa, Heisei, NewEra }

4. Creemos una fecha en el sistema de calendario de Hijrah:

jshell> HijrahDate hd = HijrahDate.of(1438, 12, 1)


hd ==> Hijrah-umalqura AH 1438-12-01

5. Incluso podemos convertir la fecha / hora ISO en fecha / hora en el sistema


de calendario Hijrah de la siguiente manera:

jshell> HijrahChronology.INSTANCE.localDateTime(LocalDateTime.now())
$23 ==> Hijrah-umalqura AH 1439-11-17T19:56:52.056465900

jshell>
HijrahChronology.INSTANCE.localDateTime(LocalDateTime.now()).toLocalDate()
$24 ==> Hijrah-umalqura AH 1439-11-17

jshell>
HijrahChronology.INSTANCE.localDateTime(LocalDateTime.now()).toLocalTime()
$25 ==> 19:57:07.705740500

Cómo funciona…
El sistema de calendario está representada por java.time.chrono.Chronology
y sus implementaciones, algunos de los cuales
son java.time.chrono.IsoChronology, java.time.chrono.HijrahChro
nology,
y java.time.chrono.JapaneseChronology. java.time.chrono.IsoChro
nology es el sistema de calendario de facto basado en ISO utilizado en el mundo. La
fecha en cada uno de estos sistemas de calendario está representada
por java.time.chrono.ChronoLocalDate y sus implementaciones, algunos de
los cuales

pág. 573
son java.time.chrono.HijrahDate, java.time.chrono.JapaneseDatey
el bien conocido java.time.LocalDate.

Para poder utilizar estas API en JShell, debe importar los paquetes relevantes, de la
siguiente manera:

jshell> import java.time.*

jshell> import java.time.chrono.*

Esto es aplicable a todas las recetas que usan JShell.

Podemos jugar directamente con las implementaciones


de java.time.chrono.ChronoLocalDate, como por
ejemplo java.time.chrono.JapaneseDate, o utilizar la implementación
de java.time.chrono.Chronology obtener representaciones fecha
pertinente, como sigue :

jshell> JapaneseDate jd = JapaneseDate.of(JapaneseEra.SHOWA, 26, 12, 25)


jd ==> Japanese Showa 26-12-25

jshell> JapaneseDate jd = JapaneseDate.now()


jd ==> Japanese Heisei 30-07-30

jshell> JapaneseDate jd = JapaneseChronology.INSTANCE.dateNow()


jd ==> Japanese Heisei 30-07-30

jshell> JapaneseDate jd =
JapaneseChronology.INSTANCE.date(LocalDateTime.now())
jd ==> Japanese Heisei 30-07-30

jshell> ThaiBuddhistChronology.INSTANCE.date(LocalDate.now())
$41 ==> ThaiBuddhist BE 2561-07-30

A partir de los fragmentos de código anteriores, podemos ver que uno puede convertir
la fecha del sistema ISO en fechas en el sistema de calendario requerido utilizando el
método date(TemporalAccessor temporal) de su sistema de calendario .

Hay más…
Puede jugar con los otros sistemas de calendario compatibles con JDK, a saber, los
sistemas de calendario tailandés, budista y Minguo (chino). También vale la pena
explorar para crear nuestros sistemas de calendario personalizado escribiendo una
implementación
de java.time.chrono.Chronology, java.time.chrono.ChronoLocalDat
e y java.time.chrono.Era.

pág. 574
Cómo formatear fechas usando
DateTimeFormatter
Mientras trabajábamos java.util.Date,
utilizamos java.text.SimpleDateFormat para formatear la fecha en diferentes
representaciones de texto y viceversa. Formatear una fecha significa, dada una fecha o
un objeto de hora que lo representa en diferentes formatos, como los siguientes:

• 23 junio 2018
• 23-08-2018
• 2018-08-23
• 23 junio 2018 11:03:33 AM

Estos formatos están controlados por cadenas de formato, como las siguientes:

• dd MMM yyyy
• dd-MM-yyyy
• yyyy-MM-DD
• dd MMM yyyy hh:mm:ss

En esta receta, veremos java.time.format.DateTimeFormatter para


formatear las instancias de fecha y hora en la nueva API de fecha y hora y también
veremos las letras de patrón más utilizadas.

Prepararse
Necesitará un JDK que tenga las nuevas API de fecha / hora, así como la herramienta
jshell.

Cómo hacerlo…
1. Usemos los formatos integrados para formatear la fecha y la hora:

jshell> LocalDate ld = LocalDate.now()


ld ==> 2018-08-01

jshell> ld.format(DateTimeFormatter.ISO_DATE)
$47 ==> "2018-08-01"

jshell> LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
$49 ==> "2018-08-01T17:24:49.1985601"

pág. 575
2. Creemos un formato de fecha / hora personalizado:

jshell> DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd MMM yyyy


hh:mm:ss a")
dtf ==> Value(DayOfMonth,2)' 'Text(MonthOfYear,SHORT)' 'V ... 2)'
'Text(AmPmOfDay,SHORT)

3. Usemos la costumbre java.time.format.DateTimeFormatterpara


formatear la fecha / hora actual:

jshell> LocalDateTime ldt = LocalDateTime.now()


ldt ==> 2018-08-01T17:36:22.442159

jshell> ldt.format(dtf)
$56 ==> "01 Aug 2018 05:36:22 PM"

Cómo funciona…
Comprendamos las letras de formato más utilizadas:

Símbolo Sentido Ejemplo

d día del mes 1,2,3,5

M: 1,2,3
M` MMM`MMMM mes del año MMM: Junio, julio, agosto
MMMM: Julio Agosto

y, yy año y, yyyy: 2017, 2018


yy: 18, 19

h hora del día (1-12) 1, 2, 3

k hora del día (0-23) 0, 1, 2, 3

m minutos 1, 2, 3

s segundos 1, 2, 3

pág. 576
a AM / PM del día AM PM

VV ID de zona horaria Asia / Kolkata

ZZ Nombre de zona horaria IST, PST, AST

O Desplazamiento de zona horaria GMT + 5: 30, GMT + 3

Según las letras de formato anteriores, formateemos java.time.ZonedDateTime:

Pruebas
Este capítulo muestra cómo probar su aplicación: cómo capturar y automatizar la
prueba de casos de uso, cómo probar sus API de forma unitaria antes de que se integren
con otros componentes y cómo integrar todas las unidades. Le presentaremos
el desarrollo impulsado por el comportamiento ( BDD) y muestre cómo puede
convertirse en el punto de partida del desarrollo de su aplicación. También
demostraremos cómo se puede utilizar el marco JUnit para pruebas unitarias. A veces,
durante las pruebas unitarias, tendríamos que bloquear las dependencias con algunos
datos ficticios, y esto se puede hacer burlándose de las dependencias. Le mostraremos
cómo hacer esto usando una biblioteca burlona. También le mostraremos cómo escribir
accesorios para llenar datos de prueba y luego cómo puede probar el comportamiento
de su aplicación integrando diferentes API y probándolas juntas. Cubriremos las
siguientes recetas:

• Pruebas de comportamiento con cucumber


• Prueba unitaria de una API utilizando JUnit
• Pruebas unitarias burlándose de dependencias
• Uso de accesorios para llenar datos para pruebas
• Pruebas de integración

Introducción
El código bien probado proporciona tranquilidad al desarrollador. Si tiene la sensación
de que escribir una prueba para el nuevo método que está desarrollando es una carga
excesiva, generalmente no lo hace bien la primera vez. Debe probar su método de todos
modos y, a la larga, lleva menos tiempo configurar o escribir una prueba unitaria que
pág. 577
compilar e iniciar la aplicación muchas veces, cada vez que el código cambia y para cada
paso lógico.

Una de las razones por las que a menudo nos sentimos presionados por el tiempo es
que no incluimos en nuestras estimaciones el tiempo necesario para redactar el
examen. Una razón es que a veces nos olvidamos de hacerlo. Otra razón es que evitamos
dar una estimación más alta porque no queremos que nos perciban como no lo
suficientemente calificados. Cualquiera sea la razón, sucede. Solo después de años de
experiencia, aprendemos a incluir pruebas en nuestras estimaciones y ganamos
suficiente respeto y poder para poder afirmar públicamente que hacer las cosas bien
requiere más tiempo por adelantado, pero ahorra mucho más tiempo a largo
plazo. Además, hacerlo bien conduce a un código robusto con mucho menos estrés, lo
que significa una mejor calidad de vida en general.

Otra ventaja de probar temprano, antes de que se complete el código principal, es que
las debilidades del código se descubren durante la fase cuando es fácil solucionarlo. Si
es necesario, incluso puede reestructurar el código para una mejor capacidad de
prueba.

Si aún no está convencido, tome nota de la fecha en que lo leyó y vuelva a consultar cada
año hasta que este consejo sea obvio para usted. Luego, comparta sus experiencias con
los demás. Así es como la humanidad progresa: pasando el conocimiento de una
generación a la siguiente.

Metodológicamente, el contenido de este capítulo también es aplicable a otros lenguajes


y profesiones, pero los ejemplos están escritos principalmente para desarrolladores de
Java.

Pruebas de comportamiento con


cucumber
Las siguientes son tres quejas recurrentes de los programadores:

• Falta de requisitos
• Ambigüedad de requisitos
• Los requisitos cambian todo el tiempo

Existen bastantes recomendaciones y procesos que ayudan a aliviar estos problemas,


pero ninguno de ellos pudo eliminarlos por completo. El más exitoso, en nuestra
opinión, fue una metodología de proceso ágil en conjunto con BDD, usando Cucumber
u otro marco similar. Las iteraciones cortas permiten un ajuste y coordinación rápidos
entre empresas (clientes) y programadores, mientras que BDD con Cucumber captura

pág. 578
los requisitos en un lenguaje formal llamado Gherkin, pero sin la sobrecarga de
mantener una extensa documentación.

Los requisitos escritos en Gherkin deben dividirse en características . Cada


característica se almacena en un archivo con una extensión .feature y consta de uno
o más escenarios que describen diferentes aspectos de la característica. Cada
escenario consta de pasos que describen las acciones del usuario o simplemente
ingresan datos y cómo la aplicación responde a ellos.

Un programador implementa la funcionalidad de aplicación necesaria y luego la usa


para implementar los escenarios en uno o varios archivos .java . Cada paso se
implementa en un método.

Después de su implementación, los escenarios se convierten en un conjunto de pruebas


que pueden ser tan precisas como una prueba unitaria o de alto nivel como una prueba
de integración, y cualquier otra cosa. Todo depende de quién escribe el escenario y
cómo se estructura el código de la aplicación. Si los autores de los escenarios son gente
de negocios, los escenarios tienden a ser casos de uso de nivel superior. Pero si la
aplicación está estructurada de manera que cada escenario (con posiblemente
múltiples permutaciones de datos de entrada) se implemente como un método,
entonces sirve efectivamente como una prueba unitaria. Alternativamente, si un
escenario abarca varios métodos o incluso subsistemas, puede servir como una prueba
de integración, mientras que los programadores pueden complementarlo con
escenarios más precisos (más pruebas de unidad). Más tarde, después de entregar el
código, todos los escenarios pueden servir como pruebas de regresión.

El precio que paga es una sobrecarga de los escenarios, el mantenimiento, pero la


recompensa es el sistema formal que captura los requisitos y proporciona una garantía
de que la aplicación hace exactamente lo que se requiere. Dicho esto, una calificación
está en orden: capturar escenarios para la capa de interfaz de usuario suele ser más
problemático porque la interfaz de usuario tiende a cambiar con más frecuencia,
especialmente al comienzo del desarrollo de la aplicación. Sin embargo, tan pronto
como la IU se haya estabilizado, los requisitos también se pueden capturar en
escenarios de Cucumber usando Selenium o un marco similar.

Cómo hacerlo...
1. Instalar cucumber. La instalación de Cucumber no es más que agregar el marco al
proyecto como una dependencia de Maven. Como vamos a agregar varios archivos
JAR de Cucumber y todos deben ser de la misma versión, tiene sentido agregar
la cucumber.version propiedad pom.xml primero:

<properties>
<cucumber.version>3.0.2</cucumber.version>
</properties>

pág. 579
Ahora podemos añadir el principal cucumber archivo JAR en pom.xml como
dependencia:

<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>

Alternativamente, si prefiere un estilo de codificación fluido basado en la transmisión,


puede agregar un archivo JAR principal de Cucumber diferente :

<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>

Si su proyecto aún no tiene JUnit configurado como una dependencia, puede agregarlo
de la siguiente manera junto con otro archivo JAR cucumber-junit :

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>

Lo anterior es necesario si planea aprovechar las afirmaciones de JUnit. Tenga en


cuenta que, al momento de escribir, Cucumber no es compatible con JUnit 5.

Alternativamente, puede usar aserciones de TestNG ( https://testng.org ):

<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-testng</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>

pág. 580
Como puede ver, en este caso, debe agregar el archivo JAR cucumber-
testng en lugar del archivo JAR cucumber-junit . TestNG ofrece una rica
variedad de métodos de afirmación, incluidas colecciones profundas y otras
comparaciones de objetos.

2. Ejecutar cucumber. El archivo JAR también proporciona una anotación que


designa una clase como corredor de prueba: cucumber-junit @RunWith

package com.packt.cookbook.ch16_testing;

import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
public class RunScenariousTest {
}

La ejecución de la clase anterior ejecutará todos los escenarios en el mismo paquete


donde se encuentra el corredor. Cucumber lee cada archivo.feature y los escenarios
en él. Para cada paso de cada escenario, intenta encontrar su implementación en el
mismo paquete que el corredor y el archivo .feature. Ejecuta cada paso
implementado en la secuencia que se enumeran en un escenario.

3. Crea un archivo .feature. Como ya hemos mencionado, un archivo


.feature contiene uno o más escenarios. El nombre del archivo no significa nada para
Cucumber. El contenido del archivo comienza con la palabra clave Feature (con los dos
puntos : después). El siguiente texto describe la función y, similar al nombre de archivo,
no significa nada para Cucumber. La descripción de la función finaliza cuando la palabra
clave Scenario (con los dos puntos : después) comienza una nueva línea. Es entonces
cuando comienza la descripción del primer escenario. Aquí hay un ejemplo:

Feature: Vehicle speed calculation


The calculations should be made based on the assumption
that a vehicle starts moving, and driving conditions are
always the same.

Scenario: Calculate speed


This the happy path that demonstrates the main case

La descripción del escenario termina cuando una de las siguientes palabras clave
comienza una nueva línea: Given, When, Then, And, o But. Cada una de estas
palabras clave, cuando comienza una nueva línea, indica el comienzo de una definición
de paso. Para Cucumber, dicha palabra clave no significa nada excepto el comienzo de
la definición del paso. Pero para los humanos, es más fácil leer si el escenario comienza
con la palabra clave , el paso que describe el estado inicial del sistema, requisito
previo. Pueden seguir varios otros pasos (requisitos previos); cada paso comienza con
una nueva línea y la palabra clave o , por ejemplo, de la siguiente
manera: Given And But

pág. 581
Given the vehicle has 246 hp engine and weighs 4000 pounds

Después de eso, el grupo de pasos describe las acciones o eventos. Para la legibilidad
humana, el grupo generalmente comienza con la palabra clave When en una nueva
línea. Siguen otras acciones o eventos, y cada uno comienza con una nueva línea y
la palabra clave And o But. Se recomienda mantener el número de pasos en este
grupo al mínimo, para que cada escenario esté bien enfocado, por ejemplo, de la
siguiente manera:

Cuando la aplicación calcula su velocidad después de 10.0


segundos

El último grupo de pasos en un escenario comienza con la Then palabra clave en la


nueva línea. Describen los resultados esperados. Como en los dos grupos anteriores
de pasos , cada paso subsiguiente en este grupo comienza con una nueva línea y las
palabras clave And o But también, por ejemplo, como sigue:

Then the result should be 117.0 mph

Para resumir lo anterior, la función tiene el siguiente aspecto:

Feature: Vehicle speed calculation


The calculations should be made based on the assumption
that a vehicle starts moving, and driving conditions are
always the same.

Scenario: Calculate speed


This the happy path that demonstrates the main case

Given the vehicle has 246 hp engine and weighs 4000 pounds
When the application calculates its speed after 10.0 sec
Then the result should be 117.0 mph

Ponemos en el archivo CalculateSpeed.feature en la carpeta


siguiente: src/test/resources/com/packt/cookbook/Chapter14_testin
g.

Tenga en cuenta que debe estar en la carpeta test/resources y la ruta debe


coincidir con el nombre del paquete al que pertenece el corredor de
prueba RunScenariosTest .

El corredor de prueba se ejecuta como cualquier prueba JUnit, utilizando


el comando mvn test , por ejemplo, o simplemente ejecutándolo en JDE. Cuando se
ejecuta, busca todos los archivos.feature en el mismo paquete (Maven los copia de
la carpeta resources a la carpeta target/classes y, por lo tanto, los configura
en el classpath). Luego lee los pasos de cada escenario de forma secuencial e intenta
encontrar la implementación de cada paso en el mismo paquete.

pág. 582
Como ya hemos mencionado, los nombres del archivo no tienen ningún significado para
Cucumber. Primero busca la extensión .feature, luego encuentra el primer paso y,
en el mismo directorio, intenta encontrar una clase que tenga un método anotado con
la misma redacción que el paso.

Para ilustrar lo que significa, ejecutemos la función creada ejecutando el corredor de


prueba. Los resultados serán los siguientes:

cucumber.runtime.junit.UndefinedThrowable:
The step "the vehicle has 246 hp engine and weighs 4000 pounds"
is undefined
cucumber.runtime.junit.UndefinedThrowable:
The step "the application calculates its speed after 10.0 sec"
is undefined
cucumber.runtime.junit.UndefinedThrowable:
The step "the result should be 117.0 mph" is undefined

Undefined scenarios:
com/packt/cookbook/ch16_testing/CalculateSpeed.feature:6
# Calculate speed
1 Scenarios (1 undefined)
3 Steps (3 undefined)
0m0.081s

You can implement missing steps with the snippets below:

@Given("the vehicle has {int} hp engine and weighs {int} pounds")


public void the_vehicle_has_hp_engine_and_weighs_pounds(Integer
int1, Integer int2) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}

@When("the application calculates its speed after {double} sec")


public void the_application_calculates_its_speed_after_sec(Double
double1) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}

@Then("the result should be {double} mph")


public void the_result_should_be_mph(Double double1) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}

Como puede ver, Cucumber no solo nos dice cuáles y cuántas características y
escenarios son undefined, sino que incluso proporciona una forma posible de
implementarlos. Tenga en cuenta que Cucumber permite pasar parámetros utilizando
un tipo entre llaves. Los siguientes son los tipos
predefinidos: int, float, word, string, biginteger, bigdecimal, byte, sho

pág. 583
rt, long, y double. La diferencia entre Word y stringes que este último permite
espacios. Pero Cucumber también nos permite definir tipos personalizados.

4. Escribir y ejecutar definiciones de pasos. El término de


cucumber undefined puede ser confuso porque definimos la característica y los
escenarios. Simplemente no los implementamos. Por lo tanto, undefined en el mensaje
de cucumber en realidad significa not implemented.

Para comenzar la implementación, primero creamos una


clase CalculateSpeedSteps, en el mismo directorio con el corredor de prueba. El
nombre de la clase no tiene significado para Cucumber, por lo que puede nombrarlo de
la otra manera que prefiera. Luego, copiamos los tres métodos sugeridos
previamente con anotaciones y los colocamos en esa clase:

package com.packt.cookbook.ch16_testing;

import cucumber.api.PendingException;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;

public class Calc {


@Given("the vehicle has {int} hp engine and weighs {int} pounds")
public void the_vehicle_has_hp_engine_and_weighs_pounds(Integer
int1, Integer int2) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}

@When("the application calculates its speed after {double} sec")


public void the_application_calculates_its_speed_after_sec(Double
double1) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}

@Then("the result should be {double} mph")


public void the_result_should_be_mph(Double double1) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}
}

If we execute the test runner again, the output will be as follows:


package com.packt.cookbook.ch16_testing;

import cucumber.api.PendingException;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;

pág. 584
public class Calc {
@Given("the vehicle has {int} hp engine and weighs {int} pounds")
public void the_vehicle_has_hp_engine_and_weighs_pounds(Integer
int1, Integer int2) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}

@When("the application calculates its speed after {double} sec")


public void the_application_calculates_its_speed_after_sec(Double
double1) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}

@Then("the result should be {double} mph")


public void the_result_should_be_mph(Double double1) {
// Write code here that turns the phrase above
// into concrete actions
throw new PendingException();
}
}

Si ejecutamos nuevamente el corredor de prueba, el resultado será el siguiente:

cucumber.api.PendingException: TODO: implement me


at com.packt.cookbook.ch16_testing.CalculateSpeedSteps.the_vehicle
_has_hp_engine_and_weighs_pounds(CalculateSpeedSteps.java:13)
at *.the vehicle has 246 hp engine and weighs 4000
pounds(com/packt/cookbook/ch16_testing/CalculateSpeed.feature:9)

Pending scenarios:
com/packt/cookbook/ch16_testing/CalculateSpeed.feature:6
# Calculate speed
1 Scenarios (1 pending)
3 Steps (2 skipped, 1 pending)
0m0.055s

cucumber.api.PendingException: TODO: implement me


at com.packt.cookbook.ch16_testing.CalculateSpeedSteps.the_vehicle
has_hp_engine_and_weighs_pounds(CalculateSpeedSteps.java:13)
at *.the vehicle has 246 hp engine and weighs 4000
pounds(com/packt/cookbook/ch16_testing/CalculateSpeed.feature:9)

El corredor dejó de ejecutar al principio PendingException, por lo que se saltaron


los otros dos pasos. Si la metodología BDD se aplica sistemáticamente, la característica
se escribe primero, antes de escribir cualquier código de la aplicación. Entonces, cada
característica produce el resultado anterior.

A medida que se desarrolla la aplicación, cada nueva característica se implementa y ya


no falla.

pág. 585
Cómo funciona...
Inmediatamente después de que los requisitos se expresen como características, la
aplicación se implementa característica por característica. Por ejemplo, podríamos
comenzar creando la clase Vehicle :

class Vehicle {
private int wp, hp;
public Vehicle(int weightPounds, int hp){
this.wp = weightPounds;
this.hp = hp;
}
protected double getSpeedMpH(double timeSec){
double v = 2.0 * this.hp * 746 ;
v = v*timeSec * 32.174 / this.wp;
return Math.round(Math.sqrt(v) * 0.68);
}
}

Luego, los pasos de la primera característica mostrada anteriormente se pueden


implementar de la siguiente manera:

package com.packt.cookbook.ch16_testing;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import static org.junit.Assert.assertEquals;

public class CalculateSpeedSteps {


private Vehicle vehicle;
private double speed;

@Given("the vehicle has {int} hp engine and weighs {int} pounds")


public void the_vehicle_has_hp_engine_and_weighs_pounds(Integer
wp, Integer hp) {
vehicle = new Vehicle(wp, hp);
}

@When("the application calculates its speed after {double} sec")


public void
the_application_calculates_its_speed_after_sec(Double t) {
speed = vehicle.getSpeedMpH(t);
}

@Then("the result should be {double} mph")


public void the_result_should_be_mph(Double speed) {
assertEquals(speed, this.speed, 0.0001 * speed);
}
}

Si volvemos a ejecutar el corredor de prueba en


el paquete com.packt.cookbook.ch16_testing , los pasos se ejecutarán con
éxito.
pág. 586
Ahora, si los requisitos cambian y el archivo .feature se modifica
correspondientemente, la prueba fallará, a menos que el código de la aplicación
también se cambie y coincida con los requisitos. Ese es el poder de BDD. Mantiene los
requisitos sincronizados con el código. También permite que las pruebas de pepino
sirvan como pruebas de regresión. Si los cambios en el código violan los requisitos, la
prueba falla.

Prueba unitaria de una API


utilizando JUnit
Según Wikipedia, más del 30% de los proyectos alojados en GitHub incluyen JUnit, uno
de una familia de marcos de prueba de unidades conocidos colectivamente como xUnit
que se originaron con SUnit. Está vinculado como un JAR en tiempo de compilación y
reside (desde JUnit 4) en el paquete org.junit .

En la programación orientada a objetos, una unidad puede ser una clase completa pero
podría ser un método individual. Hemos encontrado la última parte, una unidad como
método individual , la más útil en la práctica. Sirve de base para los ejemplos de las
recetas de este capítulo.

Prepararse
Al momento de escribir, la última versión estable de JUnit es 4.12, que se puede usar
agregando la siguiente dependencia de Maven al nivel del proyecto pom.xml:

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

Después de esto, puede escribir su primera prueba JUnit. Supongamos que tiene
la clase Vehicle creada en la carpeta
src/main/java/com/packt/cookbook.ch02_oop.a_classes (este es el
código que discutimos en el Capítulo 2 , Fast Track to OOP - Clases e interfaces ):

package com.packt.cookbook.ch02_oop.a_classes;
public class Vehicle {
private int weightPounds;
private Engine engine;
public Vehicle(int weightPounds, Engine engine) {
this.weightPounds = weightPounds;
if(engine == null){

pág. 587
throw new RuntimeException("Engine value is not set.");
}
this.engine = engine;
}
protected double getSpeedMph(double timeSec){
double v = 2.0*this.engine.getHorsePower()*746;
v = v*timeSec*32.174/this.weightPounds;
return Math.round(Math.sqrt(v)*0.68);
}
}

Ahora puede crear la carpeta


src/test/java/com/packt/cookbook.ch02_oop.a_classes y un nuevo
archivo llamado VehicleTest.java, que contiene la clase VehicleTest:

package com.packt.cookbook.ch02_oop.a_classes;
import org.junit.Test;
public class VehicleTest {
@Test
public void testGetSpeedMph(){
System.out.println("Hello!" + " I am your first test method!");
}
}

Ejecútelo usando su IDE favorito o simplemente con el comando mvn test . Verá
resultados que incluirán lo siguiente:

¡Felicidades! Has creado tu primera clase de prueba. Todavía no prueba nada, pero es
una configuración importante : la sobrecarga necesaria para hacer las cosas de la
manera correcta. En la siguiente sección, comenzaremos con las pruebas reales.

Cómo hacerlo...
Miremos la clase Vehicle más de cerca. Probar los captadores sería de poco valor,
pero aún podemos hacerlo, asegurándonos de que el valor pasado al constructor sea
devuelto por el captador correspondiente. La excepción en el constructor pertenece a
las características de prueba obligatoria, así como al método

pág. 588
getSpeedMph(). También hay un objeto de la clase Engine que tiene
el getHorsePower()método. Puede volver null? Para responder a esta pregunta,
veamos la clase Engine:

public class Engine {


private int horsePower;
public int getHorsePower() {
return horsePower;
}
public void setHorsePower(int horsePower) {
this.horsePower = horsePower;
}
}

El método getHorsePower() no puede regresar null. El campo horsePower se


iniciará en el valor cero de forma predeterminada si
el método setHorsePower() no lo establece explícitamente . Pero devolver un
valor negativo es una posibilidad definitiva, que a su vez puede causar problemas para
la función Math.sqrt()del método getSpeedMph() . ¿Debemos asegurarnos de
que el valor de potencia nunca será negativo? Depende de cuán limitado sea el uso del
método y de la fuente de los datos de entrada.

Consideraciones similares son aplicables al valor del campo weightPounds de la


clase Vehicle . Puede detener la aplicación ArithmeticException causada por
la división por cero en el método getSpeedMph() .

Sin embargo, en la práctica, hay pocas posibilidades de que los valores de la potencia
de un motor y el peso del vehículo sean negativos o cercanos a cero, por lo que
asumiremos esto y no agregaremos estas verificaciones al código.

Tal análisis es la rutina diaria y los pensamientos de fondo de cada desarrollador, y


ese es el primer paso en la dirección correcta. El segundo paso es capturar todos estos
pensamientos y dudas en las pruebas unitarias y verificar los supuestos.

Volvamos a la clase de prueba que hemos creado. Como habrás notado,


la @Test anotación convierte a cierto método en un método de prueba. Esto significa
que será ejecutado por su IDE o Maven cada vez que emita un comando para ejecutar
pruebas. El método se puede nombrar de la forma que desee, pero una práctica
recomendada indica qué método (de la clase Vehicle, en este caso) está
probando. Por lo tanto, el formato generalmente se
ve test<methodname><scenario>, donde scenario indica un caso de prueba
en particular: una ruta feliz, una falla o alguna otra condición que le gustaría
probar. En el primer ejemplo, aunque no usamos el sufijo, solo para mantener el
código simple. Mostraremos ejemplos de métodos que prueban otros escenarios más
adelante.

pág. 589
En una prueba, puede llamar al método de aplicación que está probando,
proporcionarle los datos y afirmar el resultado. Puede crear sus propias afirmaciones
(métodos que comparan los resultados reales con los esperados) o puede usar las
afirmaciones proporcionadas por JUnit. Para hacer esto último, solo agregue
la importación static:

import static org.junit.Assert.assertEquals;

Si usa un IDE moderno, puede escribir import static org.junit.Assert y ver


cuántas aserciones diferentes están disponibles (o ir a la documentación de la API de
JUnit y verlo allí). Hay una docena o más métodos sobrecargados
disponibles: assertArrayEquals(), assertEquals(), assertNotEquals(),
assertNull(), assertNotNull(), assertSame(), assertNotSame(), asse
rtFalse(), assertTrue(), assertThat(), y fail(). Sería útil si pasas unos
minutos leyendo lo que hacen estos métodos. También puedes adivinar su propósito
por su nombre. Aquí hay un ejemplo del uso del método assertEquals() :

import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class VehicleTest {
@Test
public void testGetSpeedMph(){
System.out.println("Hello!" + " I am your first test method!");
assertEquals(4, "Hello".length());
}
}

Comparamos la longitud real de la palabra Hello y la longitud esperada


de 4. Sabemos que sería el número correcto 5, pero nos gustaría que la prueba no
demuestre el comportamiento fallido. Si ejecuta la prueba anterior , obtendrá el
siguiente resultado:

Como puede ver, la última línea le dice qué salió mal: el valor esperado fue 4, mientras
que el real fue 5. Digamos que cambia el orden de los parámetros de esta manera:

assertEquals("Assert Hello length:","Hello".length(), 4);

El resultado será el siguiente:

pág. 590
El último mensaje es engañoso ahora.

Es importante recordar que, en cada uno de los métodos de afirmación, el parámetro con
el valor esperado se ubica (en la firma de una afirmación) antes del real.

Después de escribir la prueba, hará otra cosa y, meses después, probablemente olvidará
lo que cada afirmación realmente evaluó. Pero bien puede ser que algún día la prueba
falle (porque se cambió el código de la aplicación). Verá el nombre del método de
prueba, el valor esperado y el valor real, pero deberá profundizar en el código para
averiguar cuál de las aserciones falló (a menudo hay varias de ellas en cada método de
prueba). Probablemente se verá obligado a agregar una declaración de depuración y
ejecutar la prueba varias veces para resolverlo.

Para ayudarlo a evitar esta excavación adicional, cada una de las aserciones JUnit le
permite agregar un mensaje que describe la aserción particular. Por ejemplo, ejecute
esta versión de la prueba:

public class VehicleTest {


@Test
public void testGetSpeedMph(){
System.out.println("Hello!" + " I am your first test method!");
assertEquals("Assert Hello length:", 4, "Hello".length());
}
}

Si haces esto, el resultado será mucho más legible:

Para completar esta demostración, cambiamos el valor esperado a 5:

assertEquals("Assert Hello length:", 5, "Hello".length());

Ahora los resultados de la prueba no muestran fallas:

pág. 591
Cómo funciona...
Equipado con la comprensión básica del uso del marco JUnit, ahora podemos escribir
un método de prueba real para determinar el caso principal del cálculo de la velocidad
de un vehículo con cierto peso y un motor de cierta potencia. Tomamos la fórmula que
usamos para los cálculos de velocidad y calculamos el valor esperado manualmente
primero. Por ejemplo, si el vehículo tiene un motor de 246 hp y un peso de 4,000 lb, en
10 segundos, su velocidad puede alcanzar las 117 mph. Como la velocidad es
del double tipo, usaremos la aserción con algún delta. De lo contrario,
dos doublevalores pueden nunca ser iguales debido a la forma en double que se
representa un valor en la computadora. Aquí está el método de afirmación de la clase
org.junit.Assert:

void assertEquals(String message, double expected,


double actual, double delta)

El valor delta es precisión permitida. La implementación resultante


del testmétodo tendrá el siguiente aspecto:

@Test
public void testGetSpeedMph(){
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;

Engine engine = new Engine();


engine.setHorsePower(engineHorsePower);

Vehicle vehicle = new Vehicle(vehicleWeightPounds, engine);


double speed = vehicle.getSpeedMph(timeSec);
assertEquals("Assert vehicle (" + engineHorsePower
+ " hp, " + vehicleWeightPounds + " lb) speed in "
+ timeSec + " sec: ", 117, speed, 0.001 * speed);
}

Como puede ver, hemos decidido que una décima parte del uno por ciento del valor es
una precisión suficientemente buena para nuestros propósitos. Si ejecutamos la prueba
anterior, el resultado será el siguiente:

Para asegurarnos de que la prueba esté funcionando, podemos establecer el valor


esperado en 119 mph (más del uno por ciento diferente del real) y ejecutar la prueba
nuevamente. El resultado será el siguiente:

pág. 592
Cambiamos el valor esperado a 117 y continuamos escribiendo otros casos de prueba
que discutimos mientras analizamos el código.

Asegurémonos de que la excepción se produzca cuando se espera. Para hacer eso,


agregamos otra importación:

import static org.junit.Assert.fail;

Luego, podemos escribir el código que prueba el caso cuando el valor pasado en el
constructor de la clase Vehicle es nula (por lo que se debe lanzar la excepción):

@Test
public void testGetSpeedMphException(){
int vehicleWeightPounds = 4000;
Engine engine = null;
try {
Vehicle vehicle = new Vehicle(vehicleWeightPounds, engine);
fail("Exception was not thrown");
} catch (RuntimeException ex) {}
}

Esta prueba se ejecuta con éxito, lo que significa que el constructor Vehicle ha
lanzado una excepción y el código nunca ha llegado a la línea:

fail ( "No se produjo la excepción" ) ;

Para asegurarnos de que la prueba funciona correctamente, pasamos temporalmente


un valor no nulo al Vehicleconstructor:

fail("Exception was not thrown");

Luego, observamos la salida:

De esta manera, obtenemos un nivel de confianza de que nuestra prueba funciona


como se esperaba. Alternativamente, podemos crear otra prueba que falla cuando se
lanza una excepción:

pág. 593
@Test
public void testGetSpeedMphException(){
int vehicleWeightPounds = 4000;
Engine engine = new Engine();
try {
Vehicle vehicle = new Vehicle(vehicleWeightPounds, engine);
} catch (RuntimeException ex) {
fail("Exception was thrown");
}
}

La mejor manera de escribir tales pruebas es en el proceso de escribir el código de la


aplicación, para que pueda probar el código a medida que crece en complejidad. De lo
contrario, especialmente en el código más complejo, es posible que tenga problemas
para depurarlo después de que todo el código ya esté escrito.

Hay bastantes otras anotaciones y características de JUnit que pueden ser útiles para
usted, así que consulte la documentación de JUnit para obtener una comprensión más
profunda de todas las capacidades del marco.

Pruebas unitarias burlándose de


dependencias
Escribir una prueba unitaria requiere controlar todos los datos de entrada. En caso de
que un método reciba su entrada de otros objetos, surge la necesidad de limitar la
profundidad de la prueba para que cada capa se pueda probar de forma aislada como
una unidad. Esto es cuando la necesidad de burlarse del nivel inferior entra en foco.

La burla se puede hacer no solo verticalmente, sino también horizontalmente al mismo


nivel. Si un método es grande y complicado, puede considerar dividirlo en varios
métodos más pequeños para que pueda probar solo uno de ellos mientras se burla de
los demás. Esta es otra ventaja del código de prueba de unidad junto con su
desarrollo; Es más fácil rediseñar el código para una mejor capacidad de prueba en las
primeras etapas de su desarrollo.

Prepararse
Burlarse de otros métodos y clases es sencillo. La codificación de una interfaz (como se
describe en el Capítulo 2 , Fast Track to OOP - Classes and Interfaces ) lo hace mucho más
fácil, aunque existen marcos de imitación que le permiten imitar clases que no
implementan ninguna interfaz (veremos ejemplos de dicho marco uso en la siguiente
sección de esta receta). Además, el uso de fábricas de objetos y métodos le ayuda a crear

pág. 594
implementaciones específicas de prueba de dichas fábricas para que puedan generar
objetos con métodos que devuelvan los valores codificados esperados.

Por ejemplo, en el Capítulo 4 , Going Functional , presentamos FactoryTraffic, que


produjo uno o muchos objetos de TrafficUnit. En un sistema real, esta fábrica
extraería datos de algún sistema externo. Usar el sistema real como fuente podría
complicar la configuración del código. Como puede ver, para evitar este problema, nos
burlamos de los datos al generarlos de acuerdo con la distribución que se parece un
poco al real: un poco más de automóviles que camiones, el peso del vehículo depende
del tipo de automóvil, el número de pasajeros y peso de la carga útil, y similares. Lo
importante para tal simulación es que el rango de valores (mínimo y máximo) debe
reflejar los que provienen del sistema real, por lo que la aplicación se probará en el
rango completo de datos reales posibles.

La restricción importante para el código de burla es que no debería ser demasiado


complicado. De lo contrario, su mantenimiento requeriría una sobrecarga que
disminuiría la productividad del equipo o disminuiría la cobertura de la prueba.

Cómo hacerlo...
La simulación de FactoryTraffic puede ser similar a la siguiente:

public class FactoryTraffic {


public static List<TrafficUnit> generateTraffic(int
trafficUnitsNumber, Month month, DayOfWeek dayOfWeek,
int hour, String country, String city, String trafficLight){
List<TrafficUnit> tms = new ArrayList();
for (int i = 0; i < trafficUnitsNumber; i++) {
TrafficUnit trafficUnit =
FactoryTraffic.getOneUnit(month, dayOfWeek, hour, country,
city, trafficLight);
tms.add(trafficUnit);
}
return tms;
}
}

Reúne una colección de objetos TrafficUnit. En un sistema real, estos objetos se


crearían a partir de las filas del resultado de alguna consulta de base de datos, por
ejemplo. Pero en nuestro caso, simplemente codificamos los valores:

public static TrafficUnit getOneUnit(Month month,


DayOfWeek dayOfWeek, int hour, String country,
String city, String trafficLight) {
double r0 = Math.random();
VehicleType vehicleType = r0 < 0.4 ? VehicleType.CAR :
(r0 > 0.6 ? VehicleType.TRUCK : VehicleType.CAB_CREW);
double r1 = Math.random();
double r2 = Math.random();

pág. 595
double r3 = Math.random();
return new TrafficModelImpl(vehicleType, gen(4,1),
gen(3300,1000), gen(246,100), gen(4000,2000),
(r1 > 0.5 ? RoadCondition.WET : RoadCondition.DRY),
(r2 > 0.5 ? TireCondition.WORN : TireCondition.NEW),
r1 > 0.5 ? ( r3 > 0.5 ? 63 : 50 ) : 63 );
}

Como puede ver, utilizamos un generador de números aleatorios para recoger el valor
de un rango para cada uno de los parámetros. El rango está en línea con los rangos de
los datos reales. Este código es muy simple y no requiere mucho mantenimiento, pero
proporciona a la aplicación un flujo de datos similar al real.

Puedes usar otra técnica. Por ejemplo, volvamos a la clase VechicleTest. En lugar
de crear un objeto real Engine, podemos burlarnos de él usando uno de los marcos
de simulación. En este caso, usamos Mockito. Aquí está la dependencia de Maven para
ello:

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.7.13</version>
<scope>test</scope>
</dependency>

El método de prueba ahora se ve así (las dos líneas que fueron cambiadas están
resaltadas):

@Test
public void testGetSpeedMph(){
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;

Engine engine = Mockito.mock(Engine.class);


Mockito.when(engine.getHorsePower()).thenReturn(engineHorsePower);

Vehicle vehicle = new Vehicle(vehicleWeightPounds, engine);


double speed = vehicle.getSpeedMph(timeSec);
assertEquals("Assert vehicle (" + engineHorsePower
+ " hp, " + vehicleWeightPounds + " lb) speed in "
+ timeSec + " sec: ", 117, speed, 0.001 * speed);
}

Como puede ver, le indicamos al objeto mock que devuelva un valor fijo
cuando getHorsePower()se llama al método. Incluso podemos llegar a crear un
objeto simulado para el método que queremos probar:

Vehicle vehicleMock = Mockito.mock(Vehicle.class);


Mockito.when(vehicleMock.getSpeedMph(10)).thenReturn(30d);

double speed = vehicleMock.getSpeedMph(10);


System.out.println(speed);

pág. 596
Entonces, siempre devuelve el mismo valor:

Sin embargo, esto anularía el propósito de la prueba porque nos gustaría probar el
código que calcula la velocidad, no burlarse de él.

Para probar los métodos de canalización de una secuencia, se puede utilizar otra
técnica. Supongamos que necesitamos probar el método trafficByLane()en
la clase TrafficDensity1 (vamos a
tener TrafficDensity2 y TrafficDensity3 también):

public class TrafficDensity1 {


public Integer[] trafficByLane(Stream<TrafficUnit> stream,
int trafficUnitsNumber, double timeSec,
SpeedModel speedModel, double[] speedLimitByLane) {

int lanesCount = speedLimitByLane.length;

Map<Integer, Integer> trafficByLane = stream


.limit(trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(speedModel))
.map(tuw -> tuw.calcSpeed(timeSec))
.map(speed -> countByLane(lanesCount, speedLimitByLane, speed))
.collect(Collectors.groupingBy(CountByLane::getLane,
Collectors.summingInt(CountByLane::getCount)));

for(int i = 1; i <= lanesCount; i++){


trafficByLane.putIfAbsent(i, 0);
}
return trafficByLane.values()
.toArray(new Integer[lanesCount]);
}

private CountByLane countByLane(int lanesCount,


double[] speedLimit, double speed) {
for(int i = 1; i <= lanesCount; i++){
if(speed <= speedLimit[i - 1]){
return new CountByLane(1, i);
}
}
return new CountByLane(1, lanesCount);
}
}

Utiliza dos clases de soporte:

private class CountByLane{


int count, lane;
private CountByLane(int count, int lane){
this.count = count;
this.lane = lane;

pág. 597
}
public int getLane() { return lane; }
public int getCount() { return count; }
}

También usa lo siguiente:

private static class TrafficUnitWrapper {


private Vehicle vehicle;
private TrafficUnit trafficUnit;
public TrafficUnitWrapper(TrafficUnit trafficUnit){
this.vehicle = FactoryVehicle.build(trafficUnit);
this.trafficUnit = trafficUnit;
}
public TrafficUnitWrapper setSpeedModel(SpeedModel speedModel) {
this.vehicle.setSpeedModel(speedModel);
return this;
}
public double calcSpeed(double timeSec) {
double speed = this.vehicle.getSpeedMph(timeSec);
return Math.round(speed * this.trafficUnit.getTraction());
}
}

Demostramos el uso de tales clases de soporte en el Capítulo


3 , Programación modular , mientras hablamos de transmisiones. Ahora nos damos
cuenta de que probar esta clase puede no ser fácil.

Debido a que el objeto SpeedModel es un parámetro de entrada para el método


trafficByLane() , podríamos probar su método getSpeedMph() de forma
aislada:

@Test
public void testSpeedModel(){
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
double speed = getSpeedModel().getSpeedMph(timeSec,
vehicleWeightPounds, engineHorsePower);
assertEquals("Assert vehicle (" + engineHorsePower
+ " hp, " + vehicleWeightPounds + " lb) speed in "
+ timeSec + " sec: ", 117, speed, 0.001 * speed);
}

private SpeedModel getSpeedModel(){


//FactorySpeedModel possibly
}

Consulte el siguiente código:

public class FactorySpeedModel {


public static SpeedModel generateSpeedModel(TrafficUnit trafficUnit){
return new SpeedModelImpl(trafficUnit);
}
private static class SpeedModelImpl implements SpeedModel{

pág. 598
private TrafficUnit trafficUnit;
private SpeedModelImpl(TrafficUnit trafficUnit){
this.trafficUnit = trafficUnit;
}
public double getSpeedMph(double timeSec,
int weightPounds, int horsePower) {
double traction = trafficUnit.getTraction();
double v = 2.0 * horsePower * 746
* timeSec * 32.174 / weightPounds;
return Math.round(Math.sqrt(v) * 0.68 * traction);
}
}

Como puede ver, la implementación actual de FactorySpeedModel requiere


el objeto TrafficUnit para obtener el valor de tracción. Para solucionar este
problema, podemos modificar el código anterior y eliminar la dependencia
SpeedModel de TrafficUnit. Podemos hacerlo moviendo la aplicación de
tracción al método calcSpeed(). La nueva versión
de FactorySpeedModel puede verse así:

public class FactorySpeedModel {


public static SpeedModel generateSpeedModel(TrafficUnit
trafficUnit) {
return new SpeedModelImpl(trafficUnit);
}
public static SpeedModel getSpeedModel(){
return SpeedModelImpl.getSpeedModel();
}
private static class SpeedModelImpl implements SpeedModel{
private TrafficUnit trafficUnit;
private SpeedModelImpl(TrafficUnit trafficUnit){
this.trafficUnit = trafficUnit;
}
public double getSpeedMph(double timeSec,
int weightPounds, int horsePower) {
double speed = getSpeedModel()
.getSpeedMph(timeSec, weightPounds, horsePower);
return Math.round(speed *trafficUnit.getTraction());
}
public static SpeedModel getSpeedModel(){
return (t, wp, hp) -> {
double weightPower = 2.0 * hp * 746 * 32.174 / wp;
return Math.round(Math.sqrt(t * weightPower) * 0.68);
};
}
}
}

El método de prueba ahora se puede implementar de la siguiente manera:

@Test
public void testSpeedModel(){
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
double speed = FactorySpeedModel.generateSpeedModel()

pág. 599
.getSpeedMph(timeSec, vehicleWeightPounds,
engineHorsePower);
assertEquals("Assert vehicle (" + engineHorsePower
+ " hp, " + vehicleWeightPounds + " lb) speed in "
+ timeSec + " sec: ", 117, speed, 0.001 * speed);
}

Sin embargo, el método calcSpeed()en TrafficUnitWrapper permanece sin


probar. Podríamos probar el método trafficByLane()como un todo:

@Test
public void testTrafficByLane() {
TrafficDensity1 trafficDensity = new TrafficDensity1();
double timeSec = 10.0;
int trafficUnitsNumber = 120;
double[] speedLimitByLane = {30, 50, 65};
Integer[] expectedCountByLane = {30, 30, 60};
Integer[] trafficByLane =
trafficDensity.trafficByLane(getTrafficUnitStream2(
trafficUnitsNumber), trafficUnitsNumber, timeSec,
FactorySpeedModel.getSpeedModel(),speedLimitByLane);
assertArrayEquals("Assert count of "
+ trafficUnitsNumber + " vehicles by "
+ speedLimitByLane.length +" lanes with speed limit "
+ Arrays.stream(speedLimitByLane)
.mapToObj(Double::toString)
.collect(Collectors.joining(", ")),
expectedCountByLane, trafficByLane);
}

Pero esto requeriría crear una secuencia de objetos TrafficUnit con datos fijos:

TrafficUnit getTrafficUnit(int engineHorsePower,


int vehicleWeightPounds) {
return new TrafficUnit() {
@Override
public Vehicle.VehicleType getVehicleType() {
return Vehicle.VehicleType.TRUCK;
}
@Override
public int getHorsePower() {return engineHorsePower;}
@Override
public int getWeightPounds() { return vehicleWeightPounds; }
@Override
public int getPayloadPounds() { return 0; }
@Override
public int getPassengersCount() { return 0; }
@Override
public double getSpeedLimitMph() { return 55; }
@Override
public double getTraction() { return 0.2; }
@Override
public SpeedModel.RoadCondition getRoadCondition(){return null;}
@Override
public SpeedModel.TireCondition getTireCondition(){return null;}
@Override
public int getTemperature() { return 0; }

pág. 600
};
}

Dicha solución no proporciona una variedad de datos de prueba para diferentes tipos
de vehículos y otros parámetros. W e deben revisar el diseño del método
.trafficByLane() .

Cómo funciona...
Si se mira de cerca el método trafficByLane() , se dará cuenta de que el problema
es causado por la ubicación del cálculo en el interior de la clase
privada, TrafficUnitWrapper. Podemos sacarlo de allí y crear un nuevo
método calcSpeed(), en la clase TrafficDensity :

double calcSpeed(double timeSec) {


double speed = this.vehicle.getSpeedMph(timeSec);
return Math.round(speed * this.trafficUnit.getTraction());
}

Luego, podemos cambiar su firma e incluir el objeto Vehicle y el coeficiente


traction como parámetros:

double calcSpeed(Vehicle vehicle, double traction, double timeSec){


double speed = vehicle.getSpeedMph(timeSec);
return Math.round(speed * traction);
}

Agreguemos también dos métodos a la TrafficUnitWrapper clase (verá en un


momento por qué los necesitamos):

public Vehicle getVehicle() { return vehicle; }


public double getTraction() { return trafficUnit.getTraction(); }

Los cambios anteriores nos permiten reescribir la canalización de flujo de la siguiente


manera (la línea cambiada está en negrita):

Map<Integer, Integer> trafficByLane = stream


.limit(trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(speedModel))
.map(tuw -> calcSpeed(tuw.getVehicle(), tuw.getTraction(), timeSec))
.map(speed -> countByLane(lanesCount, speedLimitByLane, speed))
.collect(Collectors.groupingBy(CountByLane::getLane,
Collectors.summingInt(CountByLane::getCount)));

Al proteger el método calcSpeed()y asumir que la clase Vehicle se prueba en


su propia clase de prueba, VehicleTest ahora podemos escribir
el método testCalcSpeed() :

pág. 601
@Test
public void testCalcSpeed(){
double timeSec = 10.0;
TrafficDensity2 trafficDensity = new TrafficDensity2();

Vehicle vehicle = Mockito.mock(Vehicle.class);


Mockito.when(vehicle.getSpeedMph(timeSec)).thenReturn(100d);
double traction = 0.2;
double speed = trafficDensity.calcSpeed(vehicle, traction, timeSec);
assertEquals("Assert speed (traction=" + traction + ") in "
+ timeSec + " sec: ",20,speed,0.001 *speed);
}

La funcionalidad restante se puede probar burlándose del método calcSpeed():

@Test
public void testCountByLane() {
int[] count ={0};
double[] speeds =
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
TrafficDensity2 trafficDensity = new TrafficDensity2() {
@Override
protected double calcSpeed(Vehicle vehicle,
double traction, double timeSec) {
return speeds[count[0]++];
}
};
double timeSec = 10.0;
int trafficUnitsNumber = speeds.length;

double[] speedLimitByLane = {4.5, 8.5, 12.5};


Integer[] expectedCountByLane = {4, 4, 4};

Integer[] trafficByLane = trafficDensity.trafficByLane(


getTrafficUnitStream(trafficUnitsNumber),
trafficUnitsNumber, timeSec, FactorySpeedModel.getSpeedModel(),
speedLimitByLane );
assertArrayEquals("Assert count of " + speeds.length
+ " vehicles by " + speedLimitByLane.length
+ " lanes with speed limit "
+ Arrays.stream(speedLimitByLane)
.mapToObj(Double::toString).collect(Collectors
.joining(", ")), expectedCountByLane, trafficByLane);
}

Hay más...
Esta experiencia nos ha hecho conscientes de que el uso de una clase privada interna
puede hacer que la funcionalidad no sea comprobable de forma aislada. Vamos a tratar
de deshacerse de la clase private, CountByLane. Esto nos lleva a la tercera versión
de la clase TrafficDensity3 (se resalta el código modificado):

Integer[] trafficByLane(Stream<TrafficUnit> stream,


int trafficUnitsNumber, double timeSec,

pág. 602
SpeedModel speedModel, double[] speedLimitByLane) {
int lanesCount = speedLimitByLane.length;
Map<Integer, Integer> trafficByLane = new HashMap<>();
for(int i = 1; i <= lanesCount; i++){
trafficByLane.put(i, 0);
}
stream.limit(trafficUnitsNumber)
.map(TrafficUnitWrapper::new)
.map(tuw -> tuw.setSpeedModel(speedModel))
.map(tuw -> calcSpeed(tuw.getVehicle(), tuw.getTraction(),
timeSec))
.forEach(speed -> trafficByLane.computeIfPresent(
calcLaneNumber(lanesCount,
speedLimitByLane, speed), (k, v) -> ++v));
return trafficByLane.values().toArray(new Integer[lanesCount]);
}
protected int calcLaneNumber(int lanesCount,
double[] speedLimitByLane, double speed) {
for(int i = 1; i <= lanesCount; i++){
if(speed <= speedLimitByLane[i - 1]){
return i;
}
}
return lanesCount;
}

Este cambio nos permite extender la clase en nuestra prueba:

class TrafficDensityTestCalcLaneNumber extends TrafficDensity3 {


protected int calcLaneNumber(int lanesCount,
double[] speedLimitByLane, double speed){
return super.calcLaneNumber(lanesCount,
speedLimitByLane, speed);
}
}

También nos permite cambiar el método calcLaneNumber() de prueba de


forma aislada:

@Test
public void testCalcLaneNumber() {
double[] speeds = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
double[] speedLimitByLane = {4.5, 8.5, 12.5};
int[] expectedLaneNumber = {1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3};

TrafficDensityTestCalcLaneNumber trafficDensity =
new TrafficDensityTestCalcLaneNumber();
for(int i = 0; i < speeds.length; i++){
int ln = trafficDensity.calcLaneNumber(
speedLimitByLane.length,
speedLimitByLane, speeds[i]);
assertEquals("Assert lane number of speed "
+ speeds + " with speed limit "
+ Arrays.stream(speedLimitByLane)
.mapToObj(Double::toString).collect(
Collectors.joining(", ")),
expectedLaneNumber[i], ln);

pág. 603
}
}

Uso de accesorios para llenar


datos para pruebas
En aplicaciones más complejas (que usan una base de datos, por ejemplo), a menudo
existe la necesidad de configurar los datos antes de cada prueba y limpiarlos después
de que se complete la prueba. Algunas partes de los datos deben establecerse antes de
cada método de prueba y / o limpiarse después de que se haya completado cada método
de prueba. Es posible que sea necesario configurar otros datos antes de ejecutar y / o
limpiar cualquier método de prueba de la clase de prueba después de que se haya
completado el último método de prueba de la clase de prueba.

Cómo hacerlo...
Para lograr esto, agregue una anotación @Before delante de él, lo que indica que
este método debe ejecutarse antes de cada método de prueba. El método de limpieza
correspondiente se identifica mediante la anotación @After . Del mismo modo, los
métodos de configuración a nivel de clase están anotados
por @BeforeClass y @AfterClass, lo que significa que estos métodos de
configuración se ejecutarán solo una vez, antes de que se ejecute cualquier método de
prueba de esta clase ( @BeforeClass) y después de que se haya ejecutado el último
método de prueba de esta clase ( @AfterClass) Aquí hay una demostración rápida
de esto:

public class DatabaseRelatedTest {


@BeforeClass
public static void setupForTheClass(){
System.out.println("setupForTheClass() is called");
}
@AfterClass
public static void cleanUpAfterTheClass(){
System.out.println("cleanAfterClass() is called");
}
@Before
public void setupForEachMethod(){
System.out.println("setupForEachMethod() is called");
}
@After
public void cleanUpAfterEachMethod(){
System.out.println("cleanAfterEachMethod() is called");
}
@Test
public void testMethodOne(){
System.out.println("testMethodOne() is called");

pág. 604
}
@Test
public void testMethodTwo(){
System.out.println("testMethodTwo() is called");
}
}

Si ejecuta las pruebas ahora, obtendrá el siguiente resultado:

Dichos métodos que arreglan el contexto de prueba se denominan fixtures . Tenga en


cuenta que tienen que ser públicos, y los accesorios de configuración / limpieza a nivel
de clase deben ser estáticos. Sin embargo, la próxima versión 5 de JUnit planea levantar
estas limitaciones.

Cómo funciona...
Un ejemplo típico de tal uso sería crear tablas necesarias antes de ejecutar el primer
método de prueba y eliminarlas después de que finalice el último método de la clase de
prueba. Los métodos de configuración / limpieza también se pueden usar para crear /
cerrar una conexión de base de datos a menos que su código lo haga en la construcción
de prueba con recursos (consulte el Capítulo 11 , Administración de memoria y
depuración ).

Aquí hay un ejemplo del uso de accesorios (consulte el Capítulo 6 , Programación de la


base de datos , para obtener más información sobre cómo configurar una base de datos
para ejecutarla, sección). Supongamos que necesitamos probar
la clase DbRelatedMethods:

class DbRelatedMethods{
public void updateAllTextRecordsTo(String text){
executeUpdate("update text set text = ?", text);
}
private void executeUpdate(String sql, String text){
try (Connection conn = getDbConnection();
PreparedStatement st = conn.prepareStatement(sql)){
st.setString(1, text);
st.executeUpdate();

pág. 605
} catch (Exception ex) {
ex.printStackTrace();
}
}
private Connection getDbConnection(){
//... code that creates DB connection
}
}

Nos gustaría asegurarnos de que el método


anterior updateAllTextRecordsTo()siempre actualice todos los registros de
la texttabla con el valor proporcionado. Nuestra primera prueba, , es actualizar un
registro existente: updateAllTextRecordsTo1()

@Test
public void updateAllTextRecordsTo1(){
System.out.println("updateAllTextRecordsTo1() is called");
String testString = "Whatever";
System.out.println(" Update all records to " + testString);
dbRelatedMethods.updateAllTextRecordsTo(testString);
int count = countRecordsWithText(testString);
assertEquals("Assert number of records with "
+ testString + ": ", 1, count);
System.out.println("All records are updated to " + testString);
}

Esto significa que la tabla tiene que existir en la base de datos de prueba y debe tener
un registro en ella.

Nuestra segunda prueba, , se asegura de que dos registros se actualizan incluso si


cada registro contiene un valor diferente: updateAllTextRecordsTo2()

@Test
public void updateAllTextRecordsTo2(){
System.out.println("updateAllTextRecordsTo2() is called");
String testString = "Unexpected";
System.out.println("Update all records to " + testString);
dbRelatedMethods.updateAllTextRecordsTo(testString);
executeUpdate("insert into text(id,text) values(2, ?)","Text 01");

testString = "Whatever";
System.out.println("Update all records to " + testString);
dbRelatedMethods.updateAllTextRecordsTo(testString);
int count = countRecordsWithText(testString);
assertEquals("Assert number of records with "
+ testString + ": ", 2, count);
System.out.println(" " + count + " records are updated to " +
testString);
}

Tanto las anteriores pruebas utilizan la misma tabla, es decir, text. Por lo tanto, no
es necesario abandonar la tabla después de cada prueba. Es por eso que lo creamos y
lo soltamos a nivel de clase:

pág. 606
@BeforeClass
public static void setupForTheClass(){
System.out.println("setupForTheClass() is called");
execute("create table text (id integer not null,
text character varying not null)");
}
@AfterClass
public static void cleanUpAfterTheClass(){
System.out.println("cleanAfterClass() is called");
execute("drop table text");
}

Esto significa que todo lo que tenemos que hacer es llenar la tabla antes de cada
prueba y limpiarla después de completar cada prueba:

@Before
public void setupForEachMethod(){
System.out.println("setupForEachMethod() is called");
executeUpdate("insert into text(id, text) values(1,?)", "Text 01");
}
@After
public void cleanUpAfterEachMethod(){
System.out.println("cleanAfterEachMethod() is called");
execute("delete from text");
}

Además, dado que podemos usar el mismo objeto, dbRelatedMethods para todas
las pruebas, creémoslo también en el nivel de clase (como propiedad de la clase de
prueba), por lo que se crea solo una vez:

private DbRelatedMethods dbRelatedMethods = new DbRelatedMethods();

Si ejecutamos todas las pruebas de la clase test ahora, la salida se verá así:

pág. 607
Los mensajes impresos le permiten rastrear la secuencia de todas las llamadas a
métodos y ver que se ejecutan como se esperaba.

Pruebas de integración
Si ha leído todos los capítulos y ha mirado los ejemplos de código, puede haber notado
que, a estas alturas, hemos discutido y construido todos los componentes necesarios
para una aplicación distribuida típica. Es el momento de juntar todos los componentes
y ver si cooperan como se esperaba. Este proceso se llama integración .

Al hacerlo, analizaremos detenidamente si la aplicación se comporta de acuerdo con los


requisitos. En los casos en que los requisitos funcionales se presentan en forma
ejecutable (utilizando el marco de Cucumber, por ejemplo), podemos ejecutarlos y
verificar si todos los controles pasan. Muchas compañías de software siguen un proceso
de desarrollo basado en el comportamiento y realizan pruebas muy temprano, a veces
incluso antes de que se escriba una cantidad sustancial de código (tales pruebas fallan,
por supuesto, pero tienen éxito tan pronto como se implementa la funcionalidad
esperada). Como ya se mencionó, las pruebas iniciales pueden ser muy útiles para
escribir un código enfocado, claro y bien verificable.

Sin embargo, incluso sin una estricta adherencia al proceso de prueba primero , la fase
de integración naturalmente también incluye algún tipo de prueba de

pág. 608
comportamiento. En esta receta, veremos varios enfoques posibles y ejemplos
específicos relacionados con esto.

Prepararse
Es posible que haya notado que, en el curso de este libro, hemos creado varias clases
que componen una aplicación que analiza y modela el tráfico. Para su comodidad, los
hemos incluido todos en el paquete com.packt.cookbook.ch16_testing .

De los capítulos anteriores, y usted ya están familiarizados con los cinco interfaces de
la apicarpeta- Car, SpeedModel, TrafficUnit, Truck, y Vehicle. Sus
implementaciones están encapsulados dentro de las clases denominadas fábricas en
la carpeta con el mismo nombre: FactorySpeedModel, FactoryTraffic,
y FactoryVehicle. Estas fábricas producen información para la funcionalidad de
la AverageSpeed clase ( Capítulo 7 , Programación concurrente y multiproceso )
y la TrafficDensity clase (basada en el Capítulo 5 , Streams y Pipelines, pero
creada y discutida en este capítulo) .Las clases principales de nuestra aplicación de
demostración. En primer lugar, producen los valores que motivaron el desarrollo de
esta aplicación en particular.

La funcionalidad principal de la aplicación es sencilla. Para un número determinado de


carriles y límite de velocidad para cada carril, AverageSpeed calcula (estima) la
velocidad real de cada carril (suponiendo que todos los conductores se comporten
racionalmente, tomando el carril de acuerdo con su velocidad),
mientras TrafficDensity calcula el número de vehículos en cada carril. después
de 10 segundos (suponiendo que todos los autos arranquen al mismo tiempo después
del semáforo). Los cálculos se realizan en función de los datos del vehículo
numberOfTrafficUnits recogidos en un lugar y época del año determinados. No
significa que todos los 1,000 vehículos estaban corriendo al mismo tiempo. Estos 1,000
puntos de medición se han recolectado durante 50 años para aproximadamente 20
vehículos que circulaban en la intersección especificada durante la hora especificada
(lo que significa un vehículo cada tres minutos, en promedio).

La infraestructura global de la aplicación se apoya en las clases de la


carpeta process: Dispatcher, Processor, y Subscription. Discutimos su
funcionalidad y los demostramos en el Capítulo 7, Programación concurrente y
multiproceso . Estas clases permiten distribuir el procesamiento.

La Dispatcher clase envía una solicitud de procesamiento a la población


de Processorsun grupo, utilizando
la clase Subscription . Cada Processor clase realiza la tarea de acuerdo con la
solicitud (usando las clases AverageSpeed y ) y almacena los resultados en la base
de datos (usando la clase en la carpeta, en función de la funcionalidad discutida en

pág. 609
el Capítulo 6 , Programación de la base de
datos ).TraffciDensityDbUtilutils

Hemos probado la mayoría de estas clases como unidades. Ahora vamos a integrarlos
y probar la aplicación en su conjunto para ver si tiene un comportamiento correcto.

Los requisitos se hicieron solo con fines demostrativos. El objetivo de la demostración


era mostrar algo bien motivado (parecido a datos reales) y al mismo tiempo lo
suficientemente simple como para comprenderlo sin un conocimiento especial de
análisis y modelado de tráfico.

Cómo hacerlo...
Hay varios niveles de integración. Necesitamos integrar las clases y subsistemas de la
aplicación y también integrar nuestra aplicación con el sistema externo (la fuente de los
datos de tráfico desarrollados y mantenidos por un tercero).

Aquí hay un ejemplo de integración a nivel de clase utilizando


el método demo1_class_level_integration()en la clase
Chapter14Testing:

String result = IntStream.rangeClosed(1,


speedLimitByLane.length).mapToDouble(i -> {
AverageSpeed averageSpeed =
new AverageSpeed(trafficUnitsNumber, timeSec,
dateLocation, speedLimitByLane, i,100);
ForkJoinPool commonPool = ForkJoinPool.commonPool();
return commonPool.invoke(averageSpeed);
}).mapToObj(Double::toString).collect(Collectors.joining(", "));
System.out.println("Average speed = " + result);

TrafficDensity trafficDensity = new TrafficDensity();


Integer[] trafficByLane =
trafficDensity.trafficByLane(trafficUnitsNumber,
timeSec, dateLocation, speedLimitByLane );
System.out.println("Traffic density = "+Arrays.stream(trafficByLane)
.map(Object::toString)
.collect(Collectors.joining(", ")));

En este ejemplo, integramos cada una de las dos clases principales, a


saber , AverageSpeed y TrafficDensity, con fábricas e implementaciones de
sus interfaces.

Los resultados son los siguientes:

pág. 610
Tenga en cuenta que los resultados son ligeramente diferentes de una ejecución a
otra. Esto se debe a que los datos producidos por FactoryTraffic varían de una
solicitud a otra. Pero, en esta etapa, solo tenemos que asegurarnos de que todo funcione
en conjunto y produzca resultados más o menos precisos. Hemos probado el código por
unidades y tenemos un nivel de confianza de que cada unidad está haciendo lo que se
supone que debe hacer. Volveremos a la validación de los resultados durante
el proceso de prueba de integración real , no durante la integración.

Después de finalizar la integración a nivel de clase, vea cómo los subsistemas


funcionan juntos usando
el método demo1_subsystem_level_integration() en la clase Chapter1
4Testing :

DbUtil.createResultTable();
Dispatcher.dispatch(trafficUnitsNumber, timeSec, dateLocation,
speedLimitByLane);
try { Thread.sleep(2000L); }
catch (InterruptedException ex) {}
Arrays.stream(Process.values()).forEach(v -> {
System.out.println("Result " + v.name() + ": "
+ DbUtil.selectResult(v.name()));
});

En este código, solíamos crear la tabla DBUtil necesaria que contiene los datos de
entrada y los resultados producidos y registrados por Processor. La clase
Dispatcher envía una solicitud e ingresa datos a los objetos de la clase
Processor , como se muestra aquí:

void dispatch(int trafficUnitsNumber, double timeSec,


DateLocation dateLocation, double[] speedLimitByLane) {
ExecutorService execService = ForkJoinPool.commonPool();
try (SubmissionPublisher<Integer> publisher =
new SubmissionPublisher<>()){
subscribe(publisher, execService,Process.AVERAGE_SPEED,
timeSec, dateLocation, speedLimitByLane);
subscribe(publisher,execService,Process.TRAFFIC_DENSITY,
timeSec, dateLocation, speedLimitByLane);
publisher.submit(trafficUnitsNumber);
} finally {
try {
execService.shutdown();
execService.awaitTermination(1, TimeUnit.SECONDS);
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
} finally {
execService.shutdownNow();
}
}
}

pág. 611
La clase Subscription se usa para enviar / recibir el mensaje ( consulte
el Capítulo 7 , Programación concurrente y multiproceso , para obtener una
descripción de esta funcionalidad):

void subscribe(SubmissionPublisher<Integer> publisher,


ExecutorService execService, Process process,
double timeSec, DateLocation dateLocation,
double[] speedLimitByLane) {
Processor<Integer> subscriber = new Processor<>(process, timeSec,
dateLocation, speedLimitByLane);
Subscription subscription =
new Subscription(subscriber, execService);
subscriber.onSubscribe(subscription);
publisher.subscribe(subscriber);
}

Los procesadores están haciendo su trabajo; solo tenemos que esperar unos segundos
(puede ajustar este tiempo si la computadora que está usando requiere más tiempo
para finalizar el trabajo) antes de obtener los resultados. Usamos DBUtilpara leer los
resultados de la base de datos:

Los nombres de la clase Process enum apuntan a los registros correspondientes en


la tabla result de la base de datos. Una vez más, en esta etapa, estamos buscando
principalmente obtener algún resultado, no lo correcto que sean los valores.

Después de la integración exitosa entre los subsistemas de nuestra aplicación en


función de los datos generados FactoryTraffic, podemos intentar conectarnos al
sistema externo que proporciona datos de tráfico reales. En el
interior FactoryTraffic, ahora pasaríamos de generar objetos TrafficUnit a
obtener datos de un sistema real:

public class FactoryTraffic {


private static boolean switchToRealData = true;
public static Stream<TrafficUnit>
getTrafficUnitStream(DateLocation dl, int trafficUnitsNumber){
if(switchToRealData){
return getRealData(dL, trafficUnitsNumber);
} else {
return IntStream.range(0, trafficUnitsNumber)
.mapToObj(i -> generateOneUnit());
}
}

private static Stream<TrafficUnit>


getRealData(DateLocation dl, int trafficUnitsNumber) {
//connect to the source of the real data
// and request the flow or collection of data
return new ArrayList<TrafficUnit>().stream();

pág. 612
}
}

El conmutador se puede implementar como una propiedad Boolean en la clase (como


se ve en el código anterior) o la propiedad de configuración del proyecto. Dejamos de
lado los detalles de la conexión a una fuente particular de datos de tráfico real, ya que
esto no es relevante para el propósito de este libro.

El enfoque principal en esta etapa tiene que ser el rendimiento y tener un flujo de datos
fluido entre la fuente externa de datos reales y nuestra aplicación. Después de
asegurarnos de que todo funciona y produce resultados realistas con un rendimiento
satisfactorio, podemos pasar a las pruebas de integración con la afirmación de los
resultados reales.

Cómo funciona...
Para las pruebas, necesitamos establecer los valores esperados, que podemos comparar
con los valores reales producidos por la aplicación que procesa datos reales. Pero los
datos reales cambian ligeramente de una ejecución a otra, y un intento de predecir los
valores resultantes hace que la prueba sea frágil o obliga a la introducción de un gran
margen de error, que puede vencer efectivamente el propósito de la prueba.

Ni siquiera podemos burlarnos de los datos generados (como lo hicimos en el caso de


las pruebas unitarias) porque estamos en la etapa de integración y tenemos que usar
los datos reales.

Una posible solución sería almacenar los datos reales entrantes y el resultado que
nuestra aplicación produjo en la base de datos. Luego, un especialista de dominio puede
recorrer cada registro y afirmar si los resultados son los esperados.

Para lograr esto, introdujimos un interruptor boolean en la clase


TrafficDensity , por lo que registra la entrada junto con cada unidad de los
cálculos:

public class TrafficDensity {


public static Connection conn;
public static boolean recordData = false;
//...
private double calcSpeed(TrafficUnitWrapper tuw, double timeSec){
double speed = calcSpeed(tuw.getVehicle(),
tuw.getTrafficUnit().getTraction(), timeSec);
if(recordData) {
DbUtil.recordData(conn, tuw.getTrafficUnit(), speed);
}
return speed;
}
//...
}

pág. 613
También introdujimos una propiedad estática para mantener la misma conexión de
base de datos en todas las instancias de clase. De lo contrario, el grupo de conexiones
debería ser muy grande porque, como recordará deCapítulo 7 , Programación
concurrente y multiproceso , el número de trabajadores que ejecutan la tarea en paralelo
aumenta a medida que aumenta la cantidad de trabajo por hacer.

Si nos fijamos en DbUtils, verá un nuevo método que crea la data tabla diseñada
para contener TrafficUnits procedentes de FactoryTraffic y la tabla
data_common que mantiene los principales parámetros utilizados para solicitudes y
cálculos de datos: solicitada número de unidades de tráfico, la fecha y la
geolocalización de los datos de tráfico, el tiempo en segundos (el punto en el que se
calcula la velocidad) y el límite de velocidad para cada carril (su tamaño define cuántos
carriles planeamos usar al modelar el tráfico). Aquí está el código que configuramos
para hacer la grabación:

private static void demo3_prepare_for_integration_testing(){


DbUtil.createResultTable();
DbUtil.createDataTables();
TrafficDensity.recordData = true;
try(Connection conn = DbUtil.getDbConnection()){
TrafficDensity.conn = conn;
Dispatcher.dispatch(trafficUnitsNumber, timeSec,
dateLocation, speedLimitByLane);
} catch (SQLException ex){
ex.printStackTrace();
}
}

Una vez completada la grabación, podemos entregar los datos a un especialista de


dominio que puede afirmar la exactitud del comportamiento de la aplicación.

Los datos verificados ahora se pueden usar para pruebas de integración. Podemos
agregar otro interruptor FactoryTrafficUnit y forzarlo a leer los datos grabados
en lugar de los datos reales impredecibles:

public class FactoryTraffic {


public static boolean readDataFromDb = false;
private static boolean switchToRealData = false;
public static Stream<TrafficUnit>
getTrafficUnitStream(DateLocation dl, int trafficUnitsNumber){
if(readDataFromDb){
if(!DbUtil.isEnoughData(trafficUnitsNumber)){
System.out.println("Not enough data");
return new ArrayList<TrafficUnit>().stream();
}
return readDataFromDb(trafficUnitsNumber);
}
//....
}

pág. 614
Como habrás notado, también hemos agregado el método isEnoughData() que
verifica si hay suficientes datos grabados:

public static boolean isEnoughData(int trafficUnitsNumber){


try (Connection conn = getDbConnection();
PreparedStatement st =
conn.prepareStatement("select count(*) from data")){
ResultSet rs = st.executeQuery();
if(rs.next()){
int count = rs.getInt(1);
return count >= trafficUnitsNumber;
}
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}

Esto ayudará a evitar la frustración innecesaria de depurar el problema de la prueba,


especialmente en el caso de probar un sistema más complejo.

Ahora controlamos no solo los valores de entrada sino también los resultados
esperados que podemos usar para afirmar el comportamiento de la aplicación. Ambos
están ahora incluidos en el objeto TrafficUnit. Para poder hacer esto,
aprovechamos la nueva característica de interfaz Java que se discutió en el Capítulo
2 , Fast Track to OOP - Clases e interfaces , que es el método predeterminado de la
interfaz:

public interface TrafficUnit {


VehicleType getVehicleType();
int getHorsePower();
int getWeightPounds();
int getPayloadPounds();
int getPassengersCount();
double getSpeedLimitMph();
double getTraction();
RoadCondition getRoadCondition();
TireCondition getTireCondition();
int getTemperature();
default double getSpeed(){ return 0.0; }
}

Entonces, podemos adjuntar el resultado a los datos de entrada. Ver el siguiente


método:

List<TrafficUnit> selectData(int trafficUnitsNumber){...}

Podemos adjuntar el resultado a la clase DbUtil y también a la clase


interna TrafficUnitImpl DbUtil:

class TrafficUnitImpl implements TrafficUnit{


private int horsePower, weightPounds, payloadPounds,
passengersCount, temperature;

pág. 615
private Vehicle.VehicleType vehicleType;
private double speedLimitMph, traction, speed;
private RoadCondition roadCondition;
private TireCondition tireCondition;
...
public double getSpeed() { return speed; }
}

Y también podemos adjuntarlo dentro de la clase DbUtil.

Los cambios anteriores nos permiten escribir una prueba de integración. Primero,
probaremos el modelo de velocidad utilizando los datos registrados:

void demo1_test_speed_model_with_real_data(){
double timeSec = DbUtil.getTimeSecFromDataCommon();
FactoryTraffic.readDataFromDb = true;
TrafficDensity trafficDensity = new TrafficDensity();
FactoryTraffic.
getTrafficUnitStream(dateLocation,1000).forEach(tu -> {
Vehicle vehicle = FactoryVehicle.build(tu);
vehicle.setSpeedModel(FactorySpeedModel.getSpeedModel());
double speed = trafficDensity.calcSpeed(vehicle,
tu.getTraction(), timeSec);
assertEquals("Assert vehicle (" + tu.getHorsePower()
+ " hp, " + tu.getWeightPounds() + " lb) speed in "
+ timeSec + " sec: ", tu.getSpeed(), speed,
speed * 0.001);
});
}

Se puede escribir una prueba similar para probar el cálculo de velocidad de la clase
AverageSpeed con datos reales.

Luego, podemos escribir una prueba de integración para el nivel de clase:

private static void demo2_class_level_integration_test() {


FactoryTraffic.readDataFromDb = true;
String result = IntStream.rangeClosed(1,
speedLimitByLane.length).mapToDouble(i -> {
AverageSpeed averageSpeed = new AverageSpeed(trafficUnitsNumber,
timeSec, dateLocation, speedLimitByLane, i,100);
ForkJoinPool commonPool = ForkJoinPool.commonPool();
return commonPool.invoke(averageSpeed);
}).mapToObj(Double::toString).collect(Collectors.joining(", "));
String expectedResult = "7.0, 23.0, 41.0";
String limits = Arrays.stream(speedLimitByLane)
.mapToObj(Double::toString)
.collect(Collectors.joining(", "));
assertEquals("Assert average speeds by "
+ speedLimitByLane.length
+ " lanes with speed limit "
+ limits, expectedResult, result);

pág. 616
También se puede escribir un código similar para las pruebas de nivel
de clase TrafficDensity:

TrafficDensity trafficDensity = new TrafficDensity();


String result = Arrays.stream(trafficDensity.
trafficByLane(trafficUnitsNumber, timeSec,
dateLocation, speedLimitByLane))
.map(Object::toString)
.collect(Collectors.joining(", "));
expectedResult = "354, 335, 311";
assertEquals("Assert vehicle count by " + speedLimitByLane.length +
" lanes with speed limit " + limits, expectedResult, result);

Finalmente, también podemos escribir la prueba de integración para el nivel de


subsistema:

void demo3_subsystem_level_integration_test() {
FactoryTraffic.readDataFromDb = true;
DbUtil.createResultTable();
Dispatcher.dispatch(trafficUnitsNumber, 10, dateLocation,
speedLimitByLane);
try { Thread.sleep(3000l); }
catch (InterruptedException ex) {}
String result = DbUtil.selectResult(Process.AVERAGE_SPEED.name());
String expectedResult = "7.0, 23.0, 41.0";
String limits = Arrays.stream(speedLimitByLane)
.mapToObj(Double::toString)
.collect(Collectors.joining(", "));
assertEquals("Assert average speeds by " + speedLimitByLane.length
+ " lanes with speed limit " + limits, expectedResult, result);
result = DbUtil.selectResult(Process.TRAFFIC_DENSITY.name());
expectedResult = "354, 335, 311";
assertEquals("Assert vehicle count by " + speedLimitByLane.length
+ " lanes with speed limit " + limits, expectedResult, result);
}

Todas las pruebas anteriores se ejecutan con éxito ahora y pueden usarse para pruebas
de regresión de aplicaciones en cualquier momento posterior.

Se puede crear una prueba de integración automatizada entre nuestra aplicación y la


fuente de los datos de tráfico reales solo si esta última tiene un modo de prueba desde
donde se puede enviar el mismo flujo de datos para que podamos usarlo de la misma
manera que usamos datos (que es esencialmente lo mismo).

Una idea de despedida: todas estas pruebas de integración son posibles cuando la
cantidad de datos de procesamiento es estadísticamente significativa. Esto se debe a
que no tenemos control total sobre el número de trabajadores y cómo la JVM decide
dividir la carga. Es muy posible que, en una ocasión particular, el código demostrado en
este capítulo no funcione. En tal caso, intente aumentar el número de unidades de
tráfico solicitadas. Esto asegurará más espacio para la lógica de distribución de carga.

pág. 617
La nueva forma de codificación
con Java 10 y Java 11
En este capítulo, cubriremos las siguientes recetas:

• Uso de inferencia de tipo variable local


• Uso de sintaxis de variable local para parámetros lambda

Introducción
Este capítulo le ofrece una introducción rápida a las nuevas funciones que afectan su
codificación. Muchos otros lenguajes, incluido JavaScript, tienen esta característica: la
capacidad de declarar una variable usando una palabra clave var (en Java, en realidad
es un nombre de tipo reservado , no una palabra clave). Tiene muchas ventajas, pero
no está exento de controversia. Si se usa en exceso, especialmente con identificadores
cortos no descriptivos, puede hacer que el código sea menos legible y el valor agregado
puede ser ahogado por la mayor oscuridad del código.

Es por eso que en la siguiente receta, explicamos las razones por las que var se
introdujo el tipo reservado . Intenta evitar usarlo varen los otros casos.

Uso de inferencia de tipo variable


local
En esta receta, aprenderá sobre la inferencia de tipos de variables locales, que se
introdujo en Java 10, donde se puede usar, y sus limitaciones.

Prepararse
Una inferencia de tipo de variable local es la capacidad de un compilador para
identificar el tipo de la variable local utilizando el lado correcto de una expresión. En
Java, una variable local con un tipo inferido se declara utilizando el identificador
var . Por ejemplo:

var i = 42; //int


var s = "42"; //String
var list1 = new ArrayList(); //ArrayList of Objects;
var list2 = new ArrayList<String>(); //ArrayList of Strings

pág. 618
El tipo de cada una de las variables anteriores es claramente identificable. Capturamos
sus tipos en los comentarios.

Tenga en cuenta que var no es una palabra clave, sino un identificador, con un
significado especial como el tipo de declaración de variable local.

Definitivamente ahorra escribir y hace que el código esté menos abarrotado de código
repetido. Veamos este ejemplo:

Map<Integer, List<String>> idToNames = new HashMap<>();


//...
for(Map.Entry<Integer, List<String>> e: idToNames.entrySet()){
List<String> names = e.getValue();
//...
}

Esa era la única forma de implementar tal bucle. Pero desde Java 10, se puede escribir
de la siguiente manera:

var idToNames = new HashMap<Integer, List<String>>();


//...
for(var e: idToNames.entrySet()){
var names = e.getValue();
//...
}

Como puede ver, el código se vuelve más claro, pero es útil usar nombres de variables
más descriptivos (como idToNamesy names). Bueno, de todos modos es útil. Pero si
no presta atención a los nombres de las variables, es fácil hacer que el código no sea
fácil de entender . Por ejemplo, mira el siguiente código:

var names = getNames();

Mirando la línea anterior, no tienes idea de qué tipo es la variable names . Cambiarlo
a hace que sea más fácil adivinar. Sin embargo, muchos programadores no lo
hacen. Prefieren nombres cortos de variables y calculan el tipo de cada variable
utilizando el soporte de contexto IDE (agregando un punto después del nombre de la
variable). Pero al final del día, es solo una cuestión de estilo y preferencias
personales. idToNames

Otro problema potencial proviene del hecho de que el nuevo estilo puede violar la
encapsulación y la codificación de un principio de interfaz si no se toman precauciones
adicionales. Por ejemplo, considere esta interfaz y su implementación:

interface A {
void m();
}

static class AImpl implements A {


public void m(){}

pág. 619
public void f(){}
}

Tenga en cuenta que la clase AImpl tiene más métodos públicos que la interfaz que
implementa. El estilo tradicional de crear el objeto AImpl sería el siguiente:

A a = new AImpl();
a.m();
//a.f(); //does not compile

De esta manera, exponemos solo los métodos presentes en la interfaz, mientras que el
nuevo estilo permite el acceso a todos los métodos:

var a = new AImpl();


a.m();
a.f();

Para limitar la referencia a los métodos de la interfaz solamente, uno necesita agregar
la conversión de texto de la siguiente manera:

var a = (A) new AImpl();


a.m();
//a.f(); //does not compile

Entonces, como muchas herramientas poderosas, el nuevo estilo puede hacer que su
código sea más fácil de escribir y mucho más legible o, si no se tiene especial cuidado,
menos legible y más difícil de depurar.

Cómo hacerlo...
Puede usar un tipo de variable local de las siguientes maneras:

Con un inicializador derecho:

var i = 1;
var a = new int[2];
var l = List.of(1, 2);
var c = "x".getClass();
var o = new Object() {};
var x = (CharSequence & Comparable<String>) "x";

Las siguientes declaraciones y tareas son ilegales y no se compilarán:

var e; // no initializer
var g = null; // null type
var f = { 6 }; // array initializer
var g = (g = 7); // self reference is not allowed
var b = 2, c = 3.0; // multiple declarators re not allowed
var d[] = new int[4]; // extra array dimension brackets
var f = () -> "hello"; // lambda requires an explicit target-type

pág. 620
Por extensión, con un inicializador en el bucle :

for(var i = 0; i < 10; i++){


//...
}

Y ya hemos hablado sobre este ejemplo:

var idToNames = new HashMap<Integer, List<String>>();


//...
for(var e: idToNames.entrySet()){
var names = e.getValue();
//...
}

Como referencia de clase anónima :

interface A {
void m();
}

var aImpl = new A(){


@Override
public void m(){
//...
}
};

Como identificador:

var var = 1;

Como nombre del método:

public void var(int i){


//...
}

Pero var no se puede usar como una clase o un nombre de interfaz.

Como nombre del paquete:

package com.packt.cookbook.var;

Uso de sintaxis de variable local


para parámetros lambda

pág. 621
En esta receta, aprenderá a usar la sintaxis de variable local (discutida en la receta
anterior) para los parámetros lambda y la motivación para introducir esta función. Fue
introducido en Java 11.

Prepararse
Hasta el lanzamiento de Java 11, había dos formas de declarar tipos de parámetros:
explícito e implícito. Aquí hay una versión explícita:

BiFunction<Double, Integer, Double> f = (Double x, Integer y) -> x / y;


System.out.println(f.apply(3., 2)); //prints: 1.5

Y la siguiente es una definición de tipo de parámetro implícito:

BiFunction<Double, Integer, Double> f = (x, y) -> x / y;


System.out.println(f.apply(3., 2)); //prints: 1.5

En el código anterior, el compilador calcula el tipo de parámetros según la definición de


la interfaz.

Con Java 11, se introdujo otra forma de declaración de tipo de parámetro: usar
el var identificador.

Cómo hacerlo...
1. La siguiente declaración de parámetros es exactamente la misma que la implícita antes
de Java 11:

BiFunction<Double, Integer, Double> f = (var x, var y) -> x / y;


System.out.println(f.apply(3., 2)); //prints: 1.5

2. La nueva sintaxis de estilo de variable local nos permite agregar anotaciones sin
definir explícitamente el tipo de parámetro:

import org.jetbrains.annotations.NotNull;
...
BiFunction<Double, Integer, Double> f =
(@NotNull var x, @NotNull var y) -> x / y;
System.out.println(f.apply(3., 2)); //prints: 1.5

Las anotaciones le dicen a las herramientas que procesan el código (el IDE, por ejemplo)
sobre la intención del programador, para que puedan advertir al programador durante
la compilación o ejecución en caso de que se viole la intención declarada. Por ejemplo,
hemos intentado ejecutar el siguiente código dentro de IntelliJ IDEA:

pág. 622
BiFunction<Double, Integer, Double> f = (x, y) -> x / y;
System.out.println(f.apply(null, 2));

Falló con NullPointerException en tiempo de ejecución. Luego hemos


ejecutado el siguiente código (con anotaciones):

BiFunction<Double, Integer, Double> f4 =


(@NotNull var x, @NotNull var y) -> x / y;
Double j = 3.;
Integer i = 2;
System.out.println(f4.apply(j, i));

El resultado fue el siguiente:

Exception in thread "main" java.lang.IllegalArgumentException:


Argument for @NotNull parameter 'x' of
com/packt/cookbook/ch17_new_way/b_lambdas/Chapter15Var.lambda$main$4
must not be null

La expresión lambda ni siquiera se ejecutó.

3. La ventaja de la sintaxis de variable local en el caso de los parámetros lambda queda


clara si necesitamos usar anotaciones cuando los parámetros son los objetos de una clase con
un nombre realmente largo. Antes de Java 11, el código puede tener el siguiente aspecto:

BiFunction<SomeReallyLongClassName,
AnotherReallyLongClassName, Double> f4 =
(@NotNull SomeReallyLongClassName x,
@NotNull AnotherReallyLongClassName y) -> x.doSomething(y);

Tuvimos que declarar el tipo de variable explícitamente porque queríamos agregar


anotaciones y la siguiente versión implícita ni siquiera compilaría:

BiFunction<SomeReallyLongClassName,
AnotherReallyLongClassName, Double> f4 =
(@NotNull x, @NotNull y) -> x.doSomething(y);

Con Java 11, la nueva sintaxis nos permite usar la inferencia de tipo de parámetro
implícito usando el identificador var:

BiFunction<SomeReallyLongClassName,
AnotherReallyLongClassName, Double> f4 =
(@NotNull var x, @NotNull var y) -> x.doSomething(y);

Esa es la ventaja y la motivación detrás de la introducción de una sintaxis de variable


local para la declaración del parámetro lambda.

pág. 623
Programación de GUI usando
JavaFX
En este capítulo, cubriremos las siguientes recetas:

• Crear una GUI usando controles JavaFX


• Usando el marcado FXML para crear una GUI
• Usando CSS para dar estilo a elementos en JavaFX
• Crear un gráfico de barras
• Crear un gráfico circular
• Incrustar HTML en una aplicación
• Incrustar medios en una aplicación
• Agregar efectos a los controles
• Usando la API Robot

Introducción
La programación GUI ha estado en Java desde JDK 1.0, a través de la API
llamada Abstract Window Toolkit ( AWT ). Esto fue algo notable durante esos
tiempos, pero tenía sus propias limitaciones, algunas de las cuales son las siguientes:

• Tenía un conjunto limitado de componentes.


• No pudo crear componentes reutilizables personalizados porque AWT estaba
usando componentes nativos.
• La apariencia de los componentes no se pudo controlar, y tomaron la apariencia
del sistema operativo host.

Luego, en Java 1.2, se introdujo una nueva API para el desarrollo de GUI
llamada Swing , que trabajó en las deficiencias de AWT al proporcionar lo siguiente:

• Una biblioteca de componentes más rica.


• Soporte para crear componentes personalizados.
• Una apariencia nativa, y soporte para conectar una apariencia diferente. Algunos
temas conocidos de aspecto y sensación de Java son Nimbus, Metal, Motif y el
sistema predeterminado.

Se han creado muchas aplicaciones de escritorio que hacen uso de Swing, y muchas de
ellas todavía se están utilizando. Sin embargo, con el tiempo, la tecnología tiene que
evolucionar; de lo contrario, eventualmente quedará desactualizado y rara vez se
usará. En 2008, Adobe's Flex comenzó a llamar la atención. Era un marco para
crear aplicaciones de Internet enriquecidas ( RIA ). Las aplicaciones de escritorio

pág. 624
siempre eran interfaces de usuario basadas en componentes, pero las aplicaciones web
no eran tan sorprendentes de usar. Adobe introdujo un marco llamado Flex, que
permitió a los desarrolladores web crear interfaces de usuario ricas e inmersivas en la
web. Entonces las aplicaciones web ya no eran aburridas.

Adobe también introdujo un rico entorno de tiempo de ejecución de aplicaciones de


Internet para el escritorio llamado Adobe AIR , que permitió la ejecución de
aplicaciones Flex en el escritorio. Este fue un gran golpe para la antigua API Swing. Pero
volvamos al mercado: en 2009, Sun Microsystems introdujo algo llamado JavaFX . Este
marco se inspiró en Flex (que utilizaba XML para definir la interfaz de usuario) e
introdujo su propio lenguaje de script llamado script JavaFX , que estaba algo más
cerca de JSON y JavaScript. Puede invocar las API de Java desde el script JavaFX. Se
introdujo una nueva arquitectura, que tenía un nuevo kit de herramientas de ventanas
y un nuevo motor gráfico. Era una alternativa mucho mejor a Swing, pero tenía un
inconveniente: los desarrolladores tenían que aprender el script JavaFX para
desarrollar aplicaciones basadas en JavaFX. Además de que Sun Microsystems no puede
invertir más en JavaFX y la plataforma Java, en general, JavaFX nunca despegó como se
había previsto.

Oracle (después de adquirir Sun Microsystems) anunció una nueva versión 2.0 de
JavaFX, que era una reescritura completa de JavaFX, eliminando así el lenguaje de
secuencias de comandos y haciendo de JavaFX una API dentro de la plataforma
Java. Esto ha hecho que el uso de la API JavaFX sea similar al uso de las API
Swing. Además, puede incrustar componentes JavaFX en Swing, lo que hace que las
aplicaciones basadas en Swing sean más funcionales. Desde entonces, no ha habido
ninguna vuelta atrás para JavaFX.

JavaFX ya no se incluye con JDK 11 en adelante (ni Oracle JDK ni OpenJDK


compilaciones). Y ya no se incluye con la compilación OpenJDK 10. Deben descargarse
por separado desde la página del Proyecto OpenJFX
( https://wiki.openjdk.java.net/display/OpenJFX/Main ). Se ha
lanzado un nuevo sitio web de la comunidad para OpenJFX
( https://openjfx.io/ )

En este capítulo, nos centraremos por completo en las recetas en torno a


JavaFX. Intentaremos abarcar tantas recetas como sea posible para brindarles a todos
una buena experiencia de uso de JavaFX.

Crear una GUI usando controles


JavaFX
En esta receta, veremos cómo crear una aplicación GUI simple, utilizando controles
JavaFX. Crearemos una aplicación que lo ayudará a calcular su edad, después de que

pág. 625
proporcione su fecha de nacimiento. Opcionalmente, incluso puede ingresar su nombre,
y la aplicación lo saludará y le mostrará su edad. Es un ejemplo bastante simple que
intenta mostrar cómo puede crear una GUI mediante diseños, componentes y manejo
de eventos.

Prepararse
Los siguientes son los módulos que forman parte de JavaFX:

• javafx.base
• javafx.controls
• javafx.fxml
• javafx.graphics
• javafx.media
• javafx.swing
• javafx.web

Si está utilizando Oracle JDK 10 y 9, viene con los módulos JavaFX mencionados
anteriormente como parte de la configuración; es decir, puedes encontrarlos en
el JAVA_HOME/jmodsdirectorio. Y si está utilizando OpenJDK 10 en adelante y JDK
11 en adelante, debe descargar el SDK de JavaFX
desde https://gluonhq.com/products/javafx/ y poner los JAR en
la ubicación JAVAFX_SDK_PATH/libsdisponible en su modulepath,
de la siguiente manera :

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

En nuestra receta, utilizaremos algunos módulos cuando sea necesario de la lista


anterior.

Cómo hacerlo...
1. Crea una clase que se
extienda javafx.application.Application. La clase Application
gestiona el ciclo de vida de la aplicación JavaFX. La clase Application
tiene un método abstracto start(Stage stage), que debe
implementar. Este sería el punto de partida para la interfaz de usuario
JavaFX:

pág. 626
public class CreateGuiDemo extends Application{
public void start(Stage stage){
//to implement in new steps
}
}

La clase también puede ser el punto de partida para la aplicación al


proporcionar un método public static void main(String []
args) {}:

public class CreateGuiDemo extends Application{


public void start(Stage stage){
//to implement in new steps
}
public static void main(String[] args){
//launch the JavaFX application
}
}

El código para los pasos posteriores debe escribirse dentro del


método start(Stage stage).

2. Creemos un diseño de contenedor para alinear correctamente los


componentes que agregaremos. En este caso,
usaremos javafx.scene.layout.GridPane para diseñar los componentes en
forma de una cuadrícula de filas y columnas:

GridPane gridPane = new GridPane();


gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setPadding(new Insets(25, 25, 25, 25));

Junto con la creación de la instancia GridPane, estamos configurando sus


propiedades de diseño, como la alineación de GridPanelos espacios horizontales y
verticales entre las filas y columnas, y el relleno dentro de cada celda de la cuadrícula.

3. Cree una nueva etiqueta, que mostrará el nombre de nuestra aplicación,


específicamente Age calculator, y agréguela a gridPane, que creamos en el
paso anterior:

Text appTitle = new Text("Age calculator");


appTitle.setFont(Font.font("Arial", FontWeight.NORMAL, 15));
gridPane.add(appTitle, 0, 0, 2, 1);

4. Cree una combinación de etiqueta y entrada de texto, que se usará para


aceptar el nombre del usuario. Luego agregue estos dos componentes a gridPane:

Label nameLbl = new Label("Name");


TextField nameField = new TextField();
gridPane.add(nameLbl, 0, 1);
gridPane.add(nameField, 1, 1);

pág. 627
5. Cree una combinación de etiqueta y selector de fecha, que se utilizará para
aceptar la fecha de nacimiento del usuario:

Label dobLbl = new Label("Date of birth");


gridPane.add(dobLbl, 0, 2);
DatePicker dateOfBirthPicker = new DatePicker();
gridPane.add(dateOfBirthPicker, 1, 2);

6. Cree un botón, que será utilizado por el usuario para activar el cálculo de
edad, y agréguelo a gridPane:

Button ageCalculator = new Button("Calculate");


gridPane.add(ageCalculator, 1, 3);

7. Cree un componente para contener el resultado de la edad calculada:

Text resultTxt = new Text();


resultTxt.setFont(Font.font("Arial", FontWeight.NORMAL, 15));
gridPane.add(resultTxt, 0, 5, 2, 1);

8. Ahora necesitamos vincular una acción al botón creado en el paso 6. La acción


será obtener el nombre ingresado en el campo de nombre y la fecha de nacimiento
ingresada en el campo selector de fecha. Si se proporciona la fecha de nacimiento,
utilice las API de tiempo de Java para calcular el período entre ahora y la fecha de
nacimiento. Si se proporciona un nombre, añada un saludo Hello, <name>al
resultado:

ageCalculator.setOnAction((event) -> {
String name = nameField.getText();
LocalDate dob = dateOfBirthPicker.getValue();
if ( dob != null ){
LocalDate now = LocalDate.now();
Period period = Period.between(dob, now);
StringBuilder resultBuilder = new StringBuilder();
if ( name != null && name.length() > 0 ){
resultBuilder.append("Hello, ")
.append(name)
.append("n");
}
resultBuilder.append(String.format(
"Your age is %d years %d months %d days",
period.getYears(),
period.getMonths(),
period.getDays())
);
resultTxt.setText(resultBuilder.toString());
}
});

9. Cree una instancia de la clase Scene proporcionando el objeto


gridPane que creamos en el paso 2 y las dimensiones, el ancho y la altura de la
escena:

pág. 628
Scene scene = new Scene(gridPane, 300, 250);

Una instancia de Scene contiene el gráfico de los componentes de la interfaz de


usuario, que se denomina gráfico de escena .

10. Hemos visto que el método start()nos proporciona una referencia a


un objeto Stage. El objeto Stage es el contenedor de nivel superior en JavaFX,
algo así como un JFrame. Establecemos el Sceneobjeto en el objeto Stage y
usamos su método show()para representar la IU:

stage.setTitle("Age calculator");
stage.setScene(scene);
stage.show();

11. Ahora necesitamos lanzar esta interfaz de usuario JavaFX desde el método
principal. Utilizamos el método launch(String[] args) de la clase
Application para iniciar la interfaz de usuario JavaFX:

public static void main(String[] args) {


Application.launch(args);
}

El código completo se puede encontrar en Chapter16/1_create_javafx_gui.

Hemos proporcionado dos guiones run.baty run.sh,


en Chapter16/1_create_javafx_gui. El script run.bat será para ejecutar
la aplicación en Windows y run.shserá para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh, y verá la GUI, como se muestra


en la siguiente captura de pantalla:

pág. 629
Ingrese el nombre y la fecha de nacimiento y haga clic en Calculate para ver la
edad:

Cómo funciona...
Antes de entrar en otros detalles, le daremos una breve descripción de la arquitectura
JavaFX. Hemos tomado el siguiente diagrama que describe la pila de arquitectura de la
documentación de JavaFX ( http://docs.oracle.com/javase/8/javafx/get-started-tutorial/jfx-
architecture.htm#JFXST788 ):

Comencemos desde la parte superior de la pila:

• Las API de JavaFX y el gráfico de escena : este es el punto de partida de la aplicación,


y la mayor parte de nuestro enfoque se centrará en esta parte. Esto proporciona API
para diferentes componentes, diseño y otras utilidades, para facilitar el desarrollo
de una interfaz de usuario basada en JavaFX. El gráfico de escena contiene los
elementos visuales de la aplicación.

pág. 630
• Prism, Quantum Toolkit y otras cosas en azul : estos componentes administran la
representación de la interfaz de usuario y proporcionan un puente entre el sistema
operativo subyacente y JavaFX. Esta capa proporciona representación de software
en casos en los que el hardware de gráficos no puede proporcionar representación
acelerada por hardware de elementos UI y 3D enriquecidos.
• El kit de herramientas de ventanas de vidrio : este es el kit de herramientas de
ventanas, al igual que el AWT utilizado por Swing.
• El motor de medios : admite medios en JavaFX.
• El motor web : admite el componente web, que permite la representación
completa de HTML.
• Las API de JDK y JVM : se integran con la API de Java y compilan el código en
bytecode para que se ejecute en la JVM.

Volvamos a explicar la receta. La clase javafx.application.Application es el


punto de entrada para iniciar las aplicaciones JavaFX. Tiene los siguientes métodos que
se asignan al ciclo de vida de la aplicación (en su orden de invocación):

• init(): Este método se invoca inmediatamente después de la instanciación


de javafx.application.Application. Puede anular este método para
realizar una inicialización antes del inicio de la aplicación. Por defecto, este método
no hace nada.
• start(javafx.stage.Stage): Este método se llama inmediatamente
después init()y después de que el sistema haya realizado la inicialización
requerida para ejecutar la aplicación. Este método se pasa con
una javafx.stage.Stageinstancia, que es la etapa principal en la que se
representan los componentes. Puede crear
otros javafx.stage.Stageobjetos, pero el que proporciona la aplicación es la
etapa principal.
• stop(): Este método se llama cuando la aplicación debe detenerse. Puede hacer
las operaciones relacionadas con la salida necesarias.

Una etapa es un contenedor JavaFX de nivel superior. La start()plataforma crea la


etapa primaria que se pasa como argumento al método, y la aplicación puede crear
otros Stagecontenedores cuando sea necesario.

El otro método importante relacionado javafx.application.Applicationes


el launch()método. Hay dos variantes de esto:

• launch(Class<? extends Application> appClass, String...


args)
• launch(String... args)

Este método se llama desde el método principal, y debe llamarse solo una vez. La
primera variante toma el nombre de la clase que extiende la clase

pág. 631
javafx.application.Application junto con los argumentos pasados al
método principal, y la segunda variante no toma el nombre de la clase y, en su lugar,
debe invocarse desde dentro de la clase que extiende la
clase javafx.application.Application. En nuestra receta, hemos utilizado la
segunda variante.

Hemos creado una clase CreateGuiDemo, se


extiende javafx.application.Application. Este será el punto de entrada
para JavaFX UI, y también agregamos un método principal a la clase, convirtiéndolo en
un punto de entrada para nuestra aplicación.

Una construcción de diseño determina cómo se distribuyen sus componentes. Hay


múltiples diseños compatibles con JavaFX, de la siguiente manera:

• javafx.scene.layout.HBox y javafx.scene.layout.VBox: se
utilizan para alinear los componentes horizontal y verticalmente.
• javafx.scene.layout.BorderPane: Esto permite colocar los componentes
en las posiciones superior, derecha, inferior, izquierda y central.
• javafx.scene.layout.FlowPane: Este diseño permite colocar los
componentes en un flujo, es decir, uno junto al otro, envolviendo en el límite del
panel de flujo.
• javafx.scene.layout.GridPane: Este diseño permite colocar los
componentes en una cuadrícula de filas y columnas.
• javafx.scene.layout.StackPane: Este diseño coloca los componentes en
una pila de atrás hacia adelante.
• javafx.scene.layout.TilePane: Este diseño coloca los componentes en
una cuadrícula de mosaicos de tamaño uniforme.

En nuestra receta, hemos utilizado GridPaney configurado el diseño para que


podamos lograr lo siguiente:

• La cuadrícula colocada en el centro


( gridPane.setAlignment(Pos.CENTER);)
• Establezca el espacio entre las columnas en 10 ( gridPane.setHgap(10);)
• Establezca el espacio entre las filas a 10 ( gridPane.setVgap(10);)
• Establecer el relleno dentro de la celda de la cuadrícula
( gridPane.setPadding(new Insets(25, 25, 25, 25));)

javafx.scene.text.Text La fuente de un componente se puede establecer


usando el objeto javafx.scene.text.Font como se muestra
aquí: appTitle.setFont(Font.font("Arial", FontWeight.NORMAL,
15));

pág. 632
Al agregar el componente javafx.scene.layout.GridPane, debemos
mencionar el número de columna, el número de fila y el intervalo de columna, es decir,
cuántas columnas ocupa el componente y el intervalo de fila, es decir, cuántas filas
ocupa el componente en ese orden. El intervalo de columnas y el intervalo de filas son
opcionales. En nuestra receta, hemos colocado appTitle en la primera fila y
columna, y ocupa dos espacios de columna y espacio de una fila, como se muestra en
el código aquí: appTitle.setFont(Font.font("Arial",
FontWeight.NORMAL, 15));

La otra parte importante en esta receta es la configuración del evento para


el botón ageCalculator. Hacemos uso del método setOnAction()de la clase
javafx.scene.control.Button para establecer la acción realizada cuando se
hace clic en el botón. Esto acepta una implementación de la interfaz
javafx.event.EventHandler<ActionEvent> . Como javafx.event.Even
tHandler es una interfaz funcional, su implementación se puede escribir en forma de
una expresión lambda, como se muestra aquí:

ageCalculator.setOnAction((event) -> {
//event handling code here
});

La sintaxis anterior se parece a sus clases internas anónimas ampliamente utilizadas


durante los tiempos de Swing. Puede obtener más información sobre interfaces
funcionales y expresiones lambda en las recetas del Capítulo 4 , Funcionamiento
funcional .

En nuestro código de manejo de eventos, obtenemos los valores de nameField


y dateOfBirthPicker utilizando
los métodos getText()y getValue()respectivamente. DatePicker devuelve la
fecha seleccionada como una instancia de java.time.LocalDate. Esta es una de las
nuevas API de fecha y hora agregadas a Java 8. Representa una fecha, es decir, el año, el
mes y el día, sin ninguna información relacionada con la zona horaria. Luego hacemos
uso de la clase java.time.Period para encontrar la duración entre la fecha actual
y la fecha seleccionada, de la siguiente manera:

LocalDate now = LocalDate.now();


Period period = Period.between(dob, now);

Period representa la duración basada en la fecha en términos de años, meses y días,


por ejemplo, tres años, dos meses y tres días. Esto es exactamente lo que estamos
tratando de extraer con esta línea de código: String.format("Your age is %d
years %d months %d days", period.getYears(),
period.getMonths(), period.getDays()).

Ya hemos mencionado que los componentes de la interfaz de usuario en JavaFX se


representan en forma de un gráfico de escena, y este gráfico de escena se representa en

pág. 633
un contenedor, llamado Stage. La forma de crear un gráfico de escena es usando
la clase javafx.scene.Scene. Creamos una instancia javafx.scene.Scene
pasando la raíz del gráfico de escena y también proporcionando las dimensiones del
contenedor en el que se representará el gráfico de escena.

Hacemos uso del contenedor proporcionado al método start(), que no es más que
una instancia de javafx.stage.Stage. Configurar la escena para el objeto Stage
y luego llamar a sus métodos show()hace que el gráfico de escena completo se muestre
en la pantalla:

stage.setScene (escena);
Show de escenario();

Usando el marcado FXML para


crear una GUI
En nuestra primera receta, analizamos el uso de API de Java para construir una interfaz
de usuario. A menudo sucede que una persona experta en Java podría no ser un buen
diseñador de interfaz de usuario; es decir, pueden ser pobres para identificar la mejor
experiencia de usuario para su aplicación. En el mundo del desarrollo web, tenemos
desarrolladores que trabajan en la interfaz, basándose en los diseños proporcionados
por el diseñador de UX, y el otro conjunto de desarrolladores que trabajan en el
servidor, para crear servicios que son consumidos por la interfaz.

Ambas partes desarrolladoras aceptan un conjunto de API y un modelo común de


intercambio de datos. Los desarrolladores front-end trabajan utilizando algunos datos
simulados basados en el modelo de intercambio de datos y también integran la interfaz
de usuario con las API requeridas. Por otro lado, los desarrolladores de backend
trabajan en la implementación de API para que devuelvan los datos en el modelo de
intercambio acordado. Por lo tanto, ambas partes trabajan simultáneamente, utilizando
su experiencia en sus áreas de trabajo.

Sería sorprendente si lo mismo pudiera replicarse (al menos en cierta medida) en


aplicaciones de escritorio. Un paso en esta dirección fue la introducción de un lenguaje
basado en XML, llamado FXML . Esto permite un método declarativo de desarrollo de
IU, donde el desarrollador puede desarrollar de forma independiente la IU utilizando
los mismos componentes JavaFX pero disponibles como etiquetas XML. Las diferentes
propiedades de los componentes JavaFX están disponibles como atributos de las
etiquetas XML. Los controladores de eventos se pueden declarar y definir en el código
Java y, a continuación, referirse desde FXML.

pág. 634
En esta receta, lo guiaremos a través de la construcción de la interfaz de usuario
utilizando FXML y luego integrando FXML con el código Java para vincular la acción y
para iniciar la interfaz de usuario definida en FXML.

Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el SDK
de JavaFX desde https://gluonhq.com/products/javafx/ e incluir los JAR presentes en
la libcarpeta del SDK en la ruta modular utilizando la -popción, como se muestra
aquí:

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

Desarrollaremos una aplicación de calculadora de edad simple. Esta aplicación le pedirá


el nombre del usuario (que es opcional) y su fecha de nacimiento, y calculará la edad a
partir de la fecha de nacimiento dada y se la mostrará al usuario.

Cómo hacerlo...
1. Todos los archivos FXML deben terminar con la extensión.fxml. Creemos
un fxml_age_calc_gui.xmlarchivo vacío en la
ubicación src/gui/com/packt. En los pasos siguientes, actualizaremos este
archivo con las etiquetas XML para los componentes JavaFX.
2. Cree un diseño GridPane, que contendrá todos los componentes en una
cuadrícula de filas y columnas. También proporcionaremos el espacio requerido
entre las filas y las columnas utilizando los atributos vgap y hgap. Además,
proporcionaremos GridPane, que es nuestro componente raíz, con la referencia
a la clase Java, donde agregaremos el manejo de eventos requerido. Esta clase de
Java será como el controlador para la interfaz de usuario:

<GridPane alignment="CENTER" hgap="10.0" vgap="10.0"


xmlns:fx="http://javafx.com/fxml"
fx:controller="com.packt.FxmlController">
</GridPane>

3. Proporcionaremos el relleno dentro de cada celda de la cuadrícula definiendo una


etiqueta padding con Insets dentro de GridPane:

pág. 635
<padding>
<Insets bottom="25.0" left="25.0" right="25.0" top="25.0" /
</padding>

4. El siguiente es agregar una etiqueta Text, que muestra el título de la


aplicación— Age Calculator. Proporcionamos la información de estilo requerida en
el atributo style y la ubicación del componente Text dentro del GridPane de uso
de los atributos GridPane.columnIndex y GridPane.rowIndex. La información
de ocupación de celda se puede proporcionar utilizando
los atributos GridPane.columnSpan y GridPane.rowSpan:

<Text style="-fx-font: NORMAL 15 Arial;" text="Age calculator"


GridPane.columnIndex="0" GridPane.rowIndex="0"
GridPane.columnSpan="2" GridPane.rowSpan="1">
</Text>

5. Luego agregamos los componentes Label y TextField para aceptar el


nombre. Tenga en cuenta el uso del fx:id atributo en TextField. Esto ayuda a
vincular este componente en el controlador Java creando un campo con el mismo nombre
que el del fx:id valor:

<Label text="Name" GridPane.columnIndex="0"


GridPane.rowIndex="1">
</Label>
<TextField fx:id="nameField" GridPane.columnIndex="1"
GridPane.rowIndex="1">
</TextField>

6. Agregamos los componentes Label y DatePicker para aceptar la fecha de


nacimiento:

<Label text="Date of Birth" GridPane.columnIndex="0"


GridPane.rowIndex="2">
</Label>
<DatePicker fx:id="dateOfBirthPicker" GridPane.columnIndex="1"
GridPane.rowIndex="2">
</DatePicker>

7. Luego, agregamos un objeto Button y establecemos su atributo onAction al


nombre del método en el controlador Java que maneja el evento de clic de este botón:

<Button onAction="#calculateAge" text="Calculate"


GridPane.columnIndex="1" GridPane.rowIndex="3">
</Button>

8. Finalmente, agregamos un Textcomponente para mostrar la edad calculada:

<Text fx:id="resultTxt" style="-fx-font: NORMAL 15 Arial;"


GridPane.columnIndex="0" GridPane.rowIndex="5"
GridPane.columnSpan="2" GridPane.rowSpan="1"

pág. 636
</Text>

9. El siguiente paso es implementar la clase Java, que está directamente relacionada


con los componentes de IU basados en XML creados en los pasos anteriores. Crea una clase
llamada FxmlController. Esto contendrá el código que es relevante para la interfaz de
usuario FXML; es decir, contendrá las referencias a los componentes creados en los
controladores de acción FXML para los componentes creados en FXML:

public class FxmlController {


//to implement in next few steps
}

10. Necesitamos referencias a los nameField, dateOfBirthPicker


y resultText componentes. Utilizamos los dos primeros para obtener el nombre y la
fecha de nacimiento ingresados, respectivamente, y el tercero para mostrar el resultado
del cálculo de la edad:

@FXML private Text resultTxt;


@FXML private DatePicker dateOfBirthPicker;
@FXML private TextField nameField;

11. El siguiente paso es implementar el método calculateAge, que se registra


como el controlador de eventos de acción para el Calculatebotón. La implementación
es similar a la de la receta anterior. La única diferencia es que es un método, a diferencia de
la receta anterior, donde era una expresión lambda:

@FXML
public void calculateAge(ActionEvent event){
String name = nameField.getText();
LocalDate dob = dateOfBirthPicker.getValue();
if ( dob != null ){
LocalDate now = LocalDate.now();
Period period = Period.between(dob, now);
StringBuilder resultBuilder = new StringBuilder();
if ( name != null && name.length() > 0 ){
resultBuilder.append("Hello, ")
.append(name)
.append("n");
}
resultBuilder.append(String.format(
"Your age is %d years %d months %d days",
period.getYears(),
period.getMonths(),
period.getDays())
);
resultTxt.setText(resultBuilder.toString());
}
}

12. En ambas etapas 10 y 11, se ha utilizado una anotación, @FXML. Esta anotación
indica que la IU basada en FXML puede acceder a la clase o al miembro.

pág. 637
13. A continuación, crearemos otra clase Java FxmlGuiDemo, que es responsable de
representar la interfaz de usuario basada en FXML y que también sería el punto de
entrada para iniciar la aplicación:

public class FxmlGuiDemo extends Application{


//code to launch the UI + provide main() method
}

14. Ahora necesitamos crear un gráfico de escena a partir de la definición de la interfaz


de usuario FXML anulando el método start(Stage stage) de
la clase javafx.application.Application y luego renderizar el gráfico de
escena dentro del objeto pasado javafx.stage.Stage:

@Override
public void start(Stage stage) throws IOException{
FXMLLoader loader = new FXMLLoader();
Pane pane = (Pane)loader.load(getClass()
.getModule()
.getResourceAsStream("com/packt/fxml_age_calc_gui.fxml")
);

Scene scene = new Scene(pane,300, 250);


stage.setTitle("Age calculator");
stage.setScene(scene);
stage.show();
}

15. Finalmente, proporcionamos la main()implementación del método:

public static void main(String[] args) {


Application.launch(args);
}

El código completo se puede encontrar en la ubicación Chapter16/2_fxml_gui.

Hemos proporcionado scripts de dos ejecuciones run.baty run.sh,


en Chapter16/2_fxml_gui. El run.bat script será para ejecutar la aplicación en
Windows y run.shserá para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh, y verá la GUI como se muestra en la


siguiente captura de pantalla:

pág. 638
Ingrese el nombre y la fecha de nacimiento y haga clic en Calculatepara ver la
edad:

Cómo funciona...
No hay XSD que defina el esquema para el documento FXML. Entonces, para conocer
las etiquetas que se utilizarán, siguen una convención de nomenclatura simple. El
nombre de la clase Java del componente también es el nombre de la etiqueta XML. Por
ejemplo, la etiqueta XML para el diseño javafx.scene.layout.GridPane
es <GridPane>, y para javafx.scene.control.TextField ello
es <TextField>, y para javafx.scene.control.DatePicke ello
es <DatePicker>:

pág. 639
Pane pane = (Pane)loader.load(getClass()
.getModule()
.getResourceAsStream("com/packt/fxml_age_calc_gui.fxml")
);

La línea de código anterior utiliza una instancia de javafx.fxml.FXMLLoader


para leer el archivo FXML y obtener la representación Java de los componentes de la
interfaz de usuario. FXMLLoader utiliza un analizador SAX basado en eventos para
analizar el archivo FXML. Las instancias de las clases Java respectivas para las etiquetas
XML se crean mediante reflexión, y los valores de los atributos de las etiquetas XML se
rellenan en las propiedades respectivas de las clases Java.

Como la raíz de nuestro FXML es javafx.scene.layout.GridPane, que se


extiende javafx.scene.layout.Pane, podemos convertir el valor de retorno
de FXMLoader.load() a javafx.scene.layout.Pane.

La otra cosa interesante en esta receta es la clase FxmlController. Esta clase actúa
como una interfaz para FXML. Indicamos lo mismo en FXML usando
el atributo fx:controller de la etiqueta <GridPane>. Podemos obtener los
componentes de la interfaz de usuario definidos en FXML utilizando la
anotación @FXML contra los campos miembros de la FxmlControllerclase, como lo
hicimos en esta receta:

@FXML private Text resultTxt;


@FXML private DatePicker dateOfBirthPicker;
@FXML private TextField nameField;

El nombre del miembro es el mismo que el del valor fx:id del atributo en FXML, y
el tipo de miembro es el mismo que el de la etiqueta en FXML. Por ejemplo, el primer
miembro está vinculado a lo siguiente:

<Text fx:id="resultTxt" style="-fx-font: NORMAL 15 Arial;"


GridPane.columnIndex="0" GridPane.rowIndex="5"
GridPane.columnSpan="2" GridPane.rowSpan="1">
</Text>

En líneas similares, creamos un controlador de eventos FxmlController y lo


anotamos @FXML, y lo mismo se ha referenciado en FXML con el atributo onAction
de <Button>. Tenga en cuenta que hemos agregado #al comienzo del nombre del
método en el valor onAction del atributo.

Usando CSS para los elementos de


estilo en JavaFX
pág. 640
Aquellos con experiencia en desarrollo web podrán apreciar la utilidad de las Hojas de
Estilo en Cascada ( CSS ), y para aquellos que no lo son, proporcionaremos una
descripción general de lo que son y cómo son útiles, antes de sumergirse en la aplicación
CSS en JavaFX.

Los elementos o los componentes que ve en las páginas web suelen tener un estilo
acorde con el tema del sitio web. Este estilo es posible mediante el uso de un lenguaje
llamado CSS . CSS consiste en un grupo de pares name:value, separados por punto y
coma. Estos pares name:value, cuando están asociados con un elemento HTML, por
ejemplo <button>, le dan el estilo requerido.

Hay varias formas de asociar estos pares name:value al elemento, la más simple es
colocar este par name:value dentro del atributo de estilo de su elemento HTML. Por
ejemplo, para darle al botón un fondo azul, podemos hacer lo siguiente:

<button style="background-color: blue;"></button>

Hay nombres predefinidos para diferentes propiedades de estilo, y estos toman un


conjunto específico de valores; es decir, la propiedad, background-color solo
tomará valores de color válidos.

El otro enfoque es definir estos grupos de pares name:value en un archivo diferente


con una .cssextensión. Llamemos a este grupo de pares name:value
de propiedades CSS . Podemos asociar estas propiedades CSS con diferentes
selectores, es decir, selectores para elegir los elementos en la página HTML para aplicar
también las propiedades CSS. Hay tres formas diferentes de proporcionar los
selectores:

1. Al dar directamente el nombre del elemento HTML, es decir, si es una etiqueta de


anclaje ( <a>), un botón o una entrada. En tales casos, las propiedades CSS se aplican a
todos los tipos de elementos HTML en la página.
2. Mediante el uso del idatributo del elemento HTML. Supongamos que tenemos un
botón con id="btn1", luego podemos definir un selector #btn1, contra el cual
proporcionamos las propiedades CSS. Eche un vistazo al siguiente ejemplo:
#btn1 { background-color: blue; }
3. Mediante el uso del atributo de clase del elemento HTML. Supongamos que
tenemos un botón con class="blue-btn", luego podemos definir un
selector .blue-btn, contra el cual proporcionamos las propiedades CSS. Mira el
siguiente ejemplo:
.blue-btn { background-color: blue; }

La ventaja de usar un archivo CSS diferente es que podemos evolucionar


independientemente la apariencia de las páginas web sin tener que estar
estrechamente acopladas a la ubicación de los elementos. Además, esto fomenta la

pág. 641
reutilización de las propiedades CSS en diferentes páginas, lo que les da un aspecto
uniforme en todas las páginas.

Cuando aplicamos un enfoque similar a JavaFX, podemos aprovechar el conocimiento


de CSS ya disponible con nuestros diseñadores web para crear CSS para componentes
JavaFX, y esto ayuda a diseñar los componentes más fácilmente que con el uso de API
de Java. Cuando este CSS se mezcla con FXML, se convierte en un dominio conocido para
los desarrolladores web.

En esta receta, veremos cómo diseñar algunos componentes JavaFX utilizando un


archivo CSS externo.

Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el SDK
de JavaFX desde https://gluonhq.com/products/javafx/ e incluir los JAR presentes en
la carpeta del SDK en la ruta modular utilizando la opción, como se muestra aquí: lib-
p

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

Hay una pequeña diferencia en la definición de las propiedades CSS para los
componentes JavaFX. Todas las propiedades deben tener el prefijo -fx-, es decir,
background-color se convierte en -fx-background-color. Los selectores, es
decir, #idy .class-name siguen siendo los mismos en el mundo JavaFX
también. Incluso podemos proporcionar múltiples clases a los componentes JavaFX,
aplicando así todas estas propiedades CSS a los componentes.

El CSS que he usado en esta receta se basa en un marco CSS popular


llamado Bootstrap ( http://getbootstrap.com/css/ ).

Cómo hacerlo...
1. Vamos a crear GridPane, que mantendrá los componentes en una cuadrícula de
filas y columnas:

GridPane gridPane = new GridPane();

pág. 642
gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setPadding(new Insets(25, 25, 25, 25));

2. Primero, crearemos un botón y le agregaremos dos clases btn, y btn-


primary. En el siguiente paso, definiremos estos selectores con las propiedades CSS
necesarias:

Button primaryBtn = new Button("Primary");


primaryBtn.getStyleClass().add("btn");
primaryBtn.getStyleClass().add("btn-primary");
gridPane.add(primaryBtn, 0, 1);

3. Ahora proporcionemos las propiedades CSS necesarias para las clases, btny btn-
primary. El selector para las clases tiene la forma .<class-name>:

.btn{
-fx-border-radius: 4px;
-fx-border: 2px;
-fx-font-size: 18px;
-fx-font-weight: normal;
-fx-text-align: center;
}
.btn-primary {
-fx-text-fill: #fff;
-fx-background-color: #337ab7;
-fx-border-color: #2e6da4;
}

4. Creemos otro botón con una clase CSS diferente:

Button successBtn = new Button("Sucess");


successBtn.getStyleClass().add("btn");
successBtn.getStyleClass().add("btn-success");
gridPane.add(successBtn, 1, 1);

5. Ahora definimos las propiedades CSS para el selector .btn-success de la


siguiente manera:

.btn-success {
-fx-text-fill: #fff;
-fx-background-color: #5cb85c;
-fx-border-color: #4cae4c;
}

6. Creemos otro botón con una clase CSS diferente:

Button dangerBtn = new Button("Danger");


dangerBtn.getStyleClass().add("btn");
dangerBtn.getStyleClass().add("btn-danger");
gridPane.add(dangerBtn, 2, 1);

pág. 643
7. Definiremos las propiedades CSS para el selector .btn-danger:

.btn-danger {
-fx-text-fill: #fff;
-fx-background-color: #d9534f;
-fx-border-color: #d43f3a;
}

8. Ahora, agreguemos algunas etiquetas con diferentes selectores, a


saber, badgey badge-info:

Label label = new Label("Default Label");


label.getStyleClass().add("badge");
gridPane.add(label, 0, 2);

Label infoLabel = new Label("Info Label");


infoLabel.getStyleClass().add("badge");
infoLabel.getStyleClass().add("badge-info");
gridPane.add(infoLabel, 1, 2);

9. Las propiedades CSS para los selectores anteriores son las siguientes:

.badge{
-fx-label-padding: 6,7,6,7;
-fx-font-size: 12px;
-fx-font-weight: 700;
-fx-text-fill: #fff;
-fx-text-alignment: center;
-fx-background-color: #777;
-fx-border-radius: 4;
}

.badge-info{
-fx-background-color: #3a87ad;
}
.badge-warning {
-fx-background-color: #f89406;
}

10. Agreguemos TextFieldcon una big-input clase:

TextField bigTextField = new TextField();


bigTextField.getStyleClass().add("big-input");
gridPane.add(bigTextField, 0, 3, 3, 1);

11. Definimos las propiedades CSS para que el contenido del cuadro de texto sea de
gran tamaño y de color rojo:

.big-input{
-fx-text-fill: red;
-fx-font-size: 18px;
-fx-font-style: italic;

pág. 644
-fx-font-weight: bold;
}

12. Agreguemos algunos botones de radio:

ToggleGroup group = new ToggleGroup();


RadioButton bigRadioOne = new RadioButton("First");
bigRadioOne.getStyleClass().add("big-radio");
bigRadioOne.setToggleGroup(group);
bigRadioOne.setSelected(true);
gridPane.add(bigRadioOne, 0, 4);
RadioButton bigRadioTwo = new RadioButton("Second");
bigRadioTwo.setToggleGroup(group);
bigRadioTwo.getStyleClass().add("big-radio");
gridPane.add(bigRadioTwo, 1, 4);

13. Definimos las propiedades CSS para que las etiquetas de los botones de radio sean
de gran tamaño y de color verde:

.big-radio{
-fx-text-fill: green;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-background-color: yellow;
-fx-padding: 5;
}

14. Finalmente, agregamos javafx.scene.layout.GridPaneal gráfico de


escena y mostramos el gráfico de escena javafx.stage.Stage. También
necesitamos asociar el stylesheet.csscon el Scene:

Scene scene = new Scene(gridPane, 600, 500);


scene.getStylesheets().add("com/packt/stylesheet.css");
stage.setTitle("Age calculator");
stage.setScene(scene);
stage.show();

15. Agregue un main()método para iniciar la GUI:

public static void main(String[] args) {


Application.launch(args);
}

El código completo se puede encontrar aquí: Chapter16/3_css_javafx.

Hemos proporcionado dos scripts de ejecución run.baty run.sh,


debajo Chapter16/3_css_javafx. El run.batserá para ejecutar la aplicación en
Windows, y run.shserá para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh, y verá la siguiente GUI:

pág. 645
Cómo funciona...
En esta receta, utilizamos los nombres de clase y sus selectores CSS correspondientes
para asociar componentes con diferentes propiedades de estilo. JavaFX admite un
subconjunto de propiedades CSS, y hay diferentes propiedades aplicables a diferentes
tipos de componentes JavaFX. La guía de referencia CSS de JavaFX
( http://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html ) lo ayudará
a identificar las propiedades CSS compatibles.

Todos los nodos del escenario gráfico se extienden desde una clase
abstracta, javax.scene.Node. Esta clase abstracta proporciona una
API, getStyleClass()que devuelve una lista de nombres de clase (que son
simples String) agregados al nodo o al componente JavaFX. Como se trata de una lista
simple de nombres de clase, incluso podemos agregarle más nombres de clase mediante
el uso getStyleClass().add("new-class-name").

La ventaja de usar nombres de clase es que nos permite agrupar componentes similares
por un nombre de clase común. Esta técnica es ampliamente utilizada en el mundo del
desarrollo web. Supongamos que tengo una lista de botones en la página HTML y quiero
que se realice una acción similar al hacer clic en cada botón. Para lograr esto, asignaré
a cada uno de los botones la misma clase, digamos my-button, y luego los
usaré document.getElementsByClassName('my-button')para obtener una

pág. 646
matriz de estos botones. Ahora podemos recorrer la matriz de botones obtenidos y
agregar los controladores de acción necesarios.

Después de asignar una clase al componente, necesitamos escribir las propiedades CSS
para el nombre de clase dado. Estas propiedades se aplican a todos los componentes
con el mismo nombre de clase.

Elija uno de los componentes de nuestra receta y veamos cómo lo diseñamos. Considere
el siguiente componente con dos clases, btny btn-primary:

primaryBtn.getStyleClass().add("btn");
primaryBtn.getStyleClass().add("btn-primary");

Hemos utilizado los selectores .btny .btn-primary, y hemos agrupado todas las
propiedades CSS en estos selectores, de la siguiente manera:

.btn{
-fx-border-radius: 4px;
-fx-border: 2px;
-fx-font-size: 18px;
-fx-font-weight: normal;
-fx-text-align: center;
}
.btn-primary {
-fx-text-fill: #fff;
-fx-background-color: #337ab7;
-fx-border-color: #2e6da4;
}

Tenga en cuenta que, en CSS, tenemos una propiedad color, y su equivalente en


JavaFX es -fx-text-fill. El resto de las propiedades de CSS, a saber border-
radius, border, font-size, font-weight, text-align, background-
color, y border-color, se puede anteponer -fx-.

La parte importante es cómo asociar la hoja de estilo con el componente Scene.

La scene.getStylesheets().add("com/packt/stylesheet.css");línea
de código asocia hojas de estilo con el componente de
escena. Como getStylesheets()devuelve una lista de cadenas, podemos agregarle
varias cadenas, lo que significa que podemos asociar múltiples hojas de estilo a una
escena.

La documentación de los estados getStylesheets()lo siguiente:

"La URL es un URI jerárquico de la forma [esquema:] [// autoridad] [ruta]. Si la URL no
tiene un componente [esquema:], la URL se considera solo el componente [ruta].
Cualquiera el carácter '/' inicial de la [ruta] se ignora y la [ruta] se trata como una ruta
relativa a la raíz de la ruta de clase de la aplicación ".

pág. 647
En nuestra receta, estamos usando pathsolo el componente y, por lo tanto, busca el
archivo en el classpath. Esta es la razón por la que hemos agregado la hoja de estilo al
mismo paquete que el de la escena. Esta es una manera más fácil de hacer que esté
disponible en el classpath.

Crear un gráfico de barras


Los datos, cuando se representan en forma de tablas, son muy difíciles de entender,
pero cuando los datos se representan gráficamente mediante gráficos, son cómodos
para la vista y fáciles de entender. Hemos visto muchas bibliotecas de gráficos para
aplicaciones web. Sin embargo, faltaba el mismo soporte en el frente de la aplicación de
escritorio. Swing no tenía soporte nativo para crear gráficos, y tuvimos que confiar en
aplicaciones de terceros como JFreeChart ( http://www.jfree.org/jfreechart/ ). Sin
embargo, con JavaFX, tenemos soporte nativo para crear gráficos, y le mostraremos
cómo representar los datos en forma de gráficos utilizando los componentes de gráficos
JavaFX.

JavaFX admite los siguientes tipos de gráficos:

• Gráfico de barras
• Gráfico de linea
• Gráfico circular
• Gráfico de dispersión
• Gráfico de área
• Tabla de burbujas

En las próximas recetas, cubriremos la construcción de cada tipo de gráfico. La


segregación de cada tipo de gráfico en una receta propia nos ayudará a explicar las
recetas de una manera más simple y ayudará a una mejor comprensión.

Esta receta tendrá que ver con gráficos de barras. Un gráfico de barras de muestra se
ve así:

pág. 648
Los gráficos de barras pueden tener una sola barra o varias barras (como en el diagrama
anterior) para cada valor en el eje x . Varias barras nos ayudan a comparar múltiples
puntos de valor para cada valor en el eje x .

Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el SDK
de JavaFX desde aquí https://gluonhq.com/products/javafx/ e incluir los
JARs presente en la carpeta del SDK en la ruta modular utilizando la opción, como se
muestra aquí: lib-p

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

Haremos uso de un subconjunto de datos del repositorio de aprendizaje automático de


rendimiento del estudiante
( https://archive.ics.uci.edu/ml/datasets/Student+Performance ).
El conjunto de datos consiste en el desempeño del estudiante en dos materias,
Matemáticas y Portugués, junto con su información de fondo social, como las

pág. 649
ocupaciones y educación de sus padres, entre otra información. Hay bastantes atributos
en el conjunto de datos, pero elegiremos lo siguiente:

• Género del estudiante


• Edad del estudiante
• Educación del padre
• Trabajo del padre
• Educación de la madre
• Profesión de la madre
• Si el alumno ha tomado clases adicionales
• Calificaciones de primer trimestre
• Calificaciones de segundo trimestre
• Notas finales

Como mencionamos anteriormente, hay muchos atributos capturados en los datos,


pero deberíamos ser buenos con algunos atributos importantes que nos ayudarán a
trazar algunos gráficos útiles. Debido a esto, hemos extraído la información del
conjunto de datos disponible en el repositorio de aprendizaje automático en un
archivo separado, que se puede encontrar
en Chapter16/4_bar_charts/src/gui/com/packt/students, en la
descarga de código para el libro. A continuación, un extracto del archivo de
estudiantes:

"F";18;4;4;"at_home";"teacher";"no";"5";"6";6
"F";17;1;1;"at_home";"other";"no";"5";"5";6
"F";15;1;1;"at_home";"other";"yes";"7";"8";10
"F";15;4;2;"health";"services";"yes";"15";"14";15
"F";16;3;3;"other";"other";"yes";"6";"10";10
"M";16;4;3;"services";"other";"yes";"15";"15";15

Las entradas están separadas por punto y coma ( ;). Cada entrada ha sido explicada por
lo que representa. La información educativa (campos 3 y 4) es un valor numérico,
donde cada número representa el nivel educativo, de la siguiente manera:

• 0: Ninguna
• 1: Educación primaria (cuarto grado)
• 2: Quinto a noveno grado
• 3: Educación Secundaria
• 4: Educación más alta

Hemos creado un módulo para procesar el archivo del alumno. El nombre del módulo
es student.processor y su código se puede encontrar
en Chapter16/101_student_data_processor. Por lo tanto, si desea cambiar
algún código allí, puede reconstruir el JAR ejecutando el archivo build-
jar.bato build-jar.sh. Esto creará un JAR
modular student.processor.jar, en el directorio mlib. Entonces, usted tiene

pág. 650
que reemplazar este JAR modular con el presente en el directorio mlib de esta
receta, es decir, Chapter16/4_bar_charts/mlib.

Recomendamos que cree el student.processorjar modular a partir de la fuente


disponible en Chapter16/101_student_data_processor. Hemos
proporcionado build-jar.baty build-jar.shguiones para ayudarlo a construir
el JAR. Sólo tienes que ejecutar el script correspondiente a su plataforma y luego copiar
el frasco construido
en 101_student_data_processor/mliba 4_bar_charts/mlib.

De esta manera, podemos reutilizar este módulo en todas las recetas que involucran
gráficos.

Cómo hacerlo...
1. Primero, cree GridPaney configúrelo para colocar los gráficos que crearemos :

GridPane gridPane = new GridPane();


gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setPadding(new Insets(25, 25, 25, 25));

2. Utilice la clase StudentDataProcessor del módulo student.processor


para analizar el archivo del estudiante y cargar los datos en List de Student:

StudentDataProcessor sdp = new StudentDataProcessor();


List<Student> students = sdp.loadStudent();

3. Los datos en bruto, es decir, la lista de objetos Student , no son útiles para trazar
un gráfico, por lo que debemos procesar las calificaciones de los estudiantes al agruparlos de
acuerdo con la educación de sus madres y padres y calcular el promedio de esos estudiantes.
calificaciones (los tres términos). Para esto, escribiremos un método simple que
acepte List<Student>, una función de agrupación, es decir, el valor en el cual los
estudiantes necesitan ser agrupados, y una función de mapeo, es decir, el valor que debe
usarse para calcular el promedio:

private Map<ParentEducation, IntSummaryStatistics> summarize(


List<Student> students,
Function<Student, ParentEducation> classifier,
ToIntFunction<Student> mapper
){
Map<ParentEducation, IntSummaryStatistics> statistics =
students.stream().collect(
Collectors.groupingBy(
classifier,
Collectors.summarizingInt(mapper)

pág. 651
)
);
return statistics;
}

El método anterior usa las nuevas API basadas en Stream. Estas API son tan poderosas
que agrupan a los estudiantes usando Collectors.groupingBy() y luego
calculan las estadísticas de sus calificaciones
usando Collectors.summarizingInt().

4. Los datos para el gráfico de barras se proporcionan como una instancia


de XYChart.Series. Cada serie da como resultado un valor y para un valor x dado , que
es una barra para un valor x dado . Tendremos varias series, una para cada trimestre, es decir,
calificaciones de primer término, calificaciones de segundo término y calificaciones
finales. Creemos un método que tome las estadísticas de las calificaciones de cada
término seriesNamey que devuelva un objeto series:

private XYChart.Series<String,Number> getSeries(


String seriesName,
Map<ParentEducation, IntSummaryStatistics> statistics
){
XYChart.Series<String,Number> series = new XYChart.Series<>();
series.setName(seriesName);
statistics.forEach((k, v) -> {
series.getData().add(
new XYChart.Data<String, Number>(
k.toString(),v.getAverage()
)
);
});
return series;
}

5. Crearemos dos gráficos de barras: uno para la calificación promedio de la educación


de la madre y el otro para la calificación promedio de la educación del padre. Para esto,
crearemos un método que tomará List<Student> y un clasificador, es decir, una función
que devolverá el valor que se utilizará para agrupar a los estudiantes. Este método hará los
cálculos necesarios y devolverá un objeto BarChart:

private BarChart<String, Number> getAvgGradeByEducationBarChart(


List<Student> students,
Function<Student, ParentEducation> classifier
){
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis();
final BarChart<String,Number> bc =
new BarChart<>(xAxis,yAxis);
xAxis.setLabel("Education");
yAxis.setLabel("Grade");
bc.getData().add(getSeries(
"G1",
summarize(students, classifier, Student::getFirstTermGrade)
));

pág. 652
bc.getData().add(getSeries(
"G2",
summarize(students, classifier, Student::getSecondTermGrade)
));
bc.getData().add(getSeries(
"Final",
summarize(students, classifier, Student::getFinalGrade)
));
return bc;
}

6. Cree BarChart para las calificaciones promedio de la educación de la madre y


agréguelo a gridPane:

BarChart<String, Number> avgGradeByMotherEdu =


getAvgGradeByEducationBarChart(
students,
Student::getMotherEducation
);
avgGradeByMotherEdu.setTitle(
"Average grade by Mother's Education"
);
gridPane.add(avgGradeByMotherEdu, 1,1);

7. Cree BarChart para las calificaciones promedio de la educación del padre y


agréguelo a gridPane:

BarChart<String, Number> avgGradeByFatherEdu =


getAvgGradeByEducationBarChart(
students,
Student::getFatherEducation
);
avgGradeByFatherEdu.setTitle(
"Average grade by Father's Education");
gridPane.add(avgGradeByFatherEdu, 2,1);

8. Cree un gráfico de escena usando gridPaney configúrelo en Stage:

Scene scene = new Scene(gridPane, 800, 600);


stage.setTitle("Bar Charts");
stage.setScene(scene);
stage.show();

El código completo se puede encontrar en Chapter16/4_bar_charts.

Hemos proporcionado dos scripts de ejecución: run.baty run.sh,


debajo Chapter16/4_bar_charts. El script run.bat será para ejecutar la
aplicación en Windows y run.sh será para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh, y verá la siguiente GUI:

pág. 653
Cómo funciona...
Primero veamos qué se necesita para crear BarChart. BarChart es un gráfico
basado en dos ejes, donde los datos se trazan en dos ejes, a saber, el eje x (eje
horizontal) y el eje y (eje vertical). Los otros dos gráficos basados en ejes son el gráfico
de área, el gráfico de burbujas y el gráfico de líneas.

En JavaFX, hay dos tipos de ejes compatibles:

• javafx.scene.chart.CategoryAxis: Esto admite valores de cadena en


los ejes
• javafx.scene.chart.NumberAxis: Esto admite valores numéricos en los
ejes

En nuestra receta, creamos BarChartcon CategoryAxis el eje x , donde trazamos


la educación, y NumberAxiscomo el eje y , donde trazamos la calificación, de la
siguiente manera:

final CategoryAxis xAxis = new CategoryAxis();


final NumberAxis yAxis = new NumberAxis();
final BarChart<String,Number> bc = new BarChart<>(xAxis,yAxis);
xAxis.setLabel("Education");
yAxis.setLabel("Grade");

pág. 654
En los siguientes párrafos, le mostramos cómo funciona el trazado de las obras
BarChart.

Los datos a trazar BarChart deben ser un par de valores, donde cada par representa
valores (x, y) , es decir, un punto en el eje xy un punto en el eje y . Este par de valores
está representado por javafx.scene.chart.XYChart.Data. Dataes una clase
anidada dentro XYChart, que representa un único elemento de datos para un gráfico
basado en dos ejes. Un XYChart.Data objeto se puede crear de manera bastante
simple, como sigue:

XYChart.Data item = new XYChart.Data("Cat1", "12");

Esto es solo un elemento de un solo dato. Un gráfico puede tener múltiples elementos
de datos, es decir, una serie de elementos de datos. Para representar una serie de
elementos de datos, JavaFX proporciona una clase
llamada javafx.scene.chart.XYChart.Series. Este objeto
XYChart.Series es una serie de elementos XYChart.Data con nombre. Creemos
una serie simple, como sigue:

XYChart.Series<String,Number> series = new XYChart.Series<>();


series.setName("My series");
series.getData().add(
new XYChart.Data<String, Number>("Cat1", 12)
);
series.getData().add(
new XYChart.Data<String, Number>("Cat2", 3)
);
series.getData().add(
new XYChart.Data<String, Number>("Cat3", 16)
);

BarChart puede tener múltiples series de elementos de datos. Si le proporcionamos


múltiples series, habrá múltiples barras para cada punto de datos en el eje x . Para
nuestra demostración de cómo funciona esto, nos quedaremos con una serie. Pero
la clase BarChart en nuestra receta usa múltiples series. Agreguemos la serie
al BarChart y luego renderícela en la pantalla:

bc.getData().add(series);
Scene scene = new Scene(bc, 800, 600);
stage.setTitle("Bar Charts");
stage.setScene(scene);
stage.show();

Esto da como resultado el siguiente cuadro:

pág. 655
La otra parte interesante de esta receta es la agrupación de estudiantes basada en la
educación de la madre y el padre y luego calcular el promedio de sus calificaciones de
primer término, segundo término y final. El código que realiza la agrupación y el cálculo
promedio son los siguientes:

Map<ParentEducation, IntSummaryStatistics> statistics =


students.stream().collect(
Collectors.groupingBy(
classifier,
Collectors.summarizingInt(mapper)
)
);

El código anterior hace lo siguiente:

• Crea una secuencia desde List<Student>.


• Reduce esta secuencia a la agrupación requerida mediante el método collect().
• Una de las versiones sobrecargadas de collect() toma dos parámetros. La
primera es la función que devuelve el valor en el que los estudiantes deben
agruparse. El segundo parámetro es una función de mapeo adicional, que mapea el

pág. 656
objeto de estudiante agrupado en el formato requerido. En nuestro caso, el formato
requerido es obtener IntSummaryStatistics para el grupo de estudiantes
cualquiera de sus valores de calificación.

Las dos piezas anteriores (configurar los datos para un gráfico de barras y crear los
objetos necesarios para llenar una instancia BarChart) son partes importantes de la
receta; entenderlos te dará una idea más clara de la receta.

Crear un gráfico circular


Los gráficos circulares, como su nombre indica, son gráficos circulares con sectores (ya
sea unidos o separados), donde cada sector y su tamaño indican la magnitud del
elemento que representa el sector. Los gráficos circulares se utilizan para comparar las
magnitudes de diferentes clases, categorías, productos y similares. Así es como se ve un
gráfico circular de muestra:

Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el
SDK de JavaFX desde aquí https://gluonhq.com/products/javafx/ e

pág. 657
incluir los JARs presente en la carpeta lib del SDK en la
ruta modular utilizando la -popción, como se muestra aquí:

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

Haremos uso de los mismos datos del estudiante (tomados del repositorio de
aprendizaje automático y procesados al final) que habíamos discutido en la
receta, Creación de una receta de gráfico de barras . Para esto, hemos creado un
módulo, student.processor que leerá los datos del alumno y nos proporcionará
una lista de Studentobjetos. El código fuente del módulo se puede encontrar
en Chapter16/101_student_data_processor. Hemos proporcionado el tarro
modular para el módulo student.processor
en Chapter16/5_pie_charts/mlibel código de esta receta.

Le recomendamos que cree el jar modular student.processor a partir de la


fuente disponible en Chapter16/101_student_data_processor. Hemos
proporcionado build-jar.baty build-jar.shguiones para ayudarlo a construir
el jar. Sólo tienes que ejecutar el script correspondiente a su plataforma y luego copiar el
frasco construido
en 101_student_data_processor/mliba 4_bar_charts/mlib.

Cómo hacerlo...
1. Primero creemos y configuremos GridPane para mantener nuestros gráficos
circulares:

GridPane gridPane = new GridPane();


gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setPadding(new Insets(25, 25, 25, 25));

2. Crear una instancia de StudentDataProcessor (que viene de


la student.processor módulo) y utilizarla para cargar List de Student:

StudentDataProcessor sdp = new StudentDataProcessor();


List<Student> students = sdp.loadStudent();

3. Ahora necesitamos obtener el recuento de estudiantes por las profesiones de sus


madres y padres. Escribiremos un método que tomará una lista de estudiantes y un

pág. 658
clasificador, es decir, la función que devuelve el valor en el que los estudiantes deben
agruparse. El método devuelve una instancia de PieChart:

private PieChart getStudentCountByOccupation(


List<Student> students,
Function<Student, String> classifier
){
Map<String, Long> occupationBreakUp =
students.stream().collect(
Collectors.groupingBy(
classifier,
Collectors.counting()
)
);
List<PieChart.Data> pieChartData = new ArrayList<>();
occupationBreakUp.forEach((k, v) -> {
pieChartData.add(new PieChart.Data(k.toString(), v));
});
PieChart chart = new PieChart(
FXCollections.observableList(pieChartData)
);
return chart;
}

4. Invocaremos el método anterior dos veces: uno con la ocupación de la madre como
clasificador y el otro con la ocupación del padre como clasificador. Luego agregamos
la instancia PieChart devuelta a gridPane. Esto debe hacerse desde dentro
del método start():

PieChart motherOccupationBreakUp =
getStudentCountByOccupation(
students, Student::getMotherJob
);
motherOccupationBreakUp.setTitle("Mother's Occupation");
gridPane.add(motherOccupationBreakUp, 1,1);

PieChart fatherOccupationBreakUp =
getStudentCountByOccupation(
students, Student::getFatherJob
);
fatherOccupationBreakUp.setTitle("Father's Occupation");
gridPane.add(fatherOccupationBreakUp, 2,1);

5. El siguiente paso es crear el gráfico de escena usando gridPane y agregarlo


a Stage:

Scene scene = new Scene(gridPane, 800, 600);


stage.setTitle("Pie Charts");
stage.setScene(scene);
stage.show();

6. La interfaz de usuario se puede iniciar desde el método principal invocando


el método Application.launch:

pág. 659
public static void main(String[] args) {
Application.launch(args);
}

El código completo se puede encontrar en Chapter16/5_pie_charts.

Hemos proporcionado dos scripts de ejecución run.bat y run.sh,


debajo Chapter16/5_pie_charts. El script run.bat será para ejecutar la
aplicación en Windows y run.sh será para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh y verá la siguiente GUI:

Cómo funciona...
El método más importante que hace todo el trabajo en esta receta
es getStudentCountByOccupation(). Hace lo siguiente:

1. Agrupa el número de estudiantes por profesión. Esto se puede hacer en una sola línea
de código utilizando la potencia de las nuevas API de transmisión (agregadas como
parte de Java 8):

Map <String, Long> ocupaciónBreakUp =


students.stream (). collect (
Collectors.groupingBy (
clasificador,
Collectors.counting ()

pág. 660
)
);

2. Datos de compilación necesarios para PieChart. Los datos PieChart de la


instancia son ObservableList de PieChart.Data. Primero hacemos uso de la Map
obtenida en el paso anterior para crear ArrayList de PieChart.Data. Luego,
usamos la API FXCollections.observableList()para
obtener ObservableList<PieChart.Data>de List<PieChart.Data>:

List <PieChart.Data> pieChartData = new ArrayList <> ();


cupationBreakUp.forEach ((k, v) -> {
pieChartData.add (nuevo PieChart.Data (k.toString (), v));
});
Gráfico de gráfico circular = nuevo gráfico circular (
FXCollections.observableList (pieChartData)
);

La otra cosa importante en la receta son los clasificadores que


usamos: Student::getMotherJoby Student::getFatherJob. Estas son las
dos referencias de métodos que invocan los métodos getMotherJob
y getFatherJob en las diferentes instancias de Studenten la lista de Student.

Una vez que obtenemos las instancias PieChart, las agregamos GridPane y luego
construimos el gráfico de escena usando GridPane. Se debe asociar el gráfico de
escena Stage para que se muestre en la pantalla.

El método principal inicia la IU invocando el método


Application.launch(args);.

JavaFX proporciona API para crear diferentes tipos de gráficos, como los siguientes:

• Cartas de área
• Gráficos de burbujas
• Gráficos de líneas
• Tablas de dispersión

Todos estos gráficos son gráficos basados en ejes X e Y y se pueden construir como un
gráfico de barras. Hemos proporcionado algunas implementaciones de ejemplo para
crear estos tipos de gráficos, y que se puede encontrar en las siguientes
ubicaciones: Chapter16/5_2_area_charts, Chapter16/5_3_line_charts,
Chapter16/5_4_bubble_charts, y Chapter16/5_5_scatter_charts.

Incrustar HTML en una


aplicación
pág. 661
JavaFX proporciona soporte para administrar páginas web a través de las clases
definidas en el paquete javafx.scene.web. Admite cargar la página web, ya sea
aceptando la URL de la página web o aceptando el contenido de la página web. También
gestiona el modelo de documento de la página web, aplica el CSS relevante y ejecuta el
código JavaScript relevante. También amplía el soporte para una comunicación
bidireccional entre JavaScript y el código Java.

En esta receta, crearemos un navegador web muy primitivo y simple que admita lo
siguiente:

• Navegando por el historial de las páginas visitadas


• Recargando la página actual
• Una barra de direcciones para aceptar la URL
• Un botón para cargar la URL ingresada
• Mostrando la página web
• Mostrar el estado de carga de la página web

Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el SDK
de JavaFX desde aquí https://gluonhq.com/products/javafx/ e incluir los
JARs presente en la carpeta lib del SDK en la ruta modular utilizando la -popción,
como se muestra aquí:

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

Necesitaremos una conexión a Internet para probar la carga de páginas. Por lo tanto,
asegúrese de estar conectado a Internet. Aparte de esto, no se requiere nada específico
para trabajar con esta receta.

Cómo hacerlo...
1. Primero creemos una clase con métodos vacíos, que representaría la aplicación
principal para iniciar la aplicación, así como la interfaz de usuario JavaFX:

public class BrowserDemo extends Application{


public static void main(String[] args) {

pág. 662
Application.launch(args);
}
@Override
public void start(Stage stage) {
//this will have all the JavaFX related code
}
}

En los pasos siguientes, escribiremos todo nuestro código dentro del método
start(Stage stage).

2. Creemos un componente javafx.scene.web.WebView, que representará


nuestra página web. Esto tiene la j instancia avafx.scene.web.WebEngine
requerida , que gestiona la carga de la página web:

WebView webView = nuevo WebView ();

3. Obtenga la instancia de javafx.scene.web.WebEngine usado


por webView. Utilizaremos esta instancia de javafx.scene.web.WebEngine para
navegar por el historial y cargar otras páginas web. Luego, por defecto, cargaremos la
URL, http://www.google.com :

WebEngine webEngine = webView.getEngine ();


webEngine.load ("http://www.google.com/");

4. Ahora creemos un componente javafx.scene.control.TextField, que


actuará como la barra de direcciones de nuestro navegador:

TextField webAddress = new


TextField ("http://www.google.com/");

5. Queremos cambiar el título del navegador y la página web en la barra de direcciones,


en función del título y la URL de la página web completamente cargada. Esto se puede hacer
al escuchar el cambio en la stateProperty del javafx.concurrent.Worker
obtenido a partir del ejemplo javafx.scene.web.WebEngine:

webEngine.getLoadWorker().stateProperty().addListener(
new ChangeListener<State>() {
public void changed(ObservableValue ov,
State oldState, State newState) {
if (newState == State.SUCCEEDED) {
stage.setTitle(webEngine.getTitle());
webAddress.setText(webEngine.getLocation());
}
}
}
);

6. Creemos una instancia javafx.scene.control.Button que, al hacer clic,


cargue la página web identificada por la URL ingresada en la barra de direcciones:

pág. 663
Button goButton = new Button("Go");
goButton.setOnAction((event) -> {
String url = webAddress.getText();
if ( url != null && url.length() > 0){
webEngine.load(url);
}
});

7. Creemos una instancia javafx.scene.control.Button que, al hacer clic,


vaya a la página web anterior en el historial. Para lograr esto, ejecutaremos el código
JavaScript history.back()desde el controlador de acciones:

Button prevButton = new Button("Prev");


prevButton.setOnAction(e -> {
webEngine.executeScript("history.back()");
});

8. Creemos una instancia javafx.scene.control.Button que, al hacer clic,


vaya a la siguiente entrada en el historial mantenido por instancia
javafx.scene.web.WebEngine. Para esto, haremos uso de la API
javafx.scene.web.WebHistory:

Button nextButton = new Button("Next");


nextButton.setOnAction(e -> {
WebHistory wh = webEngine.getHistory();
Integer historySize = wh.getEntries().size();
Integer currentIndex = wh.getCurrentIndex();
if ( currentIndex < (historySize - 1)){
wh.go(1);
}
});

9. El siguiente es el botón para volver a cargar la página actual. Nuevamente,


usaremos javafx.scene.web.WebEngine para recargar la página actual:

Button reloadButton = new Button("Refresh");


reloadButton.setOnAction(e -> {
webEngine.reload();
});

10. Ahora tenemos que agrupar todos los componentes creados hasta ahora, a
saber, prevButton, nextButton, reloadButton, webAddress, y goButton,
para que se alineen horizontalmente entre sí. Para lograr esto, haremos uso de espacios y
rellenos javafx.scene.layout.HBox relevantes para que los componentes se vean
bien espaciados:

HBox addressBar = new HBox(10);


addressBar.setPadding(new Insets(10, 5, 10, 5));
addressBar.setHgrow(webAddress, Priority.ALWAYS);
addressBar.getChildren().addAll(
prevButton, nextButton, reloadButton, webAddress, goButton

pág. 664
);

11. Nos gustaría saber si la página web se está cargando y si ha terminado. Creemos
un campo javafx.scene.layout.Label para actualizar el estado si se carga la
página web. Luego, escuchamos las actualizaciones workDoneProperty de
la instancia javafx.concurrent.Worker, que podemos obtener de la instancia
javafx.scene.web.WebEngine:

Label websiteLoadingStatus = new Label();


webEngine
.getLoadWorker()
.workDoneProperty()
.addListener(
new ChangeListener<Number>(){

public void changed(


ObservableValue ov,
Number oldState,
Number newState
) {
if (newState.doubleValue() != 100.0){
websiteLoadingStatus.setText(
"Loading " + webAddress.getText());
}else{
websiteLoadingStatus.setText("Done");
}
}

}
);

12. Alineemos toda la barra de direcciones (con sus botones de navegación)


verticalmente webView, y websiteLoadingStatus:

VBox root = new VBox();


root.getChildren().addAll(
addressBar, webView, websiteLoadingStatus
);

13. Cree un nuevo objeto Scene con la VBoxinstancia creada en el paso anterior como
raíz:

Scene scene = new Scene(root);

14. Queremos que la instancia javafx.stage.Stage ocupe el tamaño de pantalla


completo; para esto, haremos uso
de Screen.getPrimary().getVisualBounds(). Luego, como de costumbre,
representaremos el gráfico de escena en el escenario:

Rectangle2D primaryScreenBounds =
Screen.getPrimary().getVisualBounds();
stage.setTitle("Web Browser");

pág. 665
stage.setScene(scene);
stage.setX(primaryScreenBounds.getMinX());
stage.setY(primaryScreenBounds.getMinY());
stage.setWidth(primaryScreenBounds.getWidth());
stage.setHeight(primaryScreenBounds.getHeight());
stage.show();

El código completo se puede encontrar en el Chapter16/6_embed_html.

Hemos proporcionado dos scripts de ejecución run.bat y run.sh,


debajo Chapter16/6_embed_html. El run.bat script será para ejecutar la
aplicación en Windows y run.shserá para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh, y verá la siguiente GUI:

Cómo funciona...
pág. 666
Las API relacionadas con la web están disponibles en el módulo javafx.web, por lo
que tendremos que solicitarlo en module-info:

module gui{
requires javafx.controls;
requires javafx.web;
opens com.packt;
}

Las siguientes son las clases importantes en el paquete javafx.scene. web


cuando se trata de páginas web en JavaFX:

• WebView: Este componente de la interfaz de usuario se utiliza WebEngine para


administrar la carga, el procesamiento y la interacción con la página web.
• WebEngine: Este es el componente principal que se ocupa de cargar y administrar
la página web.
• WebHistory: Esto registra las páginas web visitadas en
la WebEngineinstancia actual .
• WebEvent: Estas son las instancias pasadas a los controladores de
eventos WebEngine invocados por el evento JavaScript.

En nuestra receta, hacemos uso de las primeras tres clases.

No creamos directamente una instancia de WebEngine; en su lugar,


utilizamos WebView para obtener una referencia a la instancia WebEngine
administrada por él. La instancia WebEngine carga la página web de forma
asincrónica enviando la tarea de cargar la página a las instancias
javafx.concurrent.Worker. Luego, registramos escuchas de cambio en estas
propiedades de instancia de trabajador para rastrear el progreso de la carga de la
página web. Hemos utilizado dos de esas propiedades en esta receta, a
saber, statePropertyy workDoneProperty. El primero rastrea el cambio del
estado del trabajador, y el segundo rastrea el porcentaje del trabajo realizado.

Un trabajador puede pasar por los siguientes estados (como se enumeran en


la enumeración javafx.concurrent.Worker.State):

• CANCELLED
• FAILED
• READY
• RUNNING
• SCHEDULED
• SUCCEEDED

En nuestra receta, solo estamos verificando SUCCEEDED, pero también puede


mejorarla para verificar FAILED. Esto nos ayudará a informar URL no válidas o
incluso a obtener el mensaje del objeto del evento y mostrárselo al usuario.

pág. 667
La forma en que añadimos a los oyentes a seguir el cambio en las propiedades es
mediante el uso del método addListener()en *Property()donde *puede
ser state, workDone o cualquier otro atributo del trabajador que ha sido expuesta
como una propiedad:

webEngine
.getLoadWorker()
.stateProperty()
.addListener(
new ChangeListener<State>() {
public void changed(ObservableValue ov,
State oldState, State newState) {
//event handler code here
}
}
);

webEngine
.getLoadWorker()
.workDoneProperty()
.addListener(
new ChangeListener<Number>(){
public void changed(ObservableValue ov,
Number oldState, Number newState) {
//event handler code here
}
}
);

Entonces el componente javafx.scene.web.WebEngine también admite lo


siguiente:

• Recargando la página actual


• Obtener el historial de las páginas cargadas por él
• Ejecutando el código JavaScript
• Escuchar las propiedades de JavaScript, como mostrar un cuadro de alerta o un
cuadro de confirmación
• Interactuando con el modelo de documento de la página web usando
el getDocument()método

En esta receta, también analizamos el uso WebHistory obtenido


de WebEngine. WebHistory almacena las páginas web cargadas por la instancia
WebEngine dada , lo que significa que una instancia WebEngine tendrá una instancia
WebHistory. WebHistory admite lo siguiente:

• Obtener la lista de entradas utilizando el método getEntries()Esto también nos


dará la cantidad de entradas en el historial. Esto es necesario mientras se navega hacia
adelante y hacia atrás en la historia; de lo contrario, terminaremos con una excepción
de índice fuera de límites.
• Obteniendo currentIndex, es decir, su índice dentro de la lista
getEntries().
pág. 668
• Navegando a la entrada específica en la lista de entradas de WebHistory. Esto se
puede lograr utilizando el método go(), que acepta un desplazamiento. Este
desplazamiento indica qué página web cargar, en relación con la posición actual. Por
ejemplo, +1 indica la siguiente entrada y -1 indica la entrada anterior. Es importante
verificar las condiciones de contorno; de lo contrario, terminará yendo antes de 0 , es
decir, -1 , o pasando el tamaño de la lista de entrada.

Hay más...
En esta receta, le mostramos un enfoque básico para crear un navegador web,
utilizando el soporte proporcionado por JavaFX. Puede mejorar esto para admitir lo
siguiente:

• Mejor manejo de errores y mensajes de usuario, es decir, para mostrar si la dirección


web es válida al rastrear el cambio de estado del trabajador
• Pestañas múltiples
• Marcadores
• Almacenar el estado del navegador localmente para que la próxima vez que se ejecute
cargue todos los marcadores y el historial

Incrustar medios en una


aplicación
JavaFX proporciona un componente javafx.scene.media.MediaView para ver
videos y escuchar audios. Este componente está respaldado por un motor de
medios javafx.scene.media.MediaPlayer, que carga y administra la
reproducción de los medios.

En esta receta, veremos cómo reproducir un video de muestra y controlar su


reproducción mediante el uso de los métodos en el motor de medios.

Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el SDK
de JavaFX desde aquí https://gluonhq.com/products/javafx/ e incluir los
JARs presente en la carpeta del SDK en la ruta modular utilizando la opción que se
muestra aquí: lib-p

pág. 669
javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

Haremos uso del video de muestra disponible


en Chapter16/7_embed_audio_video/sample_video1.mp4.

Cómo hacerlo...
1. Primero creemos una clase con métodos vacíos, que representaría la aplicación
principal para iniciar la aplicación, así como la interfaz de usuario JavaFX:

public class EmbedAudioVideoDemo extends Application{


public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
//this will have all the JavaFX related code
}
}

2. Crea un objeto javafx.scene.media.Media para el video ubicado


en Chapter16/7_embed_audio_video/sample_video1.mp4:

File file = new File("sample_video1.mp4");


Media media = new Media(file.toURI().toString());

3. Cree un nuevo motor de medios javafx.scene.media.MediaPlayer,


utilizando el objeto javafx.scene.media.Media creado en el paso anterior:

MediaPlayer mediaPlayer = new MediaPlayer(media);

4. Rastreemos el estado del reproductor multimedia registrando un detector de


cambios en statusProperty el objetojavafx.scene.media.MediaPlayer:

mediaPlayer.statusProperty().addListener(
new ChangeListener<Status>() {
public void changed(ObservableValue ov,
Status oldStatus, Status newStatus) {
System.out.println(oldStatus +"->" + newStatus);
}
});

pág. 670
5. Ahora creemos un visor de medios usando el motor de medios creado en el paso
anterior:

MediaView mediaView = new MediaView(mediaPlayer);

6. Restringiremos el ancho y la altura del visor de medios:

mediaView.setFitWidth(350);
mediaView.setFitHeight(350);

7. A continuación, creamos tres botones para pausar la reproducción de video,


reanudar la reproducción y detener la reproducción. Haremos uso de los métodos relevantes
en la clasejavafx.scene.media.MediaPlayer:

Button pauseB = new Button("Pause");


pauseB.setOnAction(e -> {
mediaPlayer.pause();
});

Button playB = new Button("Play");


playB.setOnAction(e -> {
mediaPlayer.play();
});

Button stopB = new Button("Stop");


stopB.setOnAction(e -> {
mediaPlayer.stop();
});

8. Alinee todos estos botones horizontalmente


usando javafx.scene.layout.HBox:

HBox controlsBox = new HBox(10);


controlsBox.getChildren().addAll(pauseB, playB, stopB);

9. Alinee el visor de medios y la barra de botones verticalmente


usando javafx.scene.layout.VBox:

VBox vbox = new VBox();


vbox.getChildren().addAll(mediaView, controlsBox);

10. Cree un nuevo gráfico de escena utilizando el VBoxobjeto como raíz y configúrelo
en el objeto de escenario:

Scene scene = new Scene(vbox);


stage.setScene(scene);
// Name and display the Stage.
stage.setTitle("Media Demo");

11. Renderice el escenario en la pantalla:

stage.setWidth(400);

pág. 671
stage.setHeight(400);
stage.show();

El código completo se puede encontrar en Chapter16/7_embed_audio_video.

Hemos proporcionado dos scripts de ejecución run.baty run.sh,


debajo Chapter16/7_embed_audio_video. El run.bat script será para ejecutar
la aplicación en Windows y run.shserá para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh, y verá la siguiente GUI:

Cómo funciona...
Las clases importantes en el paquete javafx.scene.media para la reproducción
de medios son las siguientes:

• Media: Esto representa la fuente de los medios, es decir, video o audio. Esto acepta
la fuente en forma de URL HTTP / HTTPS / FILE y JAR.
• MediaPlayer: Esto gestiona la reproducción de los medios.
• MediaView: Este es el componente de la interfaz de usuario que permite ver los
medios.

Hay algunas otras clases, pero no las hemos cubierto en esta receta. Las clases
relacionadas con los medios están en el módulo javafx.media. Por lo tanto, no
olvide requerir una dependencia de él, como se muestra aquí:

module gui{
requires javafx.controls;
requires javafx.media;
opens com.packt;

pág. 672
}

En esta receta, tenemos un video de muestra


en Chapter16/7_embed_audio_video/sample_video1.mp4, y hacemos uso
de la API java.io.File para construir URL File para ubicar el video:

File file = new File("sample_video1.mp4");


Media media = new Media(file.toURI().toString());

La reproducción de medios se gestiona utilizando la API expuesta por la clase


javafx.scene.media.MediaPlayer. En esta receta, hicimos uso de algunos de
sus métodos, a saber play(), pause(), y stop(). La clase
javafx.scene.media.MediaPlayer se inicializa usando el objeto
javafx.scene.media.Media:

MediaPlayer mediaPlayer = new MediaPlayer(media);

La javafx.scene.media.MediaView clase gestiona el procesamiento de los


medios en la interfaz de usuario y está respaldado por un objeto
javafx.scene.media.MediaPlayer:

MediaView mediaView = new MediaView(mediaPlayer);

Podemos establecer la altura y el ancho del visor utilizando


los métodos setFitWidth() y setFitHeight().

Hay más...
Dimos una demostración básica de soporte de medios en JavaFX. Hay mucho más por
explorar. Puede agregar opciones de control de volumen, opciones para buscar hacia
adelante o hacia atrás, reproducir audios y ecualizador de audio.

Agregar efectos a los controles


Agregar efectos de forma controlada da una buena apariencia a la interfaz de
usuario. Existen múltiples efectos como desenfoque, sombras, reflejo, floración,
etc. JavaFX proporciona un conjunto de clases bajo el paquete
javafx.scene.effects, que se pueden usar para agregar efectos para mejorar el
aspecto de la aplicación. Este paquete está disponible en el módulo
javafx.graphics .

En esta receta, veremos algunos efectos: desenfoque, sombra y reflejo.

pág. 673
Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el SDK
de JavaFX desde aquí https://gluonhq.com/products/javafx/ e incluir los
JARs presente en la carpeta del SDK en la ruta modular utilizando la opción que se
muestra a continuación: lib-p

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

Cómo hacerlo...
1. Primero creemos una clase con métodos vacíos, que representaría la aplicación
principal para iniciar la aplicación, así como la interfaz de usuario JavaFX:

public class EffectsDemo extends Application{


public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
//code added here in next steps
}
}

2. El código posterior se escribirá dentro del método start(Stage stage). Crea


y configura javafx.scene.layout.GridPane:

GridPane gridPane = new GridPane();


gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setPadding(new Insets(25, 25, 25, 25));

3. Cree rectángulos necesarios para aplicar los efectos de desenfoque:

Rectangle r1 = new Rectangle(100,25, Color.BLUE);


Rectangle r2 = new Rectangle(100,25, Color.RED);
Rectangle r3 = new Rectangle(100,25, Color.ORANGE);

pág. 674
4. Añadir javafx.scene.effect.BoxBlura Rectangle
r1, javafx.scene.effect.MotionBlura Rectangle
r2y javafx.scene.effect.GaussianBlura Rectangle r3:

r1.setEffect(new BoxBlur(10,10,3));
r2.setEffect(new MotionBlur(90, 15.0));
r3.setEffect(new GaussianBlur(15.0));

5. Agregue los rectángulos a gridPane:

gridPane.add(r1,1,1);
gridPane.add(r2,2,1);
gridPane.add(r3,3,1);

6. Crea tres círculos, necesarios para aplicar sombras:

Circle c1 = new Circle(20, Color.BLUE);


Circle c2 = new Circle(20, Color.RED);
Circle c3 = new Circle(20, Color.GREEN);

7. Añadir javafx.scene.effect.DropShadowa c1y javafx.scene.eff


ect.InnerShadowa c2:

c1.setEffect(new DropShadow(0, 4.0, 0, Color.YELLOW));


c2.setEffect(new InnerShadow(0, 4.0, 4.0, Color.ORANGE));

8. Agrega estos círculos a gridPane:

gridPane.add(c1,1,2);
gridPane.add(c2,2,2);
gridPane.add(c3,3,2);

9. Cree un texto simple Reflection Sample, sobre el cual aplicaremos el efecto


de reflexión:

Text t = new Text("Reflection Sample");


t.setFont(Font.font("Arial", FontWeight.BOLD, 20));
t.setFill(Color.BLUE);

10. Cree un efecto javafx.scene.effect.Reflection y agréguelo al texto:

Reflection reflection = new Reflection();


reflection.setFraction(0.8);
t.setEffect(reflection);

11. Agregue el componente de texto a gridPane:

gridPane.add(t, 1, 3, 3, 1);

12. Cree un gráfico de escena utilizando gridPane como nodo raíz:


13. Scene scene = new Scene(gridPane, 500, 300);

pág. 675
13. Establezca el gráfico de escena en el escenario y renderícelo en la pantalla:

stage.setScene(scene);
stage.setTitle("Effects Demo");
stage.show();

El código completo se puede encontrar en Chapter16/8_effects_demo.

Hemos proporcionado dos scripts de ejecución run.bat y run.sh,


debajo Chapter16/8_effects_demo. El script run.bat será para ejecutar la
aplicación en Windows y run.sh será para ejecutar la aplicación en Linux.

Ejecute la aplicación usando run.bato run.sh y verá la siguiente GUI:

Cómo funciona...
En esta receta, hemos utilizado los siguientes efectos:

• javafx.scene.effect.BoxBlur
• javafx.scene.effect.MotionBlur
• javafx.scene.effect.GaussianBlur
• javafx.scene.effect.DropShadow
• javafx.scene.effect.InnerShadow
• javafx.scene.effect.Reflection

pág. 676
El BoxBlurefecto se crea especificando el ancho y la altura del efecto de desenfoque,
y también la cantidad de veces que se debe aplicar el efecto:

BoxBlur boxBlur = new BoxBlur(10,10,3);

El efecto MotionBlur se crea al proporcionar el ángulo del desenfoque y su


radio. Esto da el efecto de algo capturado mientras está en movimiento:

MotionBlur motionBlur = new MotionBlur(90, 15.0);

El efecto GaussianBlur se crea al proporcionar el radio del efecto, y el efecto usa la


fórmula gaussiana para aplicar el efecto:

GaussianBlur gb = new GaussianBlur(15.0);

DropShadowagrega la sombra detrás del objeto, mientras que InnerShadow agrega


la sombra dentro del objeto. Cada uno de estos toma el radio de la sombra,
la ubicación x e y del inicio de la sombra y el color de la sombra:

DropShadow dropShadow = new DropShadow(0, 4.0, 0, Color.YELLOW);


InnerShadow innerShadow = new InnerShadow(0, 4.0, 4.0, Color.ORANGE);

Reflection Es un efecto bastante simple que agrega el reflejo del objeto. Podemos
establecer la fracción de cuánto se refleja el objeto original:

Reflection reflection = new Reflection();


reflection.setFraction(0.8);

Hay más...
Hay bastantes efectos más:

• El efecto de fusión, que combina dos entradas diferentes con un enfoque de fusión
predefinido
• El efecto de floración, que hace que las partes más brillantes parezcan más brillantes
• El efecto de brillo, que hace que el objeto brille
• El efecto de iluminación, que simula una fuente de luz en el objeto, dándole una
apariencia 3D.

Le recomendamos que pruebe estos efectos de la misma manera que los hemos
probado.

Usando la API Robot


pág. 677
Robot API se utiliza para simular acciones del teclado y el mouse en la pantalla, lo
que significa que debe indicar al código que escriba texto en el campo de texto, elija
una opción y luego haga clic en un botón. Las personas que provienen de la Web de
pruebas de IU pueden relacionar esto con la Biblioteca de Pruebas de
Selenium. Abstract Window Toolkit ( AWT ), que es un kit de herramientas de
ventanas más antiguo en JDK, proporciona Robot API, pero usar la misma API en
JavaFX no es sencillo y requiere algunos hacks. El kit de herramientas de la ventana
JavaFX llamado Glass tiene sus propias API de robot
( https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/robot/Robot.html ), pero no
son públicas. Entonces, como parte de la versión OpenJFX 11, se introdujeron nuevas
API públicas para lo mismo.

En esta receta, veremos cómo usar la API Robot para simular algunas acciones en la
interfaz de usuario JavaFX.

Prepararse
Como sabemos que las bibliotecas JavaFX no se envían en la instalación de JDK desde
Oracle JDK 11 en adelante y Open JDK 10 en adelante, tendremos que descargar el SDK
de JavaFX desde aquí ( https://gluonhq.com/products/javafx/ ) e incluir los
JAR presentes en la carpeta lib del SDK en la ruta modular, utilizando la -popción, que
se muestra a continuación:

javac -p "PATH_TO_JAVAFX_SDK_LIB" <other parts of the command line>

#Windows
java -p "PATH_TO_JAVAFX_SDK_LIB;COMPILED_CODE" <other parts of the command
line>
#Linux
java -p "PATH_TO_JAVAFX_SDK_LIB:COMPILED_CODE" <other parts of the command
line>

En esta receta, crearemos una aplicación simple que acepte un nombre del usuario y, al
hacer clic en un botón, imprima un mensaje para el usuario. Toda esta operación se
simulará con la API de Robot y, finalmente, antes de salir de la aplicación, capturaremos
la pantalla con la API de Robot.

Cómo hacerlo...
1. Cree una clase simple, RobotApplication que
amplíe javafx.application.Application y configure la IU requerida
para probar la API de Robot y también cree una instancia
de javafx.scene.robot.Robot. Esta clase se definirá como una clase
interna estática para la clase RobotAPIDemo principal:

pág. 678
2. public static class RobotApplication extends Application{

@Override
public void start(Stage stage) throws Exception{
robot = new Robot();
GridPane gridPane = new GridPane();
gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setPadding(new Insets(25, 25, 25, 25));

Text appTitle = new Text("Robot Demo");


appTitle.setFont(Font.font("Arial",
FontWeight.NORMAL, 15));
gridPane.add(appTitle, 0, 0, 2, 1);

Label nameLbl = new Label("Name");


nameField = new TextField();
gridPane.add(nameLbl, 0, 1);
gridPane.add(nameField, 1, 1);

greeting = new Button("Greet");


gridPane.add(greeting, 1, 2);

Text resultTxt = new Text();


resultTxt.setFont(Font.font("Arial",
FontWeight.NORMAL, 15));
gridPane.add(resultTxt, 0, 5, 2, 1);

greeting.setOnAction((event) -> {

String name = nameField.getText();


StringBuilder resultBuilder = new StringBuilder();
if ( name != null && name.length() > 0 ){
resultBuilder.append("Hello, ")
.append(name).append("\n");
}else{
resultBuilder.append("Please enter the name");
}
resultTxt.setText(resultBuilder.toString());
btnActionLatch.countDown();
});

Scene scene = new Scene(gridPane, 300, 250);

stage.setTitle("Age calculator");
stage.setScene(scene);
stage.setAlwaysOnTop(true);
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
Platform.runLater(appStartLatch::countDown));
stage.show();
appStage = stage;
}
}

2. Como la UI de JavaFX se lanzará en un subproceso de aplicación JavaFX diferente


y habrá algunos retrasos en la representación de la UI por completo antes de ejecutar los

pág. 679
comandos para interactuar con la UI, haremos uso
de java.util.concurrent.CountDownLatch para indicar diferentes
eventos. Para trabajar CountDownLatch, creamos un método auxiliar estático simple
con la siguiente definición en la clase RobotAPIDemo:

public static void waitForOperation(


CountDownLatch latchToWaitFor,
int seconds, String errorMsg) {
try {
if (!latchToWaitFor.await(seconds,
TimeUnit.SECONDS)) {
System.out.println(errorMsg);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}

3. El método typeName() es el método auxiliar que escribe el nombre de la


persona en el campo de texto:

public static void typeName(){


Platform.runLater(() -> {
Bounds textBoxBounds = nameField.localToScreen(
nameField.getBoundsInLocal());
robot.mouseMove(textBoxBounds.getMinX(),
textBoxBounds.getMinY());
robot.mouseClick(MouseButton.PRIMARY);
robot.keyType(KeyCode.CAPS);
robot.keyType(KeyCode.S);
robot.keyType(KeyCode.CAPS);
robot.keyType(KeyCode.A);
robot.keyType(KeyCode.N);
robot.keyType(KeyCode.A);
robot.keyType(KeyCode.U);
robot.keyType(KeyCode.L);
robot.keyType(KeyCode.L);
robot.keyType(KeyCode.A);
});
}

4. El método clickButton() es el método auxiliar; hace clic en el botón correcto


para activar la visualización del mensaje de saludo:

public static void clickButton(){


Platform.runLater(() -> {
//click the button
Bounds greetBtnBounds = greeting
.localToScreen(greeting.getBoundsInLocal());

robot.mouseMove(greetBtnBounds.getCenterX(),
greetBtnBounds.getCenterY());
robot.mouseClick(MouseButton.PRIMARY);
});
}

pág. 680
5. El método captureScreen() es el método auxiliar para tomar una captura de
pantalla de la aplicación y guardarla en el sistema de archivos:

public static void captureScreen(){


Platform.runLater(() -> {
try{

WritableImage screenCapture =
new WritableImage(
Double.valueOf(appStage.getWidth()).intValue(),
Double.valueOf(appStage.getHeight()).intValue()
);

robot.getScreenCapture(screenCapture,
appStage.getX(), appStage.getY(),
appStage.getWidth(), appStage.getHeight());

BufferedImage screenCaptureBI =
SwingFXUtils.fromFXImage(screenCapture, null);
String timePart = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-dd-M-m-H-ss"));
ImageIO.write(screenCaptureBI, "png",
new File("screenCapture-" + timePart +".png"));
Platform.exit();
}catch(Exception ex){
ex.printStackTrace();
}
});
}

6. Vincularemos el lanzamiento de la IU y los métodos auxiliares creados en el método


main(), de la siguiente manera:

public static void main(String[] args)


throws Exception{
new Thread(() -> Application.launch(
RobotApplication.class, args)).start();

waitForOperation(appStartLatch, 10,
"Timed out waiting for JavaFX Application to Start");
typeName();
clickButton();
waitForOperation(btnActionLatch, 10,
"Timed out waiting for Button to complete operation");
Thread.sleep(1000);
captureScreen();
}

El código completo para esto se puede encontrar


en Chapter16/9_robot_api. Puede ejecutar la muestra
utilizando run.bato run.sh. La ejecución de la aplicación iniciará la IU, ejecutará
las acciones, tomará una captura de pantalla y saldrá de la aplicación. La captura de
pantalla se colocará en la carpeta desde la que se inició la aplicación, y seguiría la
convención de nomenclatura screenCapture-yyyy-dd-M-m-H-ss.png. Aquí
hay una captura de pantalla de muestra:

pág. 681
Cómo funciona...
Como la aplicación JavaFX se ejecuta en un subproceso diferente, debemos asegurarnos
de que las operaciones de la API del robot estén ordenadas correctamente y que las
acciones de la API del robot se ejecuten solo cuando se muestre la IU completa. Para
garantizar esto, hemos utilizado java.util.concurrent.CountDownLatch
para comunicar eventos como los siguientes:

• Carga completa de la IU
• Finalización de la ejecución de la acción definida para el botón.

La comunicación sobre la finalización de la carga de la interfaz de usuario se logra


mediante el uso de CountDownLatch, como sigue:

# Declaration of the latch


static public CountDownLatch appStartLatch = new CountDownLatch(1);

# Using the latch


stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
Platform.runLater(appStartLatch::countDown));

El método countDown()se invoca en el Stagecontrolador de eventos cuando se


muestra la ventana, lo que libera el pestillo y desencadena la ejecución del siguiente
bloque de código en el método principal:

typeName();
clickButton();

El hilo principal vuelve a bloquearse de esperar a btnActionLatch que se


libere. El btnActionLatch se libera después de completar la acción en el saludo

pág. 682
del botón. Una vez que btnActionLatchse libera, el hilo principal continúa la
ejecución para invocar el método captureScreen().

Analicemos algunos de los métodos que hemos usado de la clase


javafx.scene.robot.Robot:

mouseMove(): Este método se utiliza para mover el cursor del mouse a una ubicación
determinada identificada a partir de sus coordenadas x e y . Hemos utilizado la
siguiente línea de código para obtener los límites del componente:

Bounds textBoxBounds =
nameField.localToScreen(nameField.getBoundsInLocal());

Los límites de un componente contienen lo siguiente:

• Las coordenadas x e y superior izquierda


• Las coordenadas x e y inferior derecha
• El ancho y la altura del componente.

Entonces, para nuestro caso de uso de Robot API, utilizamos


las coordenadas x e y superior izquierda , que se muestran a continuación:

robot.mouseMove(textBoxBounds.getMinX(), textBoxBounds.getMinY());

mouseClick(): Este método se usa para hacer clic en los botones del mouse. Los
botones del mouse se identifican por lo siguiente enums
en javafx.scene.input.MouseButton enum:

• PRIMARY: Representa el clic izquierdo del mouse


• SECONDARY: Representa el clic derecho del mouse
• MIDDLE: Representa el desplazamiento del mouse, o el botón central.

Entonces, para poder usar mouseClick(), necesitamos mover la ubicación del


componente en el que necesitamos realizar la operación de clic. En nuestro caso, como
se ve en la implementación del método typeName(), nos movemos a la ubicación del
campo de texto usando mouseMove()y luego invocamos mouseClick(), como se
muestra a continuación:

robot.mouseMove(textBoxBounds.getMinX(),
textBoxBounds.getMinY());
robot.mouseClick(MouseButton.PRIMARY);

keyType(): Este método se utiliza para escribir caracteres en componentes que


aceptan entrada de texto. Los caracteres a escribir están representados por las
enumeraciones en la enumeración javafx.scene.input.KeyCode. En

pág. 683
la typeName()implementación de nuestro método, escribimos la cadena Sanaulla,
que se muestra a continuación:

robot.keyType(KeyCode.CAPS);
robot.keyType(KeyCode.S);
robot.keyType(KeyCode.CAPS);
robot.keyType(KeyCode.A);
robot.keyType(KeyCode.N);
robot.keyType(KeyCode.A);
robot.keyType(KeyCode.U);
robot.keyType(KeyCode.L);
robot.keyType(KeyCode.L);
robot.keyType(KeyCode.A);

getScreenCapture(): Este método se utiliza para tomar la captura de pantalla de


la aplicación. El área para capturar la captura de pantalla está determinada por
las coordenadas x e y y la información de ancho y alto que se pasa al método. La imagen
capturada se convierte java.awt.image.BufferedImage y se guarda en el
sistema de archivos, como se muestra en el siguiente código:

WritableImage screenCapture = new WritableImage(


Double.valueOf(appStage.getWidth()).intValue(),
Double.valueOf(appStage.getHeight()).intValue()
);
robot.getScreenCapture(screenCapture,
appStage.getX(), appStage.getY(),
appStage.getWidth(), appStage.getHeight());

BufferedImage screenCaptureBI =
SwingFXUtils.fromFXImage(screenCapture, null);
String timePart = LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyy-dd-M-m-H-ss"));
ImageIO.write(screenCaptureBI, "png",
new File("screenCapture-" + timePart +".png"));

Referencias
[1] Java Projects – Second Edition

pág. 684

También podría gustarte