L1
L1
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.
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.
ISBN 978-1-78913-235-9
www.packtpub.com
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.
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.
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.
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.
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.
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 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.
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.
Convenciones utilizadas
pág. 3
Hay una serie de convenciones de texto utilizadas en este libro.
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);
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 " .
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.
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.
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!
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í:
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:
pág. 7
sistema buscará la herramienta relevante en todas las ubicaciones declaradas en
la variable PATH de entorno.
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.
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.
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.
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:
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:
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";
}
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.
PARA WINDOWS:
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 .
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:
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
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.
var n = nombres.length;
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.
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;
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.
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:
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.
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");
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.
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:
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.
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) {
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:
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.
Solo hay un comando condicional en Java, el comando if. Tiene el siguiente formato:
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.
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.
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.
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]
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:
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:
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;
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.
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.
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]
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.
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.
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.
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.
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.
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.
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.
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.
El Capítulo 10 , Redes , explica esta característica con más detalle en varias recetas.
Nueva API
Hay varias adiciones a la API estándar de Java:
var s = Character.toString(50);
System.out.println(s); //prints: 2
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
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
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
• Tres métodos de la clase que eliminan el espacio inicial, el espacio final o ambos
del valor de origen : StringString
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();
}
pág. 44
Hay más...
Hay bastantes otros cambios introducidos en JDK 18.9:
Lea las notas de la versión de Java 11 (JDK 18.9) para obtener más detalles y otros
cambios.
pág. 45
Prepararse
Las ventajas de cargar clases desde el archivo compartido se hicieron posibles por dos
razones:
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
/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home/lib/server
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.
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.
5. Una vez que se crea la lista, use el siguiente comando para generar el archivo
compartido:
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:
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.
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.
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.
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:
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 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:
if(engine == null){
if (motor == nulo) {
arroja una nueva RuntimeException ("Engine" + "es un
parámetro obligatorio");
}
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.
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í:
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.
• 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:
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
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:
pág. 55
+ vehicle.getSpeedMph(timeSec) + " mph");
}
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.
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í:
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.
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):
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.
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.
Cómo hacerlo...
1. Mira la clase Vehicle:
pág. 59
3. Crea la clase Truck:
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:
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.
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.
En el código anterior, hemos asumido que un pasajero con equipaje pesa un total de
250 libras en promedio.
Los resultados de estas modificaciones (si ejecutamos la misma clase de prueba) serán
los siguientes:
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:
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:
Dado que este método solo lo usan las subclases, puede ser protected y, por lo tanto,
accesible solo para las subclases.
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");
}
}
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:
pág. 63
this.weightPounds = weightPounds;
}
public int getPassengersCount() {
return this.passengersCount;
}
public int getMaxWeightPounds() {
return this.weightPounds + this.passengersCount * 250;
}
}
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í:
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.
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);
}
}
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.
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:
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");
}
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:
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.
Del mismo modo, la TruckImpl clase puede ser una clase privada anidada de la clase
FactoryImpl:
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:
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.
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:
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:
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.
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.
Cómo hacerlo...
Como ejemplo, cambiaremos la interfaz Truck. La interfaz Car se puede modificar de
manera similar:
pág. 73
return (int) Math.round(0.454 * getPayloadPounds());
}
}
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.
4. Tenga en cuenta que el nuevo método funciona incluso sin cambiar la clase que lo
implementó.
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:
Cómo funciona...
Aquellos que prefieren el sistema métrico de unidades ahora pueden aprovechar el
nuevo método:
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.
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.
Cómo hacerlo...
1. Agregue la implementación del método getWeightKg(int pounds):
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:
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:
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.
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:
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:
pág. 79
}
}
Cómo hacerlo...
1. Cree un objeto Optional utilizando cualquiera de los métodos que se han
demostrado, de la siguiente manera:
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:
pág. 80
Optional<String> congrats1 = Optional.empty();
System.out.println(prize1.equals(congrats1));//prints: true
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.
pág. 81
Ahora, el método processIfPresent() se ve mucho más simple:
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:
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()));
}
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:
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().
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.
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:
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
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
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
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
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:
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:
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
pág. 89
//prints: true
obj = "";
System.out.println(obj == null); //prints: false
System.out.println(Objects.isNull(obj));
//prints: false
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
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:
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):
En resumen, si desea comparar dos objetos a y b, por los valores de sus campos,
entonces:
pág. 91
Programacion Modular
En este capítulo, cubriremos las siguientes recetas:
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.
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:
El JSR enumera las ventajas que resultan de abordar los problemas anteriores:
En este capítulo, veremos algunas recetas importantes que lo ayudarán a comenzar con
la programación modular.
pág. 93
de paquete, mostrar las dependencias a nivel de clase y filtrar las dependencias, entre
otras opciones.
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 .
Los siguientes pasos lo ayudarán a configurar los requisitos previos para esta receta:
# 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.
# 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
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
# 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
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.
# 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
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.
# 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:
# On Linux
jdeps -jdkinternals -cp classes/:lib/*
classes/com/packt/Sample.class
# On Windows
jdeps -jdkinternals -cp "classes/;lib/*"
classes/com/packt/Sample.class
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 .
pág. 97
6. Veamos si hay dependencias en un nombre de paquete dado:
La salida es la siguiente:
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:
# 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
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:
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:
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.
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):
pág. 102
.limit(count).sum();
}
Podemos ver en las API anteriores que se repiten las siguientes operaciones:
Según nuestra observación, podemos refactorizar las API anteriores y extraer estas
operaciones en un método, de la siguiente manera:
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.
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:
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;
}
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:
pág. 104
• Suma de N primos
• Suma de N pares
• Suma de N probabilidades
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.
module calculator{
requires math.util;
}
pág. 106
¡Felicidades! Con esto, tenemos una aplicación modular simple en funcionamiento.
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
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í:
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.
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
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:
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.
jar -d --file=mlib/[email protected]
[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]
[email protected]
requires mandated java.base
exports com.packt.math
• 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
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:
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.
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:
Ahora, hemos compilado con éxito nuestro código no modular, que dependía del JAR
modular. Ejecutemos el código compilado:
• compile-calculator.bat
• run.bat
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:
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:
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:
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):
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!
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:
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
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;
}
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:
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.
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.
module math.util{
exports com.packt.math;
}
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:
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
$ 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.
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;
}
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.
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.
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:
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:
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.
$ ls mlib
banking.util.jar jackson.annotations.jar jackson.core.jar
jackson.databind.jar math.util.jar
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.
#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
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
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:
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;
}
#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
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.
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:
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.
module book.service{
exports com.packt.model;
exports com.packt.service;
exports com.packt.spi;
uses com.packt.spi.BookServiceProvider;
}
pág. 131
System.out.println("Mongodb Deleting ... " + id);
}
@Override
public BookService getBookService(){
return new MongoDbBookService();
}
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.
14. Vamos a compilar y ejecutar nuestro módulo principal usando los siguientes
comandos:
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.
module sqldb.book.service{
requires book.service;
provides com.packt.spi.BookServiceProvider
with com.packt.sqldb.SqlDbBookServiceProvider;
}
@Override
public BookService getBookService(){
return new SqlDbBookService();
}
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
Cómo hacerlo...
1. Vamos a compilar los módulos:
$ javac -d mods --module-path mlib --module-source-path
src $ (buscar src - nombre * .java)
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
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));
}
}
clave1, valor1
clave2, valor3
clave3, valor3
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
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)
$ "%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 -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
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 ).
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:
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;
}
}
pág. 140
5. Vamos a correr mr.jaren Java 9:
java -jar mr.jar
[element1, element2, element3]
Using factory methods
[element2, element3, element1]
#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:
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:
Prepararse
Tenemos los siguientes módulos en nuestro ejemplo:
pág. 142
• sqldb.book.service: Este es el módulo que proporciona otra implementación
a la interfaz del proveedor de servicios
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
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
module book.service{
exports com.packt.model;
exports com.packt.service;
exports com.packt.spi;
uses com.packt.spi.BookServiceProvider;
}
pág. 144
public String title;
public String author;
}
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:
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.
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 :
<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>
<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>
<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>
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;
}
}
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
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;
}
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);
}
}
Y luego ejecute el código anterior con el siguiente comando, ejecutado desde la misma
ubicación:
pág. 149
Nota: Puede utilizar los scripts run.bato run.sh para compilar y ejecutar el
código.
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 .
Automatic-Module-Name: com.packt.banking
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.
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;
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;
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:
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:
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.
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
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.
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.
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.
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
@FunctionalInterface
public interface Car extends Vehicle {
int getPassengersCount();
}
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:
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:
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:
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
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:
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:
Cómo funciona...
Podemos componer todo el método usando solo las funciones:
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.
Y los consumidores serán casi idénticos, excepto por el prefijo diferente antes de
imprimir el resultado:
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);
}
testSourceAndCondition ( 10 , 20 ) ;
testSourceAndCondition ( 1 , 20 ) ;
testSourceAndCondition ( 10 , 200 ) ;
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>
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 :
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:
La función resultante acepta un valor del T tipo y produce un valor del V tipo.
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.
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()
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().
@FunctionalInterface
public interface SpeedModel {
double getSpeedMph(double timeSec, int weightPounds, int horsePower);
}
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:
//@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:
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) ;
}
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);
};
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.
@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.
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;
i -> i ++;
(a, b) -> a + b;
(a, b) -> a == b;
(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);
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.
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;
double v = 10d;
v = 30d; //Causes compiler error
Function<Integer, Double> multiplyBy10 = i -> i * v;
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.
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);
}
}
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.
Consumer<String> consumer =
s -> System.out.println("The " + s + " is consumed.");
consumer.accept("Hello!"); //prints: The Hello! is consumed.
pág. 173
String res = "Success";
//Do something and return result—Success or Error.
return res;
};
System.out.println(supplier.get()); //prints: Success
Los ejemplos de interfaces funcionales especializadas que hemos presentado son los siguientes:
Los ejemplos de interfaces funcionales especializadas que hemos presentado son los
siguientes:
pág. 174
}
} ;
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:
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
}
}
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.
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:
res = process.apply(i)
if(condition.test(res)){
success.accept(res);
} else {
failure.accept(res);
}
}
void someOtherMethod() {
calculate(source(), process(),
condition(), success(), failure());
}
@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:
Pero uno no debe llevarlo demasiado lejos, ya que dicha alineación puede disminuir la
legibilidad del código.
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 .
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.
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()
class Food{
public static String getFavorite(){ return "Donut!"; }
public static String getFavorite(int num){
return num > 1 ? String.valueOf(num) + " donuts!" : "Donut!";
}
}
pág. 180
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.
Pero cuando se usa la referencia del método, cambia exactamente a la misma forma
que en el ejemplo anterior:
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();
}
}
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:
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:
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:
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"));
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:
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
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 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:
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>:
Una vez que hacemos esto, podemos expresar el constructor anterior usando la
referencia del método:
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());
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);}
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 "" ; }
}
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:
También hay algunos métodos útiles para trabajar con matrices y listas:
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); }
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:
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í:
El número 17 es una hora del día (5 pm) y Main1035 es una identificación de semáforo.
List<TrafficUnit> trafficUnits =
FactoryTrafficModel.generateTraffic(20, Month.APRIL,
DayOfWeek.FRIDAY, 17, "USA", "Denver", "Main103S");
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.
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:
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í:
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);
}
}
}
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):
pág. 193
double speed = vehicle.getSpeedMph(timeSec);
speed = (double) Math.round(speed * tu.getTraction());
printResult(tu, timeSec, speed);
}
}
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:
pág. 194
La implementación del método en la clase cambiaría de la siguiente
manera: speedAfterStart()TrafficImpl
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á:
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):
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():
Para hacerlo más flexible, podemos agregar otro parámetro a nuestra API:
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:
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:
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.
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);
La forma más corta de crear la colección List es comenzando con una matriz:
El resultado es el siguiente:
Aquí hay una ilustración de los resultados de los últimos dos ejemplos:
pág. 200
Y así es como solíamos crear Mapantes de Java 9:
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 ".
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:
Map.ofEntries(
entry(1, "This "),
entry(2, "is "),
entry(3, "built "),
entry(4, "by "),
entry(5, "Map.ofEntries() ")
).entrySet().forEach(System.out::print);
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 ", "not ", "created ", null);
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()
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{}
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.
list.forEach(i -> {
int j = list.get(2);
list.set(2, j + 1);
});
System.out.println();
list.forEach(System.out::print); //prints: 12545
Prepararse
Hay muchas formas de crear una secuencia:
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
• forEach()
• findFirst()
• reduce()
• collect()
• Otros métodos de la interfaz Stream que no regresan 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 :
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.
pág. 207
)
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.
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.
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.
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();
}
"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 ".
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:
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.
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.
pág. 210
15. Escribe el código que usa el método Files.find():
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.
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í.
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:
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:
Stream.concat(Stream.of(4,5,6), Stream.of(1,2,3))
.forEach(System.out::print);
El resultado es el siguiente:
El resultado es el siguiente:
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).
//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".
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("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():
pág. 218
})
.orElse(10);
System.out.println("Stream.of(1,2,3).reduce(acc): " + sum);
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);
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:
R collect(Collector<T,A,R> collector)
R collect(Supplier<R> supplier, BiConsumer<R,T> accumulator,
BiConsumer<R,R> combiner)
pág. 220
Analicemos algunos ejemplos del uso de la clase Collectors . Primero, crearemos
una pequeña clase de demostración llamada Thing:
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 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.
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.
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 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);
"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".
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:
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
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
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);
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)
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:
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.
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
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
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
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.
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:
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:
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)
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; }
}
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.
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:
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>
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:
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}]
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}]
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}]
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}]
pág. 234
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
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:
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()
Estas operaciones también se pueden usar para crear un objeto Map, y en esta receta,
vamos a demostrar cómo hacerlo.
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.
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:
O, para evitar datos redundantes en el resultado Map, podemos usar el campo de edad
como el Mapvalor:
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...:
pág. 240
);
System.out.println(map); //prints: {John=30, 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:
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:
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}
La solución anterior funciona bien siempre que no se encuentre una clave duplicada,
como en el siguiente caso:
pág. 242
valueMapper, mergeFunction));
System.out.println(map);
//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.
pág. 243
.collect(Collectors.toUnmodifiableMap(Person::getName,
Person::getAge));
System.out.println(map); //prints: {John=30, Jill=20}
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}
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
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
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 :
pág. 247
downstream para convertir los valores del mapa intermedio en los valores del
mapa resultante del tipo proporcionado por el proveedor mapFactory .
• 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 + "}";
}
}
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 + "}";
}
}
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:
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.
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 + ")";
}
}
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() .
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
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):
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)
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:
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()).
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:
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:
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:
pág. 256
Se puede lograr el mismo resultado utilizando colectores creados por los métodos
groupingBy() :
Prepararse
En el capitulo anterior, Capítulo 4 , Funcionando , al crear una API compatible con
lambda, terminamos con el siguiente método de API:
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.
Cómo hacerlo...
Recordemos cómo un usuario llamó a la API compatible con lambda:
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.
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>getTrafficUnitStream(int trafficUnitsNumber){
return FactoryTraffic.getTrafficUnitStream(trafficUnitsNumber,
Month.APRIL, DayOfWeek.FRIDAY, 17, "USA",
"Denver", "Main103S");
}
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));
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; }
}
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()));
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;
}
}
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()));
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:
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]);
}
pág. 262
}
return new CountByLane(1, lanesNumber);
}
Hay más...
La tubería permite agregar fácilmente otro filtro, o cualquier otra operación para el
caso:
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()));
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.
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:
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.
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 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.
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.
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.
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.
Los pasos 5 a 7 se pueden repetir varias veces con la misma conexión de 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.
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.
Cómo funciona...
Aquí está el fragmento de código que crea una conexión con la base de datos local de
PostgreSQL:
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.
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;
}
}
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;
}
}
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;
}
}
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:
pág. 271
Tal construcción se puede usar con cualquier objeto que
implemente java.lang.AutoCloseable la interfaz java.io.Closeable .
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),
....
);
Cómo funciona...
Aquí hay un ejemplo de un comando que crea la tabla traffic_unit en
PostgreSQL:
pág. 272
);
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:
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.
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:
pág. 275
busque su convención de nomenclatura y la siga, para que su nomenclatura se alinee
con los nombres creados automáticamente.
Prepararse
Ya hemos visto ejemplos de declaraciones SQL que crean (rellenan) datos en la base
de datos:
También hemos visto ejemplos de instancias en las que se deben agregar varios
registros de tabla:
SELECT column_name,column_name
FROM table_name WHERE some_column=some_value;
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 ).
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:
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:
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.
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.
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:
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:
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:
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>
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:
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);
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.
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");
Tenga en cuenta que hemos utilizado el prefijo dataSource para las propiedades
que no tienen setters dedicados en la clase HikariConfig .
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
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);
}
-Dhikaricp.configurationFile=src/main/resources/database.properties
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 :
pág. 285
Hay más...
Puede leer más sobre las características de HikariCP en GitHub
( https://github.com/brettwooldridge/HikariCP ).
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 (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:
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.
Cómo hacerlo...
pág. 288
Primero, agreguemos una salida al método traverseRS():
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:
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:
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:
pág. 291
Ahora, creemos una tabla test con solo una columna nameusando la consola de la
base de datos:
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:
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).
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).
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.
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:
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:
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:
pág. 296
También podemos intentar recuperar un objeto a ResultSet través del getter:
Sin conocer tales detalles, descubrir una forma de manejar los LOB requeriría mucho
tiempo y causaría mucha frustración.
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");
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 ):
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:
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:
Solo después de esto puede ejecutar los delete from lobs where id =
100 comandos de forma segura .
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.
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(); }
while (rs.next()){
Blob blob = rs.getBlob(1);
byte[] bytes = blob.getBytes(1, (int)blob.length());
System.out.println("bytes = " + bytes);
}
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):
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);
}
}
}
}
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.
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.
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.
pág. 305
El resultado es el siguiente:
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:
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:
Ahora, juntemos todo esto en código Java, creemos una función e invoquémosla en
tres estilos diferentes:
pág. 307
El resultado es el siguiente:
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:
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:
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.
"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:
pág. 310
}
}
traverseRS("select selectText(2)");
traverseRS("select * from selectText(2)");
execute("drop function if exists selectText(int)");
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:
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.
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:
pág. 313
try(Connection conn = getConnection();
Statement st = conn.createStatement()){
st.execute(sb.toString());
} catch (SQLException ex){
ex.printStackTrace();
}
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.
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();
}
pág. 315
Pero esta implementación también se puede mejorar.
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;
}
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.
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:
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 + ")");
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 ).
pág. 318
aplicación necesita con respecto a la base de datos y no genere demasiados gastos
generales.
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:
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:
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.
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
Cómo hacerlo...
Comenzaremos con las operaciones CRUD utilizando la base de datos PostgreSQL y la
clase Person1:
pág. 320
usaremos para demostrar cómo implementar la relación entre las clases y las tablas
correspondientes.
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.
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:
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).
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:
3. Escriba código que inserte un registro en la tabla person1y luego lea este registro
de la siguiente manera id:
pág. 324
ex.printStackTrace();
}
pág. 325
list =
session.selectList(mapperNamespace + ".selectPersons");
for(Person1 p1: list) {
System.out.println("All: " + p1);
}
Updated 1 records
All: Person1{id=1, age=10, name='Bill'}
Deleted 0 persons
Total records: 0
7. Para demostrar cómo MyBatis apoya las relaciones, cree la tabla family y la
tabla person2:
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.
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.
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())+ "'}";
}
}
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 .
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>
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:
session.commit();
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (Exception ex){
ex.printStackTrace();
}
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);
}
}
pág. 330
Ahora, podemos eliminar todos los familyregistros y verificar si alguna de las
tablas contiene family y person2 contiene registros posteriores:
c = session.selectOne(familyMapperNamespace + ".selectFamiliesCount");
System.out.println("Total family records: " + c);
c = session.selectOne(personMapperNamespace + ".selectPersonsCount");
System.out.println("Total person records: " + c);
Deleted 1 families
Total family records: 0
Total person records: 0
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 .
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:
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.
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>
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;
}
}
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:
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();
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.
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());
pág. 335
</persistence-unit>
</persistence>
<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>
@Override
public String toString () {
return "Person1 {id =" + id + ", age =" + age +
", name = '" + name + "'}" ;
}
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:
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<>();
@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:
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);
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();
}
}
Cómo funciona...
Si ejecutamos el código anterior, el resultado será el siguiente:
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:
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.
pág. 340
"un marco interoperable de publicación-suscripción, mejoras en la API
CompletableFuture y varias otras mejoras"
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() :
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:
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.
IntStream.range(21, 24)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);
IntStream.range(21, 24)
.peek(Chapter07Concurrency::doSomething)
.forEach(System.out::println);
pág. 343
existente. Incluso puede invocar el método run() directamente, sin pasar el objeto
al constructor Thread.
runImpl(21, 24);
}
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());
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:
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.
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.
231.69407148192175
237.44481627598856
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.
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);
}
}
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);
}
}
}
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:
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.
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 .
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.
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");
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.
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;
}
}
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.
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:
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.
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()
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:
pág. 355
}
} catch (Exception ex) {
System.out.println(ex.getClass().getName());
}
System.out.println("list: " + list);
}
System.out.println();
System.out.println("***** CopyOnWriteArrayList add():");
demoListAdd(new CopyOnWriteArrayList<>(Arrays.asList("One",
"Two", "Three")));
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);
}
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:
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);
}
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:
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.
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
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
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);
}
System.out.println();
System.out.println("*****"
+ " ConcurrentSkipListSet iter.remove():");
demoNavigableSetIterRemove(new ConcurrentSkipListSet<>
(Arrays.asList(0, 1, 2, 3)));
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:
System.out.println();
System.out.println("*****"
+ " ConcurrentSkipListSet set.add():");
demoNavigableSetAdd(new ConcurrentSkipListSet<>(Arrays
.asList(0,1,2,3)));
pág. 363
Como antes, observamos que la Setversión concurrente maneja mejor la
concurrencia.
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 ),
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)
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
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:
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()));
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:
Esto nos recuerda que no descartemos los cambios propagados a través de los objetos
a través de sus referencias.
pág. 367
String result = map.remove(2);
boolean success = map.remove(2, "Two");
iter.remove();
boolean result = map.keySet().remove(2);
boolean result = map.keySet().removeIf(e -> e == 2);
class QueueElement {
private String value;
public QueueElement(String value){
this.value = value;
}
public String getValue() {
return value;
}
}
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();
}
}
}
}
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();
}
}
}
BlockingQueue<QueueElement> queue =
new ArrayBlockingQueue<>(5);
QueueProducer producer = new QueueProducer(queue);
QueueConsumer consumer = new QueueConsumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
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.
"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".
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.
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.
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.");
}
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.
"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).
3. Generalice el código anterior y cree un método que cierre un servicio y la tarea que
ha devuelto Future:
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.");
}
}
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):
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.
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.
pág. 376
Ahora vemos que el trabajador Onepudo completar su tarea, mientras que el
trabajador Twofue interrumpido.
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:
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());
}
}
}
}
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; }
}
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);
}
}
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.
Ahora podemos usar todas estas nuevas clases y métodos para demostrar el método
invokeAll():
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);
}
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:
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.
Como puede ver, todos los hilos pudieron completarse incluso con tres segundos de
tiempo de espera.
pág. 381
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
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"
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:
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.
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.
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 .
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; }
class DateLocation {
private int hour;
private Month month;
private DayOfWeek dayOfWeek;
private String country, city, trafficLight;
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:
"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.
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.
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);
}
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);
}
void demo3_ForkJoin_invoke() {
AverageSpeed averageSpeed = createTask();
ForkJoinPool commonPool = ForkJoinPool.commonPool();
double result = commonPool.invoke(averageSpeed);
System.out.println("result = " + result);
}
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);
Ahora, revisemos las opciones de implementación else del bloque. Las primeras
líneas siempre serán las mismas:
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:
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.
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() :
Sin embargo, hay una forma aún mejor de implementar un bloque else del método
compute():
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():
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.
Prepararse
Entre muchas otras características, Java 9 introdujo estas cuatro interfaces en la clase
java.util.concurrent.Flow :
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
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> :
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);
}
}
}
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().
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.");
}
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).
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
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:
Cómo hacerlo...
Sigue estos pasos:
pBuilder.command("free", "-m");
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();
}
Cómo funciona...
Hay dos formas de informar ProcessBuilderqué comando ejecutar:
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.
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.
Cómo hacerlo...
Sigue estos pasos:
pb.redirectError(new File("error"))
.redirectOutput(new File("output"));
Process p = pb.start();
int exitValue = p.waitFor();
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.
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:
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:
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:
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:
2. Establezca el comando treey la salida y el error al mismo que el del proceso Java
actual:
pb.command("tree").inheritIO();
pb.directory(new File("/root"));
Process p = pb.start();
int exitValue = p.waitFor();
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.
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.
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.
Cómo hacerlo...
Sigue estos pasos:
pb.command("printenv").inheritIO();
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.
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.
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.
Prepararse
Primero, escribamos nuestro script de shell. Este script hace lo siguiente:
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;
Cómo hacerlo...
Sigue estos pasos:
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:
pb.command("/bin/bash", "script.sh").inheritIO();
Process p = pb.start();
int exitValue = p.waitFor();
pág. 411
Cómo funciona...
Debe anotar dos cosas en esta receta:
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.
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:
pág. 413
System.out.println("Total CPU Duration: " +
info.totalCpuDuration().get().toMillis() +"ms");
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:
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:
pág. 416
Puede obtener el código completo de Chapter08/7_spawned_process_info.
• 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:
p.destroy();
isAlive = p.isAlive();
System.out.println("Process alive? " + isAlive);
Hemos proporcionado un script de utilidad llamado run.sh, que puede usar para
compilar y ejecutar el código— sh run.sh.
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.
Cómo hacerlo...
Sigue estos pasos:
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(""));
});
Hemos proporcionado un script de utilidad llamado run.sh, que puede usar para
compilar y ejecutar el código— sh run.sh.
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.
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:
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):
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:
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:
Hemos proporcionado un script de utilidad llamado run.sh, que puede usar para
compilar y ejecutar el código— sh run.sh.
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:
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 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:
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.
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:
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());
});
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.
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:
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.
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:
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.
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";
}
}
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.
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.
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";
}
}
@Controller
@RequestMapping("/message")
public class SimpleViewController{
@GetMapping
public String message(){
return "message";
}
}
pág. 429
boot/docs/current/reference/html/common-application-
properties.html ) que podemos usar para personalizar y conectar con diferentes
componentes.
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.
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:
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.
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)
);
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:
4. Otro método para obtener los detalles de una sola persona identificada por ID es el
siguiente:
6. El método para actualizar una fila existente en la tabla, identificada por la ID, es la
siguiente:
7. El método para eliminar una fila de la tabla, identificado por la ID, es el siguiente:
@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/";
}
@GetMapping("/{id}/delete")
public String deletePerson(@PathVariable Integer id){
personMapper.delete(id);
return "redirect:/persons";
}
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
pág. 435
El código completo de esta receta se puede encontrar
en Chapter09/2_boot_db_demo.
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ó.
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!
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:
@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:
<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>
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;
}
@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);
}
@PostMapping
public ResponseEntity<Person> newPerson
(@RequestBody Person person){
personMapper.insert(person);
return new ResponseEntity<>(person, HttpStatus.OK);
}
@PostMapping("/{id}")
public ResponseEntity<Person> updatePerson
(@RequestBody Person person,
@PathVariable Integer id){
person.setId(id);
personMapper.save(person);
return new ResponseEntity<>(person, HttpStatus.OK);
}
@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:
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.
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:
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:
$ mysql -uroot -p
Enter password:
mysql> create user 'springboot'@'%' identified by 'springboot';
pág. 444
ubicación de un archivo de configuración específico para su sistema operativo es hacer
lo siguiente:
mysql -uroot
Enter password:
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.
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
spring.datasource.url=
jdbc:mysql://<digital_ocean_ip>/sample?useSSL=false
spring.datasource.username=springboot
spring.datasource.password=springboot
[
{
"id": 1,
"firstName": "David ",
"lastName": "John",
"place": "Delhi"
pág. 446
},
{
"id": 2,
"firstName": "Raj",
"lastName": "Singh",
"place": "Bangalore"
}
]
[
{
"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):
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
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 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 .
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:
pág. 452
Haga clic en nuevo | Cree una nueva aplicación , complete los detalles y haga clic
en Crear aplicación :
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:
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:
$ git init
$ git add .
$ git commit -m "deploying to heroku"
$ git push heroku master
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.
[
{
"id":1,
"firstName":"Mohamed",
"lastName":"Sanaulla",
"place":"Bangalore"
},
{
"id":2,
"firstName":"Nick",
"lastName":"Samoylov",
"place":"USA"
}
]
Hay más...
1. Agregue un nuevo complemento de base de datos a la aplicación:
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 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)
);
spring.datasource.url=jdbc:mysql://
<URL DB>:3306/x81mhi5jwesjewjg?useSSL=false
spring.datasource.username=zzu08pc38j33h89q
spring.datasource.password=<DB password>
pág. 457
10. Con esto, nos hemos integrado con una base de datos que creamos en Heroku.
pág. 458
aplicación misma. La siguiente imagen, tomada de https://docs.docker.com/get-
started/#container-diagram , representa mejor esto:
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.
Prepararse
pág. 460
Para esta receta, utilizaremos un servidor Linux con Ubuntu 16.04.2 x64:
$ wget https://download.docker.com/linux/ubuntu/dists/xenial
/pool/stable/amd64/docker-ce_17.03.2~ce-0~ubuntu-xenial_amd64.deb
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" ]
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
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.
docker ps
pág. 462
7. Puede administrar el contenedor con el siguiente comando:
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.
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.
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.
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):
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:
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.
pág. 467
5. Abra http://localhost:8080/actuator/prometheus para ver las
métricas en un formato entendido por Prometheus:
- 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.
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:
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.
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>
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:
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
management.endpoints.web.exposure.include=prometheus,health,info,metrics
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.0.6</version>
</dependency>
# 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"
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']
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:
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 ).
Cómo hacerlo...
1. Cree una instancia java.net.http.HttpClient de uso de su
generador java.net.http.HttpClient.Builder:
pág. 474
Cómo funciona...
Hay dos pasos principales para hacer una llamada HTTP a una URL:
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:
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:
pág. 477
5. Luego imprimimos el código de estado de respuesta y el cuerpo de respuesta
enviado por el servidor:
• jackson.databind.jar
• jackson.core.jar
• jackson.annotations.jar
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 .
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.
{
"authenticated": true,
"user": "user"
}
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 UsernamePasswordAuthenticator(){}
public UsernamePasswordAuthenticator ( String username,
String password){
this.username = username;
this.password = password;
}
@Override
protected PasswordAuthentication getPasswordAuthentication(){
return new PasswordAuthentication(username,
password.toCharArray());
}
pág. 480
.GET()
.version(HttpClient.Version.HTTP_1_1)
.build();
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.
public UsernamePasswordAuthenticator(){}
pág. 481
this.password = password;
}
@Override
protected PasswordAuthentication getPasswordAuthentication(){
return new PasswordAuthentication(username,
password.toCharArray());
}
}
Cómo hacerlo...
1. Cree una instancia de HttpClientuso de su constructor HttpClient.Builder:
pág. 482
CompletableFuture<HttpResponse<String>> que obtuvimos. Usaremos esto
para procesar la respuesta:
CompletableFuture<HttpResponse<String>> responseFuture =
client.sendAsync(request,
HttpResponse.BodyHandlers.ofString());
CompletableFuture<Void> processedFuture =
responseFuture.thenAccept(response -> {
System.out.println("Status code: " + response.statusCode());
System.out.println("Response Body: " + response.body());
});
CompletableFuture.allOf(processedFuture).join();
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 :
pág. 484
3. Ejecute la solicitud HTTP utilizando la HttpClient instancia para obtener una
instancia de CloseableHttpResponse:
Hay más...
Podemos proporcionar un controlador de respuesta personalizado al invocar el método
HttpClient.execute, de la siguiente manera:
pág. 485
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity) : null;
});
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.
<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 .
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();
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();
jsonResponse = Unirest.get("http://httpbin.org/basic-auth/user/passwd")
.basicAuth("user", "passwd")
.asJson();
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:
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.
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.
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.
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.
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 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).
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)
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:
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:
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.
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.
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
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.
pág. 495
Por lo tanto, depende de usted elegir cuánta información necesita para el análisis.
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):
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
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:
• 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
Las características principales del nuevo sistema de registro son las siguientes:
Prepararse
Para ver todas las posibilidades de registro de un vistazo, puede ejecutar el siguiente
comando:
java -Xlog:help
pág. 499
-Xlog[:[what][:[output][:[decorators][:output-options]]]]
-Xlog:all=info:stdout:uptime,level,tags
Cómo hacerlo...
Ejecutemos algunas de las configuraciones de registro:
No hay mensajes porque la JVM no registra mensajes solo con la etiqueta cpu. La
etiqueta se usa en combinación con otras etiquetas.
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.
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.
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:
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:
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).
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:
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:
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:
3. El JDK 9 introdujo los siguientes comandos jcmd (JDK 18.3 y JDK 18.9 no
agregaron nuevos comandos):
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
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.
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:
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:
pág. 507
Luego vienen los detalles del montón (esto es solo un pequeño fragmento de él):
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.
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;
}
}
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:
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.
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.
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:
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.
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:
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.
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();
new Throwable().printStackTrace();
Arrays.stream(Thread.currentThread().getStackTrace())
.forEach(System.out::println);
Arrays.stream(new Throwable().getStackTrace())
.forEach(System.out::println);
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());
});
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.
Prepararse
La StackWalker clase tiene cuatro métodos de fábrica
estáticos sobrecargados getInstance():
• 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
• 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:
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();
}
}
Cómo funciona...
Para una comprensión más profunda de los conceptos aquí, modifiquemos Clazz03:
pág. 516
return;
}
System.out.println("Print the stack trace:");
Thread.currentThread().dumpStack();
}
}
new Throwable().printStackTrace();
Arrays.stream(Thread.currentThread().getStackTrace())
.forEach(System.out::println);
Arrays.stream(new Throwable().getStackTrace())
.forEach(System.out::println);
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:
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:
Cómo hacerlo...
1. Presta atención al objeto creado dentro del bucle.
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
}
}
}
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.
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
}
}
}
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.
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
}
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.
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;
}
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;
}
}
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:
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.
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:
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.
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
);
}
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
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:
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.
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 .
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) :
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.
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.
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];
}
}
}
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)
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)
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.
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:
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
El bucle de lectura-evaluación-
impresión (REPL) usando JShell
pág. 533
En este capítulo, cubriremos las siguientes recetas:
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 .
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:
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 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.
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 :
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:
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;
}
}
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();
}
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:
Sin embargo, el método no puede invocarse antes de que se haya definido el método
utilizado:
pág. 542
Podemos invocar newMethod() solo después de haberlo
definido undefinedMethod().
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:
pág. 543
7. Cargue la definición Car de clase y cree un objeto Car:
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 );
}
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.
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.
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;
}
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("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()));
myShell.close();
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.
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.
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.
En este capítulo, veremos las diferentes formas en que podemos aprovechar la nueva
API de fecha / hora.
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 :
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");
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):
• 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.
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.
La fecha de finalización del rango será la cantidad de días del mes, como se muestra en
el siguiente fragmento:
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:
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.
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 :
4. Hacemos uso del método of() de fábrica para construir una instancia
de java.time.ZonedDateTime:
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.
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
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.
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
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.
jshell> ZoneOffset.ofHoursMinutes(5,30)
$27 ==> +05:30
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.
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]
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]
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
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().
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
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
jshell> Period.parse("P2Y4M23D").getDays()
$8 ==> 23
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 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> 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:
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
jshell> period1.plusDays(4)
$34 ==> P3Y15M33D
jshell> period1.plusMonths(3)
$35 ==> P3Y18M29D
jshell> period1.plusYears(1)
$36 ==> P4Y15M29D
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.
• 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.
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.parse("P12D")
$70 ==> PT288H
jshell> Duration.parse("P12DT7H5M8.009S")
$71 ==> PT295H5M8.009S
jshell> Duration.parse("PT7H5M8.009S")
$72 ==> PT7H5M8.009S
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.
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> 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> 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> 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)
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
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
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
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> d.plusDays(3)
$5 ==> 2018-07-30
jshell> d.minusYears(4)
$6 ==> 2014-07-27
jshell> dt.plusMinutes(45)
$8 ==> 2018-07-27T16:12:40.733389700
jshell> dt.minusHours(4)
$9 ==> 2018-07-27T11:27:40.733389700
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.
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> d.isBefore(d2)
$4 ==> false
jshell> d.isAfter(d2)
$5 ==> true
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> 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.
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:
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
jshell> JapaneseEra.values()
$42 ==> JapaneseEra[5] { Meiji, Taisho, Showa, Heisei, NewEra }
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> 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
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> 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> ldt.format(dtf)
$56 ==> "01 Aug 2018 05:36:22 PM"
Cómo funciona…
Comprendamos las letras de formato más utilizadas:
M: 1,2,3
M` MMM`MMMM mes del año MMM: Junio, julio, agosto
MMMM: Julio Agosto
m minutos 1, 2, 3
s segundos 1, 2, 3
pág. 576
a AM / PM del día AM PM
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:
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.
• Falta de requisitos
• Ambigüedad de requisitos
• Los requisitos cambian todo el tiempo
pág. 578
los requisitos en un lenguaje formal llamado Gherkin, pero sin la sobrecarga de
mantener una extensa documentación.
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>
<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>
<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.
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 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:
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
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.
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
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.
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;
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();
}
Pending scenarios:
com/packt/cookbook/ch16_testing/CalculateSpeed.feature:6
# Calculate speed
1 Scenarios (1 pending)
3 Steps (2 skipped, 1 pending)
0m0.055s
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);
}
}
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;
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);
}
}
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:
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.
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 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());
}
}
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:
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:
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:
@Test
public void testGetSpeedMph(){
double timeSec = 10.0;
int engineHorsePower = 246;
int vehicleWeightPounds = 4000;
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:
pág. 592
Cambiamos el valor esperado a 117 y continuamos escribiendo otros casos de prueba
que discutimos mientras analizamos el código.
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:
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");
}
}
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.
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.
Cómo hacerlo...
La simulación de FactoryTraffic puede ser similar a la siguiente:
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;
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:
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):
pág. 597
}
public int getLane() { return lane; }
public int getCount() { return count; }
}
@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);
}
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);
}
}
@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);
}
@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:
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 :
pág. 601
@Test
public void testCalcSpeed(){
double timeSec = 10.0;
TrafficDensity2 trafficDensity = new TrafficDensity2();
@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;
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):
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;
}
@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
}
}
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:
pág. 604
}
@Test
public void testMethodTwo(){
System.out.println("testMethodTwo() is called");
}
}
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 ).
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
}
}
@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.
@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:
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 .
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.
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.
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).
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.
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í:
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):
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:
pág. 612
}
}
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.
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.
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:
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:
pág. 614
Como habrás notado, también hemos agregado el método isEnoughData() que
verifica si hay suficientes datos grabados:
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:
pág. 615
private Vehicle.VehicleType vehicleType;
private double speedLimitMph, traction, speed;
private RoadCondition roadCondition;
private TireCondition tireCondition;
...
public double getSpeed() { return speed; }
}
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.
pág. 616
También se puede escribir un código similar para las pruebas de nivel
de clase TrafficDensity:
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.
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:
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.
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:
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:
Esa era la única forma de implementar tal bucle. Pero desde Java 10, se puede escribir
de la siguiente manera:
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:
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();
}
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:
Para limitar la referencia a los métodos de la interfaz solamente, uno necesita agregar
la conversión de texto de la siguiente manera:
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:
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";
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 :
interface A {
void m();
}
Como identificador:
var var = 1;
package com.packt.cookbook.var;
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:
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:
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));
BiFunction<SomeReallyLongClassName,
AnotherReallyLongClassName, Double> f4 =
(@NotNull SomeReallyLongClassName x,
@NotNull AnotherReallyLongClassName y) -> x.doSomething(y);
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);
pág. 623
Programación de GUI usando
JavaFX
En este capítulo, cubriremos las siguientes recetas:
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:
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:
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.
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.
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 :
#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. 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
}
}
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:
6. Cree un botón, que será utilizado por el usuario para activar el cálculo de
edad, y agréguelo a gridPane:
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());
}
});
pág. 628
Scene scene = new Scene(gridPane, 300, 250);
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:
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 ):
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.
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.
• 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.
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));
ageCalculator.setOnAction((event) -> {
//event handling code here
});
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();
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í:
#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. 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:
pág. 635
<padding>
<Insets bottom="25.0" left="25.0" right="25.0" top="25.0" /
</padding>
pág. 636
</Text>
@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:
@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")
);
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 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:
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:
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:
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.
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
#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.
Cómo hacerlo...
1. Vamos a crear GridPane, que mantendrá los componentes en una cuadrícula de
filas y columnas:
pág. 642
gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setPadding(new Insets(25, 25, 25, 25));
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;
}
.btn-success {
-fx-text-fill: #fff;
-fx-background-color: #5cb85c;
-fx-border-color: #4cae4c;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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 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.
• Gráfico de barras
• Gráfico de linea
• Gráfico circular
• Gráfico de dispersión
• Gráfico de área
• Tabla de burbujas
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
#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>
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:
"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.
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 :
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:
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().
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;
}
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.
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:
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:
bc.getData().add(series);
Scene scene = new Scene(bc, 800, 600);
stage.setTitle("Bar Charts");
stage.setScene(scene);
stage.show();
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:
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.
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í:
#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.
Cómo hacerlo...
1. Primero creemos y configuremos GridPane para mantener nuestros gráficos
circulares:
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:
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);
pág. 659
public static void main(String[] args) {
Application.launch(args);
}
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):
pág. 660
)
);
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.
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.
En esta receta, crearemos un navegador web muy primitivo y simple que admita lo
siguiente:
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í:
#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:
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).
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());
}
}
}
);
pág. 663
Button goButton = new Button("Go");
goButton.setOnAction((event) -> {
String url = webAddress.getText();
if ( url != null && url.length() > 0){
webEngine.load(url);
}
});
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:
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:
}
);
13. Cree un nuevo objeto Scene con la VBoxinstancia creada en el paso anterior como
raíz:
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();
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;
}
• CANCELLED
• FAILED
• READY
• RUNNING
• SCHEDULED
• SUCCEEDED
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
}
}
);
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:
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>
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:
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.setFitWidth(350);
mediaView.setFitHeight(350);
10. Cree un nuevo gráfico de escena utilizando el VBoxobjeto como raíz y configúrelo
en el objeto de escenario:
stage.setWidth(400);
pág. 671
stage.setHeight(400);
stage.show();
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
}
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.
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
#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:
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));
gridPane.add(r1,1,1);
gridPane.add(r2,2,1);
gridPane.add(r3,3,1);
gridPane.add(c1,1,2);
gridPane.add(c2,2,2);
gridPane.add(c3,3,2);
gridPane.add(t, 1, 3, 3, 1);
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();
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:
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:
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.
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:
#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));
greeting.setOnAction((event) -> {
stage.setTitle("Age calculator");
stage.setScene(scene);
stage.setAlwaysOnTop(true);
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
Platform.runLater(appStartLatch::countDown));
stage.show();
appStage = stage;
}
}
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:
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:
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();
}
});
}
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();
}
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.
typeName();
clickButton();
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().
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());
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:
robot.mouseMove(textBoxBounds.getMinX(),
textBoxBounds.getMinY());
robot.mouseClick(MouseButton.PRIMARY);
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);
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