0% encontró este documento útil (0 votos)
583 vistas

Learning Java An Introduction To Real-World Programming With Java, 5th Edition TRADUCIDO

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

Learning Java An Introduction To Real-World Programming With Java, 5th Edition TRADUCIDO

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

Aprendiendo Java

por Marc Loy, Patrick Niemeyer y Daniel Leuck

Copyright © 2020 Marc Loy, Patrick Niemeyer, Daniel Leuck. Todos los derechos reservados.

Impreso en los Estados Unidos de América.

Publicado por O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.

Los libros de O’Reilly pueden ser adquiridos para uso educativo, empresarial o promocional de ventas.
También están disponibles ediciones en línea para la mayoría de los títulos (http://oreilly.com). Para
obtener más información, póngase en contacto con nuestro departamento de ventas
corporativas/institucionales: 800-998-9938 o [email protected].

Editor de adquisiciones: Suzanne McQuade

Editor de desarrollo: Amelia Blevins

Editor de producción: Beth Kelly

Corrector de estilo: Sonia Saruba

Corrector de pruebas: Christina Edwards

Indizador: Angela Howard

Diseñador interior: David Futato

Diseñador de portada: Karen Montgomery

Ilustrador: Rebecca Demarest

Mayo de 2000: Primera edición

Julio de 2002: Segunda edición

Mayo de 2005: Tercera edición

Junio de 2013: Cuarta edición

Marzo de 2020: Quinta edición

Historial de revisiones para la quinta edición

2020-03-27: Primera publicación

Vea http://oreilly.com/catalog/errata.csp?isbn=9781492056270 para conocer los detalles de la


publicación.

El logotipo de O’Reilly es una marca registrada de O’Reilly Media, Inc. Learning Java, la imagen de la
portada y la vestimenta comercial relacionada son marcas comerciales de O’Reilly Media, Inc.

Si bien el editor y los autores han hecho esfuerzos de buena fe para garantizar que la información y las
instrucciones contenidas en este trabajo sean precisas, el editor y los autores renuncian a toda
responsabilidad por errores u omisiones, incluida, entre otros, la responsabilidad por daños que resulten
del uso o la dependencia de este trabajo. El uso de la información e instrucciones contenidas en este
trabajo es bajo su propio riesgo. Si algún fragmento de código u otra tecnología que este trabajo
contiene o describe está sujeto a licencias de código abierto o a los derechos de propiedad intelectual
de terceros, es su responsabilidad garantizar que su uso cumpla con dichas licencias y/o derechos.
Tabla de Contenidos
Índice

Prefacio...............................................................................................................................................xi

1. Un lenguaje moderno.......................................................................................................................1

Introducción a Java 2

Orígenes de Java 2

Crecimiento 3

Una máquina virtual 4

Java comparado con otros lenguajes 7

Seguridad de diseño 10

Simplificar, simplificar, simplificar… 11

Seguridad de tipos y enlace de métodos 12

Desarrollo incremental 13

Gestión de memoria dinámica 13

Manejo de errores 14

Hilos 15

Escalabilidad 15

Seguridad de implementación 16

El Verificador 17

Cargadores de clase 19

Administradores de seguridad 19

Seguridad a nivel de aplicación y usuario 20

Un mapa de ruta de Java 21

El pasado: Java 1.0 - Java 11 21

El presente: Java 14 23

El futuro 25

Disponibilidad 25

2. Una primera aplicación...................................................................................................................27

Herramientas y entorno de Java 28

Instalación del JDK 28

Instalación de OpenJDK en Linux 29

Instalación de OpenJDK en macOS 30

Instalación de OpenJDK en Windows 31

Configuración de IntelliJ IDEA y creación de un proyecto 35


Ejecución del proyecto 39

Obtener los ejemplos de Learning Java 39

HelloJava 41

Clases 44

El método main() 44

Clases y objetos 46

Variables y tipos de clase 46

HelloComponent 47

Herencia 48

La clase JComponent 49

Relaciones y referencias 50

Paquetes e importaciones 51

El método paintComponent() 52

HelloJava2: La secuela 53

Variables de instancia 55

Constructores 56

Eventos 58

El método repaint() 60

Interfaces 60

Adiós y hola de nuevo 62

3. Herramientas del oficio...................................................................................................................63

Entorno JDK 63

La máquina virtual de Java 64

Ejecución de aplicaciones Java 64

Propiedades del sistema 66

La ruta de clases (classpath) 66

javap 68

Módulos 68

El compilador de Java 69

Probando Java 70

Archivos JAR 77

Compresión de archivos 77

La utilidad jar 77

La utilidad pack200 80

Construyendo 81
4. El lenguaje Java...............................................................................................................................83

Codificación de texto 84

Comentarios 86

Comentarios Javadoc 87

Variables y constantes 89

Tipos 90

Tipos primitivos 91

Tipos de referencia 95

Inferencia de tipos 97

Paso de referencias 97

Una palabra sobre cadenas de texto 98

Sentencias y expresiones 99

Sentencias 100

Expresiones 108

Arrays 114

Tipos de arrays 115

Creación e inicialización de arrays 115

Uso de arrays 117

Arrays anónimos 119

Arrays multidimensionales 119

Tipos, clases y arrays, ¡oh Dios mío! 121

5. Objetos en Java.............................................................................................................................123

Clases 124

Declaración e instanciación de clases 125

Acceso a campos y métodos 127

Miembros estáticos 131

Métodos 134

Variables locales 135

Sombreado 135

Métodos estáticos 137

Inicialización de variables locales 139

Paso de argumentos y referencias 140

Envoltorios para tipos primitivos 141

Sobrecarga de métodos 143

Creación de objetos 145


Constructores 145

Trabajando con constructores sobrecargados 146

Destrucción de objetos 148

Recolección de basura 148

Paquetes 149

Importación de clases 150

Paquetes personalizados 151

Visibilidad y acceso de miembros 153

Compilación con paquetes 155

Diseño avanzado de clases 155

Herencia y subclases 156

Interfaces 161

Clases internas 163

Clases internas anónimas 165

Organización de contenido y planificación para fallos 167

6. Manejo de errores y registro.........................................................................................................169

Excepciones 170

Excepciones y clases de error 170

Manejo de excepciones 172

Propagación ascendente 175

Traces de pila 176

Excepciones comprobadas y no comprobadas 177

Lanzamiento de excepciones 178

La cláusula finally 183

try con recursos 184

Problemas de rendimiento 185

Afirmaciones (assertions) 186

Activar y desactivar las afirmaciones 187

Uso de las afirmaciones 188

La API de registro (Logging API) 189

Resumen 189

Niveles de registro 191

Un ejemplo sencillo 192

Propiedades de configuración del registro 193

El Logger 195
Rendimiento 195

Excepciones del mundo real 196

7. Colecciones y genéricos.................................................................................................................197

Colecciones 197

La interfaz Collection 198

Tipos de colecciones 199

La interfaz Map 201

Limitaciones de tipo 203

Contenedores: construyendo una mejor trampa para ratones 203

¿Pueden arreglarse los contenedores? 205

Introducción a los genéricos 205

Hablando sobre tipos 208

"No hay cuchara" 209

Borrado 210

Tipos en bruto 211

Relaciones de tipos parametrizados 213

¿Por qué no es una List<Date> una List<Object>? 214

Conversiones (castings) 215

Conversión entre colecciones y arrays 216

Iterador 217

Un vistazo más cercano: el método sort() 218

Aplicación: árboles en el campo 219

Conclusión 221

8. Utilidades básicas de texto y núcleo..............................................................................................223

Cadenas de texto 223

Construcción de cadenas de texto 224

Cadenas de cosas 225

Comparación de cadenas 226

Búsqueda 227

Resumen de métodos de cadenas de texto 228

Cosas a partir de cadenas de texto 229

Análisis de números primitivos 229

Tokenización de texto 230

Expresiones regulares 232

Notación Regex 232


La API java.util.regex 238

Utilidades matemáticas 243

La clase java.lang.Math 244

Números grandes / precisos 247

Fechas y horas 248

Fechas y horas locales 248

Comparación y manipulación de fechas y horas 249

Zonas horarias 250

Análisis y formateo de fechas y horas 251

Errores de análisis 253

Marcas de tiempo (timestamps) 255

Otras utilidades útiles 255

9. Hilos..............................................................................................................................................257

Introducción a los hilos 258

La clase Thread y la interfaz Runnable 258

Controlando hilos 262

Muerte de un hilo 267

Sincronización 268

Serialización de acceso a métodos 269

Acceso a variables de clase e instancia desde múltiples hilos 274

Programación y prioridad 275

Estado de los hilos 277

División de tiempo 278

Prioridades 279

Ceder (yield) 280

Rendimiento del hilo 280

El costo de la sincronización 280

Consumo de recursos del hilo 281

Utilidades de concurrencia 282

10. Aplicaciones de escritorio............................................................................................................285

¡Botones y controles deslizantes y campos de texto, oh Dios mío! 286

Jerarquías de componentes 286

Arquitectura Modelo Vista Controlador 287

Etiquetas y botones 288

Componentes de texto 294


Otros componentes 302

Contenedores y disposiciones (layouts) 306

Marcos y ventanas 307

JPanel 309

Gestores de disposición (Layout Managers) 310

Eventos 318

Eventos de ratón 319

Eventos de acción 322

Eventos de cambio 325

Otros eventos 326

Modales y pop ups 327

Diálogos de mensaje 327

Diálogos de confirmación 330

Diálogos de entrada 332

Consideraciones sobre hilos 332

SwingUtilities y actualizaciones de componentes 333

Temporizadores 336

Próximos pasos 339

Menús 339

Preferencias 341

Componentes personalizados y Java2D 341

JavaFX 342

Interfaz de usuario y experiencia de usuario 342

11. Redes y E/S.................................................................................................................................343

Flujos (Streams) 343

E/S básica 345

Flujos de caracteres 348

Envoltorios de flujos 349

La clase java.io.File 353

Flujos de archivos 358

RandomAccessFile 360

La API de archivos NIO 361

FileSystem y Path 362

Operaciones de archivos NIO 364

El paquete NIO 366


E/S asincrónica 367

Rendimiento 367

Archivos mapeados y bloqueados 368

Canales 368

Buffers 369

Codificadores y decodificadores de caracteres 372

FileChannel 374

Programación de red 377

Sockets 379

Clientes y servidores 380

El cliente DateAtHost 384

Un juego distribuido 386

Más para explorar 396

12. Programación para la web...........................................................................................................397

Localizadores de recursos uniformes 397

La clase URL 398

Datos de flujo 399

Obtener el contenido como un objeto 400

Gestión de conexiones 401

Manejadores en la práctica 402

Frameworks de manejo útiles 403

Hablar con aplicaciones web 403

Usar el método GET 404

Usar el método POST 405

El HttpURLConnection 408

SSL y comunicaciones web seguras 409

Aplicaciones web de Java 409

El ciclo de vida del servlet 411

Servlets 412

El servlet HelloClient 413

La respuesta del servlet 415

Parámetros del servlet 416

Gestión de sesiones de usuario 419

El servlet ShowSession 420

Contenedores de servlets 422


Configuración con web.xml y anotaciones 423

Mapeos de patrones de URL 426

Despliegue de HelloClient 427

El World Wide Web es, bueno, amplio 428

13. Ampliando Java...........................................................................................................................429

Versiones de Java 429

JCP y JSRs 430

Expresiones lambda 430

Adaptar su código 431

Ampliación de Java más allá del núcleo 437

Conclusión final y próximos pasos 437

A. Ejemplos de código e IntelliJ IDEA.................................................................................................439

Glosario............................................................................................................................................459

Índice................................................................................................................................................473
Prefacio

Este libro trata sobre el lenguaje de programación y el entorno Java. Ya seas un desarrollador de software
o simplemente alguien que usa Internet en tu vida diaria, seguramente has oído hablar de Java. Su
introducción fue uno de los desarrollos más emocionantes en la historia de la web, y las aplicaciones Java
han impulsado gran parte del crecimiento empresarial en Internet. Java es, probablemente, el lenguaje
de programación más popular del mundo, utilizado por millones de desarrolladores en casi todos los tipos
de computadoras imaginables. Java ha superado a lenguajes como C++ y Visual Basic en cuanto a la
demanda de desarrolladores y se ha convertido en el lenguaje de facto para ciertos tipos de desarrollo,
especialmente para servicios basados en la web. La mayoría de las universidades ahora utilizan Java en
sus cursos introductorios junto con otros importantes lenguajes modernos. ¡Quizás estés usando este
texto en una de tus clases en este momento!

Este libro te proporciona una base sólida en los fundamentos y APIs de Java. "Learning Java", quinta
edición, intenta cumplir con su nombre trazando el lenguaje Java y sus bibliotecas de clases, técnicas de
programación y modismos. Nos adentraremos profundamente en áreas interesantes y al menos
rascaremos la superficie de otros temas populares. Otros títulos de O'Reilly continúan donde nosotros
terminamos y proporcionan información más exhaustiva sobre áreas y aplicaciones específicas de Java.

Siempre que sea posible, proporcionamos ejemplos convincentes, realistas y divertidos, evitando
simplemente enumerar características. Los ejemplos son simples, pero insinúan lo que se puede hacer.
No estaremos desarrollando la próxima gran "aplicación asesina" en estas páginas, pero esperamos darte
un punto de partida para muchas horas de experimentación y exploración inspirada que te llevarán a
desarrollar una tú mismo.

¿Quién debería leer este libro?

Este libro es para profesionales de la informática, estudiantes, personas técnicas y hackers finlandeses. Es
para todos aquellos que necesitan experiencia práctica con el lenguaje Java con miras a construir
aplicaciones reales. Este libro también podría considerarse un curso intensivo en programación orientada
a objetos, redes e interfaces de usuario.

A medida que aprendes sobre Java, también aprenderás un enfoque poderoso y práctico para el
desarrollo de software, comenzando con una comprensión profunda de los fundamentos de Java y sus
APIs.

Superficialmente, Java parece C o C++, por lo que tendrás una pequeña ventaja al usar este libro si tienes
algo de experiencia con uno de estos lenguajes. Si no lo tienes, no te preocupes. No hagas demasiado
hincapié en las similitudes sintácticas entre Java y C o C++. En muchos aspectos, Java actúa como lenguajes
más dinámicos como Smalltalk y Lisp. Conocer otro lenguaje de programación orientado a objetos
ciertamente debería ayudar, aunque es posible que tengas que cambiar algunas ideas y desaprender
algunos hábitos. Java es considerablemente más simple que lenguajes como C++ y Smalltalk. Si aprendes
bien a partir de ejemplos concisos y experimentación personal, creemos que te gustará este libro.

La última parte de este libro se expande para discutir Java en el contexto de aplicaciones web, servicios
web y procesamiento de solicitudes, por lo que deberías estar familiarizado con las ideas básicas detrás
de los navegadores web, servidores y documentos.

Nuevos desarrollos

Esta edición de "Learning Java" es en realidad la séptima edición, actualizada y cambiada de nombre, de
nuestro original y popular "Exploring Java". Con cada edición, nos hemos esforzado no solo en agregar
nuevo material que cubra características adicionales, sino también en revisar y actualizar a fondo el
contenido existente para sintetizar la cobertura y agregar años de perspectiva y experiencia del mundo
real a estas páginas.

Un cambio notable en las ediciones recientes es que hemos restado importancia al uso de applets,
reflejando su papel disminuido en los últimos años en la creación de páginas web interactivas. En
contraste, hemos expandido considerablemente nuestra cobertura de aplicaciones web y servicios web
de Java, que ahora son tecnologías maduras.

Cubrimos todas las características importantes de la última versión de "soporte a largo plazo" de Java,
oficialmente llamada Java Standard Edition (SE) 11, OpenJDK 11, pero también incluimos algunos detalles
de las versiones "de características" de Java 12, Java 13 y Java 14. Sun Microsystems (el custodio de Java
antes de Oracle) ha cambiado el esquema de nombres muchas veces a lo largo de los años. Sun acuñó el
término Java 2 para cubrir las características nuevas importantes introducidas en la versión 1.2 de Java y
eliminó el término JDK en favor de SDK. Con la sexta versión, Sun saltó de la versión 1.4 a Java 5.0, pero
mantuvo el término JDK y conservó su convención de numeración allí. Después de eso, tuvimos Java 6,
Java 7, y así sucesivamente, y ahora estamos en Java 14.

Esta versión de Java refleja un lenguaje maduro con cambios sintácticos ocasionales y actualizaciones en
APIs y bibliotecas. Hemos intentado capturar estas nuevas características y actualizar cada ejemplo en
este libro para reflejar no solo la práctica actual de Java, sino también el estilo.

Nuevo en esta edición (Java 11, 12, 13, 14)

Esta edición del libro continúa nuestra tradición de revisión para ser lo más completa y actualizada posible.
Incorpora cambios tanto de la versión de soporte a largo plazo de Java 11, como de las versiones de
características Java 12, 13 y 14. (Más detalles sobre las especificaciones de las características de Java
incluidas y excluidas en las versiones recientes se encuentran en el Capítulo 13.) Los nuevos temas en esta
edición incluyen:

• Nuevas características del lenguaje, incluida la inferencia de tipos en genéricos y una mejora en el
manejo de excepciones y la sintaxis de gestión automática de recursos.

• Un nuevo entorno interactivo, jshell, para probar fragmentos de código.


• La expresión switch propuesta.
• Expresiones lambda básicas.
• Ejemplos actualizados y análisis en todo el libro.

Uso de este libro

Este libro está organizado aproximadamente de la siguiente manera:

• Los Capítulos 1 y 2 proporcionan una introducción básica a los conceptos de Java y un tutorial para
darte un impulso en la programación de Java.

• El Capítulo 3 discute herramientas fundamentales para desarrollar con Java (el compilador, el
intérprete, jshell y el paquete de archivos JAR).

• Los Capítulos 4 y 5 introducen fundamentos de programación, luego describen el propio lenguaje Java,
comenzando con la sintaxis básica y luego cubriendo clases y objetos, excepciones, matrices,
enumeraciones, anotaciones y mucho más.

• El Capítulo 6 cubre excepciones, errores y las facilidades de registro nativas de Java.


• El Capítulo 7 cubre colecciones junto con genéricos y tipos parametrizados en Java.
• El Capítulo 8 cubre el procesamiento de texto, formato, escaneo, utilidades de cadenas y gran parte de
las utilidades principales de la API.

• El Capítulo 9 cubre las facilidades de hilos integradas en el lenguaje.


• El Capítulo 10 cubre los conceptos básicos del desarrollo de interfaces gráficas de usuario (GUI) con
Swing.

• El Capítulo 11 cubre E/S (Entrada/Salida) de Java, flujos, archivos, sockets, redes y el paquete NIO.
• El Capítulo 12 cubre aplicaciones web utilizando servlets, filtros de servlets y archivos WAR, así como
servicios web.

• El Capítulo 13 introduce el Proceso de Comunidad de Java y destaca cómo rastrear los cambios futuros
en Java, al mismo tiempo que te ayuda a adaptar código existente a nuevas características, como las
expresiones lambda introducidas en Java 8.

Si eres como nosotros, no lees los libros de principio a fin. Si realmente eres como nosotros, normalmente
ni siquiera lees el prólogo. Sin embargo, en caso de que veas esto a tiempo, aquí tienes algunas
sugerencias:

• Si ya eres programador y solo necesitas aprender Java en los próximos cinco minutos, probablemente
estés buscando los ejemplos. Puede que quieras empezar echando un vistazo al tutorial en el Capítulo 2.
Si eso no es lo tuyo, al menos deberías revisar la información en el Capítulo 3, que explica cómo usar el
compilador y el intérprete. Esto debería ayudarte a comenzar.

• Los Capítulos 11 y 12 son los lugares a los que dirigirse si estás interesado en escribir aplicaciones y
servicios basados en red o web. La red sigue siendo una de las partes más interesantes e importantes de
Java.

• El Capítulo 10 discute las características gráficas y la arquitectura de componentes de Java. Deberías


leer esto si estás interesado en escribir aplicaciones Java gráficas de escritorio.

• El Capítulo 13 discute cómo mantenerte al tanto de los cambios en el propio lenguaje Java,
independientemente de tu enfoque particular.

Recursos en línea

Existen muchas fuentes en línea para obtener información sobre Java.

El sitio web oficial de Oracle para temas de Java es https://oreil.ly/Lo8QZ; aquí encontrarás el software,
las actualizaciones y las versiones de Java. Aquí encontrarás la implementación de referencia del JDK, que
incluye el compilador, el intérprete y otras herramientas.

Oracle también mantiene el sitio OpenJDK. Esta es la versión principal de código abierto de Java y las
herramientas asociadas. Usaremos OpenJDK para todos los ejemplos en este libro.

También deberías visitar el sitio web de O'Reilly en http://oreilly.com/. Allí encontrarás información sobre
otros libros de O'Reilly tanto para Java como para una creciente variedad de otros temas. También
deberías consultar las opciones de aprendizaje en línea y conferencias; O'Reilly es un verdadero defensor
de la educación en todas sus formas.

Y, por supuesto, ¡puedes revisar la página de inicio de "Learning Java"!

Convenciones utilizadas en este libro

Las convenciones tipográficas utilizadas en este libro son bastante simples.


Cursiva se utiliza para:

• Rutas de acceso, nombres de archivos y nombres de programas.


• Direcciones de Internet, como nombres de dominio y URL.
• Nuevos términos cuando se definen.
• Nombres de programas, compiladores, intérpretes, utilidades y comandos.
• Hilos (threads).
Ancho constante se utiliza para:

• Cualquier cosa que pueda aparecer en un programa de Java, incluidos nombres de métodos, nombres
de variables y nombres de clases.

• Etiquetas que pueden aparecer en un documento HTML o XML.


• Palabras clave, objetos y variables de entorno.
Ancho constante en negrita se utiliza para:

• Texto que es escrito por el usuario en la línea de comandos o en un diálogo.


Ancho constante en cursiva se utiliza para:

• Elementos reemplazables en el código.


En el cuerpo principal del texto, siempre usamos un par de paréntesis vacíos después de un nombre de
método para distinguir los métodos de las variables y otras entidades.

En las listas de código fuente de Java, seguimos las convenciones de codificación más utilizadas en la
comunidad Java. Los nombres de las clases comienzan con letras mayúsculas; los nombres de variables y
métodos comienzan con minúsculas. Todas las letras en los nombres de las constantes están en
mayúsculas. No utilizamos guiones bajos para separar palabras en un nombre largo; siguiendo la práctica
común, capitalizamos las palabras individuales (después de la primera) y juntamos las palabras. Por
ejemplo: thisIsAVariable, thisIsAMethod(), ThisIsAClass, y THIS_IS_A_CONSTANT. Además, ten en cuenta
que diferenciamos entre métodos estáticos y no estáticos cuando nos referimos a ellos. A diferencia de
algunos libros, nunca escribimos Foo.bar() para significar el método bar() de Foo a menos que bar() sea
un método estático (paralelizando la sintaxis de Java en ese caso).

Utilización de ejemplos de código

Si tienes una pregunta técnica o un problema utilizando los ejemplos de código, por favor envía un correo
electrónico a [email protected].

Este libro está aquí para ayudarte a realizar tu trabajo. En general, si se ofrece código de ejemplo con este
libro, puedes usarlo en tus programas y documentación. No necesitas contactarnos para obtener permiso
a menos que estés reproduciendo una parte significativa del código. Por ejemplo, escribir un programa
que utilice varios fragmentos de código de este libro no requiere permiso. Vender o distribuir ejemplos
de libros de O'Reilly sí requiere permiso. Responder a una pregunta citando este libro y citando ejemplos
de código no requiere permiso. Incorporar una cantidad significativa de código de ejemplo de este libro
en la documentación de tu producto sí requiere permiso.

Apreciamos, pero generalmente no requerimos, atribución. Una atribución generalmente incluye el título,
autor, editorial y ISBN. Por ejemplo: "Learning Java, quinta edición, por Marc Loy, Patrick Niemeyer y
Daniel Leuck (O'Reilly). Copyright 2020 Marc Loy, Patrick Niemeyer y Daniel Leuck, 978-1-492-05627-0."
Si sientes que tu uso de ejemplos de código está fuera del uso justo o del permiso otorgado
anteriormente, no dudes en contactarnos en [email protected].

Aprendizaje en línea de O'Reilly

Durante más de 40 años, O'Reilly Media ha proporcionado capacitación,


conocimientos e información tecnológica y empresarial para ayudar a las empresas a tener éxito.

Nuestra red única de expertos e innovadores comparte su conocimiento y experiencia a través de libros,
artículos y nuestra plataforma de aprendizaje en línea. La plataforma de aprendizaje en línea de O'Reilly
te brinda acceso bajo demanda a cursos de capacitación en vivo, rutas de aprendizaje en profundidad,
entornos interactivos de codificación y una vasta colección de textos y videos de O'Reilly y más de 200
editoriales. Para obtener más información, visita http://oreilly.com.

Cómo contactarnos

Por favor, dirige comentarios y preguntas relacionadas con este libro al editor:

O'Reilly Media, Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

800-998-9938 (en Estados Unidos o Canadá)

707-829-0515 (internacional o local)

707-829-0104 (fax)

Tenemos una página web para este libro donde listamos errores y cualquier información adicional. Puedes
acceder a esta página en https://oreil.ly/Java_5e.

El código de ejemplo se encuentra separado en GitHub. Hay dos repositorios para este libro: los ejemplos
principales y los ejemplos web. Se proporcionan más detalles sobre cómo acceder y trabajar con los
ejemplos en el Apéndice A.

Envía un correo electrónico a [email protected] para comentar o hacer preguntas técnicas


sobre este libro.

Para obtener más información sobre nuestros libros, cursos y noticias, consulta nuestro sitio web en
http://www.oreilly.com.

Encuéntranos en Facebook: http://facebook.com/oreilly

Síguenos en Twitter: http://twitter.com/oreillymedia

Visítanos en YouTube: http://www.youtube.com/oreillymedia


Agradecimientos

Muchas personas han contribuido a la elaboración de este libro, tanto en su encarnación como "Exploring
Java" como en su forma actual como "Learning Java". En primer lugar, nos gustaría agradecer a Tim
O'Reilly por darnos la oportunidad de escribir este libro. Gracias a Mike Loukides, el editor de la serie,
cuya paciencia y experiencia continúan guiándonos.

Otras personas de O'Reilly, incluidas Amelia Blevins, Zan McQuade, Corbin Collins y Jessica Haberman,
han proporcionado sabiduría y aliento consistentes. No podríamos haber pedido un equipo de personas
más hábiles o receptivas con quienes trabajar.

La versión original del glosario provino del libro de David Flanagan "Java en un Nutshell" (O'Reilly).
También tomamos prestados varios diagramas de jerarquía de clases del libro de David. Estos diagramas
se basaron en diagramas similares de Charles L. Perkins.

Un cálido agradecimiento a Ron Becker por su sólido asesoramiento e ideas interesantes desde la
perspectiva de un lego alejado del mundo de la programación. También gracias a James Elliott y Dan Leuck
por sus excelentes y oportunas retroalimentaciones sobre el contenido técnico de esta edición. Como
sucede con muchas cosas en el mundo de la programación, los ojos adicionales son indispensables y
tenemos la suerte de haber tenido tales pares atentos de nuestro lado.
CAPÍTULO 1

Un Lenguaje Moderno
Los mayores desafíos y las oportunidades más emocionantes para los desarrolladores de software hoy en
día radican en aprovechar el poder de las redes. Las aplicaciones creadas hoy, independientemente de su
alcance o audiencia prevista, casi con seguridad se ejecutarán en máquinas conectadas por una red global
de recursos informáticos. La creciente importancia de las redes está imponiendo nuevas demandas a las
herramientas existentes y alimentando la demanda de una lista rápidamente creciente de tipos
completamente nuevos de aplicaciones.

Queremos software que funcione, de manera consistente, en cualquier lugar, en cualquier plataforma, y
que se integre bien con otras aplicaciones. Queremos aplicaciones dinámicas que aprovechen un mundo
conectado, capaces de acceder a fuentes de información dispares y distribuidas. Queremos software
verdaderamente distribuido que se pueda ampliar y actualizar sin problemas. Queremos aplicaciones
inteligentes que puedan explorar la red por nosotros, buscando información y actuando como emisarios
electrónicos. Hace tiempo que sabemos qué tipo de software queremos, pero realmente solo en los
últimos años hemos empezado a obtenerlo.

El problema, históricamente, ha sido que las herramientas para construir estas aplicaciones han sido
insuficientes. Los requisitos de velocidad y portabilidad han sido, en su mayor parte, mutuamente
excluyentes, y la seguridad ha sido ampliamente ignorada o mal entendida. En el pasado, los lenguajes
verdaderamente portátiles eran voluminosos, interpretados y lentos. Estos lenguajes eran populares
tanto por su funcionalidad de alto nivel como por su portabilidad. Los lenguajes rápidos generalmente
proporcionaban velocidad al vincularse a plataformas específicas, por lo que solo resolvían parcialmente
el problema de la portabilidad. Incluso había algunos lenguajes seguros, pero eran principalmente
variantes de los lenguajes portátiles y sufrían de los mismos problemas. Java es un lenguaje moderno que
aborda estos tres frentes: portabilidad, velocidad y seguridad. Por eso sigue siendo un lenguaje dominante
en el mundo de la programación más de dos décadas después de su introducción.

Introducción a Java

El lenguaje de programación Java, desarrollado en Sun Microsystems bajo la dirección de los destacados
James Gosling y Bill Joy, fue diseñado para ser un lenguaje de programación independiente de la máquina
que es lo suficientemente seguro para atravesar redes y lo suficientemente potente para reemplazar el
código ejecutable nativo. Java aborda los problemas planteados aquí y tuvo un papel protagonista en el
crecimiento de Internet, llevándonos a donde estamos hoy.

Inicialmente, la mayoría del entusiasmo por Java se centraba en sus capacidades para construir
aplicaciones incrustadas para la web, llamadas applets. Pero en los primeros días, los applets y otras
aplicaciones GUI del lado del cliente escritas en Java eran limitadas. Hoy en día, Java cuenta con Swing,
un kit de herramientas sofisticado para construir interfaces gráficas de usuario. Este desarrollo ha
permitido que Java se convierta en una plataforma viable para desarrollar software de aplicaciones
tradicionales del lado del cliente, aunque muchos otros contendientes han entrado en este campo
abarrotado.

Sin embargo, lo más importante es que Java se ha convertido en la plataforma principal para aplicaciones
y servicios basados en la web. Estas aplicaciones utilizan tecnologías que incluyen la API de Java Servlet,
los servicios web de Java y muchos servidores de aplicaciones y marcos de trabajo de Java de código
abierto y comerciales populares. La portabilidad y velocidad de Java lo convierten en la plataforma
preferida para aplicaciones comerciales modernas. Los servidores Java que se ejecutan en plataformas
Linux de código abierto son el corazón del mundo empresarial y financiero hoy en día.
Este libro te mostrará cómo utilizar Java para llevar a cabo tareas de programación del mundo real. En los
próximos capítulos, cubriremos todo, desde procesamiento de texto hasta redes, construcción de
aplicaciones de escritorio con Swing y aplicaciones y servicios basados en la web livianos.

Orígenes de Java

Las semillas de Java fueron sembradas en 1990 por Bill Joy, patriarca de Sun Microsystems y principal
investigador. En ese momento, Sun competía en un mercado de estaciones de trabajo relativamente
pequeño, mientras que Microsoft estaba comenzando su dominio en el mundo más convencional de las
PC basadas en Intel. Cuando Sun se perdió la revolución de la PC, Joy se retiró a Aspen, Colorado, para
trabajar en investigaciones avanzadas. Estaba comprometido con la idea de lograr tareas complejas con
software simple y fundó la adecuadamente llamada Sun Aspen Smallworks.

De los miembros originales del pequeño equipo de programadores reunido en Aspen, James Gosling será
recordado como el padre de Java. Gosling se dio a conocer a principios de los años 80 como el autor de
Gosling Emacs, la primera versión del popular editor Emacs escrita en C y ejecutada bajo Unix. Gosling
Emacs se hizo popular pero pronto fue eclipsado por una versión gratuita, GNU Emacs, escrita por el
diseñador original de Emacs. Para ese momento, Gosling había pasado a diseñar NeWS de Sun, que
brevemente compitió con el X Window System por el control del escritorio GUI de Unix en 1987. Aunque
algunas personas argumentarían que NeWS era superior a X, NeWS perdió porque Sun lo mantuvo como
propietario y no publicó el código fuente, mientras que los desarrolladores principales de X formaron el
Consorcio X y adoptaron el enfoque opuesto.

Diseñar NeWS enseñó a Gosling el poder de integrar un lenguaje expresivo con una interfaz gráfica de
usuario consciente de la red. También enseñó a Sun que la comunidad de programación en internet
finalmente rechazaría los estándares propietarios, no importa cuán buenos pudieran ser. Las semillas del
esquema de licencia de Java y del código abierto (si bien no exactamente "de código abierto") fueron
sembradas por el fracaso de NeWS. Gosling llevó lo que había aprendido al incipiente proyecto de Aspen
de Bill Joy. En 1992, el trabajo en el proyecto llevó a la fundación de la subsidiaria de Sun, FirstPerson, Inc.
Su misión era llevar a Sun al mundo de la electrónica de consumo.

El equipo de FirstPerson trabajó en el desarrollo de software para electrodomésticos de información,


como teléfonos celulares y asistentes digitales personales (PDA). El objetivo era permitir la transferencia
de información y aplicaciones en tiempo real a través de redes infrarrojas baratas y basadas en paquetes
tradicionales. Las limitaciones de memoria y ancho de banda dictaban un código pequeño y eficiente. La
naturaleza de las aplicaciones también exigía que fueran seguras y robustas. Gosling y sus compañeros de
equipo comenzaron a programar en C++, pero pronto se vieron confundidos por un lenguaje que era
demasiado complejo, difícil de manejar e inseguro para la tarea. Decidieron comenzar desde cero, y
Gosling comenzó a trabajar en algo que denominó "C++ menos menos".

Con el fracaso de la Apple Newton (la primera computadora de mano de Apple), quedó claro que el barco
de las PDA aún no había llegado, por lo que Sun cambió los esfuerzos de FirstPerson a la televisión
interactiva (ITV). El lenguaje de programación elegido para los decodificadores de ITV fue el antecesor
cercano de Java, un lenguaje llamado Oak. Incluso con su elegancia y capacidad para proporcionar
interactividad segura, Oak no pudo salvar la causa perdida de la ITV en ese momento. Los clientes no lo
querían y Sun pronto abandonó el concepto.

En ese momento, Joy y Gosling se reunieron para decidir una nueva estrategia para su lenguaje innovador.
Era 1993 y la explosión de interés en la web presentaba una nueva oportunidad. Oak era pequeño, seguro,
independiente de la arquitectura y orientado a objetos. Casualmente, estas también son algunas de las
características requeridas para un lenguaje de programación universal y conocedor de Internet. Sun
cambió rápidamente su enfoque y, con algunos ajustes, Oak se convirtió en Java.

Crecimiento

No sería exagerado decir que Java (y su paquete orientado a los desarrolladores, el Java Development Kit
o JDK) se extendió como un incendio forestal. Incluso antes de su primer lanzamiento oficial cuando Java
todavía no era un producto, casi todos los principales actores de la industria se habían subido al carro de
Java. Los licenciatarios de Java incluían a Microsoft, Intel, IBM y prácticamente todos los principales
proveedores de hardware y software. Sin embargo, incluso con todo este apoyo, Java recibió muchos
golpes y experimentó algunos dolores de crecimiento durante sus primeros años.

Una serie de demandas por incumplimiento de contrato y antimonopolio entre Sun y Microsoft sobre la
distribución de Java y su uso en Internet Explorer obstaculizaron su despliegue en el sistema operativo de
escritorio más común del mundo: Windows. La participación de Microsoft en Java también se convirtió
en uno de los focos de una demanda federal más grande por prácticas anticompetitivas graves en la
empresa, con testimonios judiciales que revelaron esfuerzos concertados por parte del gigante del
software para socavar Java al introducir incompatibilidades en su versión del lenguaje. Mientras tanto,
Microsoft introdujo su propio lenguaje derivado de Java llamado C# (C-sharp) como parte de su iniciativa
.NET y eliminó Java de la inclusión en Windows. C# ha llegado a ser un lenguaje muy bueno por derecho
propio, disfrutando de más innovación en los últimos años que Java.

Pero Java continúa extendiéndose en una amplia variedad de plataformas. Al comenzar a analizar la
arquitectura de Java, verás que gran parte de lo emocionante de Java proviene del entorno de máquina
virtual autónoma en el que se ejecutan las aplicaciones Java. Java fue cuidadosamente diseñado para que
esta arquitectura de soporte pueda implementarse tanto en software, para plataformas informáticas
existentes, como en hardware personalizado. Las implementaciones de hardware de Java se utilizan en
algunas tarjetas inteligentes y otros sistemas integrados. Incluso puedes comprar dispositivos "vestibles",
como anillos y placas de identificación para perros, que tienen intérpretes de Java integrados. Las
implementaciones de software de Java están disponibles para todas las plataformas informáticas
modernas, incluso para dispositivos informáticos portátiles. Hoy en día, una derivación de la plataforma
Java es la base del sistema operativo Android de Google, que alimenta miles de millones de teléfonos y
otros dispositivos móviles.

En 2010, Oracle Corporation compró Sun Microsystems y se convirtió en el guardián del lenguaje Java. En
un comienzo algo problemático de su mandato, Oracle demandó a Google por el uso del lenguaje Java en
Android y perdió. En julio de 2011, Oracle lanzó Java SE 7, un lanzamiento significativo de Java que incluía
un nuevo paquete de E/S en 2017. Java 9 introdujo módulos para abordar algunos problemas de larga
data con la ruta de clase y el crecimiento del tamaño del JDK en sí. Java 9 también inició un proceso rápido
de actualización que llevó a Java 11 a ser la versión actual con soporte a largo plazo. (Más sobre estas y
otras versiones en "Un Mapa de Java" en la página 21.) Oracle continúa liderando el desarrollo de Java;
sin embargo, también han bifurcado el mundo Java al mover el entorno principal de implementación de
Java a una licencia comercial costosa y ofrecer una opción subsidiaria gratuita de OpenJDK que conserva
la accesibilidad que muchos desarrolladores aman y esperan.

Una Máquina Virtual

Java es tanto un lenguaje compilado como interpretado. El código fuente de Java se convierte en
instrucciones binarias simples, muy similar al código de máquina ordinario de un microprocesador. Sin
embargo, mientras que el código fuente de C o C++ se reduce a instrucciones nativas para un modelo
específico de procesador, el código fuente de Java se compila en un formato universal: instrucciones para
una máquina virtual (VM).

El bytecode compilado de Java se ejecuta mediante un intérprete de tiempo de ejecución de Java. El


sistema de tiempo de ejecución realiza todas las actividades normales de un procesador de hardware,
pero lo hace en un entorno virtual seguro. Ejecuta un conjunto de instrucciones basadas en pila y gestiona
la memoria como un sistema operativo. Crea y manipula tipos de datos primitivos, carga e invoca bloques
de código recién referenciados. Lo más importante es que realiza todo esto de acuerdo con una
especificación abierta estrictamente definida que puede ser implementada por cualquier persona que
desee producir una máquina virtual compatible con Java. En conjunto, la máquina virtual y la definición
del lenguaje proporcionan una especificación completa. No quedan características del lenguaje base de
Java sin definir ni dependientes de la implementación. Por ejemplo, Java especifica los tamaños y
propiedades matemáticas de todos sus tipos de datos primitivos en lugar de dejarlo a la implementación
de la plataforma.
El intérprete de Java es relativamente ligero y pequeño; puede implementarse en la forma que se desee
para una plataforma específica. El intérprete puede ejecutarse como una aplicación independiente o
puede integrarse en otro software, como un navegador web. En conjunto, esto significa que el código Java
es implícitamente portátil. El mismo bytecode de una aplicación Java puede ejecutarse en cualquier
plataforma que proporcione un entorno de ejecución de Java, como se muestra en la Figura 1-1. No es
necesario producir versiones alternativas de tu aplicación para diferentes plataformas, ni distribuir el
código fuente a los usuarios finales.

La unidad fundamental del código Java es la clase. Al igual que en otros lenguajes orientados a objetos,
las clases son componentes de la aplicación que contienen código ejecutable y datos. Las clases Java
compiladas se distribuyen en un formato binario universal que contiene bytecode de Java y otra
información de la clase. Las clases pueden mantenerse de forma discreta y almacenarse en archivos o
archivos de forma local o en un servidor de red. Las clases se localizan y cargan dinámicamente en tiempo
de ejecución según sean necesarias por una aplicación.

Además del sistema de tiempo de ejecución específico de la plataforma, Java tiene una serie de clases
fundamentales que contienen métodos dependientes de la arquitectura. Estos métodos nativos sirven
como el enlace entre la máquina virtual de Java y el mundo real. Se implementan en un lenguaje
nativamente compilado en la plataforma anfitriona y proporcionan acceso a nivel bajo a recursos como la
red, el sistema de ventanas y el sistema de archivos del host. Sin embargo, la gran mayoría de Java está
escrita en Java mismo, arrancada desde estos primitivos básicos, y por lo tanto es portátil. Esto incluye
herramientas fundamentales de Java como el compilador de Java, las bibliotecas de redes y GUI, que
también están escritas en Java y, por lo tanto, están disponibles en todas las plataformas de Java de la
misma manera sin necesidad de adaptación.

Históricamente, los intérpretes se han considerado lentos, pero Java no es un lenguaje interpretado
tradicional. Además de compilar el código fuente a bytecode portable, Java también ha sido diseñado
cuidadosamente para que las implementaciones de software del sistema de tiempo de ejecución puedan
optimizar aún más su rendimiento compilando el bytecode a código de máquina nativo sobre la marcha.
Esto se llama compilación justo a tiempo (JIT) o compilación dinámica. Con la compilación JIT, el código
Java puede ejecutarse tan rápido como el código nativo y mantener su portabilidad y seguridad.
Este es un punto a menudo malinterpretado entre aquellos que quieren comparar el rendimiento del
lenguaje. Solo hay una penalización de rendimiento intrínseco que sufre el código Java compilado en
tiempo de ejecución por seguridad y diseño de la máquina virtual: la verificación de límites de matrices.
Todo lo demás puede optimizarse a código nativo al igual que con un lenguaje compilado estáticamente.

Yendo más allá, el lenguaje Java incluye más información estructural que muchos otros lenguajes, lo que
permite más tipos de optimizaciones. Además, recuerda que estas optimizaciones se pueden hacer en
tiempo de ejecución, teniendo en cuenta el comportamiento y las características reales de la aplicación.

¿Qué se puede hacer en tiempo de compilación que no se pueda hacer mejor en tiempo de ejecución?
Bueno, hay un intercambio: tiempo. El problema con una compilación JIT tradicional es que optimizar el
código lleva tiempo. Por lo tanto, un compilador JIT puede producir resultados decentes, pero puede sufrir
una latencia significativa cuando la aplicación se inicia. Esto generalmente no es un problema para
aplicaciones del lado del servidor que se ejecutan durante mucho tiempo, pero es un problema grave para
el software del lado del cliente y las aplicaciones que se ejecutan en dispositivos más pequeños con
capacidades limitadas.

Para abordar esto, la tecnología de compilación de Java, llamada HotSpot, utiliza un truco llamado
compilación adaptativa. Si observas en qué pasan su tiempo los programas, resulta que pasan casi todo
su tiempo ejecutando una parte relativamente pequeña del código una y otra vez. El fragmento de código
que se ejecuta repetidamente puede ser solo una pequeña fracción del programa total, pero su
comportamiento determina el rendimiento general del programa. La compilación adaptativa también
permite que el tiempo de ejecución de Java aproveche nuevos tipos de optimizaciones que simplemente
no se pueden hacer en un lenguaje compilado estáticamente, de ahí la afirmación de que el código Java
puede ejecutarse más rápido que C/C++ en algunos casos.

Para aprovechar este hecho, HotSpot comienza como un intérprete de bytecode de Java normal, pero con
una diferencia: mide (perfilado) el código mientras se ejecuta para ver qué partes se ejecutan
repetidamente. Una vez que sabe qué partes del código son cruciales para el rendimiento, HotSpot
compila esas secciones en código de máquina nativo óptimo. Dado que compila solo una pequeña parte
del programa en código de máquina, puede permitirse el tiempo necesario para optimizar esas porciones.
Es posible que el resto del programa ni siquiera necesite ser compilado, solo interpretado, lo que ahorra
memoria y tiempo. De hecho, la máquina virtual de Java puede funcionar en uno de dos modos: cliente y
servidor, que determinan si se enfatiza el tiempo de inicio rápido y la conservación de memoria o el
rendimiento absoluto. A partir de Java 9, también puedes utilizar la compilación Ahead-of-Time (AOT) si
minimizar el tiempo de inicio de tu aplicación es realmente importante.

Una pregunta natural en este punto es, ¿por qué desechar toda esta buena información de perfilado cada
vez que se cierra una aplicación? Bueno, Sun abordó parcialmente este tema con el lanzamiento de Java
5.0 mediante el uso de clases compartidas y de solo lectura que se almacenan de forma persistente en
una forma optimizada. Esto redujo significativamente tanto el tiempo de inicio como los costos generales
de ejecutar muchas aplicaciones de Java en una máquina determinada. La tecnología para hacer esto es
compleja, pero la idea es simple: optimiza las partes del programa que necesitan ir rápido y no te
preocupes por el resto.

Comparación de Java con otros lenguajes

Java se basa en muchos años de experiencia de programación con otros lenguajes en su elección de
características. Vale la pena tomar un momento para comparar Java a un nivel alto con algunos otros
lenguajes, tanto para beneficio de aquellos con experiencia en otros lenguajes de programación como
para los recién llegados que necesitan poner las cosas en contexto. No esperamos que tengas
conocimiento de un lenguaje de programación específico en este libro, y cuando nos referimos a otros
lenguajes para comparar, esperamos que los comentarios sean autoexplicativos.

Al menos tres pilares son necesarios para soportar un lenguaje de programación universal hoy en día:
portabilidad, velocidad y seguridad. La Figura 1-2 muestra cómo se compara Java con algunos de los
lenguajes que eran populares cuando fue creado.
Puede que hayas escuchado que Java es muy similar a C o C++, pero eso realmente no es cierto excepto
a un nivel superficial. Cuando observas por primera vez el código Java, verás que la sintaxis básica se
parece a la de C o C++. Pero ahí es donde terminan las similitudes. Java no es en absoluto un descendiente
directo de C ni un C++ de próxima generación. Si comparas las características del lenguaje, verás que Java
en realidad tiene más en común con lenguajes altamente dinámicos, como Smalltalk y Lisp. De hecho, la
implementación de Java está tan alejada del C nativo como puedas imaginar.

El texto proporcionado parece ser un extracto técnico que describe diferencias y características de varios
lenguajes de programación, incluyendo Java, C#, JavaScript y otros. Aquí está una traducción del texto:

"Si estás familiarizado con el panorama actual de lenguajes de programación, notarás que falta C#, un
lenguaje popular, en esta comparación. C# es en gran parte la respuesta de Microsoft a Java,
admitidamente con varias comodidades añadidas. Dadas sus metas de diseño y enfoque comunes (por
ejemplo, el uso de una máquina virtual, bytecode, sandbox, etc.), las plataformas no difieren
sustancialmente en términos de su velocidad o características de seguridad. C# es más o menos tan
portable como Java. Al igual que Java, C# toma prestada gran parte de la sintaxis de C pero es realmente
un pariente más cercano de los lenguajes dinámicos. La mayoría de los desarrolladores de Java encuentran
relativamente fácil aprender C# y viceversa. La mayor parte del tiempo invertido al cambiar de uno a otro
es aprender la biblioteca estándar.

Sin embargo, vale la pena notar las similitudes a nivel superficial entre estos lenguajes. Java toma prestada
en gran medida la sintaxis de C y C++, por lo que verás construcciones de lenguaje concisas, incluyendo
una abundancia de llaves y puntos y comas. Java suscribe a la filosofía de C de que un buen lenguaje debe
ser compacto; en otras palabras, debe ser lo suficientemente pequeño y regular para que un programador
pueda tener todas las capacidades del lenguaje en su cabeza de una vez. Al igual que C es extensible con
bibliotecas, los paquetes de clases de Java pueden ser agregados a los componentes centrales del lenguaje
para ampliar su vocabulario.

C ha tenido éxito porque proporciona un entorno de programación con un conjunto de características


razonablemente completo, con alto rendimiento y un grado aceptable de portabilidad. Java también
intenta equilibrar funcionalidad, velocidad y portabilidad, pero lo hace de una manera muy diferente. C
sacrifica funcionalidad por portabilidad; Java inicialmente sacrificó velocidad por portabilidad. Java
también aborda problemas de seguridad que C no aborda (aunque en sistemas modernos, muchas de
esas preocupaciones ahora se abordan en el sistema operativo y en el hardware).

Si te interesa Node.js, echa un vistazo al desarrollo de Andrew Mead, "Learning Node.js Development", y
a "Learning Node" de Shelley Powers en el sitio de O'Reilly.

En los primeros días antes de la compilación JIT y adaptativa, Java era más lento que los lenguajes
compilados estáticamente, y había una constante reiteración de los detractores de que nunca alcanzaría.
Pero como describimos en la sección anterior, el rendimiento de Java es ahora comparable al de C o C++
para tareas equivalentes, y esas críticas generalmente han quedado en silencio. El motor de videojuegos
de código abierto Quake2 de ID Software ha sido portado a Java. Si Java es lo suficientemente rápido para
juegos de combate en primera persona, ciertamente es lo suficientemente rápido para aplicaciones
empresariales.

Los lenguajes de scripting como Perl, Python y Ruby siguen siendo populares. No hay razón por la cual un
lenguaje de scripting no pueda ser adecuado para aplicaciones seguras y en red. Pero la mayoría de los
lenguajes de scripting no son adecuados para programación seria a gran escala. La atracción de los
lenguajes de scripting es que son dinámicos; son herramientas poderosas para el desarrollo rápido.
Algunos lenguajes de scripting como Perl también proporcionan herramientas poderosas para tareas de
procesamiento de texto que los lenguajes de propósito general encuentran engorrosos. Los lenguajes de
scripting también son altamente portátiles, aunque a nivel de código fuente.

Para no confundirlo con Java, JavaScript es un lenguaje de secuencias de comandos basado en objetos
desarrollado originalmente por Netscape para el navegador web. Sirve como un lenguaje residente en el
navegador web para aplicaciones web dinámicas e interactivas. JavaScript toma su nombre de su
integración y similitudes con Java, pero la comparación realmente termina allí. Sin embargo, hay
aplicaciones significativas de JavaScript fuera del navegador, como Node.js, y continúa ganando
popularidad entre los desarrolladores en una variedad de campos. Para obtener más información sobre
JavaScript, consulta "JavaScript: The Definitive Guide" de David Flanagan (O'Reilly).

El problema con los lenguajes de scripting es que son bastante informales sobre la estructura del
programa y el tipado de datos. La mayoría de los lenguajes de scripting no son orientados a objetos.
También tienen sistemas de tipos simplificados y generalmente no proporcionan un alcance sofisticado
para variables y funciones. Estas características los hacen menos adecuados para construir aplicaciones
grandes y modulares. La velocidad es otro problema con los lenguajes de scripting; la naturaleza
interpretada de alto nivel, generalmente a nivel de código fuente, a menudo los hace bastante lentos.

Los defensores de los lenguajes de scripting individuales podrían discrepar con algunas de estas
generalizaciones, y sin duda tendrían razón en algunos casos. Los lenguajes de scripting han mejorado en
los últimos años, especialmente JavaScript, que ha recibido una enorme cantidad de investigación
dedicada a su rendimiento. Pero el compromiso fundamental es innegable: los lenguajes de scripting
nacieron como alternativas sueltas y menos estructuradas a los lenguajes de programación de sistemas y
generalmente no son ideales para proyectos grandes o complejos por una variedad de razones, al menos
no hoy en día.

Java Ofrecido con Otros Lenguajes | Consulta, por ejemplo, a G. Phipps, "Comparación de tasas
observadas de errores y productividad para Java y C++", Software - Práctica y Experiencia, volumen 29,
1999.

Java ofrece algunas de las ventajas esenciales de un lenguaje de secuencias de comandos: es altamente
dinámico y tiene los beneficios adicionales de un lenguaje de nivel más bajo. Java tiene una potente API
de expresiones regulares que compite con Perl para trabajar con texto y características del lenguaje que
simplifican la codificación con colecciones, listas de argumentos variables, importaciones estáticas de
métodos y otros azúcares sintácticos que lo hacen más conciso.

El desarrollo incremental con componentes orientados a objetos, combinado con la simplicidad de Java,
hace posible desarrollar aplicaciones rápidamente y cambiarlas fácilmente. Estudios han encontrado que
el desarrollo en Java es más rápido que en C o C++, estrictamente basado en características del lenguaje.
Java también cuenta con una gran base de clases centrales estándar para tareas comunes como la
construcción de GUI y el manejo de comunicaciones en red. Maven Central es un recurso externo con una
enorme gama de bibliotecas y paquetes que se pueden integrar rápidamente en tu entorno para ayudarte
a abordar todo tipo de nuevos problemas de programación. Junto con estas características, Java tiene la
escalabilidad y las ventajas de ingeniería de software de lenguajes más estáticos. Proporciona una
estructura segura sobre la cual construir marcos de trabajo de nivel superior (e incluso otros lenguajes).

Como ya hemos dicho, Java es similar en diseño a lenguajes como Smalltalk y Lisp. Sin embargo, estos
lenguajes se usaron principalmente como vehículos de investigación en lugar de para el desarrollo de
sistemas a gran escala. Una razón es que estos lenguajes nunca desarrollaron una vinculación portátil
estándar a los servicios del sistema operativo, como la biblioteca estándar de C o las clases centrales de
Java. Smalltalk se compila a un formato de bytecode interpretado, y se puede compilar dinámicamente a
código nativo sobre la marcha, al igual que Java. Pero Java mejora el diseño utilizando un verificador de
bytecode para garantizar la corrección del código Java compilado. Este verificador le da a Java una ventaja
de rendimiento sobre Smalltalk porque el código Java requiere menos comprobaciones en tiempo de
ejecución. El verificador de bytecode de Java también ayuda con problemas de seguridad, algo que
Smalltalk no aborda.

A lo largo del resto de este capítulo, presentaremos una visión general del lenguaje Java. Explicaremos
qué es nuevo y qué no es tan nuevo sobre Java y por qué.

Seguridad de Diseño

Sin duda has escuchado mucho sobre el hecho de que Java está diseñado para ser un lenguaje seguro.
Pero ¿qué queremos decir con seguro? ¿Seguro de qué o de quién? Las características de seguridad que
atraen más atención para Java son aquellas que hacen posible nuevos tipos de software dinámicamente
portátil. Java proporciona varias capas de protección contra código defectuoso peligroso, así como cosas
más traviesas como virus y caballos de Troya. En la próxima sección, echaremos un vistazo a cómo la
arquitectura de la máquina virtual Java evalúa la seguridad del código antes de ejecutarlo y cómo el
cargador de clases Java (el mecanismo de carga de bytecode del intérprete de Java) construye una barrera
alrededor de clases no confiables. Estas características proporcionan la base para políticas de seguridad
de alto nivel que pueden permitir o prohibir varios tipos de actividades en una base de aplicación por
aplicación.

En esta sección, sin embargo, veremos algunas características generales del lenguaje de programación
Java. Quizás más importante que las características de seguridad específicas, aunque a menudo pasadas
por alto en el ruido de la seguridad, es la seguridad que Java proporciona al abordar problemas comunes
de diseño y programación. Java está destinado a ser lo más seguro posible contra los errores simples que
cometemos nosotros mismos, así como aquellos que heredamos del software heredado. El objetivo con
Java ha sido mantener el lenguaje simple, proporcionar herramientas que hayan demostrado su utilidad
y permitir a los usuarios construir instalaciones más complicadas sobre el lenguaje cuando sea necesario.

Simplificar, Simplificar, Simplificar...

Con Java, la simplicidad es la regla. Dado que Java comenzó desde cero, pudo evitar características que
resultaron ser complicadas o controvertidas en otros lenguajes. Por ejemplo, Java no permite la
sobrecarga de operadores definidos por el programador (lo que en algunos lenguajes permite a los
programadores redefinir el significado de símbolos básicos como + y -). Java no tiene un preprocesador
de código fuente, por lo que no tiene cosas como macros, declaraciones #define o compilación condicional
del código fuente. Estas construcciones existen en otros lenguajes principalmente para soportar
dependencias de plataforma, por lo que en ese sentido, no deberían ser necesarias en Java. La
compilación condicional también se usa comúnmente para depurar, pero las sofisticadas optimizaciones
en tiempo de ejecución de Java y características como las aserciones resuelven el problema de manera
más elegante. (Las aserciones están fuera del alcance de este libro, pero son un tema digno de exploración
después de haber adquirido un conocimiento básico de la programación en Java).

Java proporciona una estructura de paquete bien definida para organizar archivos de clases. El sistema de
paquetes permite al compilador manejar parte de la funcionalidad de la utilidad de compilación
tradicional (una herramienta para construir ejecutables a partir de código fuente). El compilador también
puede trabajar con clases de Java compiladas directamente porque toda la información de tipo está
preservada; no hay necesidad de archivos de "encabezado" de origen superfluos, como en C/C++. Todo
esto significa que el código Java requiere menos contexto para leerlo. De hecho, a veces puede resultar
más rápido mirar el código fuente de Java que consultar la documentación de clases.

Java también adopta un enfoque diferente para algunas características estructurales que han sido
problemáticas en otros lenguajes. Por ejemplo, Java solo admite una jerarquía de clases de herencia única
(cada clase solo puede tener una clase "padre"), pero permite la herencia múltiple de interfaces. Una
interfaz, como una clase abstracta en C++, especifica el comportamiento de un objeto sin definir su
implementación. Es un mecanismo muy poderoso que permite al desarrollador definir un "contrato" para
el comportamiento del objeto que se puede utilizar y referenciar independientemente de cualquier
implementación particular del objeto. Las interfaces en Java eliminan la necesidad de la herencia múltiple
de clases y los problemas asociados.

Como verás en el Capítulo 4, Java es un lenguaje de programación bastante simple y elegante, y eso sigue
siendo una gran parte de su atractivo.

Seguridad de Tipos y Vinculación de Métodos

Un atributo de un lenguaje es el tipo de verificación que utiliza. En general, los lenguajes se categorizan
como estáticos o dinámicos, lo que se refiere a la cantidad de información sobre variables conocida en
tiempo de compilación en comparación con lo que se conoce mientras la aplicación se está ejecutando.

En un lenguaje estrictamente tipado estáticamente, como C o C++, los tipos de datos están definidos de
manera inflexible cuando se compila el código fuente. El compilador se beneficia de esto al tener
suficiente información para detectar muchos tipos de errores antes de que se ejecute el código. Por
ejemplo, el compilador no permitiría almacenar un valor de punto flotante en una variable entera. El
código entonces no requiere verificación de tipo en tiempo de ejecución, por lo que puede ser compilado
para ser pequeño y rápido. Pero los lenguajes tipados estáticamente son inflexibles. No admiten
colecciones tan naturalmente como los lenguajes con verificación de tipo dinámica, y hacen imposible
que una aplicación importe nuevos tipos de datos de manera segura mientras se está ejecutando.

En contraste, un lenguaje dinámico como Smalltalk o Lisp tiene un sistema en tiempo de ejecución que
administra los tipos de objetos y realiza la verificación de tipo necesaria mientras una aplicación se está
ejecutando. Estos tipos de lenguajes permiten un comportamiento más complejo y, en muchos aspectos,
son más poderosos. Sin embargo, también suelen ser más lentos, menos seguros y más difíciles de
depurar.

Las diferencias en los lenguajes se han comparado con las diferencias entre tipos de automóviles. Los
lenguajes tipados estáticamente como C++ se asemejan a un automóvil deportivo: razonablemente
seguros y rápidos, pero útiles solo si se conduce en una carretera bien pavimentada. Los lenguajes
altamente dinámicos como Smalltalk se asemejan más a un vehículo todoterreno: te ofrecen más libertad
pero pueden ser algo difíciles de manejar. Puede ser divertido (y a veces más rápido) atravesar los
bosques, pero también podrías quedarte atascado en una zanja o ser atacado por osos.

Otro atributo de un lenguaje es la forma en que vincula las llamadas a métodos con sus definiciones. En
un lenguaje estático como C o C++, las definiciones de métodos normalmente están vinculadas en tiempo
de compilación, a menos que el programador especifique lo contrario. Los lenguajes como Smalltalk, por
otro lado, se llaman vinculación tardía porque localizan las definiciones de métodos dinámicamente en
tiempo de ejecución. La vinculación temprana es importante por razones de rendimiento; una aplicación
puede ejecutarse sin el costo adicional de buscar métodos en tiempo de ejecución. Pero la vinculación
tardía es más flexible. También es necesaria en un lenguaje orientado a objetos donde se pueden cargar
nuevos tipos de manera dinámica y solo el sistema en tiempo de ejecución puede determinar qué método
ejecutar.

Java ofrece algunos de los beneficios tanto de C++ como de Smalltalk; es un lenguaje tipado estáticamente
con vinculación tardía. Cada objeto en Java tiene un tipo bien definido que se conoce en tiempo de
compilación. Esto significa que el compilador de Java puede hacer el mismo tipo de verificación de tipo
estático y análisis de uso que C++. Como resultado, no puedes asignar un objeto a un tipo de variable
incorrecto o llamar a métodos inexistentes en un objeto. El compilador de Java va aún más allá y evita
que uses variables no inicializadas y crees declaraciones inalcanzables (ver Capítulo 4).

Sin embargo, Java también es totalmente tipado en tiempo de ejecución. El sistema en tiempo de
ejecución de Java realiza un seguimiento de todos los objetos y hace posible determinar sus tipos y
relaciones durante la ejecución. Esto significa que puedes inspeccionar un objeto en tiempo de ejecución
para determinar qué es. A diferencia de C o C++, las conversiones de un tipo de objeto a otro son
verificadas por el sistema en tiempo de ejecución, y es posible usar nuevos tipos de objetos cargados
dinámicamente con un grado de seguridad de tipo. Y debido a que Java es un lenguaje de vinculación
tardía, es posible que una subclase anule métodos en su superclase, incluso una subclase cargada en
tiempo de ejecución.

Desarrollo Incremental

Java lleva toda la información de tipo de datos y firmas de métodos desde su código fuente hasta su forma
compilada en bytecode. Esto significa que las clases de Java se pueden desarrollar incrementalmente. Tu
propio código fuente de Java también se puede compilar de manera segura con clases de otras fuentes
que tu compilador nunca haya visto. En otras palabras, puedes escribir nuevo código que haga referencia
a archivos de clase binarios sin perder la seguridad de tipo que obtienes al tener el código fuente.

Java no sufre del problema de la "clase base frágil". En lenguajes como C++, la implementación de una
clase base puede estar efectivamente congelada porque tiene muchas clases derivadas; cambiar la clase
base puede requerir la recompilación de todas las clases derivadas. Este es un problema especialmente
difícil para los desarrolladores de bibliotecas de clases. Java evita este problema ubicando dinámicamente
los campos dentro de las clases. Si una clase mantiene una forma válida de su estructura original, puede
evolucionar sin romper otras clases que se derivan de ella o que la utilizan.

Gestión de Memoria Dinámica

Algunas de las diferencias más importantes entre Java y lenguajes de nivel más bajo como C y C++
involucran cómo Java administra la memoria. Java elimina los "punteros" ad hoc que pueden hacer
referencia a áreas arbitrarias de memoria y agrega la recolección de basura de objetos y matrices de alto
nivel al lenguaje. Estas características eliminan muchos problemas, de otro modo insuperables, con la
seguridad, portabilidad y optimización.

La recolección de basura por sí sola ha salvado a incontables programadores del mayor origen de errores
de programación en C o C++: la asignación y desasignación explícitas de memoria. Además de mantener
los objetos en memoria, el sistema en tiempo de ejecución de Java realiza un seguimiento de todas las
referencias a esos objetos. Cuando un objeto ya no se está utilizando, Java lo elimina automáticamente
de la memoria. En su mayor parte, puedes simplemente ignorar los objetos que ya no usas, con la
confianza de que el intérprete los limpiará en el momento apropiado.

Java utiliza un recolector de basura sofisticado que se ejecuta en segundo plano, lo que significa que la
mayoría de la recolección de basura tiene lugar durante los tiempos de inactividad, entre pausas de E/S,
clics de ratón o pulsaciones de teclado. Los sistemas en tiempo de ejecución avanzados, como HotSpot,
tienen una recolección de basura más avanzada que puede diferenciar los patrones de uso de objetos
(como los de corta duración versus los de larga duración) y optimizar su recolección. El sistema en tiempo
de ejecución de Java ahora puede ajustarse automáticamente para la distribución óptima de memoria
para diferentes tipos de aplicaciones según su comportamiento. Con este tipo de perfilado en tiempo de
ejecución, la gestión automática de la memoria puede ser mucho más rápida que la administración de
recursos más diligente por parte de los programadores tradicionales, algo que algunos programadores
tradicionales aún encuentran difícil de creer.

Hemos dicho que Java no tiene punteros. Estrictamente hablando, esta afirmación es cierta, pero también
es engañosa. Lo que Java proporciona son referencias, un tipo de puntero más seguro. Una referencia es
un identificador fuertemente tipado para un objeto. Todos los objetos en Java, a excepción de los tipos
numéricos primitivos, se acceden a través de referencias. Puedes usar referencias para construir todos los
tipos normales de estructuras de datos que un programador en C estaría acostumbrado a construir con
punteros, como listas enlazadas, árboles, y demás. La única diferencia es que con referencias, debes
hacerlo de manera segura en cuanto a tipos.

Otra diferencia importante entre una referencia y un puntero es que no puedes manipular referencias
para cambiar sus valores (realizar aritmética de punteros); solo pueden apuntar a métodos específicos,
objetos o elementos de una matriz. Una referencia es una cosa atómica; no puedes manipular el valor de
una referencia excepto asignándola a un objeto. Las referencias se pasan por valor y no puedes referenciar
un objeto a través de más de un nivel de indirección. La protección de las referencias es uno de los
aspectos más fundamentales de la seguridad de Java. Significa que el código Java debe jugar según las
reglas; no puede espiar lugares en los que no debería y eludir las reglas.

Finalmente, debemos mencionar que las matrices en Java son objetos de primera clase. Se pueden asignar
y asignar dinámicamente como otros objetos. Las matrices conocen su propio tamaño y tipo, y aunque no
puedes definir directamente o crear subclases de clases de matrices, tienen una relación de herencia bien
definida basada en la relación de sus tipos base. Tener matrices reales en el lenguaje alivia gran parte de
la necesidad de aritmética de punteros, como la utilizada en C o C++.

Manejo de Errores

Las raíces de Java están en dispositivos en red y sistemas integrados. Para estas aplicaciones, es
importante tener una gestión de errores sólida e inteligente. Java tiene un mecanismo potente de manejo
de excepciones, algo similar a lo que se encuentra en implementaciones más nuevas de C++. Las
excepciones proporcionan una forma más natural y elegante de manejar errores. Las excepciones te
permiten separar el código de manejo de errores del código normal, lo que hace aplicaciones más limpias
y legibles.

Cuando ocurre una excepción, hace que el flujo de ejecución del programa se transfiera a un bloque de
código "catch" predefinido. La excepción lleva consigo un objeto que contiene información sobre la
situación que causó la excepción. El compilador de Java requiere que un método declare las excepciones
que puede generar o que las capture y maneje por sí mismo. Esto eleva la información sobre errores al
mismo nivel de importancia que los argumentos y tipos de retorno para los métodos. Como programador
de Java, sabes precisamente qué condiciones excepcionales debes manejar, y cuentas con la ayuda del
compilador para escribir software correcto que no las deje sin manejar.

Hilos

Las aplicaciones modernas requieren un alto grado de paralelismo. Incluso una aplicación muy enfocada
puede tener una interfaz de usuario compleja que requiere actividades concurrentes. A medida que las
máquinas se vuelven más rápidas, los usuarios son más sensibles a la espera de tareas no relacionadas
que controlan su tiempo. Los hilos proporcionan un multiprocesamiento eficiente y la distribución de
tareas tanto para aplicaciones cliente como servidor. Java hace que los hilos sean fáciles de usar porque
su soporte está integrado en el lenguaje.

La concurrencia es agradable, pero hay más en programar con hilos que simplemente realizar múltiples
tareas simultáneamente. En la mayoría de los casos, los hilos deben sincronizarse (coordinarse), lo que
puede ser complicado sin soporte explícito del lenguaje. Java admite la sincronización basada en el
modelo de monitor y condición, una especie de sistema de bloqueo y llave para acceder a recursos. La
palabra clave synchronized designa métodos y bloques de código para un acceso seguro y serializado
dentro de un objeto. También hay métodos primitivos y simples para la espera y señalización explícitas
entre hilos interesados en el mismo objeto.

Java también tiene un paquete de concurrencia de alto nivel que proporciona utilidades poderosas que
abordan patrones comunes en la programación multihilo, como piscinas de hilos, coordinación de tareas
y bloqueo sofisticado. Con la adición del paquete de concurrencia y utilidades relacionadas, Java ofrece
algunas de las utilidades relacionadas con hilos más avanzadas de cualquier lenguaje.

Aunque algunos desarrolladores nunca tendrán que escribir código multihilo, aprender a programar con
hilos es una parte importante para dominar la programación en Java y algo que todos los desarrolladores
deben comprender. Consulta el Capítulo 9 para obtener una discusión sobre este tema.
Escalabilidad

A nivel más básico, los programas en Java consisten en clases. Las clases están diseñadas para ser
componentes pequeños y modulares. Además de las clases, Java proporciona paquetes, una capa de
estructura que agrupa clases en unidades funcionales. Los paquetes ofrecen una convención de
nomenclatura para organizar clases y un segundo nivel de control organizativo sobre la visibilidad de
variables y métodos en aplicaciones de Java.

Dentro de un paquete, una clase es visible públicamente o está protegida contra el acceso externo. Los
paquetes forman otro tipo de alcance que está más cerca del nivel de aplicación. Esto se presta para
construir componentes reutilizables que funcionan juntos en un sistema. Los paquetes también ayudan
en el diseño de una aplicación escalable que puede crecer sin convertirse en un nido de código
fuertemente acoplado. Los problemas de reutilización y escala realmente se aplican con el sistema de
módulos (nuevamente, añadido en Java 9), pero eso está más allá del alcance de este libro. El tema de los
módulos es el único enfoque de "Java 9 Modularity" de Paul Bakker y Sander Mak (O'Reilly).

Seguridad de Implementación

Una cosa es crear un lenguaje que evite que te hagas daño a ti mismo; es bastante diferente crear uno
que evite que otros te hagan daño.

La encapsulación es el concepto de ocultar datos y comportamientos dentro de una clase; es una parte
importante del diseño orientado a objetos. Te ayuda a escribir software limpio y modular. Sin embargo,
en la mayoría de los lenguajes, la visibilidad de los elementos de datos es simplemente parte de la relación
entre el programador y el compilador. Es una cuestión de semántica, no una afirmación sobre la seguridad
real de los datos en el contexto del entorno de ejecución del programa.

Cuando Bjarne Stroustrup eligió la palabra clave "private" para designar miembros ocultos de clases en
C++, probablemente estaba pensando en proteger a un desarrollador de los detalles complicados del
código de otro desarrollador, no en los problemas de proteger las clases y objetos de ese desarrollador
de ataques de virus o caballos de Troya de otra persona. La conversión arbitraria y la aritmética de
punteros en C o C++ hacen trivial violar los permisos de acceso en clases sin romper las reglas del lenguaje.
Considera el siguiente código:

En este pequeño drama de C++, hemos escrito un código que viola la encapsulación de la clase Finances
y extrae alguna información secreta. Este tipo de artimaña, abusando de un puntero no tipado, no es
posible en Java. Si este ejemplo parece irreal, considera lo importante que es proteger las clases
fundamentales (del sistema) del entorno de ejecución de ataques similares. Si el código no confiable
puede corromper los componentes que proporcionan acceso a recursos reales como el sistema de
archivos, la red o el sistema de ventanas, ciertamente tiene la posibilidad de robar tus números de tarjeta
de crédito.
Si una aplicación Java debe poder descargar dinámicamente código de una fuente no confiable en Internet
y ejecutarlo junto a aplicaciones que podrían contener información confidencial, la protección debe ser
muy profunda. El modelo de seguridad de Java envuelve tres capas de protección alrededor de las clases
importadas, como se muestra en la Figura 1-3.

En el nivel de aplicación, las decisiones de seguridad se toman por un administrador de seguridad en


conjunto con una política de seguridad flexible. Un administrador de seguridad controla el acceso a
recursos del sistema, como el sistema de archivos, los puertos de red y el entorno de ventanas. Un
administrador de seguridad depende de la capacidad de un cargador de clases para proteger clases
básicas del sistema. Un cargador de clases se encarga de cargar clases desde almacenamiento local o la
red. En el nivel más interno, toda la seguridad del sistema descansa en última instancia en el verificador
de bytecode de Java, que garantiza la integridad de las clases entrantes.

El verificador de bytecode de Java es un módulo especial y una parte fija del sistema de tiempo de
ejecución de Java. Sin embargo, los cargadores de clases y los administradores de seguridad (o políticas
de seguridad, para ser más precisos) son componentes que pueden implementarse de manera diferente
por distintas aplicaciones, como servidores o navegadores web. Todas estas piezas deben funcionar
correctamente para garantizar la seguridad en el entorno Java.

El Verificador

La primera línea de defensa de Java es el verificador de bytecode. El verificador lee el bytecode antes de
ejecutarlo y se asegura de que sea adecuado y cumpla con las reglas básicas de la especificación del
bytecode de Java. Un compilador de Java confiable no producirá código que haga lo contrario. Sin
embargo, es posible que una persona malintencionada ensamble deliberadamente un bytecode Java
defectuoso. Es trabajo del verificador detectar esto.

Una vez que el código ha sido verificado, se considera seguro contra ciertos errores inadvertidos o
maliciosos. Por ejemplo, el código verificado no puede falsificar referencias ni violar permisos de acceso
en objetos (como en nuestro ejemplo de tarjeta de crédito). No puede realizar conversiones ilegales o
utilizar objetos de formas no intencionadas. Incluso no puede causar ciertos tipos de errores internos,
como desbordamientos o subdesbordamientos de la pila interna. Estas garantías fundamentales son la
base de toda la seguridad de Java.

Puede que te preguntes, ¿no es este tipo de seguridad implícita en muchos lenguajes interpretados?
Bueno, si bien es cierto que no deberías poder corromper un intérprete de BASIC con una línea falsa de
código BASIC, recuerda que la protección en la mayoría de los lenguajes interpretados ocurre a un nivel
más alto. Es probable que esos lenguajes tengan intérpretes pesados que realizan una gran cantidad de
trabajo en tiempo de ejecución, por lo que son necesariamente más lentos y engorrosos.

En comparación, el bytecode de Java es un conjunto de instrucciones relativamente ligero y de bajo nivel.


La capacidad de verificar estáticamente el bytecode de Java antes de la ejecución permite que el
intérprete de Java funcione a toda velocidad más tarde con total seguridad, sin verificaciones costosas en
tiempo de ejecución. Esta fue una de las innovaciones fundamentales en Java.

El verificador es un tipo de "probador de teoremas" matemático. Recorre el bytecode de Java y aplica


reglas simples e inductivas para determinar ciertos aspectos del comportamiento del bytecode. Este tipo
de análisis es posible porque el bytecode de Java compilado contiene mucha más información de tipo que
el código de objeto de otros lenguajes de este tipo. El bytecode también debe obedecer algunas reglas
adicionales que simplifican su comportamiento. En primer lugar, la mayoría de las instrucciones de
bytecode operan solo en tipos de datos individuales. Por ejemplo, con operaciones de pila, hay
instrucciones separadas para referencias de objetos y para cada uno de los tipos numéricos en Java. De
manera similar, hay una instrucción diferente para mover cada tipo de valor dentro y fuera de una variable
local.

En segundo lugar, el tipo de objeto resultante de cualquier operación siempre se conoce de antemano.
Ninguna operación de bytecode consume valores y produce más de un posible tipo de valor como salida.
Como resultado, siempre es posible mirar la siguiente instrucción y sus operandos y saber el tipo de valor
que resultará.

Debido a que una operación siempre produce un tipo conocido, es posible determinar los tipos de todos
los elementos en la pila y en las variables locales en cualquier momento futuro al observar el estado inicial.
La colección de toda esta información de tipo en un momento dado se llama estado de tipo de la pila;
esto es lo que Java intenta analizar antes de ejecutar una aplicación. Java no sabe nada sobre los valores
reales de los elementos de la pila y las variables en este momento; solo sabe qué tipo de elementos son.
Sin embargo, esta es suficiente información para hacer cumplir las reglas de seguridad y garantizar que
los objetos no sean manipulados de manera ilegal.

Para hacer factible el análisis del estado de tipo de la pila, Java coloca una restricción adicional en cómo
se ejecutan las instrucciones de bytecode de Java: todos los caminos hacia el mismo punto en el código
deben llegar con exactamente el mismo estado de tipo.

Cargadores de Clases

Java agrega una segunda capa de seguridad con un cargador de clases. Un cargador de clases es
responsable de traer el bytecode de las clases de Java al intérprete. Cada aplicación que carga clases desde
la red debe usar un cargador de clases para manejar esta tarea.

Después de que una clase ha sido cargada y ha pasado por el verificador, permanece asociada con su
cargador de clases. Como resultado, las clases están efectivamente divididas en espacios de nombres
separados según su origen. Cuando una clase cargada hace referencia a otro nombre de clase, la ubicación
de la nueva clase es proporcionada por el cargador de clases original. Esto significa que las clases
recuperadas de una fuente específica pueden estar restringidas a interactuar solo con otras clases
recuperadas de esa misma ubicación. Por ejemplo, un navegador web habilitado para Java puede usar un
cargador de clases para construir un espacio separado para todas las clases cargadas desde una URL
determinada. También se puede implementar seguridad sofisticada basada en clases firmadas
criptográficamente mediante cargadores de clases.

La búsqueda de clases siempre comienza con las clases de sistema Java integradas. Estas clases se cargan
desde las ubicaciones especificadas por la ruta de clases del intérprete de Java (consultar el Capítulo 3).
Las clases en la ruta de clases son cargadas por el sistema solo una vez y no pueden ser reemplazadas.
Esto significa que es imposible para una aplicación reemplazar clases de sistema fundamentales con sus
propias versiones que cambien su funcionalidad.

Administradores de Seguridad

Un administrador de seguridad es responsable de tomar decisiones de seguridad a nivel de aplicación. Un


administrador de seguridad es un objeto que puede ser instalado por una aplicación para restringir el
acceso a los recursos del sistema. El administrador de seguridad es consultado cada vez que la aplicación
intenta acceder a elementos como el sistema de archivos, los puertos de red, los procesos externos y el
entorno de ventanas; el administrador de seguridad puede permitir o denegar la solicitud.

Los administradores de seguridad son principalmente de interés para aplicaciones que ejecutan código
no confiable como parte de su funcionamiento normal. Por ejemplo, un navegador web habilitado para
Java puede ejecutar applets que pueden ser recuperados de fuentes no confiables en la red. Un navegador
de este tipo necesita instalar un administrador de seguridad como una de sus primeras acciones. Este
administrador de seguridad luego restringe los tipos de acceso permitidos después de ese punto. Esto
permite que la aplicación imponga un nivel efectivo de confianza antes de ejecutar un código arbitrario.
Y una vez que se instala un administrador de seguridad, no se puede reemplazar.

El administrador de seguridad trabaja en conjunto con un controlador de acceso que te permite


implementar políticas de seguridad a un alto nivel editando un archivo declarativo de política de
seguridad. Las políticas de acceso pueden ser tan simples o complejas como lo requiera una aplicación en
particular. A veces es suficiente simplemente denegar el acceso a todos los recursos o a categorías
generales de servicios, como el sistema de archivos o la red. Pero también es posible tomar decisiones
sofisticadas basadas en información de alto nivel. Por ejemplo, un navegador web habilitado para Java
podría usar una política de acceso que permita a los usuarios especificar cuánto confiar en un applet o
que permita o niegue el acceso a recursos específicos caso por caso. Por supuesto, esto supone que el
navegador puede determinar qué applets debería confiar. Discutiremos cómo se aborda este problema a
través de la firma de código en breve.

La integridad de un administrador de seguridad se basa en la protección proporcionada por los niveles


inferiores del modelo de seguridad de Java. Sin las garantías proporcionadas por el verificador y el
cargador de clases, las afirmaciones de alto nivel sobre la seguridad de los recursos del sistema no tienen
sentido. La seguridad proporcionada por el verificador de bytecode de Java significa que el intérprete no
puede ser corrompido o subvertido y que el código Java debe usar componentes tal como están
destinados a ser usados. Esto, a su vez, significa que un cargador de clases puede garantizar que una
aplicación esté utilizando las clases de sistema básicas de Java y que estas clases son la única forma de
acceder a recursos del sistema básicos. Con estas restricciones en su lugar, es posible centralizar el control
sobre esos recursos a un nivel alto con un administrador de seguridad y una política definida por el
usuario.

Seguridad a Nivel de Aplicación y Usuario

Existe una línea muy fina entre tener suficiente poder para hacer algo útil y tener todo el poder para hacer
lo que se desee. Java proporciona la base para un entorno seguro en el que el código no confiable puede
ser puesto en cuarentena, administrado y ejecutado de manera segura. Sin embargo, a menos que estés
contento con mantener ese código en una pequeña caja negra y ejecutarlo solo para su propio beneficio,
tendrás que otorgarle acceso al menos a algunos recursos del sistema para que sea útil. Cada tipo de
acceso conlleva ciertos riesgos y beneficios. Por ejemplo, en el entorno del navegador web, las ventajas
de otorgar acceso a tu sistema de ventanas a un applet no confiable (desconocido) son que puede mostrar
información y permitirte interactuar de manera útil. Los riesgos asociados son que el applet puede en su
lugar mostrar algo sin valor, molesto u ofensivo.

En un extremo, el simple acto de ejecutar una aplicación le otorga un recurso: tiempo de computación,
que puede utilizar de manera útil o gastar frívolamente. Es difícil prevenir que una aplicación no confiable
desperdicie tu tiempo o incluso intente un ataque de "denegación de servicio". En el otro extremo, una
aplicación poderosa y confiable puede merecer justificadamente acceso a todo tipo de recursos del
sistema (como el sistema de archivos, la creación de procesos, interfaces de red); una aplicación maliciosa
podría causar estragos con estos recursos. El mensaje aquí es que problemas de seguridad importantes y
a veces complejos deben ser abordados.

En algunas situaciones, puede ser aceptable simplemente pedirle al usuario que "apruebe" las solicitudes.
El lenguaje Java proporciona las herramientas para implementar cualquier política de seguridad que
desees. Sin embargo, en última instancia, qué políticas serán depende de tener confianza en la identidad
y la integridad del código en cuestión. Aquí es donde entran en juego las firmas digitales.
Las firmas digitales, junto con los certificados, son técnicas para verificar que los datos realmente
provienen de la fuente que afirma haberlos enviado y no han sido modificados en el camino. Si el Banco
de Boofa firma su aplicación de chequera, puedes verificar que la aplicación realmente proviene del banco
en lugar de un impostor y no ha sido modificada. Por lo tanto, puedes indicar a tu navegador que confíe
en los applets que tienen la firma del Banco de Boofa.

Un Mapa de Ruta de Java

Con todo lo que está sucediendo, es difícil llevar un seguimiento de lo que está disponible ahora, lo que
está prometido y lo que ha existido durante algún tiempo. Las siguientes secciones constituyen un mapa
de ruta que impone cierto orden en el pasado, presente y futuro de Java. No te preocupes si algunos de
los términos te son desconocidos. Cubriremos varios de ellos en los próximos capítulos, y siempre puedes
investigar los otros términos por tu cuenta a medida que adquieras habilidad y comodidad trabajando con
lo básico de Java. En cuanto a las versiones de Java, las notas de lanzamiento de Oracle contienen buenos
resúmenes con enlaces a más detalles. Si estás utilizando versiones más antiguas para trabajar, considera
leer los documentos de Recursos de Tecnología de Oracle.

El Pasado: Java 1.0-Java 11

Java 1.0 proporcionó el marco básico para el desarrollo en Java: el lenguaje en sí mismo y paquetes que
permitían escribir applets y aplicaciones simples. Aunque el 1.0 está oficialmente obsoleto, aún existen
muchos applets que se ajustan a su API.

Java 1.1 reemplazó al 1.0, incorporando mejoras importantes en el paquete Abstract Window Toolkit
(AWT) (la instalación original de GUI de Java), un nuevo patrón de eventos, nuevas funcionalidades del
lenguaje como la reflexión y las clases internas, y muchas otras características críticas. Java 1.1 es la
versión que fue soportada nativamente por la mayoría de las versiones de Netscape y Microsoft Internet
Explorer durante muchos años. Por varias razones políticas, el mundo de los navegadores permaneció
congelado en esta condición durante mucho tiempo.

Java 1.2, llamado "Java 2" por Sun, fue un lanzamiento importante en diciembre de 1998. Proporcionó
muchas mejoras y adiciones, principalmente en términos del conjunto de APIs que se incluyeron en las
distribuciones estándar. Las adiciones más notables fueron la inclusión del paquete de GUI Swing como
una API central y una nueva API de dibujo 2D completa. Swing es la herramienta de interfaz de usuario
avanzada de Java con capacidades que superan ampliamente a las del antiguo AWT (Swing, AWT y algunos
otros paquetes se han llamado variadamente JFC o Java Foundation Classes). Java 1.2 también agregó una
API de Colecciones adecuada a Java.

Java 1.3, lanzado a principios de 2000, agregó características menores pero se enfocó principalmente en
el rendimiento. Con la versión 1.3, Java se volvió significativamente más rápido en muchas plataformas y
Swing recibió muchas correcciones de errores. En este período, APIs empresariales de Java como Servlets
y Enterprise JavaBeans también maduraron.

Java 1.4, lanzado en 2002, integró un nuevo conjunto importante de APIs y muchas características
esperadas durante mucho tiempo. Esto incluyó afirmaciones de lenguaje, expresiones regulares, APIs de
preferencias y registro, un nuevo sistema de E/S para aplicaciones de alto volumen, soporte estándar para
XML, mejoras fundamentales en AWT y Swing, y una API de Java Servlets muy madurada para aplicaciones
web.

Java 5, lanzado en 2004, fue un lanzamiento importante que introdujo muchas mejoras esperadas en la
sintaxis del lenguaje, incluyendo genéricos, enumeraciones seguras en tipo, el bucle for mejorado, listas
de argumentos variables, importaciones estáticas, autoboxing y unboxing de primitivas, así como
metadatos avanzados en clases. Una nueva API de concurrencia proporcionó capacidades avanzadas de
hilos, y se agregaron APIs para imprimir y analizar con formato similar a las de C. La Invocación de Método
Remoto (RMI) también se renovó para eliminar la necesidad de stubs y esqueletos compilados. También
hubo importantes adiciones en las APIs XML estándar.
Java 6, lanzado a finales de 2006, fue un lanzamiento relativamente menor que no agregó nuevas
características sintácticas al lenguaje Java, pero incluyó nuevas APIs de extensión como las de XML y
servicios web.

Java 7, lanzado en 2011, representó una actualización bastante importante. Varias pequeñas mejoras en
el lenguaje, como permitir cadenas en declaraciones switch (más sobre estas cosas más adelante), junto
con adiciones importantes como la nueva biblioteca de E/S java.nio, se empaquetaron en los cinco años
posteriores al lanzamiento de Java 6.

Java 8, lanzado en 2014, completó algunas de las características, como lambdas y métodos
predeterminados, que se habían omitido de Java 7 debido a las continuas demoras en la fecha de
lanzamiento de esa versión. En este lanzamiento también se trabajó en el soporte de fecha y hora, incluida
la capacidad de crear objetos de fecha inmutables, útiles para su uso en las lambdas ahora soportadas.

Java 9, lanzado después de varios retrasos en 2017, introdujo el Sistema de Módulos (Proyecto Jigsaw) y
un "repl" (Read Evaluate Print Loop) para Java: jshell. Utilizaremos jshell para gran parte de nuestras
exploraciones rápidas de muchas de las características de Java a lo largo del resto de este libro. Java 9
también eliminó JavaDB del JDK.

Java 10, lanzado poco después de Java 9 a principios de 2018, actualizó la recolección de basura y trajo
otras características como certificados raíz a las compilaciones de OpenJDK. Se agregó soporte para
colecciones no modificables y se eliminó el soporte para paquetes antiguos de apariencia (como Aqua de
Apple).

Java 11, lanzado a finales de 2018, agregó un cliente HTTP estándar y TLS 1.3. Se eliminaron los módulos
JavaFX y Java EE (JavaFX fue rediseñado para seguir existiendo como una biblioteca independiente).
También se eliminaron los applets de Java. Junto con Java 8, Java 11 es parte del Soporte a Largo Plazo
(LTS) de Oracle. Ciertos lanzamientos, Java 8, Java 11 y presumiblemente Java 17, se mantendrán durante
períodos más largos. Oracle está tratando de cambiar la forma en que los clientes y desarrolladores
interactúan con los nuevos lanzamientos, pero todavía existen buenas razones para mantenerse con
versiones conocidas. Puedes leer más sobre los pensamientos y planes de Oracle para los lanzamientos
LTS y no LTS en la Oracle Technology Network, Oracle Java SE Support Roadmap.

Java 12, lanzado a principios de 2019, agregó algunas mejoras menores en la sintaxis del lenguaje, como
una vista previa de expresiones switch.

Java 13, lanzado en septiembre de 2019, incluye más vistas previas de características del lenguaje, como
bloques de texto, así como una gran reimplementación de la API de Sockets. Según los documentos de
diseño oficiales, este impresionante esfuerzo proporciona "una implementación más simple y moderna
que es fácil de mantener y depurar".

El Presente: Java 14

Este libro incluye todas las últimas y mejores mejoras hasta el lanzamiento de Java 14 a finales de la
primavera de 2020. Esta versión agrega

algunas vistas previas de mejoras en la sintaxis del lenguaje, algunas actualizaciones de recolección de
basura y elimina las herramientas y API Pack200. También saca la vista previa de expresión switch, que se
mostró inicialmente en Java 12, de su estado de vista previa y la incorpora al lenguaje estándar. Con un
ritmo de lanzamiento cada seis meses, es casi seguro que habrá versiones más nuevas del JDK cuando
leas esto. Como se mencionó anteriormente, Oracle quiere que los desarrolladores traten estos
lanzamientos como actualizaciones de características. Para los propósitos de este libro, Java 11 es
suficiente (esta es la última versión de soporte a largo plazo). No necesitarás "mantenerte al día" mientras
lees, pero si estás usando Java para proyectos publicados, considera revisar el mapa de ruta para ver si
tiene sentido mantenerse actualizado. El Capítulo 13 muestra cómo puedes monitorear ese mapa de ruta
por ti mismo y cómo podrías actualizar código existente con nuevas características.
Visión general de características

Aquí hay un resumen breve de las características más importantes de la API central actual de Java:

JDBC (Java Database Connectivity)

Una instalación general para interactuar con bases de datos (introducida en Java 1.1).

RMI (Remote Method Invocation)

Sistema de objetos distribuidos de Java. RMI te permite llamar métodos en objetos alojados por
un servidor que se ejecuta en otro lugar de la red (introducido en Java 1.1).

Seguridad de Java

Una instalación para controlar el acceso a recursos del sistema, combinada con una interfaz
uniforme para la criptografía. La seguridad de Java es la base para las clases firmadas, que se
discutieron anteriormente.

Escritorio de Java

Un conjunto de características que incluyen los componentes de UI de Swing; "apariencia y


comportamiento enchufables", lo que significa la capacidad de la interfaz de usuario para
adaptarse a la apariencia y el comportamiento de la plataforma que estás utilizando; arrastrar y
soltar; gráficos 2D; impresión; visualización, reproducción y manipulación de imágenes y sonido;
y accesibilidad, que significa la capacidad de integrarse con software y hardware especial para
personas con discapacidades.

Internacionalización

La capacidad de escribir programas que se adaptan al idioma y la configuración regional que el


usuario desea usar; el programa muestra automáticamente texto en el idioma apropiado
(introducido en Java 1.1).

JNDI (Java Naming and Directory Interface)

Un servicio general para buscar recursos. JNDI unifica el acceso a servicios de directorio, como
LDAP, NDS de Novell y otros.

Las siguientes son APIs de "extensión estándar". Algunas, como las para trabajar con XML y servicios web,
se incluyen en la edición estándar de Java; otras deben descargarse por separado e implementarse con tu
aplicación o servidor.

JavaMail

Una API uniforme para escribir software de correo electrónico.

Java Media Framework

Otro conjunto que incluye Java 2D, Java 3D, Java Media Framework (un marco para coordinar la
visualización de muchos tipos diferentes de medios), Java Speech (para reconocimiento y síntesis
de voz), Java Sound (audio de alta calidad), Java TV (para televisión interactiva y aplicaciones
similares) y otros.

Java Servlets

Una instalación que te permite escribir aplicaciones web del lado del servidor en Java.

Criptografía de Java

Implementaciones reales de algoritmos criptográficos. (Este paquete se separó de la Seguridad


de Java por razones legales).
XML/XSL

Herramientas para crear y manipular documentos XML, validarlos, asignarlos hacia y desde
objetos Java y transformarlos con hojas de estilo.

En este libro, intentaremos darte una idea de algunas de estas características; desafortunadamente para
nosotros (pero afortunadamente para los desarrolladores de software en Java), el entorno de Java se ha
vuelto tan rico que es imposible cubrir todo en un solo libro.

El Futuro

Ciertamente, no es el novato en la cuadra en estos días, pero Java sigue siendo una de las plataformas
más populares para el desarrollo web y de aplicaciones. Esto es especialmente cierto en áreas como
servicios web, marcos de aplicaciones web y herramientas XML. Aunque Java no ha dominado las
plataformas móviles de la manera en que parecía destinado, el lenguaje Java y las APIs centrales se
pueden utilizar para programar para el sistema operativo móvil Android de Google, que se utiliza en miles
de millones de dispositivos en todo el mundo. En el campamento de Microsoft, el lenguaje derivado de
Java, C#, ha tomado gran parte del desarrollo de .NET y ha llevado la sintaxis y patrones centrales de Java
a esas plataformas.

El JVM en sí mismo también es un área interesante de exploración y crecimiento. Están surgiendo nuevos
lenguajes para aprovechar el conjunto de características y la ubicuidad del JVM. Clojure es un lenguaje
funcional robusto con una base de seguidores en crecimiento que va desde aficionados hasta las grandes
tiendas minoristas. Y Kotlin es otro lenguaje que está tomando el desarrollo de Android (anteriormente
dominio de Java) con entusiasmo. Es un lenguaje de propósito general que está ganando terreno en
nuevos entornos mientras mantiene una buena interoperabilidad con Java.

Probablemente, las áreas más emocionantes de cambio en Java hoy se encuentran en la tendencia hacia
marcos más livianos y simples para los negocios y la integración de la plataforma Java con lenguajes
dinámicos para la creación de scripts en páginas web y extensiones. Hay mucho más trabajo interesante
por venir.

Disponibilidad

Tienes varias opciones para entornos de desarrollo y sistemas de ejecución en Java. El Kit de Desarrollo
de Java de Oracle está disponible para macOS, Windows y Linux. Visita el sitio web de Java de Oracle para
obtener más información sobre cómo obtener el último JDK. El contenido en línea de este libro está
disponible en el sitio web de O’Reilly.

Desde 2017, Oracle ha respaldado oficialmente las actualizaciones del proyecto de código abierto
OpenJDK. Individuos y empresas pequeñas (incluso medianas) pueden encontrar suficiente esta versión
gratuita. Las versiones lanzadas van rezagadas con respecto a las versiones comerciales de JDK y no
incluyen el soporte profesional de Oracle, pero Oracle ha afirmado un compromiso firme de mantener el
acceso gratuito y abierto a Java. Todos los ejemplos en este libro fueron escritos y probados utilizando
OpenJDK. Puedes obtener más detalles directamente de la fuente (¿Oracle?) en las preguntas frecuentes
de OpenJDK.

Para una instalación rápida de una versión gratuita de Java 11 (suficiente para casi todos los ejemplos en
este libro, aunque se mencionan algunas características del lenguaje de versiones posteriores), Amazon
ofrece su distribución Corretto en línea con instaladores amigables y familiares para las tres plataformas
principales.

También existe una amplia gama de Entornos de Desarrollo Integrados populares para Java. Discutiremos
uno en este libro: la Edición Comunitaria gratuita de IntelliJ IDEA de JetBrains. Este entorno de desarrollo
todo en uno te permite escribir, probar y empaquetar software con herramientas avanzadas al alcance de
tu mano.
CAPÍTULO 2

Una Primera Aplicación

Antes de sumergirnos en nuestra discusión completa del lenguaje Java, mojemos nuestros pies saltando
a un código funcional y chapoteando un poco. En este capítulo, construiremos una pequeña y amigable
aplicación que ilustra muchos de los conceptos utilizados a lo largo del libro. Aprovecharemos esta
oportunidad para introducir características generales del lenguaje Java y aplicaciones.

Este capítulo también sirve como una breve introducción a los aspectos orientados a objetos y multihilo
de Java. Si estos conceptos son nuevos para ti, esperamos que encontrarte con ellos aquí por primera vez
en Java sea una experiencia directa y agradable. Si has trabajado con otro entorno de programación
orientado a objetos o multihilo, seguramente apreciarás la simplicidad y elegancia de Java. Este capítulo
tiene la intención de darte una vista panorámica del lenguaje Java y una idea de cómo se utiliza. Si tienes
dificultades con alguno de los conceptos presentados aquí, ten la seguridad de que se abordarán con más
detalle más adelante en el libro.

No podemos enfatizar lo suficiente la importancia de experimentar mientras aprendes nuevos conceptos


aquí y a lo largo del libro. No te limites a leer los ejemplos, ejecútalos. Cuando sea posible, te mostraremos
cómo usar jshell (más detalles en "Probando Java" en la página 70) para probar cosas en tiempo real. El
código fuente de estos ejemplos y todos los ejemplos de este libro se pueden encontrar en GitHub.
Compila los programas y pruébalos. Luego, convierte nuestros ejemplos en tus propios ejemplos: juega
con ellos, cambia su comportamiento, arréglalos y, con suerte, diviértete en el proceso.

Herramientas y Entorno de Java

Aunque es posible escribir, compilar y ejecutar aplicaciones Java con nada más que el Kit de Desarrollo
Java de código abierto de Oracle (OpenJDK) y un editor de texto simple (por ejemplo, vi, Notepad, etc.),
hoy en día la gran mayoría del código Java se escribe con el beneficio de un Entorno de Desarrollo
Integrado (IDE, por sus siglas en inglés). Los beneficios de usar un IDE incluyen una vista completa del
código fuente de Java con resaltado de sintaxis, ayuda de navegación, control de código fuente,
documentación integrada, construcción, refactorización e implementación, todo al alcance de tus manos.
Por lo tanto, vamos a omitir un tratamiento académico en línea de comandos y comenzar con un IDE
popular y gratuito: IntelliJ IDEA CE (Edición Comunitaria). Si prefieres no usar un IDE, siéntete libre de
utilizar los comandos de línea de comandos javac HelloJava.java para la compilación y java HelloJava para
ejecutar los ejemplos próximos.

IntelliJ IDEA requiere que Java esté instalado. Este libro cubre las características del lenguaje Java 11 (con
algunas menciones de cosas nuevas en 12 y 13), por lo que aunque los ejemplos en este capítulo
funcionarán con versiones anteriores, es mejor tener instalado JDK 11 para asegurarse de que todos los
ejemplos en el libro se compilen. El JDK incluye varias herramientas para desarrolladores que discutiremos
en el Capítulo 3. Puedes verificar qué versión, si alguna, tienes instalada escribiendo java -version en la
línea de comandos. Si Java no está presente, o si es una versión anterior a JDK 11, querrás descargar la
última versión desde la página de descargas de OpenJDK de Oracle. Todo lo que se requiere para los
ejemplos en este libro es el JDK básico, que es la primera opción en la esquina superior izquierda de la
página de descargas.

IntelliJ IDEA es un IDE disponible en jetbrains.com. Para los propósitos de este libro y para empezar con
Java en general, la Edición Comunitaria es suficiente. La descarga es un instalador ejecutable o un archivo
comprimido: .exe para Windows, .dmg para macOS y .tar.gz en Linux. Haz doble clic para expandir y
ejecutar el instalador. El Apéndice A contiene más detalles sobre la descarga e instalación de IDEA, así
como información sobre cómo cargar los ejemplos de código para este libro.
Instalación del JDK

Es importante señalar desde el principio que eres libre de descargar y usar el JDK oficial y comercial de
Oracle para uso personal. Las versiones disponibles en la página de descargas de Oracle incluyen la última
versión y la versión más reciente de soporte a largo plazo (13 y 11, respectivamente, en el momento de
escribir esto) con enlaces a versiones anteriores si la compatibilidad heredada es algo con lo que debes
lidiar.

Sin embargo, si planeas usar Java en cualquier capacidad comercial o compartida, el JDK de Oracle ahora
viene con términos de licencia estrictos (y pagos). Por esta y otras razones más filosóficas, principalmente
usamos el OpenJDK mencionado anteriormente en "Creciendo" en la página 3. Lamentablemente, esta
versión de código abierto no incluye instaladores agradables para las diferentes plataformas. Si deseas
una configuración simple y estás satisfecho con una de las versiones de soporte a largo plazo como Java
8 o Java 11, echa un vistazo a otras distribuciones de OpenJDK como Corretto de Amazon.

Para aquellos que desean la última versión y no les importa un poco de configuración, echemos un vistazo
a los pasos típicos requeridos para instalar el OpenJDK en cada una de las plataformas principales.
Independientemente del sistema operativo que uses, si vas a usar el OpenJDK, dirigirás a la página de
descargas de OpenJDK de Oracle.

Instalación de OpenJDK en Linux

El archivo que descargas para sistemas Linux genéricos es un archivo tar comprimido (tar.gz) y se puede
descomprimir en un directorio compartido de tu elección. Utilizando la aplicación de terminal, cambia al
directorio donde descargaste el archivo y ejecuta los siguientes comandos para instalar y verificar Java:

Con Java descomprimido exitosamente, puedes configurar tu terminal para utilizar ese entorno
estableciendo las variables JAVA_HOME y PATH. Probaremos esa configuración verificando la versión del
compilador de Java, javac:

Querrás hacer permanentes esos cambios en JAVA_HOME y PATH actualizando los scripts de inicio o rc
para tu terminal. Por ejemplo, podrías agregar ambas líneas de exportación tal como las usamos en la
terminal a tu archivo .bashrc.
También vale la pena mencionar que muchas distribuciones de Linux ofrecen algunas versiones de Java a
través de sus administradores de paquetes particulares. Puede que desees buscar en línea cosas como
"instalar Java Ubuntu" o "instalar Java Redhat" para ver si existen mecanismos alternativos que se adapten
mejor a cómo gestionas tu sistema Linux en general. A menos que seas un usuario *nix más avanzado y
sepas cómo manipular tus variables de entorno y rutas. En ese caso, ciertamente puedes descomprimir el
archivo en cualquier lugar que prefieras. Sin embargo, es posible que necesites indicar a otras aplicaciones
que utilizan Java dónde lo has almacenado, ya que muchas aplicaciones solo buscarán en directorios "bien
conocidos".

Instalación de OpenJDK en macOS

Para usuarios en sistemas macOS, la instalación de OpenJDK es bastante similar al proceso en Linux:
descargar un archivo binario tar.gz y descomprimirlo en el lugar adecuado. A diferencia de Linux, "el lugar
adecuado" es bastante específico.

Usando la aplicación Terminal (en la carpeta Aplicaciones → Utilidades), puedes descomprimir y reubicar
la carpeta de OpenJDK de la siguiente manera:

El comando sudo permite a los usuarios administrativos realizar acciones especiales normalmente
reservadas para el "super usuario" (el "s" y "u" en sudo). Se te pedirá tu contraseña. Una vez que hayas
movido la carpeta del JDK, establece la variable de entorno JAVA_HOME. El comando java incluido con
macOS es un envoltorio que ahora debería poder localizar tu instalación.

Al igual que con Linux, querrás agregar esa línea JAVA_HOME a un archivo de inicio adecuado (como el
archivo .bash_profile en tu directorio principal) si trabajarás con Java en la línea de comandos.

Para los usuarios en macOS 10.15 (Catalina) y presumiblemente versiones posteriores, es posible que
encuentres un poco más de dificultad al instalar Java y probarlo. Debido a cambios en macOS, Oracle aún
no ha certificado Java para Catalina. Por supuesto, aún puedes ejecutar Java en sistemas Catalina, pero es
posible que aplicaciones más avanzadas encuentren errores. Los usuarios interesados o afectados pueden
leer la nota técnica de Oracle sobre el uso de un JDK con Catalina para obtener más detalles. La primera
parte de la nota técnica cubre la instalación del JDK oficial, mientras que la última parte cubre la
instalación desde un archivo tar.gz como mostramos anteriormente.

Instalación de OpenJDK en Windows

Los sistemas Windows comparten muchos de los mismos conceptos que los sistemas *nix, aunque la
interfaz de usuario para trabajar con esos conceptos es diferente. Ve y descarga el archivo de OpenJDK
para Windows; debería ser un archivo ZIP en lugar de un archivo tar.gz. Descomprime el archivo
descargado y luego muévelo a una carpeta apropiada. Al igual que con Linux, "apropiado" realmente
depende de ti. Creamos una carpeta Java en la carpeta C:\Program Files para almacenar esta (y futuras)
versiones, como se muestra en la Figura 2-1.

Una vez que la carpeta del JDK esté en su lugar, necesitarás establecer algunas variables de entorno, al
igual que con macOS y Linux. El camino más rápido hacia la configuración de variables es buscar "entorno"
y buscar la entrada del Panel de Control titulada "Editar las variables de entorno del sistema", como se
muestra en la Figura 2-2.
Desde aquí puedes crear una nueva entrada para la variable JAVA_HOME y actualizar la entrada Path para
que reconozca Java. Elegimos agregar estos cambios a la sección del Sistema, aunque si eres el único
usuario en tu máquina con Windows, también puedes agregarlos a tu cuenta de usuario.

Para JAVA_HOME, crea una nueva variable y establece su valor como la carpeta donde instalaste este JDK
en particular, como se muestra en la Figura 2-3.

Con JAVA_HOME establecido, ahora puedes agregar una entrada a la variable Path para que Windows
sepa dónde buscar las herramientas java y javac. Debes apuntar este valor a la carpeta bin donde
instalaste Java. Para utilizar tu valor JAVA_HOME en la ruta, enciérralo con signos de porcentaje
(%JAVA_HOME%), como se muestra en la Figura 2-4.
Es posible que no utilices regularmente la línea de comandos en Windows, pero la aplicación del Símbolo
del sistema cumple el mismo propósito que las aplicaciones de terminal en macOS o Linux. Abre el
programa Símbolo del sistema y verifica la versión de Java. Deberías ver algo similar a lo que se muestra
en la Figura 2-5.
Por supuesto, puedes seguir utilizando el Símbolo del sistema, pero ahora también eres libre de apuntar
otras aplicaciones, como IntelliJ IDEA, a tu JDK instalado y simplemente trabajar con esas herramientas.

Configurando IntelliJ IDEA y Creando un Proyecto

La primera vez que ejecutes IDEA, se te pedirá que selecciones un espacio de trabajo. Este es un directorio
raíz o de nivel superior para guardar los nuevos proyectos que crees dentro de IntelliJ IDEA. La ubicación
predeterminada varía según tu plataforma. Si la ubicación predeterminada te parece adecuada, úsala; de
lo contrario, siéntete libre de elegir una ubicación alternativa y haz clic en OK.

Vamos a crear un proyecto para guardar todos nuestros ejemplos. Selecciona Archivo → Nuevo →
Proyecto Java desde el menú de la aplicación y escribe "Learning Java" en el campo "Nombre del proyecto"
en la parte superior del cuadro de diálogo, como se muestra en la Figura 2-6. Asegúrate de que la versión
de JRE esté configurada en la versión 11 o posterior, como se muestra en la figura, y haz clic en Siguiente
en la parte inferior.

Selecciona la plantilla "Aplicación de Línea de Comandos". Esta incluye una clase Java mínima con un
método main() que puede ser ejecutado. Los próximos capítulos profundizarán mucho más en la
estructura de los programas Java y en los comandos y declaraciones que puedes colocar en esos
programas. Con la plantilla seleccionada como se muestra en la Figura 2-7, haz clic en Siguiente.
Por último, debes proporcionar un nombre y una ubicación para tu proyecto. Elegimos el nombre
"HelloJava", pero ese nombre no es especial. IDEA sugerirá una ubicación según el nombre de tu proyecto
y la carpeta predeterminada de proyectos de IDEA, pero puedes utilizar el botón de puntos suspensivos
("...") para elegir una ubicación alternativa en cualquier lugar de tu computadora. Cuando hayas
completado esos dos campos, haz clic en "Finalizar", como se muestra en la Figura 2-8.
¡Felicidades! Ahora tienes un programa Java. Bueno, casi. Necesitas agregar una línea de código para
imprimir algo en la pantalla. Dentro de las llaves después de la línea public static void main(String[] args),
agrega esta línea:

Tu programa completado debería asemejarse al que se muestra en el panel derecho de la Figura 2-9.

A continuación, ejecutaremos este ejemplo y luego lo ampliaremos para darle un poco más de estilo. Los
próximos capítulos presentarán ejemplos más interesantes que unirán más y más elementos de Java.
Siempre construiremos estos ejemplos en una configuración similar, sin embargo, estos primeros pasos
son buenos para adquirir experiencia.

Ejecutar el Proyecto

Partiendo de la plantilla simple proporcionada por IDEA, deberías estar listo para ejecutar tu primer
programa. Observa que la clase Main listada bajo la carpeta src en el esquema del proyecto a la izquierda
tiene un pequeño botón verde de "play" en su icono de clase en la Figura 2-9. Esta adición indica que IDEA
comprende cómo ejecutar el método main() en esta clase. Intenta hacer clic en el botón de triángulo
verde en la barra de herramientas superior. Verás que tu mensaje "¡Hola mundo!" aparece en la pestaña
de Ejecución en la parte inferior del editor. De nuevo, felicidades, has ejecutado tu primer programa Java.

Tomando los Ejemplos de Aprendizaje en Java

Los ejemplos de este libro están disponibles en línea en el sitio de GitHub. GitHub se ha convertido en el
sitio de repositorio en la nube de facto para proyectos de código abierto disponibles para el público, así
como para proyectos empresariales de código cerrado. GitHub tiene muchas herramientas útiles más allá
del simple almacenamiento de código fuente y versionado. Si continúas desarrollando una aplicación o
biblioteca que deseas compartir con otros, vale la pena configurar una cuenta en GitHub y explorarlo más
a fondo. Afortunadamente, también puedes simplemente obtener archivos ZIP de proyectos públicos sin
iniciar sesión, como se muestra en la Figura 2-10.
Al final, deberías obtener un archivo llamado learnjava5e-master.zip (ya que estás descargando un archivo
del "master" branch de este repositorio). Si estás familiarizado/a con GitHub por otros proyectos, siéntete
libre de clonar el repositorio, pero el archivo ZIP estático contiene todo lo que necesitas para probar los
ejemplos mientras lees el resto de este libro. Cuando descomprimas la descarga, encontrarás carpetas
para todos los capítulos que tienen ejemplos, así como una carpeta de juego completada que contiene un
divertido y ligero juego de lanzamiento de manzanas para ayudar a ilustrar la mayoría de los conceptos
de programación presentados a lo largo del libro en una aplicación cohesiva. Profundizaremos más en los
detalles de los ejemplos y el juego en los próximos capítulos.

Como se mencionó anteriormente, puedes compilar y ejecutar los ejemplos desde el archivo ZIP
directamente desde la línea de comandos. También puedes importar el código en tu IDE favorito. El
Apéndice A contiene información detallada sobre la mejor manera de importar estos ejemplos en IntelliJ
IDEA.

HelloJava
En la tradición de textos introductorios de programación, comenzaremos con el equivalente en Java de la
arquetípica aplicación "Hola Mundo", HelloJava.

Terminaremos realizando varias iteraciones de este ejemplo antes de terminar (HelloJava, Hello Java2,
etc.), añadiendo características e introduciendo nuevos conceptos en el camino. Pero empecemos con la
versión minimalista:
Este programa de cinco líneas declara una clase llamada HelloJava y un método llamado main(). Utiliza un
método predefinido llamado println() para escribir texto como salida. Este es un programa de línea de
comandos, lo que significa que se ejecuta en una ventana de terminal o DOS y muestra su salida allí. Si
usaste la plantilla de "Hola Mundo" de IDEA, es posible que notes que eligieron el nombre Main para su
clase. No hay nada incorrecto en eso, pero nombres más descriptivos serán útiles a medida que comiences
a construir programas más complejos. Intentaremos usar nombres claros en nuestros ejemplos a partir
de ahora. Independientemente del nombre de la clase, este enfoque es un tanto anticuado para nuestro
gusto, así que antes de seguir adelante, vamos a dotar a HelloJava de una interfaz gráfica de usuario (GUI).
No te preocupes por el código aún; simplemente sigue el proceso aquí, y volveremos para explicaciones
en un momento.

En lugar de la línea que contiene el método println(), vamos a usar un objeto JFrame para mostrar una
ventana en la pantalla. Podemos empezar reemplazando la línea println con las siguientes tres líneas:

Este fragmento crea un objeto JFrame con el título "¡Hola, Java!". El JFrame es una ventana gráfica. Para
mostrarla, simplemente configuramos su tamaño en la pantalla usando el método setSize() y la hacemos
visible llamando al método setVisible().

Si nos detuviéramos aquí, veríamos una ventana vacía en la pantalla con nuestro letrero "¡Hola, Java!"
como título. Nos gustaría tener nuestro mensaje dentro de la ventana, no solo escrito en la parte superior
de la misma. Para colocar algo en la ventana, necesitamos un par de líneas más. El siguiente ejemplo
completo agrega un objeto JLabel para mostrar el texto centrado en nuestra ventana. La línea adicional
de importación en la parte superior es necesaria para decirle a Java dónde encontrar las clases JFrame y
JLabel (las definiciones de los objetos JFrame y JLabel que estamos utilizando).

Para compilar y ejecutar este código fuente, selecciona la clase ch02/HelloJava.java desde el explorador
de paquetes a lo largo de la izquierda y haz clic en el botón "Run" (Ejecutar) en la barra de herramientas
en la parte superior. El botón de ejecución es una flecha verde apuntando hacia la derecha. Consulta la
Figura 2-11.
Deberías ver la proclamación que se muestra en la Figura 2-12. ¡Felicidades una vez más, ahora has
ejecutado tu segunda aplicación Java! Tómate un momento para disfrutar del brillo en tu monitor.
Ten en cuenta que al hacer clic en el botón de cierre de la ventana, la ventana desaparece, pero tu
programa sigue en ejecución. (Corregiremos este comportamiento de cierre en una versión posterior del
ejemplo). Para detener la aplicación Java en IntelliJ IDEA, haz clic en el botón cuadrado rojo que está a la
derecha del botón verde de reproducción que utilizamos para ejecutar el programa. Si estás ejecutando
el ejemplo en la línea de comandos, escribe Ctrl-C. Ten en cuenta que nada te impide ejecutar más de una
instancia (copia) de la aplicación al mismo tiempo.

HelloJava puede ser un programa pequeño, pero hay bastante ocurriendo detrás de escena. Esas pocas
líneas representan la punta de un iceberg. Lo que yace bajo la superficie son las capas de funcionalidad
proporcionadas por el lenguaje Java y sus bibliotecas Swing.

Recuerda que en este capítulo, vamos a cubrir mucho terreno rápidamente en un esfuerzo por mostrarte
el panorama general. Intentaremos ofrecer suficiente detalle para una buena comprensión de lo que está
sucediendo en cada ejemplo, pero dejaremos explicaciones detalladas para los capítulos
correspondientes. Esto se aplica tanto a los elementos del lenguaje Java como a los conceptos orientados
a objetos que les aplican. Dicho esto, echemos un vistazo ahora a lo que está sucediendo en nuestro
primer ejemplo.

Clases

El primer ejemplo define una clase llamada HelloJava:

Las clases son los bloques fundamentales de la mayoría de los lenguajes orientados a objetos. Una clase
es un grupo de elementos de datos con funciones asociadas que pueden realizar operaciones sobre esos
datos. Los elementos de datos en una clase se llaman variables, o a veces campos; en Java, las funciones
se llaman métodos. Los principales beneficios de un lenguaje orientado a objetos son esta asociación
entre datos y funcionalidad en unidades de clase y también la capacidad de las clases para encapsular u
ocultar detalles, liberando al desarrollador de preocuparse por detalles de bajo nivel.

En una aplicación, una clase podría representar algo concreto, como un botón en una pantalla o la
información en una hoja de cálculo, o podría ser algo más abstracto, como un algoritmo de clasificación o
tal vez el sentido de apatía en un personaje de un videojuego. Una clase que represente una hoja de
cálculo podría, por ejemplo, tener variables que representen los valores de sus celdas individuales y
métodos que realicen operaciones en esas celdas, como "borrar una fila" o "calcular valores".

Nuestra clase HelloJava es toda una aplicación Java en una sola clase. Define solo un método, main(), que
contiene el cuerpo de nuestro programa:

Es este método main() el que se llama primero cuando se inicia la aplicación. La parte etiquetada como
String[] args nos permite pasar argumentos de línea de comandos a la aplicación. Repasaremos el método
main() en la siguiente sección. Por último, señalaremos que aunque esta versión de HelloJava no define
ninguna variable como parte de su clase, utiliza dos variables, frame y label, dentro de su método main().
Pronto hablaremos más sobre variables también.

El Método main()

Como vimos al ejecutar nuestro ejemplo, ejecutar una aplicación Java implica seleccionar una clase
particular y pasar su nombre como argumento a la máquina virtual de Java. Cuando hicimos esto, el
comando java buscó en nuestra clase HelloJava para ver si contenía el método especial llamado main()
con la forma correcta. Lo hizo, y por eso se ejecutó. Si no hubiera estado allí, habríamos recibido un
mensaje de error. El método main() es el punto de entrada para las aplicaciones. Cada aplicación Java
independiente incluye al menos una clase con un método main() que realiza las acciones necesarias para
iniciar el resto del programa.

Nuestro método main() configura una ventana (un JFrame) para mostrar la salida visual de la clase
HelloJava. En este momento, está realizando todo el trabajo en la aplicación. Pero en una aplicación
orientada a objetos, normalmente delegamos responsabilidades a muchas clases diferentes. En la
próxima versión de nuestro ejemplo, vamos a realizar esa división, creando una segunda clase, y veremos
que a medida que el ejemplo evoluciona posteriormente, el método main() sigue más o menos igual,
simplemente manteniendo el procedimiento de inicio.

Repasemos rápidamente nuestro método main(), solo para saber qué hace. Primero, main() crea un
JFrame, la ventana que contendrá nuestro ejemplo:

La palabra 'new' en esta línea de código es muy importante. JFrame es el nombre de una clase que
representa una ventana en la pantalla, pero la clase en sí misma es solo una plantilla, como un plano de
construcción. La palabra clave 'new' le indica a Java que asigne memoria y realmente cree un objeto
JFrame específico. En este caso, el argumento dentro de los paréntesis le indica al JFrame qué mostrar en
su barra de título. Podríamos haber omitido el texto "¡Hola, Java!" y utilizado paréntesis vacíos para crear
un JFrame sin título, pero solo porque el JFrame específicamente nos permite hacerlo.

Cuando se crean las ventanas JFrame por primera vez, son muy pequeñas. Antes de mostrar el JFrame,
establecemos su tamaño a algo razonable:

Este es un ejemplo de invocar un método en un objeto específico. En este caso, el método setSize() está
definido por la clase JFrame y afecta al objeto JFrame particular que hemos colocado en la variable frame.
Al igual que con el frame, también creamos una instancia de JLabel para contener nuestro texto dentro
de la ventana:

JLabel es bastante parecido a una etiqueta física. Contiene un texto en una posición particular, en este
caso, en nuestro marco (frame). Este es un concepto muy orientado a objetos: usar un objeto para
contener algún texto en lugar de simplemente invocar un método para "dibujar" el texto y continuar. La
justificación para esto se volverá más clara más adelante.

A continuación, debemos colocar la etiqueta (label) dentro del marco (frame) que creamos:

Aquí, estamos llamando a un método llamado add() para colocar nuestra etiqueta (label) dentro del
JFrame. El JFrame es una especie de contenedor que puede contener cosas. Hablaremos más sobre eso
más adelante.

La tarea final del método main() es mostrar la ventana del marco (frame) y su contenido, que de lo
contrario sería invisible. Una ventana invisible hace que la aplicación sea bastante aburrida.

Ese es todo el método main(). A medida que avanzamos a través de los ejemplos en este capítulo, este
método permanecerá en su mayoría sin cambios a medida que la clase HelloJava evoluciona a su
alrededor.
Clases y Objetos

Una clase es un plano para una parte de una aplicación; contiene métodos y variables que conforman ese
componente. Mientras una aplicación está activa, pueden existir muchas copias de trabajo individuales
de una clase dada. Estas encarnaciones individuales se llaman instancias de la clase, u objetos. Dos
instancias de una clase dada pueden contener datos diferentes, pero siempre tienen los mismos métodos.

Como ejemplo, considera una clase Button. Solo hay una clase Button, pero una aplicación puede crear
muchos objetos Button diferentes, cada uno una instancia de la misma clase. Además, dos instancias de
Button podrían contener datos diferentes, tal vez dando a cada una una apariencia diferente y realizando
una acción diferente. En este sentido, una clase puede considerarse un molde para hacer el objeto que
representa, algo así como un cortador de galletas que estampa instancias de sí mismo en la memoria de
la computadora. Como verás más adelante, hay un poco más que eso: una clase puede, de hecho,
compartir información entre sus instancias, pero esta explicación es suficiente por ahora. El Capítulo 5
tiene toda la historia sobre clases y objetos.

El término "objeto" es muy general y en algunos otros contextos se usa casi de manera intercambiable
con "clase". Los objetos son las entidades abstractas a las que todos los lenguajes orientados a objetos se
refieren de una forma u otra. Utilizaremos "objeto" como un término genérico para una instancia de una
clase. Por lo tanto, podríamos referirnos a una instancia de la clase Button como un botón, un objeto
Button o, indiscriminadamente, como un objeto.

El método main() en el ejemplo anterior crea una única instancia de la clase JLabel y la muestra en una
instancia de la clase JFrame. Podrías modificar main() para crear muchas instancias de JLabel, tal vez cada
una en una ventana separada.

Variables y Tipos de Clases

En Java, cada clase define un nuevo tipo (tipo de dato). Una variable puede declararse para ser de este
tipo y luego contener instancias de esa clase. Por ejemplo, una variable podría ser del tipo Button y
contener una instancia de la clase Button, o del tipo SpreadSheetCell y contener un objeto
SpreadSheetCell, al igual que podría ser cualquiera de los tipos más simples, como int o float, que
representan números. El hecho de que las variables tengan tipos y no puedan simplemente contener
cualquier tipo de objeto es otra característica importante del lenguaje que garantiza la seguridad y
corrección del código.

Ignorando las variables utilizadas dentro del método main() por el momento, solo se declara otra variable
en nuestro simple ejemplo de HelloJava. Se encuentra en la declaración del propio método main():

Al igual que las funciones en otros lenguajes, un método en Java declara una lista de parámetros
(variables) que acepta como argumentos, y especifica los tipos de esos parámetros. En este caso, el
método main está requiriendo que cuando se invoque, se le pase una matriz de objetos String en la
variable llamada args. El String es el objeto fundamental que representa texto en Java. Como insinuamos
anteriormente, Java utiliza el parámetro args para pasar cualquier argumento de línea de comandos
suministrado a la máquina virtual de Java (VM) a tu aplicación. (No los utilizamos aquí).

Hasta este punto, nos hemos referido vagamente a las variables como contenedoras de objetos. En
realidad, las variables que tienen tipos de clase no tanto contienen objetos como apuntan a ellos. Las
variables de tipo de clase son referencias a objetos. Una referencia es un puntero o un identificador para
un objeto. Si declaras una variable de tipo de clase sin asignarle un objeto, no apunta a nada. Se le asigna
el valor predeterminado de null, lo que significa "ningún valor". Si intentas usar una variable con un valor
null como si estuviera apuntando a un objeto real, se produce un error en tiempo de ejecución,
NullPointerException.
Por supuesto, las referencias a objetos tienen que provenir de alguna parte. En nuestro ejemplo, creamos
dos objetos usando el operador new. Examinaremos la creación de objetos con más detalle un poco más
adelante en el capítulo.

HelloComponent

Hasta ahora, nuestro ejemplo HelloJava se ha contenido en una sola clase. De hecho, debido a su
naturaleza simple, realmente ha funcionado como un solo método grande. Aunque hemos utilizado un
par de objetos para mostrar nuestro mensaje de GUI, nuestro propio código no ilustra ninguna estructura
orientada a objetos. Bueno, vamos a corregir eso ahora mismo agregando una segunda clase. Para darnos
algo en lo que trabajar a lo largo de este capítulo, vamos a asumir el trabajo de la clase JLabel (¡adiós,
JLabel!) y reemplazarlo con nuestra propia clase gráfica: HelloComponent. Nuestra clase HelloComponent
comenzará de manera simple, simplemente mostrando nuestro mensaje "¡Hola, Java!" en una posición
fija. Añadiremos capacidades más adelante.

El código para nuestra nueva clase es muy simple; solo agregamos unas cuantas líneas más:

Puedes añadir este texto al archivo HelloJava.java, o puedes colocarlo en su propio archivo llamado
HelloComponent.java. Si lo colocas en el mismo archivo, debes mover la nueva declaración de importación
al principio del archivo, junto con la otra. Para usar nuestra nueva clase en lugar de JLabel, simplemente
reemplaza las dos líneas que hacen referencia a la etiqueta con:

Esta vez, al compilar HelloJava.java, verás dos archivos binarios de clase: HelloJava.class y
HelloComponent.class (independientemente de cómo hayas organizado el código fuente). Al ejecutar el
código, debería verse muy similar a la versión con JLabel, pero si cambias el tamaño de la ventana, notarás
que nuestra clase no se ajusta automáticamente para centrar el código.

Entonces, ¿qué hemos hecho y por qué nos hemos esforzado en insultar al componente JLabel que
funciona perfectamente bien? Hemos creado nuestra nueva clase HelloComponent, que extiende una
clase gráfica genérica llamada JComponent. Extender una clase simplemente significa agregar
funcionalidad a una clase existente, creando una nueva. Entraremos en eso en la próxima sección. Aquí
hemos creado un nuevo tipo de JComponent que contiene un método llamado paintComponent(), que es
responsable de dibujar nuestro mensaje. Nuestro método paintComponent() toma un argumento llamado
(de manera algo concisa) g, que es del tipo Graphics. Cuando se invoca el método paintComponent(), se
asigna un objeto Graphics a g, que usamos en el cuerpo del método. Hablaremos más sobre
paintComponent() y la clase Graphics en un momento. Respecto al porqué, lo comprenderás cuando
añadamos toda clase de nuevas características a nuestro nuevo componente más adelante.

Herencia

Las clases Java se organizan en una jerarquía de padres e hijos en la que el padre y el hijo se conocen
como superclase y subclase, respectivamente. Exploraremos estos conceptos más a fondo en el Capítulo
5. En Java, cada clase tiene exactamente una superclase (un solo padre), pero posiblemente muchas
subclases. La única excepción a esta regla es la clase Object, que se sitúa en la cima de toda la jerarquía
de clases; no tiene superclase.
La declaración de nuestra clase en el ejemplo anterior utiliza la palabra clave extends para especificar que
HelloComponent es una subclase de la clase JComponent:

Una subclase puede heredar algunas o todas las variables y métodos de su superclase. A través de la
herencia, la subclase puede usar esas variables y métodos como si los hubiera declarado ella misma. Una
subclase puede agregar variables y métodos propios, y también puede anular o cambiar el significado de
los métodos heredados. Cuando usamos una subclase, los métodos anulados son ocultados
(reemplazados) por las propias versiones de la subclase. De esta manera, la herencia proporciona un
mecanismo poderoso mediante el cual una subclase puede refinar o extender la funcionalidad de su
superclase.

Por ejemplo, la clase hipotética de hoja de cálculo podría ser subclaseada para producir una nueva clase
de hoja de cálculo científica con funciones matemáticas adicionales y constantes integradas especiales.
En este caso, el código fuente de la hoja de cálculo científica podría declarar métodos para las funciones
matemáticas agregadas y variables para las constantes especiales, pero la nueva clase automáticamente
tendría todas las variables y métodos que constituyen la funcionalidad normal de una hoja de cálculo;
estos se heredan de la clase principal de hoja de cálculo. Esto también significa que la hoja de cálculo
científica mantiene su identidad como una hoja de cálculo, y podemos usar la versión extendida en
cualquier lugar donde se pueda usar la hoja de cálculo más simple. Esa última oración tiene implicaciones
profundas, que exploraremos a lo largo del libro. Significa que los objetos especializados pueden ser
usados en lugar de objetos más genéricos, personalizando su comportamiento sin cambiar la aplicación
subyacente. Esto se llama polimorfismo y es uno de los fundamentos de la programación orientada a
objetos.

Nuestra clase HelloComponent es una subclase de la clase JComponent y hereda muchas variables y
métodos que no se declaran explícitamente en nuestro código fuente. Esto es lo que permite que nuestra
pequeña clase sirva como un componente en un JFrame, con solo algunas personalizaciones.

La Clase JComponent

La clase JComponent proporciona el marco para construir todo tipo de componentes de interfaz de
usuario (UI). Componentes particulares, como botones, etiquetas y listas, se implementan como subclases
de JComponent.

Sobrescribimos métodos en dicha subclase para implementar el comportamiento de nuestro componente


particular. Esto puede sonar restrictivo, como si estuviéramos limitados a un conjunto predefinido de
rutinas, pero no es el caso en absoluto. Ten en cuenta que los métodos de los que estamos hablando son
formas de interactuar con el sistema de ventanas. No tenemos que meter toda nuestra aplicación allí.
Una aplicación realista podría involucrar cientos o miles de clases, con legiones de métodos y variables, y
muchos hilos de ejecución. La gran mayoría de estos están relacionados con los detalles de nuestro trabajo
(estos se llaman objetos de dominio). La clase JComponent y otras clases predefinidas sirven solo como
un marco sobre el cual basar el código que maneja ciertos tipos de eventos de la interfaz de usuario y
muestra información al usuario.

El método paintComponent() es un método importante de la clase JComponent; lo sobrescribimos para


implementar la forma en que nuestro componente particular se muestra en la pantalla. El
comportamiento predeterminado de paintComponent() no dibuja nada en absoluto. Si no lo hubiéramos
sobrescrito en nuestra subclase, nuestro componente simplemente habría sido invisible. Aquí, estamos
sobrescribiendo paintComponent() para hacer algo solo ligeramente más interesante. No anulamos
ninguno de los otros miembros heredados de JComponent porque proporcionan funcionalidad básica y
valores predeterminados razonables para este ejemplo (trivial). A medida que HelloJava crezca,
profundizaremos en los miembros heredados y utilizaremos métodos adicionales. También agregaremos
algunos métodos y variables específicos de la aplicación específicamente para las necesidades de
HelloComponent.
JComponent es realmente la punta de otro iceberg llamado Swing. Swing es la herramienta de interfaz de
usuario (UI) de Java, representada en nuestro ejemplo por la declaración de importación en la parte
superior; lo discutiremos con cierto detalle en el Capítulo 10.

Relaciones y Apuntadores

Podemos referirnos correctamente a HelloComponent como un JComponent porque la subclase se puede


considerar como la creación de una relación de "es un", en la que la subclase "es un" tipo de su superclase.
Por lo tanto, HelloComponent es una especie de JComponent. Cuando nos referimos a un tipo de objeto,
nos referimos a cualquier instancia de la clase de ese objeto o de cualquiera de sus subclases.

Más adelante, observaremos más detenidamente la jerarquía de clases de Java y veremos que
JComponent en sí es una subclase de la clase Container, que se deriva aún más de una clase llamada
Component, y así sucesivamente, como se muestra en la Figura 2-13.

En este sentido, un objeto HelloComponent es una especie de JComponent, que es una especie de
Container, y cada uno de estos puede considerarse en última instancia como una especie de Component.
Es de estas clases de las que HelloComponent hereda su funcionalidad GUI básica y (como discutiremos
más adelante) la capacidad de tener otros componentes gráficos incrustados en ella también.

Component es una subclase de la clase Object de nivel superior, por lo que todas estas clases son tipos de
Object. Todas las demás clases en la API de Java heredan comportamiento de Object, que define algunos
métodos básicos, como verás en el Capítulo 5. Continuaremos utilizando la palabra "objeto" (minúscula)
de manera genérica para referirnos a una instancia de cualquier clase; usaremos Object para referirnos
específicamente al tipo de esa clase.

Paquetes e Importaciones

Mencionamos anteriormente que la primera línea de nuestro ejemplo le indica a Java dónde encontrar
algunas de las clases que hemos estado utilizando:
Específicamente, esta línea le indica al compilador que vamos a estar utilizando clases del kit de
herramientas de interfaz gráfica de usuario (GUI) Swing (en este caso, JFrame, JLabel y JComponent). Estas
clases están organizadas en un paquete de Java llamado javax.swing. Un paquete de Java es un grupo de
clases relacionadas por propósito o aplicación. Las clases en el mismo paquete tienen privilegios de acceso
especiales entre sí y pueden estar diseñadas para trabajar estrechamente juntas.

Los paquetes se nombran de manera jerárquica con componentes separados por puntos, como java.util
y java.util.zip. Las clases en un paquete deben seguir convenciones sobre dónde se encuentran en la ruta
de clases. También adoptan el nombre del paquete como parte de su "nombre completo" o, utilizando la
terminología adecuada, su nombre completamente calificado.

Por ejemplo, el nombre completamente calificado de la clase JComponent es javax.swing.JComponent.


Podríamos haberlo referenciado directamente con ese nombre, en lugar de usar la declaración de
importación:

La declaración import javax.swing.* nos permite referirnos a todas las clases en el paquete javax.swing
con sus nombres simples. Así que no tenemos que usar nombres completamente calificados para
referirnos a las clases JComponent, JLabel y JFrame.

Como vimos cuando agregamos nuestra segunda clase de ejemplo, puede haber una o más declaraciones
de importación en un archivo fuente de Java dado. Las importaciones crean efectivamente una "ruta de
búsqueda" que indica a Java dónde buscar las clases a las que nos referimos por sus nombres simples y
no calificados. (No es realmente una ruta, pero evita nombres ambiguos que pueden causar errores). Las
importaciones que hemos visto usan la notación de punto y asterisco (.*) para indicar que se debe
importar todo el paquete. Pero también puedes especificar solo una clase. Por ejemplo, nuestro ejemplo
actual solo usa la clase Graphics del paquete java.awt. Entonces podríamos haber usado import
java.awt.Graphics en lugar de usar el comodín * para importar todas las clases del paquete Abstract
Window Toolkit (AWT). Sin embargo, anticipamos usar varias clases más de este paquete más adelante.

Las jerarquías de paquetes java. y javax. son especiales. Cualquier paquete que comience con java. es
parte de la API principal de Java y está disponible en cualquier plataforma que admita Java. El paquete
javax. normalmente denota una extensión estándar a la plataforma principal, que puede estar instalada
o no. Sin embargo, en años recientes, muchas extensiones estándar se han agregado a la API principal de
Java sin cambiarles el nombre. El paquete javax.swing es un ejemplo; es parte de la API principal a pesar
de su nombre.

La Figura 2-14 ilustra algunos de los paquetes principales de Java, mostrando una clase representativa o
dos de cada uno.
java.lang contiene clases fundamentales necesarias para el propio lenguaje Java; este paquete se importa
automáticamente, y es por eso que no necesitamos una declaración de importación para usar nombres
de clase como String o System en nuestros ejemplos. El paquete java.awt contiene clases de la antigua
interfaz gráfica AWT (Abstract Window Toolkit); java.net contiene las clases de networking; y así
sucesivamente.

A medida que adquieras más experiencia con Java, te darás cuenta de que tener un dominio de los
paquetes disponibles, qué hacen, cuándo usarlos y cómo usarlos es una parte crítica para convertirse en
un desarrollador de Java exitoso.

El Método paintComponent()

El código fuente de nuestra clase HelloComponent define un método, paintComponent(), que anula el
método paintComponent() de la clase JComponent:

El método paintComponent() se llama cuando es el momento de que nuestro ejemplo se dibuje en la


pantalla. Toma un único argumento, un objeto Graphics, y no devuelve ningún tipo de valor (void) a quien
lo llama.

Los modificadores son palabras clave colocadas antes de clases, variables y métodos para alterar su
accesibilidad, comportamiento o semántica. paintComponent() está declarado como público (public), lo
que significa que puede ser invocado (llamado) por métodos en clases distintas de HelloComponent. En
este caso, es el entorno de ventanas de Java el que llama a nuestro método paintComponent(). Un método
o variable declarado como privado (private) es accesible solo desde su propia clase.

El objeto Graphics, una instancia de la clase Graphics, representa un área gráfica específica para dibujar.
(También se le llama contexto gráfico). Contiene métodos que se pueden utilizar para dibujar en esta área
y variables que representan características como recortes o modos de dibujo. El objeto Graphics particular
que se nos pasa en el método paintComponent() corresponde al área de la pantalla de nuestro
HelloComponent, dentro de nuestro marco.

La clase Graphics proporciona métodos para renderizar formas, imágenes y texto. En HelloComponent,
invocamos el método drawString() de nuestro objeto Graphics para escribir nuestro mensaje en las
coordenadas especificadas.

Como hemos visto anteriormente, accedemos a un método de un objeto agregando un punto (.) y su
nombre al objeto que lo contiene. Invocamos el método drawString() del objeto Graphics (referenciado
por nuestra variable g) de esta manera:

Lo más probable es que te resulte difícil acostumbrarte a la idea de que nuestra aplicación sea dibujada
por un método que es llamado por un agente externo en momentos arbitrarios. ¿Cómo podemos hacer
algo útil con esto? ¿Cómo controlamos lo que se hace y cuándo? Estas respuestas están por venir. Por
ahora, solo piensa en cómo podrías comenzar a estructurar aplicaciones que respondan bajo comando en
lugar de por su propia iniciativa.

HelloJava2: La Secuela

Ahora que hemos cubierto algunos conceptos básicos, hagamos que nuestra aplicación sea un poco más
interactiva. La siguiente actualización menor nos permite arrastrar el texto del mensaje alrededor con el
mouse. Sin embargo, si eres nuevo en la programación, la actualización puede no parecer tan menor. ¡No
temas! Analizaremos detenidamente todos los temas cubiertos en este ejemplo en capítulos posteriores.
Por ahora, disfruta jugando con el ejemplo y utilízalo como una oportunidad para sentirte más cómodo
creando y ejecutando programas en Java, incluso si no te sientes tan cómodo con el código en sí.

Llamaremos a este ejemplo HelloJava2 en lugar de causar confusión al seguir expandiendo el antiguo,
pero los cambios principales aquí y en adelante radican en agregar capacidades a la clase HelloComponent
y simplemente realizar los cambios correspondientes en los nombres para mantenerlos claros (por
ejemplo, HelloComponent2, HelloComponent3, y así sucesivamente). Habiendo visto recién la herencia
en acción, podrías preguntarte por qué no creamos una subclase de HelloComponent y explotamos la
herencia para construir sobre nuestro ejemplo anterior y extender su funcionalidad. Bueno, en este caso,
eso no proporcionaría mucha ventaja, y para claridad simplemente empezamos desde cero.

Aquí está HelloJava2:


Líneas con dos barras consecutivas indican que el resto de la línea es un comentario. Hemos agregado
algunos comentarios a HelloJava2 para ayudarte a mantener un seguimiento de todo.

Coloca el texto de este ejemplo en un archivo llamado HelloJava2.java y compílalo como antes. Deberías
obtener nuevos archivos de clase, HelloJava2.class y HelloComponent2.class, como resultado. Ejecuta el
ejemplo usando el siguiente comando:

O, si estás siguiendo en IDEA, haz clic en el botón de Ejecutar. Siéntete libre de sustituir tu propio
comentario picante por el mensaje "¡Hola, Java!" y disfruta de muchas horas de diversión moviendo el
texto con tu ratón. Observa que ahora, al hacer clic en el botón de cierre de la ventana, la aplicación se
cierra; explicaremos eso más adelante cuando hablemos sobre eventos. Ahora veamos qué ha cambiado.

Variables de Instancia

Hemos añadido algunas variables a la clase HelloComponent2 en nuestro ejemplo:

messageX y messageY son enteros que contienen las coordenadas actuales de nuestro mensaje móvil. Los
hemos inicializado de manera rudimentaria con valores predeterminados que deberían ubicar el mensaje
en algún lugar cerca del centro de la ventana. Los enteros en Java son números con signo de 32 bits, por
lo que pueden contener fácilmente nuestros valores de coordenadas. La variable theMessage es de tipo
String y puede contener instancias de la clase String.

Debes tener en cuenta que estas tres variables están declaradas dentro de las llaves de la definición de la
clase, pero no dentro de ningún método específico en esa clase. Estas variables se llaman variables de
instancia y pertenecen al objeto en su conjunto. Específicamente, copias de ellas aparecen en cada
instancia separada de la clase. Las variables de instancia siempre son visibles (y utilizables por) todos los
métodos dentro de su clase. Dependiendo de sus modificadores, también pueden ser accesibles desde
fuera de la clase.

A menos que se inicialicen de otra manera, las variables de instancia se establecen en un valor
predeterminado de 0, false o null, según su tipo. Los tipos numéricos se establecen en 0, las variables
booleanas se establecen en false y las variables de tipo clase siempre tienen su valor establecido en null,
lo que significa "sin valor". Intentar usar un objeto con un valor null resulta en un error en tiempo de
ejecución.

Las variables de instancia difieren de los argumentos de método y otras variables que se declaran dentro
del ámbito de un método en particular. Estas últimas se llaman variables locales. Son efectivamente
variables privadas que solo pueden ser vistas por el código dentro de un método u otro bloque de código.
Java no inicializa las variables locales, por lo que debes asignar valores tú mismo. Si intentas usar una
variable local a la que aún no se le ha asignado un valor, tu código generará un error en tiempo de
compilación. Las variables locales solo existen mientras el método se está ejecutando y luego
desaparecen, a menos que algo más guarde su valor. Cada vez que se invoca el método, se recrean sus
variables locales y se les deben asignar valores.

Hemos utilizado las nuevas variables para hacer que nuestro método paintComponent() anteriormente
pesado sea más dinámico. Ahora, todos los argumentos en la llamada a drawString() son determinados
por estas variables.
Constructores

La clase HelloComponent2 incluye un tipo especial de método llamado constructor. Un constructor es


llamado para configurar una nueva instancia de una clase. Cuando se crea un nuevo objeto, Java asigna
almacenamiento para él, establece las variables de instancia en sus valores predeterminados y llama al
método constructor de la clase para hacer cualquier configuración requerida a nivel de aplicación.

Un constructor siempre tiene el mismo nombre que su clase. Por ejemplo, el constructor para la clase
HelloComponent2 se llama HelloComponent2(). Los constructores no tienen un tipo de retorno, pero
puedes pensar en ellos como creando un objeto de tipo de su clase. Al igual que otros métodos, los
constructores pueden recibir argumentos. Su única misión en la vida es configurar e inicializar las
instancias de clase recién nacidas, posiblemente utilizando información pasada a ellos en estos
parámetros.

Un objeto se crea con el operador new especificando el constructor para la clase y los argumentos
necesarios. La instancia de objeto resultante se devuelve como un valor. En nuestro ejemplo, se crea una
nueva instancia HelloComponent2 en el método main() con esta línea:

Esta línea en realidad hace dos cosas. Podríamos escribirlas como dos líneas separadas que son un poco
más fáciles de entender:

La primera línea es la importante, donde se crea un nuevo objeto HelloComponent2.

El constructor HelloComponent2 recibe un String como argumento y, como lo hemos dispuesto, lo utiliza
para establecer el mensaje que se muestra en la ventana. Con un poco de magia del compilador de Java,
el texto entre comillas en el código fuente de Java se convierte en un objeto String. (Consulte el Capítulo
8 para una discusión sobre la clase String). La segunda línea simplemente añade nuestro nuevo
componente al marco para hacerlo visible, tal como hicimos en ejemplos anteriores.

Ya que estamos en el tema, si quisieras hacer nuestro mensaje configurable, puedes cambiar la línea del
constructor por la siguiente:

Ahora puedes pasar el texto por la línea de comandos al ejecutar la aplicación usando el siguiente
comando:

args[0] se refiere al primer parámetro de la línea de comandos. Su significado será más claro cuando
discutamos los arrays en el Capítulo 4. Si estás usando un Entorno de Desarrollo Integrado (IDE),
necesitarás configurarlo para que acepte tus parámetros antes de ejecutarlo, tal como se muestra para
IntelliJ IDEA en la Figura 2-15.
El constructor de HelloComponent2 realiza entonces dos acciones: establece el texto de la variable de
instancia theMessage y llama a addMouseMotionListener(). Este método forma parte del mecanismo de
eventos, el cual discutiremos a continuación. Le indica al sistema: "Oye, estoy interesado en cualquier
cosa que ocurra con el ratón".

La variable especial de solo lectura llamada this se utiliza para referirse explícitamente a nuestro objeto
(el contexto del objeto "actual") en la llamada a addMouseMotionListener(). Un método puede usar this
para referirse a la instancia del objeto que lo contiene. Por lo tanto, las siguientes dos declaraciones son
formas equivalentes de asignar el valor a la variable de instancia theMessage:

Ó:

Lo normal es que utilicemos la forma más corta e implícita para referirnos a variables de instancia, pero
necesitaremos usar `this` cuando tengamos que pasar explícitamente una referencia a nuestro objeto a
un método en otra clase. Frecuentemente hacemos esto para que los métodos en otras clases puedan
invocar nuestros métodos públicos o usar nuestras variables públicas.

Eventos

Los dos últimos métodos de HelloComponent2, mouseDragged() y mouseMoved(), nos permiten obtener
información del mouse. Cada vez que el usuario realiza una acción, como presionar una tecla en el teclado,
mover el mouse, o incluso golpear la pantalla táctil, Java genera un evento. Un evento representa una
acción que ha ocurrido; contiene información sobre la acción, como su tiempo y ubicación.
La mayoría de los eventos están asociados con un componente GUI particular en una aplicación. Por
ejemplo, una pulsación de tecla puede corresponder a un carácter que se escribe en un campo de entrada
de texto específico. Hacer clic en un botón del mouse puede activar un botón particular en la pantalla.
Incluso mover el mouse dentro de una cierta área de la pantalla puede desencadenar efectos como
resaltado o cambiar la forma del cursor.

Para trabajar con estos eventos, hemos importado un nuevo paquete, java.awt.event, que proporciona
objetos Event específicos que usamos para obtener información del usuario. (Observa que importar
java.awt.* no importa automáticamente el paquete de eventos. Las importaciones no son recursivas. Los
paquetes no contienen realmente otros paquetes, incluso si el esquema de nombres jerárquicos
implicaría que lo hacen).

Existen muchas clases de eventos diferentes, incluyendo MouseEvent, KeyEvent y ActionEvent. En su


mayoría, el significado de estos eventos es bastante intuitivo. Un MouseEvent ocurre cuando el usuario
realiza alguna acción con el mouse, un KeyEvent ocurre cuando el usuario presiona una tecla, y así
sucesivamente. ActionEvent es un poco especial; lo veremos en acción en el Capítulo 10. Por ahora, nos
centraremos en tratar con MouseEvents.

Los componentes GUI en Java generan eventos para tipos específicos de acciones del usuario. Por
ejemplo, si haces clic en el mouse dentro de un componente, el componente genera un evento de mouse.
Los objetos pueden solicitar recibir los eventos de uno o más componentes registrándose como
escuchadores (listeners) en la fuente del evento. Por ejemplo, para declarar que un escuchador quiere
recibir los eventos de movimiento del mouse de un componente, se invoca el método
addMouseMotionListener() de ese componente, especificando el objeto escuchador como argumento.
Eso es lo que nuestro ejemplo está haciendo en su constructor. En este caso, el componente está llamando
a su propio método addMouseMotionListener(), con el argumento `this`, lo que significa "Quiero recibir
mis propios eventos de movimiento del mouse".

Así es como nos registramos para recibir eventos. Pero, ¿cómo los obtenemos realmente? Eso es para lo
que sirven los dos métodos relacionados con el mouse en nuestra clase. El método mouseDragged() se
llama automáticamente en un escuchador para recibir los eventos generados cuando el usuario arrastra
el mouse, es decir, mueve el mouse con algún botón presionado. El método mouseMoved() se llama cada
vez que el usuario mueve el mouse sobre el área sin hacer clic en un botón. En este caso, hemos colocado
estos métodos en nuestra clase HelloComponent2 y la hemos registrado como su propio escuchador. Esto
es completamente apropiado para nuestro nuevo componente de arrastre de texto. Más generalmente,
un buen diseño generalmente dicta que los escuchadores de eventos se implementen como clases de
adaptadores que proporcionan una mejor separación entre la interfaz gráfica de usuario y la "lógica del
negocio". Discutiremos esto en detalle en el Capítulo 10.

Nuestro método mouseMoved() es aburrido: no hace nada. Ignoramos los movimientos simples del
mouse y reservamos nuestra atención para el arrastre. mouseDragged() tiene un poco más de contenido.
Este método se llama repetidamente por el sistema de ventanas para proporcionarnos actualizaciones
sobre la posición del mouse. Aquí está:

El primer argumento de mouseDragged() es un objeto MouseEvent, llamado 'e', que contiene toda la
información que necesitamos saber sobre este evento. Solicitamos al MouseEvent que nos proporcione
las coordenadas x e y de la posición actual del mouse llamando a sus métodos getX() y getY(). Guardamos
estos valores en las variables de instancia messageX y messageY para utilizarlas en otro lugar.

La belleza del modelo de eventos es que solo tienes que manejar los tipos de eventos que deseas. Si no
te importan los eventos del teclado, simplemente no registras un escuchador para ellos; el usuario puede
escribir todo lo que quiera y no te molestará. Si no hay escuchadores para un tipo particular de evento,
Java ni siquiera lo generará. Como resultado, el manejo de eventos es bastante eficiente.

Mientras discutimos los eventos, deberíamos mencionar otra pequeña adición que incluimos en
HelloJava2:

Esta línea indica al marco (frame) que salga de la aplicación cuando se hace clic en su botón de Cerrar. Se
llama la "operación" de cierre predeterminada porque esta operación, al igual que casi todas las demás
interacciones de la interfaz gráfica de usuario (GUI), está gobernada por eventos. Podríamos registrar un
"window listener" para recibir notificaciones cuando el usuario hace clic en el botón de Cerrar y tomar la
acción que deseemos, pero este método de conveniencia maneja los casos comunes.

Finalmente, hemos evitado un par de preguntas aquí: ¿cómo sabe el sistema que nuestra clase contiene
los métodos necesarios mouseDragged() y mouseMoved() (¿de dónde provienen estos nombres)? ¿Y por
qué tenemos que proporcionar un método mouseMoved() que no hace nada? La respuesta a estas
preguntas tiene que ver con las interfaces. Discutiremos las interfaces después de resolver algunos
asuntos pendientes con repaint().

El método repaint()

Como hemos cambiado las coordenadas para el mensaje (cuando arrastramos el ratón), nos gustaría que
HelloComponent2 se redibujara. Hacemos esto llamando a repaint(), que le pide al sistema que redibuje
la pantalla en un momento posterior. No podemos llamar directamente a paintComponent(), incluso si
quisiéramos, porque no tenemos un contexto gráfico para pasarle.

Podemos usar el método repaint() de la clase JComponent para solicitar que se redibuje nuestro
componente. repaint() hace que el sistema de ventanas de Java programe una llamada a nuestro método
paintComponent() en el próximo momento posible; Java suministra el objeto Graphics necesario, como
se muestra en la Figura 2-16.

Este modo de operación no es solo una incomodidad causada por no tener el contexto gráfico adecuado
a mano. La principal ventaja de este modo de operación es que el comportamiento de repintado es
manejado por otra entidad mientras estamos libres de continuar con nuestras tareas. El sistema Java tiene
un hilo de ejecución separado y dedicado que maneja todas las solicitudes de repaint(). Puede programar
y consolidar las solicitudes de repaint() según sea necesario, lo que ayuda a prevenir que el sistema de
ventanas se sobrecargue durante situaciones intensivas de pintado, como el desplazamiento. Otra ventaja
es que toda la funcionalidad de pintado debe estar encapsulada a través de nuestro método
paintComponent(); no estamos tentados a dispersarlo por toda la aplicación.
Interfaces
Ahora es momento de abordar la pregunta que evitamos anteriormente: ¿cómo sabe el sistema llamar a
mouseDragged() cuando ocurre un evento del mouse? ¿Es simplemente una cuestión de saber que
mouseDragged() es algún nombre mágico que debe tener nuestro método de manejo de eventos? No
exactamente; la respuesta a la pregunta se relaciona con la discusión de las interfaces, que son una de las
características más importantes del lenguaje Java.

El primer indicio de una interfaz aparece en la línea de código que introduce la clase HelloComponent2:
decimos que la clase implementa la interfaz MouseMotionListener:

Básicamente, una interfaz es una lista de métodos que la clase debe tener; esta interfaz en particular
requiere que nuestra clase tenga métodos llamados mouseDragged() y mouseMoved(). La interfaz no
especifica qué deben hacer estos métodos; de hecho, mouseMoved() no hace nada. Lo que sí establece
es que los métodos deben recibir un MouseEvent como argumento y no devolver ningún valor (eso es lo
que significa void).

Una interfaz es un contrato entre tú, el desarrollador de código, y el compilador. Al decir que tu clase
implementa la interfaz MouseMotionListener, estás diciendo que estos métodos estarán disponibles para
que otras partes del sistema los llamen. Si no los proporcionas, ocurrirá un error de compilación.

Esa no es la única forma en que las interfaces impactan este programa. Una interfaz también actúa como
una clase. Por ejemplo, un método podría devolver un MouseMotionListener o tomar un
MouseMotionListener como argumento. Cuando te refieres a un objeto por un nombre de interfaz de
esta manera, significa que no te importa la clase real del objeto; lo único necesario es que la clase
implemente esa interfaz. addMouseMotionListener() es uno de estos métodos: su argumento debe ser
un objeto que implemente la interfaz MouseMotionListener. El argumento que pasamos es this, el objeto
HelloComponent2 en sí mismo. El hecho de que sea una instancia de JComponent es irrelevante; podría
ser una Cookie, un Oso Hormiguero, o cualquier otra clase que imaginemos. Lo importante es que
implementa MouseMotionListener y, por lo tanto, declara que tendrá los dos métodos nombrados. Es por
eso que necesitamos un método mouseMoved(); aunque el que proporcionamos no haga nada, la interfaz
MouseMotionListener dice que debemos tener uno.

La distribución de Java viene con muchas interfaces que definen qué deben hacer las clases. Esta idea de
un contrato entre el compilador y una clase es muy importante. Hay muchas situaciones, como la que
acabamos de ver, donde no te importa qué clase es algo, solo te importa que tenga cierta capacidad,
como escuchar eventos del mouse. Las interfaces nos brindan una forma de actuar sobre objetos según
sus capacidades sin conocer ni preocuparnos por su tipo real. Son un concepto tremendamente
importante en cómo utilizamos Java como lenguaje orientado a objetos. Hablaremos sobre ellas en detalle
en el Capítulo 5.

El Capítulo 5 también discute cómo las interfaces proporcionan una especie de cláusula de escape a la
regla de Java de que una nueva clase solo puede extender una sola clase ("herencia única"). Una clase en
Java solo puede extender una clase, pero puede implementar tantas interfaces como desee. Las interfaces
pueden usarse como tipos de datos, pueden extender otras interfaces (pero no clases) y pueden ser
heredadas por clases (si la clase A implementa la interfaz B, las subclases de A también implementan B).
La diferencia crucial es que las clases en realidad no heredan métodos de las interfaces; las interfaces
simplemente especifican los métodos que la clase debe tener.
Adiós y Hola de Nuevo

Bueno, es hora de decir adiós a HelloJava. Esperamos que hayas desarrollado una idea de algunas de las
características del lenguaje Java y los conceptos básicos para escribir y ejecutar un programa en Java. Esta
breve introducción debería ayudarte mientras exploras los detalles de la programación con Java. Si te
sientes un poco confundido por parte del material presentado aquí, no te preocupes. Cubriremos todos
los temas importantes presentados aquí nuevamente en sus propios capítulos a lo largo del libro. Este
tutorial pretendía ser algo así como una "prueba de fuego" para introducirte en los conceptos importantes
y la terminología para que la próxima vez que los escuches, tengas una ventaja inicial.

Si bien estamos dejando de lado HelloJava por el momento, conoceremos mejor las herramientas del
mundo Java en el próximo capítulo. Veremos detalles sobre los comandos que ya has visto, como javac,
así como otros programas importantes. ¡Sigue leyendo para saludar a varios de tus nuevos mejores
amigos como desarrollador de Java!
CAPÍTULO 3

Herramientas del Oficio

Si bien es casi seguro que realizarás la mayoría de tu desarrollo en Java en un entorno de desarrollo
integrado (IDE) como Eclipse, VS Code o (el favorito del autor) IntelliJ IDEA, todas las herramientas
principales que necesitas para construir aplicaciones en Java están incluidas en el JDK que probablemente
ya hayas descargado en la sección "Instalación del JDK" en la página 28 de Oracle u otro proveedor de
OpenJDK. En este capítulo, discutiremos algunas de estas herramientas de línea de comandos que puedes
utilizar para compilar, ejecutar y empaquetar aplicaciones en Java. Hay muchas herramientas adicionales
para desarrolladores incluidas en el JDK que discutiremos a lo largo de este libro. Para obtener más
detalles sobre IntelliJ IDEA e instrucciones para cargar todos los ejemplos de este libro como un proyecto,
consulta el Apéndice A.

Entorno JDK

Después de instalar Java, es posible que el comando central de ejecución de Java aparezca
automáticamente en tu ruta (disponible para ejecutarse). Sin embargo, es probable que muchas de las
otras herramientas proporcionadas con el JDK no estén disponibles a menos que agregues el directorio
bin de Java a tu ruta de ejecución. Los siguientes comandos muestran cómo hacer esto en Linux, macOS
y Windows. Por supuesto, tendrás que cambiar la ruta para que coincida con la versión de Java que has
instalado.

En macOS, la situación puede ser más confusa debido a que las versiones recientes vienen con
"pseudocomandos" para los comandos de Java instalados. Si intentas ejecutar uno de estos comandos, el
sistema operativo te pedirá que descargues Java en ese momento. Puedes adelantarte y obtener OpenJDK
de Oracle siguiendo las instrucciones en "Herramientas y Entorno de Java" en la página 28.

Cuando tengas dudas, tu prueba estándar para determinar qué versión de las herramientas estás
utilizando es utilizar la bandera -version en los comandos java y javac:
La Máquina Virtual de Java

Una máquina virtual de Java (VM) es un software que implementa el sistema de ejecución de Java y
ejecuta aplicaciones Java. Puede ser una aplicación independiente como el comando java que viene con
el JDK, o estar integrada en una aplicación más grande como un navegador web. Por lo general, el
intérprete en sí es una aplicación nativa suministrada para cada plataforma, la cual inicia otras
herramientas escritas en el lenguaje Java. Herramientas como compiladores de Java e IDEs a menudo se
implementan directamente en Java para maximizar su portabilidad y extensibilidad. Por ejemplo, Eclipse
es una aplicación puramente Java.

La VM de Java realiza todas las actividades en tiempo de ejecución de Java. Carga archivos de clase de
Java, verifica clases de fuentes no confiables y ejecuta el bytecode compilado. Gestiona la memoria y los
recursos del sistema. Las buenas implementaciones también realizan optimización dinámica, compilando
el bytecode de Java en instrucciones de máquina nativas.

Ejecución de Aplicaciones Java

Una aplicación Java independiente debe tener al menos una clase que contenga un método llamado
main(), que es el primer código que se ejecuta al iniciar. Para ejecutar la aplicación, inicia la VM,
especificando esa clase como argumento. También puedes especificar opciones para el intérprete, así
como argumentos para pasárselos a la aplicación:

La clase debe especificarse como un nombre de clase completamente calificado, incluyendo el nombre
del paquete, si lo hay. Sin embargo, es importante tener en cuenta que no se incluye la extensión de
archivo .class. Aquí tienes un par de ejemplos:

El intérprete busca la clase en el classpath, que es una lista de directorios y archivos de archivo donde se
almacenan las clases. Discutiremos el classpath en detalle en la próxima sección. El classpath puede ser
especificado ya sea mediante una variable de entorno o con la opción de línea de comandos -classpath.
Si ambos están presentes, se utiliza la opción de línea de comandos.

Alternativamente, el comando java se puede utilizar para iniciar un archivo de archivo Java (JAR)
"ejecutable":

En este caso, el archivo JAR incluye metadatos con el nombre de la clase de inicio que contiene el método
main(), y el classpath se convierte en el propio archivo JAR.

Después de cargar la primera clase y ejecutar su método main(), la aplicación puede hacer referencia a
otras clases, iniciar hilos adicionales y crear su interfaz de usuario u otras estructuras, como se muestra
en la Figura 3-1.
El método main() debe tener la firma de método correcta. La firma de un método es el conjunto de
información que define el método. Incluye el nombre del método, los argumentos y el tipo de retorno, así
como los modificadores de tipo y visibilidad. El método main() debe ser un método público y estático que
tome un arreglo de objetos String como argumento y no devuelva ningún valor (void):

El hecho de que main() sea un método público y estático simplemente significa que es accesible
globalmente y que puede ser llamado directamente por su nombre. Discutiremos las implicaciones de los
modificadores de visibilidad como public y el significado de static en el Capítulo 4 y Capítulo 5.

El único argumento del método main(), el arreglo de objetos String, contiene los argumentos de línea de
comandos pasados a la aplicación. El nombre del parámetro no importa; solo es importante el tipo. En
Java, el contenido de myArgs es un arreglo (Más sobre arreglos en el Capítulo 4). En Java, los arreglos
saben cuántos elementos contienen y pueden proporcionar esa información felizmente:

myArgs[0] es el primer argumento de línea de comandos, y así sucesivamente.

El intérprete de Java continúa ejecutándose hasta que el método main() del archivo de clase inicial retorna
y hasta que cualquier hilo (thread) que haya iniciado también se cierre. (Más sobre hilos en el Capítulo 9).
Hilos especiales designados como hilos daemon se terminan automáticamente cuando el resto de la
aplicación ha finalizado.

Propiedades del Sistema

Aunque es posible leer variables de entorno del sistema desde Java, se desaconseja para la configuración
de la aplicación. En su lugar, Java permite que se pasen al inicio de la aplicación cualquier cantidad de
valores de propiedades del sistema cuando se inicia la Máquina Virtual (VM). Las propiedades del sistema
son simplemente pares de cadenas nombre-valor que están disponibles para la aplicación a través del
método estático System.getProperty(). Puedes usar estas propiedades como una alternativa más
estructurada y portátil a los argumentos de línea de comandos y variables de entorno para proporcionar
información de configuración general a tu aplicación al iniciar. Cada propiedad del sistema se pasa al
intérprete en la línea de comandos usando la opción -D seguida de nombre=valor. Por ejemplo:

El valor de la propiedad 'street' es accesible de la siguiente manera:

Una aplicación puede obtener su configuración de una multitud de otras maneras, incluyendo archivos o
configuración de red en tiempo de ejecución.

El Classpath

El concepto de un 'path' debería resultar familiar para cualquiera que haya trabajado en una plataforma
DOS o Unix. Es una variable de entorno que proporciona a una aplicación una lista de lugares para buscar
algún recurso. El ejemplo más común es un 'path' para programas ejecutables. En un shell de Unix, la
variable de entorno PATH es una lista separada por dos puntos de directorios que se buscan, en orden,
cuando el usuario escribe el nombre de un comando.

De manera similar, la variable de entorno CLASSPATH de Java es una lista de ubicaciones que se buscan
para archivos de clases Java. Tanto el intérprete de Java como el compilador de Java utilizan el CLASSPATH
al buscar paquetes y clases Java.
Un elemento del classpath puede ser un directorio o un archivo JAR. Java también soporta archivos en el
formato ZIP convencional, pero los archivos JAR y ZIP son realmente el mismo formato. Los archivos JAR
son archivos simples que incluyen archivos adicionales (metadatos) que describen el contenido de cada
archivo. Los archivos JAR se crean con la utilidad 'jar' de JDK; existen muchas herramientas para crear
archivos ZIP públicamente disponibles que pueden ser utilizadas para inspeccionar o crear archivos JAR
también. El formato de archivo de archivo permite que grandes grupos de clases y sus recursos se
distribuyan en un solo archivo; el tiempo de ejecución de Java extrae automáticamente archivos de clase
individuales del archivo según sea necesario.

Los medios precisos y el formato para establecer el classpath varían de un sistema a otro. En un sistema
Unix (incluyendo macOS), se establece la variable de entorno CLASSPATH con una lista separada por dos
puntos de directorios y archivos de archivo de clases:

Este ejemplo especifica un classpath con tres ubicaciones: un directorio en el hogar del usuario, un archivo
JAR en el directorio de otro usuario y el directorio actual, que siempre se especifica con un punto (.). El
último componente del classpath, el directorio actual, es útil cuando estás experimentando con clases.

En un sistema Windows, la variable de entorno CLASSPATH se establece con una lista separada por punto
y coma (;) de directorios y archivos de archivo de clases:

El lanzador de Java y otras herramientas de línea de comandos saben cómo encontrar las clases
principales, que son las clases incluidas en cada instalación de Java. Las clases en los paquetes java.lang,
java.io, java.net y javax.swing, por ejemplo, son todas clases principales, por lo que no es necesario
incluirlas en tu classpath.

El classpath también puede incluir comodines "*" que coinciden con todos los archivos JAR dentro de un
directorio. Por ejemplo:

Para encontrar otras clases, el intérprete de Java busca los elementos del classpath en orden. La búsqueda
combina la ubicación de la ruta y los componentes del nombre de clase completamente cualificado. Por
ejemplo, considera una búsqueda para la clase animals.birds.BigBird. Buscar en el directorio del classpath
/usr/lib/java significa que el intérprete busca un archivo de clase individual en
/usr/lib/java/animals/birds/BigBird.class. Buscar en un archivo ZIP o JAR en el classpath, digamos
/home/vicky/myutils.jar, significa que el intérprete busca el archivo de componente
animals/birds/BigBird.class dentro de ese archivo comprimido.

Para el tiempo de ejecución de Java (java) y el compilador de Java (javac), el classpath también puede ser
especificado con la opción -classpath:

Si no especificas la variable de entorno CLASSPATH o la opción de línea de comandos, el classpath por


defecto es el directorio actual (.); esto significa que los archivos en tu directorio actual normalmente están
disponibles. Si cambias el classpath y no incluyes el directorio actual, estos archivos ya no serán accesibles.

Sospechamos que aproximadamente el 80% de los problemas que enfrentan los recién llegados al
aprender Java están relacionados con el classpath. Puede que desees prestar especial atención al
establecimiento y verificación del classpath al comenzar. Si estás trabajando dentro de un entorno de
desarrollo integrado (IDE), este puede eliminar parte o la totalidad de la carga de administrar el classpath.
Sin embargo, comprender el classpath y saber exactamente qué contiene cuando se ejecuta tu aplicación
es muy importante para tu cordura a largo plazo. El comando javap, que se discute a continuación, puede
ser útil para depurar problemas relacionados con el classpath.
javap

Una herramienta útil para conocer es el comando javap. Con javap, puedes imprimir una descripción de
una clase compilada. No necesitas el código fuente, ni siquiera necesitas saber exactamente dónde está,
solo que está en tu classpath. Por ejemplo:

Imprime la información sobre la clase java.util.Stack:

Este texto parece ser un extracto de un documento que proporciona información sobre el uso de
herramientas relacionadas con Java, como javap y javac. Aquí está una traducción del texto
proporcionado:

Esto es muy útil si no tienes otra documentación a mano y también puede ser útil para solucionar
problemas de ruta de clases. Usando javap, puedes determinar si una clase está en la ruta de clases y
posiblemente incluso qué versión estás viendo (muchos problemas de ruta de clases involucran clases
duplicadas en la ruta de clases). Si tienes mucha curiosidad, puedes probar javap con la opción -c, lo que
hace que también imprima las instrucciones de la JVM para cada método en la clase.

Módulos

A partir de Java 9, como alternativa al enfoque clásico de la ruta de clases (que sigue estando disponible),
puedes aprovechar el nuevo enfoque de módulos para aplicaciones Java. Los módulos permiten
despliegues de aplicaciones más refinados y de alto rendimiento, incluso cuando la aplicación en cuestión
es grande. Requieren configuración adicional, por lo que no los abordaremos en este libro, pero es
importante saber que cualquier aplicación distribuida comercialmente probablemente estará basada en
módulos. Puedes consultar 'Java 9 Modularity' de Paul Bakker y Sander Mak para obtener más detalles y
ayuda para modularizar tus propios proyectos más grandes si comienzas a buscar compartir tu trabajo
más allá de solo publicar el código fuente en repositorios públicos.

El Compilador de Java

En esta sección, diremos algunas palabras sobre javac, el compilador de Java en el JDK. El compilador javac
está escrito completamente en Java, por lo que está disponible para cualquier plataforma que admita el
sistema de tiempo de ejecución de Java. javac convierte el código fuente de Java en una clase compilada
que contiene bytecode de Java. Por convención, los archivos fuente tienen la extensión .java; los archivos
de clase resultantes tienen la extensión .class. Cada archivo de código fuente se considera una unidad de
compilación única. Como verás en el Capítulo 5, las clases en una unidad de compilación dada comparten
ciertas características, como declaraciones de paquete e importación.

javac permite una clase pública por archivo e insiste en que el archivo tenga el mismo nombre que la
clase. Si el nombre de archivo y el nombre de la clase no coinciden, javac emite un error de compilación.
Un solo archivo puede contener múltiples clases, siempre y cuando solo una de las clases sea pública y
tenga el mismo nombre que el archivo. Evita empaquetar demasiadas clases en un solo archivo fuente.
Empaquetar clases juntas en un archivo .java solo las asocia superficialmente. En el Capítulo 5, hablaremos
sobre clases internas: clases que contienen otras clases e interfaces.

Como ejemplo, coloca el siguiente código fuente en el archivo BigBird.javav:

Después, compílalo con:

A diferencia del intérprete de Java, que solo toma el nombre de la clase como argumento, javac necesita
un nombre de archivo (con la extensión .java) para procesarlo. El comando anterior produce el archivo de
clase BigBird.class en el mismo directorio que el archivo fuente. Si bien es bueno ver el archivo de clase
en el mismo directorio que el origen para este ejemplo, para la mayoría de las aplicaciones reales, es
necesario almacenar el archivo de clase en un lugar adecuado en la ruta de clases.

Puedes usar la opción -d con javac para especificar un directorio alternativo para almacenar los archivos
de clase que genera javac. El directorio especificado se utiliza como la raíz de la jerarquía de clases, por lo
que los archivos .class se colocan en este directorio o en un subdirectorio debajo de él, dependiendo de
si la clase está contenida en un paquete. (El compilador crea subdirectorios intermedios
automáticamente, si es necesario). Por ejemplo, podemos usar el siguiente comando para crear el archivo
BigBird.class en /home/vicky/Java/classes/animals/birds/BigBird.class:

Puedes especificar múltiples archivos .java en un solo comando de javac; el compilador crea un archivo
de clase para cada archivo fuente. Sin embargo, no necesitas listar las otras clases a las que tu clase hace
referencia siempre y cuando estén en la ruta de clases en forma de fuente o compiladas. Durante la
compilación, Java resuelve todas las demás referencias de clase utilizando la ruta de clases.

El compilador de Java es más inteligente que un compilador promedio, reemplazando parte de la


funcionalidad de una utilidad make. Por ejemplo, javac compara los tiempos de modificación de los
archivos fuente y de clase para todas las clases y las recompila según sea necesario. Una clase Java
compilada recuerda el archivo fuente del que fue compilado, y mientras el archivo fuente esté disponible,
javac puede recompilarlo si es necesario. Si, en el ejemplo anterior, la clase BigBird hace referencia a otra
clase, animals.furry.Grover, javac busca el archivo fuente Grover.java en un paquete animals.furry y lo
recompila, si es necesario, para actualizar el archivo de clase Grover.class.

Sin embargo, por defecto, javac solo verifica los archivos fuente a los que se hace referencia directamente
desde otros archivos fuente. Esto significa que si tienes un archivo de clase desactualizado que solo es
referenciado por un archivo de clase actualizado, es posible que no se note y no se recompile. Por esa y
muchas otras razones, la mayoría de los proyectos utilizan una utilidad de compilación real, como Gradle,
para administrar compilaciones, empaquetados y más.

Finalmente, es importante tener en cuenta que javac puede compilar una aplicación incluso si solo están
disponibles las versiones compiladas (binarias) de algunas de las clases. No necesitas el código fuente para
todos tus objetos. Los archivos de clase de Java contienen toda la información de tipo de datos y firma de
métodos que contienen los archivos fuente, por lo que compilar contra archivos de clase binarios es tan
seguro en cuanto a tipos (y excepciones) como compilar con código fuente de Java.
Probando Java

Java 9 introdujo una utilidad llamada jshell, que te permite probar fragmentos de código Java y ver los
resultados inmediatamente. jshell es un REPL, un bucle de lectura, evaluación e impresión. Muchos
lenguajes los tienen, y antes de Java 9 había muchas variaciones de terceros disponibles, pero nada
incorporado en el JDK mismo. Vimos un indicio de lo que puede hacer jshell en el capítulo anterior; veamos
un poco más detenidamente sus capacidades.

Puedes usar una terminal o ventana de comandos desde tu sistema operativo, o puedes abrir una pestaña
de terminal en IntelliJ IDEA, como se muestra en la Figura 3-2. Simplemente escribe jshell en tu línea de
comandos y verás un poco de información de versión junto con un recordatorio rápido sobre cómo ver la
ayuda desde el REPL.

Adelante, intentemos ahora ese comando de ayuda:


JShell es bastante poderoso y no estaremos utilizando todas sus funciones en este libro. Sin embargo,
definitivamente lo usaremos para probar código Java y hacer ajustes rápidos aquí y a lo largo de la mayoría
de los capítulos restantes. Piensa en nuestro ejemplo HelloJava2, 'HelloJava2: The Sequel', en la página
53. Podemos crear elementos de interfaz de usuario como ese JFrame directamente en el REPL y luego
manipularlos, ¡todo mientras obtenemos retroalimentación inmediata! No es necesario guardar,
compilar, ejecutar, editar, guardar, compilar, ejecutar, etc. Probemos:

¡Ups! jshell es inteligente y tiene muchas características, pero también es bastante literal. Recuerda que
si deseas usar una clase que no esté incluida en el paquete predeterminado, debes importarla. Esto es
cierto en archivos fuente de Java y también es cierto al usar jshell. Intentemos de nuevo:

Esto es mejor. Un poco extraño, probablemente, pero mejor. Nuestro objeto frame ha sido creado. Esa
información adicional después de la flecha ==> son solo detalles sobre nuestro JFrame, como su tamaño
(0x0) y su posición en pantalla (0,23). Otros tipos de objetos mostrarán otros detalles. Démonos un ancho
y alto a nuestro frame como hicimos antes y pongamos nuestro frame en la pantalla donde podamos
verlo:

¡Deberías ver una ventana aparecer justo ante tus ojos! Estará resplandeciente con elegancia moderna,
tal como se muestra en la Figura 3-3.
Por cierto, no te preocupes por cometer errores en el REPL. Verás un mensaje de error, pero simplemente
puedes corregir lo que estaba mal y seguir adelante. Como ejemplo rápido, imagina cometer un error
tipográfico al intentar cambiar el tamaño del marco:

Java es sensible a mayúsculas y minúsculas, por lo que setSize() no es lo mismo que setsize(). jshell te
proporciona el mismo tipo de información de error que el compilador de Java, pero la presenta en línea.
Corrige ese error y observa cómo el marco se vuelve un poco más grande (Figura 3-4).

¡Increíble! Bueno, está bien, tal vez sea menos útil, pero apenas estamos comenzando. Añadamos algo
de texto usando la clase JLabel:
Interesante, pero ¿por qué nuestro etiqueta no apareció en el marco? Profundizaremos mucho más en
esto en el capítulo sobre interfaces de usuario, pero Java permite que algunos cambios gráficos se
acumulen antes de hacerlos visibles en la pantalla. Esto puede ser un truco inmensamente eficiente, pero
a veces puede tomarte por sorpresa. Forcémos al marco a volver a dibujarse (Figura 3-5):

Ahora podemos ver nuestra etiqueta. Algunas acciones automáticamente desencadenarán una llamada a
revalidate() o repaint(). Cualquier componente ya agregado a nuestro marco antes de hacerlo visible, por
ejemplo, aparecería de inmediato cuando mostremos el marco. O podemos quitar la etiqueta de manera
similar a cómo la agregamos. Observa de nuevo qué sucede cuando cambiamos el tamaño del marco
inmediatamente después de eliminar nuestra etiqueta (Figura 3-6):
¿Ves? Tenemos una ventana nueva y más delgada, sin etiqueta, todo esto sin repintado forzado.
Trabajaremos más con elementos de interfaz de usuario en capítulos posteriores, pero intentemos un
último ajuste a nuestra etiqueta solo para mostrarte lo fácil que es probar nuevas ideas o métodos que
hayas consultado en la documentación. Por ejemplo, podemos centrar el texto de la etiqueta, lo que
resultaría en algo similar a la Figura 3-7:

Sabemos que esta fue otra rápida visita con varios fragmentos de código que quizás aún no tengan
sentido, como ¿por qué CENTER está todo en mayúsculas? ¿O por qué se utiliza el nombre de la clase
JLabel antes de nuestra alineación central? Con suerte, escribir a lo largo, probablemente cometiendo
algunos pequeños errores, corrigiéndolos y viendo los resultados, te hará querer saber más. Solo
queremos asegurarnos de que tengas las herramientas necesarias para seguir experimentando a medida
que avanzas en el resto de este libro. ¡Como tantas otras habilidades, la programación se beneficia de
hacer además de leer!

Archivos JAR

Los archivos de Java (JAR) son las maletas de Java. Son la forma estándar y portátil de empaquetar todas
las partes de tu aplicación de Java en un paquete compacto para su distribución o instalación. Puedes
poner lo que quieras en un archivo JAR: archivos de clase de Java, objetos serializados, archivos de datos,
imágenes, audio, etc. Un archivo JAR también puede llevar una o más firmas digitales que certifican su
integridad y autenticidad. Una firma puede estar adjunta al archivo en su totalidad o a elementos
individuales en el archivo.

El sistema de tiempo de ejecución de Java puede cargar archivos de clase directamente desde un archivo
en tu CLASS PATH, como se describió anteriormente. Los archivos que no son de clase (datos, imágenes,
etc.) contenidos en tu archivo JAR también pueden ser recuperados desde la ruta de clases por tu
aplicación utilizando el método getResource(). Al usar esta facilidad, tu código no tiene que saber si algún
recurso está en un archivo normal o es miembro de un archivo JAR. Ya sea que una clase o archivo de
datos dado sea un elemento en un archivo JAR o un archivo individual en la ruta de clases, siempre puedes
referirte a él de una manera estándar y dejar que el cargador de clases de Java resuelva la ubicación.

Compresión de archivos

Los elementos almacenados en archivos JAR están comprimidos con la compresión estándar de archivos
ZIP. La compresión hace que la descarga de clases a través de una red sea mucho más rápida. Un rápido
análisis de la distribución estándar de Java muestra que un archivo de clase típico se reduce
aproximadamente un 40% cuando se comprime. Los archivos de texto, como HTML o ASCII, que contienen
palabras en inglés a menudo se comprimen a una décima parte de su tamaño original o menos. (Por otro
lado, los archivos de imagen normalmente no se hacen más pequeños cuando se comprimen, ya que la
mayoría de los formatos de imagen comunes son en sí mismos un formato de compresión).

Java también tiene un formato de archivo llamado Pack200, que está optimizado específicamente para el
bytecode de clase de Java y puede lograr una compresión de más de cuatro veces mayor que ZIP solo.
Hablaremos sobre Pack200 más adelante en este capítulo.

La utilidad jar

La utilidad jar proporcionada con el JDK es una herramienta simple para crear y leer archivos JAR. Su
interfaz de usuario no es particularmente amigable. Imita el comando tar (archivo de cinta) de Unix. Si
estás familiarizado con tar, reconocerás las siguientes invocaciones:

En estos comandos, las letras de las banderas c, t y x indican a jar si está creando un archivo, listando el
contenido de un archivo o extrayendo archivos de un archivo. La letra f significa que el siguiente
argumento es el nombre del archivo JAR en el que debe operar. La bandera opcional v le dice a jar que
sea detallado al mostrar información sobre los archivos. En el modo detallado, se obtiene información
sobre tamaños de archivo, tiempos de modificación y relaciones de compresión.

Los elementos subsiguientes en la línea de comandos (es decir, cualquier cosa aparte de las letras que le
dicen a jar qué hacer y el archivo en el que jar debe operar) se toman como nombres de elementos del
archivo. Si estás creando un archivo, los archivos y directorios que enlistas se colocan en él. Si estás
extrayendo, solo se extraen los nombres de archivo que enlistas del archivo. (Si no enlistas ningún archivo,
jar extrae todo en el archivo.)

Por ejemplo, supongamos que acabamos de completar nuestro nuevo juego, spaceblaster. Todos los
archivos asociados con el juego están en tres directorios. Las clases de Java en sí están en el directorio
spaceblaster/game, spaceblaster/images contiene las imágenes del juego y spaceblaster/docs contiene
datos asociados al juego. Podemos empacar todo esto en un archivo con este comando:
Debido a que solicitamos una salida detallada, jar nos indica lo que está haciendo:

jar crea el archivo spaceblaster.jar y añade el directorio spaceblaster, agregando los directorios y archivos
dentro de spaceblaster al archivo. En modo detallado, jar reporta el ahorro obtenido al comprimir los
archivos en el archivo.

Podemos desempaquetar el archivo con este comando:

Del mismo modo, podemos extraer un archivo o directorio individual con:

Pero, por supuesto, normalmente no es necesario desempaquetar un archivo JAR para usar su contenido;
las herramientas de Java saben cómo extraer archivos de los archivos automáticamente. Podemos listar
el contenido de nuestro JAR con el comando:

Aquí está la salida; lista todos los archivos, sus tamaños y sus tiempos de creación:

Manifestos de JAR

Ten en cuenta que el comando jar añade automáticamente un directorio llamado META-INF a nuestro
archivo. El directorio META-INF contiene archivos que describen el contenido del archivo JAR. Siempre
contiene al menos un archivo: MANIFEST.MF. El archivo MANIFEST.MF puede contener una "lista de
empaque" que nombra los archivos en el archivo junto con un conjunto de atributos definidos por el
usuario para cada entrada.
El manifiesto es un archivo de texto que contiene un conjunto de líneas en la forma clave: valor. Por
defecto, el manifiesto está vacío y contiene únicamente información sobre la versión del archivo JAR:

También es posible firmar archivos JAR con una firma digital. Cuando haces esto, se añade información
de resumen (checksum) al manifiesto para cada elemento archivado (como se muestra a continuación) y
el directorio META-INF contiene archivos de firma digital para los elementos en el archivo:

Puedes añadir tu propia información a las descripciones del manifiesto al especificar tu propio archivo
complementario de manifiesto al crear el archivo. Este es un lugar posible para almacenar otros tipos
simples de información de atributos sobre los archivos en el archivo, tal vez información de versión o
autoría.

Por ejemplo, podemos crear un archivo con las siguientes líneas de “clave: valor”:

Para añadir esta información al manifiesto en nuestro archivo, colócala en un archivo llamado
myManifest.mf y usa el siguiente comando jar:

Incluimos una opción adicional, 'm', que especifica que jar debe leer información adicional del manifiesto
del archivo dado en la línea de comandos. ¿Cómo sabe jar qué archivo es cuál? Debido a que 'm' está
antes que 'f', espera encontrar la información del manifiesto antes del nombre del archivo JAR que creará.
Si piensas que esto es incómodo, tienes razón; si intercambias el orden de los nombres, jar hará lo
incorrecto.

Una aplicación puede obtener esta información del manifiesto de un archivo JAR utilizando la clase
`java.util.jar.Manifest`.

Haciendo un archivo JAR ejecutable

Aparte de los atributos, puedes colocar algunos valores especiales en el archivo de manifiesto. Uno de
ellos, Main-Class, te permite especificar la clase que contiene el método principal primary main() para una
aplicación contenida en el JAR:

Si agregas esto al manifiesto de tu archivo JAR (utilizando la opción 'm' descrita anteriormente), puedes
ejecutar la aplicación directamente desde el JAR:

Algunos entornos de GUI solían admitir hacer doble clic en el archivo JAR para iniciar la aplicación. El
intérprete busca el valor de Main-Class en el manifiesto y luego carga la clase designada como la clase de
inicio de la aplicación. Esta característica parece estar en cambio y no es compatible con todos los sistemas
operativos, por lo que es posible que necesites investigar otras opciones de distribución de aplicaciones
Java si estás creando una aplicación que deseas compartir con otros.
La utilidad pack200

Pack200 es un formato de archivo optimizado para almacenar archivos de clase de Java compilados.
Pack200 no es una nueva forma de compresión, sino más bien un diseño eficiente para la información de
clase que elimina muchos tipos de desperdicio y redundancia entre clases relacionadas. Es efectivamente
un formato de archivo de clase a granel que descompone muchas clases y vuelve a ensamblar sus partes
de manera eficiente en un catálogo. Esto permite que un formato de compresión estándar como ZIP
funcione con máxima eficiencia en el archivo, logrando una compresión de cuatro o más veces mayor. El
tiempo de ejecución de Java no comprende el formato pack200, por lo que no puedes colocar archivos de
este tipo en la ruta de clases. En cambio, es principalmente un formato intermedio que es muy útil para
transferir JAR de aplicaciones a través de la red para applets u otros tipos de aplicaciones basadas en web.

Solía ser popular para distribuir applets en la web en el pasado, pero a medida que los applets han
desaparecido, también ha disminuido la utilidad del formato pack200. Es posible que aún encuentres
algunos archivos .pack.gz, por lo que queríamos mencionar las herramientas que usarías, pero las
herramientas en sí mismas han sido eliminadas en Java 14.

Puedes convertir un JAR hacia y desde el formato pack200 con los comandos pack200 y unpack200
proporcionados con el JDK y OpenJDK antes de la versión 14 de Java.

Por ejemplo, para convertir foo.jar a foo.pack.gz, usa el comando pack200:

Para convertir foo.pack.gz a foo.jar:

Ten en cuenta que el proceso pack200 descompone completamente y reconstruye tus clases a nivel de
clase, por lo que el archivo foo.jar resultante no será exactamente igual byte a byte que el original.

Construyendo

Bien entonces. Obviamente, hay bastantes herramientas en el ecosistema de Java, y acertaron con el
nombre al reunir todo en el "Kit" de Desarrollo de Java. No utilizarás todas las herramientas mencionadas
de inmediato, así que no te preocupes si la lista de utilidades parece un poco abrumadora. Nos
centraremos en el uso del compilador javac a medida que te adentres más y más en el mundo de Java.
Incluso entonces, el compilador y varias otras herramientas están convenientemente integradas detrás
de botones en tu entorno de desarrollo integrado (IDE). Nuestro objetivo para este capítulo es
asegurarnos de que conozcas qué herramientas están disponibles para que puedas volver para obtener
detalles cuando los necesites.

Con suerte, ahora que has visto parte del arsenal disponible para ayudar a procesar y empaquetar código
Java, estás listo para escribir algo de ese código. Los próximos capítulos sientan las bases para hacer
precisamente eso, ¡así que vamos a sumergirnos!
CAPÍTULO 4

El lenguaje Java

Este capítulo comienza nuestra introducción a la sintaxis del lenguaje Java. Debido a que los lectores
vienen a este libro con diferentes niveles de experiencia en programación, es difícil establecer el nivel
adecuado para todas las audiencias. Hemos tratado de lograr un equilibrio entre brindar un recorrido
exhaustivo con varios ejemplos de la sintaxis del lenguaje para principiantes y proporcionar suficiente
información de fondo para que un lector más experimentado pueda comparar rápidamente las diferencias
entre Java y otros lenguajes. Dado que la sintaxis de Java se deriva de C, realizamos algunas
comparaciones con características de ese lenguaje, pero no es necesario tener conocimientos previos de
C. El Capítulo 5 se basará en este capítulo hablando sobre el lado orientado a objetos de Java y completará
la discusión del lenguaje principal. El Capítulo 7 trata sobre genéricos, una característica que mejora la
forma en que funcionan los tipos en el lenguaje Java, lo que te permite escribir ciertos tipos de clases de
manera más flexible y segura. Después de eso, nos sumergimos en las API de Java y vemos qué podemos
hacer con el lenguaje. El resto de este libro está lleno de ejemplos concisos que realizan tareas útiles en
una variedad de áreas. Si te quedan preguntas después de estos capítulos introductorios, esperamos que
se respondan al observar el código. ¡Siempre hay más por aprender, por supuesto! Intentaremos señalar
otros recursos en el camino que puedan ser útiles para aquellos que buscan continuar su viaje con Java
más allá de los temas que cubrimos.

Para lectores que recién comienzan su viaje en la programación, es probable que la web sea una
compañera constante. Muchos, muchos sitios, artículos de Wikipedia, publicaciones de blog y, bueno,
toda la extensión de Stack Overflow pueden ayudarte a profundizar en temas particulares o responder
pequeñas preguntas que puedan surgir. Por ejemplo, aunque este libro cubre el lenguaje Java y cómo
comenzar a escribir programas útiles con Java y sus herramientas, no cubrimos componentes básicos más
bajos de la programación, como algoritmos. Estos fundamentos de programación aparecerán
naturalmente en nuestras discusiones y ejemplos de código, pero podrías disfrutar de algunos enlaces
tangenciales para ayudar a afianzar ciertos detalles o completar vacíos que necesariamente dejamos.

Para obtener más información sobre Unicode, consulta http://www.unicode.org. Irónicamente, uno de
los guiones enumerados como "obsoleto y arcaico" y actualmente no compatible con el estándar Unicode
es el javanés, un idioma histórico del pueblo de la Isla de Java.

Codificación de texto

Java es un lenguaje para internet. Dado que los ciudadanos de la Red hablan y escriben en muchos idiomas
humanos diferentes, Java debe poder manejar una gran cantidad de idiomas. Una de las formas en que
Java admite la internacionalización es a través del conjunto de caracteres Unicode. Unicode es un estándar
mundial que admite los guiones de la mayoría de los idiomas. La última versión de Java basa sus datos de
caracteres y cadenas en el estándar Unicode 6.0, que utiliza al menos dos bytes para representar cada
símbolo internamente.

El código fuente de Java se puede escribir utilizando Unicode y almacenarse en una serie de codificaciones
de caracteres, desde una forma binaria completa hasta valores de caracteres Unicode codificados en
ASCII. Esto hace que Java sea un lenguaje amigable para programadores que no hablan inglés, ya que
pueden utilizar su idioma nativo para nombres de clase, método y variable, al igual que para el texto
mostrado por la aplicación.

El tipo char de Java y la clase String admiten nativamente valores Unicode. Internamente, el texto se
almacena utilizando char[] o byte[]; sin embargo, el lenguaje Java y las API hacen que esto sea
transparente para ti y generalmente no tendrás que pensar en ello. Unicode también es muy compatible
con ASCII (ASCII es la codificación de caracteres más común para el inglés). Los primeros 256 caracteres
se definen como idénticos a los primeros 256 caracteres en el conjunto de caracteres ISO 8859-1 (Latin-
1), por lo que Unicode es efectivamente compatible con las codificaciones de caracteres más comunes en
inglés. Además, una de las codificaciones de archivos más comunes para Unicode, llamada UTF-8,
conserva los valores ASCII en su forma de un solo byte. Esta codificación se utiliza de forma
predeterminada en archivos de clase Java compilados, por lo que el almacenamiento sigue siendo
compacto para el texto en inglés.

La mayoría de las plataformas no pueden mostrar todos los caracteres Unicode actualmente definidos.
Como resultado, los programas Java se pueden escribir con secuencias de escape especiales de Unicode.
Un carácter Unicode puede representarse con esta secuencia de escape:

xxxx es una secuencia de uno a cuatro dígitos hexadecimales. La secuencia de escape indica un carácter
Unicode codificado en ASCII. Esta es también la forma que Java utiliza para mostrar (imprimir) caracteres
Unicode en un entorno que de otra manera no los soportaría. Java también cuenta con clases para leer y
escribir flujos de caracteres Unicode en codificaciones específicas, incluyendo UTF-8.

Como con muchos estándares de larga duración en el mundo tecnológico, Unicode fue originalmente
diseñado con tanto espacio adicional que ninguna codificación de caracteres concebible podría necesitar
más de 64K caracteres. Suspiro. Naturalmente, hemos superado ese límite y algunas codificaciones UTF-
32 están en circulación popular. Especialmente, los caracteres emoji dispersos en las aplicaciones de
mensajería están codificados más allá del rango estándar de caracteres Unicode. (Por ejemplo, el emoji
de la carita sonriente canónica tiene el valor Unicode 1F600.) Java admite secuencias de escape UTF-16
multibyte para estos caracteres. No todos los entornos que soportan Java admitirán la salida de emoji,
pero puedes iniciar jshell para averiguar si tu entorno puede mostrar caracteres emoji (ver Figura 4-1).

Ten cuidado al usar tales caracteres, sin embargo. Tuvimos que usar una captura de pantalla para
asegurarnos de que pudieras ver a los pequeños lindos en jshell ejecutándose en un Mac. Pero al iniciar
una aplicación de escritorio Java en ese mismo sistema con un JFrame y JLabel como lo hicimos en el
Capítulo 3, obtienes la Figura 4-2.
No es que no puedas usar o admitir emoji en tus aplicaciones, simplemente debes ser consciente de las
diferencias en las características de salida. Asegúrate de que tus usuarios tengan una buena experiencia
donde sea que estén ejecutando tu código.

Comentarios

Java admite tanto comentarios de bloque al estilo C delimitados por /* y */ como comentarios de línea al
estilo C++ indicados por //:

Comentarios de bloque tienen tanto un inicio como un fin y pueden abarcar grandes rangos de texto. Sin
embargo, no pueden estar "anidados", lo que significa que no puedes tener un comentario de bloque
dentro de otro comentario de bloque sin que el compilador se confunda. Los comentarios de una sola
línea tienen solo una secuencia de inicio y están delimitados por el final de una línea; indicadores
adicionales de // dentro de una sola línea no tienen efecto. Los comentarios de línea son útiles para
comentarios cortos dentro de métodos; no entran en conflicto con los comentarios de bloque, por lo que
aún puedes comentar bloques más grandes de código en los que estén anidados.

Comentarios Javadoc

Un comentario de bloque que comienza con /** indica un comentario de documentación especial. Un
comentario de documentación está diseñado para ser extraído por generadores de documentación
automatizados, como el programa javadoc de JDK o los tooltips con contexto en muchos entornos de
desarrollo integrados (IDEs). Un comentario de documentación se termina con */, al igual que un
comentario de bloque regular. Dentro del comentario de documentación, las líneas que comienzan con
@ se interpretan como instrucciones especiales para el generador de documentación, proporcionándole
información sobre el código fuente. Por convención, cada línea de un comentario de documentación
comienza con un *, como se muestra en el siguiente ejemplo, pero esto es opcional. Cualquier espacio
inicial y el * en cada línea son ignorados:

El comando javadoc crea documentación en HTML para clases leyendo el código fuente y extrayendo los
comentarios incrustados y las etiquetas @. En este ejemplo, las etiquetas causan que se presente
información del autor y la versión en la documentación de la clase. Las etiquetas @see producen enlaces
de hipertexto a la documentación de clases relacionadas.

El compilador también examina los comentarios de documentación; en particular, está interesado en la


etiqueta @deprecated, lo que significa que el método ha sido declarado obsoleto y se debe evitar en
programas nuevos. El hecho de que un método esté deprecado se nota en el archivo de clase compilado,
por lo que se puede generar un mensaje de advertencia cada vez que se use una función obsoleta en tu
código (incluso si el origen no está disponible).

Los comentarios de documentación pueden aparecer sobre definiciones de clase, método y variables,
pero algunas etiquetas pueden no ser aplicables a todos ellos. Por ejemplo, la etiqueta @exception solo
se puede aplicar a métodos. La Tabla 4-1 resume las etiquetas utilizadas en comentarios de
documentación.
Javadoc como metadatos

Las etiquetas Javadoc en comentarios de documentación representan metadatos sobre el código fuente;
es decir, añaden información descriptiva sobre la estructura o el contenido del código que no es,
estrictamente hablando, parte de la aplicación. Algunas herramientas adicionales extienden el concepto
de etiquetas de estilo Javadoc para incluir otros tipos de metadatos sobre programas Java que se
transportan con el código compilado y que pueden ser utilizados más fácilmente por la aplicación para
afectar su compilación o comportamiento en tiempo de ejecución. La facilidad de anotaciones de Java
proporciona una manera más formal y extensible de añadir metadatos a clases, métodos y variables de
Java. Estos metadatos también están disponibles en tiempo de ejecución.

Anotaciones

El prefijo @ cumple otro papel en Java que puede parecer similar a las etiquetas. Java soporta la noción
de anotaciones como un medio de marcar cierto contenido para un tratamiento especial. Aplicas
anotaciones al código fuera de los comentarios. La anotación puede proporcionar información útil al
compilador o a tu Entorno de Desarrollo Integrado (IDE, por sus siglas en inglés). Por ejemplo, la anotación
@SuppressWarnings hace que el compilador (y a menudo también tu IDE) oculte advertencias sobre cosas
como código inalcanzable. A medida que te adentras en la creación de clases más interesantes en "Diseño
de Clases Avanzado" en la página 155, es posible que veas que tu IDE añade anotaciones @Override a tu
código. Esta anotación indica al compilador realizar algunas verificaciones adicionales; estas verificaciones
están destinadas a ayudarte a escribir un código válido y detectar errores antes de que tú (o tus usuarios)
ejecuten el programa.

Incluso puedes crear anotaciones personalizadas para trabajar con otras herramientas o frameworks.
Aunque una discusión más profunda sobre anotaciones está fuera del alcance de este libro,
aprovecharemos algunas anotaciones muy útiles para programación web en el Capítulo 12.

Variables y Constantes

Si bien comentar tu código es fundamental para producir archivos legibles y mantenibles, en algún
momento debes comenzar a escribir contenido compilable. Programar es manipular ese contenido. En
casi todos los lenguajes, esa información se almacena en variables y constantes para facilitar su uso por
parte del programador. Java tiene ambos. Las variables almacenan información que planeas cambiar y
reutilizar con el tiempo (o información que no conoces de antemano, como la dirección de correo
electrónico de un usuario). Las constantes almacenan información que es, bueno, constante. Hemos visto
ejemplos de ambos elementos incluso en nuestros programas de inicio simples. Recuerda nuestra simple
etiqueta gráfica de "HelloJava" en la página 41:

En este fragmento, `frame` es una variable. La cargamos en la línea 5 con una nueva instancia de la clase
JFrame. Luego, reutilizamos esa misma instancia en la línea 7 para agregar nuestro etiqueta (label).
Volvemos a usar la variable nuevamente para establecer el tamaño de nuestro frame en la línea 8 y para
hacerlo visible en la línea 9. Toda esa reutilización es exactamente donde brillan las variables.
La línea 6 contiene una constante: `JLabel.CENTER`. Las constantes contienen algún valor que nunca
cambia a lo largo de tu programa. La información que no cambia puede parecer algo extraño para
almacenar, ¿por qué no usar simplemente la información misma cada vez? Dado que el programador que
escribe el código puede seleccionar el nombre de la constante, un beneficio inmediato es que puedes
describir la información de una manera útil. `JLabel.CENTER` puede parecer un poco opaco aún, pero la
palabra "CENTER" al menos te da una pista sobre lo que está sucediendo.

El uso de constantes con nombres también permite cambios más simples en el futuro. Si codificas algo
como el número máximo de algún recurso que utilizas, alterar ese límite es mucho más fácil si todo lo que
tienes que hacer es cambiar el valor inicializado de la constante. Si usas un número literal como "5",
tendrías que buscar en todos tus archivos de Java para rastrear cada ocurrencia de un 5 y cambiarlo
también, si ese 5 en particular se refería al límite del recurso. Ese tipo de búsqueda y reemplazo manual
es propenso a errores y va más allá de ser tedioso.

Veremos más detalles sobre los tipos y los valores iniciales de variables y constantes más adelante en la
siguiente sección. Como siempre, ¡siéntete libre de usar jshell para explorar y descubrir algunos de esos
detalles por tu cuenta! Aunque ten en cuenta que, debido a limitaciones del intérprete, no puedes
declarar tus propias constantes de nivel superior en jshell. Aún así, puedes usar constantes definidas para
clases como `JLabel.CENTER` arriba o definirlas en tus propias clases que puedas escribir en jshell. La clase
Math tiene todo tipo de funciones ingeniosas y una constante para π. Intenta calcular y almacenar el área
de un círculo en una variable. Luego, demuéstrate a ti mismo que reasignar constantes no funcionará.

El error del compilador se produce cuando intentamos establecer π como 3. También observa que tanto
el radio (radius) como el área (area) pueden cambiarse después de ser declarados e inicializados. Pero las
variables solo contienen un valor a la vez. El cálculo más reciente es lo único que permanece en la variable
area.

Tipos

El sistema de tipos de un lenguaje de programación describe cómo se asocian sus elementos de datos (las
variables y constantes de las que acabamos de hablar) con el almacenamiento en memoria y cómo están
relacionados entre sí. En un lenguaje de tipo estático, como C o C++, el tipo de un elemento de datos es
un atributo simple e inmutable que a menudo corresponde directamente a algún fenómeno subyacente
del hardware, como un registro o un valor de puntero. En un lenguaje más dinámico, como Smalltalk o
Lisp, las variables pueden asignarse elementos arbitrarios y pueden cambiar efectivamente su tipo a lo
largo de su vida útil. Se invierte una cantidad considerable de recursos en validar lo que ocurre en estos
lenguajes en tiempo de ejecución. Los lenguajes de script, como Perl, logran la facilidad de uso
proporcionando sistemas de tipos drásticamente simplificados en los que solo se pueden almacenar
ciertos elementos de datos y los valores se unifican en una representación común, como cadenas de texto.

Java combina muchas de las mejores características tanto de los lenguajes de tipo estático como de los
dinámicos. Al igual que en un lenguaje de tipo estático, cada variable y elemento de programación en Java
tiene un tipo que se conoce en tiempo de compilación, por lo que el sistema en tiempo de ejecución
normalmente no tiene que comprobar la validez de las asignaciones entre tipos mientras el código se está
ejecutando. A diferencia del tradicional C o C++, Java también mantiene información en tiempo de
ejecución sobre objetos y la utiliza para permitir un comportamiento verdaderamente dinámico. El código
Java puede cargar nuevos tipos en tiempo de ejecución y usarlos de manera completamente orientada a
objetos, permitiendo la conversión de tipos y un polimorfismo completo (extensión de tipos). El código
Java también puede "reflejar" o examinar sus propios tipos en tiempo de ejecución, lo que permite tipos
avanzados de comportamiento de aplicaciones como intérpretes que pueden interactuar dinámicamente
con programas compilados.

Los tipos de datos de Java se dividen en dos categorías. Los tipos primitivos representan valores simples
que tienen funcionalidad incorporada en el lenguaje; representan valores simples como números,
booleanos y caracteres. Los tipos de referencia (o tipos de clase) incluyen objetos y matrices; se llaman
tipos de referencia porque "se refieren" a un tipo de datos grande que se pasa "por referencia", como
explicaremos en breve. Los tipos genéricos y los métodos definen y operan sobre objetos de varios tipos
mientras proporcionan seguridad de tipos en tiempo de compilación. Por ejemplo, un List<String> es una
Lista que solo puede contener Strings. Estos también son tipos de referencia y veremos mucho más de
ellos en el Capítulo 7.

Tipos Primitivos

Los números, caracteres y valores booleanos son elementos fundamentales en Java. A diferencia de
algunos otros lenguajes de programación orientados a objetos (quizás más puros), no son objetos. Para
aquellas situaciones en las que es deseable tratar un valor primitivo como un objeto, Java proporciona
clases "wrapper" (envoltorio). (Más sobre esto más adelante). La principal ventaja de tratar los valores
primitivos como especiales es que el compilador y el tiempo de ejecución de Java pueden optimizar más
fácilmente su implementación. Los valores primitivos y los cálculos todavía pueden mapearse al hardware
como siempre lo han hecho en lenguajes de nivel inferior. De hecho, si trabajas con bibliotecas nativas
utilizando la Java Native Interface (JNI) para interactuar con otros lenguajes o servicios, estos tipos
primitivos tendrán un papel destacado en tu código.

Una característica importante de portabilidad de Java es que los tipos primitivos están precisamente
definidos. Por ejemplo, nunca tienes que preocuparte por el tamaño de un int en una plataforma
específica; siempre es un número de 32 bits, con signo y complemento a dos. El "tamaño" de un tipo
numérico determina cuán grande (o preciso) puede ser un valor que puedes almacenar. Por ejemplo, el
tipo byte es para números pequeños, desde -128 hasta 127, mientras que el tipo int puede manejar la
mayoría de las necesidades numéricas, almacenando valores entre (aproximadamente) +/- dos mil
millones. La Tabla 4-2 resume los tipos primitivos de Java.
Los que tengan experiencia en C podrán notar que los tipos primitivos se parecen a una
idealización de los tipos escalares de C en una máquina de 32 bits, y están absolutamente en lo correcto.
Esa es la apariencia que se busca. Los caracteres de 16 bits fueron impuestos por Unicode y los punteros
ad hoc fueron eliminados por otras razones. Sin embargo, en general, la sintaxis y semántica de los tipos
primitivos de Java derivan de C.

Pero, ¿por qué tener tamaños en absoluto? Nuevamente, esto se remonta a la eficiencia y la optimización.
La cantidad de goles en un partido de fútbol rara vez supera los dígitos individuales; cabrían en una
variable de tipo byte. Sin embargo, la cantidad de fanáticos que miran ese partido necesitaría algo más
grande. La cantidad total de dinero gastado por todos los fanáticos en todos los partidos de fútbol en
todos los países de la Copa del Mundo necesitaría algo aún más grande. Al elegir el tamaño correcto, le
estás dando al compilador la mejor oportunidad para optimizar tu código, haciendo que tu aplicación se
ejecute más rápido o consuma menos recursos del sistema, o ambos.

Si necesitas números más grandes de lo que ofrecen los tipos primitivos, puedes echar un vistazo a las
clases BigInteger y BigDecimal en el paquete java.Math. Estas clases ofrecen un tamaño o precisión casi
infinitos. Algunas aplicaciones científicas o criptográficas requieren almacenar y manipular números muy
grandes (o muy pequeños) y valoran la precisión por encima del rendimiento. No cubriremos esas clases
en este libro, pero guarda sus nombres en tu mente para investigar en algún día lluvioso.

Precisión de punto flotante

Las operaciones de punto flotante en Java siguen la especificación internacional IEEE 754, lo que significa
que el resultado de los cálculos de punto flotante suele ser el mismo en diferentes plataformas Java. Sin
embargo, Java permite una precisión extendida en plataformas que la admiten. Esto puede introducir
diferencias extremadamente pequeñas y arcanas en los resultados de operaciones de alta precisión. La
mayoría de las aplicaciones nunca notarían esto, pero si quieres asegurarte de que tu aplicación produzca
exactamente los mismos resultados en diferentes plataformas, puedes usar la palabra clave especial
strictfp como modificador de clase en la clase que contiene la manipulación de punto flotante (cubrimos
las clases en el próximo capítulo). El compilador entonces prohíbe estas optimizaciones específicas de la
plataforma.

Declaración e inicialización de variables

Las variables se declaran dentro de métodos y clases con un nombre de tipo seguido de uno o más
nombres de variables separados por comas. Por ejemplo:

Las variables pueden ser opcionalmente inicializadas con una expresión del tipo apropiado al momento
de su declaración:
Las variables que se declaran como miembros de una clase se establecen en valores predeterminados si
no se inicializan (ver Capítulo 5). En este caso, los tipos numéricos se establecen en la variante apropiada
de cero, los caracteres se establecen en el carácter nulo (\0), y las variables booleanas tienen el valor false.
(Los tipos de referencia también obtienen un valor predeterminado, null, pero más adelante en "Tipos de
Referencia" en la página 95.) Por otro lado, las variables locales, que se declaran dentro de un método y
existen solo durante la duración de una llamada al método, deben inicializarse explícitamente antes de
poder ser utilizadas. Como veremos, el compilador hace cumplir esta regla para evitar cualquier riesgo de
olvido.

Literales enteros

Los literales enteros pueden especificarse en binario (base 2), octal (base 8), decimal (base 10) o
hexadecimal (base 16). Las bases binarias, octales y hexadecimales se utilizan principalmente al trabajar
con datos de archivos o redes a un nivel más bajo. Representan agrupaciones útiles de bits individuales:
1, 3 y 4 bits, respectivamente. Los valores decimales no tienen tal asignación, pero son mucho más
amigables para los humanos en la mayoría de la información numérica. Un entero decimal se especifica
mediante una secuencia de dígitos que comienza con uno de los caracteres del 1 al 9:

Un número binario se representa con los caracteres iniciales 0b o 0B (cero "b"), seguidos de una
combinación de ceros y unos:

Los números octales se distinguen de los números decimales por un simple cero inicial:

Un número hexadecimal se denota por los caracteres iniciales 0x o 0X (cero "x"), seguidos por una
combinación de dígitos y los caracteres a-f o A-F, que representan los valores decimales 10-15:

Los literales enteros son del tipo int a menos que tengan un sufijo L, indicando que deben ser tratados
como un valor long:

La letra minúscula "l" también es aceptable, pero se debe evitar porque a menudo se confunde con el
número 1.

Cuando se utiliza un tipo numérico en una asignación o una expresión que implica un tipo "mayor" con
un rango más grande, puede ser promovido al tipo más grande. En la segunda línea del ejemplo anterior,
el número 13 tiene el tipo predeterminado int, pero se promociona al tipo long para ser asignado a la
variable long. Ciertas operaciones numéricas y de comparación también causan este tipo de promoción
aritmética, al igual que las expresiones matemáticas que involucran más de un tipo. Por ejemplo, al
multiplicar un valor byte por un valor int, el compilador primero promociona el byte a un int:
Un valor numérico nunca puede ser asignado a un tipo con un rango más pequeño sin una conversión
explícita (casting), sin embargo:

Las conversiones de punto flotante a tipos enteros siempre requieren una conversión explícita debido a
la posible pérdida de precisión.

Finalmente, debemos mencionar que si estás utilizando Java 7 o una versión posterior, puedes agregar un
poco de formato a tus literales numéricos utilizando el carácter de guión bajo "_" entre dígitos. Entonces,
si tienes cadenas de dígitos particularmente largas, puedes dividirlas como en los siguientes ejemplos:

Los guiones bajos solo pueden aparecer entre dígitos, no al principio o al final de un número o junto al
identificador "L" de enteros largos. Prueba algunos números grandes en jshell. Observa que si intentas
almacenar un valor long sin el identificador, obtendrás un error. Puedes ver cómo el formato realmente
es solo para tu comodidad. No se almacena; solo se conserva el valor en tu variable o constante.

Prueba otros ejemplos. Puede ser útil para tener una idea de lo que es legible para ti. También puede
ayudar a entender los tipos de promociones y audiciones disponibles o requeridas. ¡Nada como tener
retroalimentación inmediata para aprender estas sutilezas!

Literales de punto flotante

Los valores de punto flotante pueden especificarse en notación decimal o científica. Los literales de punto
flotante son del tipo double a menos que tengan una f o F al final, indicando que deben producirse como
un valor float. Y al igual que con los literales enteros, en Java 7 puedes usar caracteres de guion bajo "_"
para formatear números de punto flotante, pero solo entre dígitos, no al principio, al final o junto al punto
decimal o al signo "F" del número.
Literales de caracteres

Un valor de carácter literal puede especificarse ya sea como un carácter entre comillas simples o como
una secuencia ASCII o Unicode escapada:

Tipos de Referencia

En un lenguaje orientado a objetos como Java, creas nuevos tipos de datos complejos a partir de primitivas
simples mediante la creación de una clase. Cada clase funciona como un nuevo tipo en el lenguaje. Por
ejemplo, si creamos una nueva clase llamada Foo en Java, implícitamente estamos creando un nuevo tipo
llamado Foo. El tipo de un elemento determina cómo se utiliza y dónde puede ser asignado. Al igual que
con las primitivas, un elemento de tipo Foo, en general, puede ser asignado a una variable de tipo Foo o
pasado como argumento a un método que acepte un valor de tipo Foo.

Un tipo no es solo un atributo simple. Las clases pueden tener relaciones con otras clases y lo mismo
ocurre con los tipos que representan. Todas las clases en Java existen en una jerarquía de padre-hijo,
donde una clase hija o subclase es un tipo especializado de su clase padre. Los tipos correspondientes
tienen la misma relación, donde el tipo de la clase hija se considera un subtipo de la clase padre. Debido
a que las clases hijas heredan toda la funcionalidad de sus clases padres, un objeto del tipo de la clase hija
es en cierto sentido equivalente o una extensión del tipo padre. Un objeto del tipo hijo se puede usar en
lugar de un objeto del tipo padre. Por ejemplo, si creas una nueva clase, Cat, que extiende Animal, el
nuevo tipo, Cat, se considera un subtipo de Animal. Los objetos de tipo Cat pueden usarse en cualquier
lugar donde se pueda usar un objeto de tipo Animal; se dice que un objeto de tipo Cat se puede asignar a
una variable de tipo Animal. Esto se denomina polimorfismo de subtipos y es una de las características
principales de un lenguaje orientado a objetos. Abordaremos más de cerca las clases y objetos en el
Capítulo 5.

Los tipos primitivos en Java se usan y pasan "por valor". En otras palabras, cuando se asigna un valor
primitivo como un int a una variable o se pasa como argumento a un método, simplemente se copia su
valor. Los tipos de referencia (tipos de clase), por otro lado, siempre se acceden "por referencia". Una
referencia es un identificador o un nombre para un objeto. Lo que una variable de un tipo de referencia
contiene es un "puntero" a un objeto de su tipo (o de un subtipo, como se describió anteriormente).
Cuando la referencia se asigna a una variable o se pasa a un método, solo se copia la referencia, no el
objeto al que apunta. Una referencia es como un puntero en C o C++, excepto que su tipo se aplica
estrictamente. El valor de la referencia en sí no se puede crear o cambiar explícitamente. Una variable
adquiere un valor de referencia solo mediante la asignación a un objeto apropiado.

Veamos un ejemplo. Declaramos una variable de tipo Foo, llamada myFoo, y le asignamos un objeto
apropiado:

La variable myFoo es de tipo de referencia y almacena una referencia al objeto Foo recién construido.
(Por ahora, no te preocupes por los detalles de cómo se crea un objeto; nuevamente, abordaremos eso
en el Capítulo 5). Declaramos una segunda variable de tipo Foo, llamada anotherFoo, y la asignamos al
mismo objeto. Ahora hay dos referencias idénticas: myFoo y anotherFoo, pero solo existe una instancia
real del objeto Foo. Si realizamos cambios en el estado del objeto Foo en sí, veremos el mismo efecto al
observarlo con cualquiera de las referencias. Podemos entender un poco más viendo lo que sucede detrás
de escena con jshell:
Observa el resultado de la creación y asignaciones. Aquí puedes ver que los tipos de referencia en Java
vienen con un valor de puntero (21213b92, el lado derecho de la @) y su tipo (Foo, el lado izquierdo de la
@). Cuando creamos un nuevo objeto Foo, notMyFoo, obtenemos un valor de puntero diferente. myFoo
y anotherFoo apuntan al mismo objeto; notMyFoo apunta a un segundo objeto separado.

Inferencia de Tipos

Las versiones modernas de Java han mejorado continuamente la capacidad de inferir tipos de variables
en muchas situaciones. Puedes utilizar la palabra clave var en conjunto con la declaración e inicialización
de una variable y permitir que el compilador infiera el tipo correcto:

Observa la salida (admitidamente poco atractiva) cuando creas myFoo3 en jshell. Aunque no
especificamos explícitamente el tipo como lo hicimos para myFoo2, el compilador puede entender
fácilmente el tipo correcto a utilizar y, de hecho, obtenemos un objeto Foo2.

Paso de Referencias

Las referencias de objetos se pasan a los métodos de la misma manera. En este caso, ya sea myFoo o
anotherFoo servirían como argumentos equivalentes:

Una distinción importante, pero a veces confusa en este punto es que la propia referencia es un valor y
ese valor se copia cuando se asigna a una variable o se pasa en una llamada a un método. Dado nuestro
ejemplo anterior, el argumento pasado a un método (una variable local desde el punto de vista del
método) es en realidad una tercera referencia al objeto Foo, además de myFoo y anotherFoo. El método
puede alterar el estado del objeto Foo a través de esa referencia (llamando a sus métodos o alterando
sus variables), pero no puede cambiar la noción de la referencia de myFoo del llamador: es decir, el
método no puede cambiar que myFoo del llamador apunte a un objeto Foo diferente; solo puede cambiar
su propia referencia. Esto será más evidente cuando hablemos sobre métodos más adelante.
Java difiere de C++ en este aspecto. Si necesitas cambiar la referencia de un objeto del llamador en Java,
necesitas un nivel adicional de indirección. El llamador tendría que envolver la referencia en otro objeto
para que ambos pudieran compartir la referencia a él.

Los tipos de referencia siempre apuntan a objetos (o a null), y los objetos siempre están definidos por
clases. Similar a los tipos nativos, las variables de instancia o de clase que no se inicializan explícitamente
al ser declaradas recibirán el valor predeterminado de null. Además, al igual que los tipos nativos, las
variables locales que tienen un tipo de referencia no se inicializan por defecto, por lo que debes establecer
tu propio valor antes de usarlas. Sin embargo, dos tipos especiales de tipos de referencia: arrays e
interfaces, especifican el tipo de objeto al que apuntan de una manera ligeramente diferente.

Los arrays en Java tienen un lugar especial en el sistema de tipos. Son un tipo especial de objeto creado
automáticamente para contener una colección de algún otro tipo de objeto, conocido como el tipo base.
Declarar una referencia de tipo array implícitamente crea el nuevo tipo de clase diseñado como un
contenedor para su tipo base, como verás más adelante en este capítulo.

Las interfaces son un poco más astutas. Una interfaz define un conjunto de métodos y le da un tipo
correspondiente. Un objeto que implementa los métodos de la interfaz puede ser referenciado por ese
tipo de interfaz, así como por su propio tipo. Las variables y los argumentos de método pueden declararse
como tipos de interfaz, al igual que otros tipos de clase, y cualquier objeto que implemente la interfaz
puede ser asignado a ellos. Esto agrega flexibilidad en el sistema de tipos y permite a Java cruzar las líneas
de la jerarquía de clases y crear objetos que efectivamente tienen muchos tipos. Cubriremos las interfaces
en el próximo capítulo también.

Los tipos genéricos o tipos parametrizados, como mencionamos anteriormente, son una extensión de la
sintaxis de clase de Java que permite una abstracción adicional en la forma en que las clases trabajan con
otros tipos de Java. Los genéricos permiten la especialización de clases por parte del usuario sin cambiar
ninguno de los códigos de la clase original. Cubriremos los genéricos en detalle en el Capítulo 7.

Una nota sobre las cadenas (Strings)

Las cadenas en Java son objetos; por lo tanto, son un tipo de referencia. Sin embargo, los objetos String
tienen cierta ayuda especial del compilador de Java que hace que se vean más como tipos primitivos. Los
valores literales de cadenas en el código fuente de Java se convierten en objetos String por el compilador.
Se pueden usar directamente, pasar como argumentos a métodos o asignarse a variables de tipo String:

El símbolo + en Java está "sobrecargado" para realizar la concatenación de cadenas de texto, así como
para realizar la suma numérica regular. Junto con su operador relacionado +=, estos son los únicos
operadores sobrecargados en Java:

Java construye un único objeto String a partir de las cadenas concatenadas y lo proporciona como el
resultado de la expresión. Discutimos la clase String y todo lo relacionado con el texto en detalle en el
Capítulo 8.

Sentencias y Expresiones

Las sentencias en Java aparecen dentro de métodos y clases; describen todas las actividades de un
programa en Java. Las declaraciones de variables y asignaciones, como las que se muestran en la sección
anterior, son sentencias, al igual que las estructuras básicas del lenguaje, como las condicionales if/then
y los bucles. (Más sobre estas estructuras más adelante en este capítulo).
Las expresiones producen valores; una expresión se evalúa para producir un resultado que se utilizará
como parte de otra expresión o en una sentencia. Las llamadas a métodos, las asignaciones de objetos y,
por supuesto, las expresiones matemáticas son ejemplos de expresiones.

Uno de los principios de Java es mantener las cosas simples y consistentes. Con este fin, cuando no hay
otras restricciones, las evaluaciones e inicializaciones en Java siempre ocurren en el orden en que
aparecen en el código, de izquierda a derecha, de arriba hacia abajo. Veremos esta regla utilizada en la
evaluación de expresiones de asignación, llamadas a métodos e índices de arrays, por nombrar algunos
casos. En algunos otros lenguajes, el orden de evaluación es más complicado o incluso depende de la
implementación. Java elimina este elemento de peligro al definir de manera precisa y simple cómo se
evalúa el código. Sin embargo, esto no significa que debas comenzar a escribir declaraciones oscuras y
complicadas. Depender del orden de evaluación de expresiones de manera compleja es un mal hábito de
programación, incluso cuando funciona. Produce un código difícil de leer y más difícil de modificar.

Sentencias

En cualquier programa, las sentencias realizan la verdadera magia. Las sentencias nos ayudan a
implementar esos algoritmos que mencionamos al principio de este capítulo. De hecho, no solo ayudan,
son precisamente el ingrediente de programación que usamos; cada paso en un algoritmo corresponderá
a una o más sentencias. En general, las sentencias hacen una de cuatro cosas: recopilan entradas para
asignarlas a una variable, escriben salidas (en tu terminal, en una etiqueta de Java, etc.), toman decisiones
sobre qué sentencias ejecutar, o repiten una o más otras sentencias. Veamos ejemplos de cada categoría
en Java.

Las sentencias y expresiones en Java aparecen dentro de un bloque de código. Un bloque de código es
sintácticamente una serie de sentencias rodeadas por llaves abiertas ({) y llaves cerradas (}). Las sentencias
en un bloque de código pueden incluir declaraciones de variables y la mayoría de los otros tipos de
sentencias y expresiones que mencionamos anteriormente:

Los métodos, que se asemejan a funciones en C, son en cierto sentido solo bloques de código que toman
parámetros y pueden ser invocados por sus nombres. Por ejemplo, el método setUpDog():

Las declaraciones de variables tienen un alcance limitado al bloque de código que las encierra, es decir,
no pueden ser vistas fuera del conjunto más cercano de llaves:
De esta manera, los bloques de código pueden usarse para agrupar arbitrariamente otras sentencias y
variables. Sin embargo, el uso más común de los bloques de código es definir un grupo de declaraciones
para usar en una sentencia condicional o iterativa.

Condiciones if/else

Uno de los conceptos clave en la programación es la noción de tomar una decisión. "Si este archivo
existe..." o "Si el usuario tiene una conexión WiFi..." son ejemplos de las decisiones que los programas de
computadora y las aplicaciones toman todo el tiempo. Podemos definir una cláusula if/else de la siguiente
manera:

Todo el ejemplo anterior es en sí una declaración y podría estar anidado dentro de otra cláusula if/else.
La cláusula if tiene la funcionalidad común de tomar dos formas diferentes: una "en una línea" o un
bloque. La forma de bloque es la siguiente:

La condición es una expresión booleana. Una expresión booleana es un valor verdadero o falso, o una
expresión que se evalúa como uno de esos valores. Por ejemplo, i == 0 es una expresión booleana que
verifica si el entero i tiene el valor 0.

En la segunda forma, las sentencias están en bloques de código, y todas sus sentencias incluidas se
ejecutan si se toma la rama correspondiente (if o else). Cualquier variable declarada dentro de cada
bloque solo es visible para las sentencias dentro del bloque. Al igual que la condición if/else, la mayoría
de las demás sentencias en Java se ocupan de controlar el flujo de ejecución. Actúan, en su mayor parte,
como sus homólogos en otros lenguajes.

Sentencias switch

Muchos lenguajes admiten una condición de "uno de muchos" comúnmente conocida como una
sentencia "switch" o "case". Dada una variable o expresión, una sentencia switch proporciona múltiples
opciones que podrían coincidir. La primera coincidencia gana, por lo que el orden es importante. Y
queremos decir "podrían". Un valor no tiene que coincidir con ninguna de las opciones de switch; en ese
caso, no ocurre nada.

La forma más común de la sentencia switch en Java toma un entero (o un argumento de tipo numérico
que puede ser automáticamente "promovido" a un tipo entero), un argumento de tipo cadena, o un tipo
"enum" (discutido en breve) y selecciona entre varios casos alternativos y constantes:
La expresión case para cada rama debe evaluarse a un valor constante entero o de cadena diferente en
tiempo de compilación. Las cadenas se comparan usando el método equals() de String, del cual
hablaremos con más detalle en el Capítulo 8. Se puede especificar un caso predeterminado opcional para
capturar condiciones no coincidentes. Cuando se ejecuta, el switch simplemente encuentra la rama que
coincide con su expresión condicional (o la rama predeterminada) y ejecuta la declaración
correspondiente. Pero eso no es el final de la historia. Quizás de manera contraintuitiva, la sentencia
switch continúa ejecutando ramas después de la rama coincidente hasta que llega al final del switch o a
una declaración especial llamada break. Aquí hay un par de ejemplos:

Usar break para terminar cada rama es más común:

En este ejemplo, se ejecuta solo una rama: GOOD, BAD o el caso predeterminado. El comportamiento de
"paso a través" del switch se justifica cuando deseas cubrir varios valores de caso posibles con la misma
declaración sin recurrir a un montón de declaraciones if/else:
Este ejemplo agrupa efectivamente los seis valores posibles en tres casos. Y esta función de agrupación
ahora puede aparecer directamente en expresiones. Java 12 ofrece una vista previa de una expresión
switch. Por ejemplo, en lugar de imprimir los nombres de los tamaños en el ejemplo anterior, podríamos
crear una nueva variable para el tamaño, de esta manera:

Observa cómo utilizamos la instrucción break con un valor esta vez. También puedes utilizar una nueva
sintaxis dentro de la sentencia switch para hacer las cosas un poco más compactas y tal vez más legibles:

Estas expresiones son obviamente nuevas en el lenguaje (Java 12 incluso requiere compilar con la bandera
--enable-preview para usarlas), por lo que es posible que no las encuentres usadas muy a menudo en los
recursos en línea y ejemplos que mencionamos anteriormente. Sin embargo, definitivamente encontrarás
buenos ejemplos dedicados a explicar el poder de las expresiones switch si esta declaración despierta tu
interés en las condiciones.

Bucles do/while

El otro concepto importante en el control de qué sentencia se ejecuta a continuación ("flujo de control"
en la jerga de los programadores) es la repetición. Las computadoras son realmente buenas haciendo
cosas una y otra vez. Repetir un bloque de código se hace con un bucle. Hay dos variedades principales
de bucles en Java. Las sentencias iterativas do y while se ejecutan mientras una expresión booleana
devuelve un valor verdadero:

Un bucle while es perfecto para esperar alguna condición externa, como la recepción de un correo
electrónico:

Por supuesto, el método wait() necesita tener un límite (normalmente un límite de tiempo, como esperar
durante un segundo) para que finalice y dé al bucle otra oportunidad de ejecutarse. Pero una vez que
tienes algunos correos electrónicos, también deseas procesar todos los mensajes que llegaron, no solo
uno. Una vez más, un bucle while es perfecto:

En este pequeño fragmento, utilizamos el operador booleano ! para negar la prueba anterior. Queremos
seguir trabajando mientras haya algo en la cola. Esa pregunta a menudo se expresa en programación
como "no está vacío" en lugar de "tiene algo". Además, observa que el cuerpo del bucle tiene más de una
instrucción, por lo que lo colocamos dentro de las llaves. Dentro de esas llaves, eliminamos el siguiente
mensaje de la cola y lo almacenamos en una variable local (message arriba). Luego hacemos algunas cosas
con nuestro mensaje y volvemos al condicional para ver si la cola está vacía todavía. Si no está vacía,
repetimos todo el proceso, comenzando con la toma del siguiente mensaje disponible.

A diferencia de los bucles while o for (que veremos a continuación) que prueban sus condiciones primero,
un bucle do-while (o más a menudo solo un bucle do) siempre ejecuta su cuerpo de instrucción al menos
una vez. Un ejemplo clásico es la validación de la entrada de un usuario o tal vez de un sitio web. Sabes
que necesitas obtener cierta información, así que solicita esa información en el cuerpo del bucle. La
condición del bucle puede probar los errores. Si hay un problema, el bucle comenzará de nuevo y solicitará
la información nuevamente. Ese proceso puede repetirse hasta que tu solicitud regrese sin un error y
sepas que tienes información válida.
El bucle for

La forma más general del bucle for también es un remanente del lenguaje C:

La sección de inicialización de variables puede declarar o inicializar variables que están limitadas al ámbito
del bucle for. Luego, el bucle for comienza una posible serie de rondas en las que primero se verifica la
condición y, si es verdadera, se ejecuta la sentencia (o bloque) del cuerpo. Después de cada ejecución del
cuerpo, se evalúan las expresiones del incremento para darles la oportunidad de actualizar las variables
antes de que comience la próxima ronda:

Este bucle se ejecutará 100 veces, imprimiendo valores del 0 al 99. Observa que la variable j es local al
bloque (visible solo para las sentencias dentro de él) y no será accesible para el código "después" del bucle
for. Si la condición de un bucle for devuelve falso en la primera verificación, el cuerpo y la sección del
incremento nunca se ejecutarán.

Puedes utilizar múltiples expresiones separadas por comas en las secciones de inicialización e incremento
del bucle for. Por ejemplo:

También puedes inicializar variables existentes desde fuera del alcance del bucle for dentro del bloque de
inicialización. Podrías hacer esto si quisieras utilizar el valor final de la variable del bucle en otro lugar,
pero en general, esta práctica no se recomienda ya que es propensa a errores; puede hacer que tu código
sea difícil de entender. No obstante, es legal y podrías encontrarte en una situación donde este
comportamiento tenga más sentido para ti.

El "bucle for mejorado" de Java, denominado auspiciosamente, funciona como la declaración foreach en
algunos otros lenguajes, iterando sobre una serie de valores en un array u otro tipo de colección:

El bucle for mejorado se puede utilizar para recorrer arrays de cualquier tipo, así como cualquier tipo de
objeto Java que implemente la interfaz java.lang.Iterable. Esto incluye la mayoría de las clases del API de
Colecciones de Java. Hablaremos sobre arrays en este y el próximo capítulo; el Capítulo 7 cubre las
Colecciones de Java. Aquí tienes un par de ejemplos:
Nuevamente, no hemos discutido arrays o la clase List y su sintaxis especial en este ejemplo. Lo que
estamos mostrando aquí es el bucle for mejorado iterando sobre un array de enteros y también sobre
una lista de valores de tipo string. En el segundo caso, List implementa la interfaz Iterable y, por lo tanto,
puede ser un objetivo del bucle for.

break/continue

La sentencia 'break' de Java y su compañero 'continue' también se pueden utilizar para interrumpir un
bucle o una declaración condicional saliendo de él. Un 'break' hace que Java detenga el bucle actual (o
interruptor) y reanude la ejecución después de él. En el siguiente ejemplo, el bucle while continúa
indefinidamente hasta que el método condition() devuelve true, lo que desencadena una sentencia
'break' que detiene el bucle y continúa en el punto marcado como "después del while":

Una declaración 'continue' hace que los bucles 'for' y 'while' pasen a su siguiente iteración al regresar al
punto donde verifican su condición. El siguiente ejemplo imprime los números del 0 al 99, omitiendo el
número 33:

Las sentencias 'break' y 'continue' se parecen a las de lenguaje C, pero las formas en Java tienen la
capacidad adicional de tomar una etiqueta como argumento y salir de múltiples niveles hasta el ámbito
del punto etiquetado en el código. Este uso no es muy común en la programación diaria en Java, pero
puede ser importante en casos especiales. Aquí tienes un esquema:
Las sentencias de encerramiento, como bloques de código, condicionales y bucles, pueden ser
etiquetadas con identificadores como labelOne y labelTwo. En este ejemplo, un break o continue sin
argumentos en la posición indicada tiene el mismo efecto que en los ejemplos anteriores. Un break hace
que el procesamiento se reanude en el punto etiquetado como "después de labelTwo"; un continue hace
que el bucle labelTwo vuelva inmediatamente a su prueba de condición.

La declaración break labelTwo en el punto indicado tiene el mismo efecto que un break ordinario, pero
break labelOne rompe ambos niveles y se reanuda en el punto etiquetado como "después de labelOne".
De manera similar, continue labelTwo sirve como un continue normal, pero continue labelOne regresa a
la prueba del bucle labelOne. Las sentencias break y continue de varios niveles eliminan la principal
justificación para la malévola sentencia goto en C/C++.

Hay algunas sentencias de Java que no vamos a discutir en este momento. Las sentencias try, catch y
finally se utilizan en el manejo de excepciones, como discutiremos en el Capítulo 6. La sentencia
synchronized en Java se utiliza para coordinar el acceso a declaraciones entre múltiples hilos de ejecución;
consulta el Capítulo 9 para una discusión sobre sincronización de hilos.

Sentencias inalcanzables

Como nota final, debemos mencionar que el compilador de Java marca como errores de tiempo de
compilación las "sentencias inalcanzables". Una sentencia inalcanzable es aquella que el compilador
determina que no será llamada en absoluto. Por supuesto, muchos métodos pueden que nunca sean
llamados en tu código, pero el compilador detecta solo aquellos que puede "demostrar" que nunca son
llamados mediante una simple verificación en tiempo de compilación. Por ejemplo, un método con una
sentencia de retorno incondicional en su mitad provoca un error en tiempo de compilación, al igual que
un método con una condición que el compilador puede determinar que nunca se cumplirá:

Expresiones

Las expresiones producen un resultado o valor cuando son evaluadas. El valor de una expresión puede ser
un tipo numérico, como en una expresión aritmética; un tipo de referencia, como en una asignación de
objeto; o el tipo especial void, que es el tipo declarado de un método que no devuelve un valor. En este
último caso, la expresión se evalúa únicamente por sus efectos secundarios; es decir, el trabajo que realiza
además de producir un valor. El tipo de una expresión se conoce en tiempo de compilación. El valor
producido en tiempo de ejecución es de este tipo o, en el caso de un tipo de referencia, un subtipo
compatible (asignable).

Ya hemos visto varias expresiones en nuestros programas de ejemplo y fragmentos de código. También
veremos muchos más ejemplos de expresiones en la sección "Asignación" en la página 110.

Operadores

Los operadores te ayudan a combinar o alterar expresiones de varias formas. Ellos "operan" expresiones.
Java soporta casi todos los operadores estándar del lenguaje C. Estos operadores también tienen la misma
precedencia en Java que en C, como se muestra en la Tabla 4-3.
También debemos señalar que el operador porcentaje (%) no es estrictamente un módulo, sino un
residuo, y puede tener un valor negativo. Intenta jugar con algunos de estos operadores en jshell para
tener una mejor idea de sus efectos. Si eres nuevo en la programación, es especialmente útil familiarizarse
con los operadores y su orden de precedencia. Regularmente te encontrarás con expresiones y
operadores incluso al realizar tareas cotidianas en tu código.
Java también añade algunos operadores nuevos. Como hemos visto, el operador + puede ser utilizado con
valores de tipo String para realizar la concatenación de cadenas. Debido a que todos los tipos integrales
en Java son valores firmados, el operador >> puede ser utilizado para llevar a cabo una operación de
desplazamiento aritmético hacia la derecha con extensión de signo. El operador >>> trata el operando
como un número sin signo y realiza un desplazamiento aritmético hacia la derecha sin extensión de signo.
No manipulamos los bits individuales en nuestras variables tanto como solíamos hacerlo, por lo que es
probable que no encuentres esos operadores de desplazamiento con mucha frecuencia. Si aparecen en
algún código que leas en línea, siéntete libre de usar jshell para ver cómo funcionan o averiguar qué está
haciendo exactamente el código de ejemplo. (¡Esta es una de nuestras formas favoritas de utilizar jshell!)
El operador new se utiliza para crear objetos; lo discutiremos en detalle en breve.

Asignación

Si bien la inicialización de variables (es decir, la declaración y asignación juntas) se considera una
declaración sin valor resultante, la asignación de variables por sí sola es una expresión:

Normalmente, confiamos en la asignación por sus efectos secundarios solamente, pero una asignación
puede ser utilizada como un valor en otra parte de una expresión:

Nuevamente, depender en gran medida del orden de evaluación (en este caso, usando asignaciones
compuestas en expresiones complejas) puede hacer que el código sea confuso y difícil de leer.

El valor null

La expresión null puede asignarse a cualquier tipo de referencia. Significa "ninguna referencia". Una
referencia nula no puede usarse para referenciar nada y cualquier intento de hacerlo genera una
NullPointerException en tiempo de ejecución. Recuerda de "Tipos de referencia" en la página 95 que null
es el valor predeterminado asignado a variables de clase e instancia no inicializadas; asegúrate de realizar
tus inicializaciones antes de utilizar variables de tipo de referencia para evitar esa excepción.

Acceso a variables

El operador punto (.) se utiliza para seleccionar miembros de una clase o instancia de objeto. (Hablaremos
de estos en detalle en los capítulos siguientes). Puede recuperar el valor de una variable de instancia (de
un objeto) o una variable estática (de una clase). También puede especificar un método a invocar en un
objeto o clase:

Una expresión de tipo de referencia puede ser utilizada en evaluaciones compuestas al seleccionar más
variables o métodos sobre el resultado:

Aquí hemos encontrado la longitud de nuestra variable 'name' invocando el método length() del objeto
String. En el segundo caso, tomamos un paso intermedio y solicitamos una subcadena de la cadena
'name'. El método substring de la clase String también devuelve una referencia String, para la cual
pedimos la longitud. Combinar operaciones de esta manera también se llama encadenar llamadas a
métodos, sobre lo cual hablaremos más adelante. Una operación de selección encadenada que ya hemos
utilizado mucho es llamar al método println() en la variable 'out' de la clase System:

Invocación de método

Los métodos son funciones que residen dentro de una clase y pueden ser accesibles a través de la clase o
sus instancias, dependiendo del tipo de método. Invocar un método significa ejecutar sus declaraciones
de cuerpo, pasando cualquier variable de parámetro requerida y posiblemente obteniendo un valor a
cambio. Una invocación de método es una expresión que resulta en un valor. El tipo del valor es el tipo
de retorno del método:

Aquí, invocamos los métodos println() y length() en diferentes objetos. El método length() devolvió un
valor entero; el tipo de retorno de println() es void (sin valor). Vale la pena enfatizar que println() produce
salida pero no un valor. No podemos asignar ese método a una variable como hicimos anteriormente con
length().

Los métodos constituyen la mayor parte de un programa Java. Aunque podrías escribir algunas
aplicaciones triviales que existan completamente dentro de un único método main() de una clase,
rápidamente descubrirás que necesitas descomponer las cosas. Los métodos no solo hacen que tu
aplicación sea más legible, sino que también abren las puertas a aplicaciones complejas, interesantes y
útiles que simplemente no son posibles sin ellos. De hecho, echa un vistazo a nuestras aplicaciones
gráficas de Hello World en "HelloJava" en la página 41. Utilizamos varios métodos definidos para la clase
JFrame.

Estos son ejemplos simples, pero en el Capítulo 5 veremos que se vuelve un poco más complejo cuando
hay métodos con el mismo nombre pero diferentes tipos de parámetros en la misma clase o cuando un
método es redefinido en una clase hija.

Declaraciones, expresiones y algoritmos

Vamos a ensamblar una colección de declaraciones y expresiones de estos diferentes tipos para lograr un
objetivo real. En otras palabras, vamos a escribir código Java para implementar un algoritmo. Un ejemplo
clásico de un algoritmo es el proceso de Euclides para encontrar el máximo común denominador de dos
números utilizando un proceso simple (aunque tedioso) de sustracción repetida. Podemos usar el bucle
while de Java, condicionales if/else y algunas asignaciones para hacer el trabajo:
No es sofisticado, pero funciona y es exactamente el tipo de tarea que un programa informático es
excelente para realizar. ¡Esto es para lo que estás aquí! Bueno, probablemente no estás aquí para el
máximo común divisor de 2701 y 222 (que, por cierto, es 37), pero estás aquí para empezar a formular
soluciones a problemas como algoritmos y luego traducir esos algoritmos en código Java ejecutable a su
vez. Con suerte, algunas piezas más del rompecabezas de la programación están empezando a encajar.
Pero no te preocupes si estas ideas aún no están claras. Todo este proceso de codificación requiere mucha
práctica. Intenta colocar ese bloque de código anterior en una clase Java real dentro del método main().
Intenta cambiar los valores de a y b. En el Capítulo 8 veremos cómo convertir cadenas de texto en números
para que puedas encontrar el MCD simplemente ejecutando el programa nuevamente, pasando dos
números como parámetros al método main(), como se muestra en la Figura 2-9, sin necesidad de
recompilar.

Creación de objetos

Los objetos en Java se asignan con el operador new:

El argumento para `new` es el constructor de la clase. El constructor es un método que siempre tiene el
mismo nombre que la clase. El constructor especifica cualquier parámetro requerido para crear una
instancia del objeto. El valor de la expresión `new` es una referencia del tipo del objeto creado. Los objetos
siempre tienen uno o más constructores, aunque no siempre pueden ser accesibles para ti.

Exploramos la creación de objetos en detalle en el Capítulo 5. Por ahora, solo ten en cuenta que la creación
de objetos es un tipo de expresión y que el resultado es una referencia de objeto. Una pequeña
peculiaridad es que la vinculación de `new` es "más estrecha" que la del selector de punto (`.`). Entonces,
puedes crear un nuevo objeto e invocar un método en él sin asignar el objeto a una variable de tipo
referencia si tienes alguna razón para hacerlo:

La clase Date es una clase de utilidad que representa la hora actual. Aquí creamos una nueva instancia de
Date con el operador `new` y llamamos a su método `getHours()` para obtener la hora actual como un
valor entero. La referencia del objeto Date vive el tiempo suficiente para atender la llamada al método y
luego se libera y se recolecta como basura en algún momento futuro (consulta "Recolección de basura"
en la página 148 para obtener más información sobre la recolección de basura).

Llamar a métodos en referencias de objetos de esta manera es, nuevamente, una cuestión de estilo. Sería
más claro asignar una variable intermedia de tipo Date para contener el nuevo objeto y luego llamar a su
método `getHours()`. Sin embargo, combinar operaciones de esta manera es común. A medida que
aprendas Java y te familiarices con sus clases y tipos, probablemente adoptarás algunos de estos patrones.
Hasta ese momento, no te preocupes por ser "verboso" en tu código. La claridad y la legibilidad son más
importantes mientras trabajas a través de este libro.
El operador `instanceof`

El operador `instanceof` se puede utilizar para determinar el tipo de un objeto en tiempo de ejecución.
Prueba si un objeto es del mismo tipo o un subtipo del tipo objetivo. (¡De nuevo, más sobre esta jerarquía
de clases por venir!) Esto es lo mismo que preguntar si el objeto se puede asignar a una variable del tipo
objetivo. El tipo objetivo puede ser una clase, interfaz o tipo de array, como veremos más adelante.
`instanceof` devuelve un valor booleano que indica si el objeto coincide con el tipo:

`instanceof` también informa correctamente si el objeto es del tipo de un array o una interfaz específica
(como discutiremos más adelante):

También es importante tener en cuenta que el valor `null` no se considera una instancia de ninguna clase.
La siguiente prueba devuelve false, sin importar cuál sea el tipo declarado de la variable:

Arrays

Un array es un tipo especial de objeto que puede contener una colección ordenada de elementos. El tipo
de los elementos del array se llama el tipo base del array; la cantidad de elementos que contiene es un
atributo fijo llamado su longitud. Java admite arrays de todos los tipos primitivos y de referencia.

Si has programado en C o C++, la sintaxis básica de los arrays se ve similar. Creamos un array de una
longitud especificada y accedemos a los elementos con el operador de índice, []. Sin embargo, a diferencia
de otros lenguajes, los arrays en Java son objetos verdaderos de primera clase. Un array es una instancia
de una clase de array especial en Java y tiene un tipo correspondiente en el sistema de tipos. Esto significa
que, para usar un array, al igual que con cualquier otro objeto, primero declaramos una variable del tipo
apropiado y luego usamos el operador new para crear una instancia de él.

Los objetos de array difieren de otros objetos en Java en tres aspectos:

• Java crea implícitamente un tipo especial de clase de array para nosotros siempre que declaremos un
nuevo tipo de array. No es estrictamente necesario conocer este proceso para usar arrays, pero ayuda a
entender su estructura y su relación con otros objetos en Java más adelante.

• Java nos permite usar el operador [] para acceder a los elementos del array para que los arrays se vean
como esperamos. Podríamos implementar nuestras propias clases que actúen como arrays, pero
tendríamos que conformarnos con tener métodos como get() y set() en lugar de usar la notación especial
[].

• Java proporciona una forma especial correspondiente del operador new que nos permite construir una
instancia de un array con una longitud especificada con la notación [], o inicializarlo directamente desde
una lista estructurada de valores.
Tipos de Arrays

Una variable de tipo array se denota por un tipo base seguido de corchetes vacíos, []. Alternativamente,
Java acepta una declaración al estilo de C con los corchetes colocados después del nombre del array.

Lo siguiente es equivalente:

En cada caso, arrayOfInts se declara como un array de enteros. El tamaño del array aún no es un problema
porque estamos declarando solo la variable de tipo array. Aún no hemos creado una instancia real de la
clase de array, con su almacenamiento asociado. Ni siquiera es posible especificar la longitud de un array
al declarar una variable de tipo array. El tamaño es estrictamente una función del objeto de array en sí,
no de la referencia a él.

Un array de tipos de referencia se puede crear de la misma manera:

Creación e inicialización de matrices

El operador nuevo se utiliza para crear una instancia de una matriz. Después del operador nuevo,
especificamos el tipo base de la matriz y su longitud con una expresión entera entre corchetes:

Por supuesto, podemos combinar los pasos de declarar y asignar memoria a la matriz:

Los índices de los arrays comienzan en cero. Por lo tanto, el primer elemento de someNumbers[] es 0 y el
último elemento es 19. Después de su creación, los elementos del array se inicializan con los valores por
defecto de su tipo. Para tipos numéricos, esto significa que los elementos se inicializan inicialmente en
cero:

Los elementos de un array de objetos son referencias a los objetos, al igual que las variables individuales
a las que apuntan, pero no contienen realmente instancias de los objetos. El equivalente en C o C++ sería
un array de punteros a objetos. Sin embargo, los punteros en C o C++ son en sí mismos valores de dos o
cuatro bytes. Asignar un array de punteros implica, en realidad, asignar el almacenamiento para algún
número de esos objetos puntero. Un array de referencias es conceptualmente similar, aunque las
referencias en sí no son objetos. No podemos manipular referencias o partes de referencias, excepto
mediante asignación, y sus requisitos de almacenamiento (o la falta de ellos) no son parte de la
especificación del lenguaje Java de alto nivel.

El valor predeterminado de cada elemento es, por lo tanto, null hasta que asignemos instancias de objetos
apropiados:
Esta es una distinción importante que puede causar confusión. En muchos otros lenguajes, el acto de crear
un array es equivalente a asignar almacenamiento para sus elementos. En Java, un array recién asignado
de objetos contiene en realidad solo variables de referencia, cada una con el valor null. Esto no quiere
decir que no haya memoria asociada con un array vacío; se necesita memoria para contener esas
referencias (los "espacios" vacíos en el array). La Figura 4-3 ilustra el array de nombres del ejemplo
anterior.

`names` es una variable de tipo String[] (es decir, un array de cadenas). Este objeto específico de tipo
String[] contiene cuatro variables de tipo String. Hemos asignado objetos de tipo String a los tres primeros
elementos del array. El cuarto elemento tiene el valor predeterminado null.

Java admite la construcción de estilo C con llaves {} para crear un array e inicializar sus elementos:

Se crea implícitamente un objeto de tipo array con la longitud adecuada, y los valores de la lista de
expresiones separadas por comas se asignan a sus elementos. Es importante notar que no hemos utilizado
la palabra clave new ni el tipo de array aquí. El tipo del array fue inferido a partir de la asignación.

Podemos utilizar la sintaxis {} con un array de objetos. En este caso, cada expresión debe evaluar a un
objeto que pueda ser asignado a una variable del tipo base del array o al valor null. Aquí tienes algunos
ejemplos:

Lo siguiente son equivalentes:


Utilizando Arrays

El tamaño de un objeto de array está disponible en la variable pública length:

`length` es el único campo accesible de un array; es una variable, no un método. (No te preocupes; el
compilador te avisa cuando accidentalmente utilizas paréntesis como si fuera un método, como hace todo
el mundo de vez en cuando).

El acceso a un array en Java es igual que en otros lenguajes; accedes a un elemento colocando una
expresión con valor entero entre corchetes después del nombre del array.

El siguiente ejemplo crea un array de objetos Button llamado keyPad y luego llena el array con objetos
Button:

Recuerda que también podemos utilizar el bucle for mejorado (enhanced for loop) para iterar sobre los
valores de un array. Aquí lo usaremos para imprimir todos los valores que acabamos de asignar:

Intentar acceder a un elemento que está fuera del rango del array genera un
ArrayIndexOutOfBoundsException. Este es un tipo de RuntimeException, por lo que puedes capturarlo y
manejarlo tú mismo si realmente lo esperas, o simplemente ignorarlo, como discutiremos en el Capítulo
6. Aquí tienes un ejemplo de la sintaxis try/catch que Java utiliza para envolver dicho código
potencialmente problemático:

Es una tarea común copiar un rango de elementos de un array a otro. Una forma de copiar arrays es
utilizando el método de bajo nivel arraycopy() de la clase System:

El siguiente ejemplo duplica el tamaño del array "names" de un ejemplo anterior:


Se asigna un nuevo array, con el doble del tamaño del array "names", a una variable temporal, tmpVar.
Luego, se utiliza el método arraycopy() para copiar los elementos de "names" al nuevo array. Finalmente,
el nuevo array se asigna a "names". Si no quedan referencias al antiguo objeto de array después de que
se haya copiado "names", será recolectado por el recolector de basura (garbage collector) en el siguiente
ciclo.

Una manera más sencilla es utilizar los métodos ArrayscopyOf() y copyOfRange() de java.util.Arrays:

El método copyOf() toma el array original y una longitud deseada. Si la longitud deseada es mayor que la
longitud del array original, entonces el nuevo array se rellena (con ceros o nulls) hasta alcanzar la longitud
deseada. Por otro lado, copyOfRange() recibe un índice de inicio (inclusive) y un índice de fin (exclusivo),
y una longitud deseada, la cual también se rellenará si es necesario.

Arrays Anónimos

A menudo resulta conveniente crear arrays "desechables", es decir, arrays que se utilizan en un único
lugar y no se hacen referencia en ningún otro lugar. Estos arrays no necesitan un nombre porque nunca
se necesita referenciarlos nuevamente en ese contexto. Por ejemplo, es posible que desees crear una
colección de objetos para pasarla como argumento a algún método. Es bastante fácil crear un array
normal con nombre, pero si en realidad no trabajas con el array (si solo usas el array como contenedor
para alguna colección), no deberías necesitar hacerlo. Java facilita la creación de arrays "anónimos" (es
decir, sin nombre).

Digamos que necesitas llamar a un método llamado setPets(), que toma un array de objetos Animal como
argumentos. Suponiendo que Cat y Dog son subclases de Animal, así es cómo llamarías a setPets() usando
un array anónimo:

La sintaxis se parece a la inicialización de un array en la declaración de una variable. Definimos


implícitamente el tamaño del array y llenamos sus elementos utilizando la notación de llaves {}. Sin
embargo, debido a que esto no es una declaración de variable, tenemos que usar explícitamente el
operador new y el tipo de array para crear el objeto array.

Los arrays anónimos a veces se utilizaban como sustitutos de listas de argumentos de longitud variable
para métodos. Quizás familiar para programadores en C, una lista de argumentos de longitud variable te
permite enviar una cantidad arbitraria de datos a un método. Un ejemplo podría ser un método que
calcula un promedio de un conjunto de números. Podrías colocar todos los números en un array, o podrías
permitir que tu método acepte uno, dos, tres o muchos números como argumentos. Con la introducción
de listas de argumentos de longitud variable en Java, la utilidad de los arrays anónimos ha disminuido.
Arrays Multidimensionales

Java soporta arrays multidimensionales en forma de arrays de objetos tipo array. Creas un array
multidimensional con una sintaxis similar a C, usando múltiples pares de corchetes, uno para cada
dimensión. También utilizas esta sintaxis para acceder a elementos en diversas posiciones dentro del
array. Aquí tienes un ejemplo de un array multidimensional que representa un tablero de ajedrez:

Aquí, chessBoard se declara como una variable de tipo ChessPiece[][] (es decir, un array de arrays de
ChessPiece). Esta declaración crea implícitamente el tipo ChessPiece[] también.

El ejemplo ilustra la forma especial del operador new utilizado para crear un array multidimensional. Crea
un array de objetos ChessPiece[] y luego, a su vez, convierte cada elemento en un array de objetos
ChessPiece. Luego, indexamos chessBoard para especificar valores para elementos específicos de
ChessPiece. (Aquí omitiremos el color de las piezas).

Por supuesto, puedes crear arrays con más de dos dimensiones. Aquí tienes un ejemplo ligeramente
impráctico:

Podemos especificar un índice parcial de un array multidimensional para obtener un subarray de objetos
de tipo array con menos dimensiones. En nuestro ejemplo, la variable chessBoard es de tipo
ChessPiece[][]. La expresión chessBoard[0] es válida y se refiere al primer elemento de chessBoard, que,
en Java, es de tipo ChessPiece[]. Por ejemplo, podemos llenar nuestro tablero de ajedrez una fila a la vez:

No es necesario especificar los tamaños de las dimensiones de un array multidimensional con una única
operación new. La sintaxis del operador new nos permite dejar sin especificar los tamaños de algunas
dimensiones. Se debe especificar el tamaño al menos de la primera dimensión (la dimensión más
significativa del array), pero los tamaños de cualquier cantidad de dimensiones menos significativas y
posteriores pueden dejarse indefinidos. Podemos asignar valores apropiados de tipo array más adelante.

Podemos crear un tablero de ajedrez de valores booleanos (lo cual no es suficiente para un juego real de
damas) utilizando esta técnica:

Aquí, se declara y crea `checkerBoard`, pero sus elementos, los ocho objetos boolean[] del siguiente nivel,
se dejan vacíos. Por lo tanto, por ejemplo, `checkerBoard[0]` es nulo hasta que creamos explícitamente
un array y lo asignamos, como sigue:
El código de los dos ejemplos anteriores es equivalente a:

Una razón por la que podríamos querer dejar dimensiones de un array sin especificar es para poder
almacenar arrays que nos son proporcionados por otro método.

Es importante tener en cuenta que debido a que la longitud del array no es parte de su tipo, los arrays en
el tablero de ajedrez no necesariamente tienen que ser de la misma longitud; es decir, los arrays
multidimensionales no tienen que ser rectangulares. Aquí hay un tablero de ajedrez defectuoso (pero
perfectamente legal en Java):

Y aquí está cómo podrías crear e inicializar un array triangular:

Tipos y Clases y Arrays, ¡Oh Dios mío!

Java cuenta con una amplia variedad de tipos para almacenar información, cada uno con su propia manera
de representar bits literales de esa información. Con el tiempo, adquirirás familiaridad y comodidad con
enteros (ints), números con decimales (doubles), caracteres (chars) y cadenas de texto (Strings). Pero no
te apresures, estos bloques de construcción fundamentales son exactamente el tipo de cosas para las que
jshell fue diseñado para ayudarte a explorar. Siempre vale la pena tomarse un momento para verificar tu
comprensión de lo que una variable puede almacenar. Los arrays, en particular, podrían beneficiarse de
un poco de experimentación. Puedes probar las diferentes técnicas de declaración y confirmar que tienes
un dominio sobre cómo acceder a los elementos individuales dentro de estructuras unidimensionales y
multidimensionales.

También puedes jugar con declaraciones simples de flujo de control en jshell, como nuestras
declaraciones de ramificación 'if' y bucles 'while'. Requiere un poco de paciencia escribir de vez en cuando
un fragmento multilínea, pero no podemos subestimar lo útil que es jugar y practicar de esta manera a
medida que cargas más y más detalles de Java en tu cerebro. Los lenguajes de programación ciertamente
no son tan complejos como los lenguajes humanos, pero aún tienen muchas similitudes. Puedes adquirir
una alfabetización en Java al igual que lo has hecho en inglés (o en el idioma que estás usando para leer
este libro si tienes una traducción). Comenzarás a comprender qué se supone que debe hacer el código
incluso si no entiendes de inmediato los detalles particulares.

Y algunas partes de Java, como los arrays, están definitivamente llenas de detalles particulares. Notamos
anteriormente que los arrays son instancias de clases de arrays especiales en el lenguaje Java. Si los arrays
tienen clases, ¿dónde encajan en la jerarquía de clases y cómo están relacionados? Estas son buenas
preguntas, pero necesitamos hablar más sobre los aspectos orientados a objetos de Java antes de
responderlas. Ese es el tema del próximo capítulo. Por ahora, tómalo como un hecho que los arrays
encajan en la jerarquía de clases.
CAPÍTULO 5

Objetos en Java

En este capítulo, llegamos al corazón de Java y exploramos los aspectos orientados a objetos del lenguaje.
El término diseño orientado a objetos se refiere al arte de descomponer una aplicación en un cierto
número de objetos, que son componentes de aplicación autónomos que trabajan juntos. El objetivo es
descomponer tu problema en una serie de problemas más pequeños que sean más simples y fáciles de
manejar y mantener. Los diseños basados en objetos se han demostrado a lo largo de los años, y los
lenguajes orientados a objetos como Java proporcionan una base sólida para escribir aplicaciones, desde
las más pequeñas hasta las más grandes. Java fue diseñado desde cero para ser un lenguaje orientado a
objetos, y todas las APIs y bibliotecas de Java están construidas en torno a patrones de diseño basados en
objetos sólidos.

Una "metodología" de diseño de objetos es un sistema o un conjunto de reglas creadas para ayudarte a
descomponer tu aplicación en objetos. A menudo, esto significa mapear entidades y conceptos del mundo
real (a veces llamado el "dominio del problema") en componentes de aplicación. Varias metodologías
intentan ayudarte a factorizar tu aplicación en un buen conjunto de objetos reutilizables. Esto es bueno
en principio, pero el problema es que un buen diseño orientado a objetos sigue siendo más arte que
ciencia. Si bien puedes aprender de las diversas metodologías de diseño disponibles, ninguna te ayudará
en todas las situaciones. La verdad es que no hay sustituto para la experiencia.

Aquí no intentaremos empujarte hacia una metodología particular; hay estanterías llenas de libros para
hacer eso. En cambio, proporcionaremos algunos consejos de sentido común a medida que comiences.

Clases

Las clases son los bloques de construcción de una aplicación Java. Una clase puede contener métodos
(funciones), variables, código de inicialización y, como discutiremos más adelante, otras clases. Clases
separadas que describen partes individuales de una idea más compleja a menudo se agrupan en paquetes,
lo que te ayuda a organizar proyectos más grandes. (Cada clase pertenece a algún paquete, incluso los
ejemplos simples que hemos visto hasta ahora). Una interfaz puede describir ciertas similitudes
específicas entre clases de otro modo dispares. Las clases pueden estar relacionadas entre sí por extensión
o a interfaces por implementación. La Figura 5-1 ilustra las ideas en este párrafo tan denso.
En esta figura, puedes ver la clase Object en la esquina superior izquierda. Object es la clase fundamental
en el corazón de todas las demás clases en Java. Forma parte del paquete principal de Java, java.lang. Java
también tiene un paquete para sus elementos de interfaz gráfica de usuario llamado javax.swing. Dentro
de ese paquete, la clase JComponent define todas las propiedades comunes a bajo nivel de elementos
gráficos como marcos (frames), botones y lienzos (canvases). Por ejemplo, la clase JLabel extiende la clase
JComponent. Esto significa que JLabel hereda detalles de JComponent pero añade características
específicas de etiquetas. Es posible que hayas notado que JComponent en sí misma se extiende de Object,
o al menos, eventualmente se extiende hasta Object. Por razones de brevedad, hemos omitido las clases
y paquetes intermedios que hay entre ellos.

También podemos definir nuestras propias clases y paquetes. El paquete ch05 en la esquina inferior
derecha es un paquete personalizado que hemos creado. En él, tenemos nuestras clases de juego como
Apple y Field. También puedes ver la interfaz GamePiece que contendrá algunos elementos comunes
requeridos para todas las piezas del juego y que es implementada por las clases Apple, Tree y Physicist.
(En nuestro juego, la clase Field es donde se mostrarán todas las piezas del juego, pero no es una pieza de
juego en sí misma. Observa que no implementa la interfaz GamePiece).

Abordaremos mucho más en detalle con más ejemplos de cada concepto a medida que avances en este
capítulo. Es importante probar los ejemplos a medida que avanzas y utilizar la herramienta jshell discutida
en "Probando Java" en la página 70 para ayudar a afianzar tu comprensión de los nuevos temas.

Declaración e Instanciación de Clases

Una clase sirve como un plano para crear instancias, que son objetos en tiempo de ejecución (copias
individuales) que implementan la estructura de la clase. Declaras una clase con la palabra clave class y un
nombre de tu elección. Por ejemplo, nuestro juego permite que los físicos lancen manzanas a los árboles.
Cada uno de los sustantivos en esa oración es un buen objetivo para convertirse en clases. Dentro de una
clase, añadimos variables que almacenan detalles u otra información útil, y métodos que describen qué
podemos hacer con las instancias de esta clase.

Comencemos con una clase para nuestras manzanas. Por (fuerte) convención, los nombres de las clases
comienzan con letras mayúsculas. Eso hace que la palabra "Apple" sea un buen nombre para usar. No
intentaremos incluir todo lo que necesitamos saber sobre nuestras manzanas de juego en la clase de
inmediato, solo algunos elementos para ayudar a ilustrar cómo se relacionan una clase, variables y
métodos.

La clase Apple contiene cuatro variables: mass (masa), diameter (diámetro), x e y. También define un
método llamado isTouching(), que toma como argumento una referencia a otra instancia de Apple y
devuelve un valor booleano como resultado. Las declaraciones de variables y métodos pueden aparecer
en cualquier orden, pero los inicializadores de variables no pueden hacer "referencias anticipadas" a otras
variables que aparecen más adelante. (En nuestro pequeño fragmento, la variable diameter podría usar
la variable mass para ayudar a calcular su valor inicial, pero mass no podría usar la variable diameter para
hacer lo mismo). Una vez que hemos definido la clase Apple, podemos crear un objeto Apple (una
instancia de esa clase) de la siguiente manera:
Recuerda que nuestra declaración de la variable a1 no crea un objeto Apple; simplemente crea una
variable que se refiere a un objeto del tipo Apple. Todavía tenemos que crear el objeto, utilizando la
palabra clave new, como se muestra en la segunda línea del fragmento de código anterior. Pero puedes
combinar esos pasos en una sola línea, como hicimos para la variable a2. Las mismas acciones separadas
ocurren bajo el capó, por supuesto. A veces, la combinación de la declaración y la inicialización puede
sentirse más legible.

Ahora que hemos creado un objeto Apple, podemos acceder a sus variables y métodos, como hemos visto
en varios de nuestros ejemplos del Capítulo 4 o incluso en nuestra aplicación gráfica "Hello" de "HelloJava"
en la página 41. Aunque no sea muy emocionante, podríamos ahora construir otra clase,
PrintAppleDetails, que sea una aplicación completa para crear una instancia de Apple e imprimir sus
detalles:

Si compilas y ejecutas este ejemplo, deberías ver la siguiente salida en tu terminal o en la ventana de
terminal de tu entorno de desarrollo integrado (IDE):

Pero hmm, ¿por qué no tenemos una masa? Si retrocedes y ves cómo declaramos las variables para
nuestra clase Apple, solo inicializamos el diámetro. Todas las otras variables obtendrán el valor
predeterminado asignado por Java, que es 0 ya que son tipos numéricos. (Rápidamente, las variables
booleanas obtienen un valor predeterminado de false y los tipos de referencia obtienen un valor
predeterminado de null). Idealmente, nos gustaría tener una manzana más interesante. Veamos cómo
proporcionar esos detalles interesantes.

Accediendo a Campos y Métodos

Una vez que tengas una referencia a un objeto, puedes utilizar y manipular sus variables y métodos
utilizando la notación de punto que vimos en el Capítulo 4. Creemos una nueva clase, PrintAppleDetails2,
proporcionemos algunos valores para la masa y la posición de nuestra instancia a1, y luego imprimamos
los nuevos detalles.
Y la nueva salida sería:

¡Genial! a1 luce un poco mejor. Pero mira el código de nuevo. Tuvimos que repetir las tres líneas que
imprimen los detalles del objeto. Ese tipo de replicación exacta pide a gritos un método. Los métodos nos
permiten "hacer cosas" dentro de una clase. Entraremos en muchos más detalles en "Métodos" en la
página 134. Podríamos mejorar la clase Apple para proporcionar estas declaraciones de impresión:

Con esas declaraciones de detalles reubicadas, podemos crear PrintAppleDetails3 de manera más concisa
que su predecesor:
Echemos otro vistazo al método printDetails() que agregamos a la clase Apple. Dentro de una clase,
podemos acceder a variables y llamar métodos de la clase directamente por su nombre. Las declaraciones
de impresión simplemente utilizan los nombres simples como masa y diámetro. O considera completar el
método isTouching(). Podemos usar nuestras propias coordenadas x e y sin ningún prefijo especial. Pero
para acceder a las coordenadas de otra manzana, necesitamos recurrir a la notación de punto. Aquí hay
una forma de escribir ese método utilizando algo de matemáticas (más sobre esto en "La clase
java.lang.Math" en la página 244) y la declaración if/then que vimos en "condicionales if/else" en la página
100:

Vamos a completar un poco más nuestro juego y crear nuestra clase Field que utiliza algunos objetos
Apple. Crea instancias como variables miembro y trabaja con esos objetos en los métodos setupApples()
y detectCollision(), invocando métodos de Apple y accediendo a variables de esos objetos a través de las
referencias a1 y a2, visualizadas en la Figura 5-2.
Podemos demostrar que la clase Field tiene acceso a las variables y métodos de las manzanas con otra
iteración de nuestra aplicación, PrintAppleDetails4:

Deberíamos ver los detalles familiares de la manzana seguidos de una respuesta sobre si las dos manzanas
están o no en contacto:

Genial, justo lo que esperábamos. Antes de seguir leyendo, intenta cambiar las posiciones de las manzanas
para hacer que se toquen.
Vista previa de modificadores de acceso

Varios factores afectan si los miembros de una clase pueden ser accedidos desde otra clase. Puedes usar
los modificadores de visibilidad public, private y protected para controlar el acceso; además, las clases
pueden ser colocadas en un paquete, lo que afecta su alcance. Por ejemplo, el modificador private designa
una variable o método para ser utilizado únicamente por otros miembros de la clase en sí misma. En el
ejemplo anterior, podríamos cambiar la declaración de nuestra variable diameter a private:

Ahora no podemos acceder a diameter desde Field:

Si todavía necesitamos acceder a diameter de alguna manera, normalmente añadiríamos métodos


getDiameter() y setDiameter() públicos a la clase Apple:

Crear métodos de esta manera es una buena regla de diseño porque permite flexibilidad futura para
cambiar el tipo o el comportamiento del valor. Más adelante en este capítulo, veremos más sobre
paquetes, modificadores de acceso y cómo afectan la visibilidad de variables y métodos.

Miembros estáticos

Como hemos mencionado, las variables y métodos de instancia están asociados y se acceden a través de
una instancia de la clase (es decir, a través de un objeto particular, como a1 o f en los ejemplos anteriores).
En contraste, los miembros que se declaran con el modificador static viven en la clase y son compartidos
por todas las instancias de la clase. Las variables declaradas con el modificador static se llaman variables
estáticas o variables de clase; de manera similar, a estos tipos de métodos se les llama métodos estáticos
o métodos de clase. Los miembros estáticos son útiles como indicadores e identificadores, que pueden
ser accedidos desde cualquier lugar. Podemos agregar una variable estática a nuestro ejemplo de Apple,
tal vez para almacenar el valor de la aceleración debido a la gravedad, de modo que podamos calcular la
trayectoria de una manzana lanzada cuando comencemos a animar nuestro juego:

Hemos declarado la nueva variable float gravAccel como estática. Eso significa que está asociada con la
clase, no con una instancia individual, y si cambiamos su valor (ya sea directamente o a través de cualquier
instancia de Apple), el valor cambia para todos los objetos Apple, como se muestra en la Figura 5-3.

Los miembros estáticos pueden ser accedidos como los miembros de instancia. Dentro de nuestra clase
Apple, podemos referirnos a gravAccel como cualquier otra variable:

Sin embargo, dado que los miembros estáticos existen en la clase misma, independientemente de
cualquier instancia, también podemos acceder a ellos directamente a través de la clase. Si queremos
lanzar manzanas en Marte, por ejemplo, no necesitamos un objeto Apple como a1 o a2 para obtener o
establecer la variable gravAccel. En su lugar, podemos usar la clase para seleccionar la variable:

Esto cambia el valor de gravAccel tal como es visto por todas las instancias. No tenemos que configurar
manualmente cada instancia de Apple para que caiga en Marte. Las variables estáticas son útiles para
cualquier tipo de datos que se comparta entre clases en tiempo de ejecución. Por ejemplo, puedes crear
métodos para registrar las instancias de tus objetos para que puedan comunicarse, o para que puedas
hacer un seguimiento de todas ellas. También es común usar variables estáticas para definir valores
constantes. En este caso, usamos el modificador static junto con el modificador final. Así que, si nos
preocupamos solo por las manzanas bajo la influencia de la atracción gravitatoria de la Tierra, podríamos
cambiar Apple de la siguiente manera:

Hemos seguido una convención común aquí y hemos nombrado nuestra constante con letras mayúsculas
y guiones bajos (si el nombre tiene más de una palabra). El valor de EARTH_ACCEL es una constante; se
puede acceder a través de la clase Apple o sus instancias, pero su valor no se puede cambiar durante la
ejecución.

Es importante usar la combinación de static y final solo para cosas que son realmente constantes. El
compilador tiene permitido "en línea" esos valores dentro de las clases que los referencian. Esto significa
que si cambias una variable static final, es posible que debas recompilar todo el código que usa esa clase
(este es realmente el único caso en el que debes hacerlo en Java). Los miembros estáticos también son
útiles para valores necesarios en la construcción de una instancia en sí misma. En nuestro ejemplo,
podríamos declarar una serie de valores estáticos para representar varios tamaños de objetos Apple:

Podríamos utilizar estas opciones en un método que establezca el tamaño de una manzana, o en un
constructor especial, como discutiremos en breve:

Nuevamente, dentro de la clase Apple, también podemos usar miembros estáticos directamente por su
nombre; no es necesario el prefijo Apple.:

Métodos

Hasta ahora, nuestras clases de ejemplo han sido bastante simples. Mantenemos un par de piezas de
información por ahí: las manzanas tienen masa, los campos tienen un par de manzanas, etc. Pero también
hemos tocado la idea de hacer que esas clases hagan cosas. Todas nuestras diversas clases
PrintAppleDetails tienen una lista de pasos que se ejecutan cuando ejecutamos el programa, por ejemplo.
Como señalamos brevemente antes, en Java, esos pasos se agrupan en un método. En el caso de
PrintAppleDetails, ese es el método main().

En cualquier lugar donde tengas pasos que seguir o decisiones que tomar, necesitas un método. Además
de almacenar variables como la masa y el diámetro en nuestra clase Apple, también agregamos algunas
piezas de código que contenían acciones y lógica. Los métodos son tan fundamentales para las clases que
tuvimos que crear algunos (piensa en el método printDetails() en Apple o el método setupApples() en
Field) incluso antes de llegar aquí a la discusión formal sobre ellos. ¡Esperemos que los métodos que
hemos discutido hasta ahora hayan sido lo suficientemente directos para seguirlos solo a partir del
contexto! Pero los métodos pueden hacer mucho más que imprimir algunas variables o calcular una
distancia. Pueden contener declaraciones de variables locales y otras instrucciones de Java que se
ejecutan cuando se invoca el método. Los métodos pueden devolver un valor al llamador. Siempre
especifican un tipo de retorno, que puede ser un tipo primitivo, un tipo de referencia o el tipo void, que
indica que no se devuelve ningún valor. Los métodos pueden tomar argumentos, que son valores
suministrados por el llamador del método.

Aquí tienes un ejemplo simple:

En este ejemplo, la clase Bird define un método, fly(), que toma como argumentos dos enteros: x e y.
Devuelve un valor de tipo double como resultado, utilizando la palabra clave return.

No entramos en detalles sobre listas de argumentos de este tipo, pero si tienes curiosidad y te gustaría
investigar un poco por tu cuenta, busca en línea la palabra clave en el argot de programación "varargs".

Nuestro método tiene un número fijo de argumentos (dos); sin embargo, los métodos pueden tener listas
de argumentos de longitud variable, lo que permite que el método especifique que puede tomar cualquier
cantidad de argumentos y ordenarlos por sí mismo en tiempo de ejecución.

Variables locales

Nuestro método fly() declara una variable local llamada distancia, que utiliza para calcular la distancia
volada. Una variable local es temporal; existe solo dentro del alcance (el bloque) de su método. Las
variables locales se asignan cuando se invoca un método; normalmente se destruyen cuando el método
devuelve. No se pueden referenciar desde fuera del método en sí. Si el método se está ejecutando
concurrentemente en diferentes hilos, cada hilo tiene su propia versión de las variables locales del
método. Los argumentos de un método también sirven como variables locales dentro del alcance del
método; la única diferencia es que se inicializan al pasarlos desde el llamador del método.

Un objeto creado dentro de un método y asignado a una variable local puede persistir o no después de
que el método haya devuelto. Como veremos detalladamente en "Destrucción de objetos" en la página
148, depende de si quedan referencias al objeto. Si se crea un objeto, se asigna a una variable local y
nunca se usa en ningún otro lugar, ese objeto ya no está referenciado cuando la variable local desaparece
del alcance, por lo que la recolección de basura (más sobre este proceso en "Recolección de basura" en
la página 148) elimina el objeto. Sin embargo, si asignamos el objeto a una variable de instancia de un
objeto, lo pasamos como argumento a otro método o lo devolvemos como valor de retorno, puede ser
guardado por otra variable que contenga su referencia.
Sombreado

Si una variable local o un argumento de método y una variable de instancia tienen el mismo nombre, la
variable local oculta el nombre de la variable de instancia dentro del alcance del método. Esto puede
sonar como una situación extraña, pero sucede con bastante frecuencia cuando la variable de instancia
tiene un nombre común u obvio. Por ejemplo, podríamos agregar un método move a nuestra clase Apple.
Nuestro método necesitará las nuevas coordenadas que nos indiquen dónde colocar la manzana. Una
opción fácil para los argumentos de coordenadas serían x e y. Pero ya tenemos variables de instancia con
el mismo nombre:

Si la manzana se encuentra actualmente en la posición (20, 40) y llamamos a moveTo(40, 50), ¿qué crees
que mostrará la declaración println()? Dentro de moveTo(), los nombres x e y se refieren únicamente a
los argumentos con esos nombres. Nuestra salida sería:

Si no podemos acceder a las variables de instancia x e y, ¿cómo podemos mover la manzana? Resulta que
Java comprende el enmascaramiento (shadowing) y proporciona un mecanismo para trabajar en estas
situaciones.

La referencia "this"

Puedes usar la referencia especial this en cualquier momento que necesites referirte explícitamente al
objeto actual o a un miembro del objeto actual. A menudo no es necesario utilizar this, porque la
referencia al objeto actual es implícita; tal es el caso al usar variables de instancia con nombres
inequívocos dentro de una clase. Pero podemos utilizar this para referirnos explícitamente a variables de
instancia en nuestro objeto, incluso si están enmascaradas. El siguiente ejemplo muestra cómo podemos
usar this para permitir nombres de argumentos que enmascaran nombres de variables de instancia. Esta
es una técnica bastante común porque evita tener que inventar nombres alternativos. Así es cómo
podríamos implementar nuestro método moveTo() con variables enmascaradas:
En este ejemplo, la expresión this.x se refiere a la variable de instancia x y le asigna el valor de la variable
local x, la cual de otro modo ocultaría su nombre. Hacemos lo mismo para this.y pero agregamos un poco
de protección para asegurarnos de que no movemos la manzana por debajo de nuestro suelo. La única
razón por la que necesitamos usar this en el ejemplo anterior es porque hemos utilizado nombres de
argumentos que ocultan nuestras variables de instancia, y queremos referirnos a las variables de
instancia. También puedes usar la referencia this en cualquier momento que desees pasar una referencia
al objeto "actual" contenedor a algún otro método, como hicimos para la versión gráfica de nuestra
aplicación "Hola Java" en "HolaJava2: La secuela" en la página 53.

Métodos estáticos

Los métodos estáticos (métodos de clase), al igual que las variables estáticas, pertenecen a la clase y no a
instancias individuales de la misma. ¿Qué significa esto? Principalmente, un método estático existe fuera
de cualquier instancia particular de la clase. Puede ser invocado por su nombre, a través del nombre de
la clase, sin necesidad de objetos alrededor. Debido a que no está vinculado a una instancia particular del
objeto, un método estático solo puede acceder directamente a otros miembros estáticos (variables
estáticas y otros métodos estáticos) de la clase. No puede ver directamente variables de instancia ni llamar
a métodos de instancia, ya que para hacerlo tendríamos que preguntar "¿en qué instancia?". Los métodos
estáticos pueden ser llamados desde instancias, sintácticamente igual que los métodos de instancia, pero
lo importante es que también pueden ser utilizados de manera independiente.

Nuestro método isTouching() usa un método estático, Math.sqrt(), el cual está definido por la clase
java.lang.Math; exploraremos esta clase en detalle en el Capítulo 8. Por ahora, lo importante a tener en
cuenta es que Math es el nombre de una clase y no una instancia de un objeto Math. Debido a que los
métodos estáticos pueden ser invocados siempre que el nombre de la clase esté disponible, los métodos
de clase se asemejan más a las funciones de estilo C. Los métodos estáticos son particularmente útiles
para métodos de utilidad que realizan un trabajo útil ya sea independientemente de las instancias o al
trabajar en instancias. Por ejemplo, en nuestra clase Apple, podríamos enumerar todos los tamaños
disponibles como cadenas legibles por humanos a partir de las constantes que creamos en "Accediendo
a Campos y Métodos" en la página 127:

Aquí hemos definido un método estático, getAppleSizes(), que devuelve un array de cadenas que
contienen nombres de tamaños de manzana. Hacemos el método estático porque la lista de tamaños es
la misma independientemente del tamaño que pueda tener una instancia particular de Apple. Aun así,
aún podemos utilizar getAppleSizes() desde dentro de una instancia de Apple si quisiéramos, de la misma
manera que un método de instancia. Podríamos cambiar el método printDetails (no estático) para
imprimir un nombre de tamaño en lugar de un diámetro exacto, por ejemplo:
Sin embargo, también podemos llamarlo desde otras clases, utilizando el nombre de la clase Apple con la
notación de punto. Por ejemplo, la primera clase PrintAppleDetails podría usar una lógica similar para
imprimir una declaración resumida utilizando nuestro método estático y variables estáticas, de la
siguiente manera:

Aquí tenemos nuestra confiable instancia de la clase Apple, a1, pero no es necesaria para obtener la lista
de nuestros tamaños. Observa que cargamos la lista de nombres agradables antes de crear a1. Sin
embargo, todo sigue funcionando como se ve en la salida:

Los métodos estáticos también desempeñan un papel importante en varios patrones de diseño, donde se
limita el uso del operador new para una clase a un solo método, llamado un método de fábrica (factory
method) estático. Hablaremos más sobre la construcción de objetos en "Constructores" en la página 145.

No hay una convención de nombres específica para los métodos de fábrica, pero es común ver el uso
como esto:
No escribiremos ningún método de fábrica, pero es probable que los encuentres en la práctica,
especialmente al buscar preguntas en sitios como Stack Overflow.

Inicialización de Variables Locales

A diferencia de las variables de instancia que reciben valores predeterminados si no proporcionamos uno
explícitamente, las variables locales deben inicializarse antes de poder utilizarse. Es un error en tiempo
de compilación intentar acceder a una variable local sin asignarle primero un valor:

Observa que esto no implica que las variables locales deban inicializarse al ser declaradas, simplemente
que la primera vez que se hacen referencia a ellas debe ser en una asignación. Surgen posibilidades más
sutiles cuando se realizan asignaciones dentro de condicionales:

En este ejemplo, la variable 'bar' se inicializa solo si 'someCondition' es verdadero. El compilador no


permite realizar esta apuesta, por lo que marca el uso de 'bar' como un error. Podríamos corregir esta
situación de varias maneras. Podríamos inicializar la variable con un valor predeterminado de antemano
o mover el uso dentro de la condición. También podríamos asegurarnos de que la ejecución del programa
no llegue a la variable no inicializada a través de otros medios, dependiendo de lo que tenga sentido para
nuestra aplicación en particular. Por ejemplo, podríamos asegurarnos simplemente de asignar un valor a
'bar' en ambas ramas del 'if' y 'else'. O podríamos salir abruptamente del método:

En este caso, no hay posibilidad de que 'bar' se alcance en un estado no inicializado, por lo que el
compilador permite el uso de 'bar' después de la condición.
¿Por qué Java es tan exigente con las variables locales? Una de las fuentes más comunes (y perniciosas)
de errores en otros lenguajes como C o C++ es olvidar inicializar variables locales, por lo que Java intenta
ayudar en este aspecto.

Paso de Argumentos y Referencias

Al principio del Capítulo 4, describimos la distinción entre tipos primitivos, que se pasan por valor
(mediante copia), y objetos, que se pasan por referencia. Ahora que tenemos un mejor manejo de los
métodos en Java, avancemos a través de un ejemplo:

El bloque de código llama a `myMethod()`, pasándole dos argumentos. El primer argumento, `i`, se pasa
por valor; cuando se llama al método, el valor de `i` se copia en el parámetro del método (una variable
local para él) llamado `j`. Si `myMethod()` cambia el valor de `j`, solo está cambiando su copia de la variable
local.

De la misma manera, una copia de la referencia al objeto `obj` se coloca en la variable de referencia `o`
de `myMethod()`. Ambas referencias se refieren al mismo objeto, por lo que cualquier cambio realizado
a través de cualquiera de las referencias afecta a la instancia real (única) del objeto. Si cambiamos el valor
de, digamos, `o.size`, el cambio es visible tanto como `o.size` (dentro de `myMethod()`) como `obj.size`
(en el método que realiza la llamada). Sin embargo, si `myMethod()` cambia la referencia `o` en sí misma,
para apuntar a otro objeto, solo está afectando a su referencia local de variable. No afecta a la variable
`obj` del llamador, que todavía se refiere al objeto original. En este sentido, pasar la referencia es similar
a pasar un puntero en C y a diferencia del paso por referencia en C++.

¿Qué sucede si `myMethod()` necesita modificar la noción de la referencia `obj` del método que realiza la
llamada (es decir, hacer que `obj` apunte a un objeto diferente)? La forma fácil de hacerlo es envolver
`obj` dentro de algún tipo de objeto. Por ejemplo, podríamos envolver el objeto como el único elemento
en un array:

Todas las partes podrían referirse al objeto como `wrapper[0]` y tendrían la capacidad de cambiar la
referencia. Esto no es estéticamente agradable, pero ilustra que lo que se necesita es el nivel de
indirección.

Otra posibilidad es utilizar `this` para pasar una referencia al objeto que realiza la llamada. En ese caso, el
objeto que realiza la llamada actúa como el contenedor para la referencia. Echemos un vistazo a un
fragmento de código que podría ser parte de la implementación de una lista enlazada:
Cada elemento en una lista enlazada contiene un puntero al siguiente elemento en la lista. En este código,
la clase Element representa un elemento; incluye un método para agregarse a sí mismo a la lista. La clase
List en sí misma contiene un método para agregar un Elemento arbitrario a la lista. El método `addToList()`
llama a `insertElement()` con el argumento `this` (que es, por supuesto, un Elemento). `insertElement()`
puede utilizar la referencia `this` que se pasó para modificar la variable de instancia `nextElement` del
Elemento y luego actualizar el inicio de la lista. La misma técnica se puede usar en conjunto con interfaces
para implementar devoluciones de llamada (callbacks) para invocaciones de métodos arbitrarios.

Envoltorios para Tipos Primitivos

Como describimos en el Capítulo 4, hay una separación en el mundo Java entre tipos de clase (es decir,
objetos) y tipos primitivos (es decir, números, caracteres y valores booleanos). Java acepta este
compromiso simplemente por razones de eficiencia. Cuando estás realizando cálculos numéricos, deseas
que tus operaciones sean livianas; tener que usar objetos para tipos primitivos complica las
optimizaciones de rendimiento. Para las ocasiones en las que deseas tratar los valores como objetos, Java
proporciona una clase envolvente estándar para cada uno de los tipos primitivos, como se muestra en la
Tabla 5-1.

Una instancia de una clase envoltorio encapsula un único valor de su tipo correspondiente. Es un objeto
inmutable que actúa como contenedor para almacenar el valor y permitirnos recuperarlo más adelante.
Puedes construir un objeto envoltorio a partir de un valor primitivo o a partir de una representación de
cadena (String) del valor. Las siguientes declaraciones son equivalentes:

Los constructores de envoltorios (wrappers) lanzan una NumberFormatException cuando hay un error al
analizar una cadena.

Cada uno de los envoltorios de tipos numéricos implementa la interfaz java.lang.Number, la cual
proporciona métodos de acceso a su valor en todas las formas primitivas. Puedes obtener valores
escalares con los métodos doubleValue(), floatValue(), longValue(), intValue(), shortValue() y byteValue():

Este código es equivalente a realizar un casting del valor primitivo double a varios tipos.

La necesidad más común de un envoltorio (wrapper) es cuando deseas pasar un valor primitivo a un
método que requiere un objeto. Por ejemplo, en el Capítulo 7, veremos la API de Colecciones de Java, un
conjunto sofisticado de clases para tratar con grupos de objetos, como listas, conjuntos y mapas. La API
de Colecciones trabaja con tipos de objetos, por lo que los primitivos deben ser envueltos cuando se
almacenan en ellos. Veremos en la siguiente sección que Java hace este proceso de envoltura
automáticamente. Sin embargo, por ahora, hagámoslo nosotros mismos. Como veremos, un List es una
colección extensible de Objects. Podemos usar envoltorios para almacenar números en un List (junto con
otros objetos):

Aquí hemos creado un objeto envoltorio Integer para poder insertar el número en la List, utilizando el
método add(), el cual acepta un objeto. Más adelante, cuando extraemos elementos de la List, podemos
recuperar el valor int de la siguiente manera:

Como insinuamos anteriormente, permitir que Java haga esto por nosotros ("autoboxing") hace que el
código sea más conciso y seguro. El uso de la clase envoltorio está en su mayoría oculto para nosotros por
el compilador, pero aún se está utilizando internamente:

Veremos más acerca de genéricos más adelante.

Sobrecarga de Métodos

La sobrecarga de métodos es la capacidad de definir varios métodos con el mismo nombre en una clase;
cuando se invoca el método, el compilador selecciona el correcto en función de los argumentos pasados
al método. Esto implica que los métodos sobrecargados deben tener diferentes números o tipos de
argumentos. (En "Sobrescritura de métodos" en la página 159, veremos la sobrescritura de métodos, que
ocurre cuando declaramos métodos con firmas idénticas en subclases).

La sobrecarga de métodos (también llamada polimorfismo ad hoc) es una característica poderosa y útil.
La idea es crear métodos que actúen de la misma manera en diferentes tipos de argumentos. Esto crea la
ilusión de que un solo método puede operar en muchos tipos de argumentos. El método print() en la clase
estándar PrintStream es un buen ejemplo de la sobrecarga de métodos en acción. Como probablemente
hayas deducido hasta ahora, puedes imprimir una representación en cadena de casi cualquier cosa
usando esta expresión:

La variable 'out' es una referencia a un objeto (un PrintStream) que define nueve versiones diferentes,
"sobrecargadas", del método print(). Las versiones aceptan argumentos de los siguientes tipos: Object,
String, char[], char, int, long, float, double y boolean.
El método print() puede ser invocado con cualquiera de estos tipos como argumento y se imprimirá de
manera apropiada. En un lenguaje sin sobrecarga de métodos, esto requeriría algo más engorroso, como
un método con un nombre único para imprimir cada tipo de objeto. En ese caso, sería tu responsabilidad
determinar qué método usar para cada tipo de dato.

En el ejemplo anterior, el método print() ha sido sobrecargado para admitir dos tipos de referencia: Object
y String. ¿Qué sucede si intentamos llamar a print() con algún otro tipo de referencia? Por ejemplo, ¿un
objeto Date? Cuando no hay una coincidencia exacta de tipo, el compilador busca una coincidencia
aceptable y asignable. Dado que Date, al igual que todas las clases, es una subclase de Object, un objeto
Date puede asignarse a una variable de tipo Object. Por lo tanto, es una coincidencia aceptable y se
selecciona el método Object.

¿Qué pasa si hay más de una coincidencia posible? Por ejemplo, ¿qué pasa si queremos imprimir el literal
"Hola"? Ese literal se puede asignar tanto a String (ya que es un String) como a Object. Aquí, el compilador
determina cuál coincidencia es "mejor" y selecciona ese método. En este caso, es el método String.

La explicación intuitiva de esto es que la clase String está "más cerca" del literal "Hola" en la jerarquía de
herencia. Es una coincidencia más específica. Una forma ligeramente más rigurosa de especificarlo sería
decir que un método dado es más específico que otro método si los tipos de argumentos del primer
método son todos asignables a los tipos de argumentos del segundo método. En este caso, el método
String es más específico porque el tipo String es asignable al tipo Object. Lo contrario no es cierto.

Si estás prestando mucha atención, es posible que hayas notado que dijimos que el compilador resuelve
los métodos sobrecargados. La sobrecarga de métodos no es algo que suceda en tiempo de ejecución;
esta es una distinción importante. Significa que el método seleccionado se elige una vez, cuando el código
se compila. Una vez que se selecciona el método sobrecargado, la elección está fijada hasta que el código
se vuelva a compilar, incluso si la clase que contiene el método llamado se revisa más tarde y se agrega
un método sobrecargado aún más específico. Esto contrasta con los métodos anulados, que se localizan
en tiempo de ejecución y pueden encontrarse incluso si no existían cuando se compiló la clase que los
llama. En la práctica, esta distinción generalmente no será relevante para ti, ya que probablemente
recompilarás todas las clases necesarias al mismo tiempo. Hablaremos sobre la anulación de métodos
más adelante en el capítulo.

Creación de Objetos

Los objetos en Java se asignan en un espacio de memoria "heap" del sistema. Sin embargo, a diferencia
de algunos otros lenguajes, no necesitamos gestionar esa memoria nosotros mismos. Java se encarga de
la asignación y desasignación de memoria por ti. Java asigna explícitamente almacenamiento para un
objeto cuando lo creas con el operador new. Más importante aún, los objetos son eliminados por el
recolector de basura (garbage collector) cuando ya no están referenciados.

Constructores

Los objetos se asignan con el operador new utilizando un constructor. Un constructor es un método
especial con el mismo nombre que su clase y sin tipo de retorno. Se llama cuando se crea una nueva
instancia de la clase, lo que brinda a la clase la oportunidad de configurar el objeto para su uso. Los
constructores, al igual que otros métodos, pueden aceptar argumentos y pueden ser sobrecargados (sin
embargo, no se heredan como otros métodos).
En este ejemplo, la clase Date tiene dos constructores. El primero no toma argumentos; se conoce como
el constructor predeterminado. Los constructores predeterminados tienen un papel especial: si no
definimos ningún constructor para una clase, se suministra automáticamente un constructor
predeterminado vacío.

El constructor predeterminado es el que se llama cuando creas un objeto llamando a su constructor sin
argumentos. Aquí hemos implementado el constructor predeterminado para que establezca la variable
de instancia 'time' llamando a un método hipotético, 'currentTime()', que se asemeja a la funcionalidad
real de la clase java.util.Date.

El segundo constructor toma un argumento de tipo String. Presumiblemente, este String contiene una
representación de cadena del tiempo que se puede analizar para establecer la variable 'time'. Dados los
constructores en el ejemplo anterior, creamos un objeto Date de las siguientes maneras:

En cada caso, Java elige el constructor apropiado en tiempo de compilación basándose en las reglas para
la selección de métodos sobrecargados.

Si posteriormente eliminamos todas las referencias a un objeto asignado, este será recolectado por el
recolector de basura (garbage collector), como discutiremos en breve:

Establecer esta referencia como null significa que ya no apunta al objeto de fecha "25 de diciembre de
2006". Establecer la variable 'christmas' a cualquier otro valor tendría el mismo efecto. A menos que el
objeto de fecha original sea referenciado por otra variable, ahora es inaccesible y puede ser recolectado
por el recolector de basura (garbage collector). No estamos sugiriendo que tengas que establecer las
referencias como null para que los valores sean recolectados por el recolector de basura. A menudo esto
sucede naturalmente cuando las variables locales salen de alcance, pero los elementos referenciados por
variables de instancia de objetos viven tanto tiempo como el objeto mismo (a través de referencias a él),
y las variables estáticas viven efectivamente para siempre.

Algunos puntos más: los constructores no pueden ser declarados abstractos, sincronizados o finales
(definiremos el resto de esos términos más adelante). Sin embargo, los constructores pueden ser
declarados con los modificadores de visibilidad public, private o protected, al igual que otros métodos,
para controlar su accesibilidad. Hablaremos en detalle sobre los modificadores de visibilidad en el próximo
capítulo.

Trabajando con Constructores Sobrecargados

Un constructor puede referirse a otro constructor en la misma clase o en la superclase inmediata usando
formas especiales de las referencias this y super. Discutiremos el primer caso aquí y volveremos al del
constructor de la superclase después de hablar más sobre la subclase y la herencia. Un constructor puede
invocar a otro constructor sobrecargado en su clase usando la llamada auto-referencial this() con
argumentos apropiados para seleccionar el constructor deseado. Si un constructor llama a otro
constructor, debe hacerlo como su primera declaración:

En este ejemplo, la clase Car tiene dos constructores. El primero, más explícito, acepta argumentos que
especifican el modelo del automóvil y su número de puertas. El segundo constructor toma solo el modelo
como argumento y, a su vez, llama al primer constructor con un valor predeterminado de cuatro puertas.
La ventaja de este enfoque es que puedes tener un solo constructor que realice todo el trabajo complicado
de configuración; otros constructores auxiliares simplemente proporcionan los argumentos apropiados a
ese constructor.

La llamada especial a this() debe aparecer como la primera instrucción en nuestro constructor delegado.
La sintaxis está restringida de esta manera porque hay una necesidad de identificar una clara cadena de
comando en la llamada de constructores. Al final de la cadena, Java invoca el constructor de la superclase
(si no lo hacemos explícitamente) para asegurarse de que los miembros heredados se inicialicen
correctamente antes de proceder.

También hay un punto en la cadena, justo después de invocar el constructor de la superclase, donde se
evalúan los inicializadores de las variables de instancia de la clase actual. Antes de ese punto, ni siquiera
podemos hacer referencia a las variables de instancia de nuestra clase. Explicaremos esta situación
nuevamente en detalle completo después de haber hablado sobre la herencia.

Por ahora, todo lo que necesitas saber es que puedes invocar a un segundo constructor (delegar a él) solo
como la primera declaración de tu constructor. Por ejemplo, el siguiente código es ilegal y causa un error
en tiempo de compilación:

El constructor simple que recibe solo el nombre del modelo no puede realizar ninguna configuración
adicional antes de llamar al constructor más explícito. Ni siquiera puede hacer referencia a un miembro
de instancia para obtener un valor constante:
La variable de instancia `defaultDoors` no se inicializa hasta un punto posterior en la cadena de llamadas
de constructores que configuran el objeto, por lo que el compilador no nos permite acceder a ella todavía.
Afortunadamente, podemos resolver este problema particular utilizando una variable estática en lugar de
una variable de instancia:

En Java, los miembros estáticos de una clase se inicializan cuando la clase es cargada por primera vez en
la máquina virtual, por lo que es seguro acceder a ellos en un constructor.

Destrucción de Objetos

Ahora que hemos visto cómo crear objetos, es hora de hablar sobre su destrucción. Si estás acostumbrado
a programar en C o C++, probablemente hayas pasado tiempo buscando fugas de memoria en tu código.
Java se encarga de la destrucción de objetos por ti; no tienes que preocuparte por las fugas de memoria
tradicionales y puedes concentrarte en tareas de programación más importantes.

Recolección de Basura

Java utiliza una técnica conocida como recolección de basura (garbage collection) para eliminar objetos
que ya no son necesarios. El recolector de basura es como el segador sombrío de Java. Permanece en
segundo plano, acechando objetos y esperando su desaparición. Los encuentra y los observa, contando
periódicamente las referencias a ellos para ver cuándo ha llegado su momento. Cuando todas las
referencias a un objeto desaparecen y ya no es accesible, el mecanismo de recolección de basura declara
al objeto como inaccesible y reclama su espacio de vuelta al conjunto de recursos disponibles. Un objeto
inalcanzable es aquel que ya no puede ser encontrado a través de ninguna combinación de referencias
"vivas" en la aplicación en ejecución.

La recolección de basura utiliza una variedad de algoritmos; la arquitectura de la máquina virtual de Java
no requiere un esquema particular. Sin embargo, vale la pena señalar cómo algunas implementaciones
de Java han logrado esta tarea. Al principio, Java utilizaba una técnica llamada "marcar y barrer" (mark
and sweep). En este esquema, Java primero recorre el árbol de todas las referencias de objetos accesibles
y los marca como vivos. Luego, escanea el montón (heap), buscando objetos identificables que no estén
marcados. En esta técnica, Java puede encontrar objetos en el montón porque se almacenan de una
manera característica y tienen una firma particular de bits en sus identificadores que es poco probable
que se reproduzcan naturalmente. Este tipo de algoritmo no se confunde con el problema de referencias
cíclicas, en el cual los objetos pueden referirse mutuamente y parecer vivos incluso cuando están muertos
(Java maneja este problema automáticamente). Sin embargo, este esquema no era el método más rápido
y causaba pausas en el programa. Desde entonces, las implementaciones se han vuelto mucho más
sofisticadas.

Los recolectores de basura modernos de Java se ejecutan eficazmente de forma continua sin causar
demoras prolongadas en la ejecución de la aplicación de Java. Debido a que forman parte de un sistema
en tiempo de ejecución, también pueden lograr algunas cosas que no se pueden hacer estáticamente. La
implementación de Java de Sun divide el montón de memoria en varias áreas para objetos con diferentes
esperanzas de vida estimadas. Los objetos de corta vida se colocan en una parte especial del montón, lo
que reduce drásticamente el tiempo para reciclarlos. Los objetos que viven más tiempo pueden ser
trasladados a otras partes menos volátiles del montón. En implementaciones recientes, el recolector de
basura incluso puede "ajustarse" ajustando el tamaño de partes del montón en función del rendimiento
real de la aplicación. La mejora en la recolección de basura de Java desde las versiones tempranas ha sido
notable y es una de las razones por las que Java ahora es aproximadamente equivalente en velocidad a
muchos lenguajes tradicionales que ponen la carga de la gestión de memoria en los hombros del
programador.

En general, no es necesario preocuparse por el proceso de recolección de basura. Pero un método de


recolección de basura puede ser útil para la depuración. Puedes solicitar explícitamente al recolector de
basura que haga una limpieza completa invocando el método System.gc(). Este método depende
completamente de la implementación y puede no hacer nada, pero puede usarse si deseas alguna
garantía de que Java ha realizado la limpieza antes de realizar alguna actividad.

Paquetes

Incluso al quedarse con ejemplos más simples, es posible que hayas notado que resolver problemas en
Java requiere crear un número de clases. Para nuestras clases de juego mencionadas anteriormente,
tenemos nuestras manzanas, nuestros físicos y nuestro campo de juego, solo por nombrar algunos. Para
aplicaciones o bibliotecas más complejas, puedes tener cientos o incluso miles de clases. Necesitas una
forma de organizar las cosas, y Java utiliza la noción de un paquete (package) para realizar esta tarea.

Recuerda nuestro segundo ejemplo de Hello World en el Capítulo 2. Las primeras líneas en el archivo nos
muestran mucha información sobre dónde vivirá el código:

Hemos nombrado el archivo Java de acuerdo con la clase principal en ese archivo. Cuando hablamos de
organizar cosas que van en archivos, es natural pensar en usar carpetas para organizar esos archivos a su
vez. Eso es esencialmente lo que hace Java. Los paquetes se mapean en nombres de carpetas de manera
similar a como las clases se mapean en nombres de archivos. Si estuvieras viendo el código fuente de Java
para los componentes Swing que usamos en HelloJava, por ejemplo, encontrarías una carpeta llamada
javax, y debajo de esa, una llamada swing, y debajo de eso encontrarías archivos como JFrame.java y
JLabel.java.

Importación de Clases

Una de las mayores fortalezas de Java radica en la vasta colección de bibliotecas de soporte disponibles
bajo licencias comerciales y de código abierto. ¿Necesitas generar un PDF? Hay una biblioteca para eso.
¿Necesitas importar una hoja de cálculo? Hay una biblioteca para eso. ¿Necesitas encender esa bombilla
inteligente en el sótano desde tu servidor web? También hay una biblioteca para eso. Si las computadoras
están realizando alguna tarea, casi siempre encontrarás una biblioteca de Java para ayudarte a escribir
código para realizar esa tarea también.

Importando clases individuales

En programación, a menudo escucharás el axioma de que "menos es más". Menos código es más
mantenible. Menos sobrecarga significa más rendimiento, etc., etc. (Aunque al seguir este estilo de
codificación, queremos recordarte que sigas otra famosa cita de nada menos que Einstein: "Todo debería
hacerse lo más simple posible, pero no más simple"). Si solo necesitas una o dos clases de un paquete
externo, puedes importar exactamente esas clases. Esto hace que tu código sea un poco más legible, ya
que otros saben exactamente qué clases estarás utilizando.

Reexaminemos ese fragmento de HelloJava anterior. Utilizamos una importación general (más sobre eso
en la siguiente sección), pero podríamos ajustar un poco las cosas importando solo las clases que
necesitamos, de la siguiente manera:

Este tipo de configuración de importación es ciertamente más detallada al escribir y leer, pero
nuevamente significa que cualquiera que lea o compile tu código sabe exactamente qué otras
dependencias existen. Muchos IDE incluso tienen una función de "Optimizar Importaciones" que
automáticamente encontrará esas dependencias y las listará individualmente. Una vez que adquieres el
hábito de listar y ver estas importaciones explícitas, es sorprendente lo útiles que pueden ser al orientarte
en una clase nueva (o quizás olvidada hace mucho tiempo).

Importación de paquetes completos

Por supuesto, no todos los paquetes se prestan para importaciones individuales de uno o dos elementos.
El mismo paquete Swing, javax.swing, es un gran ejemplo. Si estás escribiendo una aplicación gráfica de
escritorio, es casi seguro que utilizarás Swing y una gran cantidad de sus componentes.

Puedes importar todas las clases en el paquete usando la sintaxis que pasamos por alto anteriormente:

El * es una especie de comodín para importaciones de clases. Esta versión de la declaración de


importación le indica al compilador que todas las clases en el paquete estén listas para ser utilizadas.
Verás este tipo de importación con bastante frecuencia en muchos de los paquetes comunes de Java,
como AWT, Swing, Utils y I/O. Nuevamente, funciona para cualquier paquete, pero donde tiene sentido
ser más específico, obtendrás mejoras en el rendimiento en tiempo de compilación y mejorarás la
legibilidad de tu código.

Omitir importaciones

Tienes otra opción para usar clases externas de otros paquetes: no es necesario importarlas en absoluto.
Puedes utilizar sus nombres completamente calificados directamente en tu código. Por ejemplo, nuestra
clase HelloJava utilizó las clases JFrame y JLabel del paquete javax.swing. Podríamos importar solo la clase
JLabel si quisiéramos:
Esto puede parecer excesivamente detallado para una línea donde creamos nuestro marco, pero en clases
más grandes con una lista de importaciones ya larga, el uso puntual puede hacer que tu código sea más
legible. Una entrada completamente calificada a menudo señala el único uso de esta clase dentro de un
archivo. Si lo estuvieras usando muchas veces, lo importarías. Este tipo de uso nunca es un requisito, pero
lo verás de vez en cuando.

Paquetes personalizados

A medida que continúas aprendiendo Java y escribes más código para resolver problemas más grandes,
sin duda comenzarás a recopilar un número cada vez mayor de clases. Puedes usar paquetes tú mismo
para ayudar a organizar esa colección. Utilizas la palabra clave `package` para declarar un paquete
personalizado. Como se señaló al principio de esta sección, luego colocas el archivo con tu clase dentro
de una estructura de carpetas que corresponda al nombre del paquete. Como recordatorio rápido, los
paquetes usan nombres en minúsculas (por convención) separados por puntos, como en nuestro paquete
de interfaz gráfica, `javax.swing`.

Otra convención ampliamente aplicada a los nombres de paquetes es algo llamado "nomenclatura de
nombre de dominio invertido". Aparte de los paquetes asociados directamente con Java, las bibliotecas
de terceros y otro código contribuido suelen organizarse utilizando el nombre de dominio del correo
electrónico de la empresa o individuo. Por ejemplo, la Fundación Mozilla ha contribuido con una variedad
de bibliotecas Java a la comunidad de código abierto. La mayoría de esas bibliotecas y utilidades estarán
en paquetes que comienzan con el dominio de Mozilla, mozilla.org, en orden inverso: `org.mozilla`. Esta
denominación inversa tiene el efecto útil (y pretendido) de mantener la estructura de carpetas en la parte
superior bastante pequeña. No es raro tener proyectos de buen tamaño que utilicen bibliotecas solo de
los dominios de nivel superior `com` y `org`.

Si estás construyendo tus propios paquetes separados de cualquier trabajo de empresa o contrato,
puedes usar tu dirección de correo electrónico y revertirla, similar a los nombres de dominio de empresas.
Otra opción popular para el código distribuido en línea es usar el dominio de tu proveedor de alojamiento.
Por ejemplo, GitHub aloja muchos, muchos proyectos de Java para aficionados y entusiastas. Podrías crear
un paquete llamado `com.github.myawesomeproject` donde "myawesomeproject" obviamente sería
reemplazado por el nombre real de tu proyecto. Ten en cuenta que los repositorios en sitios como GitHub
a menudo permiten nombres que no son válidos en nombres de paquetes. Es posible que tengas un
proyecto llamado `my-awesome-project`, pero los guiones no están permitidos en ninguna parte de un
nombre de paquete. A menudo, esos caracteres no permitidos se omiten simplemente para crear un
nombre válido.

Es posible que ya hayas echado un vistazo a más ejemplos en el archivo de código de este libro. Si es así,
habrás notado que los colocamos en paquetes. Si bien la organización de clases dentro de paquetes es un
tema confuso sin grandes prácticas recomendadas disponibles, hemos adoptado un enfoque diseñado
para hacer que los ejemplos sean fáciles de localizar mientras lees el libro. Para ejemplos pequeños en un
capítulo, verás un paquete como `ch05`. Para el ejemplo de juego en curso, usamos `game`. Podríamos
reescribir nuestros primeros ejemplos para que encajen en este esquema fácilmente:

Para lograr esto, necesitaríamos crear la estructura de carpetas `ch02` y luego colocar nuestro archivo
`HelloJava.java` en esa carpeta `ch02`. Luego podríamos compilar y ejecutar el ejemplo desde la línea de
comandos manteniéndonos en la parte superior de la jerarquía de carpetas y utilizando la ruta
completamente calificada del archivo y el nombre de la clase, de la siguiente manera:
Si estás utilizando un entorno de desarrollo integrado (IDE), este gestionará felizmente estos problemas
de paquetes por ti. Simplemente crea y organiza tus clases y continúa identificando la clase principal que
inicia tu programa.

Visibilidad de miembros y acceso

Hemos hablado un poco sobre los modificadores de acceso que puedes usar al declarar variables y
métodos. Hacer algo público significa que cualquiera, en cualquier lugar, puede ver tu variable o llamar a
tu método. Hacer algo protegido significa que cualquier subclase puede acceder a la variable, llamar al
método o anular el método para proporcionar alguna funcionalidad alternativa más adecuada para tu
subclase. El modificador privado significa que la variable o el método solo están disponibles dentro de la
propia clase.

Los paquetes afectan a los miembros protegidos. Además de ser accesibles por cualquier subclase, dichos
miembros son visibles y anulables por otras clases en ese paquete. Los paquetes también entran en juego
si omites el modificador por completo. Considera algunos componentes de texto de ejemplo en el paquete
personalizado `mytools.text`, como se muestra en la Figura 5-4.

La clase `TextComponent` no tiene ningún modificador. Tiene una visibilidad predeterminada o visibilidad
"privada de paquete". Esto significa que otras clases en el mismo paquete pueden acceder a la clase, pero
aquellas clases fuera del paquete no pueden. Esto puede ser muy útil para clases específicas de
implementación o ayudantes internos. Puedes usar libremente los elementos privados del paquete, pero
otros programadores solo utilizarán tus elementos públicos y protegidos.

La Figura 5-5 muestra más detalles con variables y métodos que son utilizados tanto por subclases como
por código externo.
Observa que al extender la clase `TextArea` te da acceso a los métodos públicos `getText()` y `setText()`,
así como al método protegido `formatText()`. Pero `MyTextDisplay` (más sobre subclases y extensiones
en breve en "Subclases e Herencia" en la página 156) no tiene acceso a la variable de paquete privado
`linecount`. Sin embargo, dentro del paquete `mytools.text` donde creamos la clase `TextEditor`, podemos
acceder a `linecount` así como a aquellos métodos que son públicos o protegidos. Nuestro
almacenamiento interno para el contenido, `text`, permanece privado y no está disponible para nadie
más que la clase `TextArea` misma.

La Tabla 5-2 resume los niveles de visibilidad disponibles en Java; generalmente van desde los más
restrictivos a los menos restrictivos. Los métodos y variables siempre son visibles dentro de la propia clase
que los declara, por lo que la tabla no aborda ese ámbito.

Compilar con Paquetes

Ya has visto algunos ejemplos de cómo usar un nombre de clase completamente calificado para compilar
un ejemplo simple. Si no estás utilizando un entorno de desarrollo integrado (IDE), tienes otras opciones
disponibles. Por ejemplo, es posible que desees compilar todas las clases en un paquete dado. Si es así,
puedes hacer lo siguiente:

Es importante destacar que para aplicaciones comerciales, a menudo se utilizan nombres de paquetes
más complejos para evitar colisiones. Una práctica común es revertir el nombre de dominio de Internet
de tu empresa. Por ejemplo, este libro de O'Reilly podría utilizar un prefijo de paquete completo como
com.oreilly.learningjava5e. Cada capítulo sería un subpaquete bajo ese prefijo. Compilar y ejecutar clases
en dichos paquetes es bastante directo, aunque un poco verbose:

El comando javac también comprende la dependencia básica de clases. Si tu clase principal utiliza otras
clases en la misma jerarquía de origen, incluso si no todas están en el mismo paquete, al compilar esa
clase principal se "recogerán" las otras clases dependientes y se compilarán también.

Sin embargo, más allá de programas simples con unas pocas clases, es más probable que confíes en tu IDE
o en una herramienta de gestión de compilación como Gradle o Maven. Esas herramientas están fuera
del alcance de este libro, pero existen muchas referencias en línea sobre ellas. Maven, en particular, es
excelente para gestionar proyectos grandes con muchas dependencias. Consulta "Maven: The Definitive
Guide" del creador de Maven, Jason Van Zyl, y su equipo en Sonatype (O'Reilly) para explorar las
características y capacidades de esta herramienta popular.

Diseño Avanzado de Clases

Tal vez recuerdes de "HelloJava2: The Sequel" en la página 53 que teníamos dos clases en el mismo
archivo. Eso simplificó el proceso de compilación, pero no otorgó a ninguna clase un acceso especial a la
otra. A medida que comiences a pensar en problemas más complejos, encontrarás casos en los que un
diseño de clases más avanzado que sí otorgue un acceso especial no solo es útil, sino fundamental para
escribir código mantenible.

Herencia y Subclases

Las clases en Java existen en una jerarquía. Una clase en Java puede declararse como una subclase de otra
clase utilizando la palabra clave extends. Una subclase hereda variables y métodos de su superclase y
puede usarlos como si estuvieran declarados dentro de la propia subclase:

En este ejemplo, un objeto de tipo Mammal tiene tanto la variable de instancia weight como el método
eat(). Estos son heredados de Animal.

Una clase puede extender solo otra clase. Utilizando la terminología adecuada, Java permite la herencia
única de la implementación de clases. Más adelante en este capítulo, hablaremos sobre interfaces, que
toman el lugar de la herencia múltiple tal como se usa principalmente en otros lenguajes.

Una subclase puede ser aún más especializada. Normalmente, la subclasificación especializa o perfecciona
una clase agregando variables y métodos (no puedes eliminar ni ocultar variables o métodos mediante la
subclasificación). Por ejemplo:

La clase Cat es un tipo de Mammal que, en última instancia, es un tipo de Animal. Los objetos de tipo Cat
heredan todas las características de los objetos de tipo Mammal y, a su vez, de los objetos de tipo Animal.
Además, la clase Cat proporciona comportamiento adicional en forma del método purr() y la variable
longHair. Podemos representar la relación entre las clases en un diagrama, como se muestra en la Figura
5-6.
Una subclase hereda todos los miembros de su superclase que no estén designados como privados. Como
discutiremos en breve, otros niveles de visibilidad afectan qué miembros heredados de la clase pueden
ser vistos desde fuera de la clase y sus subclases, pero como mínimo, una subclase siempre tiene el mismo
conjunto de miembros visibles que su clase padre. Por esta razón, el tipo de una subclase puede
considerarse un subtipo de su clase padre, y las instancias del subtipo pueden utilizarse en cualquier lugar
donde se permitan instancias del supertipo. Considera el siguiente ejemplo:

La instancia de Cat, llamada simon en este ejemplo, puede asignarse a la variable de tipo Animal,
'creature', porque Cat es un subtipo de Animal. De manera similar, cualquier método que acepte un objeto
Animal aceptaría también una instancia de un Cat o cualquier tipo de Mammal. Este es un aspecto
importante del polimorfismo en un lenguaje orientado a objetos como Java. Veremos cómo se puede
utilizar para refinar el comportamiento de una clase, así como para añadir nuevas capacidades a la misma.

Variables sombreadas

Hemos visto que una variable local con el mismo nombre que una variable de instancia sombrea (oculta)
la variable de instancia. De manera similar, una variable de instancia en una subclase puede sombrear a
una variable de instancia del mismo nombre en su clase padre, como se muestra en la Figura 5-7.

Vamos a cubrir los detalles de este ocultamiento de variables ahora para completitud y en preparación
para temas más avanzados, pero en la práctica casi nunca deberías hacer esto. Es mucho mejor en la
práctica estructurar tu código para diferenciar claramente las variables usando diferentes nombres o
convenciones de nomenclatura.

En la Figura 5-7, la variable 'weight' se declara en tres lugares: como una variable local en el método
'foodConsumption()' de la clase Mammal, como una variable de instancia de la clase Mammal, y como
una variable de instancia de la clase Animal. La variable exacta seleccionada cuando se hace referencia a
ella en el código dependería del ámbito en el que estemos trabajando y de cómo califiques la referencia
a la misma.
En el ejemplo anterior, todas las variables eran del mismo tipo. Un uso ligeramente más plausible de
variables sombreadas implicaría cambiar sus tipos. Podríamos, por ejemplo, sombrear una variable de
tipo int con una variable de tipo double en una subclase que necesita valores decimales en lugar de valores
enteros. Podemos hacer esto sin cambiar el código existente porque, como su nombre sugiere, cuando
sombreamos variables, no las reemplazamos sino que las ocultamos. Ambas variables siguen existiendo;
los métodos de la superclase ven la variable original, y los métodos de la subclase ven la nueva versión.
La determinación de qué variables ven los distintos métodos ocurre en tiempo de compilación.

Aquí tienes un ejemplo sencillo:

En este ejemplo, sombrearemos la variable de instancia "sum" para cambiar su tipo de int a double. Los
métodos definidos en la clase IntegerCalculator verán la variable de tipo entero "sum", mientras que los
métodos definidos en DecimalCalculator verán la variable de tipo flotante "sum". Sin embargo, ambas
variables realmente existen para una instancia dada de DecimalCalculator y pueden tener valores
independientes. De hecho, cualquier método que DecimalCalculator herede de IntegerCalculator verá la
variable de tipo entero "sum".

Dado que ambas variables existen en DecimalCalculator, necesitamos una manera de hacer referencia a
la variable heredada de IntegerCalculator. Hacemos eso utilizando la palabra clave "super" como
calificador en la referencia:

Dentro de DecimalCalculator, la palabra clave super utilizada de esta manera selecciona la variable sum
definida en la superclase. Explicaremos el uso de super más detalladamente en breve.

Otro punto importante sobre las variables sombreadas tiene que ver con cómo funcionan cuando nos
referimos a un objeto a través de un tipo menos derivado (un tipo padre). Por ejemplo, podemos
referirnos a un objeto DecimalCalculator como un IntegerCalculator usándolo a través de una variable de
tipo IntegerCalculator. Si hacemos esto y luego accedemos a la variable sum, obtenemos la variable
entera, no la decimal:

Lo mismo sería cierto si accediéramos al objeto usando una conversión explícita al tipo IntegerCalculator
o al pasar una instancia a un método que acepta ese tipo padre.

Para reiterar, la utilidad de las variables sombreadas es limitada. Es mucho mejor abstraer el uso de
variables como esta de otras maneras que utilizar reglas de ámbito complicadas. Sin embargo, es
importante comprender los conceptos aquí antes de hablar sobre hacer lo mismo con métodos. Veremos
un tipo de comportamiento diferente y más dinámico cuando los métodos sombrean otros métodos, o
para usar la terminología correcta, anulan otros métodos.
Sobrescritura de métodos

Hemos visto que podemos declarar métodos sobrecargados (es decir, métodos con el mismo nombre
pero un número diferente o tipo de argumentos) dentro de una clase. La selección de métodos
sobrecargados funciona de la manera que describimos en todos los métodos disponibles para una clase,
incluidos los heredados. Esto significa que una subclase puede definir métodos sobrecargados adicionales
que se suman a los métodos sobrecargados proporcionados por una superclase.

Una subclase puede hacer más que eso; puede definir un método que tiene exactamente la misma firma
de método (nombre y tipos de argumentos) que un método en su superclase. En ese caso, el método en
la subclase anula el método en la superclase y reemplaza efectivamente su implementación, como se
muestra en la Figura 5-8. Sustituir métodos para cambiar el comportamiento de los objetos se llama
polimorfismo de subtipos. Es el uso que la mayoría de la gente piensa cuando habla sobre el poder de los
lenguajes orientados a objetos.

En la Figura 5-8, Mammal anula el método reproduce() de Animal, tal vez para especializar el método para
el comportamiento de los mamíferos que dan a luz crías vivas.7 El comportamiento de dormir del objeto
Cat también se anula para ser diferente al de un Animal general, tal vez para acomodar las siestas de los
gatos. La clase Cat también agrega comportamientos más únicos como el ronroneo y la caza de ratones.

Por lo que has visto hasta ahora, los métodos anulados probablemente parecen sombrear métodos en
superclases, al igual que lo hacen las variables. Pero los métodos anulados son realmente más poderosos
que eso. Cuando hay múltiples implementaciones de un método en la jerarquía de herencia de un objeto,
el que se encuentra en la "clase más derivada" (la más hacia abajo en la jerarquía) siempre anula a los
demás, incluso si nos referimos al objeto a través de una referencia de uno de los tipos de superclase.8

Por ejemplo, si tenemos una instancia de Cat asignada a una variable del tipo más general Animal, y
llamamos a su método sleep(), todavía obtenemos el método sleep() implementado en la clase Cat, no el
de Animal:

En otras palabras, para fines de comportamiento (invocación de métodos), un Cat actúa como un Cat,
independientemente de cómo te refieras a él. En otros aspectos, la variable "creature" aquí puede
comportarse como una referencia de Animal. Como explicamos anteriormente, el acceso a una variable
sombreada a través de una referencia de Animal encontraría una implementación en la clase Animal, no
en la clase Cat. Sin embargo, debido a que los métodos se ubican dinámicamente, buscando primero en
las subclases, se invoca el método apropiado en la clase Cat, incluso si lo estamos tratando de manera
más general como un objeto Animal. Esto significa que el comportamiento de los objetos es dinámico.
Podemos tratar con objetos especializados como si fueran tipos más generales y aún aprovechar sus
implementaciones especializadas de comportamiento.
Interfaces

Java amplía el concepto de métodos abstractos con interfaces. A menudo es deseable especificar un grupo
de métodos abstractos que definan algún comportamiento para un objeto sin vincularlo a ninguna
implementación en absoluto. En Java, esto se llama una interfaz. Una interfaz define un conjunto de
métodos que una clase debe implementar. Una clase en Java puede declarar que implementa una interfaz
si implementa los métodos requeridos. A diferencia de extender una clase abstracta, una clase que
implementa una interfaz no tiene que heredar de ninguna parte particular de la jerarquía de herencia ni
utilizar una implementación específica.

Las interfaces son algo así como las insignias de mérito de los Boy Scouts o Girl Scouts. Un scout que ha
aprendido a construir una casita para pájaros puede pasear luciendo un pequeño parche en la manga con
una imagen de esa insignia. Esto dice al mundo: "Sé cómo construir una casita para pájaros". De manera
similar, una interfaz es una lista de métodos que define algún conjunto de comportamientos para un
objeto. Cualquier clase que implemente cada método listado en la interfaz puede declarar en tiempo de
compilación que implementa la interfaz y usar, como su insignia de mérito, un tipo adicional: el tipo de la
interfaz.

Los tipos de interfaz actúan como tipos de clase. Puedes declarar variables para ser de un tipo de interfaz,
puedes declarar argumentos de métodos para aceptar tipos de interfaz y puedes especificar que el tipo
de retorno de un método es un tipo de interfaz. En cada caso, lo que se quiere decir es que cualquier
objeto que implemente la interfaz (es decir, que use la insignia de mérito correcta) puede desempeñar
ese papel. En este sentido, las interfaces son ortogonales a la jerarquía de clases. Cortan a través de los
límites de qué tipo de objeto es un elemento y tratan con él solo en términos de lo que puede hacer. Una
clase puede implementar tantas interfaces como desee. De esta manera, las interfaces en Java
reemplazan gran parte de la necesidad de la herencia múltiple en otros lenguajes (y todas sus
complicaciones desordenadas).

Una interfaz se ve, en esencia, como una clase puramente abstracta (es decir, una clase con solo métodos
abstractos). Defines una interfaz con la palabra clave interface y listas sus métodos sin cuerpos, solo
prototipos (firmas):

El ejemplo anterior define una interfaz llamada Driveable con cuatro métodos. Es aceptable, pero no
necesario, declarar los métodos en una interfaz con el modificador abstract; no lo hemos hecho aquí. Más
importante aún, los métodos de una interfaz siempre se consideran públicos y puedes declararlos
opcionalmente como tal.

¿Por qué públicos? Bueno, el usuario de la interfaz no necesariamente podría verlos de otra manera, y las
interfaces generalmente están destinadas a describir el comportamiento de un objeto, no su
implementación.

Las interfaces definen capacidades, por lo que es común nombrar las interfaces según sus capacidades.
Driveable, Runnable y Updateable son buenos nombres de interfaces. Cualquier clase que implemente
todos los métodos puede declarar que implementa la interfaz usando una cláusula especial implements
en su definición de clase. Por ejemplo:
Aquí, la clase Automobile implementa los métodos de la interfaz Driveable y se declara a sí misma como
un tipo de Driveable utilizando la palabra clave implements.

Como se muestra en la Figura 5-9, otra clase, como Lawnmower, también puede implementar la interfaz
Driveable. La figura ilustra la interfaz Driveable siendo implementada por dos clases diferentes. Si bien es
posible que tanto Automobile como Lawnmower pudieran derivar de algún tipo primitivo de vehículo, no
es necesario que lo hagan en este escenario.

Después de declarar la interfaz, tenemos un nuevo tipo, Driveable. Podemos declarar variables de tipo
Driveable y asignarles cualquier instancia de un objeto Driveable:
Ambos Automobile y Lawnmower implementan Driveable, por lo que pueden considerarse objetos
intercambiables de ese tipo.

Clases internas

Todas las clases que hemos visto hasta ahora en este libro han sido clases de nivel superior,
"independientes", declaradas en el nivel de archivo y paquete. Pero las clases en Java realmente se
pueden declarar en cualquier nivel de ámbito, dentro de cualquier conjunto de llaves (es decir, casi en
cualquier lugar donde podrías poner cualquier otra instrucción en Java). Estas clases internas pertenecen
a otra clase o método como lo haría una variable, y su visibilidad puede estar limitada a su ámbito de la
misma manera. Las clases internas son una facilidad útil y estéticamente agradable para estructurar el
código. Sus equivalentes, las clases internas anónimas, son una forma aún más potente y concisa que hace
parecer como si pudieras crear nuevos tipos de objetos dinámicamente dentro del entorno estáticamente
tipado de Java. En Java, las clases internas anónimas desempeñan parte del papel de los cierres en otros
lenguajes, dando el efecto de manejar el estado y el comportamiento de manera independiente de las
clases.

Sin embargo, al adentrarnos en su funcionamiento interno, veremos que las clases internas no son tan
estéticamente agradables ni dinámicas como parecen. Las clases internas son puramente azúcar
sintáctico; no son compatibles con la Máquina Virtual y en su lugar son mapeadas a clases Java regulares
por el compilador. Como programador, es posible que nunca necesites ser consciente de esto;
simplemente puedes confiar en las clases internas como cualquier otro constructo del lenguaje. Sin
embargo, deberías saber un poco sobre cómo funcionan las clases internas para comprender mejor el
código compilado y algunos posibles efectos secundarios.

Las clases internas son esencialmente clases anidadas. Por ejemplo:

Aquí, la clase Brain es una clase interna: es una clase declarada dentro del ámbito de la clase Animal.
Aunque los detalles de lo que eso significa requieren un poco de explicación, comenzaremos diciendo que
Java intenta hacer que el significado, tanto como sea posible, sea similar al de los otros miembros
(métodos y variables) que viven en ese nivel de ámbito. Por ejemplo, agreguemos un método a la clase
Animal:

Tanto la clase interna Brain como el método performBehavior() están dentro del ámbito de Animal. Por
lo tanto, en cualquier lugar dentro de Animal, podemos referirnos directamente a Brain y
performBehavior() por su nombre. Dentro de Animal, podemos llamar al constructor de Brain (new
Brain()) para obtener un objeto Brain o invocar performBehavior() para llevar a cabo la función de ese
método. Pero ni Brain ni performBehavior() son generalmente accesibles fuera de la clase Animal sin
alguna calificación adicional.

Dentro del cuerpo de la clase interna Brain y el cuerpo del método performBehavior(), tenemos acceso
directo a todos los demás métodos y variables de la clase Animal. Así como el método performBehavior()
podría funcionar con la clase Brain y crear instancias de Brain, los métodos dentro de la clase Brain pueden
invocar el método performBehavior() de Animal, así como trabajar con cualquier otro método y variable
declarados en Animal. La clase Brain "ve" todos los métodos y variables de la clase Animal directamente
en su ámbito.

Esa última parte tiene consecuencias importantes. Desde dentro de Brain, podemos invocar el método
performBehavior(); es decir, desde dentro de una instancia de Brain, podemos invocar el método
performBehavior() de una instancia de Animal. Bueno, ¿cuál instancia de Animal? Si tenemos varios
objetos Animal alrededor (digamos, algunos Gatos y Perros), necesitamos saber de cuál
performBehavior() estamos hablando. ¿Qué significa que una definición de clase esté "dentro" de otra
definición de clase? La respuesta es que un objeto Brain siempre vive dentro de una única instancia de
Animal: aquella a la que se le informó cuando se creó. Llamaremos al objeto que contiene cualquier
instancia de Brain su instancia contenedora.

Un objeto Brain no puede existir fuera de una instancia contenedora de un objeto Animal. Donde sea que
veas una instancia de Brain, estará vinculada a una instancia de Animal. Aunque es posible construir un
objeto Brain desde otro lugar (es decir, desde otra clase), Brain siempre requiere una instancia
contenedora de Animal para "contenerlo". También diremos ahora que si Brain va a ser referido desde
fuera de Animal, actúa algo así como una clase Animal.Brain. Y al igual que con el método
performBehavior(), se pueden aplicar modificadores para restringir su visibilidad. Se aplican todos los
modificadores de visibilidad habituales, y las clases internas también pueden declararse estáticas, como
discutiremos más adelante.

Clases internas anónimas

Ahora llegamos a la mejor parte. Como regla general, cuanto más encapsuladas y limitadas en alcance
sean nuestras clases, más libertad tendremos para nombrarlas. Vimos esto en nuestro ejemplo anterior
del iterador. Este no es solo un problema puramente estético. La nomenclatura es una parte importante
de escribir código legible y mantenible. Generalmente queremos usar los nombres más concisos y
significativos posibles. Una consecuencia de esto es que preferimos evitar otorgar nombres a objetos
puramente efímeros que se usarán solo una vez.

Las clases internas anónimas son una extensión de la sintaxis de la operación new. Cuando creas una clase
interna anónima, combinas una declaración de clase con la asignación de una instancia de esa clase,
creando efectivamente una clase "única" y una instancia de clase en una sola operación. Después de la
palabra clave new, especificas el nombre de una clase o una interfaz, seguido de un cuerpo de clase. El
cuerpo de la clase se convierte en una clase interna, que ya sea extiende la clase especificada o, en el caso
de una interfaz, se espera que implemente la interfaz. Se crea una única instancia de la clase y se devuelve
como el valor.

Por ejemplo, podríamos volver a visitar la aplicación gráfica de "HelloJava2: The Sequel" en la página 53
que crea un HelloComponent2 que extiende JComponent e implementa la interfaz MouseMotionListener.
Al observar el ejemplo un poco más de cerca, nunca esperamos que HelloComponent2 responda a
eventos de movimiento del mouse provenientes de otros componentes. Podría tener más sentido crear
una clase interna anónima específicamente para mover nuestra etiqueta "Hello". De hecho, dado que
HelloComponent2 está destinado realmente solo para nuestro demo, podríamos refactorizar (un proceso
común de desarrollo realizado para optimizar o mejorar código que ya está funcionando) esa clase
separada en una clase interna. Ahora que conocemos un poco más sobre constructores e herencia,
también podríamos convertir nuestra clase en una extensión de JFrame en lugar de construir un marco
dentro de nuestro método main().

Aquí está nuestro HelloJava3 con solo estas refactorizaciones en su lugar:


Intenta compilar y ejecutar este ejemplo. Debería comportarse exactamente como lo hace la aplicación
original HelloJava2. La diferencia real está en cómo hemos organizado las clases y quién puede acceder a
ellas (y a las variables y métodos dentro de ellas).

Organización de Contenido y Planificación para Fallos

Las clases son la idea más importante en Java. Forman el núcleo de cada programa ejecutable, biblioteca
portátil o ayudante. Hemos examinado el contenido de las clases y cómo se relacionan en un proyecto
más grande. Sabemos más sobre cómo crear y destruir objetos basados en las clases que escribimos. Y
hemos visto cómo las clases internas (y las clases internas anónimas) pueden ayudarnos a escribir código
más mantenible.

Veremos más de estas clases internas a medida que profundicemos en temas como los hilos en el Capítulo
9 y Swing en el Capítulo 10.
Al construir tus clases, hay algunas pautas a tener en cuenta:

• Oculta tanto de tu implementación como sea posible. Nunca expongas más de los internos de
un objeto de lo que necesitas. Esto es clave para construir código mantenible y reutilizable. Evita
variables públicas en tus objetos, con la posible excepción de constantes. En su lugar, define
métodos de acceso para establecer y devolver valores (incluso si son tipos simples). Más
adelante, cuando sea necesario, podrás modificar y extender el comportamiento de tus objetos
sin romper otras clases que dependan de ellos.

• Especializa objetos solo cuando sea necesario: usa composición en lugar de herencia. Cuando
usas un objeto en su forma existente, como parte de un objeto nuevo, estás componiendo
objetos. Cuando cambias o refinas el comportamiento de un objeto (mediante subclases), estás
usando herencia. Deberías intentar reutilizar objetos mediante composición en lugar de herencia
siempre que sea posible, porque al componer objetos, estás aprovechando al máximo las
herramientas existentes. La herencia implica romper la encapsulación de un objeto y solo se debe
hacer cuando haya una ventaja real. Pregúntate si realmente necesitas heredar toda la clase
(¿quieres ser un "tipo" de ese objeto?) o si simplemente puedes incluir una instancia de esa clase
en tu propia clase y delegar algo de trabajo al objeto incluido.

• Minimiza las relaciones entre objetos e intenta organizar objetos relacionados en paquetes. Las
clases que trabajan estrechamente pueden agruparse usando paquetes de Java (recuerda la
Figura 5-1), que también pueden ocultar aquellas que no son de interés general. Solo expone
clases que pretendas que otras personas utilicen. Cuanto menos acoplados estén tus objetos,
más fácil será reutilizarlos más adelante.

Podemos aplicar estos principios incluso en proyectos pequeños. La carpeta de ejemplos ch05 contiene
versiones simples de las clases e interfaces que usaremos para crear nuestro juego de lanzamiento de
manzanas. Tómate un momento para ver cómo las clases Apple, Tree y Physicist implementan la interfaz
GamePiece, como el método draw() que incluye cada clase. Observa cómo Field extiende JComponent y
cómo la clase principal del juego, AppleToss, extiende JFrame.

Puedes ver estas piezas jugando juntas en la sencilla Figura 5-10. Para probarlo por ti mismo, compila y
ejecuta la clase ch05.AppleToss utilizando los pasos discutidos anteriormente en "Paquetes
Personalizados" en la página 151.
Revisa los comentarios en las clases. Intenta ajustar algunas cosas. Agrega otro árbol.

Más juego siempre es bueno. Estaremos construyendo sobre estas clases a lo largo de los capítulos
restantes, así que familiarizarse con cómo encajan hará más fácil leer las próximas discusiones.

Independientemente de cómo organices los miembros en tus clases, las clases en tus paquetes o los
paquetes en tu proyecto, tendrás que lidiar con errores que surjan. Algunos de esos errores son simples
errores de sintaxis que corregirás en tu editor. Otros errores son más interesantes y pueden surgir solo
mientras tu programa se está ejecutando. El próximo capítulo cubrirá la noción de Java sobre estos
problemas y te ayudará a manejarlos.
CAPÍTULO 6

Manejo de Errores y Registro

Java tiene sus raíces en sistemas integrados: software que se ejecuta dentro de dispositivos
especializados, como computadoras portátiles, teléfonos celulares y tostadoras sofisticadas que
podríamos considerar parte del internet de las cosas (IoT) en estos días. En ese tipo de aplicaciones, es
especialmente importante manejar los errores del software de manera robusta. La mayoría de los
usuarios estarían de acuerdo en que es inaceptable que su teléfono simplemente se bloquee o que su
tostada (y quizás su casa) se queme porque su software falló. Dado que no podemos eliminar la posibilidad
de errores de software, es un paso en la dirección correcta reconocer y tratar los errores anticipados a
nivel de aplicación de manera metódica.

El manejo de errores en algunos lenguajes es enteramente responsabilidad del programador. El lenguaje


en sí mismo no proporciona ayuda para identificar tipos de error ni herramientas para tratarlos
fácilmente. En el lenguaje C, una rutina generalmente indica un fallo al devolver un valor "inaceptable"
(por ejemplo, el idiomático -1 o nulo). Como programador, debes saber qué constituye un resultado malo
y qué significa. A menudo es incómodo trabajar con las limitaciones de pasar valores de error en el camino
normal del flujo de datos.1 Un problema aún peor es que ciertos tipos de errores pueden ocurrir
legítimamente casi en cualquier lugar, y es prohibitivo e irrazonable probarlos explícitamente en cada
punto del software.

En este capítulo consideraremos cómo Java aborda el problema de, bueno, problemas. Revisaremos la
noción de excepciones para analizar cómo y por qué ocurren, así como cómo y dónde manejarlas.
También veremos errores y afirmaciones. Los errores representan problemas más serios que a menudo
no se pueden solucionar en tiempo de ejecución, pero aún pueden registrarse para depuración. Las
afirmaciones son una forma popular de proteger tu código contra excepciones o errores verificando que
existan condiciones seguras de antemano.

Excepciones

Java ofrece una solución elegante para ayudar al programador a abordar problemas comunes de
codificación y de tiempo de ejecución a través de excepciones. (El manejo de excepciones en Java es
similar, pero no del todo igual, al manejo de excepciones en C++). Una excepción indica una condición
inusual o un estado de error. El control del programa se transfiere incondicionalmente o se "lanza" a una
sección especialmente designada del código donde se captura y maneja esa condición. De esta manera,
el manejo de errores es independiente del flujo normal del programa. No necesitamos valores de retorno
especiales para todos nuestros métodos; los errores son manejados por un mecanismo separado. El
control puede ser pasado a una gran distancia desde una rutina profundamente anidada y manejado en
una sola ubicación cuando sea deseable, o un error puede ser manejado inmediatamente en su origen.
Algunos métodos estándar de la API de Java todavía devuelven -1 como un valor especial, pero
generalmente se limitan a situaciones en las que esperamos un valor especial y la situación no está
realmente fuera de los límites.2

Se requiere que un método de Java especifique las excepciones comprobadas que puede lanzar, y el
compilador se asegura de que los llamadores del método las manejen. De esta manera, la información
sobre los errores que un método puede producir se eleva al mismo nivel de importancia que sus
argumentos y tipos de retorno. Aún puedes decidir ignorar explícitamente errores obvios, pero en Java
debes hacerlo de manera explícita. (Discutiremos las excepciones y errores en tiempo de ejecución, que
no se requieren declarar ni manejar en el método, en un momento).
Excepciones y Clases de Errores

Las excepciones están representadas por instancias de la clase java.lang.Exception y sus subclases. Las
subclases de Exception pueden contener información especializada (y posiblemente comportamiento)
para diferentes tipos de condiciones excepcionales. Sin embargo, con más frecuencia son simplemente
subclases "lógicas" que sirven solo para identificar un nuevo tipo de excepción. La Figura 6-1 muestra las
subclases de Exception en el paquete java.lang. Debería darte una idea de cómo se organizan las
excepciones; entraremos en más detalles sobre la organización de clases en el próximo capítulo. La
mayoría de los otros paquetes definen sus propios tipos de excepciones, que generalmente son subclases
de Exception en sí misma o de su importante subclase RuntimeException, a la que llegaremos en un
momento.

Por ejemplo, otra clase de excepción importante es IOException en el paquete java.io. La clase
IOException extiende Exception y tiene muchas subclases para problemas típicos de E/S (como
FileNotFoundException) y problemas de redes (como MalformedURLException). Las excepciones de red
pertenecen al paquete java.net.

Un objeto Exception es creado por el código en el punto donde surge la condición de error. Puede ser
diseñado para contener cualquier información necesaria para describir la condición excepcional e incluye
también un rastreo completo de la pila (stack trace) para depuración. Un rastreo de la pila es la lista (a
veces engorrosa) de todos los métodos llamados y el orden en que fueron llamados hasta el punto donde
se lanzó la excepción. Veremos estas útiles listas con más detalle en "Rastreos de Pila" en la página 176.
El objeto Exception se pasa como un argumento al bloque de código de manejo, junto con el flujo de
control. Aquí es de donde provienen los términos "lanzar" (throw) y "capturar" (catch): el objeto Exception
es lanzado desde un punto en el código y capturado por otro, donde se reanuda la ejecución.

La API de Java también define la clase java.lang.Error para errores irreparables. Las subclases de Error en
el paquete java.lang se muestran en la Figura 6-2. Un tipo de Error notable es AssertionError, que es
utilizado por la instrucción assert de Java para indicar un fallo (las afirmaciones se discuten más adelante
en este capítulo). Algunos otros paquetes definen sus propias subclases de Error, pero las subclases de
Error son mucho menos comunes (y menos útiles) que las subclases de Exception. Generalmente no
necesitas preocuparte por estos errores en tu código (es decir, no es necesario capturarlos); están
destinados a indicar problemas fatales o errores de la máquina virtual. Por lo general, un error de este
tipo provoca que el intérprete de Java muestre un mensaje y se cierre. Se desaconseja encarecidamente
intentar capturarlos o recuperarse de ellos porque se supone que indican un error fatal en el programa,
no una condición rutinaria.
Tanto Exception como Error son subclases de Throwable. La clase Throwable es la clase base para objetos
que pueden ser "lanzados" con la instrucción throw. En general, deberías extender solo Exception, Error,
o una de sus subclases.

Manejo de Excepciones

Las declaraciones de protección try/catch envuelven un bloque de código y capturan tipos designados de
excepciones que ocurren dentro de él:

En este ejemplo, las excepciones que ocurren dentro del cuerpo de la sección try del enunciado son
dirigidas a la cláusula catch para un posible manejo. La cláusula catch actúa como un método; especifica
como argumento el tipo de excepción que quiere manejar y si es invocado, recibe el objeto Exception
como argumento. Aquí, recibimos el objeto en la variable 'e' e imprimimos un mensaje junto con este.

Podemos probar esto nosotros mismos. Recuerda el programa simple para calcular el máximo común
denominador usando el algoritmo de Euclides en el Capítulo 4. Podríamos mejorar ese programa para
permitir al usuario pasar los dos números 'a' y 'b' como argumentos de línea de comandos a través de ese
array args[] en el método main(). Sin embargo, ese array es de tipo String. Si avanzamos un poco y
utilizamos un método de análisis que cubrimos en "Análisis de Números Primitivos" en la página 229,
podemos convertir esos argumentos en valores int. Sin embargo, ese método de análisis puede lanzar una
excepción si no pasamos un número válido. Aquí tienes un vistazo a nuestra nueva clase Euclid2:
Si ejecutamos este programa desde una ventana de terminal o utilizamos la opción de argumentos de
línea de comandos en nuestro entorno de desarrollo integrado (IDE) como hicimos en la Figura 2-9,
podemos probar varios números sin necesidad de recompilar el código.

Pero si ingresamos argumentos que no son numéricos, obtendremos esa NumberFormatException y


veremos nuestro mensaje de error. Sin embargo, debemos tener en cuenta que nos recuperamos de
manera adecuada y aún así proporcionamos cierta salida. Esta es la esencia del manejo de errores.
Siempre te encontrarás con errores en el mundo real. La forma en que los manejes ayuda a mostrar la
calidad de tu código.

Un enunciado try puede tener múltiples cláusulas catch que especifican diferentes tipos (subclases) de
Exception:
Las cláusulas catch son evaluadas en orden, y se toma la primera coincidencia asignable. A lo sumo, se
ejecuta una cláusula catch, lo que significa que las excepciones deben enumerarse de más específicas a
menos específicas. En el ejemplo anterior, anticipamos que la hipotética función readFromFile() puede
lanzar dos tipos diferentes de excepciones: una por no encontrar un archivo y otra por un error de lectura
más general. En el ejemplo anterior, FileNotFoundException es una subclase de IOException, por lo que si
la primera cláusula catch no estuviera allí, la excepción sería capturada por la segunda en este caso. De
manera similar, cualquier subclase de Exception es asignable al tipo padre Exception, por lo que la tercera
cláusula catch capturaría cualquier cosa pasada por las dos primeras. Actúa aquí como la cláusula
predeterminada en una declaración switch y maneja cualquier posibilidad restante. Lo hemos mostrado
aquí por completitud, pero en general, deseas ser lo más específico posible en los tipos de excepciones
que capturas.

Una ventaja del esquema try/catch es que cualquier declaración en el bloque try puede asumir que todas
las declaraciones anteriores en el bloque se ejecutaron con éxito. Un problema no surgirá repentinamente
porque un programador olvidó verificar el valor de retorno de un método. Si una declaración anterior
falla, la ejecución salta inmediatamente a la cláusula catch; las declaraciones posteriores nunca se
ejecutan.

A partir de Java 7, existe una alternativa al usar múltiples cláusulas catch, y es manejar múltiples tipos de
excepciones discretas en una sola cláusula catch usando el símbolo "|" o sintaxis:

Usando este "|" o sintaxis, recibimos ambos tipos de excepción en la misma cláusula catch.

Entonces, ¿cuál es el tipo real de la variable 'e' que estamos pasando a nuestro método de registro?

(¿Qué podemos hacer con ello?) En este caso, no será ni ZipException ni SSLException, sino IOException,
que es el ancestro común más cercano de las dos excepciones (el tipo de clase padre más cercano al que
ambos son asignables). En muchos casos, el tipo común más cercano entre dos o más tipos de excepciones
puede ser simplemente Exception, el padre de todos los tipos de excepciones. La diferencia entre capturar
estos tipos de excepción discreta con una cláusula catch de múltiples tipos y simplemente capturar el tipo
común de excepción padre es que estamos limitando nuestra captura solo a estos tipos de excepción
específicamente enumerados y no capturaremos todos los otros tipos de IOException, como sería la
alternativa en este caso. La combinación de captura de múltiples tipos y ordenar las cláusulas catch desde
el más específico al más amplio (de tipos "estrechos" a "amplios") te brinda una gran flexibilidad para
estructurar tus cláusulas catch. Puedes consolidar la lógica de manejo de errores donde sea apropiado y
no repetir código. Hay más matices en esta característica, y volveremos a ella después de haber discutido
las "excepciones" y "re-lanzamiento de excepciones".

Propagación ascendente

¿Qué pasaría si no capturáramos la excepción? ¿A dónde iría? Bueno, si no hay una declaración try/catch
que la encierre, la excepción salta del método en el que se originó y es lanzada desde ese método hasta
su llamador. Si en ese punto del método de llamada está dentro de una cláusula try, el control pasa a la
cláusula catch correspondiente. De lo contrario, la excepción continúa propagándose hacia arriba por la
pila de llamadas, de un método a su llamador. De esta manera, la excepción va subiendo hasta que se
capture, o hasta que salga por la parte superior del programa, terminándolo con un mensaje de error en
tiempo de ejecución. Hay un poco más en ello que eso, porque en este caso, el compilador podría
habernos obligado a lidiar con ello en el camino. "Excepciones comprobadas y no comprobadas" en la
página 177 habla sobre esta distinción con más detalle.
Echemos un vistazo a otro ejemplo. En la Figura 6-3, el método getContent() invoca el método
openConnection() desde una declaración try/catch. A su vez, openConnection() invoca el método
sendRequest(), que llama al método write() para enviar algunos datos.

En esta figura, la segunda llamada a write() arroja una IOException. Dado que sendRequest() no contiene
una declaración try/catch para manejar la excepción, esta se lanza nuevamente desde el punto donde fue
llamada en el método openConnection(). Como openConnection() tampoco captura la excepción, se lanza
una vez más. Finalmente, es capturada por la declaración try en getContent() y manejada por su cláusula
catch.

Observa que cada método que lanza una excepción debe declarar con una cláusula throws que puede
lanzar el tipo particular de excepción. Discutiremos esto más adelante en "Excepciones comprobadas y
no comprobadas" en la página 177.

Agregar una declaración try de alto nivel al principio de tu código también puede ayudar a manejar errores
que podrían surgir de hilos de fondo. Discutiremos los hilos con mucho más detalle en el Capítulo 9, pero
vale la pena señalar aquí que las excepciones no capturadas pueden causar dolores de cabeza en la
depuración en programas más grandes y complejos.

Rastreos de pila

Debido a que una excepción puede propagarse bastante distancia antes de ser capturada y manejada,
puede que necesitemos una forma de determinar exactamente dónde se lanzó. También es muy
importante conocer el contexto de cómo se alcanzó el punto de la excepción; es decir, qué métodos
llamaron a qué métodos para llegar a ese punto. Para estos fines de depuración y registro, todas las
excepciones pueden volcar una pila de rastreo que lista su método de origen y todas las llamadas de
método anidadas que tomó para llegar allí. Comúnmente, el usuario ve un rastreo de pila cuando se
imprime utilizando el método printStackTrace().

Por ejemplo, la traza de pila (stack trace) para una excepción podría lucir de la siguiente manera:

Esta traza de pila indica que el método main() de la clase MyApplication llamó al método loadFile(). Luego,
el método loadFile() intentó construir un FileInputStream, lo que generó la FileNotFoundException.
Observa que una vez que la traza de la pila alcanza las clases del sistema Java (como FileInputStream), los
números de línea pueden perderse. Esto también puede ocurrir cuando el código es optimizado por
algunas máquinas virtuales. Por lo general, hay una forma de desactivar la optimización temporalmente
para encontrar los números de línea exactos. Sin embargo, en situaciones complicadas, cambiar el
momento de la aplicación puede afectar el problema que estás intentando depurar y puede requerir otras
técnicas de depuración.

Los métodos en la excepción te permiten recuperar la información de la traza de pila programáticamente


utilizando el método Throwable getStackTrace(). (Throwable es la clase base de Exception y Error). Este
método devuelve un arreglo de objetos StackTraceElement, cada uno de los cuales representa una
llamada de método en la pila. Puedes solicitar a un StackTraceElement detalles sobre la ubicación de ese
método usando los métodos getFileName(), getClassName(), getMethodName() y getLineNumber(). El
elemento cero del arreglo es la parte superior de la pila, la última línea de código que causó la excepción;
los elementos subsiguientes retroceden una llamada de método cada uno hasta que se alcanza el método
main() original.

Excepciones comprobadas y no comprobadas

Mencionamos anteriormente que Java nos obliga a ser explícitos sobre nuestro manejo de errores, pero
no es necesario requerir que cada tipo concebible de error sea manejado explícitamente en cada
situación. Por lo tanto, las excepciones en Java se dividen en dos categorías: comprobadas y no
comprobadas. La mayoría de las excepciones a nivel de aplicación son comprobadas, lo que significa que
cualquier método que lance una, ya sea generándola él mismo (como discutiremos en "Lanzamiento de
Excepciones" en la página 178) o al ignorar una que ocurra dentro de él, debe declarar que puede lanzar
ese tipo de excepción en una cláusula especial throws en la declaración de su método. Por ahora, todo lo
que necesitas saber es que los métodos deben declarar las excepciones comprobadas que pueden lanzar
o permitir lanzar.

Nuevamente en la Figura 6-3, observa que los métodos openConnection() y sendRequest() especifican
que pueden lanzar una IOException. Si tuviéramos que lanzar varios tipos de excepciones, podríamos
declararlos separados por comas:

La cláusula throws le indica al compilador que un método es una posible fuente de ese tipo de excepción
comprobada y que cualquiera que llame a ese método debe estar preparado para lidiar con ella. El
llamador debe entonces usar un bloque try/catch para manejarla, o a su vez, declarar que puede lanzar
la excepción desde sí mismo.

En contraste, las excepciones que son subclases de la clase java.lang.RuntimeException o de la clase


java.lang.Error son excepciones no comprobadas. Consulta la Figura 6-1 para ver las subclases de
RuntimeException. (Las subclases de Error generalmente se reservan para problemas graves de carga de
clases o problemas del sistema en tiempo de ejecución). No es un error en tiempo de compilación ignorar
la posibilidad de estas excepciones; los métodos tampoco tienen que declarar que pueden lanzarlas. En
todos los demás aspectos, las excepciones no comprobadas se comportan igual que otras excepciones.
Somos libres de capturarlas si lo deseamos, pero en este caso no estamos obligados a hacerlo.

Las excepciones comprobadas están destinadas a cubrir problemas a nivel de aplicación, como archivos
faltantes y hosts no disponibles. Como buenos programadores (y ciudadanos responsables), deberíamos
diseñar software para recuperarse de manera elegante de este tipo de condiciones.

Las excepciones no comprobadas están destinadas a problemas a nivel del sistema, como "falta de
memoria" y "índice de matriz fuera de límites". Si bien estas pueden indicar errores de programación a
nivel de aplicación, pueden ocurrir casi en cualquier lugar y generalmente no se pueden recuperar.
Afortunadamente, debido a que son excepciones no comprobadas, no tienes que envolver cada una de
tus operaciones de índice de matriz en un bloque try/catch (o declarar todos los métodos de llamada
como una posible fuente de ellas).

En resumen, las excepciones comprobadas son problemas que una aplicación razonable debería intentar
manejar elegantemente; las excepciones no comprobadas (excepciones en tiempo de ejecución o errores)
son problemas de los cuales normalmente no esperaríamos que nuestro software se recupere. Los tipos
de error son aquellos explícitamente destinados a ser condiciones de las que normalmente no
intentaríamos manejar o recuperarnos.

Lanzar Excepciones

Podemos lanzar nuestras propias excepciones, ya sea instancias de Exception, una de sus subclases
existentes o nuestras propias clases de excepción especializadas. Todo lo que tenemos que hacer es crear
una instancia de la Exception y lanzarla con la instrucción throw:

La ejecución se detiene y se transfiere a la declaración try/catch más cercana que puede manejar el tipo
de excepción. (No tiene mucho sentido mantener una referencia al objeto Exception que hemos creado
aquí). Un constructor alternativo nos permite especificar una cadena con un mensaje de error:

Puedes recuperar esta cadena utilizando el método getMessage() del objeto Exception. Sin embargo, a
menudo puedes simplemente imprimir (o usar toString()) el objeto de la excepción en sí para obtener el
mensaje y la traza de la pila.

Por convención, todos los tipos de Exception tienen un constructor de tipo String como este. El mensaje
de cadena anterior no es muy útil. Normalmente, lanzará una subclase de Exception más específica, que
captura detalles o al menos una explicación más específica en forma de cadena.

Aquí tienes otro ejemplo:

En este código, implementamos parcialmente un método para verificar una ruta ilegal. Si encontramos
una, lanzamos una SecurityException con información sobre la infracción. Por supuesto, podríamos incluir
cualquier otra información que sea útil en nuestras propias subclases especializadas de Exception. A
menudo, sin embargo, simplemente tener un nuevo tipo de excepción es suficiente porque es suficiente
para ayudar a dirigir el flujo de control. Por ejemplo, si estamos construyendo un analizador, podríamos
querer crear nuestro propio tipo de excepción para indicar un tipo particular de falla:
Mira Constructores en la página 145 para obtener una descripción completa de las clases y constructores
de clases.

El cuerpo de nuestra clase Exception aquí simplemente permite crear una ParseException de las formas
convencionales en las que hemos creado excepciones anteriormente (ya sea de manera genérica o con
un poco de información adicional). Ahora que tenemos nuestro nuevo tipo de excepción, podemos
proteger así:

Como puedes ver, incluso sin la información especial como el número de línea donde nuestro input causó
un problema, nuestra excepción personalizada nos permite distinguir un error de análisis de un error
arbitrario de E/S en el mismo bloque de código.

Encadenamiento y relanzamiento de excepciones

A veces querrás tomar alguna acción basada en una excepción y luego lanzar una nueva excepción en su
lugar. Esto es común al construir frameworks donde las excepciones detalladas de bajo nivel son
manejadas y representadas por excepciones de nivel superior que pueden ser gestionadas más
fácilmente. Por ejemplo, podrías querer capturar una IOException en un paquete de comunicaciones,
posiblemente realizar alguna limpieza y finalmente lanzar una excepción de nivel superior propia, tal vez
algo como LostServerConnection.

Puedes hacer esto de manera obvia simplemente capturando la excepción y luego lanzando una nueva,
pero entonces pierdes información importante, incluyendo la traza de la pila de la excepción original
"causal". Para lidiar con esto, puedes usar la técnica de "excepción encadenada". Esto significa que
incluyes la excepción causal en la nueva excepción que lanzas. Java tiene soporte explícito para el
encadenamiento de excepciones. La clase base Exception puede ser construida con una excepción como
argumento o con el mensaje String estándar y una excepción:

Puedes acceder a la excepción envuelta más tarde utilizando el método getCause(). Más importante aún,
Java imprime automáticamente tanto las excepciones como sus respectivas trazas de pila si imprimes la
excepción o si se muestra al usuario.

Puedes añadir este tipo de constructor a tus propias subclases de excepciones (delegando al constructor
padre) o puedes aprovechar este patrón utilizando el método Throwable initCause() para establecer
explícitamente la excepción causal después de construir tu excepción y antes de lanzarla:
A veces es suficiente simplemente realizar algún registro o tomar alguna acción y luego relanzar la
excepción original:

Reenvío de excepciones estrechado

Antes de Java 7, si querías manejar un conjunto de tipos de excepciones en una única cláusula catch y
luego relanzar la excepción original, inevitablemente terminarías ampliando el tipo de excepción
declarado a lo que se requería para capturar todas ellas o teniendo que hacer mucho trabajo para evitarlo.
En Java 7, el compilador se ha vuelto más inteligente y ahora puede hacer la mayor parte del trabajo por
nosotros al permitirnos estrechar el tipo de excepciones lanzadas de vuelta a los tipos originales en la
mayoría de los casos. Esto se explica mejor con un ejemplo:

En este ejemplo, somos extremadamente perezosos y simplemente capturamos todas las excepciones
con una cláusula catch Exception amplia para registrarlas antes de relanzarlas. Antes de Java 7, el
compilador habría insistido en que la cláusula throws de nuestro método declarara que lanza el tipo
amplio Exception también. Sin embargo, el compilador de Java ahora es lo suficientemente inteligente en
la mayoría de los casos como para analizar los tipos reales de excepciones que pueden ser lanzadas y
permitirnos prescribir el conjunto preciso de tipos. Lo mismo sería cierto si hubiéramos utilizado la
cláusula catch de múltiples tipos en este ejemplo, como probablemente habrías imaginado. Lo anterior
es un poco menos intuitivo, pero muy útil para fortalecer la especificidad del manejo de excepciones en
el código, incluido el código escrito antes de Java 7, sin requerir reajustes potencialmente complicados de
las cláusulas catch.

Intento Escalofriante (try creep)

La sentencia try impone una condición a las declaraciones que protege. Indica que si ocurre una excepción
dentro de ella, las declaraciones restantes se abandonan. Esto tiene consecuencias para la inicialización
de variables locales. Si el compilador no puede determinar si una asignación de variable local colocada
dentro de un bloque try/catch ocurrirá, no nos permitirá usar la variable. Por ejemplo:
En este ejemplo, no podemos usar 'foo' en el lugar indicado porque existe la posibilidad de que nunca se
le haya asignado un valor. Una opción obvia es mover la asignación dentro de la declaración try:

A veces esto funciona perfectamente bien. Sin embargo, ahora tenemos el mismo problema si queremos
usar 'bar' más adelante en 'miMetodo()'. Si no tenemos cuidado, podríamos terminar moviendo todo
dentro de la declaración try. La situación cambia, sin embargo, si transferimos el control fuera del método
en la cláusula catch:

El compilador es lo suficientemente inteligente para saber que si se hubiera producido un error en la


cláusula try, no habríamos llegado a la asignación de 'bar', por lo que nos permite hacer referencia a 'foo'.
Tu código dictará sus propias necesidades; simplemente debes ser consciente de las opciones.

La cláusula finally

¿Qué ocurre si tenemos algo importante que hacer antes de salir de nuestro método desde una de las
cláusulas catch? Para evitar duplicar el código en cada rama catch y hacer que la limpieza sea más explícita,
puedes usar la cláusula finally. Una cláusula finally puede añadirse después de un bloque try y de cualquier
cláusula catch asociada. Cualquier declaración en el cuerpo de la cláusula finally está garantizado que se
ejecutará sin importar cómo salga el control del cuerpo del try, ya sea que se haya lanzado una excepción
o no:

En este ejemplo, las declaraciones en el punto de limpieza se ejecutan eventualmente, sin importar cómo
salga el control del bloque try. Si el control se transfiere a una de las cláusulas catch, las declaraciones en
finally se ejecutan después de que el catch se complete. Si ninguna de las cláusulas catch maneja la
excepción, las declaraciones en finally se ejecutan antes de que la excepción se propague al siguiente
nivel.

Si las declaraciones en el try se ejecutan correctamente, o si realizamos un return, break o continue, las
declaraciones en la cláusula finally aún se ejecutan. Para garantizar que algunas operaciones se ejecuten,
incluso podemos usar try y finally sin ninguna cláusula catch:

Las excepciones que ocurren en una cláusula catch o finally se manejan normalmente; la búsqueda de un
bloque try/catch que envuelva comienza fuera del bloque try ofensivo, después de que se haya ejecutado
el bloque finally.

try con Recursos

Un uso común de la cláusula finally es asegurar que los recursos utilizados en un bloque try sean limpiados,
sin importar cómo el código salga del bloque.

Lo que queremos decir con "limpieza" aquí es desasignar recursos costosos o cerrar conexiones como
archivos, sockets o conexiones de base de datos. En algunos casos, estos recursos podrían limpiarse por
sí solos eventualmente cuando Java reclame la basura, pero eso sería como mucho en un momento
desconocido en el futuro y en el peor de los casos podría nunca ocurrir o no suceder antes de que te
quedes sin recursos. Por eso, siempre es mejor prevenir estas situaciones. Hay dos problemas con este
enfoque venerable: primero, requiere trabajo adicional para llevar a cabo este patrón en todo tu código,
incluyendo cosas importantes como verificaciones de nulos, como se muestra en nuestro ejemplo, y
segundo, si estás manejando múltiples recursos en un solo bloque finally, existe la posibilidad de que tu
código de limpieza arroje una excepción (por ejemplo, en close()) y deje el trabajo incompleto.

En Java 7, las cosas se han simplificado enormemente mediante la nueva forma de "try con recursos" del
bloque try. En esta forma, puedes colocar una o más declaraciones de inicialización de recursos dentro de
paréntesis después de la palabra clave try, y esos recursos se "cerrarán" automáticamente para ti cuando
el control salga del bloque try:
En este ejemplo, inicializamos tanto un objeto Socket como un objeto FileWriter dentro del bloque try-
with-resources y los utilizamos dentro del cuerpo de la instrucción try. Cuando el control sale de la
declaración try, ya sea después de completarse con éxito o a través de una excepción, ambos recursos se
cierran automáticamente llamando a su método close().

Los recursos se cierran en orden inverso al que fueron construidos, por lo que se pueden acomodar
dependencias entre ellos. Este comportamiento está soportado para cualquier clase que implemente la
interfaz AutoCloseable (que, en el recuento actual, incluye más de cien clases incorporadas diferentes). El
método close() de esta interfaz está prescrito para liberar todos los recursos asociados con el objeto, y
también puedes implementar esto fácilmente en tus propias clases.

Cuando se utiliza try with resources, no tenemos que agregar ningún código específico para cerrar el
archivo o el socket; se hace automáticamente por nosotros.

Otro problema que resuelve try with resources es la molesta situación a la que aludimos, donde una
excepción podría ser lanzada durante una operación de cierre. Mirando hacia atrás en el ejemplo anterior
en el que usamos una cláusula finally para realizar nuestra limpieza, si se hubiera generado una excepción
en el método close(), se habría lanzado en ese momento, abandonando completamente la excepción
original del cuerpo de la cláusula try. Pero al usar try with resources, preservamos la excepción original.
Si ocurre una excepción mientras estamos dentro del cuerpo del try y se producen una o más excepciones
durante las operaciones de cierre automáticas posteriores, es la excepción original del cuerpo del try la
que se propaga hacia arriba al llamador. Veamos un ejemplo:

Una vez que el bloque try ha comenzado, si ocurre una excepción en un punto de excepción (#1), Java
intentará cerrar ambos recursos en orden inverso, lo que puede llevar a posibles excepciones en los
lugares #2 y #3. En este caso, el código que llama aún recibirá la excepción #1. Sin embargo, las
excepciones #2 y #3 no se pierden; simplemente se "suprimen" y se pueden recuperar mediante el
método Throwable getSuppressed() de la excepción lanzada al llamador. Esto devuelve un array con todas
las excepciones suprimidas.

Problemas de rendimiento

Debido a la forma en que está implementada la máquina virtual de Java, protegerse contra una excepción
que se lance (usando un try) no tiene ningún costo adicional en la ejecución de tu código. Sin embargo,
lanzar una excepción no es gratuito. Cuando se lanza una excepción, Java tiene que localizar el bloque
try/catch adecuado y realizar otras actividades que consumen tiempo en tiempo de ejecución.

Si has programado, espero que no hayas escrito mensajes de error tan opacos. Cuanto más útiles y
explicativos sean tus mensajes, mejor.

El resultado es que deberías lanzar excepciones solo en circunstancias verdaderamente "excepcionales"


y evitar usarlas para condiciones esperadas, especialmente cuando el rendimiento es un problema. Por
ejemplo, si tienes un bucle, puede ser mejor realizar una pequeña comprobación en cada iteración y evitar
lanzar la excepción en lugar de hacerlo frecuentemente. Por otro lado, si la excepción se lanza solo una
vez entre un número muy grande de veces, es posible que quieras eliminar el costo del código de prueba
y no preocuparte por el costo de lanzar esa excepción. La regla general debería ser que las excepciones
se usan para situaciones "fuera de límites" o anormales, no para condiciones rutinarias y esperadas (como
el final de un archivo).
Afirmaciones (Assertions)

Una afirmación es una simple prueba de "aprobado/fallido" de alguna condición, realizada mientras tu
aplicación está en ejecución. Las afirmaciones se pueden usar para "verificar la coherencia" de tu código
en cualquier lugar donde creas que ciertas condiciones están garantizadas por el comportamiento
correcto del programa. Las afirmaciones son distintas de otros tipos de pruebas porque verifican
condiciones que nunca deben violarse a nivel lógico: si la afirmación falla, se considera que la aplicación
está rota y generalmente se detiene con un mensaje de error apropiado. Las afirmaciones son soportadas
directamente por el lenguaje Java y se pueden activar o desactivar en tiempo de ejecución para eliminar
cualquier penalización de rendimiento al incluirlas en tu código.

Utilizar afirmaciones para probar el comportamiento correcto de tu aplicación es una técnica simple pero
poderosa para garantizar la calidad del software. Llena un vacío entre aquellos aspectos del software que
pueden ser verificados automáticamente por el compilador y aquellos verificados más generalmente por
"pruebas unitarias" y pruebas humanas. Las afirmaciones prueban suposiciones sobre el comportamiento
del programa y los convierten en garantías (al menos mientras están activadas).

Si has programado antes, es posible que hayas visto algo como lo siguiente:

Una aserción en Java es equivalente a este ejemplo, pero se realiza con la palabra clave assert del lenguaje.
Toma una condición booleana y un valor de expresión opcional. Si la aserción falla, se lanza un
AssertionError, lo que generalmente hace que Java salga de la aplicación.

La expresión opcional puede evaluar tanto a un tipo primitivo como a un objeto. De cualquier manera, su
único propósito es convertirse en una cadena de texto y mostrarse al usuario si la aserción falla; la mayoría
de las veces se utiliza un mensaje de cadena explícitamente. Aquí tienes algunos ejemplos:

En caso de fallo, las dos primeras aserciones imprimen únicamente un mensaje genérico, mientras que la
tercera imprime el valor de "a", y la última imprime el mensaje "¡foo es nulo!".

Nuevamente, lo importante acerca de las aserciones no es solo que sean más concisas que la condición if
equivalente, sino que pueden habilitarse o deshabilitarse al ejecutar la aplicación. Deshabilitar las
aserciones significa que sus condiciones de prueba ni siquiera se evalúan, por lo que no hay penalización
de rendimiento por incluirlas en tu código (excepto, tal vez, el espacio en los archivos de clase cuando se
cargan).

Habilitar y deshabilitar las aserciones

Las aserciones se activan o desactivan en tiempo de ejecución. Cuando están desactivadas, las aserciones
todavía existen en los archivos de clase pero no se ejecutan y no consumen tiempo. Puedes habilitar y
deshabilitar las aserciones para toda una aplicación, por paquetes o incluso clase por clase. Por defecto,
las aserciones están desactivadas en Java. Para habilitarlas en tu código, utiliza la bandera -ea o -
enableassertions del comando java:

Para activar las aserciones para una clase específica, añade el nombre de la clase al habilitar las aserciones
de la siguiente manera:
Para activar las aserciones solo para paquetes específicos, añade el nombre del paquete seguido de
puntos suspensivos (...) al habilitar las aserciones de la siguiente manera:

Cuando activas las aserciones para un paquete, Java también habilita todos los nombres de paquetes
subordinados (por ejemplo, com.oreilly.examples.text). Sin embargo, puedes ser más selectivo utilizando
la bandera correspondiente -da o -disableassertions para negar paquetes o clases individuales. Puedes
combinar todo esto para lograr agrupaciones arbitrarias de la siguiente manera:

Este ejemplo habilita las aserciones para el paquete com.oreilly.examples en su totalidad, excluye el
paquete com.oreilly.examples.text y luego activa las excepciones solo para una clase,
MonkeyTypewriters, en ese paquete.

Una aserción impone una regla sobre algo que debería ser inmutable en tu código y de lo contrario no
sería verificado. Puedes utilizar una aserción para obtener mayor seguridad en cualquier lugar donde
desees verificar tus suposiciones sobre el comportamiento del programa que no puede ser comprobado
por el compilador.

Una situación común que requiere una aserción es probar múltiples condiciones o valores donde siempre
debería encontrarse uno. En este caso, una aserción que falla como comportamiento predeterminado o
de "caída" indica que el código está roto. Por ejemplo, supongamos que tenemos un valor llamado
dirección que siempre debería contener el valor constante IZQUIERDA o DERECHA:

Lo mismo se aplica al caso por defecto de una sentencia switch:

En general, no deberías usar aserciones para verificar la validez de los argumentos de los métodos porque
deseas que ese comportamiento sea parte de tu aplicación, no solo una prueba de control de calidad que
pueda ser desactivada. La validez de la entrada a un método se llama sus precondiciones, y normalmente
deberías lanzar una excepción si no se cumplen; esto eleva las precondiciones a ser parte del "contrato"
del método con el usuario. Sin embargo, verificar la corrección de los resultados de tus métodos con
aserciones antes de devolverlos es una buena idea; a esto se le llama postcondiciones.

A veces, determinar qué es o no una precondición depende de tu punto de vista. Por ejemplo, cuando un
método se usa internamente dentro de una clase, las precondiciones pueden estar garantizadas por los
métodos que lo llaman. Los métodos públicos de la clase probablemente deberían lanzar excepciones
cuando sus precondiciones no se cumplen, pero un método privado podría usar aserciones porque sus
llamadores siempre son código estrechamente relacionado que debería obedecer el comportamiento
correcto.
La API de Registro

El paquete java.util.logging proporciona un marco de registro altamente flexible y fácil de usar para
información del sistema, mensajes de error y salida de rastreo (debugging) detallada. Con el paquete de
registro, puedes aplicar filtros para seleccionar mensajes de registro, dirigir su salida a uno o más destinos
(incluidos archivos y servicios de red) y formatear los mensajes adecuadamente para sus consumidores.

Lo más importante es que gran parte de esta configuración básica de registro se puede establecer
externamente en tiempo de ejecución mediante el uso de un archivo de propiedades de configuración de
registro o un programa externo. Por ejemplo, al establecer las propiedades adecuadas en tiempo de
ejecución, puedes especificar que los mensajes de registro se envíen tanto a un archivo designado en
formato XML como a la consola del sistema en un formato digerido y legible para humanos. Además, para
cada uno de esos destinos, puedes especificar el nivel o la prioridad de los mensajes a registrar,
descartando aquellos por debajo de un cierto umbral de importancia. Siguiendo las convenciones de
origen correctas en tu código, incluso puedes ajustar los niveles de registro para partes específicas de tu
aplicación, permitiéndote apuntar a paquetes y clases individuales para un registro detallado sin ser
abrumado por demasiada salida. La API de Registro de Java incluso se puede controlar de forma remota
a través de las API de Java Management Extensions MBean.

Resumen

Cualquier buena API de registro debe tener al menos dos principios rectores. En primer lugar, el
rendimiento no debe impedir al desarrollador usar mensajes de registro libremente. Al igual que las
aserciones del lenguaje Java, cuando los mensajes de registro están desactivados, no deberían consumir
una cantidad significativa de tiempo de procesamiento. Esto significa que no hay penalización de
rendimiento por incluir declaraciones de registro siempre que estén desactivadas. En segundo lugar,
aunque algunos usuarios puedan querer características y configuraciones avanzadas, una API de registro
debe tener un modo de uso simple que sea lo suficientemente conveniente para que los desarrolladores
con poco tiempo lo usen en lugar del antiguo y confiable System.out.println(). La API de Registro de Java
proporciona un modelo simple y muchos métodos de conveniencia que lo hacen muy tentador.

Registros

El corazón del marco de registro es el registrador, una instancia de java.util.logging.Logger. En la mayoría


de los casos, esta es la única clase con la que tu código tendrá que lidiar. Un registrador se construye a
partir del método estático Logger.getLogger(), con un nombre de registrador como argumento. Los
nombres de los registradores colocan a los registradores en una jerarquía con un registrador raíz global
en la parte superior y un árbol y subordinados debajo. Esta jerarquía permite que la configuración sea
heredada por partes del árbol para que el registro se pueda configurar automáticamente para diferentes
partes de tu aplicación. La convención es utilizar una instancia de registrador separada en cada clase o
paquete importante y utilizar el nombre del paquete y/o clase separados por puntos como nombre del
registrador. Por ejemplo:

El registrador proporciona una amplia gama de métodos para registrar mensajes; algunos toman
información muy detallada, y algunos métodos de conveniencia solo requieren una cadena (string) para
facilitar su uso. Por ejemplo:

Cubrimos los métodos de la clase Logger en detalle un poco más adelante. Los nombres `warning` e `info`
son dos ejemplos de niveles de registro; existen siete niveles que van desde `SEVERE` en la parte superior
hasta `FINEST` en la parte inferior. Distinguir los mensajes de registro de esta manera nos permite
seleccionar el nivel de información que queremos ver en tiempo de ejecución. En lugar de simplemente
registrar todo y clasificarlo más tarde (con un impacto negativo en el rendimiento), podemos ajustar qué
mensajes se generan. Hablaremos más sobre los niveles de registro en la próxima sección.

También deberíamos mencionar que, para conveniencia en aplicaciones muy simples o experimentos, se
proporciona un registrador para el nombre "global" en el campo estático Logger.global. Puedes usarlo
como una alternativa al antiguo System.out.println() en aquellos casos donde esa sigue siendo una
tentación:

Los manejadores (Handlers)

Los registradores (Loggers) representan la interfaz del cliente hacia el sistema de registro, pero la labor
real de publicar mensajes hacia destinos (como archivos o la consola) la realizan objetos llamados
manejadores (Handlers). Cada registrador puede tener uno o más objetos Handler asociados, lo que
incluye varios manejadores predefinidos suministrados con la API de Registro: ConsoleHandler,
FileHandler, StreamHandler y SocketHandler. Cada manejador sabe cómo enviar mensajes a su respectivo
destino. ConsoleHandler se utiliza en la configuración predeterminada para imprimir mensajes en la línea
de comandos o la consola del sistema. FileHandler puede dirigir la salida a archivos utilizando una
convención de nombres suministrada y rotar automáticamente los archivos a medida que se llenan. Los
demás envían mensajes a flujos (streams) y sockets, respectivamente. Existe un manejador adicional,
MemoryHandler, que puede almacenar un número de mensajes de registro en la memoria.
MemoryHandler tiene un búfer circular, que mantiene un cierto número de mensajes hasta que se activa
para enviarlos a otro manejador designado.

Como mencionamos, los registradores pueden configurarse para usar uno o más manejadores. Además,
los registradores envían mensajes hacia arriba en el árbol hacia los manejadores de cada uno de sus
padres. En la configuración más simple, esto significa que todos los mensajes terminan siendo distribuidos
por los manejadores del registrador raíz. Pronto veremos cómo configurar la salida usando los
manejadores estándar para la consola, archivos, etc.

Filtros

Antes de que un registrador entregue un mensaje a sus manejadores o a los manejadores de su padre,
primero verifica si el nivel de registro es suficiente para proceder. Si el mensaje no cumple con el nivel
requerido, se descarta en la fuente. Además del nivel, puedes implementar filtros arbitrarios de mensajes
creando clases de Filtro (Filter) que examinen el mensaje de registro antes de que sea procesado. Un Filtro
puede aplicarse a un registrador externamente en tiempo de ejecución de la misma manera que el nivel
de registro, los manejadores y los formateadores, que se discuten a continuación, pueden ser
configurados. Un Filtro también puede estar adjunto a un Handler individual para filtrar registros en la
etapa de salida (en lugar de en la fuente).

Formateadores (Formatters)

Internamente, los mensajes se transportan en un formato neutral, incluyendo toda la información


proporcionada por la fuente. No es hasta que son procesados por un manejador que se formatean para
la salida por una instancia de un objeto Formateador (Formatter). El paquete de registro viene con dos
formateadores básicos: SimpleFormatter y XMLFormatter. SimpleFormatter es el formato
predeterminado utilizado para la salida en consola. Produce resúmenes breves y legibles para humanos
de los mensajes de registro. XMLFormatter codifica todos los detalles del mensaje de registro en un
formato de registro XML. La DTD para el formato se puede encontrar en https://oreil.ly/iiDCW.
Niveles de registro

La Tabla 6-1 enumera los niveles de registro de mayor a menor importancia.

Estos niveles se dividen en tres categorías: usuario final, administrador y desarrollador. Las aplicaciones a
menudo se configuran por defecto para registrar únicamente mensajes del nivel INFO en adelante (INFO,
WARNING y SEVERE). Estos niveles son generalmente visibles para los usuarios finales, y los mensajes
registrados en ellos deberían ser adecuados para su consumo general. En otras palabras, deben estar
redactados de manera clara para que tengan sentido para un usuario promedio de la aplicación.
Frecuentemente, este tipo de mensajes se presentan al usuario final en una consola del sistema o en un
cuadro de diálogo emergente.

El nivel CONFIG debería usarse para información del sistema relativamente estática pero detallada que
podría ser útil para un administrador o instalador. Esto podría incluir información sobre los módulos de
software instalados, las características del sistema anfitrión y los parámetros de configuración. Estos
detalles son importantes, pero probablemente no tan significativos para un usuario final.

Los niveles FINE, FINER y FINEST son para desarrolladores u otras personas con conocimiento de los
entresijos de la aplicación. Deben usarse para rastrear la aplicación en niveles sucesivos de detalle. Puedes
definir tus propios significados para estos niveles. Sugeriremos un esquema básico en nuestro ejemplo,
que se presentará a continuación.

Un Ejemplo Sencillo

En el siguiente ejemplo (admitidamente muy artificial), usamos todos los niveles de registro para poder
experimentar con la configuración del registro. Aunque la secuencia de mensajes carece de sentido, el
texto representa mensajes de ese tipo.
No hay mucho en este ejemplo. Pedimos una instancia del registrador (logger) para nuestra clase
utilizando el método estático Logger.getLogger(), especificando un nombre de clase. La convención es
usar el nombre de clase completamente calificado, así que fingiremos que nuestra clase está en un
paquete com.oreilly.

Ahora, ejecuta LogTest. Deberías ver una salida similar a la siguiente en la consola del sistema:

Vemos los mensajes INFO, WARNING y SEVERE, cada uno identificado con una marca de fecha y hora y el
nombre de la clase y método (LogTest main) del que provienen. Observa que los mensajes de nivel inferior
no aparecieron. Esto se debe a que el nivel de registro predeterminado normalmente se establece en
INFO, lo que significa que solo se registran mensajes de gravedad INFO y superior. También ten en cuenta
que la salida se dirigió a la consola del sistema y no a un archivo de registro en algún lugar; eso también
es lo predeterminado. Ahora describiremos dónde se establecen estos valores predeterminados y cómo
anularlos en tiempo de ejecución.

Propiedades de Configuración del Registro

Como mencionamos en la introducción, probablemente la característica más importante de la API de


registro es la capacidad de configurar gran parte de ella en tiempo de ejecución mediante el uso de
propiedades o aplicaciones externas. La configuración de registro predeterminada se almacena en el
archivo jre/lib/logging.properties en el directorio donde se instaló Java. Es un archivo de propiedades Java
estándar (del tipo que describimos anteriormente en este capítulo).

El formato de este archivo es simple. Puedes realizar cambios en él, pero no es obligatorio. En su lugar,
puedes especificar tu propio archivo de propiedades de configuración de registro caso por caso utilizando
una propiedad del sistema en tiempo de ejecución, de la siguiente manera:

En esta línea de comando, 'myfile' es tu archivo de propiedades que contiene la directiva, la cual
describiremos a continuación. Si deseas hacer que esta designación de archivo sea más permanente,
puedes hacerlo configurando el nombre del archivo en la entrada correspondiente utilizando la API de
Preferencias de Java. Incluso puedes ir más allá y, en lugar de especificar un archivo de configuración,
proporcionar una clase que sea responsable de configurar toda la configuración de registro, pero no
entraremos en eso aquí.

Un archivo de propiedades de configuración de registro muy simple podría verse así:

Aquí hemos establecido el nivel de registro predeterminado para toda la aplicación utilizando la propiedad
.level (punto nivel). También hemos utilizado la propiedad handlers para especificar que se debe usar una
instancia de ConsoleHandler (tal como la configuración predeterminada) para mostrar mensajes en la
consola. Si ejecutas nuestra aplicación nuevamente, especificando este archivo de propiedades como la
configuración de registro, ahora verás todos nuestros mensajes de registro.

Pero apenas estamos comenzando. A continuación, echemos un vistazo a una configuración más
compleja:
En este ejemplo, hemos configurado dos manipuladores de registro (log handlers): un ConsoleHandler
con el nivel de registro establecido en WARNING y también una instancia de FileHandler que envía la
salida a un archivo XML. El manipulador de archivo está configurado para registrar mensajes en el nivel
FINEST (todos los mensajes) y para rotar archivos de registro cada 25,000 líneas, manteniendo un máximo
de 4 archivos. El nombre del archivo está controlado por la propiedad pattern. Las barras inclinadas hacia
adelante en el nombre del archivo se localizan automáticamente a una barra invertida (\) si es necesario.
El símbolo especial %h se refiere al directorio personal del usuario. Puedes usar %t para referirte al
directorio temporal del sistema. Si hay conflictos en los nombres de archivo, se agrega automáticamente
un número después de un punto (comenzando en cero). Alternativamente, puedes usar %u para indicar
dónde se debe insertar un número único en el nombre. De manera similar, al rotar los archivos, se agrega
un número después de un punto al final. Puedes controlar dónde se coloca el número de rotación con el
identificador %g.

En nuestro ejemplo, especificamos la clase XMLFormatter. También podríamos haber usado la clase
SimpleFormatter para enviar el mismo tipo de salida simple a la consola. El ConsoleHandler también nos
permite especificar cualquier formateador que deseemos, utilizando la propiedad formatter.

Finalmente, mencionamos anteriormente que podrías controlar los niveles de registro para partes de tus
aplicaciones. Para hacer esto, establece propiedades en los registradores de tu aplicación utilizando sus
nombres jerárquicos:

Aquí, hemos establecido el nivel de registro solo para nuestro registro de pruebas, por nombre. Las
propiedades de registro siguen la jerarquía, por lo que podríamos establecer el nivel de registro para todas
las clases en el paquete de Oreilly con:

Los niveles de registro se establecen en el orden en que se leen en el archivo de propiedades, por lo que
es recomendable establecer primero los niveles generales. Ten en cuenta que los niveles establecidos en
los manejadores permiten que el manejador de archivos filtre solo los mensajes suministrados por los
registradores. Por lo tanto, configurar el manejador de archivos en FINEST no recuperará mensajes
silenciados por un registrador establecido en SEVERE (solo los mensajes SEVERE llegarán al manejador
desde ese registrador).

El Registrador ( The logger )

En nuestro ejemplo, utilizamos siete métodos de conveniencia nombrados según los diversos niveles de
registro. También existen tres grupos de métodos generales que pueden usarse para proporcionar
información más detallada. Los más generales son:
Estos métodos aceptan como su primer argumento un identificador estático de nivel de registro de la
clase Level, seguido por un parámetro, un arreglo o un tipo de excepción. El identificador de nivel es uno
de Level.SEVERE, Level.WARNING, Level.INFO, y así sucesivamente.

Además de estos cuatro métodos, existen métodos de conveniencia llamados entering(), exiting(), y
throwing() que los desarrolladores pueden utilizar para registrar información detallada de seguimiento.

Rendimiento

En la introducción, mencionamos que el rendimiento es una prioridad en la API de Registro. Con ese fin,
hemos descrito que los mensajes de registro se filtran en la fuente, utilizando niveles de registro para
interrumpir el procesamiento de mensajes temprano. Esto ahorra gran parte del costo de manejarlos. Sin
embargo, no puede evitar ciertos tipos de trabajos de configuración que podrías realizar antes de la
llamada al registro. Específicamente, debido a que estamos pasando cosas a los métodos de registro, es
común construir mensajes detallados o convertir objetos a cadenas como argumentos. A menudo, este
tipo de operación es costoso. Para evitar la construcción innecesaria de cadenas, deberías envolver
operaciones costosas de registro en una prueba condicional utilizando el método Logger isLoggable() para
verificar si debes llevar a cabo la operación:

Excepciones del mundo real

La adopción de excepciones por parte de Java como técnica de manejo de errores hace que sea mucho
más sencillo para los desarrolladores escribir código robusto. El compilador te obliga a pensar en las
excepciones verificadas con anticipación. Las excepciones no verificadas definitivamente surgirán, pero
las afirmaciones pueden ayudarte a estar alerta ante esos problemas en tiempo de ejecución y, con
suerte, prevenir un fallo.

La funcionalidad try-with-resources agregada en Java 7 simplifica aún más a los desarrolladores mantener
su código limpio y hacer "lo correcto" al interactuar con recursos del sistema limitados, como archivos y
conexiones de red. Como mencionamos al principio del capítulo, otros lenguajes ciertamente tienen
facilidades o costumbres para lidiar con estos problemas. Java, como lenguaje, trabaja arduamente para
ayudarte a considerar cuidadosamente los problemas que pueden surgir en tu código. Y cuanto más
trabajes en resolver esos problemas, más estable será tu aplicación y, por lo tanto, más satisfechos estarán
tus usuarios.

Incluso cuando los errores son sutiles y no causan que tu aplicación se bloquee, Java proporciona el
paquete java.util.logging para ayudar a rastrear el problema raíz. Puedes ajustar los detalles que se
producen en los registros manteniendo un buen rendimiento de tu aplicación.

Muchos de nuestros ejemplos hasta ahora han sido directos y realmente no han requerido una
verificación de errores sofisticada. Ten la seguridad de que exploraremos código más interesante con
muchas cosas que requieren manejo de excepciones. Los capítulos posteriores abordarán temas como
programación multinúcleo y redes. Esos temas están llenos de situaciones que pueden salir mal en tiempo
de ejecución, como un cálculo grande que se vuelve caótico o una conexión WiFi que se interrumpe.
Disculpa el juego de palabras, ¡pero pronto estarás probando todos estos nuevos trucos de excepciones
y errores!
CAPÍTULO 7

Colecciones y Genéricos

A medida que comenzamos a utilizar nuestro creciente conocimiento de objetos para resolver problemas
cada vez más interesantes, surgirá una pregunta recurrente. ¿Cómo almacenamos los datos que estamos
manipulando mientras resolvemos esos problemas? Definitivamente usaremos variables de todos los
tipos diferentes, pero también necesitaremos opciones de almacenamiento más grandes y sofisticadas.
Los arrays que discutimos anteriormente en "Arrays" en la página 114 son un comienzo, pero los arrays
tienen algunas limitaciones. En este capítulo veremos cómo obtener acceso eficiente y flexible a grandes
cantidades de datos. Ahí es donde entra en juego la API de Colecciones de Java que abordamos en la
próxima sección. También veremos cómo lidiar con los diversos tipos de datos que queremos almacenar
en estos contenedores grandes, al igual que lo hacemos con valores individuales en variables. Ahí es
donde entran en juego los genéricos. Nos ocuparemos de esos en "Limitaciones de Tipo" en la página 203.

Colecciones

Las colecciones son estructuras de datos fundamentales para todo tipo de programación. Siempre que
necesitamos referirnos a un grupo de objetos, tenemos algún tipo de colección. A nivel del lenguaje
central, Java admite colecciones en forma de arrays. Pero los arrays son estáticos y, debido a que tienen
una longitud fija, son incómodos para grupos de elementos que crecen y disminuyen a lo largo de la vida
de una aplicación. Además, los arrays no representan bien relaciones abstractas entre objetos. En los
primeros días, la plataforma Java tenía solo dos clases básicas para abordar estas necesidades: la clase
java.util.Vector, que representa una lista dinámica de objetos, y la clase java.util.Hashtable, que contiene
un mapa de pares clave/valor. Hoy en día, Java tiene un enfoque más completo para las colecciones
llamado Framework de Colecciones. Las clases más antiguas aún existen, pero se han adaptado al
framework (con algunas excentricidades) y generalmente ya no se utilizan.

A pesar de ser conceptualmente simples, las colecciones son una de las partes más poderosas de cualquier
lenguaje de programación. Las colecciones implementan estructuras de datos que son fundamentales
para manejar problemas complejos. Gran parte de la ciencia informática básica se dedica a describir las
formas más eficientes de implementar ciertos tipos de algoritmos sobre colecciones. Tener estas
herramientas a tu disposición y entender cómo usarlas puede hacer que tu código sea mucho más
pequeño y rápido. También puede ahorrarte tener que reinventar la rueda.

El Framework de Colecciones original tenía dos grandes inconvenientes. El primero era que las colecciones
eran, por necesidad, no tipificadas y solo trabajaban con objetos indiferenciados en lugar de tipos
específicos como Fechas y Cadenas. Esto significaba que tenías que realizar una conversión de tipo cada
vez que extraías un objeto de una colección. Esto iba en contra de la seguridad de tipos en tiempo de
compilación de Java. Pero en la práctica, esto fue menos un problema que simplemente engorroso y
tedioso. El segundo problema era que, por razones prácticas, las colecciones solo podían trabajar con
objetos y no con tipos primitivos. Esto significaba que cada vez que querías poner un número u otro tipo
primitivo en una colección, tenías que almacenarlo primero en una clase contenedora y luego
desempaquetarlo al recuperarlo. La combinación de estos factores hacía que el código que trabajaba con
colecciones fuera menos legible y más peligroso.

Todo esto cambió con la introducción de tipos genéricos y autoboxing de valores primitivos. En primer
lugar, la introducción de tipos genéricos (nuevamente, más sobre esto en "Limitaciones de Tipo" en la
página 203) ha hecho posible que las colecciones verdaderamente seguras en cuanto a tipos estén bajo
el control del programador. En segundo lugar, la introducción de autoboxing y unboxing de tipos
primitivos significa que generalmente puedes tratar objetos y tipos primitivos como iguales en lo que
respecta a las colecciones. La combinación de estas nuevas características puede reducir
significativamente la cantidad de código que escribes y agregar seguridad. Como veremos, todas las clases
de colecciones ahora aprovechan estas características.

El Framework de Colecciones se basa en un puñado de interfaces en el paquete java.util. Estas interfaces


están divididas en dos jerarquías. La primera jerarquía desciende de la interfaz Collection. Esta interfaz (y
sus descendientes) representa un contenedor que contiene otros objetos. La segunda jerarquía separada
se basa en la interfaz Map, que representa un grupo de pares clave/valor donde la clave se puede usar
para recuperar el valor de manera eficiente.

La Interfaz Collection

La madre de todas las colecciones es una interfaz llamada adecuadamente Collection. Sirve como un
contenedor que contiene otros objetos, sus elementos. No especifica exactamente cómo se organizan los
objetos; no dice, por ejemplo, si se permiten objetos duplicados o si los objetos están ordenados de alguna
manera. Este tipo de detalles se dejan a las interfaces hijas. Sin embargo, la interfaz Collection define
algunas operaciones básicas comunes a todas las colecciones:

public boolean add( element )

Agrega el objeto suministrado a esta colección. Si la operación tiene éxito, este método devuelve
true. Si el objeto ya existe en esta colección y la colección no permite duplicados, se devuelve
false. Además, algunas colecciones son de solo lectura. Esas colecciones lanzan una
UnsupportedOperationException si se llama a este método.

public boolean remove( element )

Elimina el objeto especificado de esta colección. Al igual que el método add(), este método
devuelve true si se elimina el objeto de la colección. Si el objeto no existe en esta colección, se
devuelve false. Las colecciones de solo lectura lanzan una UnsupportedOperationException si se
llama a este método.

public boolean contains( element )

Devuelve true si la colección contiene el objeto especificado.

public int size()

Devuelve el número de elementos en esta colección.

public boolean isEmpty()

Devuelve true si esta colección no tiene elementos.

public Iterator iterator()

Examina todos los elementos en esta colección. Este método devuelve un Iterator, que es un
objeto que puedes usar para recorrer los elementos de la colección. Hablaremos más sobre los
iteradores en la próxima sección.
Además, los métodos addAll(), removeAll() y containsAll() aceptan otra Collection y agregan, eliminan o
prueban todos los elementos de la colección suministrada.

Tipos de Colección

La interfaz Collection tiene tres interfaces hijas. Set representa una colección en la que no se permiten
elementos duplicados. List es una colección cuyos elementos tienen un orden específico. La interfaz
Queue es un búfer para objetos con noción de un elemento "cabeza" que es el siguiente en la fila para su
procesamiento.

Set

Set no tiene métodos además de los que hereda de Collection. Simplemente impone su regla de no
duplicados. Si intentas agregar un elemento que ya existe en un Set, el método add() simplemente
devuelve false. SortedSet mantiene los elementos en un orden prescrito; como una lista ordenada que no
puede contener duplicados. Puedes recuperar subconjuntos (que también están ordenados) usando los
métodos subSet(), headSet() y tailSet(). Estos métodos aceptan uno o un par de elementos que marcan
los límites. Los métodos first(), last() y comparator() proporcionan acceso al primer elemento, al último
elemento y al objeto usado para comparar elementos (más sobre esto en "Un Vistazo Más Cercano: El
Método sort()" en la página 218).

Java 7 agregó NavigableSet, que extiende SortedSet y agrega métodos para encontrar la coincidencia más
cercana mayor o menor que un valor objetivo dentro del orden de clasificación del Set. Esta interfaz se
puede implementar de manera eficiente utilizando técnicas como skip lists, que hacen que la búsqueda
de elementos ordenados sea rápida.

List

La siguiente interfaz hija de Collection es List. La List es una colección ordenada, similar a un array pero
con métodos para manipular la posición de los elementos en la lista:

public boolean add( E element )

Agrega el elemento especificado al final de la lista.

public void add( int index, E element )

Inserta el objeto dado en la posición suministrada en la lista. Si la posición es menor que cero o
mayor que la longitud de la lista, se lanzará un IndexOutOfBoundsException. El elemento que
estaba previamente en la posición suministrada y todos los elementos después de él se mueven
hacia arriba una posición de índice.

public void remove( int index )

Elimina el elemento en la posición especificada. Todos los elementos subsiguientes se mueven


hacia abajo una posición de índice.

public E get( int index )

Devuelve el elemento en la posición dada.


public Object set( int index, E element )

Cambia el elemento en la posición dada al objeto especificado. Debe haber un objeto en el índice
o de lo contrario se lanzará un IndexOutOfBoundsException.

El tipo E en estos métodos se refiere al tipo de elemento parametrizado de la clase List. Collection, Set y
List son todos tipos de interfaces. Este es un ejemplo de la característica de Genéricos a la que hicimos
referencia en la introducción de este capítulo, y veremos implementaciones concretas de estas en breve.

Cola( Queue )

Una Queue es una colección que actúa como un búfer para elementos. La cola mantiene el orden de
inserción de los elementos colocados en ella y tiene la noción de un elemento "cabeza". Las colas pueden
ser del tipo primero en entrar, primero en salir (FIFO) o último en entrar, primero en salir (LIFO),
dependiendo de la implementación:

public boolean offer( E element ), public boolean add( E element )

El método offer() intenta colocar el elemento en la cola, devolviendo true si tiene éxito.
Diferentes tipos de Queue pueden tener límites o restricciones diferentes en los tipos de
elementos (incluida la capacidad). Este método difiere del método add() heredado de Collection
en que devuelve un valor booleano en lugar de lanzar una excepción para indicar que el elemento
no puede ser aceptado.

public E poll(), public E remove()

El método poll() elimina el elemento en la cabeza de la cola y lo devuelve. Este método difiere
del método remove() de Collection en que si la cola está vacía, se devuelve null en lugar de lanzar
una excepción.

public E peek()

Devuelve el elemento de la cabeza sin eliminarlo de la cola. Si la cola está vacía, se devuelve null.

La Interfaz Map

El Framework de Colecciones también incluye java.util.Map, que es una colección de pares clave/valor.
Otros nombres para Map son "diccionario" o "array asociativo". Los Mapas almacenan y recuperan
elementos con valores de clave; son muy útiles para cosas como cachés o bases de datos minimalistas.
Cuando almacenas un valor en un mapa, asocias un objeto clave con un valor. Cuando necesitas buscar el
valor, el mapa lo recupera utilizando la clave. Con genéricos, un tipo Map se parametriza con dos tipos:
uno para las claves y otro para los valores. El siguiente fragmento de código usa un HashMap, que es una
implementación eficiente pero desordenada de mapas que discutiremos más adelante:

En el código heredado, los mapas simplemente asignan tipos de objetos a tipos de objetos y requieren la
conversión apropiada para recuperar valores.

Las operaciones básicas en Map son sencillas. En los siguientes métodos, el tipo K se refiere al tipo de
parámetro de clave, y el tipo V se refiere al tipo de parámetro de valor:
public V put( K key, V value )

Agrega el par clave/valor especificado al mapa. Si el mapa ya contiene un valor para la clave
especificada, el valor antiguo es reemplazado y devuelto como resultado.

public V get( K key )

Recupera el valor correspondiente a la clave del mapa.

public V remove( K key )

Elimina el valor correspondiente a la clave del mapa. Se devuelve el valor eliminado.

public int size()

Devuelve el número de pares clave/valor en este mapa.

Puedes recuperar todas las claves o valores en el mapa usando los siguientes métodos:

public Set keySet()

Este método devuelve un Set que contiene todas las claves en este mapa.

public Collection values()

Usa este método para recuperar todos los valores en este mapa. La Collection devuelta puede
contener elementos duplicados.

public Set entrySet()

Este método devuelve un Set que contiene todos los pares clave/valor (como objetos Map.Entry)
en este mapa.

Map tiene una interfaz secundaria, SortedMap. Un SortedMap mantiene sus pares clave/valor ordenados
en un orden específico según los valores de las claves. Proporciona los métodos subMap(), headMap() y
tailMap() para recuperar subconjuntos de mapas ordenados. Al igual que SortedSet, también proporciona
un método comparator(), que devuelve un objeto que determina cómo se ordenan las claves del mapa.
Hablaremos más sobre eso en "Un Vistazo Más Cercano: El Método sort()" en la página 218. Java 7 agregó
un NavigableMap con funcionalidades paralelas a las de NavigableSet; es decir, agrega métodos para
buscar los elementos ordenados por un valor objetivo mayor o menor.

Finalmente, debemos dejar claro que, aunque relacionado, Map no es literalmente un tipo de Collection
(Map no extiende la interfaz Collection). Puede que te preguntes por qué. Todos los métodos de la interfaz
Collection parecerían tener sentido para Map, excepto iterator(). Un Map, nuevamente, tiene dos
conjuntos de objetos: claves y valores, y tiene iteradores separados para cada uno. Es por eso que un Map
no implementa una Collection. Si quieres una vista similar a Collection de un Map con claves y valores,
puedes usar el método entrySet().

Una nota más sobre los mapas: algunas implementaciones de mapas (incluido el HashMap estándar de
Java) permiten que se use null como clave o valor, pero otras pueden no hacerlo.
Limitaciones de Tipo

Los genéricos se tratan de abstracción. Los genéricos te permiten crear clases y métodos que funcionan
de la misma manera en diferentes tipos de objetos. El término "genérico" proviene de la idea de que nos
gustaría poder escribir algoritmos generales que puedan reutilizarse ampliamente para muchos tipos de
objetos en lugar de tener que adaptar nuestro código a cada circunstancia. Este concepto no es nuevo; es
el impulso detrás de la programación orientada a objetos en sí misma.

Los genéricos en Java no agregan nuevas capacidades al lenguaje tanto como hacen que el código Java
reutilizable sea más fácil de escribir y leer.

Los genéricos llevan la reutilización al siguiente nivel al hacer que el tipo de los objetos con los que
trabajamos sea un parámetro explícito del código genérico. Por esta razón, los genéricos también se
denominan tipos parametrizados. En el caso de una clase genérica, el desarrollador especifica un tipo
como un parámetro (un argumento) cada vez que usa el tipo genérico. La clase se parametriza por el tipo
suministrado al que el código se adapta.

En otros lenguajes, los genéricos a veces se denominan plantillas, que es más un término de
implementación. Las plantillas son como clases intermedias, esperando sus parámetros de tipo para que
puedan ser utilizadas. Java toma un camino diferente, que tiene tanto beneficios como inconvenientes
que describiremos detalladamente en este capítulo.

Hay mucho que decir sobre los genéricos en Java. Algunos detalles pueden parecer un poco oscuros al
principio, pero no te desanimes. La gran mayoría de lo que harás con genéricos, como usar clases
existentes como List y Set, por ejemplo, es fácil e intuitivo. Diseñar y crear tus propios genéricos requiere
una comprensión más cuidadosa y vendrá con un poco de paciencia y experimentación.

De hecho, comenzamos nuestra discusión en ese espacio intuitivo con el caso más convincente para los
genéricos: las clases de contenedores y colecciones que acabamos de cubrir. A continuación,
retrocedemos y analizamos lo bueno, lo malo y lo feo de cómo funcionan los genéricos en Java.
Concluimos echando un vistazo a un par de clases genéricas del mundo real en la API de Java.

Contenedores: Construyendo una Mejor Trampa para Ratones

En un lenguaje de programación orientado a objetos como Java, el polimorfismo significa que los objetos
siempre son en cierto grado intercambiables. Cualquier hijo de un tipo de objeto puede servir en lugar de
su tipo padre y, en última instancia, cada objeto es un hijo de java.lang.Object, la "Eva" orientada a
objetos, por así decirlo. Es natural, por lo tanto, que los tipos más generales de contenedores en Java
trabajen con el tipo Object para que puedan contener casi cualquier cosa. Por contenedores, nos
referimos a clases que contienen instancias de otras clases de alguna manera. La API de Colecciones de
Java que vimos en la sección anterior es el mejor ejemplo de contenedores. List, para recapitular, contiene
una colección ordenada de elementos de tipo Object. Y Map contiene una asociación de pares clave/valor,
con las claves y los valores también siendo del tipo más general, Object. Con un poco de ayuda de
envoltorios para tipos primitivos, este arreglo nos ha servido bien. Pero (sin ponerme demasiado Zen
contigo), en cierto sentido, una "colección de cualquier tipo" también es una "colección de ningún tipo",
y trabajar con Objects impone una gran responsabilidad al usuario del contenedor.

Es como una fiesta de disfraces para objetos donde todos usan la misma máscara y se pierden entre la
multitud de la colección. Una vez que los objetos se visten como el tipo Object, el compilador ya no puede
ver los tipos reales y los pierde de vista. Depende del usuario penetrar la anonimidad de los objetos más
tarde usando una conversión de tipo. Y, al igual que al intentar arrancar la barba falsa de un fiestero, es
mejor tener la conversión correcta o te llevarás una sorpresa no deseada.
La interfaz List tiene un método add() que acepta cualquier tipo de Objecto. Aquí asignamos una instancia
de ArrayList, que es simplemente una implementación de la interfaz List, y añadimos un objeto Date. ¿Es
correcta la conversión en este ejemplo? Depende de lo que suceda en el período de tiempo omitido "..."
De hecho, el compilador de Java sabe que este tipo de actividad es problemático y actualmente emite
advertencias cuando se añaden elementos a un simple ArrayList como se ha hecho arriba. Podemos ver
esto con un pequeño desvío en jshell. Después de importar los paquetes java.util y javax.swing, intenta
crear un ArrayList y añadir algunos elementos dispares:

Puedes ver que la advertencia es la misma sin importar qué tipo de objeto añadamos(). En el último paso,
donde mostramos el contenido de 'things', tanto el objeto String simple como el objeto JLabel están
felizmente en la lista. El compilador no está preocupado por el uso de tipos dispares; está advirtiendo
útilmente que no sabrá si las conversiones como la conversión (Date) mencionada anteriormente
funcionarán en tiempo de ejecución.

¿Pueden arreglarse los contenedores?

Es natural preguntarse si hay una manera de mejorar esta situación. ¿Qué pasa si sabemos que solo vamos
a colocar objetos Date en nuestra lista? ¿No podemos simplemente crear nuestra propia lista que solo
acepte objetos Date, eliminar la conversión y dejar que el compilador nos ayude de nuevo? La respuesta,
sorprendentemente quizás, es no. Al menos, no de una manera muy satisfactoria.

Nuestro primer instinto puede ser intentar "sobrescribir" los métodos de ArrayList en una subclase. Pero,
por supuesto, reescribir el método add() en una subclase en realidad no anularía nada; añadiría un nuevo
método sobrecargado:

El objeto resultante todavía acepta cualquier tipo de objeto, simplemente invoca métodos diferentes para
lograrlo.

Avanzando, podríamos asumir una tarea más grande. Por ejemplo, podríamos escribir nuestra propia
clase DateList que no extienda ArrayList, sino que delegue los detalles de sus métodos a la
implementación de ArrayList. Con una cantidad considerable de trabajo tedioso, eso nos daría un objeto
que hace todo lo que hace una List pero que trabaja con Dates de una manera que tanto el compilador
como el entorno de ejecución pueden entender y hacer cumplir. Sin embargo, ahora nos hemos metido
en problemas porque nuestro contenedor ya no es una implementación de List y no podemos usarlo de
manera interoperable con todas las utilidades que tratan con colecciones, como Collections.sort(), o
añadirlo a otra colección con el método Collection addAll().

Para generalizar, el problema es que en lugar de refinar el comportamiento de nuestros objetos, lo que
realmente queremos hacer es cambiar su contrato con el usuario. Queremos adaptar su API a un tipo más
específico y la polimorfismo no permite eso. Parecería que estamos atrapados con Objects para nuestras
colecciones. Y aquí es donde entran los genéricos.

Introducción a los Genéricos

Como señalamos al introducir las limitaciones de tipo en la sección anterior, los genéricos son una mejora
en la sintaxis de las clases que nos permite especializar la clase para un tipo dado o conjunto de tipos. Una
clase genérica requiere uno o más parámetros de tipo dondequiera que hagamos referencia al tipo de
clase y los utiliza para personalizarse.

Si miras el código fuente o la documentación de la clase List, por ejemplo, verás que define algo así:

El identificador E entre los corchetes angulares (<>) es un parámetro de tipo.1 Indica que la clase List es
genérica y requiere un tipo de Java como argumento para completarse. El nombre E es arbitrario, pero
existen convenciones que veremos a medida que avancemos. En este caso, la variable de tipo E representa
el tipo de elementos que queremos almacenar en la lista. La clase List se refiere a la variable de tipo dentro
de su cuerpo y métodos como si fuera un tipo real, que se sustituirá más adelante. La variable de tipo
puede usarse para declarar variables de instancia, argumentos de métodos y el tipo de retorno de
métodos. En este caso, E se usa como el tipo para los elementos que añadiremos mediante el método
add() y como tipo de retorno del método get(). Veamos cómo usarlo.

La misma sintaxis de corchetes angulares proporciona el parámetro de tipo cuando queremos usar el tipo
List:

En este fragmento, hemos declarado una variable llamada `listOfStrings` utilizando el tipo genérico `List`
con un parámetro de tipo `String`. `String` se refiere a la clase String, pero podríamos tener una lista
especializada con cualquier tipo de clase Java. Por ejemplo:

Completar el tipo suministrando su parámetro de tipo se llama instanciar el tipo. También a veces se le
llama invocar el tipo, por analogía con invocar un método y suministrar sus argumentos. Mientras que
con un tipo de Java regular simplemente nos referimos al tipo por su nombre, un tipo genérico debe ser
instanciado con parámetros en cualquier lugar donde se utilice.2 Específicamente, esto significa que
debemos instanciar el tipo en todos los lugares donde los tipos pueden aparecer como el tipo declarado
de una variable (como se muestra en este fragmento de código), como el tipo de un argumento de
método, como el tipo de retorno de un método, o en una expresión de asignación de un objeto usando la
palabra clave `new`.

Retomando nuestro `listOfStrings`, lo que tenemos ahora es efectivamente una lista en la que el tipo
`String` ha sido sustituido por la variable de tipo `E` en el cuerpo de la clase:
Hemos especializado la clase List para trabajar con elementos del tipo String y solamente con elementos
del tipo String. Esta firma de método ya no es capaz de aceptar un tipo Object arbitrario.

List es simplemente una interfaz. Para utilizar la variable, necesitaremos crear una instancia de alguna
implementación real de List. Como hicimos en nuestra introducción, utilizaremos ArrayList.

Como antes, ArrayList es una clase que implementa la interfaz List, pero en este caso, tanto List como
ArrayList son clases genéricas. Por lo tanto, requieren parámetros de tipo para instanciarse donde se
utilicen. Por supuesto, crearemos nuestro ArrayList para almacenar elementos de tipo String para que
coincida con nuestra Lista de Strings:

Como siempre, la palabra clave `new` toma un tipo de Java y paréntesis con posibles argumentos para el
constructor de la clase. En este caso, el tipo es `ArrayList<String>`: el tipo genérico ArrayList instanciado
con el tipo String.

Declarar variables como se muestra en la primera línea del ejemplo anterior es un poco engorroso porque
nos obliga a escribir el tipo del parámetro genérico dos veces (una vez en el lado izquierdo en el tipo de
la variable y una vez en la expresión de inicialización). Y en casos complicados, los tipos genéricos pueden
volverse muy largos y estar anidados unos dentro de otros. A partir de Java 7, el compilador es lo
suficientemente inteligente como para inferir el tipo de la expresión de inicialización a partir del tipo de
la variable a la que le estás asignando.

Esto se llama inferencia de tipo genérico y se reduce al hecho de que puedes usar una forma abreviada
en el lado derecho de tus declaraciones de variables omitiendo el contenido de la notación <> , como se
muestra en la segunda versión del ejemplo.

Ahora podemos usar nuestra Lista especializada con strings. El compilador nos impide intentar poner
cualquier cosa que no sea un objeto String (o un subtipo de String si lo hubiera) en la lista y nos permite
obtener los elementos con el método `get()` sin requerir ningún tipo de conversión:

El interfaz Map proporciona un mapeo tipo diccionario que asocia objetos clave con objetos valor. Las
claves y los valores no tienen que ser del mismo tipo. El interfaz genérico Map requiere dos parámetros
de tipo: uno para el tipo de clave y otro para el tipo de valor. La documentación JavaDoc se ve así:
Podemos crear un Map que almacene objetos Employee mediante números enteros de "ID" de empleado
de esta manera:

Aquí utilizamos HashMap, que es una clase genérica que implementa la interfaz Map, e instanciamos
ambos tipos con los parámetros de tipo Integer y Employee. El Map ahora funciona solo con claves de
tipo Integer y almacena valores de tipo Employee.

La razón por la que usamos Integer aquí para almacenar nuestro número es que los parámetros de tipo
para una clase genérica deben ser tipos de clase. No podemos parametrizar una clase genérica con un
tipo primitivo, como int o boolean. Afortunadamente, la autoboxing de primitivos en Java (ver
"Envoltorios para Tipos Primitivos" en la página 141) hace que casi parezca que podemos hacerlo,
permitiéndonos usar tipos primitivos como si fueran tipos de envoltura.

Docenas de otras APIs más allá de las colecciones usan genéricos para permitirte adaptarlos a tipos
específicos. Hablaremos de ellas a medida que aparezcan a lo largo del libro.

Hablando sobre tipos

Antes de avanzar a cosas más importantes, deberíamos decir unas palabras sobre la forma en que
describimos una parametrización particular de una clase genérica. Debido a que el caso más común y
convincente para los genéricos es para objetos tipo contenedor, es común pensar en un tipo genérico
"conteniendo" un tipo de parámetro. En nuestro ejemplo, llamamos a nuestro List<String> una "lista de
cadenas" porque, efectivamente, eso era lo que era. De manera similar, podríamos haber llamado a
nuestro mapa de empleados un "Mapa de IDs de empleados a objetos Employee". Sin embargo, estas
descripciones se centran un poco más en lo que hacen las clases que en el tipo en sí mismo. Toma en su
lugar un contenedor de un solo objeto llamado Trap<E> que podría instanciarse en un objeto de tipo
Mouse o de tipo Bear; es decir, Trap<Mouse> o Trap<Bear>. Nuestro instinto es llamar al nuevo tipo una
"trampa para ratones" o "trampa para osos". De manera similar, podríamos haber pensado en nuestra
lista de cadenas como un nuevo tipo: "lista de cadenas", o nuestro mapa de empleados como un nuevo
tipo de "mapa de objetos Employee con clave Integer". Puedes usar el vocabulario que prefieras, pero
estas últimas descripciones se centran más en la noción del genérico como un tipo y pueden ayudarte a
mantener claros los términos cuando discutamos cómo se relacionan los tipos genéricos en el sistema de
tipos. Allí veremos que la terminología de contenedor resulta ser un poco contraintuitiva.

En la siguiente sección, continuaremos nuestra discusión sobre los tipos genéricos en Java desde una
perspectiva diferente. Hemos visto un poco de lo que pueden hacer; ahora necesitamos hablar sobre
cómo lo hacen.

"No hay cuchara"

En la película The Matrix, el héroe Neo se enfrenta a una elección. Tomar la pastilla azul y permanecer en
el mundo de fantasía, o tomar la pastilla roja y ver las cosas como realmente son. Al tratar con genéricos
en Java, nos enfrentamos a un dilema ontológico similar. Solo podemos ir hasta cierto punto en cualquier
discusión sobre genéricos antes de que nos veamos obligados a confrontar la realidad de cómo están
implementados. Nuestro mundo de fantasía es creado por el compilador para hacer nuestras vidas
escribiendo código más fáciles de aceptar. Nuestra realidad (aunque no es exactamente la pesadilla
distópica de la película) es un lugar más duro, lleno de peligros y preguntas invisibles. ¿Por qué los casts y
tests no funcionan correctamente con genéricos? ¿Por qué no puedo implementar lo que parecen ser dos
interfaces genéricas diferentes en una clase? ¿Por qué puedo declarar un array de tipos genéricos, aunque
no haya forma en Java de crear tal array?!? Respondemos a estas preguntas y más en este capítulo, y ni
siquiera tendrás que esperar la secuela. Doblarás cucharas (bueno, tipos) en poco tiempo. Comencemos.

Los objetivos de diseño para los genéricos de Java eran formidables: agregar una nueva sintaxis radical al
lenguaje que introdujera tipos parametrizados sin impactar el rendimiento y, además, hacerlo compatible
con todo el código Java existente sin cambiar las clases compiladas de manera seria. Es realmente
asombroso que se pudieran satisfacer estas condiciones y no es sorprendente que haya llevado un
tiempo. Pero como siempre, se requirieron compromisos, lo que llevó a algunos dolores de cabeza.

Borrado

Para lograr esta hazaña, Java emplea una técnica llamada borrado (erasure), que se relaciona con la idea
de que como casi todo lo que hacemos con genéricos se aplica estáticamente en tiempo de compilación,
la información genérica no necesita ser llevada a las clases compiladas. La naturaleza genérica de las
clases, impuesta por el compilador, puede ser "borrada" en las clases compiladas, lo que nos permite
mantener la compatibilidad con código no genérico. Si bien Java conserva información sobre las
características genéricas de las clases en la forma compilada, esta información se utiliza principalmente
por el compilador. El entorno de ejecución de Java no sabe nada sobre genéricos en absoluto.

Echemos un vistazo a una clase genérica compilada: nuestro amigo, List. Podemos hacer esto fácilmente
con el comando javap:

El resultado se ve exactamente igual que antes de la introducción de los genéricos en Java, como puedes
confirmar con cualquier versión antigua del JDK. Es notable que el tipo de elementos utilizado con los
métodos `add()` y `get()` es `Object`. Ahora, podrías pensar que esto es solo una artimaña y que cuando
se instancia el tipo real, Java creará una nueva versión de la clase internamente. Pero ese no es el caso.
Esta es la única y verdadera clase `List`, y es el tipo de ejecución real utilizado por todas las
parametrizaciones de `List`; por ejemplo, `List<Date>` y `List<String>`, como podemos confirmar:

Pero nuestra lista genérica de fechas (`dateList`) claramente no implementa los métodos de List que
acabamos de discutir:

Esto ilustra la naturaleza algo esquizofrénica de los genéricos en Java. El compilador cree en ellos, pero el
entorno de ejecución dice que son una ilusión. ¿Qué pasa si intentamos algo un poco más sensato y
simplemente verificamos si nuestra `dateList` es una `List<Date>`?:

En este caso, el compilador simplemente se muestra inflexible y dice: "No". No se puede probar un tipo
genérico en una operación `instanceof`. Dado que no existen clases diferenciables reales para diferentes
parametrizaciones de `List` en tiempo de ejecución, no hay forma para el operador `instanceof` de
distinguir entre una encarnación de `List` y otra. Toda la verificación de seguridad genérica se realizó en
tiempo de compilación y ahora estamos tratando con un solo tipo de `List` real.

Lo que realmente ha sucedido es que el compilador ha borrado toda la sintaxis de los corchetes angulares
y ha reemplazado las variables de tipo en nuestra clase `List` con un tipo que puede funcionar en tiempo
de ejecución con cualquier tipo permitido: en este caso, `Object`. Parecería que estamos de vuelta donde
comenzamos, excepto que el compilador aún tiene el conocimiento para hacer cumplir nuestro uso de los
genéricos en el código en tiempo de compilación y, por lo tanto, puede manejar la conversión por
nosotros. Si descompilas una clase que utiliza una `List<Date>` (el comando `javap` con la opción `-c`
muestra el bytecode, si te atreves), verás que el código compilado realmente contiene la conversión a
`Date`, aunque no la hayamos escrito nosotros mismos.

Ahora podemos responder una de las preguntas que planteamos al principio de la sección: "¿Por qué no
puedo implementar lo que parecen ser dos interfaces genéricas diferentes en una clase?" No podemos
tener una clase que implemente dos instanciaciones diferentes de List genérico porque en realidad son el
mismo tipo en tiempo de ejecución y no hay forma de distinguirlos.

Afortunadamente, siempre hay soluciones alternativas. En este caso, por ejemplo, puedes utilizar una
superclase común o crear múltiples clases. Las alternativas pueden no ser tan elegantes como te gustaría,
pero casi siempre puedes encontrar una solución limpia, incluso si es un poco más verbosa.

Tipos sin formato (Raw Types)

Aunque el compilador trata diferentes parametrizaciones de un tipo genérico como tipos diferentes (con
APIs diferentes) en tiempo de compilación, hemos visto que solo existe un único tipo real en tiempo de
ejecución. Por ejemplo, la clase List<Date> y List<String> comparten la clase Java común List. A List se le
llama el tipo sin formato (raw type) de la clase genérica. Cada genérico tiene un tipo sin formato. Es la
forma degenerada o "simple" en Java, de la cual se ha eliminado toda la información de tipo genérico y
se han reemplazado las variables de tipo por un tipo Java general como Object.

Todavía es posible utilizar tipos sin formato en Java como antes de que se agregaran los genéricos al
lenguaje. La única diferencia es que el compilador de Java genera una advertencia dondequiera que se
utilicen de manera "poco segura". Fuera de jshell, el compilador sigue notando estos problemas:

Este fragmento de código utiliza el tipo sin formato `List` de la misma manera que lo haría un código
antiguo en Java antes de Java 5. La diferencia es que ahora el compilador de Java emite una advertencia
de tipo "unchecked" sobre el código si intentamos insertar un objeto en la lista:

El compilador nos indica que usemos la opción -Xlint:unchecked para obtener información más específica
sobre las ubicaciones de las operaciones inseguras:
Ten en cuenta que crear y asignar el `ArrayList` sin formato no genera una advertencia. Solo cuando
intentamos usar un método "inseguro" (uno que hace referencia a una variable de tipo) es que obtenemos
la advertencia. Esto significa que todavía está bien usar APIs de Java más antiguas que trabajan con tipos
sin formato. Solo obtenemos advertencias cuando hacemos algo inseguro en nuestro propio código.

Un detalle más sobre el borrado (erasure) antes de seguir adelante. En los ejemplos anteriores, las
variables de tipo fueron reemplazadas por el tipo `Object`, que podría representar cualquier tipo aplicable
a la variable de tipo `E`. Más adelante, veremos que esto no siempre es así. Podemos establecer
limitaciones o restricciones en los tipos de parámetros, y cuando lo hacemos, el compilador puede ser
más restrictivo sobre el borrado del tipo, por ejemplo:

Esta declaración del tipo de parámetro indica que el tipo de elemento `E` debe ser un subtipo del tipo
`Date`. En este caso, el borrado (erasure) del método `addElement()` es, por lo tanto, más restrictivo que
`Object`, y el compilador utiliza `Date`:

A Date es llamado el límite superior (upper bound) de este tipo, lo que significa que es la cima de la
jerarquía de objetos aquí y el tipo solo puede ser instanciado en el tipo Date o en tipos "inferiores" (más
derivados).

Ahora que tenemos una idea de qué son realmente los tipos genéricos, podemos adentrarnos un poco
más en cómo se comportan.

Relaciones entre tipos parametrizados

Ahora sabemos que los tipos parametrizados comparten un tipo sin formato común. Es por eso que
nuestro List<Date> es simplemente un List en tiempo de ejecución. De hecho, podemos asignar cualquier
instanciación de List al tipo sin formato si así lo deseamos:

Incluso podemos hacer lo contrario y asignar un tipo sin formato a una instanciación específica del tipo
genérico:

Esta declaración genera una advertencia de tipo "unchecked" en la asignación, pero luego el compilador
confía en que la lista contenía solo objetos de tipo `Date` antes de la asignación. También es permisible,
aunque no tenga sentido, realizar un casting en esta declaración. Hablaremos sobre la conversión a tipos
genéricos en breve en la sección "Conversiones" en la página 215.

Independientemente de los tipos en tiempo de ejecución, el compilador está a cargo y no nos permite
asignar cosas que son claramente incompatibles:

Por supuesto, el ArrayList<String> no implementa los métodos de List<Date> sugeridos por el compilador,
por lo que estos tipos son incompatibles.
Pero, ¿qué hay de las relaciones entre tipos más interesantes? La interfaz List, por ejemplo, es un subtipo
de la interfaz más general Collection. ¿Es una instancia particular del tipo genérico List también asignable
a alguna instancia del tipo genérico Collection? ¿Depende de los parámetros de tipo y sus relaciones?
Claramente, un List<Date> no es un Collection<String>. Pero, ¿es un List<Date> un Collection<Date>?
¿Puede un List<Date> ser un Collection<Object>?

Simplemente daremos la respuesta aquí primero, y luego la analizaremos y explicaremos. La regla es que
para los tipos simples de instanciaciones genéricas que hemos discutido hasta ahora, la herencia se aplica
solo al tipo genérico "base" y no a los tipos de parámetros. Además, la asignabilidad se aplica solo cuando
los dos tipos genéricos están instanciados exactamente con el mismo tipo de parámetro. En otras
palabras, sigue existiendo una herencia unidimensional, siguiendo el tipo de clase genérica base, pero con
la restricción adicional de que los tipos de parámetros deben ser idénticos.

Por ejemplo, recordando que un List es un tipo de Collection, podemos asignar instanciaciones de List a
instanciaciones de Collection cuando el tipo de parámetro es exactamente el mismo:

Este fragmento de código indica que un List<Date> es un Collection<Date> - bastante intuitivo.

Pero al intentar la misma lógica con una variación en los tipos de parámetros, falla:

Aunque nuestra intuición nos dice que los objetos de tipo Date en ese List podrían vivir felizmente como
Objetos en un List, la asignación es un error. Explicaremos precisamente por qué en la próxima sección,
pero por ahora simplemente observa que los tipos de parámetros no son exactamente iguales y que no
hay una relación de herencia entre los tipos de parámetros en los genéricos. Este es un caso en el que
pensar en la instanciación en términos de tipos y no en términos de lo que hacen ayuda. Estos no son
realmente una "lista de fechas" y una "lista de objetos", sino más bien una DateList y una ObjectList, cuya
relación no es inmediatamente evidente.

Intenta identificar qué está bien y qué no está bien en el siguiente ejemplo:

Es posible que una instanciación de List sea una instanciación de Collection, pero solo si los tipos de
parámetros son exactamente iguales. La herencia no sigue los tipos de parámetros y este ejemplo falla.

Una cosa más: anteriormente mencionamos que esta regla se aplica a los tipos simples de instanciaciones
que hemos discutido hasta ahora en este capítulo. ¿Qué otros tipos hay? Bueno, los tipos de
instanciaciones que hemos visto hasta ahora, donde enchufamos un tipo Java real como parámetro, se
llaman instanciaciones de tipo concretas. Más adelante, hablaremos sobre las instanciaciones de
comodines, que son similares a operaciones matemáticas de conjuntos en tipos. Veremos que es posible
hacer instanciaciones más exóticas de genéricos donde las relaciones de tipos son realmente
bidimensionales, dependiendo tanto del tipo base como de la parametrización. Pero no te preocupes:
esto no ocurre muy a menudo y no es tan aterrador como suena.
¿Por qué no es una List<Date> una List<Object>?

Es una pregunta razonable. Incluso con nuestros cerebros pensando en tipos arbitrarios DateList y
ObjectList, todavía podemos preguntarnos por qué no podrían ser asignables. ¿Por qué no deberíamos
poder asignar nuestro List<Date> a un List<Object> y trabajar con los elementos Date como tipos Object?

La razón se remonta al corazón de la justificación de los genéricos que discutimos en la introducción:


cambiar las APIs. En el caso más simple, suponiendo que un tipo ObjectList extienda un tipo DateList, el
DateList tendría todos los métodos de ObjectList y aún podríamos insertar objetos en él. Ahora, podrías
objetar que los genéricos nos permiten cambiar las APIs, por lo que eso ya no se aplica. Es cierto, pero
hay un problema mayor. Si pudiéramos asignar nuestro DateList a una variable ObjectList, tendríamos que
poder usar métodos Object para insertar elementos de tipos distintos a Date en él. Podríamos alias
(proporcionar un tipo alternativo, más amplio) el DateList como un ObjectList e intentar engañarlo para
que acepte algún otro tipo:

Este escenario generaría un error en tiempo de ejecución cuando la implementación real de DateList fuera
presentada con el tipo incorrecto de objeto. Y aquí radica el problema. Los genéricos en Java no tienen
representación en tiempo de ejecución. Incluso si esta funcionalidad fuera útil, no hay forma, con el
esquema actual de Java, de saber qué hacer en tiempo de ejecución. Otra forma de verlo es que esta
característica es simplemente peligrosa porque permite un error en tiempo de ejecución que no podría
ser capturado en tiempo de compilación. En general, preferimos detectar errores de tipo en tiempo de
compilación.

Puede que pienses que Java podría garantizar que tu código es seguro en cuanto a tipos si se compila sin
advertencias sin verificar estas asignaciones. Desafortunadamente, no puede hacerlo, pero esto no tiene
que ver con los genéricos, sino con los arrays. Si esto te suena familiar, es porque lo mencionamos
anteriormente en relación con los arrays de Java. Los tipos de arrays tienen una relación de herencia que
permite que este tipo de aliasing ocurra:

Sin embargo, los arrays tienen representaciones en tiempo de ejecución como clases diferentes y se
verifican a sí mismos en tiempo de ejecución, lanzando una ArrayStoreException en este caso específico.
Por lo tanto, en teoría, el código de Java no está garantizado para ser seguro en cuanto a tipos por el
compilador si se usan arrays de esta manera.

En cuanto a las conversiones (casts):

Hasta ahora hemos hablado sobre las relaciones entre tipos genéricos e incluso entre tipos genéricos y
tipos no parametrizados. Pero aún no hemos explorado el concepto de conversiones en el mundo de los
genéricos. No fue necesario realizar una conversión cuando intercambiamos genéricos con sus tipos no
parametrizados. En cambio, simplemente cruzamos una línea que desencadena advertencias no
verificadas del compilador:

En Java, normalmente usamos una conversión (cast) para trabajar con dos tipos que podrían ser
asignables. Por ejemplo, podríamos intentar convertir un Object a un Date porque es plausible que el
Object sea un valor de tipo Date. La conversión realiza la verificación en tiempo de ejecución para
determinar si estamos en lo correcto. Sin embargo, realizar una conversión entre tipos no relacionados
es un error en tiempo de compilación. Por ejemplo, no podemos intentar convertir un Integer a un String,
ya que esos tipos no tienen relación de herencia. ¿Qué sucede con las conversiones entre tipos genéricos
compatibles?

Este fragmento de código muestra una conversión válida de una Collection<Date> más general a un
List<Date>. La conversión es plausible en este caso porque una Collection<Date> es asignable y de hecho
podría ser un List<Date>. Del mismo modo, la siguiente conversión detecta nuestro error donde hemos
utilizado un TreeSet<Date> como si fuera una Collection<Date> y luego intentamos convertirlo a un
List<Date>:

Este fragmento de código es un ejemplo donde los casteos no son efectivos con genéricos, especialmente
cuando se intenta diferenciar los tipos basándose en sus tipos de parámetros:

Aquí, hemos dado un alias a un ArrayList<String> como un Object simple. Luego, lo convertimos a un
List<Date>. Desafortunadamente, Java no distingue entre un List<String> y un List<Date> en tiempo de
ejecución, por lo que la conversión es inútil. El compilador nos advierte sobre esto generando una
advertencia no verificada en el lugar de la conversión; debemos ser conscientes de que cuando
intentemos usar el objeto convertido más adelante, podríamos descubrir que es incorrecto. Las
conversiones en tipos genéricos son ineficaces en tiempo de ejecución debido al borrado de tipo y a la
falta de información de tipo.

Conversión entre Colecciones y Matrices (arrays)

Convertir entre colecciones y arrays es fácil. Para mayor comodidad, los elementos de una colección
pueden ser obtenidos como un array usando los siguientes métodos:

El primer método devuelve una matriz (array) de Object simples. Con la segunda forma, podemos ser más
específicos y obtener de vuelta una matriz del tipo de elemento correcto. Si suministramos una matriz de
tamaño suficiente, esta se llenará con los valores. Pero si la matriz es demasiado corta (por ejemplo, de
longitud cero), se creará y nos devolverá una nueva matriz del mismo tipo pero con la longitud requerida.
Por lo tanto, simplemente puedes pasar una matriz vacía del tipo correcto de esta manera:

(Este truco es un poco incómodo y sería bueno si Java nos permitiera especificar el tipo explícitamente
usando una referencia de Clase, pero por alguna razón, esto no es posible). Yendo en la otra dirección,
puedes convertir una matriz (array) de objetos a una colección List utilizando el método estático asList()
de la clase java.util.Arrays:
Iterator

Un iterador es un objeto que te permite recorrer una secuencia de valores. Esta clase de operación es tan
común que tiene una interfaz estándar: java.util.Iterator. La interfaz Iterator tiene solo dos métodos
principales:

public E next()

Este método devuelve el siguiente elemento (un elemento de tipo genérico E) de la colección
asociada.

public boolean hasNext()

Este método devuelve true si aún no has recorrido todos los elementos de la colección. En otras
palabras, devuelve true si puedes llamar a next() para obtener el siguiente elemento.

El siguiente ejemplo muestra cómo podrías usar un Iterator para imprimir cada elemento de una
colección:

Además de los métodos de recorrido, Iterator proporciona la capacidad de eliminar un elemento de una
colección:

public void remove()

Este método elimina el objeto más reciente devuelto por next() de la Colección asociada.

No todos los iteradores implementan remove(). No tiene sentido poder eliminar un elemento de una
colección de solo lectura, por ejemplo. Si la eliminación de elementos no está permitida, se lanzará una
UnsupportedOperationException desde este método. Si llamas a remove() antes de llamar a next() por
primera vez, o si llamas a remove() dos veces seguidas, obtendrás un IllegalStateException.

Ciclo for sobre colecciones

Una forma del ciclo for, descrita en "El ciclo for" en la página 105, puede operar sobre todos los tipos
Iterable, lo que significa que puede iterar sobre todos los tipos de objetos Collection, ya que esa interfaz
extiende Iterable. Por ejemplo, ahora podemos recorrer todos los elementos de una colección tipada de
objetos Date de la siguiente manera:

Esta característica del bucle for incorporado en Java se llama "bucle for mejorado" (a diferencia del bucle
for previo a los genéricos, que era solo para valores numéricos). El bucle for mejorado se aplica solo a
colecciones de tipo Collection, no a Maps. Los Maps son otro tipo de estructura que realmente contienen
dos conjuntos distintos de objetos (claves y valores), por lo que no está claro cuáles serían tus intenciones
en un bucle de este tipo. Pero como recorrer un mapa parece razonable, puedes usar los métodos keySet()
o values() (o incluso entrySet() si realmente deseas cada par clave/valor como una sola entidad) para
obtener la colección correcta de tu mapa que funcione con este bucle for mejorado.
Un vistazo más de cerca: El método sort()

Explorando la clase java.util.Collections, encontramos todo tipo de métodos de utilidad estáticos para
trabajar con colecciones. Entre ellos está este método interesante: el método estático genérico sort():

Otro desafío para resolver. Centrémonos en la última parte del límite:

Esta es una instanciación de comodín (wildcard) de la interfaz Comparable, por lo que podemos leer el
"extends" como "implements" si eso ayuda. Comparable contiene un método compareTo() para algún
tipo de parámetro. Un Comparable<String> significa que el método compareTo() toma el tipo String. Por
lo tanto, Comparable<? super T> es el conjunto de instanciaciones de Comparable en T y todas sus
superclases. Un Comparable<T> es suficiente y, en el otro extremo, también lo es un
Comparable<Object>. Lo que esto significa en inglés es que los elementos deben ser comparables con su
propio tipo o algún supertipo de su propio tipo para que el método sort() los utilice. Esto es suficiente
para asegurar que los elementos puedan ser comparados entre sí, pero no es tan restrictivo como decir
que todos deben implementar el método compareTo() ellos mismos. Algunos de los elementos pueden
heredar la interfaz Comparable de una clase padre que sabe cómo comparar solo con un supertipo de T,
y eso es exactamente lo que se permite aquí.

Hablemos de aplicación: Árboles en el campo

Hay mucha teoría en este capítulo. No temas a la teoría, puede ayudarte a predecir el comportamiento
en escenarios novedosos e inspirar soluciones a problemas nuevos. Pero la práctica es igual de
importante, así que pongamos en práctica algunas de estas colecciones al volver a nuestro juego que
comenzamos en "Clases" en la página 124. En particular, es hora de almacenar más de un objeto de cada
tipo.

En el Capítulo 11 cubriremos redes y consideraremos la creación de una configuración multijugador que


requeriría almacenar múltiples físicos. Por ahora, todavía tenemos nuestro físico capaz de lanzar una
manzana a la vez. Pero podemos poblar nuestro campo con varios árboles para practicar puntería.
¡Newton tendrá su venganza!

Agreguemos seis árboles, aunque usaremos un par de bucles para que puedas aumentar fácilmente la
cantidad de árboles si lo deseas. Nuestro Campo actualmente almacena una instancia de árbol solitaria.
Podemos convertir eso en una lista tipada. A partir de ahí, podemos abordar la adición y eliminación de
árboles de varias maneras. Podemos crear algunos métodos para el Campo que trabajen con la lista y tal
vez hacer cumplir algunas otras reglas del juego (como administrar un número máximo de árboles).
Podríamos simplemente usar la lista directamente, ya que la clase List ya tiene métodos útiles para la
mayoría de las cosas que queremos hacer. O podríamos usar alguna combinación de esos enfoques:
métodos especiales donde tenga sentido y manipulación directa en todas partes.

Dado que tenemos algunas reglas del juego que son peculiares a nuestro Campo, adoptaremos el primer
enfoque aquí. (Pero mira los ejemplos y piensa en cómo podrías modificarlos para usar la lista de árboles
directamente). Comenzaremos con un método addTree(). Un beneficio de este enfoque es que también
podemos trasladar la creación de la instancia del árbol a nuestro método en lugar de crear y manipular el
árbol por separado. Aquí hay una forma de agregar un árbol en un punto deseado en el campo:

Con ese método en su lugar, podríamos agregar un par de árboles bastante rápido:
Esas dos líneas agregan un par de árboles uno al lado del otro. Continuemos y escribamos los bucles que
necesitamos para crear nuestros seis árboles:

Con suerte, ahora puedes ver lo fácil que sería agregar ocho, nueve o cien árboles si así lo deseas. Como
mencionamos antes, las computadoras son realmente buenas en la repetición. ¡Hurra por crear nuestro
bosque de objetivos de manzanas! Sin embargo, dejamos algunos detalles críticos pendientes. El más
importante es mostrar ese bosque en la pantalla. Necesitamos mejorar nuestro método de dibujo para la
clase Field para que entienda y use correctamente nuestra lista de árboles. Eventualmente, haremos lo
mismo para nuestros físicos y manzanas a medida que agreguemos más funcionalidades a nuestro juego.
También necesitaremos una forma de eliminar elementos que ya no estén activos. ¡Pero primero, nuestro
bosque!

Dado que ya estamos en la clase Field donde se almacenan nuestros árboles, no es necesario escribir una
función separada para extraer un árbol individual y pintarlo. Podemos utilizar la práctica estructura
alternativa del bucle for y rápidamente obtener todos nuestros árboles en el campo, como se muestra en
la Figura 7-1. ¡Genial!
Conclusión

Las colecciones y los genéricos de Java son adiciones muy potentes y útiles al lenguaje. Aunque algunos
de los detalles en los que profundizamos en la segunda mitad de este capítulo pueden parecer
desafiantes, su uso común es muy simple y convincente: los genéricos mejoran las colecciones. A medida
que comiences a escribir más código utilizando genéricos, descubrirás que tu código se vuelve más legible
y comprensible. Las colecciones permiten un almacenamiento elegante y eficiente. Los genéricos hacen
explícito lo que antes se debía inferir a partir del uso.
CAPÍTULO 8

Texto y Utilidades Principales

Si has estado leyendo este libro secuencialmente, has leído todo sobre los constructos básicos del
lenguaje Java, incluyendo los aspectos orientados a objetos del lenguaje y el uso de hilos. Ahora es
momento de cambiar de enfoque y comenzar a hablar sobre la interfaz de programación de aplicaciones
(API) de Java, la colección de clases que componen los paquetes estándar de Java y que vienen con cada
implementación de Java. Los paquetes centrales de Java son una de sus características más distintivas.
Muchos otros lenguajes orientados a objetos tienen características similares, pero ninguno tiene un
conjunto tan extenso de APIs y herramientas estandarizadas como Java. Esto es tanto un reflejo como
una razón del éxito de Java.

Cadenas de texto

Comenzaremos por echar un vistazo más de cerca a la clase String de Java (o, más específicamente,
java.lang.String). Debido a que trabajar con cadenas de texto es tan fundamental, es importante entender
cómo están implementadas y qué puedes hacer con ellas. Un objeto String encapsula una secuencia de
caracteres Unicode. Internamente, estos caracteres se almacenan en un arreglo regular de Java, pero el
objeto String protege este arreglo celosamente y solo te da acceso a él a través de su propia API. Esto es
para respaldar la idea de que las cadenas de texto son inmutables; una vez que creas un objeto String, no
puedes cambiar su valor. Muchas operaciones en un objeto String parecen cambiar los caracteres o la
longitud de una cadena, pero en realidad lo que hacen es devolver un nuevo objeto String que copia o
hace referencia internamente a los caracteres necesarios del original. Las implementaciones de Java
hacen un esfuerzo por consolidar cadenas idénticas utilizadas en la misma clase en un pool de cadenas
compartido y compartir partes de cadenas cuando es posible.

La motivación original de todo esto fue el rendimiento. Las cadenas de texto inmutables pueden ahorrar
memoria y ser optimizadas para velocidad por la Máquina Virtual de Java. El lado negativo es que un
programador debe tener un entendimiento básico de la clase String para evitar crear un número excesivo
de objetos String en lugares donde el rendimiento es un problema. Esto era especialmente cierto en el
pasado, cuando las Máquinas Virtuales eran lentas y manejaban mal la memoria. Hoy en día, el uso de
cadenas de texto generalmente no suele ser un problema en el rendimiento global de una aplicación real.

Construyendo Cadenas de Texto

Las cadenas literales, definidas en tu código fuente, se declaran con comillas dobles y pueden asignarse a
una variable String.

Java automáticamente convierte la cadena literal en un objeto String y la asigna a la variable. Las cadenas
de texto llevan un seguimiento de su propia longitud, por lo que los objetos String en Java no requieren
terminadores especiales. Puedes obtener la longitud de una cadena de texto con el método length().
También puedes comprobar si una cadena tiene longitud cero utilizando isEmpty():
Las cadenas de texto pueden aprovechar el único operador sobrecargado en Java, el operador +, para la
concatenación de cadenas. El siguiente código produce cadenas equivalentes:

Las cadenas literales no pueden (todavía2) abarcar líneas en archivos fuente de Java, pero podemos
concatenar líneas para lograr el mismo efecto:

Incrustar textos extensos en el código fuente normalmente no es algo que desees hacer. En el Capítulo
11, hablaremos sobre formas de cargar cadenas de texto desde archivos y URL.

Además de crear cadenas de texto a partir de expresiones literales, puedes construir un String
directamente a partir de un arreglo de caracteres:

También puedes construir un String a partir de un arreglo de bytes:

En este caso, el segundo argumento para el constructor de String es el nombre de un esquema de


codificación de caracteres. El constructor de String lo utiliza para convertir los bytes en bruto en la
codificación especificada al esquema de codificación interno elegido por el tiempo de ejecución. Si no
especificas una codificación de caracteres, se utiliza el esquema de codificación predeterminado en tu
sistema.3

Por otro lado, el método charAt() de la clase String te permite acceder a los caracteres de una cadena de
texto de manera similar a un arreglo:

Este código imprime los caracteres de la cadena uno a uno.

La noción de que una cadena de texto es una secuencia de caracteres también está codificada por la clase
String al implementar la interfaz java.lang.CharSequence, la cual prescribe los métodos length() y charAt()
como una forma de obtener un subconjunto de los caracteres.

Cadenas desde Objetos

Los objetos y tipos primitivos en Java pueden ser convertidos a una representación textual
predeterminada como un String. Para tipos primitivos como números, la cadena resultante debería ser
bastante obvia; para tipos de objeto, está bajo el control del propio objeto. Podemos obtener la
representación de cadena de un elemento con el método estático String.valueOf(). Varias versiones
sobrecargadas de este método aceptan cada uno de los tipos primitivos:
Todos los objetos en Java tienen un método toString() que es heredado de la clase Object. Para muchos
objetos, este método devuelve un resultado útil que muestra el contenido del objeto. Por ejemplo, el
método toString() de un objeto java.util.Date devuelve la fecha que representa formateada como una
cadena de texto. Para objetos que no proporcionan una representación, el resultado de la cadena es solo
un identificador único que puede ser utilizado para fines de depuración. El método String.valueOf(),
cuando se llama para un objeto, invoca el método toString() del objeto y devuelve el resultado. La única
diferencia real al utilizar este método es que si le pasas una referencia de objeto nula, te devuelve la
cadena "null" en lugar de producir una NullPointerException:

La concatenación de cadenas utiliza internamente el método valueOf(), por lo que si "añades" un objeto
o un primitivo usando el operador de suma (+), obtienes una cadena de texto:

A veces verás a personas utilizar la cadena vacía y el operador de suma (+) como una forma abreviada
para obtener el valor de una cadena de un objeto. Por ejemplo:

Comparando Cadenas

El método estándar equals() puede comparar cadenas de texto para determinar si son iguales; esto
significa que contienen exactamente los mismos caracteres en el mismo orden. Puedes utilizar un método
diferente, equalsIgnoreCase(), para verificar la equivalencia de las cadenas de texto de manera que no
distinga entre mayúsculas y minúsculas:

Un error común para los programadores novatos en Java es comparar cadenas de texto con el operador
== cuando en realidad quieren utilizar el método equals(). Recuerda que en Java, las cadenas de texto son
objetos y == comprueba la identidad del objeto; es decir, si los dos argumentos que se están probando
son el mismo objeto. En Java, es fácil crear dos cadenas de texto que tengan los mismos caracteres pero
que no sean el mismo objeto de cadena. Por ejemplo:

Este error es particularmente peligroso porque a menudo funciona para el caso común en el que estás
comparando cadenas literales (cadenas declaradas con comillas dobles directamente en el código). La
razón de esto es que Java intenta gestionar eficientemente las cadenas de texto combinándolas. En
tiempo de compilación, Java encuentra todas las cadenas idénticas dentro de una clase determinada y
crea solo un objeto para ellas. Esto es seguro porque las cadenas de texto son inmutables y no pueden
cambiar. Puedes combinar cadenas de esta manera tú mismo en tiempo de ejecución utilizando el método
intern() de la clase String. Internar una cadena devuelve una referencia de cadena equivalente que es
única en toda la Máquina Virtual (VM).

El método compareTo() compara el valor léxico de la cadena de texto con otra cadena, determinando si
se ordena alfabéticamente antes, es igual que, o después de la cadena objetivo. Retorna un entero que
es menor que, igual a, o mayor que cero:

El método compareTo() compara las cadenas estrictamente por las posiciones de sus caracteres en la
especificación Unicode. Esto funciona para textos simples, pero no maneja bien todas las variaciones de
idioma. La clase Collator, discutida a continuación, puede ser utilizada para comparaciones más
sofisticadas.

Búsqueda

La clase String proporciona varios métodos simples para encontrar subcadenas fijas dentro de una cadena
de texto. Los métodos startsWith() y endsWith() comparan una cadena de texto argumento con el
comienzo y el final de la cadena de texto, respectivamente:

El método indexOf() busca la primera ocurrencia de un carácter o subcadena y devuelve la posición del
carácter de inicio, o -1 si la subcadena no se encuentra:

De manera similar, lastIndexOf() busca hacia atrás en la cadena la última ocurrencia de un carácter o
subcadena.

El método contains() maneja la tarea muy común de verificar si una subcadena dada está contenida en la
cadena de texto objetivo:

Para búsquedas más complejas, puedes utilizar la API de Expresiones Regulares, que te permite buscar y
analizar patrones complejos. Hablaremos sobre las expresiones regulares más adelante en este capítulo.
Resumen de Métodos de String

La Tabla 8-1 resume los métodos proporcionados por la clase String. Hemos incluido varios métodos que
no hemos discutido en este capítulo para asegurarnos de que estés al tanto de otras capacidades de la
clase String. Siéntete libre de probar estos métodos en jshell o consultar la documentación en línea.
Cosas desde Cadenas

Analizar y formatear texto es un tema amplio y abierto. Hasta ahora en este capítulo, solo hemos
examinado operaciones primitivas en cadenas: creación, búsqueda y conversión de valores simples a
cadenas. Ahora nos gustaría avanzar hacia formas más estructuradas de texto.

Java tiene un conjunto completo de APIs para analizar e imprimir cadenas formateadas, incluyendo
números, fechas, horas y valores de moneda. Cubriremos la mayoría de estos temas en este capítulo, pero
esperaremos para discutir el formato de fechas y horas en "Fechas y Horas Locales" en la página 248.

Comenzaremos con el análisis —leyendo números primitivos y valores como cadenas, y dividiendo
cadenas largas en tokens. Luego echaremos un vistazo a las expresiones regulares, la herramienta de
análisis de texto más potente que ofrece Java. Las expresiones regulares le permiten definir sus propios
patrones de complejidad arbitraria, buscarlos y analizarlos desde el texto.

Análisis de Números Primitivos

En Java, los números, caracteres y booleanos son tipos primitivos, no objetos. Pero para cada tipo
primitivo, Java también define una clase envolvente primitiva. Específicamente, el paquete java.lang
incluye las siguientes clases: Byte, Short, Integer, Long, Float, Double, Character y Boolean. Hablamos de
estos en "Envoltorios para Tipos Primitivos" en la página 141, pero los mencionamos ahora porque estas
clases contienen métodos de utilidad estáticos que saben cómo analizar sus respectivos tipos desde
cadenas. Cada una de estas clases envolventes primitivas tiene un método estático "parse" que lee una
cadena y devuelve el tipo primitivo correspondiente. Por ejemplo:

Alternativamente, java.util.Scanner proporciona una única API no solo para analizar tipos primitivos
individuales desde cadenas, sino también para leerlos desde un flujo de tokens. Este ejemplo muestra
cómo utilizarlo en lugar de las clases envolventes anteriores:

Tokenización de texto

Una tarea común en programación implica analizar una cadena de texto en palabras o "tokens" que están
separados por un conjunto de caracteres delimitadores, como espacios o comas. El primer ejemplo
contiene palabras separadas por espacios simples. El segundo, un problema más realista, implica campos
delimitados por comas.
Java tiene varios (lamentablemente superpuestos) APIs para manejar situaciones como esta. Los más
potentes y útiles son los métodos split() de la clase String y los métodos de Scanner. Ambos utilizan
expresiones regulares para permitirte dividir la cadena en patrones arbitrarios. Aún no hemos hablado
sobre expresiones regulares, pero para mostrarte cómo funciona esto, simplemente te daremos la magia
necesaria y la explicaremos en detalle más adelante en este capítulo. También mencionaremos una
utilidad heredada, java.util.StringTokenizer, que utiliza conjuntos simples de caracteres para dividir una
cadena. StringTokenizer no es tan poderoso, pero no requiere un conocimiento de expresiones regulares.

El método split() de la clase String acepta una expresión regular que describe un delimitador y la utiliza
para dividir la cadena en un array de Strings:

En el primer ejemplo, utilizamos la expresión regular \\s, que coincide con un solo carácter de espacio en
blanco (espacio, tabulación o retorno de carro). El método split() devolvió un array de ocho cadenas. En
el segundo ejemplo, usamos una expresión regular más complicada, \\s*,\\s*, que coincide con una coma
rodeada por cualquier cantidad de espacios contiguos (posiblemente cero). Esto redujo nuestro texto a
tres campos ordenados y limpios.

Con el nuevo API de Scanner, podríamos dar un paso más y analizar los números de nuestro segundo
ejemplo a medida que los extraemos:

Aquí, hemos indicado al Scanner que utilice nuestra expresión regular como delimitador y luego lo
llamamos repetidamente para analizar cada campo como su tipo correspondiente. El Scanner es
conveniente porque puede leer no solo desde cadenas, sino directamente desde fuentes de flujo (más en
el Capítulo 11), como InputStreams, Files y Channels:

Otra cosa que puedes hacer con el Scanner es anticiparte con los métodos "hasNext" para ver si viene
otro elemento:

StringTokenizer

A pesar de que la clase StringTokenizer que mencionamos ahora es un elemento heredado, es bueno
saber que está ahí porque ha existido desde los inicios de Java y se utiliza en una gran cantidad de código.
StringTokenizer te permite especificar un delimitador como un conjunto de caracteres y coincide con
cualquier número o combinación de esos caracteres como delimitador entre tokens. El siguiente
fragmento lee las palabras de nuestro primer ejemplo:

Invocamos los métodos hasMoreTokens() y nextToken() para iterar sobre las palabras del texto. Por
defecto, la clase StringTokenizer utiliza los caracteres estándar de espaciado: retorno de carro, nueva línea
y tabulación, como delimitadores. También puedes especificar tu propio conjunto de caracteres
delimitadores en el constructor de StringTokenizer. Cualquier combinación contigua de los caracteres
especificados que aparezca en la cadena objetivo es omitida entre tokens:

El StringTokenizer no es tan limpio como nuestro ejemplo con expresiones regulares. Aquí usamos una
coma como delimitador, por lo que obtenemos espacios en blanco adicionales al inicio de nuestro campo
de descripción. Si hubiéramos agregado un espacio a nuestra cadena delimitadora, StringTokenizer habría
dividido nuestra descripción en dos palabras, "Java" y "Programming", lo cual no es lo que queríamos.
Una solución sería usar trim() para eliminar los espacios en blanco al inicio y al final de cada elemento.

Expresiones Regulares

Ahora es hora de hacer un breve desvío en nuestro recorrido por Java y adentrarnos en el mundo de las
expresiones regulares. Una expresión regular, o regex para abreviar, describe un patrón de texto. Las
expresiones regulares se utilizan con muchas herramientas, incluyendo el paquete java.util.regex,
editores de texto y muchos lenguajes de secuencias de comandos, para proporcionar capacidades
sofisticadas de búsqueda de texto y manipulación poderosa de cadenas.

Si ya estás familiarizado con el concepto de expresiones regulares y cómo se usan con otros lenguajes,
puedes echar un vistazo rápido a esta sección. Al menos, tendrás que revisar "La API java.util.regex" en la
página 238 más adelante en este capítulo, que cubre las clases de Java necesarias para usarlas. Por otro
lado, si has llegado hasta este punto en tu viaje por Java sin conocimiento previo sobre este tema y te
preguntas exactamente qué son las expresiones regulares, entonces abre tu bebida favorita y prepárate.
Estás a punto de aprender sobre la herramienta más poderosa en el arsenal de manipulación de texto,
que de hecho es un pequeño lenguaje dentro de un lenguaje, todo en el transcurso de algunas páginas.

Notación de Regex

Una expresión regular describe un patrón en el texto. Por patrón, nos referimos a casi cualquier
característica que puedas imaginar identificando en el texto solo a través de los caracteres literales, sin
comprender realmente su significado. Esto incluye características como palabras, agrupaciones de
palabras, líneas y párrafos, puntuación, mayúsculas y minúsculas, y más generalmente, cadenas y
números con una estructura específica, como números de teléfono, direcciones de correo electrónico y
frases entre comillas. Con expresiones regulares, puedes buscar en el diccionario todas las palabras que
tienen la letra "q" sin su compañero "u" a su lado, o palabras que comienzan y terminan con la misma
letra. Una vez que has construido un patrón, puedes usar herramientas simples para buscarlo en el texto
o determinar si una cadena dada coincide con él. Además, una expresión regular puede ser configurada
para ayudarte a desmembrar partes específicas del texto que coincidió, las cuales podrías utilizar como
elementos de texto de reemplazo si lo deseas.

Escribe una vez, huye

Antes de continuar, deberíamos decir algunas palabras sobre la sintaxis de las expresiones regulares en
general. Al principio de esta sección, mencionamos casualmente que estaríamos discutiendo un nuevo
lenguaje. Las expresiones regulares, de hecho, constituyen una forma simple de lenguaje de
programación. Si piensas por un momento en los ejemplos que mencionamos anteriormente, puedes ver
que se necesitará algo parecido a un lenguaje para describir incluso patrones simples, como direcciones
de correo electrónico, que tienen cierta variación en su forma.

Un libro de ciencias de la computación clasificaría a las expresiones regulares en la parte inferior de la


jerarquía de lenguajes de computación, tanto en términos de lo que pueden describir como en lo que
puedes hacer con ellas. Sin embargo, siguen siendo capaces de ser bastante sofisticadas. Como ocurre
con la mayoría de los lenguajes de programación, los elementos de las expresiones regulares son simples,
pero se pueden combinar para obtener una complejidad arbitraria. Y ahí es donde comienzan a
complicarse las cosas.

Dado que las expresiones regulares funcionan en cadenas, es conveniente tener una notación muy
compacta que se pueda insertar fácilmente entre caracteres. Pero la notación compacta puede ser muy
críptica y la experiencia demuestra que es mucho más fácil escribir una declaración compleja que volver
a leerla más tarde. Tal es la maldición de la expresión regular. Puedes descubrir que en un momento de
inspiración nocturna, alimentado por la cafeína, puedes escribir un patrón glorioso para simplificar el resto
de tu programa en una sola línea. Sin embargo, cuando regresas a leer esa línea al día siguiente, puede
parecerte como jeroglíficos egipcios. Por lo general, lo más sencillo es mejor, pero si puedes dividir tu
problema y hacerlo de manera más clara en varios pasos, tal vez debas hacerlo.

Caracteres escapados

Ahora que estás debidamente advertido, tenemos que lanzarte una cosa más antes de reconstruirte. No
solo la notación regex puede ser un poco complicada, sino que también es algo ambigua con las cadenas
de Java. Una parte importante de la notación son los caracteres escapados, es decir, un carácter con una
barra invertida delante. Por ejemplo, el carácter escapado d, \d (barra invertida "d"), es una abreviatura
que coincide con cualquier carácter de dígito individual (0-9). Sin embargo, no puedes simplemente
escribir \d como parte de una cadena de Java, porque quizás recuerdes que Java usa la barra invertida
para sus propios caracteres especiales y para especificar secuencias de caracteres Unicode (\uxxxx).
Afortunadamente, Java nos da una solución: una barra invertida escapada, que son dos barras invertidas
(\\), significa una barra invertida literal.

La regla es: cuando quieres que aparezca una barra invertida en tu expresión regular, debes escaparla con
otra barra invertida adicional:

Y solo para volver las cosas aún más locas, debido a que la notación de las expresiones regulares en sí
misma utiliza una barra invertida para denotar caracteres especiales, debe proporcionar la misma "vía de
escape" también, permitiéndote duplicar las barras invertidas si deseas un literal de barra invertida.
Entonces, si deseas especificar una expresión regular que incluya una sola barra invertida literal, se vería
así:
La mayoría de los caracteres operadores "mágicos" de los que se lee en esta sección operan en el carácter
que los precede, por lo que también deben ser escapados si quieres su significado literal. Esto incluye
caracteres como ., *, +, llaves {}, y paréntesis ().

Si necesitas crear una parte de una expresión que tenga muchos caracteres literales, puedes usar los
delimitadores especiales \Q y \E para ayudarte. Cualquier texto que aparezca entre \Q y \E se escapa
automáticamente. (Aún necesitas los escapes de cadena de Java: doble barra invertida para barra
invertida, pero no cuádruple). También hay un método estático llamado Pattern.quote(), que hace lo
mismo, devolviendo una versión correctamente escapada de cualquier cadena que le pases.

Más allá de eso, nuestra única sugerencia para ayudar a mantener tu cordura cuando trabajas con estos
ejemplos es mantener dos copias: una línea de comentario mostrando la expresión regular sin
modificaciones, y la cadena de Java real, donde debes duplicar todas las barras invertidas. ¡Y no te olvides
de jshell! Puede ser un patio de juegos muy poderoso para probar y ajustar tus patrones.

Caracteres y clases de caracteres

Ahora, sumerjámonos en la sintaxis real de las expresiones regulares. La forma más simple de una
expresión regular es texto literal, que no tiene un significado especial y se empareja directamente
(carácter por carácter) en la entrada. Esto puede ser un solo carácter o más. Por ejemplo, en la siguiente
cadena, el patrón "s" puede coincidir con el carácter s en las palabras "rose" y "is":

El patrón "rose" solo puede coincidir con la palabra literal "rose". Pero esto no es muy interesante. Vamos
a subir un poco el nivel introduciendo algunos caracteres especiales y la noción de "clases de caracteres".

Cualquier carácter: punto (.)

El carácter especial punto (.) coincide con cualquier carácter individual. El patrón ".ose" coincide
con "rose", "nose", "_ose" (espacio seguido de ose), o cualquier otro carácter seguido por la
secuencia "ose". Dos puntos coinciden con cualquier par de caracteres (como "prose", "close",
etc.). El operador punto no discrimina; normalmente se detiene solo para un carácter de fin de
línea (y, opcionalmente, puedes indicarle que no lo haga; discutiremos eso más adelante).

Podemos considerar que "." representa el grupo o clase de todos los caracteres. Y las expresiones
regulares definen clases de caracteres más interesantes también.

Carácter de espacio en blanco o no espacio en blanco: \s, \S

El carácter especial \s coincide con un espacio en blanco literal o con uno de los siguientes
caracteres: \t (tabulación), \r (retorno de carro), \n (nueva línea), \f (avance de formulario) y
retroceso. El carácter especial correspondiente \S hace lo contrario, coincidiendo con cualquier
carácter excepto espacio en blanco.

Dígito o carácter que no es un dígito: \d, \D

\d coincide con cualquiera de los dígitos del 0 al 9. \D hace lo contrario, coincidiendo con todos
los caracteres excepto dígitos.
Carácter de palabra o no carácter de palabra: \w, \W

\w coincide con un carácter de "palabra", incluyendo letras mayúsculas y minúsculas de la A a la


Z, los dígitos del 0 al 9 y el guión bajo (_). \W coincide con todo excepto esos caracteres.

Clases de caracteres personalizadas

Puedes definir tus propias clases de caracteres utilizando la notación […]. Por ejemplo, la siguiente clase
coincide con cualquiera de los caracteres a, b, c, x, y o z:

La notación especial x-y de rango se puede utilizar como una abreviatura para los caracteres
alfanuméricos. El siguiente ejemplo define una clase de caracteres que contiene todas las letras
mayúsculas y minúsculas:

Colocar un acento circunflejo (^) como el primer carácter dentro de los corchetes invierte la clase de
caracteres. Este ejemplo coincide con cualquier carácter excepto las letras mayúsculas de la A a la F:

La anidación de clases de caracteres simplemente las agrega:

La notación && de AND lógico se puede utilizar para tomar la intersección (caracteres en común):

El patrón "[Aa] rose" (incluyendo una letra A mayúscula o minúscula) coincide tres veces en la siguiente
frase:

Los caracteres de posición te permiten designar la ubicación relativa de una coincidencia. Los más
importantes son ^ y $, que coinciden con el inicio y el final de una línea, respectivamente:

Para ser un poco más precisos, ^ y $ coinciden con el inicio y el final de la "entrada", que a menudo es una
sola línea. Si estás trabajando con múltiples líneas de texto y deseas coincidir con el inicio y el final de
líneas dentro de una sola cadena grande, puedes activar el modo "multiline" con una bandera, como se
describe más adelante en "Opciones especiales" en la página 237.

Los marcadores de posición \b y \B coinciden con un límite de palabra o un límite que no es de palabra,
respectivamente. Por ejemplo, el siguiente patrón coincide con "rose" y "rosemary", pero no con
"primrose":
Iteración (multiplicidad)

Simplemente coincidir con patrones de caracteres fijos no nos llevaría muy lejos. A continuación, veremos
operadores que cuentan el número de ocurrencias de un carácter (o más generalmente, de un patrón,
como veremos en "Pattern" en la página 239):

Cualquier (cero o más iteraciones): asterisco (*)

Colocar un asterisco (*) después de un carácter o clase de caracteres significa "permitir cualquier
número de ese tipo de carácter", en otras palabras, cero o más. Por ejemplo, el siguiente patrón
coincide con un dígito con cualquier cantidad de ceros iniciales (posiblemente ninguno):

Algunos (una o más iteraciones): signo más (+)

El signo más (+) significa "una o más" iteraciones y es equivalente a XX* (patrón seguido por
asterisco de patrón). Por ejemplo, el siguiente patrón coincide con un número con uno o más
dígitos, más ceros iniciales opcionales:

Puede parecer redundante coincidir con los ceros al principio de una expresión porque el cero es un dígito
y, por lo tanto, es coincidido por la parte \d+ de la expresión de todos modos. Sin embargo, más adelante
mostraremos cómo puedes desglosar la cadena usando una expresión regular y acceder solo a las piezas
que deseas. En este caso, podrías querer eliminar los ceros iniciales y mantener solo los dígitos.

Opcional (cero o una iteración): signo de interrogación (?)

El operador de signo de interrogación (?) permite exactamente cero o una iteración. Por ejemplo,
el siguiente patrón coincide con una fecha de vencimiento de tarjeta de crédito, que puede tener
o no una barra en el medio:

Rango (entre x e y iteraciones, inclusivas): {x,y}

El operador de rango con llaves {x,y} es el operador de iteración más general. Especifica un rango
preciso para coincidir. Un rango toma dos argumentos: un límite inferior y un límite superior,
separados por una coma. Esta expresión regular coincide con cualquier palabra que tenga entre
cinco y siete caracteres, ambos inclusive:

Al menos x o más iteraciones (y es infinito): {x,}

Si omites el límite superior, dejando simplemente una coma en el rango, el límite superior se
vuelve infinito. Esta es una forma de especificar un mínimo de ocurrencias sin un máximo.

Alternancia

El operador de barra vertical (|) denota la operación lógica O, también llamada alternancia o elección. El
operador | no opera en caracteres individuales, sino que se aplica a todo lo que está a cada lado de él.
Divide la expresión en dos a menos que esté limitado por el agrupamiento de paréntesis. Por ejemplo, un
enfoque ligeramente ingenuo para analizar fechas podría ser el siguiente:
En esta expresión, el lado izquierdo coincide con patrones como Fri, Oct 12, 2001, y el lado derecho
coincide con 10/12/2001.

La siguiente expresión regular podría usarse para coincidir con direcciones de correo electrónico con uno
de tres dominios (net, edu y gov):

Opciones Especiales

Hay varias opciones especiales que afectan la forma en que el motor de regex realiza sus coincidencias.
Estas opciones se pueden aplicar de dos maneras:

- Puedes pasar uno o más indicadores durante el paso de Pattern.compile() (discutido en la siguiente
sección).

- Puedes incluir un bloque especial de código en tu regex.

Mostraremos aquí el último enfoque. Para hacer esto, incluye uno o más indicadores en un bloque
especial (?x), donde x es el indicador para la opción que queremos activar. Por lo general, se hace al
principio del regex. También puedes desactivar indicadores agregando un signo menos (?-x), lo que te
permite aplicar indicadores a partes selectas de tu patrón.

Los siguientes indicadores están disponibles:

Insensible a mayúsculas y minúsculas: (?i)

El indicador (?i) le dice al motor de regex que ignore las mayúsculas y minúsculas al hacer
coincidencias. Por ejemplo:

Dot todo: (?s)

El indicador (?s) activa el modo "dot todo", lo que permite que el carácter punto coincida con
cualquier cosa, incluidos los caracteres de fin de línea. Es útil si estás buscando patrones que
abarcan varias líneas. La "s" significa "modo de una sola línea", un nombre algo confuso derivado
de Perl.

Multilínea: (?m)

Por defecto, ^ y $ realmente no coinciden con el principio y el final de las líneas (según las
combinaciones de retorno de carro o nueva línea); en su lugar, coinciden con el principio o el
final de todo el texto de entrada. En muchos casos, "una línea" es sinónimo de toda la entrada.
Si tienes un bloque grande de texto para procesar, a menudo dividirás ese bloque en líneas
separadas por otras razones, y luego comprobar cualquier línea dada para una expresión regular
es sencillo y ^ y $ se comportan como se espera. Sin embargo, si deseas utilizar una expresión
regular con toda la cadena de entrada que contiene múltiples líneas (separadas por esas
combinaciones de retorno de carro o nueva línea), puedes activar el modo multilínea con (?m).
Este indicador hace que ^ y $ coincidan con el principio y el final de las líneas individuales dentro
del bloque de texto, así como con el principio y el final de todo el bloque. Específicamente, esto
significa el lugar antes del primer carácter, el lugar después del último carácter y los lugares justo
antes y después de los terminadores de línea dentro de la cadena.
Líneas Unix: (?d)

El indicador (?d) limita la definición del terminador de línea para los caracteres especiales ^, $ y
. a solo una nueva línea de estilo Unix (\n). Por defecto, también se permite el retorno de carro
nueva línea (\r\n).

La API java.util.regex

Ahora que hemos cubierto la teoría sobre cómo construir expresiones regulares, la parte difícil ha
terminado. Todo lo que queda es investigar la API de Java para aplicar estas expresiones.

Patrón

Como hemos dicho, los patrones de regex que escribimos como cadenas son, en realidad, pequeños
programas que describen cómo coincidir con el texto. En tiempo de ejecución, el paquete de regex de
Java compila estos pequeños programas a una forma que puede ejecutar contra algún texto objetivo.
Varios métodos simples de conveniencia aceptan cadenas directamente para usar como patrones. Más
generalmente, sin embargo, Java te permite compilar explícitamente tu patrón y encapsularlo en una
instancia de un objeto Pattern. Esta es la forma más eficiente de manejar patrones que se utilizan más de
una vez, porque elimina la recompilación innecesaria de la cadena. Para compilar un patrón, usamos el
método estático Pattern.compile():

Una vez que tienes un Patrón, puedes pedirle que cree un objeto Matcher, que asocia el patrón con una
cadena objetivo:

El Matcher ejecuta las coincidencias. Hablaremos de eso a continuación. Pero antes de hacerlo,
mencionaremos un método de conveniencia de Pattern. El método estático Pattern.matches()
simplemente toma dos cadenas, una expresión regular y una cadena objetivo, y determina si la cadena
objetivo coincide con la expresión regular. Esto es muy conveniente si quieres hacer una prueba rápida
en tu aplicación. Por ejemplo:

Esta línea de código puede probar si la cadena myText contiene un número en punto flotante de estilo
Java, como "42.0f." Ten en cuenta que la cadena debe coincidir completamente para considerarse una
coincidencia. Si deseas ver si un patrón pequeño está contenido dentro de una cadena más grande pero
no te importa el resto de la cadena, debes usar un Matcher como se describe en "El Matcher" en la página
241.

Intentemos otro patrón (simplificado) que podríamos usar en nuestro juego una vez que comencemos a
permitir que varios jugadores compitan entre sí. Muchos sistemas de inicio de sesión utilizan direcciones
de correo electrónico como identificador de usuario. Tales sistemas no son perfectos, por supuesto, pero
una dirección de correo electrónico funcionará muy bien para nuestras necesidades. Nos gustaría invitar
al usuario a ingresar su dirección de correo electrónico, pero queremos asegurarnos de que parezca válida
antes de usarla. Una expresión regular puede ser una forma rápida de realizar dicha validación.

Al igual que escribir algoritmos para resolver problemas de programación, diseñar una expresión regular
requiere que descompongas tu problema de coincidencia de patrones en piezas pequeñas. Si pensamos
en direcciones de correo electrónico, hay algunos patrones que destacan de inmediato. El más obvio es
el símbolo @ en el medio de cada dirección. Un patrón ingenuo (¡pero mejor que nada!) que depende de
ese hecho podría construirse así:
Pero ese patrón es demasiado permisivo. Ciertamente reconocerá direcciones de correo electrónico
válidas, pero también reconocerá muchas direcciones inválidas como "bad.address@" o "@also.bad" e
incluso "@@". (¡Prueba estas en un jshell y tal vez inventa algunos ejemplos más incorrectos por tu
cuenta!) ¿Cómo podemos lograr coincidencias mejores? Un ajuste rápido sería usar el modificador + en
lugar del *. El patrón actualizado ahora requiere al menos un carácter en cada lado del @. Pero sabemos
algunas otras cosas sobre las direcciones de correo electrónico. Por ejemplo, la "mitad" izquierda de la
dirección (la parte del nombre) no puede contener el carácter @. Por ese motivo, tampoco puede la parte
del dominio. Podemos usar una clase de caracteres personalizada para esta próxima mejora:

Este patrón es mejor, pero aún permite varias direcciones inválidas, como "still@bad", ya que los nombres
de dominio tienen al menos un nombre seguido de un punto (.) seguido de un dominio de nivel superior
(TLD, por sus siglas en inglés) como "oreilly.com". Entonces, tal vez un patrón como este:

Ese patrón resuelve nuestro problema con una dirección como "still@bad", pero hemos ido un poco
demasiado lejos en la otra dirección. Hay muchos, muchos TLD (Dominios de Nivel Superior), demasiados
como para listar razonablemente, incluso si ignoramos el problema de mantener esa lista a medida que
se agregan nuevos TLD. Por lo tanto, retrocedamos un poco. Mantendremos el "punto" en la parte del
dominio, pero eliminaremos el TLD específico y simplemente aceptaremos una secuencia simple de letras:

Mucho mejor. Podemos agregar un último ajuste para asegurarnos de que no nos preocupemos por el
caso de la dirección, ya que todas las direcciones de correo electrónico no distinguen entre mayúsculas y
minúsculas. Solo agreguemos una bandera:

Una vez más, este validador de correo electrónico no es perfecto, pero definitivamente es un buen
comienzo y será suficiente para nuestro simple sistema de inicio de sesión una vez que agreguemos la
funcionalidad de red. Si deseas experimentar con el patrón de validación y expandirlo o mejorarlo,
recuerda que puedes "reutilizar" líneas en jshell con las teclas de flecha del teclado. Usa la flecha hacia
arriba para recuperar la línea anterior. De hecho, puedes usar la flecha hacia arriba y la flecha hacia abajo
para navegar por todas tus líneas recientes. Dentro de una línea, usa las teclas de flecha izquierda y
derecha para moverte y eliminar/agregar/editar tu comando. Luego, simplemente presiona la tecla Enter
para ejecutar el comando recién modificado; no es necesario mover el cursor al final de la línea antes de
presionar Enter.
En los ejemplos anteriores, solo escribimos la línea completa Pattern.matches(…) una vez. Después de
eso, fue simplemente presionar la flecha hacia arriba, editar y luego Enter para las siguientes cinco líneas.
¿Puedes ver por qué falló la prueba final de coincidencia?

El Matcher

Un Matcher asocia un patrón con una cadena y proporciona herramientas para probar, encontrar e iterar
sobre coincidencias del patrón en ella. El Matcher es "mutable". Por ejemplo, el método find() intenta
encontrar la siguiente coincidencia cada vez que se llama. Pero puedes limpiar el Matcher y comenzar de
nuevo llamando a su método reset().

Si solo estás interesado en "una gran coincidencia" —es decir, esperas que tu cadena coincida o no con el
patrón— puedes usar matches() o lookingAt(). Estos corresponden aproximadamente a los métodos
equals() y startsWith() de la clase String. El método matches() pregunta si la cadena coincide con el patrón
en su totalidad (sin caracteres de cadena sobrantes) y devuelve verdadero o falso. El método lookingAt()
hace lo mismo, excepto que pregunta solo si la cadena comienza con el patrón y no le importa si el patrón
utiliza todos los caracteres de la cadena.

Más generalmente, querrás poder buscar en la cadena y encontrar una o más coincidencias. Para hacer
esto, puedes usar el método find(). Cada llamada a find() devuelve verdadero o falso para la próxima
coincidencia del patrón y toma nota internamente de la posición del texto coincidente. Puedes obtener
las posiciones de inicio y finalización de caracteres con los métodos start() y end() del Matcher, o
simplemente puedes recuperar el texto coincidente con el método group(). Por ejemplo:

El fragmento anterior imprime la ubicación de inicio de las palabras "horse" y "course" (en total cuatro
veces):

El método para recuperar el texto coincidente se llama group() porque hace referencia al grupo de captura
cero (la coincidencia completa). También puedes recuperar el texto de otros grupos de captura
numerados al proporcionarle al método group() un argumento entero. Puedes determinar cuántos grupos
de captura tienes con el método groupCount():

Dividir y tokenizar cadenas

Una necesidad muy común es analizar una cadena en un conjunto de campos basados en algún
delimitador, como una coma. Es un problema tan común que la clase String contiene un método para
hacer precisamente esto. El método split() acepta una expresión regular y devuelve un array de
subcadenas divididas alrededor de ese patrón. Considera la siguiente cadena y las llamadas a split():
El primer split() devuelve un array de Strings, pero el uso ingenuo de "," para separar la cadena significa
que el espacio en nuestra variable de texto permanece unido a los caracteres más interesantes.
Obtenemos "Foo" como una única palabra como se esperaba, pero luego obtenemos "bar<espacio>" y
finalmente "<espacio><espacio><espacio>blah". ¡Vaya! El segundo split() también produce un array de
Strings, pero esta vez contiene lo esperado: "Foo", "bar" (sin espacio al final) y "blah" (sin espacios al
inicio).

Si planeas utilizar una operación como esta más de unas pocas veces en tu código, probablemente
deberías compilar el patrón y usar su método split(), que es idéntico a la versión en String. El método
split() de String es equivalente a:

Como mencionamos antes, hay mucho que aprender sobre expresiones regulares más allá de las
capacidades específicas de regex proporcionadas por Java. Revisa el uso de jshell ("Pattern" en la página
239) para experimentar con expresiones y divisiones. Definitivamente, este es un tema que se beneficia
de la práctica.

Utilidades matemáticas

Java soporta aritmética de enteros y de punto flotante directamente en el lenguaje. Operaciones


matemáticas de nivel superior son compatibles a través de la clase java.lang.Math. Como probablemente
hayas visto hasta ahora, las clases de envoltura (wrapper classes) para tipos de datos primitivos te
permiten tratarlos como objetos. Las clases de envoltura también contienen algunos métodos para
conversiones básicas.

Primero, algunas palabras sobre la aritmética incorporada en Java. Java maneja errores en la aritmética
de enteros lanzando una ArithmeticException:

Para generar el error en este ejemplo, creamos la variable intermedia 'zero'. El compilador es algo astuto
y nos habría detectado si hubiéramos intentado realizar divisiones por un literal cero de manera flagrante.

Las expresiones de aritmética de punto flotante, por otro lado, no arrojan excepciones. En su lugar,
adoptan los valores especiales fuera de rango mostrados en la Tabla 8-2.

El siguiente ejemplo genera un resultado infinito:


El valor especial NaN (no es un número) indica el resultado de dividir cero entre cero. Este valor tiene la
distinción matemática especial de no ser igual a sí mismo (NaN != NaN se evalúa como verdadero). Utiliza
Float.isNaN() o Double.isNaN() para comprobar si es NaN.

La Clase java.lang.Math

La clase java.lang.Math es la biblioteca matemática de Java. Contiene una serie de métodos estáticos que
cubren todas las operaciones matemáticas habituales como sin(), cos() y sqrt(). La clase Math no es muy
orientada a objetos (no puedes crear una instancia de Math). En su lugar, es más bien un contenedor
conveniente para métodos estáticos que son más como funciones globales.

Como vimos en el Capítulo 5, es posible utilizar la funcionalidad de importación estática para importar los
nombres de los métodos estáticos y constantes directamente al ámbito de nuestra clase y utilizarlos por
sus nombres simples y sin calificación.

La Tabla 8-3 resume los métodos en java.lang.Math.

Las funciones log(), pow() y sqrt() pueden arrojar una ArithmeticException en tiempo de ejecución. abs(),
max() y min() están sobrecargadas para todos los valores escalares: int, long, float o double, y devuelven
el tipo correspondiente. Las versiones de Math.round() aceptan tanto float como double y devuelven int
o long, respectivamente. El resto de los métodos operan con valores double y también devuelven valores
double:

Y solo para resaltar la conveniencia de esa opción de importación estática, podemos probar estas
funciones simples en jshell:

Math también contiene los valores estáticos finales double E y PI:

Matemáticas en acción

Ya hemos mencionado el uso de la clase Math y sus métodos estáticos en "Accediendo a campos y
métodos" en la página 127. Podemos utilizarla nuevamente para hacer nuestro juego un poco más
divertido al aleatorizar dónde aparecen los árboles. El método Math.random() devuelve un número
double aleatorio mayor o igual a 0 y menor que 1. Agregando un poco de aritmética y redondeo o
truncamiento, puedes utilizar ese valor para crear números aleatorios en cualquier rango que necesites.
En particular, convertir este valor en un rango deseado sigue esta fórmula:

¡Inténtalo! Intenta generar un número aleatorio de cuatro dígitos en jshell. Podrías establecer el mínimo
en 1000 y el máximo en 10000, así:

Para ubicar nuestros árboles, necesitaremos dos números aleatorios para las coordenadas x e y. Podemos
establecer un rango que mantenga los árboles en la pantalla al considerar un margen alrededor de los
bordes. Para la coordenada x, una forma de hacerlo podría ser así:
Configura un método similar para encontrar un valor de y y comenzarás a ver algo similar a la imagen
mostrada en la Figura 8-1. Incluso podrías mejorar y utilizar el método isTouching() que discutimos en el
Capítulo 5 para evitar colocar árboles en contacto directo con nuestro físico. Aquí está nuestro bucle de
configuración de árboles mejorado:

Intenta cerrar el juego y volver a iniciarlo. Deberías ver los árboles en lugares diferentes cada vez que
ejecutes la aplicación.
Números grandes/precisos

Si los tipos long y double no son lo suficientemente grandes o precisos para ti, el paquete java.math
proporciona dos clases, BigInteger y BigDecimal, que admiten números de precisión arbitraria. Estas clases
completas tienen una gran cantidad de métodos para realizar cálculos matemáticos de precisión arbitraria
y controlar con precisión el redondeo de los restos. En el siguiente ejemplo, usamos BigDecimal para
sumar dos números muy grandes y luego crear una fracción con un resultado de 100 dígitos:

Si implementas algoritmos criptográficos o científicos por diversión, BigInteger es crucial. BigDecimal, a


su vez, se puede encontrar en aplicaciones que manejan datos de moneda y financieros. Aparte de eso,
es poco probable que necesites estas clases.

Fechas y Horas

Trabajar con fechas y horas sin las herramientas adecuadas puede ser una tarea tediosa. Antes de Java 8,
tenías acceso a tres clases que manejaban la mayor parte del trabajo por ti. La clase java.util.Date
encapsula un punto en el tiempo. La clase java.util.GregorianCalendar, que extiende la clase abstracta
java.util.Calendar, traduce entre un punto en el tiempo y campos de calendario como mes, día y año.
Finalmente, la clase java.text.DateFormat sabe cómo generar y analizar representaciones de cadenas de
fechas y horas en muchos idiomas.

Si bien las clases Date y Calendar cubrían muchos casos de uso, carecían de granularidad y les faltaban
otras características. Esto provocó la creación de varias bibliotecas de terceros, todas destinadas a facilitar
que los desarrolladores trabajen con fechas, horas y duraciones de tiempo. Java 8 proporcionó mejoras
muy necesarias en esta área con la adición del paquete java.time. Exploraremos este nuevo paquete, pero
aún encontrarás muchos ejemplos de Date y Calendar en la práctica, por lo que es útil saber que existen.
Como siempre, la documentación en línea es una fuente invaluable para revisar partes de la API de Java
que no abordamos aquí.

Fechas y Horas Locales

La clase java.time.LocalDate representa una fecha sin información de hora para tu región local. Piensa en
una festividad como el 4 de mayo de 2019. De manera similar, java.time.LocalTime representa una hora
sin información de fecha. Quizás tu despertador suene a las 7:15 todas las mañanas. La
java.time.LocalDateTime almacena tanto valores de fecha como de hora para cosas como citas con tu
oftalmólogo para que puedas seguir leyendo libros sobre Java. Todas estas clases ofrecen métodos
estáticos para crear nuevas instancias utilizando valores numéricos apropiados con `of()` o analizando
cadenas con `parse()`. Vamos a entrar en jshell y tratar de crear algunos ejemplos.
Otro gran método estático para crear estos objetos es `now()`, el cual proporciona la fecha, la hora o la
fecha y hora actual, como podrías esperar:

¡Genial! Después de importar el paquete java.time, podemos crear instancias de cada una de las clases
Local... para momentos específicos o para "en este momento". Es posible que hayas notado que los
objetos creados con `now()` incluyen segundos y nanosegundos. Puedes proporcionar esos valores a los
métodos `of()` y `parse()` si los necesitas o los deseas. No hay mucha emoción allí, pero una vez que tienes
estos objetos, puedes hacer mucho con ellos. ¡Sigue leyendo!

Comparar y Manipular Fechas y Horas

Una de las grandes ventajas de usar las clases java.time es el conjunto consistente de métodos que tienes
disponibles para comparar y cambiar fechas y horas. Por ejemplo, muchas aplicaciones de chat te
mostrarán "hace cuánto tiempo" se envió un mensaje. El subpaquete java.time.temporal tiene justo lo
que necesitamos: la interfaz ChronoUnit. Contiene varias unidades de fecha y hora, como MESES, DÍAS,
HORAS, MINUTOS, etc. Estas unidades se pueden utilizar para calcular diferencias. Por ejemplo,
podríamos calcular cuánto tiempo nos lleva crear dos ejemplos de fechas y horas en jshell usando el
método `between()`:
Un chequeo visual rápido muestra que de hecho tomó alrededor de 11 segundos escribir la línea que creó
nuestra segunda variable. Deberías revisar la documentación de ChronoUnit para obtener una lista
completa de las unidades disponibles, pero obtienes el rango completo desde nanosegundos hasta
milenios.

Estas unidades también pueden ayudarte a manipular fechas y horas con los métodos `plus()` y `minus()`.
Para establecer un recordatorio para dentro de una semana a partir de hoy, por ejemplo:

¡Genial! Pero este ejemplo del recordatorio plantea otra manipulación que quizás necesites realizar de
vez en cuando. Puede que desees el recordatorio en un momento específico del día 19. Puedes convertir
entre fechas o horas y fechas y horas fácilmente con los métodos `atDate()` o `atTime()`:

¡Ahora obtendremos ese recordatorio a las 9 A.M.! Sin embargo, ¿qué sucedería si configuramos ese
recordatorio en Atlanta y luego volamos a San Francisco? ¿Cuándo sonaría la alarma? LocalDateTime es,
bueno, ¡local! Entonces, la parte T09:00 sigue siendo las 9 A.M. donde sea que estemos cuando
ejecutamos el programa. Pero si estamos manejando algo como un calendario compartido y programando
una reunión, no podemos ignorar los diferentes husos horarios involucrados. Afortunadamente, el
paquete java.time ha pensado en eso también.

Zonas horarias

Los autores del nuevo paquete java.time ciertamente te animan a usar las variaciones locales de las clases
de tiempo y fecha siempre que sea posible. Agregar soporte para zonas horarias significa agregar
complejidad a tu aplicación; quieren que evites esa complejidad si es posible. Pero hay muchos escenarios
donde el soporte para zonas horarias es inevitable.

Puedes trabajar con fechas y horas "zonificadas" utilizando las clases ZonedDateTime y OffsetDateTime.
La variante zonificada comprende zonas horarias con nombres y cosas como ajustes por horario de
verano. La variante de compensación es un desplazamiento numérico constante y simple desde
UTC/Greenwich.

La mayoría de los usos orientados al usuario de fechas y horas usarán el enfoque de zona nombrada, así
que veamos cómo crear un fecha y hora zonificada. Para adjuntar una zona, usamos la clase ZoneId, que
tiene el método estático común `of()` para crear nuevas instancias. Puedes proporcionar una zona de
región como una cadena para obtener tu valor zonificado:

Y ahora puedes hacer cosas como asegurarte de que tus amigos en París puedan unirse a ti en el momento
correcto usando el método verboso pero acertadamente nombrado `withZoneSameInstant()`:
Si tienes otros amigos que no están convenientemente ubicados en una gran área metropolitana pero
quieres que también se unan, puedes usar el método `systemDefault()` de ZoneId para obtener su zona
horaria programáticamente:

En nuestro caso, jshell se ejecutaba en una computadora portátil en la zona horaria estándar del Este (no
durante el período de horario de verano) de los Estados Unidos, y piOther resulta exactamente como
esperado. El identificador de zona `systemDefault()` es una forma muy útil de adaptar rápidamente fechas
y horas de alguna otra zona para que coincidan con lo que probablemente marcaría el reloj y el calendario
de tus usuarios. En aplicaciones comerciales, es posible que desees permitir que el usuario te indique su
zona preferida, pero `systemDefault()` suele ser una buena suposición.

Análisis y Formateo de Fechas y Horas

Para crear y mostrar nuestras fechas y horas locales y zonificadas utilizando cadenas, hemos estado
confiando en los formatos predeterminados que siguen los valores ISO y generalmente funcionan donde
necesitamos aceptar o mostrar fechas y horas. Pero como todo programador sabe, "generalmente" no es
"siempre". Afortunadamente, puedes utilizar la clase de utilidad java.time.format.DateTimeFormatter
para ayudar tanto en el análisis de entrada como en el formateo de salida.

El núcleo de DateTimeFormatter se centra en construir una cadena de formato que gobierna tanto el
análisis como el formateo. Construyes tu formato con las piezas enumeradas en la Tabla 8-4. Aquí solo
estamos enumerando una parte de las opciones disponibles, pero estas deberían ser suficientes para la
mayoría de las fechas y horas que encontrarás. ¡Ten en cuenta que las mayúsculas y minúsculas importan
al usar los caracteres mencionados!

Para crear un formato corto común en Estados Unidos, por ejemplo, podrías utilizar los caracteres M, d y
y. Construyes el formateador utilizando el método estático `ofPattern()`. Ahora, el formateador puede ser
utilizado (y reutilizado) con el método `parse()` de cualquiera de las clases de fecha u hora:
Y como mencionamos anteriormente, el formateador funciona en ambas direcciones. Simplemente utiliza
el método `format()` de tu formateador para producir una representación en forma de cadena de tu fecha
u hora:

¡Por supuesto, los formateadores también funcionan para horas y fechas-horas!

Observa en la parte de ZonedDateTime anterior que colocamos el identificador de zona horaria (el
carácter 'z') al final, ¡probablemente no donde esperabas encontrarlo! Queríamos ilustrar el poder de
estos formatos. Puedes diseñar un formato para acomodar una amplia gama de estilos de entrada o
salida. Datos heredados y formularios web mal diseñados vienen a la mente como ejemplos directos de
dónde DateTimeFormatter puede ayudarte a mantener tu cordura.

Errores de análisis

Incluso con todo este poder de análisis al alcance de tu mano, a veces las cosas saldrán mal. Y
lamentablemente, las excepciones que ves a menudo son demasiado vagas para ser inmediatamente
útiles. Considera el siguiente intento de analizar una hora con horas, minutos y segundos:

¡Ay! Se lanzará un DateTimeParseException cada vez que la entrada de cadena no se pueda analizar.
También se lanzará en casos como nuestro ejemplo anterior; los campos se analizaron correctamente
desde la cadena, pero no suministraron suficiente información para crear un objeto LocalTime. Puede que
no sea obvio, pero nuestra hora, "3:14:15", podría ser tanto media tarde como muy, muy temprano en la
mañana. Nuestra elección del patrón 'hh' para las horas resulta ser el culpable. Podemos elegir un patrón
de hora que utilice una escala de 24 horas sin ambigüedades, o podemos agregar un elemento explícito
de AM/PM:

Entonces, si alguna vez obtienes un DateTimeParseException pero tu entrada parece ser una coincidencia
correcta para el formato, verifica nuevamente que tu formato incluya todo lo necesario para crear tu
fecha u hora. Un último pensamiento sobre estas excepciones: es posible que necesites usar el carácter
no mnemotécnico "u" para analizar años.
Hay muchos, muchos más detalles sobre DateTimeFormatter. Más que la mayoría de las clases de utilidad,
vale la pena visitar la documentación en línea para leer más al respecto.

Marcas de Tiempo (timestamps)

Otro concepto popular de fecha y hora que java.time comprende es la noción de un sello temporal
(timestamp). En cualquier situación donde sea necesario realizar un seguimiento del flujo de información,
necesitarás un registro de cuándo se produce o modifica exactamente la información. Aún verás la clase
java.util.Date utilizada para almacenar estos momentos en el tiempo, pero la clase java.time.Instant
contiene todo lo que necesitas para un sello temporal y ofrece todos los demás beneficios de las otras
clases en el paquete java.time:

Si las fechas o horas aparecen en tu trabajo, el paquete java.time se convierte en una adición bienvenida
a Java. Ahora cuentas con un conjunto de herramientas maduro y bien diseñado para manejar estos datos,
¡sin necesidad de bibliotecas de terceros!

Otras utilidades útiles

Hemos revisado algunos de los componentes fundamentales de Java, incluyendo cadenas y números, así
como una de las combinaciones más populares de estos elementos: fechas en las clases LocalDate y
LocalTime. Con suerte, esta gama de utilidades te ha dado una idea de cómo Java trabaja con muchos
elementos simples o comunes que es probable que encuentres al resolver problemas del mundo real.
Asegúrate de leer la documentación de los paquetes java.util, java.text y java.time para encontrar más
utilidades que puedan ser útiles. Por ejemplo, podrías investigar cómo utilizar java.util.Random para
generar las coordenadas aleatorias de los árboles que vimos en la Figura 8-1. También es importante
señalar que a veces el trabajo de "utilidades" es en realidad complejo y requiere atención detallada. A
menudo, puedes buscar en línea ejemplos de código o incluso bibliotecas completas escritas por otros
desarrolladores que puedan acelerar tus propios esfuerzos.

A continuación, queremos comenzar a construir sobre estos conceptos más fundamentales. Java sigue
siendo tan popular porque incluye soporte para técnicas más avanzadas además de lo básico. Una de esas
técnicas avanzadas que desempeñó un papel importante en el éxito inicial de Java son las características
de "hilos" integradas directamente. Los hilos proporcionan al programador un mejor acceso a sistemas
modernos y potentes, manteniendo el rendimiento de tus aplicaciones incluso al manejar muchas tareas
complejas. Profundicemos para ver cómo puedes aprovechar este soporte característico.
CAPÍTULO 9

Hilos(Threads)

Damos por sentado que los sistemas informáticos modernos pueden manejar muchas aplicaciones y
tareas del sistema operativo (SO) ejecutándose simultáneamente y hacer que parezca que todo el
software se ejecuta al mismo tiempo. La mayoría de los sistemas hoy en día tienen múltiples procesadores
y/o al menos múltiples núcleos y pueden lograr un impresionante grado de paralelismo. El SO todavía
maneja las aplicaciones a un nivel más alto, pero cambia su atención de una a la siguiente tan rápidamente
que también parecen ejecutarse a la vez.

En el pasado, la unidad de concurrencia para tales sistemas era la aplicación o proceso. Para el SO, un
proceso era más o menos una caja negra que decidía qué hacer por su cuenta. Si una aplicación requería
mayor concurrencia, solo podía lograrlo ejecutando múltiples procesos y comunicándose entre ellos, pero
este era un enfoque pesado y poco elegante. Más tarde, se introdujo el concepto de hilos. Los hilos
proporcionan una concurrencia fina dentro de un proceso bajo el control propio de la aplicación. Los hilos
han existido durante mucho tiempo, pero históricamente han sido difíciles de usar. En Java, el soporte
para los hilos está integrado en el lenguaje, lo que facilita trabajar con hilos. Las utilidades de concurrencia
de Java abordan patrones y prácticas comunes en aplicaciones multihilo y los elevan al nivel de APIs
tangibles en Java. En conjunto, esto significa que Java es un lenguaje que admite los hilos tanto de manera
nativa como a un nivel alto. También significa que las APIs de Java aprovechan al máximo los hilos, por lo
que es importante que adquieras cierto grado de familiaridad con estos conceptos al principio de tu
exploración de Java. No todos los desarrolladores necesitarán escribir aplicaciones que utilicen
explícitamente hilos o concurrencia, pero la mayoría utilizará alguna función que se vea afectada por ellos.

Los hilos son fundamentales para el diseño de muchas APIs de Java, especialmente aquellas involucradas
en aplicaciones del lado del cliente, gráficos y sonido. Por ejemplo, cuando veamos la programación de
interfaces gráficas (GUI) más adelante en este libro, verás que el método paint() de un componente no es
llamado directamente por la aplicación, sino por un hilo de dibujo separado dentro del sistema de tiempo
de ejecución de Java. En cualquier momento, muchos de esos hilos en segundo plano pueden estar
llevando a cabo actividades en paralelo con tu aplicación. En el lado del servidor, los hilos de Java también
están presentes, atendiendo cada solicitud y ejecutando los componentes de tu aplicación. Es importante
entender cómo tu código encaja en ese entorno.

En este capítulo, hablaremos sobre cómo escribir aplicaciones que crean y usan sus propios hilos
explícitamente. Hablaremos primero sobre el soporte de bajo nivel para hilos incorporado en el lenguaje
Java y luego discutiremos en detalle el paquete de utilidades de hilos java.util.concurrent al final de este
capítulo.

Introducción a los Hilos

Conceptualmente, un hilo es un flujo de control dentro de un programa. Un hilo es similar a la noción más
familiar de un proceso, excepto que los hilos dentro de la misma aplicación están mucho más relacionados
y comparten gran parte del mismo estado. Es como un campo de golf, que muchos golfistas usan al mismo
tiempo. Los hilos cooperan para compartir un área de trabajo. Tienen acceso a los mismos objetos,
incluyendo variables estáticas e de instancia, dentro de su aplicación. Sin embargo, los hilos tienen sus
propias copias de variables locales, al igual que los jugadores comparten el campo de golf pero no
comparten algunos objetos personales como palos y pelotas.

Múltiples hilos en una aplicación tienen los mismos problemas que los golfistas, en una palabra, la
sincronización. Así como no puedes tener dos conjuntos de jugadores jugando ciegamente en el mismo
green al mismo tiempo, no puedes tener varios hilos intentando acceder a las mismas variables sin algún
tipo de coordinación. Alguien está destinado a salir perjudicado. Un hilo puede reservar el derecho a usar
un objeto hasta que haya terminado su tarea, al igual que un grupo de golf tiene derechos exclusivos para
el green hasta que haya terminado. Y un hilo que sea más importante puede aumentar su prioridad,
afirmando su derecho a pasar.

El diablo está en los detalles, por supuesto, y esos detalles históricamente han hecho que los hilos sean
difíciles de usar. Afortunadamente, Java simplifica la creación, control y coordinación de hilos al integrar
directamente algunos de estos conceptos en el lenguaje.

Es común tropezar con hilos cuando trabajas con ellos por primera vez porque crear un hilo ejercita
muchas de tus nuevas habilidades en Java de una sola vez. Puedes evitar la confusión recordando que
siempre están involucrados dos jugadores en la ejecución de un hilo: un objeto Thread del lenguaje Java
que representa al hilo en sí mismo y un objeto objetivo arbitrario que contiene el método que el hilo debe
ejecutar. Más adelante, verás que es posible hacer un poco de magia y combinar estos dos roles, pero ese
caso especial solo cambia el empaquetado, no la relación.

La Clase Thread y la Interfaz Runnable

Toda la ejecución en Java está asociada con un objeto Thread, comenzando con un hilo "principal" que es
iniciado por la VM de Java para lanzar tu aplicación. Un nuevo hilo nace cuando creamos una instancia de
la clase java.lang.Thread. El objeto Thread representa un hilo real en el intérprete de Java y sirve como un
controlador para controlar y coordinar su ejecución. Con él, podemos iniciar el hilo, esperar a que
complete, hacer que duerma durante un tiempo o interrumpir su actividad. El constructor de la clase
Thread acepta información sobre dónde debería comenzar su ejecución el hilo. Conceptualmente, nos
gustaría simplemente decirle qué método ejecutar. Hay varias formas de hacer esto; Java 8 permite
referencias a métodos que podrían resolver el problema. Aquí haremos un pequeño desvío y usaremos la
interfaz java.lang.Runnable para crear o marcar un objeto que contiene un método "ejecutable".
Runnable define un único método run() de propósito general:

Cada hilo comienza su vida ejecutando el método run() en un objeto Runnable, que es el "objeto objetivo"
que se pasó al constructor del hilo. El método run() puede contener cualquier código, pero debe ser
público, no tomar argumentos, no tener un valor de retorno y no lanzar excepciones comprobadas.

Cualquier clase que contenga un método run() apropiado puede declarar que implementa la interfaz
Runnable. Una instancia de esta clase es entonces un objeto runnable que puede servir como el objetivo
de un nuevo hilo. Si no quieres colocar el método run() directamente en tu objeto (y muy a menudo no lo
haces), siempre puedes crear una clase adaptadora que sirva como Runnable para ti. El método run() del
adaptador puede llamar a cualquier método que desee después de que se inicie el hilo. Mostraremos
ejemplos de estas opciones más adelante.

Creación e inicio de hilos

Un hilo recién nacido permanece inactivo hasta que le damos una especie de palmada en la parte inferior
llamando a su método start(). El hilo entonces se despierta y procede a ejecutar el método run() de su
objeto objetivo. start() solo puede ser llamado una vez en la vida de un hilo. Una vez que un hilo comienza,
continúa ejecutándose hasta que el método run() del objeto objetivo devuelve (o lanza una excepción no
comprobada de algún tipo). El método start() tiene una especie de método gemelo malvado llamado
stop(), que mata permanentemente al hilo. Sin embargo, este método está obsoleto y ya no debería
usarse. Explicaremos por qué y daremos algunos ejemplos de una mejor manera de detener tus hilos más
adelante en este capítulo. También veremos algunos otros métodos que puedes usar para controlar el
progreso de un hilo mientras se está ejecutando.
Veamos un ejemplo. La siguiente clase, Animator, implementa un método run() para impulsar un bucle
de dibujo que podríamos usar en nuestro juego para actualizar el Campo (Field):

Para utilizarlo, creamos un objeto Thread, pasándole una instancia de Animator como su objeto objetivo,
e invocamos su método start(). Podemos realizar estos pasos de manera explícita así:

Creamos una instancia de nuestra clase Animator y la pasamos como argumento al constructor para
`myThread`. Como se muestra en la Figura 9-1, cuando llamamos al método `start()`, `myThread`
comienza a ejecutar el método `run()` de Animator. ¡Que comience el espectáculo!

Un hilo natural

La interfaz Runnable nos permite hacer que un objeto arbitrario sea el objetivo de un hilo, como lo hicimos
en el ejemplo anterior. Este es el uso más importante y general de la clase Thread. En la mayoría de las
situaciones en las que necesitas usar hilos, crearás una clase (posiblemente una clase adaptadora simple)
que implemente la interfaz Runnable.

Sin embargo, seríamos negligentes si no te mostráramos la otra técnica para crear un hilo. Otra opción de
diseño es hacer que nuestra clase objetivo sea una subclase de un tipo que ya sea Runnable. Resulta que
la clase Thread en sí misma implementa convenientemente la interfaz Runnable; tiene su propio método
`run()`, que podemos sobrescribir directamente para hacer lo que necesitemos:
El esqueleto de nuestra clase Animator se ve bastante similar al anterior, excepto que ahora nuestra clase
es una subclase de Thread. Para seguir este esquema, el constructor por defecto de la clase Thread se
convierte en su propio objetivo por defecto; es decir, por defecto, el hilo ejecuta su propio método `run()`
cuando llamamos al método `start()`, como se muestra en la Figura 9-2. Ahora nuestra subclase
simplemente puede sobrescribir el método `run()` en la clase Thread. (La clase Thread en sí misma define
un método `run()` vacío).

A continuación, creamos una instancia de Animator y llamamos a su método `start()` (que también heredó
de Thread):

Alternativamente, podemos hacer que el objeto Animator inicie su hilo cuando se crea:

Aquí, nuestro objeto Animator simplemente llama a su propio método start() cuando se crea una
instancia. (Probablemente sea una mejor práctica comenzar y detener nuestros objetos explícitamente
después de que se crean en lugar de iniciar hilos como un efecto secundario oculto de la creación del
objeto, pero esto sirve bien al ejemplo).

Subclasear Thread puede parecer una forma conveniente de agrupar un hilo y su método `run()` objetivo.
Sin embargo, este enfoque a menudo no es el mejor diseño. Si subclaseas Thread para implementar un
hilo, estás diciendo que necesitas un nuevo tipo de objeto que sea un tipo de Thread, lo que expone toda
la API pública de la clase Thread. Si bien es satisfactorio tomar un objeto que se preocupa principalmente
por realizar una tarea y convertirlo en un Thread, las situaciones reales en las que querrás crear una
subclase de Thread no deberían ser muy comunes. En la mayoría de los casos, es más natural dejar que
los requisitos de tu programa dicten la estructura de la clase y usar Runnables para conectar la ejecución
y la lógica de tu programa.

Controlando Hilos

Hemos visto que el método `start()` se utiliza para comenzar la ejecución de un nuevo hilo. Varios otros
métodos de instancia nos permiten controlar explícitamente la ejecución de un hilo:

- El método estático `Thread.sleep()` hace que el hilo que se está ejecutando espere durante un
período de tiempo designado (más o menos), sin consumir mucho (o posiblemente ningún)
tiempo de CPU.
- Los métodos `wait()` y `join()` coordinan la ejecución de dos o más hilos. Los discutiremos en
detalle cuando hablemos sobre la sincronización de hilos más adelante en este capítulo.

- El método `interrupt()` despierta a un hilo que está durmiendo en una operación `sleep()` o
`wait()` o que está bloqueado de otra manera en una larga operación de E/S.

Métodos obsoletos

También deberíamos mencionar tres métodos de control de hilos obsoletos: `stop()`, `suspend()` y
`resume()`. El método `stop()` complementa `start()`, destruye el hilo. `start()` y el método obsoleto
`stop()` solo se pueden llamar una vez en el ciclo de vida del hilo. En contraste, los métodos obsoletos
`suspend()` y `resume()` se usaban para pausar y luego reiniciar arbitrariamente la ejecución de un hilo.

Aunque estos métodos obsoletos todavía existen en la última versión de Java (y probablemente estarán
allí para siempre), no deberían usarse en el desarrollo de código nuevo. El problema con `stop()` y
`suspend()` es que toman el control de la ejecución de un hilo de una manera no coordinada y brusca.
Esto hace que la programación sea difícil; no siempre es fácil para una aplicación anticipar y recuperarse
adecuadamente de ser interrumpida en un punto arbitrario de su ejecución. Además, cuando un hilo es
detenido usando uno de estos métodos, el sistema de tiempo de ejecución de Java debe liberar todos sus
bloqueos internos utilizados para la sincronización de hilos. Esto puede causar un comportamiento
inesperado y, en el caso de `suspend()`, que no libera estos bloqueos, puede llevar fácilmente a un
bloqueo.

Una mejor manera de afectar la ejecución de un hilo, que requiere un poco más de trabajo de tu parte,
es crear alguna lógica simple en el código de tu hilo para usar variables de monitor (si estas variables son
booleanas, es posible que las veas referidas como "banderas"), posiblemente en conjunto con el método
`interrupt()`, que te permite despertar a un hilo dormido. En otras palabras, deberías hacer que tu hilo se
detenga o reanude lo que está haciendo pidiéndoselo amablemente en lugar de quitarle la base de
manera inesperada. Los ejemplos de hilos en este libro utilizan esta técnica de una manera u otra.

El método `sleep()`

A menudo necesitamos indicarle a un hilo que se quede inactivo o "dormido" durante un período de
tiempo fijo. Mientras un hilo está dormido, o de otra manera bloqueado de algún tipo de entrada, no
consume tiempo de CPU ni compite con otros hilos para procesamiento. Para esto, podemos llamar al
método estático `Thread.sleep()`, que afecta al hilo que se está ejecutando actualmente. La llamada hace
que el hilo se quede inactivo durante un número especificado de milisegundos:

El método `sleep()` puede arrojar una `InterruptedException` si es interrumpido por otro hilo a través del
método `interrupt()` (más abajo se detalla esto). Como viste en el código anterior, el hilo puede capturar
esta excepción y aprovechar la oportunidad para realizar alguna acción, como verificar una variable para
determinar si debe salir o simplemente realizar algunas tareas de limpieza y luego volver a dormir.

El método `join()`

Finalmente, si necesitas coordinar tus actividades con otro hilo esperando a que complete su tarea,
puedes usar el método `join()`. Llamar al método `join()` de un hilo hace que el llamador se bloquee hasta
que el hilo objetivo haya completado su ejecución. Alternativamente, puedes comprobar periódicamente
el hilo llamando a `join()` con un número de milisegundos para esperar. Esta es una forma muy básica de
sincronización de hilos. Java admite mecanismos más generales y poderosos para coordinar la actividad
de los hilos, incluyendo los métodos `wait()` y `notify()`, así como APIs de nivel superior en el paquete
`java.util.concurrent`. Dejamos esos temas principalmente para tu exploración personal, pero vale la pena
señalar que el lenguaje Java hace que el código con múltiples hilos sea más fácil de escribir que muchos
de sus predecesores.

El método `interrupt()`

Anteriormente, describimos el método `interrupt()` como una forma de despertar a un hilo que está
inactivo en una operación `sleep()`, `wait()` o en una operación de E/S larga. Cualquier hilo que no se
ejecute continuamente (no es un "bucle duro") debe ingresar periódicamente a uno de estos estados y,
por lo tanto, este es un punto donde se puede señalar al hilo para detenerse. Cuando se interrumpe un
hilo, se establece su bandera de estado de interrupción. Esto puede suceder en cualquier momento, ya
sea que el hilo esté inactivo o no. El hilo puede verificar este estado con el método `isInterrupted()`.
`isInterrupted(boolean)`, otra forma, acepta un valor booleano que indica si borrar o no el estado de
interrupción. De esta manera, un hilo puede utilizar el estado de interrupción como una bandera y una
señal.

Esta es de hecho la funcionalidad prescrita del método. Sin embargo, históricamente, este ha sido un
punto débil, y las implementaciones de Java han tenido problemas para hacerlo funcionar correctamente
en todos los casos. En las primeras VM de Java (antes de la versión 1.1), `interrupt()` no funcionaba en
absoluto. Versiones más recientes todavía tienen problemas con la interrupción de llamadas de E/S. Por
una llamada de E/S, nos referimos a cuando una aplicación está bloqueada en un método `read()` o
`write()`, moviendo bytes desde o hacia una fuente como un archivo o la red. En este caso, Java debería
arrojar un `InterruptedIOException` cuando se realiza la interrupción (`interrupt()`). Sin embargo, esto
nunca ha sido confiable en todas las implementaciones de Java. El marco de Nuevas E/S (java.nio) fue
introducido en Java 1.4 con uno de sus objetivos siendo abordar específicamente estos problemas.
Cuando se interrumpe el hilo asociado con una operación NIO, el hilo se despierta y el flujo de E/S (llamado
"canal") se cierra automáticamente. (Consulta el Capítulo 11 para más información sobre el paquete NIO).

Revisando la animación con hilos

Como discutimos al principio de este capítulo, una tarea común en las interfaces gráficas es manejar
animaciones. A veces, las animaciones son transiciones sutiles, otras veces son el enfoque de la aplicación
misma, como en nuestro juego de lanzamiento de manzanas. Hay varias formas de implementar la
animación; veremos el uso de hilos simples junto con las funciones `sleep()` así como el uso de un
temporizador. Combinar esas opciones con algún tipo de función de avance o "próximo cuadro" es un
enfoque popular que también es fácil de entender. Mostraremos ambas técnicas para animar nuestras
manzanas voladoras.

Podemos usar un hilo similar a "Creación e inicio de hilos" en la página 259 para generar una animación
real. La idea básica es pintar o posicionar todos tus objetos animados, pausar, moverlos a sus siguientes
posiciones y luego repetir. Veamos cómo dibujar algunas piezas de nuestro campo de juego sin animación
primero.
Suficientemente fácil. Empezamos pintando el campo de fondo, luego nuestro físico, después los árboles
y finalmente cualquier manzana. Eso garantiza que las manzanas se muestren "encima" de los otros
elementos. La clase Field anula el método paintComponent() de nivel medio disponible para todos los
elementos gráficos en Swing de Java para el dibujo personalizado, pero más sobre eso en el Capítulo 10.

Ahora, si pensamos en lo que cambia en la pantalla mientras jugamos, realmente hay dos elementos
"movibles": la manzana a la que apunta nuestro físico desde su torre y cualquier manzana que esté
volando después de ser lanzada. Sabemos que la "animación" de apuntar es simplemente en respuesta a
la actualización del físico mientras movemos un deslizador. Eso no requiere una animación separada. Así
que solo necesitamos concentrarnos en manejar las manzanas voladoras. Eso significa que la función de
paso de nuestro juego debería mover cada manzana que esté activa según las reglas de la gravedad. Aquí
están los dos métodos que cubren este trabajo. Configuramos las condiciones iniciales en el método toss()
según los valores de los deslizadores de apuntar y fuerza de nuestro físico. Luego hacemos un movimiento
para la manzana en el método step().

Ahora que sabemos cómo actualizar nuestras manzanas, podemos poner eso en un bucle de animación
que realizará los cálculos de actualización, repintará nuestro campo, hará una pausa y repetirá el proceso.
Utilizaremos esta implementación de Runnable en un hilo simple. Nuestra clase Field mantendrá una
instancia del hilo y contendrá el siguiente método start:

Con los eventos de interfaz de usuario que discutiremos en "Eventos" en la página 318, podríamos lanzar
nuestras manzanas bajo comando. Por ahora, simplemente lanzaremos la primera manzana tan pronto
como comience nuestro juego. Sabemos que la Figura 9-3 no parece mucho como una captura de pantalla
estática, pero confíen en nosotros, es asombroso en persona. :)
La Muerte de un Hilo

Un hilo continúa ejecutándose hasta que uno de los siguientes eventos ocurre:

- Retorna explícitamente desde su método objetivo run().

- Se encuentra con una excepción de tiempo de ejecución no capturada.

- Se llama al malvado y desagradable método stop() que está en desuso.

¿Qué sucede si ninguno de estos eventos ocurre, y el método run() de un hilo nunca termina? La respuesta
es que el hilo puede seguir vivo, incluso después de que lo que aparentemente es la parte de la aplicación
que lo creó haya finalizado. Esto significa que debemos ser conscientes de cómo nuestros hilos terminan
eventualmente, o una aplicación puede terminar dejando hilos huérfanos que consumen recursos
innecesariamente o mantienen viva la aplicación cuando de otro modo se cerraría.

En muchos casos, realmente queremos crear hilos en segundo plano que realicen tareas simples y
periódicas en una aplicación. El método setDaemon() puede usarse para marcar un hilo como un hilo
daemon que debe ser eliminado y descartado cuando no queden otros hilos no daemon en la aplicación.
Normalmente, el intérprete de Java sigue funcionando hasta que todos los hilos hayan terminado. Pero
cuando los hilos daemon son los únicos hilos aún vivos, el intérprete se cerrará.

Aquí hay un ejemplo diabólico que utiliza hilos daemon:

En este ejemplo, el hilo Devil establece su estado daemon cuando se crea. Si quedan hilos Devil cuando
nuestra aplicación está completa de otra manera, el sistema en tiempo de ejecución los elimina por
nosotros. No tenemos que preocuparnos por limpiarlos.

Los hilos daemon son principalmente útiles en aplicaciones Java independientes y en la implementación
de marcos de servidores, pero no en aplicaciones de componentes (donde una pequeña pieza de código
se ejecuta dentro de uno más grande). Dado que estos componentes se ejecutan dentro de otra aplicación
Java, cualquier hilo daemon que puedan crear puede seguir vivo hasta que la aplicación controladora se
cierre, probablemente no sea el efecto deseado. Cualquier aplicación de este tipo puede usar
ThreadGroups para contener todos los hilos creados por subsistemas o componentes y luego limpiarlos si
es necesario.

Una nota final sobre terminar hilos de manera adecuada. Un problema muy común que enfrentan los
nuevos desarrolladores la primera vez que crean una aplicación usando un componente Swing es que su
aplicación nunca termina; la máquina virtual de Java parece colgarse indefinidamente después de que
todo está terminado. Al trabajar con gráficos, Java ha creado un hilo de interfaz de usuario para procesar
eventos de entrada y de pintado. El hilo de la interfaz de usuario no es un hilo daemon, por lo que no se
cierra automáticamente cuando otros hilos de la aplicación han terminado, y el desarrollador debe llamar
a System.exit() explícitamente. (Si lo piensas, tiene sentido. Debido a que la mayoría de las aplicaciones
de GUI son impulsadas por eventos y simplemente esperan la entrada del usuario, de lo contrario
simplemente se cerrarían después de que se completara su código de inicio).
Sincronización

Cada hilo tiene su propia voluntad. Normalmente, un hilo hace su trabajo sin preocuparse por lo que están
haciendo otros hilos en la aplicación. Los hilos pueden ser divididos en tiempos, lo que significa que
pueden ejecutarse en ráfagas arbitrarias según lo indique el sistema operativo. En un sistema
multiprocesador o multinúcleo, incluso es posible que muchos hilos diferentes se ejecuten
simultáneamente en diferentes CPUs. Esta sección trata sobre coordinar las actividades de dos o más hilos
para que puedan trabajar juntos y no colisionar en su uso de las mismas variables y métodos (coordinando
su juego en el campo de golf).

Java proporciona algunas estructuras simples para sincronizar las actividades de los hilos. Todas están
basadas en el concepto de monitores, un esquema de sincronización ampliamente utilizado. No es
necesario conocer los detalles sobre cómo funcionan los monitores para poder utilizarlos, pero puede
ayudarte tener una idea en mente.

Un monitor es esencialmente un bloqueo. El bloqueo está asociado a un recurso al que muchos hilos
pueden necesitar acceder, pero que solo debe ser accedido por un hilo a la vez. Es muy parecido a un
baño con un cerrojo en la puerta; si está desbloqueado, puedes entrar y cerrar la puerta mientras lo usas.
Si el recurso no está siendo utilizado, el hilo puede adquirir el bloqueo y acceder al recurso. Cuando el hilo
termina, libera el bloqueo, al igual que tú desbloqueas la puerta del baño y la dejas abierta para la
siguiente persona.

Sin embargo, si otro hilo ya tiene el bloqueo para el recurso, todos los demás hilos deben esperar hasta
que el hilo actual haya terminado y haya liberado el bloqueo. Esto es similar a cuando el baño está
ocupado cuando llegas: tienes que esperar hasta que el usuario actual termine y desbloquee la puerta.

Afortunadamente, Java hace el proceso de sincronizar el acceso a los recursos bastante fácil. El lenguaje
se encarga de configurar y adquirir los bloqueos; todo lo que necesitas hacer es especificar los recursos
que requieren sincronización.

Serialización del Acceso a Métodos

La necesidad más común de sincronización entre hilos en Java es serializar su acceso a algún recurso (un
objeto), en otras palabras, asegurarse de que solo un hilo a la vez pueda manipular un objeto o variable.
En Java, cada objeto tiene un bloqueo asociado. Para ser más específicos, cada clase y cada instancia de
una clase tiene su propio bloqueo. La palabra clave synchronized marca los lugares donde un hilo debe
adquirir el bloqueo antes de continuar.

Por ejemplo, supongamos que implementamos una clase SpeechSynthesizer que contiene un método
say(). No queremos que múltiples hilos llamen a say() al mismo tiempo porque no podríamos entender
nada de lo que se dice. Entonces, marcamos el método say() como synchronized, lo que significa que un
hilo debe adquirir el bloqueo en el objeto SpeechSynthesizer antes de que pueda hablar:

Debido a que say() es un método de instancia, un hilo debe adquirir el bloqueo en la instancia de
SpeechSynthesizer que está utilizando antes de poder invocar el método say(). Cuando say() ha
completado su ejecución, libera el bloqueo, lo que permite al siguiente hilo en espera adquirir el bloqueo
y ejecutar el método. No importa si el hilo es propiedad de SpeechSynthesizer en sí mismo o de algún otro
objeto; cada hilo debe adquirir el mismo bloqueo, el de la instancia de SpeechSynthesizer. Si say() fuera
un método de clase (estático) en lugar de un método de instancia, aún podríamos marcarlo como
synchronized. En este caso, dado que no hay ningún objeto de instancia involucrado, el bloqueo está en
el objeto de clase en sí mismo.

A menudo, se desea sincronizar múltiples métodos de la misma clase para que solo un método modifique
o examine partes de la clase a la vez. Todos los métodos estáticos sincronizados en una clase utilizan el
mismo bloqueo del objeto de clase. De la misma manera, todos los métodos de instancia en una clase
utilizan el mismo bloqueo del objeto de instancia. De esta manera, Java puede garantizar que solo uno de
un conjunto de métodos sincronizados se está ejecutando en un momento dado. Por ejemplo, una clase
SpreadSheet podría contener una serie de variables de instancia que representan valores de celda, así
como algunos métodos que manipulan las celdas en una fila:

En este ejemplo, los métodos setRow() y sumRow() acceden a los valores de celda. Puedes ver que podrían
surgir problemas si un hilo estuviera cambiando los valores de las variables en setRow() al mismo tiempo
que otro hilo estaba leyendo los valores en sumRow(). Para evitar esto, hemos marcado ambos métodos
como synchronized. Cuando los hilos están sincronizados, solo uno se ejecuta a la vez. Si un hilo está en
medio de la ejecución de setRow() cuando otro hilo llama a sumRow(), el segundo hilo espera hasta que
el primero termine de ejecutar setRow() antes de ejecutar sumRow(). Esta sincronización nos permite
preservar la consistencia de la SpreadSheet. Lo mejor es que todo este bloqueo y espera es manejado por
Java; es invisible para el programador.

Además de sincronizar métodos completos, la palabra clave synchronized se puede utilizar en una
construcción especial para proteger bloques de código arbitrarios. En esta forma, también toma un
argumento explícito que especifica el objeto para el cual debe adquirir un bloqueo:

Este bloque de código puede aparecer en cualquier método. Cuando se alcanza, el hilo debe adquirir el
bloqueo en miObjeto antes de continuar. De esta manera, podemos sincronizar métodos (o partes de
métodos) en diferentes clases de la misma forma que los métodos en la misma clase.

Un método de instancia sincronizado es, por lo tanto, equivalente a un método con sus declaraciones
sincronizadas en el objeto actual. Por lo tanto:

es equivalente a:
Podemos demostrar los conceptos básicos de la sincronización con un escenario clásico de
"productor/consumidor". Tenemos algunos recursos comunes con productores que crean nuevos
recursos y consumidores que toman y utilizan esos recursos. Un ejemplo podría ser una serie de
rastreadores web que recogen imágenes en línea. El "productor" en esto podría ser un hilo (o varios hilos)
realizando el trabajo real de cargar y analizar páginas web en busca de imágenes y sus URL. Esas URL
podrían colocarse en una cola común y el hilo "consumidor" tomaría la siguiente URL en la cola y
descargaría la imagen al sistema de archivos o a una base de datos. Aquí no intentaremos realizar toda la
entrada/salida real (más sobre archivos y redes en el Capítulo 11), pero podemos configurar fácilmente
algunos hilos productores y consumidores para ver cómo funciona la sincronización.

Sincronización de una cola de URLs

Comencemos primero con la cola donde se almacenarán las URLs. No estamos intentando ser sofisticados
con la cola en sí misma; es solo una lista donde podemos añadir URLs (como cadenas de texto) al final y
extraerlas desde el principio. Utilizaremos una LinkedList similar a la ArrayList que vimos en el Capítulo 7.
Es una estructura diseñada para el acceso eficiente y la manipulación que deseamos para esta cola.

¡Ten en cuenta que no todos los métodos están sincronizados! Permitimos que cualquier hilo pregunte si
la cola está vacía o no sin detener a otros hilos que podrían estar añadiendo o eliminando elementos. Esto
significa que podríamos informar una respuesta incorrecta si el momento de los diferentes hilos es
exactamente incorrecto, pero nuestro sistema es algo tolerante a fallos, por lo que la eficiencia de no
bloquear la cola solo para verificar su tamaño es más ventajosa que tener un conocimiento más perfecto.

Ahora que sabemos cómo almacenaremos y recuperaremos nuestras URLs, podemos crear las clases de
productor y consumidor. El productor ejecutará un bucle simple para crear URLs ficticias, las etiquetará
con un ID de productor y las almacenará en nuestra cola. Aquí está el método run() para URLProducer:
La clase del consumidor será bastante similar, con la excepción obvia de sacar URLs de la cola. Extraerá
una URL, la etiquetará con un ID de consumidor y continuará hasta que los productores hayan terminado
de producir y la cola esté vacía.

Podemos comenzar ejecutando nuestra simulación con números muy pequeños: dos productores y dos
consumidores, donde cada productor creará solo tres URLs.

Incluso con estos números tan pequeños involucrados, aún podemos ver los efectos de utilizar múltiples
hilos para realizar el trabajo:
Notemos que los hilos no toman turnos perfectos de manera circular, pero cada hilo obtiene al menos
algo de tiempo de trabajo. Y puedes ver que los consumidores no están bloqueados a productores
específicos. Nuevamente, la idea es hacer un uso eficiente de recursos limitados. Los productores pueden
seguir agregando tareas sin preocuparse por cuánto tiempo llevará cada tarea o a quién asignársela. Los
consumidores, a su vez, pueden tomar una tarea sin preocuparse por otros consumidores. Si un
consumidor recibe una tarea simple y termina antes que otros consumidores, puede volver y obtener una
nueva tarea de inmediato.

Intenta ejecutar este ejemplo por ti mismo y aumenta algunos de esos números. ¿Qué sucede con cientos
de URLs? ¿Qué sucede con cientos de productores o consumidores? A gran escala, este tipo de multitarea
es casi obligatoria. No encontrarás programas grandes por ahí que no utilicen hilos para manejar al menos
parte de su trabajo en segundo plano. De hecho, como vimos anteriormente, el propio paquete gráfico
de Java, Swing, necesita un hilo separado para mantener la interfaz de usuario receptiva y correcta, sin
importar cuán pequeña sea tu aplicación.

Acceso a Variables de Clase e Instancia desde Múltiples Hilos

En el ejemplo de SpreadSheet, protegimos el acceso a un conjunto de variables de instancia con un


método sincronizado para evitar cambiar una de las variables mientras alguien estaba leyendo las otras.
Queríamos mantenerlas coordinadas. Pero, ¿qué pasa con tipos de variables individuales? ¿Necesitan ser
sincronizados? Normalmente, la respuesta es no.

Casi todas las operaciones en tipos de primitivas y referencias de objetos en Java suceden atómicamente:
es decir, son manejadas por la VM en un solo paso, sin oportunidad para que dos hilos colisionen. Esto
evita que los hilos miren referencias mientras están siendo accedidas por otros hilos.

Pero ten cuidado: dijimos casi. Si lees cuidadosamente la especificación de la VM de Java, verás que los
tipos primitivos double y long no están garantizados que sean manejados atómicamente. Ambos tipos
representan valores de 64 bits. El problema tiene que ver con cómo la pila de la VM de Java los maneja.
Es posible que esta especificación se modifique en el futuro. Pero por ahora, para ser estrictos, deberías
sincronizar el acceso a tus variables de instancia double y long a través de métodos de acceso, o usar la
palabra clave volatile o una clase de contenedor atómico, que describiremos a continuación.

Otro problema, independiente de la atomicidad de los valores, es la noción de que diferentes hilos en la
VM almacenan en caché valores durante períodos de tiempo; es decir, aunque un hilo haya cambiado el
valor, la VM de Java no está obligada a hacer que ese valor aparezca hasta que la VM alcance un estado
conocido como "barrera de memoria". Puedes empezar a abordar esto declarando la variable con la
palabra clave volatile. Esta palabra clave indica a la VM que el valor puede ser cambiado por hilos externos
y sincroniza automáticamente el acceso a él. Cualificamos esa afirmación con "empezar a abordar" porque
los sistemas multinúcleo introducen más oportunidades para comportamientos inconsistentes y con
errores. Los párrafos finales de "Utilidades de Concurrencia" en la página 282 tienen algunas sugerencias
de lectura excelentes si tienes planes de desarrollo comercial para tu código multihilo.
Finalmente, el paquete java.util.concurrent.atomic proporciona clases de contenedor sincronizadas para
todos los tipos primitivos y referencias. Estos contenedores no solo ofrecen operaciones simples set() y
get() en los valores, sino también operaciones "combo" especializadas, como compareAndSet(), que
funcionan de manera atómica y se pueden utilizar para construir componentes de aplicación
sincronizados de nivel superior. Las clases en este paquete fueron diseñadas específicamente para
mapearse a funcionalidades a nivel de hardware en muchos casos y pueden ser muy eficientes.

Planificación y Prioridad

Java hace pocas garantías sobre cómo planifica los hilos. Casi toda la planificación de hilos en Java se deja
a la implementación de Java y, en cierto grado, a la aplicación. Aunque habría tenido sentido (y
ciertamente habría hecho más felices a muchos desarrolladores) si los desarrolladores de Java hubieran
especificado un algoritmo de planificación, un solo algoritmo no es necesariamente adecuado para todos
los roles que Java puede desempeñar. En su lugar, los diseñadores de Java pusieron la carga en ti de
escribir código robusto que funcione independientemente del algoritmo de planificación y dejaron que la
implementación ajustara el algoritmo para un mejor ajuste.

Las reglas de prioridad que describiremos a continuación están cuidadosamente redactadas en la


especificación del lenguaje Java como una guía general para la planificación de hilos. Deberías poder
confiar en este comportamiento en general (estadísticamente), pero no es una buena idea escribir código
que dependa de características muy específicas del planificador para funcionar correctamente. En su
lugar, deberías usar las herramientas de control y sincronización que hemos descrito en este capítulo para
coordinar tus hilos.

Cada hilo tiene un valor de prioridad. En general, cada vez que un hilo de una prioridad más alta que el
hilo actual se vuelve ejecutable (se inicia, deja de dormir o es notificado), desplaza al hilo de menor
prioridad y comienza a ejecutarse. Por defecto, los hilos con la misma prioridad se programan en modo
round-robin, lo que significa que una vez que un hilo comienza a ejecutarse, continúa hasta que hace una
de las siguientes acciones:

- Duerme, llamando a Thread.sleep() o wait().

- Espera un bloqueo, para ejecutar un método sincronizado.

- Se bloquea en I/O, por ejemplo, en una llamada a read() o accept().

- Cede el control explícitamente, llamando a yield().

- Termina completando su método objetivo.

Esta situación se parece algo a lo mostrado en la Figura 9-4.


Estado de un Hilo

El estado de un hilo puede ser uno de los cinco estados generales que abarcan su ciclo de vida y
actividades. Estos estados están definidos en la enumeración Thread.State y se consultan mediante el
método getState() de la clase Thread:

NEW (Nuevo)

El hilo ha sido creado, pero aún no ha comenzado.

RUNNABLE (Ejecutable)

El estado activo normal de un hilo en ejecución, incluido el tiempo en el que un hilo está
bloqueado en una operación de E/S, como una lectura o escritura o una conexión de red.

BLOCKED (Bloqueado)

El hilo está bloqueado, esperando para ingresar a un método o bloque de código sincronizado.
Esto incluye el tiempo en el que un hilo ha sido despertado por un notify() y está intentando
volver a adquirir su bloqueo después de un wait().

WAITING, TIMED_WAITING (Esperando, Espera con tiempo)

El hilo está esperando a otro hilo mediante una llamada a wait() o join(). En el caso de
TIMED_WAITING, la llamada tiene un tiempo de espera.

TERMINATED (Terminado)

El hilo ha finalizado debido a una salida, una excepción o ha sido detenido.

Podemos mostrar el estado de todos los hilos en Java (en el grupo de hilos actual) con el siguiente
fragmento de código:

Probablemente no utilices esta API en programación general, pero es interesante y útil para experimentar
y aprender sobre los hilos de Java.

División de Tiempo

Además de la priorización, todos los sistemas modernos (con la excepción de algunos entornos Java
integrados y "micro") implementan la división de tiempo de los hilos. En un sistema de división de tiempo,
el procesamiento de hilos se divide para que cada hilo se ejecute durante un breve período de tiempo
antes de que se cambie el contexto al siguiente hilo, como se muestra en la Figura 9-5.
Los hilos de mayor prioridad todavía desplazan a los hilos de menor prioridad en este esquema. La adición
de la división de tiempo mezcla el procesamiento entre hilos de la misma prioridad; en una máquina
multiprocesador, los hilos incluso pueden ejecutarse simultáneamente. Esto puede introducir diferencias
en el comportamiento para aplicaciones que no usan hilos y sincronización de manera adecuada.

Estrictamente hablando, debido a que Java no garantiza la división de tiempo, no deberías escribir código
que dependa de este tipo de programación; cualquier software que escribas debería funcionar bajo
programación round-robin. Si te preguntas qué hace tu versión particular de Java, prueba el siguiente
experimento:

La clase Thready inicia dos objetos ShowThread. ShowThread es un hilo que entra en un bucle fuerte (muy
mala práctica) e imprime su mensaje. Debido a que no especificamos una prioridad para ninguno de los
hilos, ambos heredan la prioridad de su creador, por lo que tienen la misma prioridad. Cuando ejecutas
este ejemplo, verás cómo tu implementación de Java realiza su programación. Bajo un esquema de round-
robin, solo se imprimirá "Foo"; "Bar" nunca aparece. En una implementación de división de tiempo,
ocasionalmente deberías ver los mensajes "Foo" y "Bar" alternarse.

Prioridades

Como mencionamos anteriormente, las prioridades de los hilos existen como una guía general para cómo
la implementación debe asignar tiempo entre hilos competidores. Desafortunadamente, con la
complejidad de cómo se asignan los hilos de Java a implementaciones de hilos nativas, no puedes
depender del significado exacto de las prioridades. En cambio, solo debes considerarlas como una
sugerencia para la VM.

Juguemos con la prioridad de nuestros hilos:


Con este cambio en nuestra clase Thready, esperaríamos que el hilo Bar tome completamente el control.
Si ejecutas este código en una antigua implementación de Solaris de Java 5.0, eso es lo que sucede. Lo
mismo no ocurre en Windows o con algunas versiones más antiguas de Java. Del mismo modo, si cambias
las prioridades a valores que no sean mínimo y máximo, es posible que no veas ninguna diferencia. Las
sutilezas relacionadas con la prioridad y el rendimiento están relacionadas con cómo se asignan los hilos
y las prioridades de Java a los hilos reales en el sistema operativo. Por esta razón, las prioridades de los
hilos deberían reservarse para el desarrollo de sistemas y marcos de trabajo.

Rendición (Yielding)

Cuando un hilo duerme, espera o se bloquea en E/S, cede su intervalo de tiempo y otro

hilo es programado. Siempre y cuando no escribas métodos que utilicen bucles intensivos, todos

los hilos deberían recibir su turno. Sin embargo, un hilo también puede señalar que está dispuesto a ceder

voluntariamente su tiempo en cualquier momento con la llamada a yield(). Podemos modificar nuestro
ejemplo anterior para incluir un yield() en cada iteración:

Deberías ver mensajes de "Foo" y "Bar" alternando estrictamente. Si tienes hilos que realizan cálculos
muy intensivos u ocupan mucho tiempo de CPU, es posible que desees encontrar un lugar apropiado para
que cedan el control ocasionalmente. Alternativamente, es posible que desees reducir la prioridad de tu
hilo intensivo en cómputo para que el procesamiento más importante pueda avanzar a su alrededor.

Desafortunadamente, la especificación del lenguaje Java es muy débil con respecto a yield(). Es otro de
esos elementos que deberías considerar como una pista de optimización en lugar de una garantía. En el
peor de los casos, el sistema de tiempo de ejecución puede simplemente ignorar las llamadas a yield().

Rendimiento de los hilos

La forma en que las aplicaciones utilizan los hilos y los costos y beneficios asociados han impactado en
gran medida el diseño de muchos APIs de Java. Discutiremos algunos de los problemas en detalle en otros
capítulos. Pero vale la pena mencionar brevemente algunos aspectos del rendimiento de los hilos y cómo
el uso de hilos ha dictado la forma y funcionalidad de varios paquetes Java recientes.

El Costo de la Sincronización

El acto de adquirir bloqueos para sincronizar hilos lleva tiempo, incluso cuando no hay contención. En
implementaciones antiguas de Java, este tiempo podía ser significativo. Con máquinas virtuales más
nuevas, es casi despreciable. Sin embargo, la sincronización innecesaria a bajo nivel aún puede ralentizar
las aplicaciones al bloquear hilos donde, de lo contrario, se podría permitir un acceso concurrente
legítimo. Debido a esto, dos APIs importantes, el API de Colecciones de Java y el API de GUI Swing, fueron
específicamente diseñados para evitar sincronizaciones innecesarias colocándolas bajo el control del
desarrollador.
El API de Colecciones de java.util reemplaza tipos de agregación Java anteriores y simples, como Vector y
Hashtable, con tipos más completos y notablemente no sincronizados (List y Map). El API de Colecciones
deja que el código de la aplicación sincronice el acceso a las colecciones cuando sea necesario y
proporciona funcionalidades especiales de "falla rápida" para ayudar a detectar el acceso concurrente y
lanzar una excepción. También proporciona "envoltorios" de sincronización que pueden proporcionar
acceso seguro al estilo antiguo. Implementaciones especiales amigables con el acceso concurrente de los
Map y Queue están incluidas como parte del paquete java.util.concurrent. Estas implementaciones van
incluso más allá en el sentido de que están escritas para permitir un alto grado de acceso concurrente sin
ninguna sincronización del usuario.

La GUI Java Swing ha adoptado un enfoque diferente para proporcionar velocidad y seguridad. Swing dicta
que la modificación de sus componentes (con excepciones notables) debe hacerse por un único hilo: la
cola principal de eventos. Swing resuelve problemas de rendimiento y problemas desagradables de
determinismo en el orden de eventos al forzar a un hilo súper único a controlar la GUI. La aplicación puede
acceder al hilo de la cola de eventos indirectamente al enviar comandos a través de una interfaz simple.
Veremos cómo hacerlo en el Capítulo 10 y aplicaremos ese conocimiento al problema común de
reaccionar a la información entregada externamente a través de la red en el Capítulo 11.

Consumo de recursos del hilo

Un patrón fundamental en Java es iniciar muchos hilos para manejar recursos externos asíncronos, como
conexiones de socket. Para obtener la máxima eficiencia, un servidor web podría verse tentado a crear
un hilo para cada conexión de cliente que esté atendiendo. Con cada cliente teniendo su propio hilo, las
operaciones de E/S pueden bloquearse y reiniciarse según sea necesario. Pero por eficiente que pueda
ser en términos de rendimiento, es un uso muy ineficiente de los recursos del servidor. Los hilos consumen
memoria; cada hilo tiene su propia "pila" para variables locales y el cambio entre hilos en ejecución
(cambio de contexto) agrega sobrecarga a la CPU. Si bien los hilos son relativamente livianos (en teoría,
es posible tener cientos o miles en un servidor grande), llega un punto en el que los recursos consumidos
por los propios hilos comienzan a desvirtuar el propósito de iniciar más hilos. A menudo, este punto se
alcanza con solo unas pocas docenas de hilos. Crear un hilo por cliente no siempre es una opción escalable.

Un enfoque alternativo es crear "pools de hilos" donde un número fijo de hilos extraen tareas de una cola
y regresan por más cuando terminan. Este reciclaje de hilos permite una escalabilidad sólida, pero
históricamente ha sido difícil de implementar eficientemente para servidores en Java porque la E/S de
flujo (para cosas como sockets) no ha admitido completamente operaciones sin bloqueo. El paquete NIO
tiene canales de E/S asíncronos: lecturas y escrituras no bloqueantes y la capacidad de "seleccionar" o
probar la disponibilidad de flujos para mover datos. Los canales también pueden cerrarse de forma
asincrónica, permitiendo que los hilos trabajen con ellos de manera óptima. Con el paquete NIO, es
posible crear servidores con patrones de hilos mucho más sofisticados y escalables.

Los grupos de hilos y los servicios de "executor" de trabajos están codificados como utilidades como parte
del paquete java.util.concurrent, lo que significa que no tienes que escribirlos tú mismo. Los resumiremos
a continuación cuando discutamos las utilidades de concurrencia en Java.

Utilidades de Concurrencia

Hasta ahora en este capítulo, hemos demostrado cómo crear y sincronizar hilos a un nivel bajo, utilizando
primitivas del lenguaje Java. El paquete java.util.concurrent y sus subpaquetes introducidos con Java 5.0
se basan en esta funcionalidad, agregando utilidades importantes para hilos y codificando algunos
patrones de diseño comunes al suministrar implementaciones estándar. Aproximadamente en orden de
generalidad, estas áreas incluyen:

Implementaciones de Colecciones conscientes de hilos

El paquete java.util.concurrent amplía el API de Colecciones de Java en el Capítulo 7 con varias


implementaciones para modelos de hilos específicos. Estos incluyen implementaciones de espera
cronometrada y bloqueo de la interfaz Queue, así como implementaciones optimizadas para
acceso concurrente no bloqueante de las interfaces Queue y Map. El paquete también agrega
implementaciones de List y Set de "copia en escritura" para casos extremadamente eficientes de
"casi siempre lectura". Estas pueden sonar complejas, pero realmente cubren algunos casos
bastante simples de manera muy efectiva.

Ejecutores

Los ejecutores ejecutan tareas, incluyendo Runnable, y abstraen el concepto de creación y


agrupación de hilos del usuario. Los ejecutores están destinados a ser un reemplazo de alto nivel
para el paradigma de crear nuevos hilos para atender una serie de trabajos. Junto con los
ejecutores, se introducen las interfaces Callable y Future, que expanden Runnable para permitir
la gestión, el retorno de valores y el manejo de excepciones.

Constructos de sincronización a nivel bajo

El paquete java.util.concurrent.locks contiene un conjunto de clases, incluyendo Lock y


Condition, que se asemejan a las primitivas de sincronización a nivel de lenguaje Java y las
promueven al nivel de una API concreta. El paquete locks también agrega el concepto de
bloqueos de lector/escritor no exclusivos, lo que permite una mayor concurrencia en el acceso
sincronizado a los datos.

Constructos de sincronización a nivel alto

Esto incluye las clases CyclicBarrier, CountDownLatch, Semaphore y Exchanger. Estas clases
implementan patrones comunes de sincronización extraídos de otros lenguajes y sistemas y
pueden servir como base para nuevas herramientas de alto nivel.

Operaciones atómicas (suena muy James Bond, ¿no?)

El paquete java.util.concurrent.atomic proporciona envoltorios y utilidades para operaciones


atómicas de "todo o nada" en tipos primitivos y referencias. Esto incluye operaciones atómicas
simples como probar un valor antes de establecerlo y obtener e incrementar un número en una
sola operación.

Con la posible excepción de las optimizaciones realizadas por la JVM para el paquete de operaciones
atómicas, todas estas utilidades están implementadas en Java puro, sobre las construcciones de
sincronización estándar del lenguaje Java. Esto significa que, en cierto sentido, son solo utilidades de
conveniencia y no agregan realmente nuevas capacidades al lenguaje. Su función principal es ofrecer
patrones e idiomas estándar en la programación de hilos en Java y hacerlos más seguros y eficientes de
usar. Un buen ejemplo de esto es la utilidad Executor, que permite a un usuario gestionar un conjunto de
tareas en un modelo de hilos predefinido sin tener que crear hilos en absoluto. APIs de nivel superior
como esta simplifican la codificación y permiten una mayor optimización de los casos comunes.

Si bien no veremos ninguno de estos paquetes en este capítulo, queremos que sepas dónde podrías
profundizar más si la concurrencia te resulta interesante o parece útil para resolver los problemas en tu
trabajo. Como mencionamos anteriormente en "Sincronizar una cola de URL" en la página 271, "Java
Concurrency In Practice" de Brian Goetz es lectura obligatoria para proyectos del mundo real. También
queremos dar un reconocimiento a Doug Lea, el autor de Concurrent Programming in Java (Addison-
Wesley), quien lideró el grupo que agregó estos paquetes a Java y es en gran medida responsable de
crearlos. Hemos mencionado el framework Java Swing de pasada varias veces en este libro, incluso en
este capítulo con respecto al rendimiento de los hilos. A continuación, finalmente es el momento de
analizar ese framework en más detalle.
CAPÍTULO 10

Aplicaciones de escritorio

Java saltó a la fama y gloria gracias a los applets: elementos sorprendentes e interactivos en una página
web. Suena mundano en estos días, pero en ese momento fue algo asombroso. Sin embargo, Java también
contaba con soporte multiplataforma y podía ejecutar el mismo código en sistemas Windows, Unix y
macOS. Las primeras versiones del JDK tenían un conjunto rudimentario de componentes gráficos
conocidos colectivamente como Abstract Window Toolkit (AWT).

Lo "abstracto" en AWT proviene del uso de clases comunes (Button, Window, etc.) con implementaciones
nativas. Escribes aplicaciones AWT con código abstracto y multiplataforma; tu computadora ejecuta tu
aplicación y proporciona componentes concretos y nativos.

Esa ingeniosa combinación de abstracto y nativo viene con algunas limitaciones bastante serias,
desafortunadamente. En el ámbito abstracto, te encuentras con diseños de "mínimo común
denominador" que solo te dan acceso a funciones disponibles en todas las plataformas que Java soporta.
En implementaciones nativas, incluso algunas características disponibles de manera aproximada en todas
partes eran notablemente diferentes cuando se representaban realmente en la pantalla. Muchos
desarrolladores de escritorio que trabajaron con Java en esos primeros días bromearon que el lema
"escribe una vez, ejecuta en todas partes" era realmente "escribe una vez, depura en todas partes". El
paquete Java Swing se propuso mejorar este estado lamentable. Si bien Swing no resolvió todos los
problemas de entrega de aplicaciones multiplataforma, sí hizo posible el desarrollo serio de aplicaciones
de escritorio en Java. Puedes encontrar muchos proyectos de código abierto de calidad e incluso algunas
aplicaciones comerciales escritas en Swing. De hecho, el IDE que detallamos en el Apéndice A, IntelliJ IDEA,
¡es una aplicación Swing! Claramente compite mano a mano con los IDE nativos tanto en rendimiento
como en usabilidad.

Si miras la documentación para el paquete javax.swing, verás que contiene una multitud de clases. Y aún
necesitarás algunas piezas del mundo original de java.awt también. Hay libros enteros sobre AWT (Java
AWT Reference, Zukowski, O’Reilly) y sobre Swing (Java Swing, 2nd Edition, Loy, et al., O’Reilly), e incluso
libros sobre subpaquetes como gráficos 2D (Java 2D Graphics, Knudsen, O’Reilly). En este capítulo, nos
conformaremos con cubrir algunos componentes populares como botones y campos de texto. Veremos
cómo colocarlos en la ventana de tu aplicación y cómo interactuar con ellos. Podrías sorprenderte de lo
sofisticada que puede volverse tu aplicación con estos simples temas iniciales. Si realizas más desarrollo
de escritorio después de este libro, también podrías sorprenderte de la cantidad de contenido de interfaz
gráfica de usuario (GUI, o simplemente UI) disponible para Java. Queremos aguzar tu apetito mientras
reconocemos que hay muchas, muchas más discusiones de UI que debemos dejar a un lado para que las
descubras más adelante. Dicho esto, ¡que comience la visita rápida!

¡Botones y deslizadores y campos de texto, oh Dios mío!

Entonces, ¿por dónde empezar? Tenemos un problema algo similar al del huevo y la gallina. Necesitamos
discutir las "cosas" para poner en la pantalla, como los objetos JLabel que usamos en "Hello-Java" en la
página 41. Pero también necesitamos discutir en qué colocas esas cosas. Y dónde colocas esas cosas
merece una discusión, ya que es un proceso no trivial. Así que ahora tenemos un problema de gallina,
huevo y brunch. Toma una taza de café o un mimosa y empecemos. Primero cubriremos algunos
componentes populares (las "cosas"), luego sus contenedores y, finalmente, el tema de cómo colocar tus
componentes en esos contenedores. Una vez que puedas colocar un buen conjunto de widgets en la
pantalla, discutiremos cómo interactuar con ellos, así como cómo manejar la interfaz de usuario en un
mundo multihilo.
Jerarquías de componentes

Como hemos discutido en capítulos anteriores, las clases Java están diseñadas y extendidas de manera
jerárquica. JComponent y JContainer se encuentran en la cima de la jerarquía de clases Swing, como se
muestra en la Figura 10-1. No cubriremos estas dos clases en detalle, pero recuerda sus nombres.
Encontrarás varios atributos y métodos comunes en estas clases mientras lees la documentación de
Swing. A medida que avances en tus esfuerzos de programación, es probable que llegues a un punto en
el que quieras construir tu propio componente. JComponent es un excelente punto de partida. Haremos
precisamente eso para completar nuestro ejemplo de juego de lanzamiento de manzanas.

Estaremos cubriendo la mayoría de las otras clases mencionadas en la jerarquía abreviada anterior, pero
definitivamente querrás visitar la documentación en línea para ver los muchos componentes que tuvimos
que omitir.

Arquitectura Modelo Vista Controlador (MVC)

En la base de la noción de "cosas" de Swing se encuentra un patrón de diseño conocido como Modelo
Vista Controlador (MVC). Los autores del paquete Swing trabajaron arduamente para aplicar
consistentemente este patrón para que cuando te encuentres con nuevos componentes, su
comportamiento y uso deberían resultar familiares. La arquitectura MVC tiene como objetivo separar lo
que ves (la vista) del estado detrás de escena (el modelo) y de la colección de interacciones (el
controlador) que causan cambios en esas partes. Esta separación de preocupaciones te permite
concentrarte en cada pieza individualmente. El tráfico de red puede actualizar el modelo detrás de escena.
La vista puede sincronizarse a intervalos regulares que se sienten fluidos y receptivos para el usuario. MVC
proporciona un marco poderoso pero manejable para usar al construir cualquier aplicación de escritorio.

Al observar nuestra pequeña selección de componentes, resaltaremos los elementos del modelo y la vista.
Luego entraremos en más detalle sobre los controladores en "Eventos" en la página 318. Si encuentras
intrigante la noción de patrones de programación, "Design Patterns: Elements of Reusable Object-
Oriented Software" de Gamma, Helm, Johnson y Vlissides (el renombrado Grupo de los Cuatro), es el
trabajo seminal. Para obtener más detalles sobre el uso del patrón MVC en Swing, consulta el capítulo
introductorio de "Java Swing, 2nd Edition" de Loy et al.
Etiquetas y Botones

El componente de interfaz de usuario más simple no sorprendentemente es uno de los más populares.
Las etiquetas se utilizan en muchos lugares para indicar funcionalidad, mostrar estado y enfocar la
atención. Utilizamos una etiqueta para nuestra primera aplicación gráfica en el Capítulo 2. Usaremos
muchas más etiquetas a medida que continuemos construyendo programas más interesantes. El
componente JLabel es una herramienta versátil. Veamos algunos ejemplos para entender cómo usar
JLabel y personalizar sus numerosos atributos. Comenzaremos volviendo a nuestro programa "Hola, Java"
con algunas modificaciones preparatorias:

Brevemente, las partes interesantes son:

Establecer el gestor de diseño para ser utilizado por el marco (frame).

Establecer la acción tomada al utilizar el botón "cerrar" del sistema operativo (en este caso,
el botón rojo en la esquina superior izquierda de la ventana). La acción que seleccionamos aquí
cierra la aplicación.

Crear nuestra simple etiqueta.

Puedes ver la etiqueta declarada e inicializada y luego añadida al marco (frame). Esperemos que eso te
resulte familiar. Lo que probablemente sea nuevo es nuestro uso de una instancia de FlowLayout. Esa
línea nos ayuda a producir la captura de pantalla mostrada en la Figura 10-2.
Revisaremos los gestores de diseño con mucho más detalle en "Contenedores y Disposiciones" en la
página 306, pero necesitamos algo que nos permita avanzar y también nos permita agregar múltiples
componentes a un solo contenedor. La clase FlowLayout llena un contenedor al centrar horizontalmente
los componentes en la parte superior, agregándolos de izquierda a derecha hasta que esa "fila" se quede
sin espacio, y luego continúa en una nueva fila debajo. Este tipo de disposición no será de mucha utilidad
en aplicaciones más grandes, pero es ideal para colocar rápidamente varias cosas en la pantalla.

Probemos ese punto añadiendo algunas etiquetas más. Simplemente agrega algunas declaraciones de
etiquetas adicionales y agrégalas al marco (frame), luego revisa los resultados mostrados en la Figura 10-
3:

¡Genial, ¿verdad? Una vez más, este diseño simple no está destinado a la mayoría de los tipos de
contenido que encuentras en aplicaciones de producción, pero definitivamente es útil cuando estás
comenzando. Hay otro punto sobre los diseños (layouts) que queremos mencionar, ya que encontraremos
esta idea más adelante: FlowLayout también trata con el tamaño de las etiquetas. Puede ser difícil notarlo
en este ejemplo porque las etiquetas tienen un fondo transparente de forma predeterminada. Si
importamos la clase java.awt.Color, podemos usar esa clase para ayudar a hacerlas opacas y darles un
color de fondo específico:
Si hacemos lo mismo para todas nuestras etiquetas, ahora podemos ver sus tamaños reales y los espacios
entre ellas en la Figura 10-4. Pero si podemos controlar el color de fondo de las etiquetas, ¿qué más
podemos hacer? ¿Podemos cambiar el color del primer plano? (Sí). ¿Podemos cambiar la fuente? (Sí).
¿Podemos cambiar la alineación? (Sí). ¿Podemos agregar iconos? (Sí). ¿Podemos crear etiquetas
autoconscientes que eventualmente construyan Skynet y provoquen el fin de la humanidad? (Tal vez,
pero probablemente no, y ciertamente no fácilmente. Mejor así).

La Figura 10-5 muestra algunas de estas modificaciones posibles.

Y aquí está el código fuente respectivo que construyó esta variedad:


Utilizamos algunas otras clases para ayudar, como java.awt.Font y javax.swing.ImageIcon. Hay muchas
más opciones que podríamos revisar, pero necesitamos echar un vistazo a algunos otros componentes. Si
deseas experimentar con estas etiquetas y probar más de las opciones que ves en la documentación de
Java, intenta importar un ayudante que creamos para jshell y experimentar. Los resultados de nuestras
pocas líneas se muestran en la Figura 10-6.
Espero que veas lo sencillo que es ahora crear una etiqueta (o cualquier otro componente, como un botón,
que exploraremos a continuación) y ajustar sus parámetros de manera interactiva. Esta es una excelente
manera de familiarizarte con los elementos que tienes a tu disposición para construir aplicaciones de
escritorio en Java. Si utilizas mucho nuestro Widget, es posible que encuentres útil su método reset(). Este
método elimina todos los componentes actuales y actualiza la pantalla para que puedas empezar de
nuevo rápidamente.

Botones

El otro componente casi universal que necesitarás para aplicaciones gráficas es el botón. La clase JButton
es tu botón de referencia en Swing. (También encontrarás otros tipos populares de botones, como
JCheckbox y JToggleButton, en la documentación). Crear un botón es muy similar a crear una etiqueta,
como se muestra en la Figura 10-7.
Puedes controlar los colores, la alineación, la fuente, y demás para los botones de manera muy similar a
como lo haces para las etiquetas. La diferencia, por supuesto, es que puedes hacer clic en un botón y
reaccionar a ese clic en tu programa, mientras que las etiquetas son estáticas en su mayor parte. Intenta
ejecutar este ejemplo y hacer clic en el botón. Debería cambiar de color y sentirse "presionado" aunque
aún no realice ninguna otra función en nuestro programa.

Espero que hayas utilizado suficientes aplicaciones o sitios web para estar familiarizado con los botones y
su comportamiento. Queremos revisar algunos componentes más antes de abordar la noción de
"reaccionar" a un clic de botón (un "evento" en el lenguaje de Swing), pero puedes pasar a "Eventos" en
la página 318 ¡si no puedes esperar más!

Componentes de texto

Justo detrás de los botones y las etiquetas en popularidad estarían los campos de texto. Estos elementos
de entrada que permiten la entrada libre de información son casi omnipresentes en formularios en línea.
Puedes capturar nombres, direcciones de correo electrónico, números de teléfono y tarjetas de crédito.
Puedes hacer todo eso en idiomas que componen sus caracteres, o en otros que leen de derecha a
izquierda. Sería imposible imaginar una aplicación de escritorio o web hoy en día sin la disponibilidad de
entrada de texto. Swing tiene tres grandes componentes de texto: JTextField, JTextArea y JTextPane;
todos extienden a un padre común, JTextComponent. JTextField es un campo de texto clásico destinado
a entradas breves de una palabra o una línea. JTextArea permite mucha más entrada repartida en
múltiples líneas. JTextPane es un componente especializado destinado a editar texto enriquecido. No
utilizaremos JTextPane en este capítulo, pero vale la pena señalar que hay algunos componentes muy
interesantes disponibles en Swing sin necesidad de utilizar bibliotecas de terceros.

Campos de texto

Vamos a poner un ejemplo de cada uno en nuestra aplicación simple y fluida. Reduciremos las cosas a un
par de etiquetas y campos de texto correspondientes, que son, con mucho, los componentes de entrada
más comunes:
Observa en la Figura 10-8 que el tamaño de un campo de texto está dictado por el número de columnas
que especificamos en su constructor. Esa no es la única forma de inicializar un campo de texto, pero es
útil cuando no hay otros mecanismos de diseño que dicten el ancho del campo. (Aquí el FlowLayout nos
falló un poco: la etiqueta "Email:" no se mantuvo en la misma línea que el campo de texto del correo
electrónico. Pero de nuevo, más sobre los diseños pronto). ¡Adelante y escribe algo! Puedes ingresar y
eliminar texto; cortar, copiar y pegar como esperarías; y resaltar cosas dentro del campo con tu ratón.

Todavía necesitamos la capacidad de reaccionar a esta entrada, lo cual se abordará en "Eventos" en la


página 318, pero si agregas un campo de texto a nuestra aplicación de demostración en jshell, como se
muestra en la Figura 10-9, puedes llamar a su método getText() para ver que el contenido está
efectivamente disponible para ti.

Ten en cuenta que la propiedad de texto es de lectura y escritura. Puedes llamar a setText() en tu campo
de texto para cambiar su contenido programáticamente. Esto puede ser útil para establecer valores
predeterminados, dar formato automáticamente a cosas como números de teléfono o para rellenar un
formulario con información que recopiles desde la red. Pruébalo en jshell.
Áreas de texto

Cuando simples palabras o incluso entradas de URLs largas no son suficientes, es probable que recurras a
JTextArea para dar al usuario suficiente espacio. Podemos crear un área de texto vacía con un constructor
similar al que usamos para JTextField, pero esta vez especificando el número de filas además del número
de columnas. El código para agregar nuestra área de texto a nuestra aplicación de demostración de
entrada de texto en ejecución está abajo, y los resultados se muestran en la Figura 10-10:

Puedes ver fácilmente que tenemos espacio para varias líneas de texto. Adelante, ejecuta esta nueva
versión y pruébalo por ti mismo. ¿Qué sucede si escribes más allá del final de una línea? ¿Qué pasa cuando
presionas la tecla Enter? Esperemos que obtengas los comportamientos con los que estás familiarizado.
Veremos cómo ajustar esos comportamientos más adelante, pero queremos señalar que aún tienes
acceso a su contenido, al igual que lo haces con un campo de texto.

Vamos a agregar un área de texto a nuestro widget en jshell:

¡Genial! Puedes ver que la tecla "Return" que escribimos para producir nuestras tres líneas en la Figura
10-11 se codifica como el carácter \n en la cadena que recuperamos.
Pero, ¿qué sucede si intentas escribir una oración larga y continua que se extiende más allá del final de la
línea? Podrías obtener un área de texto extraña que se expande al tamaño de nuestra ventana y más allá,
como se muestra en la Figura 10-12.

Podemos corregir ese comportamiento de tamaño incorrecto al observar un par de propiedades de JTex

tArea, que se muestran en la Tabla 10-1.

lineWrap false Si las líneas más largas que la tabla deben envolverse en absoluto

wrapStyleWord false Si las líneas se envuelven, si los saltos de línea deben estar en límites de
palabras o caracteres

Entonces, empecemos de nuevo y activemos el ajuste de líneas por palabras. Podemos usar
setLineWrap(true) para asegurarnos de que el texto se ajuste. Pero eso probablemente no sea suficiente.
Utiliza setWrapStyleWord(true) además para asegurarte de que el área de texto no solo divida palabras
por la mitad. Eso debería darnos la imagen en la Figura 10-13.

Puedes probar esto por ti mismo en jshell o en tu propia aplicación si quieres comprobar que la tercera
línea se ajusta. Cuando recuperes el texto de nuestro objeto bodyArea, no deberías ver un salto de línea
(\n) en la línea tres entre el segundo "on" y el "but".
Desplazamiento de texto

Hemos solucionado lo que sucede si tenemos demasiados caracteres para una línea, pero ¿qué pasa si
tenemos demasiadas filas? Por sí sola, JTextArea hace ese extraño truco de "crecer hasta que no podemos
más", como se muestra en la Figura 10-14.

Para solucionar este problema, necesitamos utilizar el soporte de un componente auxiliar estándar de
Swing: JScrollPane. Este es un contenedor de propósito general que facilita la presentación de
componentes grandes en espacios confinados. Para mostrarte lo fácil que es, arreglemos nuestro área de
texto:

Puedes ver en la Figura 10-15 que ya no nos expandimos más allá de los límites del marco. También puedes
observar las barras de desplazamiento estándar a lo largo del lado y la parte inferior. Si solo necesitas un
desplazamiento simple, ¡ya has terminado! Pero al igual que la mayoría de los otros componentes en
Swing, JScrollPane tiene muchos detalles finos que puedes ajustar según sea necesario. No cubriremos la
mayoría de esos detalles aquí, pero queremos mostrarte cómo abordar una configuración común para
áreas de texto: ajuste de líneas (con ruptura de palabras) con desplazamiento vertical, lo que significa sin
desplazamiento horizontal.
Deberíamos terminar con un área de texto similar a la que se muestra en la Figura 10-16.

¡Hurra! Ahora has tenido un vistazo a los componentes Swing más comunes, incluyendo etiquetas,
botones y campos de texto. Pero realmente apenas hemos rasguñado la superficie de estos componentes.
Todos tienen muchos atributos diferentes que se pueden ajustar fácilmente con llamadas como el método
setBackground() que utilizamos en nuestras instancias de JLabel en "Etiquetas y Botones" en la página
288. Revisa la documentación de Java y experimenta con cada uno de estos componentes en jshell o en
tus propias aplicaciones pequeñas. Sentirte cómodo con el diseño de interfaz de usuario depende de la
práctica. Definitivamente te animamos a buscar otros libros y recursos en línea si vas a desarrollar
aplicaciones de escritorio para el trabajo o incluso solo para compartir con amigos, pero nada supera el
tiempo que pasas en el teclado creando realmente una aplicación y corrigiendo las cosas que
inevitablemente salen mal.

Otros Componentes

Si ya has revisado la documentación en el paquete javax.swing, sabrás que hay varios docenas de otros
componentes disponibles para usar en tus aplicaciones. Dentro de esa larga lista, hay algunos que
queremos asegurarnos de que conozcas.

JSlider

Los deslizadores son un componente de entrada ingenioso y eficiente cuando se trata de un rango de
valores. Probablemente hayas visto deslizadores en cosas como selectores de tamaño de fuente,
selectores de color (piensa en los rangos de rojo, verde y azul), selectores de zoom, etc. De hecho, un
deslizador es perfecto tanto para los valores de ángulo como para los valores de fuerza que necesitamos
en nuestro juego de lanzamiento de manzanas. Nuestros ángulos van desde 0 hasta 180, y nuestro valor
de fuerza varía de 0 a 20 (nuestro máximo arbitrario). La Figura 10-17 muestra estos deslizadores en su
lugar, simplemente ignora cómo logramos el diseño por ahora.

Para crear un nuevo deslizador, típicamente proporcionas tres valores: el mínimo (0 para nuestro
deslizador de ángulo), el máximo (180) y el valor inicial (optaremos por el punto medio en 90). Podemos
agregar un deslizador justo así a nuestro entorno de jshell de la siguiente manera:

Mueve el deslizador como ves en la Figura 10-18 y luego mira su valor actual usando el método getValue():
En "Eventos" en la página 318, veremos cómo recibir esos valores a medida que el usuario los cambia en
tiempo real.

Si revisas la documentación de los constructores de JSlider, notarás que utilizan enteros para los valores
mínimo y máximo. También habrás notado que getValue() también devuelve un entero. Si necesitas
valores fraccionarios, eso depende de ti. El deslizador de fuerza en nuestro juego, por ejemplo, se
beneficiaría de admitir más de 21 niveles discretos. Podemos abordar ese tipo de necesidad construyendo
el deslizador con un rango (a menudo mucho) más grande y luego simplemente dividiendo el valor actual
por un factor de escala apropiado.

JList

Si tienes un rango de valores pero esos valores no son simples enteros contiguos, el elemento de interfaz
de usuario "lista" es una gran elección. JList es la implementación Swing de este tipo de entrada. Puedes
configurarlo para permitir selecciones individuales o múltiples, y si profundizas en las características de
Swing, puedes producir vistas personalizadas que muestren los elementos en tu lista con información
adicional o detalles. (Por ejemplo, puedes hacer listas de iconos, o iconos y texto, o texto en varias líneas,
etc., etc.)

A diferencia de los otros componentes que hemos visto hasta ahora, JList requiere un poco más de
información para empezar. Para crear un componente de lista útil, debes usar uno de los constructores
que recibe los datos que deseas mostrar. El constructor más simple de este tipo acepta un arreglo de
objetos. Aunque puedes pasar un arreglo de objetos extraños, el comportamiento predeterminado de
JList será mostrar la salida del método toString() de tus objetos en la lista. Usar un arreglo de objetos
String es muy común y produce los resultados esperados. La Figura 10-19 muestra una lista simple de
ciudades.
Observa que utilizamos la misma información de tipo <String> con el constructor como lo hacemos al
crear objetos de colección como ArrayList (ver "Limitaciones de tipo" en la página 203). Como Swing se
agregó mucho antes que los genéricos, es posible que encuentres ejemplos en línea o en libros que no
agreguen la información de tipo. Al igual que con las clases de colecciones, esto no impide que tu código
se compile o se ejecute, pero recibirás el mismo mensaje de advertencia no verificado en tiempo de
compilación.

Similar a obtener el valor actual de un deslizador, puedes recuperar el ítem o ítems seleccionados en una
lista usando uno de cuatro métodos:

• getSelectedIndex() para listas de selección única, devuelve un entero.


• getSelectedIndices() para listas de selección múltiple, devuelve un array de enteros.
• getSelectedValue() para listas de selección única, devuelve un objeto.
• getSelectedValues() para listas de selección múltiple, devuelve un array de objetos.

Obviamente, la principal diferencia es si el índice del ítem seleccionado o los valores reales son más útiles
para ti. Jugando con nuestra lista de ciudades en jshell, podemos extraer una ciudad seleccionada de esta
manera:

Ten en cuenta que para listas largas, probablemente querrás una barra de desplazamiento. Swing
promueve la reutilización en su código, por lo que quizás no sea sorprendente que puedas usar un
JScrollPane con JList de la misma manera que lo hicimos para las áreas de texto en "Desplazamiento de
texto" en la página 299.

Contenedores y Diseños

¡Esa es una lista considerable de componentes! Y realmente es solo un subconjunto de los widgets
disponibles para tus aplicaciones gráficas. Pero dejaremos la exploración de los otros componentes Swing
en tus manos a medida que te familiarices más con Java en general y diseñes soluciones específicas de
programación para problemas reales. En esta sección, queremos concentrarnos en ensamblar los
componentes mencionados anteriormente en disposiciones útiles. Esas disposiciones ocurren dentro de
un contenedor, así que comencemos esta discusión examinando los contenedores más comunes.
Ventanas y Marcos

Cada aplicación de escritorio necesitará al menos una ventana. Este término precede a Swing y se utiliza
en la mayoría de las interfaces gráficas disponibles en los tres grandes sistemas operativos. Swing
proporciona una clase de nivel bajo llamada JWindow si la necesitas, pero lo más probable es que
construyas tu aplicación dentro de un JFrame. De hecho, nuestra primera aplicación gráfica en el Capítulo
2 utilizó un JFrame. La Figura 10-20 ilustra la jerarquía de clases de JFrame. Nos centraremos en las
características básicas de JFrame, pero a medida que tus aplicaciones se vuelvan más complejas, es
posible que desees crear tus propias ventanas utilizando elementos más avanzados en la jerarquía.

Claro, vamos a repasar la creación de esa primera aplicación gráfica y enfocarnos un poco más en lo que
hacemos exactamente con el objeto JFrame que creamos:

La cadena que pasamos al constructor de JFrame se convierte en el título de la ventana. Luego


establecemos algunas propiedades específicas en nuestro objeto. Nos aseguramos de que cuando el
usuario cierre la ventana, salgamos de nuestro programa. (Eso puede parecer obvio, pero aplicaciones
complejas podrían tener múltiples ventanas, como paletas de herramientas o soporte para varios
documentos. Cerrar una ventana en estas aplicaciones puede no significar "salir"). Luego elegimos un
tamaño inicial para la ventana y agregamos nuestro componente real, la etiqueta (que a su vez coloca la
etiqueta en su panel de contenido, más sobre eso en un momento). Una vez que se agrega el componente,
hacemos visible la ventana y el resultado es la Figura 10-21.
Este proceso básico es la base de cada aplicación Swing. La parte interesante de tu aplicación viene de lo
que haces con ese panel de contenido. Pero, ¿qué es ese panel de contenido? Resulta que el marco
(frame) utiliza su propio componente o contenedor, una instancia de JPanel (más sobre JPanel en la
próxima sección). Si observas detenidamente la documentación de JFrame, podrás notar que puedes
establecer tu propio panel de contenido para que sea cualquier objeto descendiente de
java.awt.Container, pero por ahora nos quedaremos con el valor predeterminado. Como quizás hayas
notado anteriormente, también estamos utilizando un atajo para agregar nuestra etiqueta. La versión de
add() de JFrame llamará al add() del panel de contenido. Podríamos haber dicho, por ejemplo:

La clase JFrame no tiene atajos para todo lo que podrías hacer con el panel de contenido, sin embargo.
Lee la documentación y usa un atajo si existe. Si no lo hay, no dudes en obtener una referencia a través
de getContentPane() y luego configurar o ajustar ese objeto.

JPanel

La clase JPanel es el contenedor por excelencia en Swing. Es un componente al igual que JButton o JLabel,
por lo que tus paneles pueden contener otros paneles. Este anidamiento a menudo juega un papel
importante en el diseño de una aplicación. Por ejemplo, podrías crear un JPanel para albergar los botones
de formato de un editor de texto en una "barra de herramientas", de modo que sea fácil mover esa barra
de herramientas según las preferencias del usuario.

JPanel te da la capacidad de agregar y quitar componentes de la pantalla. (Más precisamente, esos


métodos add/remove se heredan de la clase Container, pero accedemos a ellos a través de nuestros
objetos JPanel). También puedes usar repaint() en un panel si algo ha cambiado y quieres actualizar tu
interfaz de usuario. Podemos ver los efectos de los métodos add() y remove() mostrados en la Figura 10-
22 utilizando nuestro objeto playground en jshell:
¡Inténtalo tú mismo! Sin embargo, la mayoría de las aplicaciones no añaden y eliminan componentes sin
ton ni son. Por lo general, construyes tu interfaz agregando lo que necesitas y luego simplemente la dejas
así. Puedes habilitar o deshabilitar algunos botones en el camino, pero no quieres tener la costumbre de
sorprender al usuario con partes que desaparecen o nuevos elementos que aparecen de repente.

Gestores de diseño

Otra característica clave de JPanel en Swing (o de cualquier descendiente de Container, realmente) es la


noción de dónde terminan ubicados los componentes que agregas en el contenedor y qué tamaño tienen.
En términos de interfaz de usuario, esto es "organizar" tu contenedor, y Java proporciona varios gestores
de diseño (layout managers) para ayudarte a lograr los resultados deseados.

BorderLayout

Ya hemos visto el FlowLayout en acción (al menos en su orientación horizontal, uno de sus constructores
puede crear una columna de componentes). También estábamos usando otro gestor de diseño sin
realmente saberlo. El panel de contenido de un JFrame utiliza BorderLayout de forma predeterminada.
La Figura 10-23 muestra las cinco áreas controladas por BorderLayout, junto con los nombres de sus
regiones. Observa que las regiones NORTH y SOUTH tienen el ancho de la ventana de la aplicación, pero
solo son tan altas como se requiera para ajustarse a la etiqueta. De manera similar, las regiones EAST y
WEST llenan el espacio vertical entre las regiones NORTH y SOUTH, pero solo tienen el ancho necesario,
dejando el espacio restante para ser llenado tanto horizontal como verticalmente por la región CENTER.

Observa que el método add() en este caso toma un argumento adicional. Ese argumento se pasa al gestor
de diseño. No todos los gestores necesitan este argumento, como vimos con FlowLayout.

Aquí tienes un ejemplo donde el anidamiento de objetos JPanel puede ser muy útil: la aplicación principal
en un JPanel en el centro, una barra de herramientas en un JPanel en la parte superior, una barra de
estado en un JPanel en la parte inferior, un administrador de proyectos en un JPanel en un lateral, etc.
BorderLayout define esas regiones utilizando direcciones de los puntos cardinales. La Figura 10-24
muestra un ejemplo muy simple de dicho anidamiento de contenedores. Usamos un área de texto para
un mensaje grande en el centro y luego agregamos algunos botones de acción a un panel en la parte
inferior. Una vez más, sin los eventos que cubriremos en la próxima sección, ninguno de estos botones
hace algo, pero queremos mostrar cómo trabajar con múltiples contenedores. Y podrías continuar
anidando objetos JPanel si así lo deseas; simplemente asegúrate de que tu jerarquía sea legible. A veces,
una mejor elección de diseño de nivel superior hace que tu aplicación sea más mantenible y más eficiente.

Dos cosas para señalar en este ejemplo. Primero, podrías notar que no especificamos el número de filas
o columnas al crear nuestro objeto JTextArea. A diferencia de FlowLayout, BorderLayout establecerá el
tamaño de sus componentes cuando sea posible. Para la parte superior e inferior, esto significa usar la
altura propia del componente, similar a cómo funciona FlowLayout, pero luego estableciendo el ancho
del componente para llenar el marco. Los laterales utilizan el ancho de sus componentes, pero luego
establecen la altura. El componente en el centro, como nuestro área de texto anterior, obtiene su ancho
y altura establecidos por BorderLayout.

La segunda cosa puede ser obvia, pero queremos llamar la atención sobre ella de todos modos. Observa
que cuando agregamos los objetos messageArea y buttonPanel al marco, especificamos el argumento
adicional "where" para el método add() del marco. Sin embargo, cuando agregamos los propios botones
a buttonPanel, usamos la versión más simple de add() con solo el argumento del componente. Esas
diferentes llamadas a add() están relacionadas con el contenedor que realiza la llamada y pasan
argumentos apropiados para el gestor de diseño de ese contenedor. Entonces, aunque el buttonPanel
está en la región SOUTH del marco, el saveButton y sus compañeros no conocen ni les importa ese detalle.
GridLayout

Muchas veces necesitas (o quieres) que tus componentes o etiquetas ocupen espacios simétricos. Piensa
en los botones Sí, No y Cancelar en la parte inferior de un diálogo de confirmación. (Swing también puede
crear esos diálogos, pero hablaremos más de eso en "Modales y Pop Ups" en la página 327). La clase
GridLayout es uno de los gestores de diseño tempranos que ayuda con ese espaciado uniforme.
Intentemos usar GridLayout para esos botones en nuestro ejemplo anterior. Todo lo que tenemos que
hacer es cambiar una línea:

Las llamadas a add() siguen siendo exactamente las mismas; no se necesita un argumento de restricción
separado. El resultado se muestra en la Figura 10-25.

Como puedes ver en la Figura 10-25, los botones de GridLayout tienen el mismo tamaño, aunque el texto
del botón Cancelar es un poco más largo que los demás. Al crear el gestor de diseño, le indicamos que
queremos exactamente una fila, sin restricciones (el "cero") en la cantidad de columnas. Aunque, como
su nombre indica, las cuadrículas pueden ser bidimensionales y podemos especificar exactamente cuántas
filas y columnas queremos. La Figura 10-26 muestra el diseño clásico del teclado de un teléfono.
Añadir los botones en orden de izquierda a derecha, de arriba hacia abajo, debería resultar en la aplicación
que ves en la Figura 10-26.

Muy útil y fácil si necesitas elementos perfectamente simétricos. Pero, ¿qué sucede si deseas algo
principalmente simétrico? Piensa en formularios web populares con una columna de etiquetas a la
izquierda y una columna de campos de texto a la derecha. GridLayout podría manejar perfectamente un
formulario básico como ese, pero muchas veces tus etiquetas son cortas y simples, mientras que tus
campos de texto son más anchos, permitiendo más entrada por parte del usuario. ¿Cómo acomodamos
esos diseños?

GridBagLayout

Si necesitas un diseño más interesante pero no quieres anidar muchos paneles, GridBagLayout es una
posibilidad. Es un poco más complejo de configurar, pero permite algunos diseños intrincados que aún
mantienen los elementos alineados y dimensionados estéticamente. Similar a BorderLayout, agregas
componentes con un argumento adicional. Sin embargo, el argumento para GridBagLayout es un objeto
GridBagConstraints completo en lugar de una simple cadena.

El "grid" en GridBagLayout es exactamente eso, un contenedor rectangular dividido en varias filas y


columnas. La parte "bag", sin embargo, proviene de una especie de noción de bolsa de la que dispones
para utilizar las celdas creadas por esas filas y columnas. Las filas y columnas pueden tener su propia altura
o ancho, y los componentes pueden ocupar cualquier colección rectangular de celdas. Podemos
aprovechar esta flexibilidad para construir nuestra interfaz de juego con un solo JPanel en lugar de con
varios paneles anidados. La Figura 10-27 muestra una forma de dividir la pantalla en cuatro filas y tres
columnas, y luego colocar los componentes.
Puedes ver las diferentes alturas de fila y anchos de columna. Y observa cómo algunos componentes
ocupan más de una celda. Este tipo de disposición no funcionará para todas las aplicaciones, pero es
poderoso y funciona para muchos interfaces de usuario que necesitan más que diseños simples.

Para construir una aplicación con GridBagLayout, necesitas mantener un par de referencias mientras
agregas componentes. Primero, configuremos la cuadrícula:

Genial. Este paso requiere un poco de planificación por tu parte, pero siempre es fácil ajustarlo una vez
que tienes algunos componentes en la pantalla. Para agregar esos componentes, necesitas crear y
configurar un objeto GridBagConstraints. Afortunadamente, puedes reutilizar el mismo objeto para todos
tus componentes, solo necesitas repetir la configuración antes de agregar cada elemento. Aquí tienes un
ejemplo de cómo podríamos agregar el componente principal del campo de juego:
Observa cómo establecemos qué celdas ocupará el campo. Este es el núcleo de la configuración de las
restricciones del grid bag. También puedes ajustar cosas como cómo un componente llenará las celdas
que ocupa y cuánto margen recibirá cada componente. Nos hemos decidido por simplemente rellenar
todo el espacio disponible en un grupo de celdas ("both" para un relleno horizontal y vertical), pero
puedes encontrar más opciones en la documentación para GridBagConstraints.

Agreguemos una etiqueta para llevar el marcador en la parte superior:

Para este segundo componente, fíjate en lo similar que es la configuración de las restricciones a cómo
manejamos el campo de juego. Cada vez que observes similitudes como esta, deberías considerar agrupar
esos pasos en una función que puedas reutilizar. Podríamos hacer precisamente eso:

Y luego reescribir los bloques anteriores de código para la etiqueta de puntuación y el campo de juego,
de esta manera:
Con esa función en su lugar, podemos rápidamente añadir los varios otros componentes y etiquetas que
queremos para completar nuestra interfaz de juego. Por ejemplo, el botón de lanzamiento en la esquina
inferior derecha de la Figura 10-27 puede configurarse así:

¡Mucho más limpio! Simplemente continuamos construyendo nuestros componentes y colocándolos en


la fila y columna correctas, con los espacios apropiados. Al final, tenemos un conjunto de componentes
razonablemente interesantes distribuidos en un único contenedor.

Como en otras secciones de este capítulo, no tenemos tiempo para cubrir todos los administradores de
diseño, ni siquiera todas las características de los administradores de diseño que discutimos. Asegúrate
de consultar la documentación de Java y prueba creando algunas aplicaciones de prueba para
experimentar con los diferentes diseños. Como punto de partida, BoxLayout es una mejora agradable
respecto a la idea de cuadrícula, y GroupLayout puede producir formularios de entrada de datos bien
alineados. Por ahora, sin embargo, vamos a avanzar y finalmente conectar todos estos componentes y
empezar a responder a toda la escritura, clics y pulsaciones de botones; todas las acciones que están
codificadas en Java como eventos.

Eventos

Al pensar en la arquitectura MVC, podemos ver que los elementos del modelo y la vista son directos. Ya
hemos visto varios componentes Swing y hemos tocado su vista, así como el modelo para componentes
más interesantes como JList. (Las etiquetas y los botones también tienen modelos, por supuesto,
simplemente no son muy complejos).

Con ese trasfondo establecido, veamos la funcionalidad del controlador. En Swing (y Java en general), la
interacción entre los usuarios y los componentes se comunica mediante eventos. Un evento contiene
información general, como cuándo ocurrió, así como información específica del tipo de evento, como el
punto en la pantalla donde hiciste clic con tu ratón. Un listener (o controlador) recoge el mensaje y puede
responder de alguna manera útil.

A medida que trabajas a través de los ejemplos a continuación, es probable que notes que algunos de los
eventos y listeners forman parte del paquete javax.swing.event, mientras que otros están en
java.awt.event. Esto refleja el hecho de que Swing sucedió a AWT. Las partes de AWT que siguen siendo
relevantes se siguen utilizando, pero Swing añadió varios elementos nuevos para adaptarse a la
funcionalidad en expansión proporcionada por la biblioteca.

Eventos de ratón

La forma más sencilla de empezar es simplemente generar y manejar un evento. Volvamos a nuestra
simple aplicación HelloJava (actualizada a HelloMouse!) y añadamos un listener para eventos de ratón.
Cuando hagamos clic con nuestro ratón, usaremos ese evento de clic para determinar la posición de
nuestro JLabel. (Esto requerirá eliminar el administrador de diseño, por cierto. Queremos establecer las
coordenadas de nuestra etiqueta manualmente). Aquí está el código de nuestra nueva aplicación
interactiva:
Adelante y ejecuta la aplicación. Obtendrás una aplicación gráfica bastante familiar de "Hola, Mundo",
como se muestra en la Figura 10-28. El mensaje amigable debería seguirte mientras haces clic por ahí.
Mientras observas el código fuente de este ejemplo, presta atención a algunos elementos específicos:

Al hacer clic, tu computadora está generando eventos de bajo nivel que son entregados al
JVM y terminan en tu código para ser manejados por un escuchador. En Java, los escuchadores
son interfaces y puedes crear clases especiales solo para implementar la interfaz, o puedes
implementar los escuchadores como parte de la clase principal de tu aplicación, tal como lo
hicimos aquí. Dónde manejes los eventos realmente depende de las acciones que necesites
tomar en respuesta. Verás varios ejemplos de ambos enfoques a lo largo del resto de este libro.

Implementamos la interfaz MouseListener además de extender JFrame. Tuvimos que


proporcionar un cuerpo para cada método listado en MouseListener, pero realizamos nuestro
trabajo real en mouseClicked(). Puedes ver que tomamos las coordenadas del clic del objeto
evento y las usamos para cambiar la posición de nuestra etiqueta.

La clase MouseEvent contiene una gran cantidad de información sobre el evento. Cuándo
ocurrió, en qué componente ocurrió, qué botón del mouse estuvo involucrado, las coordenadas
(x, y) donde ocurrió el evento, etc. Intenta imprimir algo de esa información en algunos de los
métodos no implementados, como mouseDown().

Puede que hayas notado que agregamos bastantes métodos para otros tipos de eventos de
ratón que no utilizamos. Esto es común con eventos de nivel inferior, como eventos de mouse y
teclado. Las interfaces de escuchadores están diseñadas para brindarte un punto central para
obtener muchos eventos relacionados. Simplemente responde a los eventos particulares que te
interesan y deja los otros métodos vacíos.

Otro fragmento de código nuevo crucial es la llamada a addMouseListener() para nuestro


panel de contenido. La sintaxis puede parecer un poco extraña, pero es un enfoque válido. El uso
de getContentPane() a la izquierda dice "aquí es donde se generarán los eventos", y el uso de this
como argumento dice "aquí es donde se entregarán los eventos". Para nuestro ejemplo, los
eventos del panel de contenido del marco serán entregados de nuevo a la misma clase, que es
donde colocamos todo el código de manejo del ratón.

Adaptadores de mouse

Si queremos probar el enfoque de la clase auxiliar, podríamos agregar otra clase separada a nuestro
archivo e implementar MouseListener en esa clase. Pero si vamos a crear una clase separada, podemos
aprovechar un atajo que Swing proporciona para muchos escuchadores. La clase MouseAdapter es una
implementación simple y vacía de la interfaz MouseListener con métodos vacíos escritos para cada tipo
de evento. Al extender esta clase, tienes la libertad de anular solo los métodos que te interesan. Esto
puede facilitar un manejador más limpio.
Lo importante de recordar sobre las clases auxiliares es que necesitan tener una referencia a cada objeto
con el que interactuarán. Puedes ver que pasamos nuestra etiqueta al constructor. Esa es una forma
popular de establecer las conexiones necesarias, pero ciertamente podrías agregar el acceso requerido
más tarde, siempre y cuando el manejador pueda comunicarse con cada objeto que necesita antes de
comenzar a recibir eventos.

Eventos de Acción

Si bien los eventos de ratón y teclado están disponibles en casi todos los componentes de Swing, pueden
resultar un poco tediosos. La mayoría de las bibliotecas de interfaz de usuario proporcionan eventos de
nivel superior que son más simples de manejar. Swing no es una excepción. La clase JButton, por ejemplo,
admite un ActionEvent que te informa que se ha hecho clic en el botón. La mayor parte del tiempo, esto
es exactamente lo que deseas. Pero los eventos de ratón aún están disponibles si necesitas algún
comportamiento especial, como reaccionar a clics de diferentes botones del ratón, o distinguir entre un
clic largo y uno corto en una pantalla táctil.

Una forma popular de demostrar el evento de clic en el botón es construir un contador simple como el
que ves en la Figura 10-29. Cada vez que haces clic en el botón, actualizamos la etiqueta. Este sencillo
prototipo demuestra que estás listo para agregar muchos botones con múltiples respuestas. Veamos la
conexión necesaria para esta demostración:

No está mal. Actualizamos una variable de contador simple y mostramos el resultado dentro del método
actionPerformed(), que es donde los objetos ActionListener reciben sus eventos. Utilizamos el enfoque
de implementación directa del escuchador, pero igualmente podríamos haber creado una clase auxiliar
como hicimos con el primer ejemplo en "Eventos de ratón" en la página 319.
Los eventos de acción son directos; no tienen tantos detalles disponibles como los eventos de ratón, pero
llevan consigo una propiedad "comando". Esta propiedad puede ser personalizada, pero para los botones,
el valor por defecto es pasar el texto de la etiqueta del botón. La clase JTextField también genera un
evento de acción si presionas la tecla de retorno mientras escribes en el campo de texto. En este caso, el
comando pasado sería el texto actualmente presente en el campo.

La Figura 10-30 muestra una pequeña demostración que conecta un botón y un campo de texto a una
etiqueta.
Observa algo muy interesante sobre este código: usamos un objeto ActionListener para manejar los
eventos tanto del botón como del campo de texto. Esta es una gran característica del enfoque de
escuchadores que utiliza Swing para manejar eventos. Cualquier componente que genere un determinado
tipo de evento puede informar a cualquier escuchador que reciba ese tipo. A veces, los manejadores de
eventos son únicos y construirás un manejador separado para cada componente. Pero muchas
aplicaciones ofrecen múltiples formas de lograr la misma tarea. A menudo puedes manejar esas entradas
diferentes con un solo escuchador. Y cuanto menos código tengas, ¡menos cosas pueden salir mal!

Eventos de Cambio

Otro tipo de evento que aparece en varios componentes de Swing es el ChangeEvent. Este es un evento
simple que principalmente te informa sobre algo que cambió. La clase JSlider utiliza solo este mecanismo
para informar cambios en la posición del deslizador. La clase ChangeEvent tiene una referencia al
componente que cambió (el origen del evento) pero no tiene detalles sobre qué podría haber cambiado
dentro de ese componente. Depende de ti solicitar esos detalles al componente. Este proceso de escuchar
y luego consultar puede parecer tedioso, pero permite notificaciones eficientes de que se necesitan
actualizaciones sin crear cientos de clases con miles de métodos para cubrir todas las variaciones de
eventos que puedan surgir.

No vamos a reproducir toda la aplicación aquí, pero echemos un vistazo a cómo el juego de lanzamiento
de manzanas usa ChangeListener para mapear el deslizador de apuntado a nuestro físico:

En este fragmento, utilizamos un patrón de diseño de fábrica para crear nuestro deslizador y devolverlo
para su uso en el método add() de nuestro contenedor gamePane. Puedes ver que creamos una simple
clase interna anónima. Cambiar nuestro deslizador de apuntado tiene un solo efecto, y solo hay una
manera de apuntar la manzana. Dado que no existe la posibilidad de reutilización de clase, la clase interna
anónima es muy eficiente. No hay nada de malo en crear una clase auxiliar completa y pasarle los
elementos player1 y field como argumentos a un constructor o método de inicialización, pero encontrarás
que el enfoque utilizado arriba es bastante común en la práctica. Si bien puede parecer un poco extraño
al principio, después de familiarizarte con el patrón, se vuelve fácil. Se autodocumenta y puedes confiar
en que no hay efectos secundarios ocultos. Para los programadores, "lo que ves es lo que obtienes" es
una situación maravillosa.

Nuestro widget no es realmente adecuado para la prueba y error de eventos en jshell. Si bien ciertamente
puedes escribir código como el ChangeListener interno anónimo anterior en una línea de comandos,
puede ser tedioso y propenso a errores, los cuales no son fáciles de corregir desde esa misma línea de
comandos. Por lo general, es más sencillo escribir aplicaciones de demostración pequeñas y enfocadas. Si
bien te animamos a iniciar el juego de lanzamiento de manzanas para jugar con el deslizador mostrado
en el código anterior, también deberías intentar crear tus propias aplicaciones originales.

Otros eventos

Existen docenas de otros eventos y escuchadores distribuidos en los paquetes java.awt.event y


javax.swing.event. Vale la pena echar un vistazo a la documentación solo para tener una idea de los otros
tipos de eventos con los que podrías encontrarte. La Tabla 10-2 muestra los eventos y escuchadores
asociados con los componentes que hemos discutido hasta ahora en este capítulo, así como algunos que
vale la pena revisar mientras trabajas más con Swing. Nuevamente, esta no es una lista exhaustiva, pero
debería ayudarte a trabajar con estos componentes básicos y dejarte seguro para explorar otros
componentes y sus eventos.

Si no estás seguro de qué eventos admite un componente en particular, verifica su documentación en


busca de métodos que se parezcan a addXYZListener(). Ese tipo "XYZ" te dará una pista directa sobre
dónde buscar en la documentación. Una vez que tengas la documentación para el escuchador, intenta
implementar cada método e imprimir simplemente qué evento se informó. Es un poco de prueba y error,
pero puedes aprender mucho sobre cómo reaccionan los diversos componentes de Swing a los eventos
de teclado y ratón de esta manera.

Modales y ventanas emergentes

Los eventos permiten que el usuario llame tu atención, o al menos la atención de algún método en tu
aplicación. Pero, ¿qué sucede si necesitas captar la atención del usuario? Un mecanismo popular para
esta tarea en las interfaces de usuario es la ventana emergente. A menudo escucharás que a esta ventana
se le llama "modal" o "diálogo" o incluso "diálogo modal". El uso de "diálogo" proviene del hecho de que
estas ventanas emergentes presentan cierta información al usuario y esperan o requieren una respuesta.
Quizás no tan elevado como un simposio socrático, pero aún así. El término "modal" hace referencia al
hecho de que algunos de esos diálogos que requieren una respuesta realmente deshabilitarán el resto de
la aplicación hasta que hayas proporcionado esa respuesta. Es posible que hayas experimentado dicho
diálogo en otras aplicaciones de escritorio. Si tu software te exige mantenerlo actualizado con la última
versión, por ejemplo, podría "desactivar" la aplicación indicando que no puedes usarla y luego mostrarte
un diálogo modal con un botón que inicia el proceso de actualización. La aplicación te ha obligado a un
modo restringido hasta que indiques cómo proceder.

Un "ventana emergente" es un término más general. Si bien puedes tener ventanas emergentes modales,
también puedes tener ventanas emergentes simples (o "sin modalidad", aunque el uso de esa definición
técnica está desapareciendo) que no te bloquean el uso del resto de la aplicación. Piensa en un cuadro de
diálogo de búsqueda que puedas dejar disponible y simplemente mover hacia un lado de tu documento
principal de procesamiento de texto.
Diálogos de mensajes

Swing proporciona una clase JDialog básica que se puede utilizar para crear ventanas de diálogo
personalizadas, pero para interacciones de diálogo típicas con tus usuarios, la clase JOptionPane tiene
algunos atajos realmente útiles.

Quizás la ventana emergente más molesta sea el diálogo de "algo falló" que te avisa (de manera vaga)
que la aplicación no está funcionando como se esperaba. Esta ventana emergente muestra al usuario un
mensaje breve y un botón de OK que se puede hacer clic para deshacerse del diálogo. El propósito de este
diálogo es mantener detenida la operación del programa hasta que el usuario reconozca que ha visto el
mensaje. La Figura 10-31 muestra un ejemplo básico de cómo presentar un cuadro de diálogo de mensaje
en respuesta al clic de un botón.
Espero que reconozcas el código que conecta nuestro botón goButton con este escuchador. Es el mismo
patrón que usamos con nuestro primer ActionEvent. Lo nuevo es lo que hacemos con ese evento.
Mostramos nuestro cuadro de diálogo de mensaje y luego actualizamos nuestra etiqueta para indicar que
presentamos el diálogo correctamente.

La llamada showMessageDialog() toma cuatro argumentos. El argumento this que ves en la primera
posición es el marco o ventana "propietaria" de la ventana emergente; la alerta intentará centrarse sobre
su propietario cuando se muestre. Especificamos nuestra aplicación en sí misma como el propietario. El
segundo y tercer argumentos son cadenas de texto para el mensaje y el título del diálogo,
respectivamente. El argumento final indica el "tipo" de ventana emergente, que afecta principalmente al
icono que se muestra. Puedes especificar varios tipos:

• ERROR_MESSAGE, ícono rojo de detener


• INFORMATION_MESSAGE, ícono de Duke6
• WARNING_MESSAGE, ícono de triángulo amarillo
• QUESTION_MESSAGE, ícono de Duke
• PLAIN_MESSAGE, sin ícono

Si quieres experimentar con estas ventanas emergentes, puedes regresar a tu jshell. Podemos usar
nuestro objeto Widget como el propietario, o puedes emplear la opción práctica de usar null para indicar
que no hay un marco o ventana en particular a cargo, pero que la ventana emergente debería pausar toda
la aplicación y mostrarse en el centro de tu pantalla, así:
Es posible que tengas que ejecutar el ModalDemo varias veces, pero observa el texto en nuestro objeto
modalLabel. Notarás que solo cambia después de que cierres la ventana emergente. Es importante
recordar que estos diálogos modales detienen el flujo normal de tu aplicación. Eso es exactamente lo que
quieres para condiciones de error o cuando se requiere alguna entrada del usuario, pero puede que no
sea lo que deseas para actualizaciones simples de estado.

Quizás puedas imaginar otras situaciones más valiosas para tales alertas. O si te encuentras con la
situación de "algo falló" en tu aplicación, esperamos que puedas proporcionar un mensaje de error útil
que ayude al usuario a solucionar lo que salió mal. ¿Recuerdas la expresión regular para validar correos
electrónicos de "Pattern" en la página 239? Podrías adjuntar un ActionListener a un campo de texto y,
cuando el usuario presione Enter, mostrar un cuadro de diálogo de error si el contenido del campo no
parece una dirección de correo electrónico.

Diálogos de confirmación

Otra tarea común para las ventanas emergentes es verificar la intención del usuario. Muchas aplicaciones
preguntan si estás seguro de que quieres salir, eliminar algo o realizar alguna otra acción aparentemente
irreversible, como chasquear los dedos mientras usas un guantelete incrustado con las Gemas del Infinito.
JOptionPane tiene lo que necesitas. Podemos probar este nuevo diálogo en jshell de la siguiente manera:

Lo que debería producir es una ventana emergente con los botones Sí, No y Cancelar, como se muestra
en la Figura 10-33. Puedes determinar qué respuesta seleccionó el usuario al conservar el valor de retorno
(un entero) de la llamada al método showConfirmDialog(). (Al ejecutar este ejemplo mientras escribíamos
este capítulo, hicimos clic en el botón Sí. Ese es el valor de retorno 0 que se muestra en el fragmento de
jshell anterior). Entonces, modifiquemos nuestra llamada para capturar esa respuesta (volveremos a
hacer clic en Sí):
Existen otros diálogos de confirmación estándar que se pueden mostrar con un par adicional de
argumentos: un String de título para mostrar en el diálogo y uno de los siguientes tipos de opciones:

• YES_NO_OPTION
• YES_NO_CANCEL_OPTION
• OK_CANCEL_OPTION

Es posible que notes que nuestro ejemplo no especificó los argumentos adicionales, por lo que obtuvimos
el título predeterminado "Select an Option" y los botones dictados por la constante de tipo
YES_NO_CANCEL_OPTION. En la mayoría de las situaciones, tener tanto una opción "No" como "Cancelar"
puede resultar confuso para los usuarios. Recomendamos usar una opción como "Sí No", o "Aceptar
Cancelar", o solo "Aceptar", pero no "Sí No Cancelar". El usuario siempre puede cerrar el diálogo utilizando
el control de ventana "x" estándar sin hacer clic en ninguno de los botones proporcionados. Puedes
detectar esa acción de cierre mediante la comprobación de JOption Pane.CLOSED_OPTION en el
resultado.

No lo cubriremos aquí, pero puedes utilizar el método showOptionDialog() si necesitas crear algo similar
a los diálogos de confirmación anteriores, pero deseas usar un conjunto personalizado de botones. Como
siempre, ¡la documentación de JDK es tu amiga!

Diálogos de entrada

Por último, pero no menos importante en el mundo de las ventanas emergentes, están las ventanas que
solicitan una rápida entrada arbitraria. Puedes usar el método showInputDialog() para hacer una pregunta
y permitir que el usuario escriba una respuesta. Esa respuesta (un String) se puede almacenar de manera
similar a cómo conservas la elección de confirmación. Añadamos un botón más que produzca una ventana
emergente en nuestra demostración, como se muestra en la Figura 10-34.

La utilización de ventanas modales debería reservarse para solicitudes puntuales, no es recomendable si


tienes una serie de preguntas para hacerle al usuario. Mantén las ventanas modales limitadas a tareas
rápidas. Interrumpen al usuario y, aunque a veces eso es exactamente lo que necesitas, si abusas de esa
atención, es probable que molestes al usuario y que este simplemente ignore todas las ventanas
emergentes de tu aplicación.
Consideraciones de hilos

Si has leído alguna parte de la documentación de JDK sobre Swing mientras trabajabas en este capítulo,
es posible que hayas encontrado la advertencia de que los componentes de Swing no son seguros para
hilos (thread safe). Si recuerdas del Capítulo 9, Java admite múltiples hilos de ejecución para aprovechar
la potencia de procesamiento de las computadoras modernas. Una de las preocupaciones acerca de las
aplicaciones con múltiples hilos es que dos hilos podrían pelearse por el mismo recurso o actualizar la
misma variable al mismo tiempo pero con valores diferentes. No saber si tus datos son correctos puede
afectar gravemente tu capacidad para depurar un programa o incluso para confiar en su salida. En el caso
de los componentes de Swing, esta advertencia te recuerda que tus elementos de interfaz de usuario
están sujetos a este tipo de corrupción.

Para ayudar a mantener una interfaz de usuario consistente, Swing te anima a actualizar tus componentes
en el hilo de despacho de eventos de AWT (AWT event dispatch thread). Este es el hilo que maneja
naturalmente cosas como clics de botones. Si actualizas un componente en respuesta a un evento (como
nuestro ejemplo del contador en "Eventos de acción" en la página 322 arriba), estás bien. La idea es que
si cada otro hilo en tu aplicación envía actualizaciones de la interfaz de usuario al hilo de despacho de
eventos, ningún componente puede verse afectado adversamente por cambios simultáneos y
posiblemente conflictivos.

Un ejemplo común en el que el enhebrado está en primer plano en las aplicaciones gráficas es la "tarea
de larga duración". Piensa en descargar un archivo desde la nube mientras un indicador animado está en
tu pantalla, con suerte manteniéndote entretenido. Pero, ¿qué sucede si te impacientas? ¿Y si parece que
la descarga ha fallado pero el indicador sigue funcionando? Si tu tarea de larga duración está usando el
hilo de despacho de eventos, tu usuario no podrá hacer clic en un botón Cancelar ni tomar ninguna acción.
Las tareas de larga duración deben ser manejadas por hilos separados que puedan ejecutarse en segundo
plano, dejando tu aplicación receptiva y disponible. Pero, ¿cómo actualizamos la interfaz de usuario
cuando ese hilo en segundo plano finaliza? Swing tiene un ayudante para esa tarea.

SwingUtilities y actualizaciones de componentes

Puedes usar la clase SwingUtilities desde cualquier hilo para realizar actualizaciones en tus componentes
de interfaz de usuario de manera segura y estable. Hay dos métodos estáticos que puedes usar para
comunicarte con tu interfaz de usuario:

• invokeAndWait()
• invokeLater()
Como sus nombres indican, el primer método ejecuta algún código de actualización de la interfaz de
usuario y hace que el hilo actual espere a que ese código termine antes de continuar. El segundo método
pasa algún código de actualización de la interfaz de usuario al hilo de despacho de eventos y luego
reanuda inmediatamente la ejecución en el hilo actual. Cuál utilizar depende realmente de si tu hilo en
segundo plano necesita conocer el estado de la interfaz de usuario antes de continuar. Por ejemplo, si
estás agregando un nuevo botón a tu interfaz, es posible que desees usar invokeAndWait() para que
cuando tu hilo en segundo plano continúe, pueda estar seguro de que habrá un botón para actualizar en
futuras actualizaciones.

Si no te preocupa tanto cuándo se actualiza algo, solo que finalmente sea manejado de manera segura
por el hilo de despacho de eventos, invokeLater() es perfecto. Piensa en la actualización de una barra de
progreso a medida que se descarga un archivo grande. Es posible que envíes varias actualizaciones con
más y más de la descarga completada. No necesitas esperar a que esas actualizaciones gráficas terminen
antes de reanudar tu descarga. Si una actualización de progreso se retrasa o se ejecuta muy cerca de una
segunda actualización, no hay un daño real. Pero no quieres que una interfaz gráfica ocupada interrumpa
tu descarga, especialmente si el servidor es sensible a pausas.

Veremos varios ejemplos de exactamente este tipo de interacción entre red/UI en el próximo capítulo,
pero veamos cómo simular tráfico de red y actualizar una pequeña etiqueta para mostrar Swing Utilities.
Podemos configurar un botón de inicio que actualizará una etiqueta de estado con una pantalla de
porcentaje simple y comenzará un hilo en segundo plano que simplemente duerme durante un segundo,
luego incrementa el progreso. Cada vez que se despierte el hilo, actualizará la etiqueta utilizando
invokeLater() para establecer correctamente el texto de la etiqueta. Primero, veamos cómo configurar
nuestra demostración:

Espero que la mayor parte de esto te resulte familiar, pero queremos señalar algunos detalles
interesantes. En primer lugar, observa cómo creamos nuestro hilo. Pasamos una nueva llamada a
ProgressPretender como argumento al constructor de nuestro Thread. Podríamos haber dividido eso en
partes separadas, pero como no nos referimos a nuestro objeto ProgressPretender nuevamente,
podemos seguir con este enfoque más limpio y denso. Sin embargo, nos referimos al hilo en sí, por lo que
creamos una variable adecuada para él. Luego podemos iniciar nuestro hilo en el ActionListener de
nuestro botón. ¡Observa en este ActionListener que deshabilitamos nuestro botón de inicio! ¡No
queremos que el usuario intente iniciar un hilo que ya está en ejecución!

Otra cosa que queremos señalar es que agregamos un campo de texto para que puedas escribir. Mientras
se actualiza el progreso, tu aplicación debería seguir respondiendo a la entrada del usuario, como escribir.
¡Pruébalo! El campo de texto no está conectado a nada, por supuesto, pero deberías poder ingresar y
eliminar texto mientras observas cómo el contador de progreso sube lentamente, como se muestra en la
Figura 10-35.

Así que, ¿cómo actualizamos esa etiqueta sin bloquear la aplicación? Vamos a ver la clase
ProgressPretender e inspeccionar el método run():
En esta clase, almacenamos la etiqueta pasada a nuestro constructor para saber dónde mostrar nuestro
progreso actualizado. El método run() consta de tres pasos básicos: 1) actualizar la etiqueta, 2) dormir
durante 1000 milisegundos y 3) incrementar nuestro progreso.

Para el paso 1, observa el argumento bastante complejo que pasamos a invokeLater(). Se parece mucho
a una definición de clase, pero se basa en la interfaz Runnable que vimos en el Capítulo 9. Este es otro
ejemplo de uso de clases internas anónimas en Java. Hay otras formas de crear el objeto Runnable, pero
al igual que manejar eventos simples con escuchadores anónimos, este patrón de hilo es muy común. Este
argumento Runnable anidado actualiza la etiqueta con nuestro valor de progreso actual, pero
nuevamente, realiza esta actualización en el hilo de despacho de eventos. Esta es la magia que hace que
el campo de texto responda aunque nuestro hilo de "progreso" esté durmiendo la mayor parte del tiempo.

El paso 2 es un simple proceso de dormir del hilo. Recuerda que el método sleep() sabe que puede ser
interrumpido, así que el compilador se asegurará de que proporciones un bloque try/catch como hemos
hecho anteriormente. Hay muchas formas en las que podríamos manejar la interrupción, pero en este
caso elegimos simplemente salir del bucle.

Finalmente, incrementamos nuestro contador de progreso y comenzamos todo el proceso nuevamente.


Una vez que alcanzamos 100, el bucle termina y nuestra etiqueta de progreso debería dejar de cambiar.
Si esperas pacientemente, verás ese valor final. Sin embargo, la aplicación en sí debería seguir activa.
Todavía puedes escribir en el campo de texto. Nuestra descarga está completa y todo está bien en el
mundo.

Temporizadores

La biblioteca Swing también incluye un temporizador diseñado para trabajar en el espacio de la interfaz
de usuario. La clase javax.swing.Timer es bastante sencilla. Espera un período de tiempo especificado y
luego dispara un evento de acción. Puede activar esa acción una vez o repetidamente. Hay muchas
razones para usar temporizadores con aplicaciones gráficas. Además de un ciclo de animación, es posible
que desees cancelar automáticamente alguna acción, como cargar un recurso de red si está tardando
demasiado. O, por el contrario, podrías mostrar un pequeño icono de "espere, por favor" o un mensaje
para informar al usuario que la operación está en curso. Es posible que desees cerrar un diálogo modal si
el usuario no responde dentro de un período de tiempo especificado. En todos estos casos, los
temporizadores simples son geniales. El temporizador de Swing puede manejar todos ellos.

Animación con Timer

Volvamos a nuestra animación de manzanas voladoras de "Revisitando la animación con hilos" en la


página 264 e intentemos implementarla con una instancia de Timer. De hecho, pasamos por alto el uso
de un método de utilidad correcto como invokeLater() para repintar de manera segura el juego al usar
hilos estándar. La clase Timer se encarga de ese detalle por nosotros. Y afortunadamente, todavía
podemos usar nuestro método step() en la clase Apple de nuestro primer intento de animación.
Simplemente necesitamos alterar el método start y mantener una variable adecuada para el
temporizador:

Hay dos aspectos positivos sobre este enfoque. Es definitivamente más fácil de leer porque no somos
responsables de los intervalos de pausa entre acciones. Creamos el temporizador pasando al constructor
el intervalo de tiempo entre eventos y un ActionListener para recibir los eventos, en nuestro caso, nuestra
clase Field. Le damos al temporizador un buen comando de acción, lo convertimos en un temporizador
repetitivo ¡y lo iniciamos! Como señalamos como parte de la motivación para mirar los temporizadores,
la otra cosa buena es específica de Swing y las aplicaciones gráficas: javax.swing.Timer dispara sus eventos
de acción en el hilo de despacho de eventos. No es necesario envolver nada en invokeAndWait() o
invokeLater(). Simplemente coloca tu código basado en el tiempo en el método actionPerformed() de un
escuchador adjunto y listo.

Debido a que varios componentes generan objetos ActionEvent como hemos visto, tomamos una
pequeña precaución contra colisiones configurando el atributo actionCommand para nuestro
temporizador. Este paso no es estrictamente necesario en nuestro caso, pero deja espacio para que la
clase Field maneje otros eventos en el futuro sin romper nuestra animación.
Otras aplicaciones del Timer

Como se mencionó al principio de esta sección, las aplicaciones maduras y pulidas tienen una variedad de
momentos pequeños donde ayuda tener un temporizador de una sola vez. Nuestro juego de manzanas
es simple en comparación con la mayoría de las aplicaciones comerciales o juegos, pero incluso aquí
podemos agregar un poco de "realismo" con un temporizador: después de lanzar una manzana,
podríamos hacer que el físico se detenga antes de poder lanzar otra manzana. El físico tiene que agacharse
y tomar otra manzana de un cubo antes de apuntar o lanzar. Este tipo de retraso es otro punto perfecto
para un Timer.

Podemos agregar tal pausa al fragmento de código en la clase Field donde lanzamos la manzana:

Observa esta vez que configuramos el temporizador para ejecutarse solo una vez con la llamada a
setRepeats(false). Esto significa que después de un poco menos de un segundo, se disparará un solo
evento a nuestro físico. La clase Physicist, a su vez, necesita agregar la parte implements ActionListener a
la definición de la clase e incluir una función actionPerformed() apropiada, de la siguiente manera:

Nuevamente, el uso de Timer no es la única forma de llevar a cabo estas tareas, pero en Swing, la
combinación de eventos eficientes basados en el tiempo y el uso automático del hilo de despacho de
eventos lo hace digno de consideración. Si nada más, es una excelente manera de hacer prototipos. Si es
necesario, siempre puedes regresar y refactorizar tu aplicación para utilizar un código de hilos
personalizado.
Próximos Pasos

Como mencionamos al principio del capítulo, hay muchas más discusiones, temas y exploraciones
disponibles en el mundo de las aplicaciones gráficas de Java. Dejaremos que explores eso por tu cuenta,
pero queríamos pasar por al menos algunos temas clave que vale la pena enfocar primero si tienes planes
para una aplicación de escritorio.

Menús

Aunque no son técnicamente necesarios, la mayoría de las aplicaciones de escritorio tienen un menú de
tareas comunes en toda la aplicación, como guardar archivos modificados o configurar preferencias, y
funciones específicas como las aplicaciones de hojas de cálculo que permiten ordenar los datos en una
columna o seleccionarlos. Las clases JMenu, JMenuBar y JMenuItem te ayudan a agregar esta
funcionalidad a tus aplicaciones Swing. Los menús van dentro de una barra de menú y los elementos del
menú van dentro de los menús.

Swing tiene tres clases predefinidas de elementos de menú: JMenuItem para entradas de menú básicas,
JCheckBoxMenuItem para elementos de opción y JRadioButtonMenuItem para elementos de menú
agrupados, como los que podrías ver para la fuente seleccionada actualmente o el tema de color. La clase
JMenu es ella misma un elemento de menú válido para que puedas construir menús anidados. JMenuItem
se comporta como un botón (al igual que sus compañeros de elementos de menú) y puedes capturar
eventos de menú usando los mismos escuchadores.

La Figura 10-36 muestra un ejemplo de una barra de menú simple poblada con algunos menús y
elementos.

Observa que la aplicación de macOS difiere ligeramente de la versión de Linux. Swing (y Java) aún reflejan
muchos aspectos de los entornos nativos en los que se ejecutan. Aunque una discrepancia notable aquí
es que las aplicaciones de macOS suelen utilizar una barra de menú global en la parte superior de su
pantalla principal. Puedes realizar acciones específicas de la plataforma, como utilizar el menú de macOS
o establecer iconos de aplicación, a medida que te sientas más cómodo programando y desees comenzar
a compartir tu código o distribuir tu aplicación a otros. Pero por ahora, nos conformaremos con el menú
de macOS local a la ventana de la aplicación.
Obviamente, no estamos realizando muchas acciones con los elementos del menú aquí, pero queremos
mostrar cómo puedes empezar a desarrollar las partes esperadas de una aplicación profesional.
Preferencias

La API de Preferencias de Java acomoda la necesidad de almacenar datos de configuración tanto del
sistema como del usuario de manera persistente entre ejecuciones de la Máquina Virtual de Java. La API
de Preferencias es como una versión portátil del registro de Windows, una mini base de datos en la que
puedes guardar pequeñas cantidades de información, accesibles para todas las aplicaciones. Las entradas
se almacenan como pares de nombre/valor, donde los valores pueden ser de varios tipos estándar,
incluyendo cadenas, números, booleanos e incluso matrices de bytes cortas (recuerda que mencionamos
pequeñas cantidades de datos). A medida que construyas aplicaciones de escritorio más interesantes,
seguramente encontrarás elementos que tus usuarios puedan personalizar. La API de Preferencias es una
excelente manera de mantener esa información disponible en una forma multiplataforma que es fácil de
usar y mejorará la experiencia del usuario.

Puedes obtener más información de Oracle en su nota técnica sobre Preferencias.

Componentes personalizados y Java2D

Tocamos brevemente la creación de componentes personalizados con nuestro juego y su clase Field.
Proporcionamos un método paintComponent() personalizado para dibujar nuestras manzanas, árboles y
físicos. Esto es un comienzo, pero puedes agregar mucha (mucha) más funcionalidad. Puedes tomar
eventos de mouse y teclado de bajo nivel y mapearlos en interfaces visuales más elegantes. Puedes
generar tus propios eventos personalizados. Puedes construir tu propio administrador de diseño. ¡Incluso
puedes crear un aspecto y sensación completos que afecten a cada componente en la biblioteca Swing!
Esta asombrosa capacidad de extensión requiere un conocimiento bastante profundo de Swing y Java,
pero está allí esperándote.

En el ámbito del dibujo, puedes explorar la API Java 2D (consulta la descripción general en línea de Oracle).
Esta API proporciona varias mejoras agradables a las capacidades de dibujo e imagen en el paquete AWT.
Si tienes interés en las capacidades gráficas 2D de Java, asegúrate de consultar "Java 2D Graphics" de
Jonathan Knudsen. Y nuevamente, "Java Swing, 2nd Edition" de Loy et al., es un recurso profundo para
todo lo relacionado con Swing.

JavaFX

Otra API que deberías considerar es JavaFX. Esta colección de paquetes fue originalmente diseñada para
reemplazar a Swing e incluye opciones de multimedia como video y audio de alta fidelidad. Es lo
suficientemente diferente de Swing que ambas bibliotecas siguen formando parte del JDK y no parece
haber planes reales para deprecar o eliminar Swing. A partir de Java 11, recuerda que esta es la versión
actual de soporte a largo plazo, el proyecto OpenJDK ganó soporte para JavaFX en forma del proyecto
OpenJFX. Puedes encontrar más información en línea en https://openjfx.io.

Interfaz de Usuario y Experiencia de Usuario

Esta fue una rápida revisión de algunos de los elementos más comunes que utilizarás al crear una interfaz
de usuario para tus aplicaciones de escritorio. Hemos visto componentes como JButton, JLabel y JTextField
que probablemente estarán presentes en cualquier aplicación gráfica que hagas. Discutimos cómo
organizar esos componentes en contenedores y cómo crear combinaciones más complejas de
contenedores y componentes para manejar presentaciones más interesantes. Esperamos haber
introducido suficientes otros componentes para brindarte las herramientas necesarias para asegurarte de
que la experiencia de usuario de tu aplicación sea positiva.

En la actualidad, las aplicaciones de escritorio son solo una parte de la historia. Muchas aplicaciones
funcionan en línea en coordinación con otras aplicaciones. Los dos capítulos restantes cubrirán conceptos
básicos de redes e introducirán la capacidad de programación web de Java.
CAPÍTULO 11

Redes y E/S (Networking and I/O)

En este capítulo, continuamos nuestra exploración de la API de Java al examinar muchas de las clases en
los paquetes java.io y java.nio. Estos paquetes ofrecen un conjunto amplio de herramientas para la E/S
básica (entrada/salida) y también proporcionan el marco sobre el cual se construye toda la comunicación
de archivos y redes en Java. La Figura 11-1 muestra la jerarquía de clases de estos paquetes. Solo
cubriremos una selección de esta jerarquía, pero se puede observar que es bastante amplia. Una vez que
tengas un manejo de la E/S de archivos locales, agregaremos el paquete java.net y exploraremos algunos
conceptos básicos de redes. (Abordaremos el entorno de redes más popular, la web, en el Capítulo 12).

Comenzaremos examinando las clases de flujos en java.io, las cuales son subclases de las clases básicas
InputStream, OutputStream, Reader y Writer. Luego examinaremos la clase File y discutiremos cómo
puedes leer y escribir archivos utilizando las clases en java.io. También echaremos un vistazo rápido a la
compresión de datos y a la serialización. En el camino, también introduciremos el paquete java.nio. El
paquete NIO (o "nueva" E/S) añade funcionalidades significativas diseñadas para construir servicios de
alto rendimiento y, en algunos casos, simplemente proporciona APIs más nuevas y mejores que pueden
ser usadas en lugar de algunas características de java.io.

Flujos

La E/S más fundamental en Java se basa en flujos. Un flujo representa un flujo de datos con (al menos
conceptualmente) un escritor en un extremo y un lector en el otro. Cuando trabajas con el paquete java.io
para realizar entrada y salida en la terminal, leer o escribir archivos, o comunicarte a través de sockets en
Java, estás utilizando varios tipos de flujos. Más adelante en este capítulo, veremos el paquete NIO, que
introduce un concepto similar llamado canal. Una diferencia entre los dos es que los flujos están
orientados hacia bytes o caracteres, mientras que los canales están orientados hacia "buffers" que
contienen esos tipos de datos, aunque realizan aproximadamente el mismo trabajo. Comencemos
resumiendo los tipos disponibles de flujos:

InputStream, OutputStream

Clases abstractas que definen la funcionalidad básica para leer o escribir una secuencia no
estructurada de bytes. Todos los demás flujos de bytes en Java se construyen sobre InputStream
y OutputStream básicos.

Reader, Writer

Clases abstractas que definen la funcionalidad básica para leer o escribir una secuencia de datos
de caracteres, con soporte para Unicode. Todos los demás flujos de caracteres en Java se
construyen sobre Reader y Writer.

InputStreamReader, OutputStreamWriter

Clases que conectan flujos de bytes y caracteres mediante la conversión según un esquema
específico de codificación de caracteres. (Recuerda: en Unicode, un carácter no es
necesariamente un byte).
DataInputStream, DataOutputStream

Filtros de flujos especializados que añaden la capacidad de leer y escribir datos de varios bytes,
como primitivas numéricas y objetos String en un formato universal.

ObjectInputStream, ObjectOutputStream

Filtros de flujos especializados capaces de escribir grupos enteros de objetos Java serializados y
reconstruirlos.

BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter

Filtros de flujos especializados que añaden almacenamiento en búfer para mayor eficiencia. Para
la E/S del mundo real, casi siempre se utiliza un búfer.

PrintStream, PrintWriter

Flujos especializados que simplifican la impresión de texto.

PipedInputStream, PipedOutputStream, PipedReader, PipedWriter

Flujos de "retroalimentación" que se pueden utilizar en pares para mover datos dentro de una
aplicación. Los datos escritos en un PipedOutputStream o PipedWriter se leen desde su
correspondiente PipedInputStream o PipedReader.

FileInputStream, FileOutputStream, FileReader, FileWriter

Implementaciones de InputStream, OutputStream, Reader y Writer que leen y escriben en


archivos en el sistema de archivos local.

Los flujos en Java son calles de un solo sentido. Las clases de entrada y salida de java.io representan los
extremos de un flujo simple, como se muestra en la Figura 11-1. Para conversaciones bidireccionales,
utilizarás uno de cada tipo de flujo.

`InputStream` y `OutputStream` son clases abstractas que definen la interfaz de nivel más bajo para todos
los flujos de bytes. Contienen métodos para leer o escribir un flujo no estructurado de datos a nivel de
bytes. Debido a que estas clases son abstractas, no puedes crear un flujo de entrada o salida genérico.
Java implementa subclases de estas para actividades como la lectura desde y la escritura en archivos, así
como la comunicación con sockets. Debido a que todos los flujos de bytes heredan la estructura de
`InputStream` o `OutputStream`, los diferentes tipos de flujos de bytes se pueden usar de manera
intercambiable. Un método que especifica un `InputStream` como argumento puede aceptar cualquier
subclase de `InputStream`. También se pueden agregar tipos especializados de flujos o envolverlos
alrededor de flujos básicos para agregar características como almacenamiento en búfer, filtrado o manejo
de tipos de datos de nivel superior.

`Reader` y `Writer` son muy similares a `InputStream` y `OutputStream`, excepto que trabajan con
caracteres en lugar de bytes. Como verdaderos flujos de caracteres, estas clases manejan correctamente
los caracteres Unicode, lo cual no siempre es el caso con flujos de bytes. A menudo, se necesita un puente
entre estos flujos de caracteres y los flujos de bytes de dispositivos físicos, como discos y redes.
`InputStreamReader` y `OutputStreamWriter` son clases especiales que utilizan un esquema de
codificación de caracteres para traducir entre flujos de caracteres y de bytes.

Esta sección describe todos los tipos de flujos interesantes con la excepción de `FileInputStream`,
`FileOutputStream`, `FileReader` y `FileWriter`. Postergamos la discusión de flujos de archivos hasta la
próxima sección, donde cubriremos los problemas relacionados con el acceso al sistema de archivos en
Java.

E/S Básica (Basic I/O)

El ejemplo prototípico de un objeto `InputStream` es la entrada estándar de una aplicación Java. Al igual
que `stdin` en C o `cin` en C++, esta es la fuente de entrada para un programa de línea de comandos (no
GUI). Es un flujo de entrada desde el entorno, generalmente una ventana de terminal o posiblemente la
salida de otro comando. La salida estándar es un flujo que generalmente se reserva para mensajes de
texto relacionados con errores que deben mostrarse al usuario de una aplicación de línea de comandos.
Se diferencia de la salida estándar, que a menudo podría ser redirigida a un archivo u otra aplicación y no
ser vista por el usuario.

La clase `java.lang.System`, un repositorio general de recursos relacionados con el sistema, proporciona


una referencia al flujo de entrada estándar en la variable estática `System.in`. También proporciona un
flujo de salida estándar y un flujo de error estándar en las variables `out` y `err`, respectivamente.2 El
siguiente ejemplo muestra la correspondencia:

Este fragmento oculta el hecho de que `System.out` y `System.err` no son simplemente objetos
`OutputStream`, sino objetos `PrintStream` más especializados y útiles. Explicaremos esto más adelante
en "PrintWriter y PrintStream" en la página 352, pero por ahora podemos hacer referencia a `out` y `err`
como objetos `OutputStream` porque derivan de `OutputStream`.

Podemos leer un solo byte a la vez desde la entrada estándar con el método `read()` de `InputStream`. Si
observas detenidamente la API, verás que el método `read()` de la clase base `InputStream` es un método
abstracto. Lo que se encuentra detrás de `System.in` es una implementación particular de `InputStream`
que proporciona la implementación real del método `read()`:

Aunque hemos mencionado que el método `read()` lee un valor de byte, el tipo de retorno en el ejemplo
es `int`, no `byte`. Esto se debe a que el método `read()` de los flujos de entrada básicos en Java utiliza
una convención heredada del lenguaje C para indicar el final de un flujo con un valor especial. Los valores
de bytes de datos se devuelven como enteros sin signo en el rango de 0 a 255, y el valor especial de -1 se
utiliza para indicar que se ha alcanzado el final del flujo. Necesitarás comprobar esta condición al usar el
método `read()` simple. Luego puedes convertir el valor a un byte si es necesario. El siguiente ejemplo lee
cada byte de un flujo de entrada e imprime su valor:
Como hemos mostrado en los ejemplos, el método `read()` también puede lanzar una `IOException` si hay
un error al leer desde la fuente de flujo subyacente. Varias subclases de `IOException` pueden indicar que
una fuente como un archivo o una conexión de red ha tenido un error. Además, los flujos de nivel superior
que leen tipos de datos más complejos que un solo byte pueden arrojar `EOFException` ("fin de archivo"),
lo que indica un final inesperado o prematuro del flujo.

Una forma sobrecargada de `read()` llena una matriz de bytes con la mayor cantidad de datos posible
hasta la capacidad de la matriz y devuelve el número de bytes leídos:

En teoría, también podemos verificar la cantidad de bytes disponibles para leer en un momento dado en
un `InputStream` utilizando el método `available()`. Con esa información, podríamos crear una matriz del
tamaño exacto necesario:

Sin embargo, la fiabilidad de esta técnica depende de la capacidad de la implementación subyacente del
flujo para detectar cuántos datos se pueden recuperar. Generalmente funciona para archivos, pero no se
debe depender de ello para todos los tipos de flujos.

Estos métodos `read()` bloquean hasta que al menos se lee algún dato (al menos un byte). En general,
debes verificar el valor devuelto para determinar cuántos datos obtuviste y si necesitas leer más
(exploraremos la E/S no bloqueante más adelante en este capítulo). El método `skip()` de `InputStream`
proporciona una forma de saltar sobre una cantidad específica de bytes. Dependiendo de la
implementación del flujo, saltar bytes puede ser más eficiente que leerlos.

El método `close()` cierra el flujo y libera cualquier recurso del sistema asociado. Es importante cerrar la
mayoría de los tipos de flujos cuando hayas terminado de usarlos por razones de rendimiento. En algunos
casos, los flujos pueden cerrarse automáticamente cuando los objetos son recolectados por el recolector
de basura, pero no es recomendable confiar en este comportamiento. En Java 7, se introdujo la
funcionalidad de "try-with-resources" para facilitar el cierre automático de flujos y otras entidades
cerrables. Veremos algunos ejemplos de esto en "Flujos de archivos" en la página 358. La interfaz de
marcado `java.io.Closeable` identifica todos los tipos de flujos, canales y clases de utilidad relacionadas
que pueden ser cerradas.

Finalmente, cabe mencionar que además de las corrientes estándar `System.in` y `System.out`, Java
proporciona la API `java.io.Console` a través de `System.console()`. Puedes usar la consola para leer
contraseñas sin que se muestren en la pantalla.

Flujos de Caracteres

En las primeras versiones de Java, algunos tipos de `InputStream` y `OutputStream` incluían métodos para
leer y escribir cadenas, pero la mayoría operaba asumiendo ingenuamente que un carácter Unicode de
16 bits era equivalente a un byte de 8 bits en el flujo. Esto funcionaba solo para caracteres Latin-1 (ISO
8859-1) y no para el mundo de otras codificaciones que se utilizan con diferentes idiomas. En el Capítulo
8, vimos que la clase `java.lang.String` tiene un constructor de matriz de bytes y un método `getBytes()`
correspondiente, cada uno de los cuales acepta una codificación de caracteres como argumento. En
teoría, podríamos usar estos como herramientas para transformar matrices de bytes hacia y desde
caracteres Unicode para trabajar con flujos de bytes que representan datos de caracteres en cualquier
formato de codificación. Afortunadamente, sin embargo, no tenemos que depender de esto porque Java
tiene flujos que manejan esto por nosotros.

Las clases de flujo de caracteres `java.io.Reader` y `java.io.Writer` fueron introducidas como flujos que
manejan solo datos de caracteres. Cuando usas estas clases, piensas solo en términos de caracteres y
datos de cadena, y permites que la implementación subyacente maneje la conversión de bytes a una
codificación de caracteres específica. Como veremos, existen algunas implementaciones directas de
`Reader` y `Writer`, por ejemplo, para leer y escribir archivos. Pero más generalmente, dos clases
especiales, `InputStreamReader` y `OutputStreamWriter`, actúan como un puente entre el mundo de los
flujos de caracteres y el mundo de los flujos de bytes. Estas son, respectivamente, un `Reader` y un
`Writer` que pueden envolverse alrededor de cualquier flujo de bytes subyacente para convertirlo en un
flujo de caracteres. Se utiliza un esquema de codificación para convertir entre valores codificados
posiblemente multibyte y caracteres Unicode de Java. Un esquema de codificación puede ser especificado
por nombre en el constructor de `InputStreamReader` o `OutputStreamWriter`. Por conveniencia, el
constructor predeterminado usa el esquema de codificación predeterminado del sistema.

Por ejemplo, analicemos una cadena legible por humanos desde la entrada estándar en un entero.
Supondremos que los bytes provenientes de `System.in` utilizan el esquema de codificación
predeterminado del sistema:

Primero, envolvemos un `InputStreamReader` alrededor de `System.in`. Este lector convierte los bytes
entrantes de `System.in` a caracteres utilizando el esquema de codificación predeterminado. Luego,
envolvemos un `BufferedReader` alrededor del `InputStreamReader`. `BufferedReader` agrega el método
`readLine()`, que podemos usar para obtener una línea completa de texto (hasta una combinación de
caracteres específica de la plataforma, que indica el final de la línea) en un `String`. Luego, la cadena se
analiza en un entero utilizando las técnicas descritas en el Capítulo 8.

Lo importante es notar que hemos tomado un flujo de entrada orientado a bytes, `System.in`, y lo hemos
convertido de manera segura a un `Reader` para leer caracteres. Si deseáramos usar una codificación
diferente a la predeterminada del sistema, podríamos haberlo especificado en el constructor de
`InputStreamReader`, de la siguiente manera:

Por cada carácter que se lee del lector, InputStreamReader lee uno o más bytes y realiza la conversión
necesaria a Unicode.

Volveremos al tema de las codificaciones de caracteres cuando discutamos la API java.nio.charset, que te
permite consultar y utilizar codificadores y decodificadores explícitamente en búferes de caracteres y
bytes. Tanto InputStreamReader como OutputStreamWriter pueden aceptar un objeto de códec de tipo
Charset, así como un nombre de codificación de caracteres.

Envolturas de Flujo

¿Qué pasa si queremos hacer más que leer y escribir una secuencia de bytes o caracteres? Podemos usar
un "filtro" de flujo, que es un tipo de InputStream, OutputStream, Reader o Writer que envuelve otro flujo
y agrega nuevas características. Un filtro de flujo toma el flujo de destino como argumento en su
constructor y delega llamadas a él después de realizar algún procesamiento adicional propio. Por ejemplo,
podemos construir un BufferedInputStream para envolver la entrada estándar del sistema:

El BufferedInputStream es un tipo de flujo de filtro que lee anticipadamente y almacena en búfer una
cierta cantidad de datos. El BufferedInputStream envuelve una capa adicional de funcionalidad alrededor
del flujo subyacente. La Figura 11-2 muestra este arreglo para un DataInputStream, que es un tipo de flujo
que puede leer tipos de datos de nivel superior, como primitivas de Java y cadenas.

Como se puede observar en el fragmento de código anterior, el filtro BufferedInputStream es un tipo de


InputStream. Debido a que los flujos de filtro son ellos mismos subclases de los tipos básicos de flujo,
pueden usarse como argumentos para la construcción de otros flujos de filtro. Esto permite que los flujos
de filtro se superpongan uno sobre otro para proporcionar diferentes combinaciones de características.
Por ejemplo, podríamos primero envolver nuestro System.in con un BufferedInputStream y luego
envolver el BufferedInputStream con un DataInputStream para leer tipos de datos especiales con
almacenamiento en búfer.

Java proporciona clases base para crear nuevos tipos de flujos de filtro: FilterInputStream,
FilterOutputStream, FilterReader y FilterWriter. Estas superclases proporcionan los mecanismos básicos
para un filtro "sin operación" (un filtro que no hace nada) al delegar todas sus llamadas de método a su
flujo subyacente. Los flujos de filtro reales son subclases de estos y anulan varios métodos para agregar
su procesamiento adicional. Más adelante en este capítulo, realizaremos un ejemplo de un flujo de filtro.

Flujos de datos

DataInputStream y DataOutputStream son flujos de filtro que te permiten leer o escribir cadenas y tipos
de datos primitivos compuestos por más de un solo byte. DataInputStream y DataOutputStream
implementan las interfaces DataInput y DataOutput, respectivamente. Estas interfaces definen métodos
para leer o escribir cadenas y todos los tipos primitivos de Java, incluyendo números y valores booleanos.
DataOutputStream codifica estos valores de una manera independiente de la máquina y luego los escribe
en su flujo de bytes subyacente. DataInputStream hace lo contrario.

Puedes construir un DataInputStream a partir de un InputStream y luego usar un método como


readDouble() para leer un tipo de dato primitivo:

Este ejemplo envuelve la secuencia de entrada estándar en un DataInputStream y la utiliza para leer un
valor double. El método readDouble() lee bytes de la secuencia y construye un double a partir de ellos.
Los métodos de DataInputStream esperan que los bytes de los tipos de datos numéricos estén en el orden
de bytes de red, un estándar que especifica que los bytes de mayor orden se envían primero (también
conocido como "big endian", como discutiremos más adelante).
La clase DataOutputStream proporciona métodos de escritura que corresponden a los métodos de lectura
en DataInputStream. Por ejemplo, writeInt() escribe un entero en formato binario en la secuencia de
salida subyacente.

Los métodos readUTF() y writeUTF() de DataInputStream y DataOutputStream leen y escriben una cadena
Java de caracteres Unicode utilizando el formato de codificación de caracteres UTF-8 "formato de
transformación". UTF-8 es una codificación compatible con ASCII de caracteres Unicode que se utiliza
ampliamente. No todas las codificaciones garantizan preservar todos los caracteres Unicode, pero UTF-8
sí lo hace. También puedes usar UTF-8 con flujos Reader y Writer especificándolo como el nombre de
codificación.

Flujos de búfer

Las clases BufferedInputStream, BufferedOutputStream, BufferedReader y BufferedWriter agregan un


búfer de datos de un tamaño especificado al camino del flujo. Un búfer puede aumentar la eficiencia al
reducir el número de operaciones físicas de lectura o escritura que corresponden a las llamadas de
métodos read() o write(). Creas un flujo con búfer con un flujo de entrada o salida adecuado y un tamaño
de búfer. (También puedes envolver otro flujo alrededor de un flujo con búfer para que se beneficie del
búfer). Aquí hay un ejemplo de un flujo de entrada con búfer simple llamado bis:

En este ejemplo, especificamos un tamaño de búfer de 32 KB. Si omitimos el tamaño del búfer en el
constructor, se elige uno de tamaño razonable por nosotros. (Actualmente, el predeterminado es de 8
KB). En nuestra primera llamada a read(), bis intenta llenar todo nuestro búfer de 32 KB con datos, si están
disponibles. Después de eso, las llamadas a read() recuperan datos del búfer, que se vuelve a llenar según
sea necesario.

Un BufferedOutputStream funciona de manera similar. Las llamadas a write() almacenan los datos en un
búfer; los datos se escriben realmente solo cuando el búfer se llena. También puedes usar el método
flush() para exprimir el contenido de un BufferedOutputStream en cualquier momento.

El método flush() es en realidad un método de la clase OutputStream en sí. Es importante porque te


permite asegurarte de que todos los datos en cualquier flujo subyacente y flujos de filtro se han enviado
(antes, por ejemplo, de esperar una respuesta).

Algunos flujos de entrada, como BufferedInputStream, admiten la capacidad de marcar una ubicación en
los datos y luego restablecer el flujo a esa posición. El método mark() establece el punto de retorno en el
flujo. Toma un valor entero que especifica la cantidad de bytes que se pueden leer antes de que el flujo
desista y olvide la marca. El método reset() devuelve el flujo al punto marcado; cualquier dato leído
después de la llamada a mark() se vuelve a leer.

Esta funcionalidad podría ser útil cuando estás leyendo el flujo en un analizador. Ocasionalmente, es
posible que no puedas analizar una estructura y debas intentar algo más. En esta situación, puedes hacer
que tu analizador genere un error y luego restablecer el flujo al punto antes de comenzar a analizar la
estructura:
Las clases BufferedReader y BufferedWriter funcionan de manera similar a sus contrapartes basadas en
bytes, excepto que operan en caracteres en lugar de bytes.

PrintWriter y PrintStream

Otro flujo envolvente útil es java.io.PrintWriter. Esta clase proporciona un conjunto de métodos print()
sobrecargados que convierten sus argumentos en cadenas y las envían al flujo. Un conjunto
complementario de métodos de conveniencia println() agrega una nueva línea al final de las cadenas. Para
la salida de texto formateado, printf() y los métodos format() idénticos te permiten escribir texto
formateado al estilo printf en el flujo.

PrintWriter es un flujo de caracteres inusual porque puede envolver tanto un OutputStream como otro
Writer. PrintWriter es el hermano mayor más capaz del flujo de bytes PrintStream heredado. Los flujos
System.out y System.err son objetos PrintStream; ya has visto dichos flujos dispersos por todo este libro:

Las primeras versiones de Java no tenían las clases Reader y Writer, y utilizaban Print Stream, el cual
convertía bytes a caracteres simplemente realizando suposiciones sobre la codificación de caracteres.
Deberías utilizar un PrintWriter para todo nuevo desarrollo.

Cuando creas un objeto PrintWriter, puedes pasar un valor booleano adicional al constructor,
especificando si debería "auto-flush" (auto-eliminar). Si este valor es verdadero (true), el PrintWriter
realiza automáticamente un flush() en el OutputStream o Writer subyacente cada vez que envía una nueva
línea:

Cuando esta técnica se utiliza con un flujo de salida con búfer, corresponde al comportamiento de
terminales que envían datos línea por línea.

La otra gran ventaja que PrintStream y PrintWriter tienen sobre los flujos de caracteres regulares es que
te protegen de las excepciones arrojadas por los flujos subyacentes. A diferencia de los métodos en otras
clases de flujo, los métodos de PrintWriter y PrintStream no lanzan IOExceptions. En su lugar,
proporcionan un método para verificar explícitamente los errores si es necesario. Esto facilita mucho la
vida al imprimir texto, que es una operación muy común. Puedes verificar los errores con el método
checkError():

La Clase java.io.File

La clase java.io.File encapsula el acceso a la información sobre un archivo o directorio. Puede ser utilizada
para obtener información de atributos sobre un archivo, listar las entradas en un directorio y realizar
operaciones básicas en el sistema de archivos, como eliminar un archivo o crear un directorio.

Mientras que el objeto File maneja estas operaciones "meta", no proporciona la API para leer y escribir
datos de archivo; para ese propósito existen flujos de archivos.
Constructores de File

Puedes crear una instancia de File a partir de una ruta de String (pathname):

También puedes crear un archivo con una ruta relativa:

En este caso, Java opera en relación con el "directorio de trabajo actual" del intérprete de Java. Puedes
determinar el directorio de trabajo actual leyendo la propiedad user.dir en la lista de Propiedades del
Sistema (System Properties):

Una versión sobrecargada del constructor de File te permite especificar la ruta del directorio y el nombre
de archivo como objetos String separados:

Con otra variación, puedes especificar el directorio con un objeto File y el nombre de archivo con un String:

Ninguno de estos constructores de File crea realmente un archivo o directorio, y no es un error crear un
objeto File para un archivo inexistente. El objeto File es simplemente un identificador para un archivo o
directorio cuyas propiedades puedes querer leer, escribir o comprobar. Por ejemplo, puedes usar el
método de instancia exists() para saber si el archivo o directorio existe.

Localización de la ruta

Un problema al trabajar con archivos en Java es que se espera que las rutas sigan las convenciones del
sistema de archivos local. Dos diferencias son que el sistema de archivos de Windows usa "raíces" o letras
de unidad (por ejemplo, C:) y una barra invertida (\) en lugar del separador de ruta de barra diagonal (/)
que se usa en otros sistemas.

Java intenta compensar estas diferencias. Por ejemplo, en plataformas de Windows, Java acepta rutas con
barras diagonales o barras invertidas. (Sin embargo, en otros sistemas, solo acepta barras diagonales).

Lo mejor es asegurarse de seguir las convenciones de nombres de archivos del sistema de archivos
anfitrión. Si tu aplicación tiene una interfaz gráfica de usuario que abre y guarda archivos a petición del
usuario, deberías poder manejar esa funcionalidad con la clase Swing JFileChooser. Esta clase encapsula
un cuadro de diálogo gráfico para seleccionar archivos. Los métodos de JFileChooser se encargan de las
características de nombres de archivo dependientes del sistema por ti.

Sin embargo, si tu aplicación necesita tratar con archivos por su cuenta, las cosas se complican un poco
más. La clase File contiene algunas variables estáticas para hacer posible esta tarea. File.separator define
un String que especifica el separador de archivos en el host local (por ejemplo, / en sistemas Unix y macOS,
y \ en sistemas Windows); File.separatorChar proporciona la misma información como un char.

Puedes usar esta información dependiente del sistema de varias maneras. Probablemente, la forma más
simple de localizar las rutas sea elegir una convención que uses internamente, como la barra diagonal (/),
y realizar un reemplazo de String para sustituir el carácter separador localizado:
Alternativamente, podrías trabajar con los componentes de una ruta de archivo y construir la ruta de
archivo local cuando la necesites:

Una cosa importante a recordar es que Java interpreta un carácter de barra invertida (\) literal
en el código fuente como un carácter de escape cuando se usa en un String. Para obtener una barra
invertida en un String, debes usar \\.

Para lidiar con el problema de sistemas de archivos con múltiples "raíces" (por ejemplo, C:\ en Windows),
la clase File proporciona el método estático listRoots(), que devuelve un array de objetos File
correspondientes a los directorios raíz del sistema de archivos. Nuevamente, en una aplicación GUI, un
cuadro de diálogo gráfico para elegir archivos te protege totalmente de este problema.

Operaciones de archivo

Una vez que tenemos un objeto File, podemos usarlo para obtener información y realizar operaciones
estándar en el archivo o directorio que representa. Varios métodos nos permiten hacer preguntas sobre
el File. Por ejemplo, isFile() devuelve true si el File representa un archivo regular, mientras que
isDirectory() devuelve true si es un directorio. isAbsolute() indica si el File encapsula una especificación de
ruta absoluta o relativa. Una ruta absoluta es una noción dependiente del sistema que significa que la
ruta no depende del directorio de trabajo de la aplicación o de algún concepto de raíz o unidad de trabajo
(por ejemplo, en Windows, es una ruta completa que incluye la letra de unidad: c:\\Usuarios\pat\foo.txt).

Los componentes de la ruta del File están disponibles a través de los siguientes métodos: getName(),
getPath(), getAbsolutePath() y getParent(). getName() devuelve un String con el nombre del archivo sin
ninguna información de directorio. Si el File tiene una especificación de ruta absoluta, getAbsolutePath()
devuelve esa ruta. De lo contrario, devuelve la ruta relativa agregada al directorio de trabajo actual
(intentando convertirla en una ruta absoluta). getParent() devuelve el directorio padre del archivo o
directorio.

El string devuelto por getPath() o getAbsolutePath() puede que no siga las mismas convenciones de
mayúsculas y minúsculas que el sistema de archivos subyacente. Puedes obtener la versión del sistema
de archivos o "canónica" de la ruta del archivo utilizando el método getCanonicalPath(). En Windows, por
ejemplo, puedes crear un objeto File cuyo getAbsolutePath() sea C:\Autoexec.bat pero cuyo
getCanonicalPath() sea C:\AUTOEXEC.BAT; ambos apuntan realmente al mismo archivo. Esto es útil para
comparar nombres de archivos que pueden haber sido suministrados con diferentes convenciones de
mayúsculas y minúsculas o para mostrarlos al usuario.

Puedes obtener o establecer el tiempo de modificación de un archivo o directorio con los métodos
lastModified() y setLastModified(). El valor es un long que representa el número de milisegundos desde
la época (1 de enero de 1970, 00:00:00 GMT). También podemos obtener el tamaño del archivo, en bytes,
con length().

Aquí tienes un fragmento de código que imprime información sobre un archivo:


Si el objeto File corresponde a un directorio, podemos enlistar los archivos en el directorio con el método
list() o el método listFiles():

El método list() devuelve un array de objetos String que contiene nombres de archivo. El método listFiles()
devuelve un array de objetos File. Ten en cuenta que en ninguno de los casos se garantiza que los archivos
estén en algún tipo de orden (por ejemplo, orden alfabético). Puedes usar la API de Collections para
ordenar cadenas alfabéticamente, así:

Si el objeto File se refiere a un directorio que no existe, podemos crear el directorio con mkdir() o mkdirs().
El método mkdir() crea a lo sumo un solo nivel de directorio, por lo que cualquier directorio intermedio
en la ruta debe existir previamente. mkdirs() crea todos los niveles de directorio necesarios para crear la
ruta completa de la especificación del File. En cualquier caso, si no se puede crear el directorio, el método
devuelve false. Usa renameTo() para renombrar un archivo o directorio y delete() para eliminar un archivo
o directorio.

Aunque podemos crear un directorio usando el objeto File, esta no es la forma más común de crear un
archivo; normalmente se hace implícitamente cuando pretendemos escribir datos en él con un
FileOutputStream o FileWriter, como discutiremos en un momento. La excepción es el método
createNewFile(), que se puede utilizar para intentar crear un archivo nuevo de longitud cero en la
ubicación señalada por el objeto File. Lo útil de este método es que la operación está garantizada como
"atómica" con respecto a toda otra creación de archivos en el sistema de archivos. createNewFile()
devuelve un valor booleano que indica si se creó el archivo o no. A veces se usa como una función de
bloqueo primitiva: quien crea el archivo primero "gana". (El paquete NIO admite bloqueos reales de
archivos, como veremos más adelante). Esto es útil en combinación con deleteOnExit(), que marca el
archivo para que se elimine automáticamente cuando la máquina virtual de Java se cierre. Esta
combinación te permite proteger recursos o crear una aplicación que solo puede ejecutarse en una sola
instancia a la vez. Otro método de creación de archivos que está relacionado con la propia clase File es el
método estático createTempFile(), que crea un archivo en una ubicación especificada utilizando un
nombre único generado automáticamente. Esto también es útil en combinación con deleteOnExit().

El método toURL() convierte una ruta de archivo a un objeto URL de archivo. Las URL son una abstracción
que te permite apuntar a cualquier tipo de objeto en cualquier lugar de la red. Convertir una referencia
de File a una URL puede ser útil para mantener la consistencia con utilidades más generales que manejan
URLs. Las URL de archivos también se utilizan más con la API de Archivos de NIO, donde se pueden usar
para referenciar nuevos tipos de sistemas de archivos que se implementan directamente en código Java.

La siguiente lista resume los métodos proporcionados por la clase File.

Los métodos proporcionados por la clase File son los siguientes:

- canExecute() - ¿Es el archivo ejecutable? (Booleano)

- canRead() - ¿Es el archivo (o directorio) legible? (Booleano)

- canWrite() - ¿Es el archivo (o directorio) escribible? (Booleano)

- createNewFile() - Crea un nuevo archivo (Booleano)

- createTempFile(String pfx, String sfx) - Método estático para crear un nuevo archivo, con el prefijo y
sufijo especificados, en el directorio temporal predeterminado (File)

- delete() - Elimina el archivo (o directorio) (Booleano)

- deleteOnExit() - Cuando finaliza, el sistema de ejecución de Java elimina el archivo (Void)


- exists() - ¿Existe el archivo (o directorio)? (Booleano)

- getAbsolutePath() - Devuelve la ruta absoluta del archivo (o directorio) (String)

- getCanonicalPath() - Devuelve la ruta absoluta, corregida en mayúsculas y minúsculas, y resuelta en


elementos relativos del archivo (o directorio) (String)

- getFreeSpace() - Obtiene la cantidad de bytes de espacio no asignado en la partición que contiene esta
ruta o 0 si la ruta es inválida (long)

- getName() - Devuelve el nombre del archivo (o directorio) (String)

- getParent() - Devuelve el nombre del directorio padre del archivo (o directorio) (String)

- getPath() - Devuelve la ruta del archivo (o directorio). (No debe confundirse con `toPath()`.) (String)

- getTotalSpace() - Obtiene el tamaño de la partición que contiene la ruta del archivo, en bytes, o 0 si la
ruta es inválida (long)

- getUsableSpace() - Obtiene la cantidad de bytes de espacio no asignado accesible para el usuario en la


partición que contiene esta ruta o 0 si la ruta es inválida. Este método intenta tener en cuenta los permisos
de escritura del usuario. (long)

- isAbsolute() - ¿Es el nombre del archivo (o directorio) absoluto? (booleano)

- isDirectory() - ¿Es el ítem un directorio? (booleano)

- isFile() - ¿Es el ítem un archivo? (booleano)

- isHidden() - ¿Está oculto el ítem? (Depende del sistema) (booleano)

- lastModified() - Devuelve la última hora de modificación del archivo (o directorio) (long)

- length() - Devuelve la longitud del archivo (long)

- list() - Devuelve una lista de archivos en el directorio (String[])

- listFiles() - Devuelve el contenido del directorio como un array de objetos File (File[])

- listRoots() - Devuelve un array de sistemas de archivos raíz, si los hay (por ejemplo, C:/, D:/) (File[])

- mkdir() - Crea el directorio (booleano)

- mkdirs() - Crea todos los directorios en la ruta (booleano)

- renameTo(File dest) - Renombra el archivo (o directorio) (booleano)

- setExecutable() - Establece permisos de ejecución para el archivo (booleano)

- setLastModified() - Establece la última hora de modificación del archivo (o directorio) (booleano)

- setReadable() - Establece permisos de lectura para el archivo (booleano)

- setReadOnly() - Establece el archivo como de solo lectura (booleano)

- setWritable() - Establece los permisos de escritura para el archivo (booleano)

- toPath() - Convierte el archivo en una ruta de archivo NIO (ver la API de Archivos NIO). (No debe
confundirse con `getPath()`.) (java.nio.file.Path)

- toURL() - Genera un objeto URL para el archivo (o directorio) (java.net.URL)


Flujos de archivos

Probablemente ya estés cansado de escuchar sobre archivos y ni siquiera hemos escrito un solo byte
todavía. Bueno, ahora comienza la diversión. Java proporciona dos flujos fundamentales para leer y
escribir archivos: FileInputStream y FileOutputStream. Estos flujos proporcionan la funcionalidad básica
de InputStream y OutputStream orientada a bytes que se aplica para leer y escribir archivos. Se pueden
combinar con los flujos de filtro descritos anteriormente para trabajar con archivos de la misma manera
que otras comunicaciones de flujo.

Puedes crear un FileInputStream a partir de una ruta de cadena (String pathname) o un objeto File:

Cuando creas un FileInputStream, el sistema en tiempo de ejecución de Java intenta abrir el archivo
especificado. Por lo tanto, los constructores de FileInputStream pueden lanzar una
FileNotFoundExcepción si el archivo especificado no existe, o una IOException si ocurre algún otro error
de E/S (entrada/salida). Debes capturar estas excepciones en tu código. Siempre que sea posible, es una
buena idea adquirir el hábito de utilizar la construcción try-with-resources de Java 7 para cerrar
automáticamente los archivos cuando hayas terminado con ellos:

Cuando se crea por primera vez el flujo, su método available() y el método length() del objeto File deberían
devolver el mismo valor.

Para leer caracteres de un archivo como un Reader, puedes envolver un InputStreamReader alrededor de
un FileInputStream. También puedes utilizar en su lugar la clase FileReader, que se proporciona como una
conveniencia. FileReader es simplemente un FileInputStream envuelto en un InputStreamReader con
algunos valores predeterminados.

La siguiente clase, ListIt, es una pequeña utilidad que envía el contenido de un archivo o directorio a la
salida estándar:
ListIt construye un objeto File a partir de su primer argumento de línea de comandos y verifica si el archivo
existe y es legible. Si el File es un directorio, ListIt muestra los nombres de los archivos en el directorio. De
lo contrario, ListIt lee y muestra el archivo línea por línea.

Para escribir archivos, puedes crear un FileOutputStream a partir de una ruta de cadena (String pathname)
o un objeto File. Sin embargo, a diferencia de FileInputStream, los constructores de FileOutputStream no
lanzan una FileNotFoundException. Si el archivo especificado no existe, FileOutputStream crea el archivo.
Los constructores de FileOutputStream pueden lanzar una IOException si ocurre algún otro error de E/S,
por lo que aún debes manejar esta excepción.

Si el archivo especificado ya existe, FileOutputStream lo abre para escritura. Cuando posteriormente


llamas al método write(), los nuevos datos sobrescriben el contenido actual del archivo. Si necesitas añadir
datos a un archivo existente, puedes usar una forma del constructor que acepta un indicador booleano
de 'append' (anexar):

Otra forma de agregar datos a archivos es mediante RandomAccessFile, del cual discutiremos pronto.

Al igual que con la lectura, para escribir caracteres (en lugar de bytes) a un archivo, puedes envolver un
OutputStreamWriter alrededor de un FileOutputStream. Si deseas utilizar el esquema de codificación de
caracteres por defecto, puedes utilizar la clase FileWriter, que se proporciona como una conveniencia.

El siguiente ejemplo lee una línea de datos desde la entrada estándar y la escribe en el archivo
/tmp/foo.txt:

Observa cómo envolvimos el FileWriter en un PrintWriter para facilitar la escritura de los datos. Además,
para ser un buen ciudadano del sistema de archivos, llamamos al método close() cuando hemos
terminado con el FileWriter. Aquí, al cerrar el PrintWriter se cierra el Writer subyacente por nosotros.
También podríamos haber utilizado try-with-resources aquí.

RandomAccessFile

La clase java.io.RandomAccessFile proporciona la capacidad de leer y escribir datos en una ubicación


específica en un archivo. RandomAccessFile implementa tanto las interfaces DataInput como DataOutput,
por lo que puedes usarla para leer y escribir cadenas y tipos primitivos en ubicaciones del archivo como
si fuera un DataInputStream y DataOutputStream. Sin embargo, debido a que la clase proporciona acceso
aleatorio en lugar de acceso secuencial a los datos del archivo, no es una subclase ni de InputStream ni de
OutputStream.

Puedes crear un RandomAccessFile a partir de una ruta de cadena (String pathname) o un objeto File. El
constructor también toma un segundo argumento de tipo String que especifica el modo del archivo. Usa
la cadena "r" para un archivo de solo lectura o "rw" para un archivo de lectura/escritura.
Cuando creas un RandomAccessFile en modo de solo lectura, Java intenta abrir el archivo especificado. Si
el archivo no existe, RandomAccessFile lanza una IOException. Sin embargo, si estás creando un
RandomAccessFile en modo de lectura/escritura, el objeto crea el archivo si no existe. El constructor aún
puede lanzar una IOException si ocurre otro error de E/S, por lo que aún debes manejar esta excepción.

Después de haber creado un RandomAccessFile, puedes llamar a cualquiera de los métodos normales de
lectura y escritura, tal como lo harías con un DataInputStream o DataOutputStream. Si intentas escribir
en un archivo de solo lectura, el método write lanza una IOException.

Lo que hace especial a RandomAccessFile es el método seek(). Este método toma un valor long y lo usa
para establecer la ubicación del desplazamiento de bytes para leer y escribir en el archivo. Puedes utilizar
el método getFilePointer() para obtener la ubicación actual. Si necesitas agregar datos al final del archivo,
usa length() para determinar esa ubicación y luego haz un seek() hacia ella. Puedes escribir o buscar más
allá del final de un archivo, pero no puedes leer más allá del final de un archivo. El método read() lanza
una EOFException si intentas hacer esto.

Aquí tienes un ejemplo de cómo escribir datos para una base de datos simplista:

En este fragmento, asumimos que la longitud de la cadena para userName, junto con cualquier dato que
le siga, cabe dentro del tamaño de registro especificado.

La API de Archivos NIO

Ahora vamos a dirigir nuestra atención desde la API de Archivos clásica original de Java hacia la nueva API
de Archivos NIO introducida con Java 7. Como mencionamos antes, la API de Archivos NIO puede ser vista
tanto como un reemplazo como un complemento a la API clásica.

Incluida en el paquete NIO, la nueva API forma parte nominalmente de un esfuerzo para llevar a Java hacia
un estilo de E/S de mayor rendimiento y más flexible que admite canales seleccionables y asincrónicos.
Sin embargo, en el contexto de trabajar con archivos, la fortaleza de la nueva API radica en que
proporciona una abstracción más completa del sistema de archivos en Java.

Además de un mejor soporte para tipos de sistemas de archivos existentes en el mundo real, incluyendo
por primera vez la capacidad de copiar y mover archivos, gestionar enlaces y obtener atributos detallados
de archivos como propietarios y permisos, la nueva API de Archivos permite la implementación de tipos
de sistemas de archivos completamente nuevos directamente en Java. El mejor ejemplo de esto es el
nuevo proveedor de sistemas de archivos ZIP que permite "montar" un archivo de archivo ZIP como un
sistema de archivos y trabajar con los archivos dentro de él directamente utilizando las APIs estándar, al
igual que cualquier otro sistema de archivos.

Además, el paquete de Archivos NIO proporciona algunas utilidades que habrían ahorrado a los
desarrolladores de Java una gran cantidad de código repetitivo a lo largo de los años, incluyendo
monitoreo de cambios en árboles de directorios, recorrido de sistemas de archivos (un patrón de
visitante), "globbing" de nombres de archivo y métodos de conveniencia para leer archivos completos
directamente en la memoria.

En esta sección, cubriremos los conceptos básicos de la API de Archivos NIO y volveremos al tema de los
búferes y canales al final del capítulo. En particular, hablaremos sobre ByteChannels y FileChannel, que
puedes pensar como flujos orientados a búferes alternativos para leer y escribir archivos y otros tipos de
datos.
FileSystem y Path

Los componentes principales en el paquete java.nio.file son: el FileSystem, que representa un mecanismo
de almacenamiento subyacente y sirve como una fábrica para objetos Path; el Path, que representa un
archivo o directorio dentro del sistema de archivos; y la utilidad Files, que contiene un conjunto rico de
métodos estáticos para manipular objetos Path y realizar todas las operaciones básicas de archivo
análogas a la API clásica.

La clase FileSystems (en plural) es nuestro punto de partida. Es una fábrica para un objeto FileSystem:

Como se muestra en este fragmento, a menudo simplemente solicitaremos el sistema de archivos


predeterminado para manipular archivos en el entorno del equipo anfitrión, al igual que con la API clásica.
Sin embargo, la clase FileSystems también puede construir un FileSystem al tomar un URI (un identificador
especial similar a una URL) que referencia un tipo de sistema de archivos personalizado. Aquí usamos
jar:file como nuestro protocolo URI para indicar que estamos trabajando con un archivo JAR o ZIP.

FileSystem implementa Closeable y cuando un FileSystem se cierra, todos los canales de archivo abiertos
y otros objetos de transmisión asociados también se cierran. Intentar leer o escribir en esos canales
lanzará una excepción en ese momento. Ten en cuenta que el sistema de archivos predeterminado
(asociado con el equipo anfitrión) no se puede cerrar.

Una vez que tenemos un FileSystem, podemos usarlo como una fábrica para objetos Path que representan
archivos o directorios. Un Path se puede construir utilizando una representación de cadena al igual que el
clásico File, y posteriormente se puede utilizar con métodos de la utilidad Files para crear, leer, escribir o
eliminar el elemento.

Este ejemplo abre un OutputStream para escribir en el archivo foo.txt. Por defecto, si el archivo no existe,
se creará, y si existe, se truncará (se establecerá a longitud cero) antes de que se escriban nuevos datos,
aunque puedes cambiar estos resultados usando opciones.

Hablaremos más sobre los métodos de Files en la próxima sección.

El objeto Path implementa la interfaz java.lang.Iterable, que se puede utilizar para iterar a través de sus
componentes de ruta literales (por ejemplo, los componentes separados por barra, como "tmp" y
"foo.txt" en el fragmento anterior). Sin embargo, si deseas recorrer la ruta para encontrar otros archivos
o directorios, es posible que te interese más el DirectoryStream y FileVisitor, que discutiremos más
adelante. Además, Path implementa la interfaz java.nio.file.Watchable, lo que permite que sea
monitoreado en busca de cambios. También discutiremos cómo observar árboles de archivos en busca de
cambios en una sección próxima.

Path tiene métodos de conveniencia para resolver rutas relativas a un archivo o directorio:
En este fragmento, hemos mostrado los métodos `Pathresolve()` y `resolveSibling()` utilizados para
encontrar archivos o directorios en relación con un objeto Path dado. El método `resolve()` se usa
generalmente para agregar una ruta relativa a un Path existente que representa un directorio. Si el
argumento proporcionado al método `resolve()` es una ruta absoluta, simplemente producirá la ruta
absoluta (actúa de manera similar al comando `cd` en Unix o DOS). El método `resolveSibling()` funciona
de la misma manera, pero es relativo al directorio padre del Path objetivo; este método es útil para
describir el objetivo de una operación `move()`.

De Path a archivo clásico y viceversa

Para conectar las APIs antigua y nueva, se han proporcionado los métodos `toPath()` y `toFile()` en
`java.io.File` y `java.nio.file.Path`, respectivamente, para convertir uno en el otro. Por supuesto, los únicos
tipos de Paths que se pueden producir a partir de un `File` son paths que representan archivos y
directorios en el sistema de archivos host predeterminado.

Operaciones de Archivos NIO

Una vez que tenemos un Path, podemos operar en él con métodos estáticos de la utilidad Files para crear
el camino como un archivo o directorio, leer y escribir en él, e interrogar y establecer sus propiedades.
Enumeraremos la mayoría de ellos y luego discutiremos algunos de los más importantes a medida que
avancemos.

La Tabla 11-2 resume estos métodos de la clase java.nio.file.Files. Como podrías esperar, debido a que la
clase Files maneja todo tipo de operaciones de archivo, contiene una gran cantidad de métodos. Para
hacer la tabla más legible, hemos omitido formas sobrecargadas del mismo método (aquellas que toman
diferentes tipos de argumentos) y agrupado tipos de métodos correspondientes y relacionados.

Lista de Métodos de Files en NIO

Aquí tienes una traducción al español de la tabla de métodos de la clase Files en NIO:

copy() Copia un flujo a una ruta de archivo, una ruta


long o Path de archivo a un flujo, o de una ruta a otra ruta.
Devuelve el número de bytes copiados o la
ruta de destino. Opcionalmente, se puede
reemplazar un archivo de destino si existe
(por defecto, falla si el archivo de destino
existe). Copiar un directorio resulta en un
directorio vacío en el destino (los contenidos
no se copian). Copiar un enlace simbólico
copia los datos del archivo enlazado
(produciendo una copia de archivo regular).

createDirectory(), Path Crea un solo directorio o todos los directorios


createDirectories() en una ruta especificada. createDirectory()
genera una excepción si el directorio ya existe,
mientras que createDirectories() ignorará los
directorios existentes y solo creará los
necesarios.

createFile() Path Crea un archivo vacío. La operación es atómica y


solo tendrá éxito si el archivo no existe. (Esta
propiedad puede usarse para crear archivos de
bandera para proteger recursos, etc.)
createTempDirectory(), Path Crea un directorio o archivo temporal con un
createTempFile() nombre único garantizado y el prefijo
especificado. Opcionalmente, se puede colocar
en el directorio temporal predeterminado del
sistema.

delete(), deleteIfExists() void Elimina un archivo o un directorio vacío.


deleteIfExists() no generará una excepción si el
archivo no existe.

exists(), notExists() boolean Determina si el archivo existe (notExists()


simplemente devuelve lo opuesto).
Opcionalmente, se puede especificar si se deben
seguir los enlaces (por defecto, se siguen).

exists(),isDirectory(), boolean Prueba características básicas del archivo: si la


isExecutable(), ruta existe, es un directorio y otros atributos
isWriteable(),isHidden(), básicos.
isReadable(),
isRegularFile()

createLink(), boolean o Path Crea un enlace duro o simbólico, prueba si un


createSymbolicLink(), archivo es un enlace simbólico o lee el archivo de
isSymbolicLink(), destino apuntado por el enlace simbólico. Los
readSymbolicLink(), enlaces simbólicos son archivos que hacen
createLink() referencia a otros archivos. Los enlaces regulares
("duros") son réplicas de bajo nivel de un archivo
donde dos nombres de archivo apuntan a los
mismos datos subyacentes. Si no estás seguro de
cuál usar, utiliza un enlace simbólico.

getAttribute(), Object, Map o Obtiene o establece atributos específicos del


setAttribute(), FileAttributeView sistema de archivos, como tiempos de acceso y
getFileAttributeView(), actualización, permisos detallados e información
readAttributes() del propietario utilizando nombres específicos de
implementación.

getFileStore() FileStore Obtiene un objeto FileStore que representa el


dispositivo, volumen u otro tipo de partición del
sistema de archivos en el que reside la ruta.

getLastModifiedTime(), FileTime o Path Obtiene o establece la última hora de


setLastModifiedTime() modificación de un archivo o directorio.

getOwner(), setOwner() UserPrincipal Obtiene o establece un objeto UserPrincipal que


representa el propietario del archivo. Usa
toString() o getName() para obtener una
representación de cadena del nombre de usuario.

getPosixFilePermissions(), Set o Path Obtiene o establece los permisos completos de


setPosixFilePermissions() lectura y escritura del estilo de usuario-grupo-
otros POSIX para la ruta como un conjunto de
valores de enumeración PosixFilePermission.

isSameFile() boolean Prueba si las dos rutas hacen referencia al mismo


archivo (lo cual puede ser potencialmente cierto
incluso si las rutas no son idénticas).

move() Path Mueve un archivo o directorio cambiándole el


nombre o copiándolo, especificando
opcionalmente si se debe reemplazar algún
destino existente. Se usará el cambio de nombre
a menos que se requiera una copia para mover un
archivo entre almacenes de archivos o sistemas
de archivos. Los directorios solo se pueden mover
usando este método si es posible el cambio de
nombre simple o si el directorio está vacío. Si
mover un directorio requiere copiar archivos
entre almacenes de archivos o sistemas de
archivos, el método genera una IOException. (En
este caso, debes copiar los archivos tú mismo. Ver
walkFileTree().)

newBufferedReader(), BufferedReader o Abre un archivo para lectura a través de un


newBufferedWriter() BufferedWriter BufferedReader, o crea y abre un archivo para
escritura a través de un BufferedWriter. En
ambos casos, se especifica una codificación de
caracteres.

newByteChannel() SeekableByteChannel Crea un archivo nuevo o abre un archivo


existente como un canal de bytes buscable. (Ver
la discusión completa de NIO más adelante en
este capítulo.) Considera usar FileChannelopen()
como alternativa.

newDirectoryStream() DirectoryStream Devuelve un DirectoryStream para iterar sobre


una jerarquía de directorios. Opcionalmente, se
puede proporcionar un patrón glob o un objeto
de filtro para coincidir con archivos.

newInputStream(), InputStream o Abre un archivo para lectura a través de un


newOutputStream() OutputStream InputStream o crea y abre un archivo para
escritura a través de un OutputStream.
Opcionalmente, se puede especificar la
truncación del archivo para el flujo de salida; por
defecto, se crea una truncación en la escritura.

probeContentType() String Devuelve el tipo MIME del archivo si puede


determinarse mediante los servicios de detección
de tipo de archivo instalados o null si es
desconocido.

readAllBytes(), byte[] o List<String> Lee todos los datos del archivo como un byte[] o
readAllLines() todos los caracteres como una lista de cadenas
utilizando una codificación de caracteres
especificada.

size() long Obtiene el tamaño, en bytes, del archivo en la


ruta especificada.
walkFileTree() Path Aplica un FileVisitor al árbol de directorios
especificado, especificando opcionalmente si se
deben seguir los enlaces y una profundidad
máxima de recorrido.

write() Path Escribe una matriz de bytes o una colección de


cadenas (con una codificación de caracteres
especificada) en el archivo en la ruta especificada
y cierra el archivo, especificando opcionalmente
el comportamiento de añadir y truncar. Por
defecto, se truncan y escriben los datos.

Con los métodos anteriores, podemos obtener flujos de entrada o salida o lectores y escritores
bufferizados para un archivo dado. También podemos crear rutas como archivos y directorios, e iterar a
través de jerarquías de archivos. Discutiremos las operaciones de directorio en la siguiente sección. Como
recordatorio, los métodos resolve() y resolveSibling() de Path son útiles para construir objetivos para las
operaciones copy() y move():

Para leer y escribir rápidamente el contenido de archivos sin transmitir, podemos utilizar los diversos
métodos readAll... y write que mueven matrices de bytes o cadenas hacia dentro y fuera de archivos en
una sola operación. Estos son muy convenientes para archivos que se ajustan fácilmente en la memoria.

El paquete NIO

Ahora vamos a completar nuestra introducción a las facilidades centrales de E/S (Entrada/Salida) de Java
volviendo al paquete java.nio. Como se mencionó anteriormente, el nombre NIO significa "New I/O"
(Nueva E/S) y, como vimos anteriormente en este capítulo en nuestra discusión de java.nio.file, un aspecto
de NIO simplemente es actualizar y mejorar las características del legado java.io. Gran parte de la
funcionalidad general de NIO realmente se superpone con APIs existentes. Sin embargo, NIO fue
introducido inicialmente para abordar problemas específicos de escalabilidad para sistemas grandes,
especialmente en aplicaciones en red. La siguiente sección describe los elementos básicos de NIO, los
cuales se centran en trabajar con búferes y canales.

E/S Asincrónica

Gran parte de la necesidad del paquete NIO fue impulsada por el deseo de añadir E/S no bloqueante y
seleccionable a Java. Antes de NIO, la mayoría de las operaciones de lectura y escritura en Java estaban
vinculadas a hilos y se veían obligadas a bloquearse durante períodos de tiempo impredecibles. Aunque
ciertas APIs como Sockets (que veremos en "Sockets" en la página 379) proporcionaban medios
específicos para limitar cuánto tiempo podía tomar una llamada de E/S, esto era una solución temporal
para compensar la falta de un mecanismo más general. En muchos lenguajes, incluso aquellos sin hilos, la
E/S aún podía realizarse eficientemente configurando los flujos de E/S en un modo no bloqueante y
probándolos para saber si estaban listos para enviar o recibir datos. En un modo no bloqueante, una
lectura o escritura realiza únicamente el trabajo que se puede hacer de inmediato, llenando o vaciando
un búfer y luego devolviendo el control. Combinado con la capacidad de probar la preparación, esto
permite que una aplicación de un solo hilo atienda continuamente muchos canales de manera eficiente.
El hilo principal "selecciona" un flujo que está listo, trabaja con él hasta que se bloquea, y luego pasa a
otro. En un sistema de un solo procesador, esto es fundamentalmente equivalente al uso de múltiples
hilos. Resulta que este estilo de procesamiento tiene ventajas de escalabilidad incluso al usar un conjunto
de hilos (en lugar de solo uno). Discutiremos esto en detalle en el Capítulo 12 cuando hablemos sobre
programación web y la construcción de servidores que pueden manejar muchos clientes
simultáneamente.

Además de la E/S no bloqueante y seleccionable, el paquete NIO permite cerrar e interrumpir operaciones
de E/S de forma asíncrona. Como se discutió en el Capítulo 9, antes de NIO no había una forma confiable
de detener o despertar a un hilo bloqueado en una operación de E/S. Con NIO, los hilos bloqueados en
operaciones de E/S siempre se despiertan cuando son interrumpidos o cuando el canal es cerrado por
cualquier persona. Además, si interrumpes un hilo mientras está bloqueado en una operación de NIO, su
canal se cierra automáticamente. (Cerrar el canal porque el hilo está interrumpido podría parecer
demasiado drástico, pero por lo general es lo correcto. Dejarlo abierto podría resultar en un
comportamiento inesperado o exponer el canal a manipulaciones no deseadas).

Rendimiento

La E/S de canal está diseñada en torno al concepto de búferes, que son una forma sofisticada de arreglo,
diseñada para trabajar con comunicaciones. El paquete NIO soporta el concepto de búferes directos,
búferes que mantienen su memoria fuera de la JVM (Máquina Virtual de Java) en el sistema operativo
anfitrión. Dado que todas las operaciones reales de E/S deben trabajar con el sistema operativo anfitrión
manteniendo allí el espacio de búfer, algunas operaciones pueden ser mucho más eficientes. Los datos
que se mueven entre dos puntos finales externos pueden transferirse sin copiarlos primero en Java y
luego devolverlos.

Archivos Mapeados y Bloqueados

NIO proporciona dos características relacionadas con archivos de propósito general que no se encuentran
en java.io: archivos mapeados en memoria y bloqueo de archivos. Hablaremos de los archivos mapeados
en memoria más adelante, pero basta decir que te permiten trabajar con datos de archivos como si
estuvieran todos mágicamente residentes en la memoria. El bloqueo de archivos admite el concepto de
bloqueos compartidos y exclusivos en regiones de archivos, útiles para el acceso concurrente por
múltiples aplicaciones.

Canales

Mientras que java.io trabaja con flujos, java.nio trabaja con canales. Un canal es un punto final para la
comunicación. Aunque en la práctica los canales son similares a los flujos, la noción subyacente de un
canal es más abstracta y primitiva. Mientras que los flujos en java.io están definidos en términos de
entrada o salida con métodos para leer y escribir bytes, la interfaz básica de canal no dice nada sobre
cómo se realizan las comunicaciones. Simplemente tiene la noción de estar abierto o cerrado, compatible
mediante los métodos isOpen() y close(). Luego, las implementaciones de canales para archivos, sockets
de red o dispositivos arbitrarios añaden sus propios métodos para operaciones, como lectura, escritura o
transferencia de datos. Los siguientes canales son proporcionados por NIO:

• FileChannel

• Pipe.SinkChannel, Pipe.SourceChannel
• SocketChannel, ServerSocketChannel, DatagramChannel

Cubriremos FileChannel en este capítulo. Los canales Pipe son simplemente los equivalentes de canal de
las facilidades Pipe de java.io. Además, en Java 7 ahora existen versiones asíncronas tanto para archivos
como para canales de sockets: AsynchronousFileChannel, AsynchronousSocketChannel,
AsynchronousServerSocketChannel y AsynchronousDatagramChannel. Estas versiones asíncronas
básicamente almacenan todas sus operaciones a través de un grupo de hilos y reportan los resultados a
través de una API asíncrona. Hablaremos sobre el canal de archivo asíncrono más adelante en este
capítulo.

Todos estos canales básicos implementan la interfaz ByteChannel, diseñada para canales que tienen
métodos de lectura y escritura al igual que los flujos de E/S. Sin embargo, los ByteChannels leen y escriben
ByteBuffers, en lugar de simples arreglos de bytes.

Además de estas implementaciones de canales, puedes enlazar canales con flujos de E/S de java.io y
lectores y escritores para interoperabilidad. Sin embargo, si mezclas estas características, es posible que
no obtengas todos los beneficios y el rendimiento que ofrece el paquete NIO.

Buffers

La mayoría de las utilidades de los paquetes java.io y java.net operan en arreglos de bytes. Las
herramientas correspondientes del paquete NIO se basan en ByteBuffers (con búferes basados en
caracteres CharBuffer para texto). Los arreglos de bytes son simples, ¿entonces por qué son necesarios
los búferes? Sirven varios propósitos:

• Formalizan los patrones de uso para datos en búfer, proporcionan cosas como búferes de solo lectura y
llevan un seguimiento de las posiciones de lectura/escritura y límites dentro de un espacio de búfer
grande. También proporcionan una facilidad de marca/restablecimiento similar a la de
java.io.BufferedInputStream.

• Proporcionan APIs adicionales para trabajar con datos brutos que representan tipos primitivos. Puedes
crear búferes que "vean" tus datos de bytes como una serie de tipos primitivos más grandes, como shorts,
ints o floats. El tipo más general de búfer de datos, ByteBuffer, incluye métodos que te permiten leer y
escribir todos los tipos primitivos de la misma manera que DataOutputStream lo hace para flujos.

• Abstraen el almacenamiento subyacente de los datos, permitiendo optimizaciones especiales por parte
de Java. Específicamente, los búferes pueden asignarse como búferes directos que utilizan búferes nativos
del sistema operativo anfitrión en lugar de arreglos en la memoria de Java. Las facilidades de Canal de NIO
que trabajan con búferes pueden reconocer búferes directos automáticamente e intentar optimizar la E/S
para usarlos. Por ejemplo, una lectura desde un canal de archivo a un arreglo de bytes de Java
normalmente requiere que Java copie los datos para la lectura desde el sistema operativo anfitrión a la
memoria de Java. Con un búfer directo, los datos pueden permanecer en el sistema operativo anfitrión,
fuera del espacio de memoria normal de Java hasta que sean necesarios.

Operaciones de búfer

Un búfer es una subclase de un objeto Buffer de java.nio. La clase base Buffer es algo así como un arreglo
con estado. No especifica qué tipo de elementos contiene (eso lo deciden los subtipos), pero sí define
funcionalidades comunes a todos los búferes de datos. Un Buffer tiene un tamaño fijo llamado su
capacidad. Aunque todos los Búferes estándar proporcionan "acceso aleatorio" a sus contenidos, un
Buffer generalmente espera ser leído y escrito secuencialmente, por lo que mantiene la noción de una
posición donde se lee o escribe el próximo elemento. Además de la posición, un Buffer puede mantener
dos piezas de información de estado adicionales: un límite, que es una posición que es un límite "suave"
para el alcance de una lectura o escritura, y una marca, que puede usarse para recordar una posición
anterior para un recuerdo futuro.
Las implementaciones de Buffer añaden métodos específicos y tipados get y put que leen y escriben los
contenidos del búfer. Por ejemplo, ByteBuffer es un búfer de bytes y tiene métodos get() y put() que leen
y escriben bytes y arreglos de bytes (junto con muchos otros métodos útiles que discutiremos más
adelante). Leer desde y escribir en el Búfer cambia el marcador de posición, por lo que el Búfer lleva un
seguimiento de sus contenidos de alguna manera similar a un flujo. Intentar leer o escribir más allá del
límite genera una BufferUnderflowException o BufferOverflowException, respectivamente.

La marca, la posición, el límite y los valores de capacidad siempre obedecen la siguiente fórmula:

La posición para leer y escribir en el búfer siempre se encuentra entre la marca, que sirve como límite
inferior, y el límite, que sirve como límite superior. La capacidad representa la extensión física del espacio
del búfer.

Puedes establecer explícitamente los marcadores de posición y límite con los métodos position() y limit().
Se proporcionan varios métodos de conveniencia para patrones de uso comunes. El método reset()
establece la posición de nuevo en la marca. Si no se ha establecido ninguna marca, se genera una
InvalidMarkException. El método clear() restablece la posición en 0 y establece el límite como la
capacidad, preparando el búfer para nuevos datos (se descarta la marca). Ten en cuenta que el método
clear() en realidad no hace nada con los datos en el búfer; simplemente cambia los marcadores de
posición.

El método flip() se usa para el patrón común de escribir datos en el búfer y luego leerlos de vuelta. flip
establece la posición actual como el límite y luego restablece la posición actual en 0 (se elimina cualquier
marca), lo que evita tener que hacer un seguimiento de cuántos datos se leyeron. Otro método, rewind(),
simplemente restablece la posición en 0, dejando el límite intacto. Podrías usarlo para escribir datos del
mismo tamaño nuevamente. Aquí tienes un fragmento de código que utiliza estos métodos para leer
datos de un canal y escribirlos en dos canales:

Esto puede resultar confuso la primera vez que se observa, ya que aquí, la lectura desde el Canal en
realidad es una escritura en el Búfer y viceversa. Debido a que este ejemplo escribe todos los datos
disponibles hasta el límite, tanto flip() como rewind() tienen el mismo efecto en este caso.

Tipos de búfer

Como se mencionó anteriormente, varios tipos de búfer agregan métodos get y put para leer y escribir
tipos de datos específicos. Cada uno de los tipos primitivos de Java tiene un tipo de búfer asociado:
ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer y DoubleBuffer. Cada uno
proporciona métodos get y put para leer y escribir su tipo y matrices de su tipo. De estos, ByteBuffer es
el más flexible. Debido a que tiene el "grano más fino" de todos los búferes, se le ha dado un conjunto
completo de métodos get y put para leer y escribir todos los otros tipos de datos, así como bytes. Aquí
hay algunos métodos de ByteBuffer:
Como dijimos, todos los búferes estándar también admiten acceso aleatorio. Para cada uno de los
métodos mencionados de ByteBuffer, existe una forma adicional que toma un índice; por ejemplo:

Pero eso no es todo. ByteBuffer también puede proporcionar "vistas" de sí mismo como cualquiera de los
tipos de grano grueso. Por ejemplo, puedes obtener una vista ShortBuffer de un ByteBuffer con el método
asShortBuffer(). La vista ShortBuffer está respaldada por el ByteBuffer, lo que significa que trabajan en los
mismos datos y los cambios en uno afectan al otro. La extensión del búfer de vista comienza en la posición
actual del ByteBuffer, y su capacidad es una función del número restante de bytes, dividido por el tamaño
del nuevo tipo. (Por ejemplo, los shorts consumen dos bytes cada uno, los floats cuatro, y los longs y los
doubles ocupan ocho). Los búferes de vista son convenientes para leer y escribir bloques grandes de un
tipo contiguo dentro de un ByteBuffer.

Los CharBuffers también son interesantes, principalmente por su integración con Strings. Tanto
CharBuffers como Strings implementan la interfaz java.lang.CharSequence. Esta es la interfaz que
proporciona los métodos estándar charAt() y length(). Debido a esto, API más recientes (como el paquete
java.util.regex) te permiten utilizar un CharBuffer o un String de forma intercambiable. En este caso, el
CharBuffer actúa como un String modificable con posiciones de inicio y final lógicas configurables por el
usuario.

Orden de bytes

Debido a que estamos hablando de leer y escribir tipos mayores que un byte, surge la pregunta: ¿en qué
orden se escriben los bytes de valores de varios bytes (por ejemplo, shorts e ints)? Hay dos métodos en
este mundo: "big endian" y "little endian". Big endian significa que los bytes más significativos van
primero; little endian es lo contrario. Si estás escribiendo datos binarios para su consumo por alguna
aplicación nativa, esto es importante. Las computadoras compatibles con Intel utilizan little endian, y
muchas estaciones de trabajo que ejecutan Unix utilizan big endian. La clase ByteOrder encapsula la
elección. Puedes especificar el orden de bytes a utilizar con el método order() de ByteBuffer, utilizando
los identificadores ByteOrder.BIG_ENDIAN y ByteOrder.LITTLE_ENDIAN, así:

Puedes obtener el orden nativo para tu plataforma utilizando el método estático


ByteOrder.nativeOrder(). (Sabemos que tienes curiosidad).
Asignación de búferes

Puedes crear un búfer ya sea asignándolo explícitamente utilizando allocate() o envolviendo un tipo de
array de Java existente. Cada tipo de búfer tiene un método allocate() estático que toma una capacidad
(tamaño) y también un método wrap() que toma un array existente:

Un búfer directo se asigna de la misma manera, utilizando el método allocateDirect():

Como describimos anteriormente, los buffers directos pueden utilizar estructuras de memoria del sistema
operativo optimizadas para ciertos tipos de operaciones de entrada/salida. El compromiso es que asignar
un buffer directo es una operación un poco más lenta y pesada que un buffer normal, por lo que se debe
intentar usarlos para buffers a largo plazo.

Codificadores y decodificadores de caracteres

Los codificadores y decodificadores convierten caracteres en bytes brutos y viceversa, mapeando desde
el estándar Unicode a esquemas de codificación particulares. En Java, los codificadores y decodificadores
han existido desde hace tiempo para ser utilizados por flujos de Reader y Writer, así como en los métodos
de la clase String que trabajan con matrices de bytes. Sin embargo, al principio no existía una API para
trabajar con codificación explícitamente; simplemente se hacía referencia a codificadores y
decodificadores siempre que fuera necesario mediante su nombre como una cadena. El paquete
java.nio.charset formalizó la idea de una codificación de conjunto de caracteres Unicode con la clase
Charset.

La clase Charset es una fábrica de instancias de Charset, que saben cómo codificar buffers de caracteres
a buffers de bytes y decodificar buffers de bytes a buffers de caracteres. Puedes buscar un conjunto de
caracteres por su nombre con el método estático Charset.forName() y usarlo en conversiones.

También puedes probar para ver si una codificación está disponible con el método estático
Charset.isSupported().

Los siguientes conjuntos de caracteres están garantizados que serán suministrados:

• US-ASCII

• ISO-8859-1

• UTF-8

• UTF-16BE

• UTF-16LE

• UTF-16

Puedes listar todos los codificadores disponibles en tu plataforma usando el método estático
availableCharsets().
El resultado de availableCharsets() es un mapa debido a que los conjuntos de caracteres pueden tener
"alias" y aparecer bajo más de un nombre.

Además de las clases orientadas a los buffers del paquete java.nio, las clases puente InputStream Reader
y OutputStreamWriter del paquete java.io también han sido actualizadas para funcionar con Charset.
Puedes especificar la codificación como un objeto Charset o por su nombre.

CharsetEncoder y CharsetDecoder

Puedes tener más control sobre el proceso de codificación y decodificación creando una instancia de
CharsetEncoder o CharsetDecoder (un codec) con los métodos newEncoder() y newDecoder() de Charset.
En el fragmento anterior, asumimos que todos los datos estaban disponibles en un solo buffer. Sin
embargo, con más frecuencia podríamos tener que procesar datos a medida que llegan en fragmentos.
La API del codificador/decodificador permite esto proporcionando métodos encode() y decode() más
generales que toman una bandera especificando si se espera más datos. El codec necesita saber esto
porque podría haber quedado a medio camino de una conversión de caracteres multibyte cuando se
acabaron los datos. Si sabe que vienen más datos, no genera un error en esta conversión incompleta. En
el siguiente fragmento, usamos un decodificador para leer desde un ByteBuffer bbuff y acumular datos
de caracteres en un CharBuffer cbuff:

Aquí, buscamos la condición de final de entrada en el canal de entrada (in channel) para establecer la
bandera 'done'. Ten en cuenta que aprovechamos el método flip() en ByteBuffer para establecer el límite
en la cantidad de datos leídos y restablecer la posición, preparándonos para la operación de
decodificación en un solo paso. Los métodos encode() y decode() también devuelven un objeto de
resultado, CoderResult, que puede determinar el progreso de la codificación (no lo usamos en el
fragmento anterior). Los métodos isError(), isUnderflow() e isOverflow() en CoderResult especifican por
qué se detuvo la codificación: por un error, falta de bytes en el búfer de entrada o un búfer de salida
completo, respectivamente.

FileChannel

Ahora que hemos cubierto los conceptos básicos de canales y buffers, es hora de ver un tipo de canal real.
El FileChannel es el equivalente en NIO del java.io.RandomAccessFile, pero proporciona varias
características nuevas además de algunas optimizaciones de rendimiento. En particular, usa un
FileChannel en lugar de un simple flujo de archivo (file stream) de java.io si deseas utilizar bloqueo de
archivos (file locking), acceso a archivos mapeados en memoria (memory-mapped file access) o
transferencia de datos altamente optimizada entre archivos o entre canal de archivo y de red.

Un FileChannel se puede crear para un Path utilizando el método estático FileChannel.open():


De forma predeterminada, open() crea un canal de solo lectura para el archivo. Podemos abrir un canal
para escritura o agregación y controlar otras características más avanzadas, como la creación atómica y
la sincronización de datos, pasando opciones adicionales, como se muestra en la segunda parte del
ejemplo anterior. La Tabla 11-3 resume estas opciones.

Tabla 11-3. java.nio.file.StandardOpenOption

READ, WRITE Abre el archivo solo para lectura o solo para


escritura (el predeterminado es solo lectura).
Utiliza ambos para leer y escribir.

APPEND Abre el archivo para escritura; todas las escrituras


se posicionan al final del archivo.
CREATE Utiliza con WRITE para abrir el archivo y crearlo si
es necesario.
CREATE_NEW Utiliza con WRITE para crear un archivo
atómicamente; falla si el archivo ya existe.

DELETE_ON_CLOSE Intenta eliminar el archivo cuando se cierra o, si


está abierto, cuando la máquina virtual se cierra.

SYNC, DSYNC Si es posible, garantiza que las operaciones de


escritura se bloqueen hasta que todos los datos se
escriban en el almacenamiento. SYNC hace esto
para todos los cambios de archivos, incluidos
datos y metadatos (atributos), mientras que
DSYNC solo agrega este requisito para el
contenido de datos del archivo.

SPARSE Utiliza al crear un archivo nuevo; solicita que el


archivo sea espaciado. En sistemas de archivos
donde esto es compatible, un archivo espaciado
maneja archivos muy grandes y mayormente
vacíos sin asignar tanto almacenamiento real para
las porciones vacías.

TRUNCATE_EXISTING Utiliza WRITE en un archivo existente; establece la


longitud del archivo en cero al abrirlo.

Un FileChannel también puede ser construido a partir de un clásico FileInputStream, FileOutputStream o


RandomAccessFile:
Los FileChannels creados a partir de estos flujos de entrada y salida de archivos son solo de lectura o solo
de escritura, respectivamente. Para obtener un FileChannel de lectura/escritura, debes construir un
RandomAccessFile con opciones de lectura/escritura, como en el ejemplo anterior.

El uso de un FileChannel es similar a un RandomAccessFile, pero trabaja con un ByteBuffer en lugar de


matrices de bytes:

Puedes controlar cuántos datos se leen y escriben ya sea configurando la posición y los límites del buffer
o utilizando otra forma de lectura/escritura que tome una posición inicial y longitud del buffer. También
puedes leer y escribir en una posición aleatoria suministrando índices con los métodos de lectura y
escritura:

En cada caso, la cantidad real de bytes leídos o escritos depende de varios factores. La operación intenta
leer o escribir hasta el límite del buffer, y la gran mayoría del tiempo es lo que sucede con el acceso local
a archivos. La operación garantiza bloquearse solo hasta que al menos un byte haya sido procesado.
Independientemente de lo que suceda, se devuelve la cantidad de bytes procesados, y la posición del
buffer se actualiza en consecuencia, preparándote para repetir la operación hasta que esté completa, si
es necesario. Esta es una de las comodidades de trabajar con buffers; pueden gestionar el recuento por
ti. Al igual que los flujos estándar, el método de lectura read() del canal devuelve -1 al alcanzar el final de
la entrada.

El tamaño del archivo siempre está disponible con el método size(). Puede cambiar si escribes más allá
del final del archivo. Por el contrario, puedes truncar el archivo a una longitud especificada con el método
truncate().

Acceso concurrente

Los FileChannels son seguros para ser utilizados por múltiples hilos y garantizan que los datos "vistos" por
ellos sean consistentes entre canales en la misma máquina virtual. A menos que especifiques las opciones
SYNC o DSYNC, no se hacen garantías sobre la rapidez con la que se propagan las escrituras al mecanismo
de almacenamiento. Si solo ocasionalmente necesitas asegurarte de que los datos estén seguros antes de
continuar, puedes usar el método force() para guardar los cambios en el disco. Este método toma un
argumento booleano que indica si se deben escribir los metadatos del archivo, incluyendo la marca de
tiempo y los permisos (sync o dsync). Algunos sistemas mantienen un registro de lecturas en archivos
además de las escrituras, por lo que puedes ahorrar muchas actualizaciones si configuras el indicador
como false, lo que indica que no te importa sincronizar esos datos inmediatamente.

Al igual que con todos los canales, un FileChannel puede ser cerrado por cualquier hilo. Una vez cerrado,
todos sus métodos relacionados con la lectura/escritura y la posición lanzarán una
ClosedChannelException.

Bloqueo de archivos

Los FileChannels admiten bloqueos exclusivos y compartidos en regiones de archivos a través del método
lock():
Los bloqueos pueden ser compartidos o exclusivos. Un bloqueo exclusivo evita que otros adquieran un
bloqueo de cualquier tipo en el archivo o región de archivo especificados. Un bloqueo compartido permite
que otros adquieran bloqueos compartidos superpuestos, pero no bloqueos exclusivos. Estos son útiles
como bloqueos de escritura y lectura, respectivamente. Cuando estás escribiendo, no quieres que otros
puedan escribir hasta que hayas terminado, pero al leer, solo necesitas bloquear a otros de escribir
concurrentemente, no de leer.

El método lock() sin argumentos en el ejemplo anterior intenta adquirir un bloqueo exclusivo para todo
el archivo. La segunda forma acepta un parámetro de inicio y longitud, así como un indicador de si el
bloqueo debe ser compartido (o exclusivo). El objeto FileLock devuelto por el método lock() se puede
utilizar para liberar el bloqueo:

Ten en cuenta que los bloqueos de archivos solo están garantizados como una API cooperativa; no
necesariamente evitan que cualquier persona lea o escriba en los contenidos bloqueados del archivo. En
general, la única forma de garantizar que los bloqueos se cumplan es que ambas partes intenten adquirir
el bloqueo y lo utilicen. Además, en algunos sistemas, los bloqueos compartidos no están implementados,
en cuyo caso todos los bloqueos solicitados son exclusivos. Puedes verificar si un bloqueo es compartido
con el método isShared().

Los bloqueos de FileChannel se mantienen hasta que el canal se cierra o se interrumpe, por lo que realizar
bloqueos dentro de una declaración try-with-resources ayudará a garantizar que los bloqueos se liberen
de manera más sólida:

Programación de Redes

La red es el alma de Java. La mayor parte de lo interesante sobre Java se centra en el potencial para
aplicaciones dinámicas y en red. A medida que las API de red de Java han madurado, Java también se ha
convertido en el lenguaje preferido para implementar aplicaciones y servicios cliente/servidor
tradicionales. En esta sección, comenzamos nuestra discusión del paquete java.net, que contiene las
clases fundamentales para comunicaciones y trabajo con recursos en red. ¡La red es un tema amplio, sin
embargo! El Capítulo 12 cubrirá más aspectos relacionados con redes, centrándose en temas relacionados
con internet.

Las clases de java.net se dividen en dos categorías generales: la API de Sockets para trabajar con
protocolos de internet de bajo nivel y APIs de nivel superior orientadas a la web que trabajan con
Localizadores Uniformes de Recursos (URLs). La Figura 11-3 muestra el paquete java.net.
El API de Sockets de Java proporciona acceso a los protocolos estándar de red utilizados para la
comunicación entre hosts en Internet. Los sockets son el mecanismo subyacente de todos los demás tipos
de comunicaciones en red portátiles. Los sockets son la herramienta de nivel más bajo en el conjunto
general de herramientas de redes: se pueden utilizar para cualquier tipo de comunicación entre
aplicaciones cliente y servidor o entre pares en la red, pero se deben implementar tus propios protocolos
a nivel de aplicación para manejar e interpretar los datos. Herramientas de redes de nivel superior, como
la invocación de método remoto, HTTP y servicios web, se implementan sobre los sockets.

En la actualidad, los servicios web es el término para la tecnología más general que proporciona la
invocación de servicios en servidores remotos de manera independiente de la plataforma y con
acoplamiento flexible, utilizando estándares web como HTTP y JSON. Hablaremos sobre servicios web en
el Capítulo 12 cuando discutamos la programación para la web.

En este capítulo, proporcionaremos algunos ejemplos simples y prácticos de programación en red en Java,
tanto a nivel alto como bajo, utilizando sockets. En el Capítulo 12, analizaremos la otra mitad del paquete
java.net, que permite a los clientes trabajar con servidores web y servicios a través de URLs. También se
introducen los servlets de Java y las herramientas que te permiten escribir tus propias aplicaciones y
servicios web.

Sockets

Los sockets son una interfaz de programación de bajo nivel para comunicaciones en red. Envían flujos de
datos entre aplicaciones que pueden estar o no en el mismo host. Los sockets se originaron en BSD Unix
y son, en algunos lenguajes de programación, cosas complicadas y complejas con muchas partes pequeñas
que pueden romperse y poner en peligro a los niños pequeños. La razón de esto es que la mayoría de las
API de sockets se pueden usar con casi cualquier tipo de protocolo de red subyacente. Dado que los
protocolos que transportan datos a través de la red pueden tener características radicalmente diferentes,
la interfaz de socket puede ser bastante compleja.

El paquete java.net admite una interfaz de socket simplificada y orientada a objetos que facilita
considerablemente las comunicaciones en red. Si has hecho programación de red utilizando sockets en
otros lenguajes, seguramente te sorprenderá lo simple que pueden ser las cosas cuando los objetos
encapsulan los detalles complejos. Si es la primera vez que te encuentras con sockets, descubrirás que
hablar con otra aplicación a través de la red puede ser tan simple como leer un archivo o recibir entrada
del usuario. La mayoría de las formas de E/S en Java, incluida la mayoría de las E/S de red, utilizan las
clases de flujo descritas en "Flujos" en la página 343. Los flujos proporcionan una interfaz de E/S unificada
para que la lectura o escritura a través de Internet sea similar a la lectura o escritura en el sistema local.
Además de las interfaces orientadas a flujos, las API de redes de Java pueden trabajar con la API orientada
a buffers de Java NIO para aplicaciones altamente escalables. Veremos ambas en este capítulo.

Java proporciona sockets para admitir tres clases distintas de protocolos subyacentes: Sockets,
DatagramSockets y MulticastSockets. En esta sección, examinaremos la clase básica Socket de Java, que
utiliza un protocolo orientado a la conexión y confiable. Un protocolo orientado a la conexión proporciona
el equivalente a una conversación telefónica. Después de establecer una conexión, dos aplicaciones
pueden enviar flujos de datos de ida y vuelta, y la conexión permanece en su lugar incluso cuando nadie
está hablando. Debido a que el protocolo es confiable, también garantiza que no se pierdan datos
(reenviándolos si es necesario) y que todo lo que envíes siempre llegue en el orden en que lo enviaste.

Dejaremos la clase DatagramSocket, que utiliza un protocolo sin conexión e inconfiable, para que la
explores por tu cuenta. (Podrías empezar con el libro "Java Network Programming" de Elliotte Rusty
Harold, O'Reilly). Un protocolo sin conexión es como el servicio postal. Las aplicaciones pueden enviarse
mensajes cortos entre sí, pero no se establece una conexión de extremo a extremo por adelantado y no
se intenta mantener los mensajes en orden. Ni siquiera se garantiza que los mensajes lleguen en absoluto.
Un MulticastSocket es una variación de un DatagramSocket que realiza multicast, enviando datos
simultáneamente a múltiples destinatarios. Trabajar con sockets de multidifusión es muy similar a trabajar
con sockets de datagrama.

En teoría, casi cualquier protocolo puede ser utilizado debajo de la capa de sockets (los veteranos
recordarán cosas como IPX de Novell, AppleTalk de Apple, etc.). Pero en la práctica, solo hay una familia
de protocolos importante utilizada en Internet y solo una familia de protocolos que Java admite: el
Protocolo de Internet (IP). La clase Socket habla TCP, la variante orientada a la conexión de IP, y la clase
DatagramSocket habla UDP, el tipo sin conexión.
Clientes y Servidores

Cuando se escriben aplicaciones de red, es común hablar de clientes y servidores. La distinción es cada
vez más vaga, pero el lado que inicia la conversación generalmente se considera el cliente. El lado que
acepta la solicitud suele ser el servidor. En el caso en que dos aplicaciones pares usen sockets para hablar,
la distinción es menos importante, pero para simplificar, usaremos esta definición.

Para nuestros propósitos, la diferencia más importante entre un cliente y un servidor es que un cliente
puede crear un socket para iniciar una conversación con una aplicación servidor en cualquier momento,
mientras que un servidor debe estar preparado de antemano para escuchar conversaciones entrantes. La
clase java.net.Socket representa un lado de una conexión de socket individual tanto en el cliente como en
el servidor. Además, el servidor utiliza la clase java.net.ServerSocket para escuchar nuevas conexiones de
clientes. En la mayoría de los casos, una aplicación que actúa como servidor crea un objeto ServerSocket
y espera, bloqueado en una llamada a su método accept(), hasta que llegue una conexión. Cuando llega,
el método accept() crea un objeto Socket que el servidor utiliza para comunicarse con el cliente. Un
servidor puede mantener conversaciones con múltiples clientes a la vez; en este caso, todavía hay solo un
ServerSocket, pero el servidor tiene múltiples objetos Socket, uno asociado con cada cliente, como se
muestra en la Figura 11-4.

A nivel de socket, un cliente necesita dos piezas de información para ubicar y conectarse a un servidor en
Internet: un nombre de host (utilizado para encontrar la dirección de red de la computadora host) y un
número de puerto. El número de puerto es un identificador que diferencia entre múltiples clientes o
servidores en el mismo host. Una aplicación de servidor escucha en un puerto preestablecido mientras
espera conexiones. Los clientes utilizan el número de puerto asignado al servicio al que desean acceder.
Si piensas en las computadoras host como hoteles y las aplicaciones como huéspedes, los puertos son
como los números de habitación de los huéspedes. Para que una persona llame a otra, deben conocer el
nombre del hotel de la otra parte y el número de habitación.

Clientes

Una aplicación cliente abre una conexión a un servidor construyendo un Socket que especifica el nombre
de host y el número de puerto del servidor deseado:
Este fragmento de código intenta conectar un Socket al puerto 25 (el servicio de correo SMTP) del host
wupost.wustl.edu. El cliente maneja la posibilidad de que el nombre de host no pueda resolverse
(UnknownHostException) y de que pueda no ser capaz de conectarse a él (IOException). En este caso, Java
utilizó DNS, el servicio estándar de nombres de dominio, para resolver el nombre de host a una dirección
IP por nosotros. El constructor también puede aceptar una cadena que contenga la dirección IP cruda del
host:

Después de establecer una conexión, se pueden obtener flujos de entrada y salida con los métodos Socket
getInputStream() y getOutputStream(). El siguiente código (bastante arbitrario) envía y recibe algunos
datos con los flujos:

En este intercambio, el cliente primero crea un Socket para comunicarse con el servidor. El constructor
de Socket especifica el nombre del servidor (foo.bar.com) y un número de puerto preestablecido (1234).
Una vez establecida la conexión, el cliente escribe un solo byte al servidor utilizando el método write() de
OutputStream. Para enviar más fácilmente una cadena de texto, envuelve un PrintWriter alrededor del
OutputStream. Luego realiza las operaciones complementarias: lee un byte del servidor utilizando el
método read() de InputStream y luego crea un BufferedReader para obtener una cadena completa de
texto. Posteriormente, el cliente termina la conexión con el método close(). Todas estas operaciones
tienen el potencial de generar IOExceptions; nuestra aplicación manejará estas excepciones utilizando la
cláusula catch.

Servidores

Después de establecer una conexión, una aplicación de servidor utiliza el mismo tipo de objeto Socket
para su lado de las comunicaciones. Sin embargo, para aceptar una conexión de un cliente, primero debe
crear un ServerSocket, vinculado al puerto correcto. Vamos a recrear la conversación anterior desde el
punto de vista del servidor:
Primero, nuestro servidor crea un ServerSocket adjunto al puerto 1234. En algunos sistemas, existen
reglas sobre qué puertos puede usar una aplicación. Los números de puerto por debajo de 1024
generalmente están reservados para procesos del sistema y servicios estándar bien conocidos, por lo que
elegimos un número de puerto fuera de este rango. El ServerSocket se crea solo una vez; a partir de
entonces, podemos aceptar tantas conexiones como lleguen.

Luego, ingresamos a un bucle, esperando a que el método accept() de ServerSocket devuelva una
conexión de Socket activa desde un cliente. Cuando se establece una conexión, realizamos el lado del
servidor de nuestro diálogo, luego cerramos la conexión y regresamos al inicio del bucle para esperar otra
conexión. Finalmente, cuando la aplicación del servidor desea dejar de escuchar conexiones por
completo, llama al método close() de ServerSocket.

Este servidor es de un solo hilo; maneja una conexión a la vez, no llama a accept() para escuchar una
nueva conexión hasta que haya terminado con los Sockets | 3835 De hecho, el sitio disponible
públicamente que utilizamos de NIST anima fuertemente a los usuarios a realizar actualizaciones.
Consulta las notas introductorias para obtener más información.

la conexión actual. Un servidor más realista tendría un bucle que acepta conexiones de forma simultánea
y las pasa a sus propios hilos para su procesamiento, o quizás utilizaría un ServerSocketChannel no
bloqueante.

Sockets y seguridad

Los ejemplos anteriores presuponen que el cliente tiene permiso para conectarse al servidor y que al
servidor se le permite escuchar en el socket especificado. Si estás escribiendo una aplicación general
independiente, normalmente esto es cierto (y probablemente puedas omitir esta sección). Sin embargo,
las aplicaciones no confiables se ejecutan bajo los auspicios de un administrador de seguridad que puede
imponer restricciones arbitrarias sobre con qué hosts pueden o no pueden comunicarse y si pueden o no
pueden escuchar conexiones.

Si vas a ejecutar tu propia aplicación bajo un administrador de seguridad, debes saber que el
administrador de seguridad predeterminado no permite acceso a la red. Por lo tanto, para realizar
conexiones de red, tendrías que modificar tu archivo de políticas para otorgar los permisos adecuados a
tu código (consulta el Capítulo 3 para más detalles). El siguiente fragmento de archivo de políticas
establece los permisos de socket para permitir conexiones hacia o desde cualquier host en cualquier
puerto no privilegiado:

Al iniciar el tiempo de ejecución de Java, puedes instalar el administrador de seguridad y utilizar este
archivo (llamémoslo mysecurity.policy):

El Cliente DateAtHost

En el pasado, muchos ordenadores en red ejecutaban un servicio de tiempo simple que proporcionaba la
hora local de su reloj en un puerto bien conocido. Esto fue un precursor del NTP, el Protocolo de Tiempo
de Red más general.5 El siguiente ejemplo, DateAtHost, incluye una subclase de java.util.Date que obtiene
la hora de un host remoto en lugar de inicializarse desde el reloj local. (Consulta el Capítulo 8 para obtener
una discusión sobre la clase Date, que aún es útil para algunos casos, pero en su mayoría ha sido
reemplazada por sus primos más nuevos y flexibles, LocalDate y LocalTime).

DateAtHost se conecta al servicio de tiempo (puerto 37) y lee cuatro bytes que representan el tiempo en
el host remoto. Estos cuatro bytes tienen una especificación peculiar que desciframos para obtener la
hora. Aquí está el código:
Eso es todo. No es muy extenso, incluso con algunos detalles adicionales. Hemos proporcionado dos
posibles constructores para DateAtHost. Normalmente, esperaríamos usar el primero, que simplemente
toma el nombre del host remoto como argumento. El segundo constructor especifica el nombre del host
y el número de puerto del servicio de tiempo remoto. (Si el servicio de tiempo se estuviera ejecutando en
un puerto no estándar, usaríamos el segundo constructor para especificar el número de puerto
alternativo). Este segundo constructor hace el trabajo de establecer la conexión y configurar la hora. El
primer constructor simplemente invoca al segundo (utilizando la construcción this()) con el puerto
predeterminado como argumento. Suministrar constructores simplificados que invoquen a sus
equivalentes con argumentos predeterminados es un patrón común y útil en Java; esa es la razón principal
por la que lo hemos mostrado aquí.

El segundo constructor abre un socket en el puerto especificado en el host remoto. Crea un


DataInputStream para envolver el flujo de entrada y luego lee un entero de cuatro bytes usando el método
readInt(). No es coincidencia que los bytes estén en el orden correcto. Las clases DataInputStream y
DataOutputStream de Java trabajan con los bytes de tipos enteros en el orden de bytes de red (de más
significativo a menos significativo). El protocolo de tiempo (y otros protocolos de red estándar que
trabajan con datos binarios) también utiliza el orden de bytes de red, por lo que no necesitamos llamar a
rutinas de conversión. Conversiones de datos explícitas probablemente serían necesarias si estuviéramos
utilizando un protocolo no estándar, especialmente al hablar con un cliente o servidor no Java. En ese
caso, tendríamos que leer byte por byte y hacer algunos reajustes para obtener nuestro valor de cuatro
bytes. Después de leer los datos, terminamos con el socket, así que lo cerramos, finalizando la conexión
con el servidor. Finalmente, el constructor inicializa el resto del objeto llamando al método setTime() de
Date con el valor de tiempo calculado.

Los cuatro bytes del valor de tiempo se interpretan como un entero que representa el número de
segundos desde el comienzo del siglo XX. DateAtHost convierte esto a la noción de tiempo absoluto de
Java: el recuento de milisegundos desde el 1 de enero de 1970 (una fecha arbitraria estandarizada por C
y Unix). La conversión primero crea un valor long, que es el equivalente sin signo del tiempo entero. Resta
un desfase para hacer que el tiempo sea relativo a la época (1 de enero de 1970) en lugar del siglo, y
multiplica por 1.000 para convertir a milisegundos. El tiempo convertido se utiliza para inicializar el objeto.

La clase DateAtHost puede trabajar con un tiempo recuperado de un host remoto casi tan fácilmente
como Date se utiliza con el tiempo en el host local. El único sobrecoste adicional es lidiar con la posible
IOException que puede ser lanzada por el constructor DateAtHost.

Este ejemplo obtiene la hora en el host time.nist.gov e imprime su valor.

Un Juego Distribuido (A Distributed Game)

Podemos utilizar nuestras recién adquiridas habilidades de redes para ampliar nuestro juego de
lanzamiento de manzanas y convertirlo en un juego multijugador. Tendremos que mantener esta
incursión simple, pero podrías sorprenderte de lo rápido que podemos poner en marcha una prueba de
concepto. Si bien existen varios mecanismos que dos jugadores podrían usar para conectarse y compartir
una experiencia, nuestro ejemplo utiliza el modelo básico cliente/servidor que hemos estado discutiendo
en este capítulo. Un usuario iniciará el servidor y el segundo usuario podrá contactar ese servidor como
cliente para "unirse". Una vez que ambos jugadores estén conectados, competirán para ver quién puede
limpiar sus árboles más rápido.
Configurando la interfaz de usuario (UI)

Comencemos agregando un menú a nuestro juego. Recuerda de la sección "Menús" en la página 339 que
los menús se encuentran en una barra de menú y funcionan con objetos ActionEvent de manera similar a
los botones estándar. Necesitamos una opción para iniciar un servidor y otra para unirse a un juego en un
servidor que alguien ya haya iniciado. El código central para estos elementos de menú es sencillo;
podemos utilizar otro método auxiliar en la clase AppleToss:

El uso de clases internas anónimas para cada ActionListener de los menús debería resultarte familiar. (O
consulta "Referencias a métodos" en la página 435 para leer sobre cómo podrías utilizar una característica
introducida en Java 8 para una configuración más compacta). También utilizamos JOptionPane, discutido
en "Diálogos de entrada" en la página 332, para preguntar al segundo jugador el nombre o la dirección IP
del servidor donde está esperando el primer jugador. La lógica de la red se maneja en una clase separada.
Miraremos la clase Multiplayer con más detalle en las secciones siguientes, pero puedes ver los métodos
que implementaremos.6

El servidor del juego

Como antes en "Servidores" en la página 382, necesitamos elegir un puerto y configurar un socket que
esté escuchando una conexión entrante. Usaremos el puerto 8677, que corresponde a "TOSS" en un
teclado de teléfono. Podemos crear una clase interna Server en nuestra clase Multiplayer para manejar
un hilo listo para comunicaciones en red. Con suerte, los otros fragmentos de código en el siguiente
ejemplo te resultarán familiares. Las variables reader y writer se utilizarán para enviar y recibir los datos
reales del juego; más sobre eso en "El protocolo del juego" en la página 391.

Configuramos nuestro ServerSocket y luego esperamos un nuevo cliente dentro de un bucle. Aunque solo
planeamos jugar con un oponente a la vez, esto nos permite aceptar clientes posteriores sin tener que
pasar por toda la configuración de red nuevamente. Para iniciar realmente el servidor escuchando por
primera vez, simplemente necesitamos un nuevo hilo que utilice nuestra clase Server:

Mantenemos una referencia a la instancia de Server en nuestra clase Multiplayer para tener acceso rápido
al cierre de las conexiones si el usuario selecciona la opción "desconectar" del menú, de esta manera:
El indicador keepPlaying se usa principalmente una vez que estamos en nuestro bucle de juego, pero
también resulta útil en lo mencionado anteriormente. Si tenemos una referencia válida al servidor pero
actualmente no estamos jugando un juego (por lo que keepPlaying es falso), sabemos que debemos cerrar
el socket del oyente. El método stopListening() en la clase interna Server es sencillo:

El cliente del juego

La configuración y eliminación del lado del cliente son similares, sin el Server Socket de escucha, por
supuesto. Vamos a reflejar la clase interna Server con una clase interna Client y construir un método run()
inteligente para implementar nuestra lógica de cliente:

Usamos un constructor para la clase Client para pasar el nombre del servidor al que nos conectaremos y
confiamos en la variable gamePort común utilizada por Server para configurar el socket de escucha.
Utilizamos la técnica "try with resources" discutida en "try with Resources" en la página 184 para crear
nuestro socket y asegurarnos de que se limpie cuando hayamos terminado. Dentro de ese bloque try con
recursos, creamos nuestras instancias de reader y writer para la mitad de la conversación del cliente,
como se muestra en la Figura 11-5.
Para poner esto en marcha, agregaremos otro método útil a nuestra clase auxiliar Multiplayer:

No hay necesidad de un método separado disconnect() ya que las variables de estado utilizadas por el
servidor también pueden controlar el cierre educado del cliente. Para el cliente, la referencia al servidor
será nula, por lo que no habrá ningún intento de cerrar un oyente inexistente.

El protocolo del juego

Probablemente hayas notado que omitimos la mayor parte del método run() para las clases Server y
Client. Después de construir y conectar nuestros flujos de datos, el trabajo restante se trata
completamente de enviar y recibir información de manera colaborativa sobre el estado de nuestro juego.
Esta comunicación estructurada es el protocolo del juego. Cada servicio de red tiene un protocolo. Piensa
en la "P" en HTTP. Incluso nuestro simple ejemplo DateAtHost utiliza un protocolo (muy simple) para que
los clientes y servidores sepan quién se espera que hable y quién debe escuchar en un momento dado. Si
cada uno de los dos lados termina esperando a que el otro diga algo (por ejemplo, tanto el servidor como
el cliente están bloqueados en una llamada a reader.readLine()), entonces la conexión parecerá estar
inactiva.

Gestionar esas expectativas de comunicación es el núcleo de cualquier protocolo, pero también son
importantes qué decir y cómo responder. De hecho, esta parte de un protocolo a menudo requiere mucho
trabajo por parte del desarrollador. Parte de la dificultad es que realmente necesitas que ambos lados
prueben tu trabajo a medida que avanzas. No puedes probar un servidor sin un cliente y viceversa.
Construir ambos lados a medida que avanzas puede parecer tedioso, pero vale la pena el esfuerzo
adicional. Como con otros consejos de depuración, arreglar un pequeño cambio incremental es mucho
más sencillo que averiguar qué podría estar mal con un gran bloque de código.

En nuestro ejemplo, haremos que el servidor dirija la conversación. Esta elección es arbitraria: podríamos
haber usado el cliente, o podríamos haber construido una base más elaborada y permitido que tanto el
cliente como el servidor estuvieran a cargo de ciertas cosas simultáneamente. Pero con la decisión de
"servidor a cargo" tomada, podemos intentar un primer paso muy simple en nuestro protocolo. Haremos
que el servidor envíe un comando "NEW_GAME" y luego espere a que el cliente responda con un "OK".
El código del lado del servidor podría verse así:

Si obtenemos la respuesta esperada "OK", podemos proceder a configurar un nuevo juego y compartir las
ubicaciones de los árboles con nuestro oponente, pero más sobre eso en un minuto. El código
correspondiente del lado del cliente para este primer paso fluye de manera similar:

Si compilas y ejecutas el juego en este punto, podrías iniciar tu servidor desde un sistema y luego unirte
a ese juego desde un segundo sistema. (También podrías simplemente iniciar una segunda copia del juego
desde una ventana de terminal separada. En ese caso, el "otro host" sería la palabra clave de red
localhost). Casi inmediatamente después de unirte desde la segunda instancia del juego, deberías ver la
confirmación "¡Comenzando un nuevo juego!" impresa en la terminal del primer juego. ¡Felicidades! Estás
en camino de diseñar un protocolo de juego. Sigamos adelante.

Una vez que sepamos que estamos comenzando un nuevo juego, necesitamos nivelar el campo de juego,
literalmente. El servidor le indicará al juego construir un nuevo campo y luego podrá enviar las
coordenadas de todos los nuevos árboles al cliente. El cliente, a su vez, puede aceptar todos los árboles
entrantes y colocarlos en un campo limpio. Una vez que el servidor haya enviado todos los árboles, puede
enviar un comando "START" y el juego puede comenzar. Seguiremos utilizando cadenas para comunicar
nuestros mensajes. Aquí hay una forma en la que podemos pasar los detalles de nuestros árboles al
cliente:
En el lado del cliente, podemos llamar a readLine() en un bucle para las líneas "TREE" hasta que veamos
la línea "START", de la siguiente manera (con un poco de manejo de errores incluido):

En este punto, ambos juegos deberían tener los mismos árboles y pueden comenzar a jugar para
eliminarlos. El servidor entrará en un bucle de sondeo y enviará la puntuación actual dos veces por
segundo. El cliente responderá con su puntuación actual. Ten en cuenta que ciertamente hay otras
opciones para compartir cambios en la puntuación. Si bien el sondeo es directo, los juegos más avanzados
o los juegos que requieren retroalimentación más inmediata sobre jugadores remotos probablemente
utilizarán opciones de comunicación más directas. Por ahora, principalmente queremos concentrarnos en
una buena ida y vuelta de red, por lo que el sondeo mantiene nuestro código más simple.

El servidor debería seguir enviando la puntuación actual hasta que el jugador local haya eliminado todos
los árboles o veamos una respuesta de finalización de juego por parte del cliente. Necesitaremos analizar
la respuesta del cliente para actualizar la puntuación del otro jugador y estar atentos a que finalicen el
juego o simplemente se desconecten. Ese bucle se vería algo así:
Y una vez más, el cliente reflejará estas acciones. Afortunadamente para el cliente, simplemente está
reaccionando a los comandos que vienen del servidor. No necesitamos un mecanismo de sondeo
separado aquí. Bloqueamos la espera para leer una línea, la analizamos y luego construimos nuestra
respuesta.

Cuando un jugador ha eliminado todos sus árboles, envían (o responden con) un comando "END" que
incluye su puntuación final. En ese momento, preguntamos si los mismos dos jugadores quieren volver a
jugar. Si es así, podemos continuar usando las mismas instancias de reader y writer tanto para el servidor
como para el cliente. Si no quieren continuar, permitiremos que el cliente se desconecte y el servidor
volverá a escuchar para que otro jugador se una.
Y un último fragmento de código correspondiente para el cliente:

Tabla 11-4 resume nuestro protocolo simple.


Podríamos dedicar mucho más tiempo a nuestro juego. Podríamos expandir el protocolo para
permitir múltiples oponentes. Podríamos cambiar el objetivo para despejar los árboles y destruir
a tu oponente. Podríamos hacer el protocolo más bidireccional, permitiendo que el cliente inicie
algunas de las actualizaciones. Podríamos utilizar protocolos alternativos de nivel inferior
admitidos por Java, como UDP en lugar de TCP. De hecho, existen libros enteros dedicados a
juegos, a la programación de redes o a la programación de juegos en red.

Más para explorar


Como siempre, tenemos que dejar esas exploraciones en tus manos, pero esperamos que tengas
una idea del sólido soporte de Java para aplicaciones en red. Si exploras algunos de esos temas
avanzados, sin duda comenzarás con una búsqueda en la web. La World Wide Web es quizás el
mejor ejemplo de un entorno en red. Dado el amplio soporte de Java para redes, no debería
sorprender que Java tenga excelentes características dedicadas al trabajo con la web. El próximo
capítulo introduce algunas de esas características tanto para el cliente, o frontend, como para
el servidor, o backend.
CAPÍTULO 12

Programación para la web

Cuando piensas en la web, probablemente piensas en aplicaciones y servicios basados en la web.


Si te piden profundizar, es posible que consideres herramientas como navegadores web y
servidores web que admiten esas aplicaciones y mueven datos en la red. Pero es importante
señalar que los estándares y protocolos, no las aplicaciones y herramientas en sí mismas, han
permitido el crecimiento de la web. Desde los primeros días de internet, ha habido formas de
mover archivos de un lugar a otro y formatos de documentos tan potentes como HTML, pero no
había un modelo unificador sobre cómo identificar, recuperar y mostrar información, ni existía
una forma universal para que las aplicaciones interactuaran con esos datos en la red. Desde que
comenzó la explosión de la web, HTML ha reinado supremo como un formato común para
documentos, y la mayoría de los desarrolladores tienen al menos cierta familiaridad con él. En
este capítulo, vamos a hablar un poco sobre su pariente, HTTP, el protocolo que maneja las
comunicaciones entre clientes y servidores web, y las URL (Localizadores Uniformes de
Recursos), que proporcionan un estándar para nombrar y direccionar objetos en la web. Java
proporciona una API muy simple para trabajar con URLs y direccionar objetos en la web. En este
capítulo, discutiremos cómo escribir clientes web que puedan interactuar con los servidores
usando los métodos HTTP GET y POST, y también hablaremos un poco sobre los servicios web,
que son el siguiente paso en la cadena evolutiva. En "Aplicaciones web de Java" en la página
409, nos trasladaremos al lado del servidor y echaremos un vistazo a los servlets y servicios web,
que son programas Java que se ejecutan en servidores web e implementan el otro lado de estas
conversaciones.

Localizadores Uniformes de Recursos


Una URL apunta a un objeto en internet. Es una cadena de texto que identifica un elemento, te
dice dónde encontrarlo y especifica un método para comunicarte con él o recuperarlo de su
origen. Una URL puede referirse a cualquier tipo de fuente de información. Podría apuntar a
datos estáticos, como un archivo en un sistema de archivos local, un servidor web o un sitio FTP;
o puede apuntar a un objeto más dinámico, como un suministro de noticias RSS o un registro en
una base de datos. Las URLs incluso pueden referirse a recursos más dinámicos, como sesiones
de comunicación y direcciones de correo electrónico.
Debido a que hay muchas formas diferentes de localizar un elemento en internet, y diferentes
medios y transportes requieren diferentes tipos de información, las URLs pueden tener muchas
formas. La forma más común tiene cuatro componentes: un host o servidor de red, el nombre
del elemento, su ubicación en ese host y un protocolo mediante el cual el host debe
comunicarse.

El protocolo (también llamado el "esquema") es un identificador como http o ftp; el nombre del
host generalmente es un host de internet y un nombre de dominio; y los componentes de la
ruta y del elemento forman una ruta única que identifica el objeto en ese host. Variantes de esta
forma permiten empaquetar información adicional en la URL, especificando, por ejemplo,
números de puerto para el protocolo de comunicaciones e identificadores de fragmentos que
hacen referencia a secciones dentro de documentos. Otros tipos más especializados de URLs,
como las URLs "mailto" para direcciones de correo electrónico o URLs para direccionar cosas
como componentes de bases de datos, pueden no seguir este formato precisamente, pero sí se
ajustan a la noción general de un protocolo seguido por un identificador único. (Algunos de estos
serían más apropiadamente llamados URI, Identificadores de Recursos Uniformes. Los URIs
pueden especificar el nombre o la ubicación de un recurso. Las URLs son un subconjunto de
URIs).
Debido a que la mayoría de las URLs tienen la noción de una jerarquía o ruta, a veces hablamos
de una URL que es relativa a otra URL, llamada URL base. En ese caso, estamos utilizando la URL
base como punto de partida y proporcionando información adicional para apuntar a un objeto
relativo a esa URL. Por ejemplo, la URL base podría apuntar a un directorio en un servidor web
y una URL relativa podría nombrar un archivo particular en ese directorio o en un subdirectorio.

La Clase URL
Llevando esto a un nivel más concreto está la clase URL de Java. La clase URL representa una
dirección URL y proporciona una API simple para acceder a recursos web, como documentos y
aplicaciones en servidores. Puede utilizar un conjunto extensible de controladores de protocolo
y contenido para realizar la comunicación necesaria y, en teoría, incluso la conversión de datos.
Con la clase URL, una aplicación puede abrir una conexión a un servidor en la red y recuperar
contenido con solo unas pocas líneas de código. A medida que evolucionan nuevos tipos de
servidores y nuevos formatos para el contenido, se pueden suministrar controladores de URL
adicionales para recuperar e interpretar los datos sin modificar sus aplicaciones.
Una URL está representada por una instancia de la clase java.net.URL. Un objeto URL gestiona
toda la información de los componentes dentro de una cadena URL y proporciona métodos para
recuperar el objeto que identifica. Podemos construir un objeto URL a partir de una cadena URL
o a partir de sus partes componentes:

Estos dos objetos URL apuntan al mismo recurso de red, el documento homepage.html en el
servidor foo.bar.com. Si el recurso realmente existe y está disponible no se conoce hasta que
intentamos acceder a él. Cuando se construye inicialmente, el objeto URL contiene únicamente
datos sobre la ubicación del objeto y cómo acceder a él. No se ha establecido ninguna conexión
con el servidor. Podemos examinar las diferentes partes de la URL con los métodos
getProtocol(), getHost() y getFile(). También podemos compararlo con otra URL usando el
método sameFile() (un nombre desafortunado para algo que puede que no apunte a un archivo),
que determina si dos URL apuntan al mismo recurso. No es infalible, pero sameFile() hace más
que comparar las cadenas de URL por igualdad; tiene en cuenta la posibilidad de que un servidor
pueda tener varios nombres, así como otros factores. Sin embargo, no llega al punto de
recuperar los recursos y compararlos.
Cuando se crea una URL, su especificación se analiza para identificar solo el componente del
protocolo. Si el protocolo no tiene sentido, o si Java no puede encontrar un controlador de
protocolo para él, el constructor de URL lanza una MalformedURLException. Un controlador de
protocolo es una clase Java que implementa el protocolo de comunicaciones para acceder al
recurso URL. Por ejemplo, dado un URL http, Java se prepara para usar el controlador de
protocolo HTTP para recuperar documentos del servidor web especificado.
A partir de Java 7, se garantiza que se proporcionen controladores de protocolo de URL para
http, https (HTTP seguro) y ftp, así como para URL de archivos locales y URL de archivos jar que
se refieran a archivos dentro de archivos JAR. Fuera de eso, las cosas se complican un poco.
Hablaremos más sobre los problemas relacionados con el contenido y los controladores de
protocolo un poco más adelante en este capítulo.

Datos de flujo
La forma más básica y general de obtener datos de una URL es solicitar un InputStream desde la
URL llamando a openStream(). Obtener los datos como un flujo también puede ser útil si deseas
recibir actualizaciones continuas de una fuente de información dinámica. La desventaja es que
debes analizar el contenido del flujo de bytes por ti mismo. Trabajar en este modo es
básicamente lo mismo que trabajar con un flujo de bytes de comunicaciones de socket, pero el
controlador de protocolo de URL ya ha tratado todas las comunicaciones con el servidor y te
está proporcionando solo la parte de contenido de la transacción. No todos los tipos de URL
admiten el método openStream() porque no todos los tipos de URL se refieren a datos
concretos; obtendrás una UnknownServiceException si la URL no lo admite.
El siguiente código (una simplificación del archivo Read.java disponible en la carpeta de ejemplos
para este capítulo) imprime el contenido de un archivo HTML desde un servidor web:

Pedimos un InputStream con openStream() y lo envolvemos en un BufferedReader para leer las


líneas de texto. Debido a que especificamos el protocolo http en la URL, recurrimos a los
servicios de un controlador de protocolo HTTP. Ten en cuenta que aún no hemos hablado sobre
controladores de contenido. En este caso, debido a que estamos leyendo directamente desde
el flujo de entrada, no hay un controlador de contenido involucrado (ninguna transformación de
los datos de contenido).

Obtener el contenido como un objeto


Como dijimos anteriormente, leer contenido en bruto de un flujo es el mecanismo más general
para acceder a datos en la web. openStream() deja el análisis de datos en tus manos. Sin
embargo, la clase URL fue diseñada para admitir un mecanismo de manejo de contenido más
sofisticado y adaptable. Lo discutiremos ahora, pero ten en cuenta que no se usa ampliamente
debido a la falta de estandarización y a limitaciones en la forma en que se pueden implementar
nuevos manejadores. Aunque la comunidad de Java ha progresado en los últimos años en
estandarizar un pequeño conjunto de controladores de protocolo, no se hizo un esfuerzo similar
para estandarizar los controladores de contenido. Esto significa que aunque esta parte de la
discusión es interesante, su utilidad es limitada.
Si Java conoce el tipo de contenido que se está recuperando de una URL y hay un controlador
de contenido adecuado disponible, puedes recuperar el contenido de la URL como un objeto
Java apropiado llamando al método getContent() de la URL. En este modo de funcionamiento,
getContent() inicia una conexión al host, recupera los datos para ti, determina el tipo de datos y
luego invoca a un controlador de contenido para convertir los bytes en un objeto Java. Java
intentará determinar el tipo de contenido mirando su tipo MIME, su extensión de archivo, o
incluso examinando directamente los bytes.
Por ejemplo, dada la URL http://foo.bar.com/index.html, una llamada a getContent() utiliza el
controlador de protocolo HTTP para recuperar datos y podría usar un controlador de contenido
HTML para convertir los datos en un objeto de documento apropiado. Del mismo modo, un
archivo GIF podría convertirse en un objeto ImageProducer de AWT usando un controlador de
contenido de GIF. Si accedemos al archivo GIF usando una URL FTP, Java utilizaría el mismo
controlador de contenido pero un controlador de protocolo diferente para recibir los datos.
Dado que el controlador de contenido debe poder devolver cualquier tipo de objeto, el tipo de
retorno de getContent() es Object. Esto podría dejarnos preguntándonos qué tipo de objeto
obtuvimos. En un momento, describiremos cómo podríamos preguntarle al controlador de
protocolo sobre el tipo MIME del objeto. Con base en esto, y cualquier otro conocimiento que
tengamos sobre el tipo de objeto que esperamos, podemos convertir el Object a su tipo más
específico y adecuado. Por ejemplo, si esperamos una imagen, podríamos convertir el resultado
de getContent() a ImageProducer:

Varios tipos de errores pueden ocurrir al intentar recuperar los datos. Por ejemplo, getContent()
puede lanzar una IOException si hay un error de comunicación. Otros tipos de errores pueden
ocurrir a nivel de la aplicación: se requiere cierto conocimiento sobre cómo los controladores
de contenido y de protocolo específicos de la aplicación manejan los errores. Un problema que
podría surgir es que un controlador de contenido para el tipo MIME de los datos no esté
disponible. En este caso, getContent() invoca un controlador especial de "tipo desconocido" que
devuelve los datos como un InputStream en bruto (volviendo al punto de partida).
En algunas situaciones, también podemos necesitar conocimiento sobre el controlador de
protocolo. Por ejemplo, considera una URL que hace referencia a un archivo inexistente en un
servidor HTTP. Cuando se solicita, el servidor devuelve el conocido mensaje "404 No
Encontrado". Para manejar operaciones específicas del protocolo como esta, es posible que
necesitemos hablar con el controlador de protocolo, lo cual discutiremos a continuación.

Gestión de conexiones
Al llamar a openStream() o getContent() en una URL, se consulta al controlador de protocolo y
se establece una conexión con el servidor remoto o la ubicación. Las conexiones están
representadas por un objeto URLConnection, cuyos subtipos gestionan diferentes
comunicaciones específicas del protocolo y ofrecen metadatos adicionales sobre la fuente. La
clase HttpURLConnection, por ejemplo, maneja solicitudes web básicas y también agrega
algunas capacidades específicas de HTTP, como interpretar mensajes de "404 No Encontrado" y
otros errores del servidor web. Hablaremos más sobre HttpURLConnection más adelante en este
capítulo.
Podemos obtener un URLConnection de nuestra URL directamente con el método
openConnection(). Una de las cosas que podemos hacer con URLConnection es solicitar el tipo
de contenido del objeto antes de leer datos. Por ejemplo:

A pesar de su nombre, un objeto URLConnection se crea inicialmente en un estado sin conexión,


es decir, no se inicia la conexión de red hasta que se llame explícitamente al método
getContentType() o se invoque de forma explícita el método connect(). El objeto URLConnection
no se comunica con la fuente hasta que se soliciten datos o se inicie la conexión de manera
explícita. Antes de la conexión, se pueden configurar parámetros de red y características
específicas del protocolo. Por ejemplo, podemos establecer límites de tiempo de espera tanto
en la conexión inicial con el servidor como en las lecturas:

Como veremos en "Usando el método POST" en la página 405, podemos obtener la información
específica del protocolo al convertir URLConnection a su subtipo específico.

Manejadores en práctica
Los mecanismos de manejo de contenido y protocolo que hemos descrito son muy flexibles;
para manejar nuevos tipos de URL, solo necesitas agregar las clases de manejadores apropiadas.
Una aplicación interesante de esto sería navegadores web basados en Java que pudieran
manejar tipos nuevos y especializados de URL descargándolos a través de internet. La idea de
esto fue promocionada en los primeros días de Java. Desafortunadamente, nunca se concretó.
No hay una API para descargar dinámicamente nuevos manejadores de contenido y protocolo.
De hecho, no hay una API estándar para determinar qué manejadores de contenido y protocolo
existen en una plataforma determinada.
Actualmente, Java exige manejadores de protocolo para HTTP, HTTPS, FTP, FILE y JAR. Aunque
en la práctica generalmente encontrarás estos manejadores de protocolo básicos en todas las
versiones de Java, eso no es totalmente reconfortante, y la situación para los manejadores de
contenido es aún menos clara. Las clases estándar de Java no incluyen, por ejemplo,
manejadores de contenido para HTML, GIF, PNG, JPEG u otros tipos de datos comunes. Además,
aunque los manejadores de contenido y protocolo forman parte de la API de Java y son una
parte intrínseca del mecanismo para trabajar con URL, no se definen manejadores de contenido
y protocolo específicos.
Incluso aquellos manejadores de protocolo que se han incluido en Java todavía están
empaquetados como parte de las clases de implementación de Sun y no son realmente parte
central de la API para que todos los vean.
En resumen, el mecanismo de manejo de contenido y protocolo de Java fue un enfoque
visionario que nunca se materializó por completo. La promesa de navegadores web que se
extienden dinámicamente para nuevos tipos de protocolos y contenido es, como los autos
voladores, siempre algo que está a unos pocos años de distancia. Aunque los conceptos básicos
del mecanismo de manejo de protocolo son útiles (especialmente ahora con cierta
estandarización) para decodificar contenido en tus propias aplicaciones, probablemente
deberías recurrir a otros frameworks más nuevos que sean un poco más específicos.

Frameworks de Manejadores Útiles


La idea de manejadores descargables de manera dinámica también podría aplicarse a otros tipos
de componentes similares a manejadores. Por ejemplo, la comunidad XML de Java gusta de
referirse al XML como una forma de aplicar semántica (significado) a documentos y a Java como
una manera portátil de suministrar el comportamiento que acompaña a esa semántica. Es
posible que se pueda construir un visor XML con manejadores descargables para mostrar
etiquetas XML.
Afortunadamente, para trabajar con flujos de URL de imágenes, música y video, existen APIs
muy maduras. La API de Imágenes Avanzadas de Java (JAI) incluye un conjunto bien definido y
extensible de manejadores para la mayoría de los tipos de imagen, y el Java Media Framework
(JMF) puede reproducir la mayoría de los tipos comunes de música y video que se encuentran
en línea.

Hablando con Aplicaciones Web


Los navegadores web son los clientes universales para aplicaciones web. Recuperan documentos
para mostrar y sirven como una interfaz de usuario, principalmente a través del uso de HTML,
JavaScript y documentos vinculados. En esta sección, mostraremos cómo escribir código Java
del lado del cliente que use HTTP a través de la clase URL para trabajar directamente con
aplicaciones web mediante operaciones GET y POST para recuperar y enviar datos.
Hay muchas razones por las cuales una aplicación podría querer comunicarse a través de HTTP.
Por ejemplo, la compatibilidad con otra aplicación basada en navegador podría ser importante,
o tal vez necesites acceder a un servidor a través de un firewall donde las conexiones de socket
directas (y RMI) sean problemáticas. HTTP es la lengua franca de internet y, a pesar de sus
limitaciones (o más probablemente debido a su simplicidad), se ha convertido rápidamente en
uno de los protocolos más ampliamente compatibles en el mundo. En cuanto al uso de Java en
el lado del cliente, todas las demás razones por las cuales escribirías una aplicación GUI o no GUI
del lado del cliente (en contraposición a una aplicación puramente basada en web/HTML)
también se presentan. Una GUI del lado del cliente puede realizar presentaciones sofisticadas y
validaciones mientras, con las técnicas presentadas aquí, aún utiliza servicios habilitados para
web a través de la red.
La tarea principal que discutimos aquí es enviar datos al servidor, específicamente datos
codificados en formularios HTML. En un navegador web, los pares nombre/valor de los campos
de formulario HTML se codifican en un formato especial y se envían al servidor usando uno de
dos métodos. El primer método, usando el comando HTTP GET, codifica la entrada del usuario
en el URL y solicita el documento correspondiente. El servidor reconoce que la primera parte del
URL se refiere a un programa e lo invoca, pasando la información codificada en el URL como un
parámetro. El segundo método utiliza el comando HTTP POST para pedir al servidor que acepte
los datos codificados y los pase a una aplicación web como un flujo. En Java, podemos crear un
URL que se refiera a un programa del lado del servidor y solicitar o enviar datos utilizando los
métodos GET y POST. En "Aplicaciones Web Java" en la página 409 a continuación, veremos
cómo construir aplicaciones web que implementen el otro lado de esta conversación.
Usando el Método GET
Usar el método GET para codificar datos en un URL es bastante fácil. Todo lo que tenemos que
hacer es crear un URL que apunte a un programa del servidor y usar una convención simple para
añadir los pares de nombre/valor codificados que conforman nuestros datos. Por ejemplo, el
siguiente fragmento de código abre un URL a un programa CGI antiguo llamado login.cgi en el
servidor myhost y le pasa dos pares de nombre/valor. Luego, imprime cualquier texto que el CGI
envíe de vuelta.

Para formar el URL con parámetros, comenzamos con el URL base de login.cgi; agregamos un
signo de interrogación (?), que marca el inicio de los datos de los parámetros, seguido del primer
par de nombre/valor. Podemos agregar tantos pares como queramos, separados por el carácter
ampersand (&). El resto de nuestro código simplemente abre el flujo y lee la respuesta del
servidor. Recuerda que crear un URL no abre realmente la conexión. En este caso, la conexión
URL se realizó implícitamente cuando llamamos a openStream(). Aunque aquí asumimos que
nuestro servidor devuelve texto, podría devolver cualquier cosa.
Es importante señalar que hemos omitido un paso aquí. Este ejemplo funciona porque nuestros
pares de nombre/valor resultan ser texto simple. Si hay caracteres "no imprimibles" o caracteres
especiales (incluyendo ? o &) en los pares, deben ser codificados primero. La clase
java.net.URLEncoder proporciona una utilidad para codificar los datos. Mostraremos cómo
usarla en el próximo ejemplo en "Usando el Método POST" en la página 405.
Otra cosa importante es que aunque este ejemplo pequeño envía un campo de contraseña,
nunca deberías enviar datos sensibles usando este enfoque simplista. Los datos en este ejemplo
se envían en texto plano a través de la red (no están encriptados). Y en este caso, el campo de
contraseña aparecería en cualquier lugar donde se imprima el URL (por ejemplo, registros del
servidor, historial del navegador y marcadores). Hablaremos sobre comunicaciones web seguras
más adelante en este capítulo cuando discutamos cómo escribir aplicaciones web utilizando
servlets.

Usando el Método POST


Para cantidades mayores de datos de entrada o para contenido sensible, es probable que utilices
la opción POST. Aquí hay una pequeña aplicación que actúa como un formulario HTML. Recopila
datos de dos campos de texto: nombre y contraseña, y envía los datos a un URL especificado
usando el método HTTP POST. Esta aplicación cliente basada en Swing funciona con una
aplicación web del lado del servidor, igual que un navegador web.
Aquí está el código:
Cuando ejecutas esta aplicación, debes especificar el URL del programa del servidor en la línea
de comandos. Por ejemplo:

El comienzo de la aplicación crea el formulario utilizando elementos Swing, como hicimos en el


Capítulo 10. Toda la magia sucede en el método protegido postData(). Primero, creamos un
StringBuilder (una versión no sincronizada de StringBuffer) y lo llenamos con pares de
nombre/valor, separados por ampersands. (No necesitamos el signo de interrogación inicial
cuando estamos usando el método POST porque no estamos añadiendo a una cadena de URL).
Cada par se codifica primero usando el método estático URLEncoder.encode(). Ejecutamos los
campos de nombre a través del codificador así como los campos de valor, aunque sabemos que
en este caso no contienen caracteres especiales.
Luego, configuramos la conexión al programa del servidor. En nuestro ejemplo anterior, no se
requería hacer nada especial para enviar los datos porque la solicitud se realizaba simplemente
al abrir el URL en el servidor. Aquí, tenemos que asumir parte de la responsabilidad de hablar
con el servidor web remoto. Afortunadamente, el objeto HttpURLConnection hace la mayor
parte del trabajo por nosotros; solo tenemos que decirle que queremos hacer un POST al URL y
el tipo de datos que estamos enviando. Solicitamos el objeto URLConnection que está utilizando
el método openConnection() del URL. Sabemos que estamos utilizando el protocolo HTTP, por
lo que deberíamos poder convertirlo de forma segura a un tipo HttpURLConnection, que tiene
el soporte que necesitamos. Debido a que HTTP es uno de los protocolos garantizados, podemos
hacer esta suposición de forma segura. (Hablando de seguridad, aquí usamos HTTP solo con
fines de demostración. Hoy en día, muchos datos se consideran sensibles. Las directrices de la
industria han optado por usar HTTPS de forma predeterminada; más sobre eso pronto en "SSL y
Comunicaciones Seguras en la Web" en la página 409.)
Luego, usamos setRequestMethod() para indicar a la conexión que queremos realizar una
operación POST. También usamos setRequestProperty() para establecer el campo Content-Type
de nuestra solicitud HTTP al tipo apropiado, en este caso, el tipo MIME correcto para datos de
formulario codificados. (Esto es necesario para indicarle al servidor qué tipo de datos estamos
enviando).
Finalmente, usamos los métodos setDoOutput() y setDoInput() para indicar a la conexión que
queremos enviar y recibir datos de flujo. La conexión URL infiere a partir de esta combinación
que vamos a realizar una operación POST y espera una respuesta.
A continuación, obtenemos un flujo de salida de la conexión con getOutputStream() y creamos
un PrintWriter para que podamos escribir fácilmente nuestros datos codificados.
Después de enviar los datos, nuestra aplicación llama a getResponseCode() para ver si el código
de respuesta HTTP del servidor indica que el POST fue exitoso. Otros códigos de respuesta
(definidos como constantes en HttpURLConnection) indican varios tipos de fallos. Al final de
nuestro ejemplo, indicamos dónde podríamos haber leído el texto de la respuesta. Para esta
aplicación, asumiremos que simplemente saber que el envío fue exitoso es suficiente.
Aunque los datos codificados en el formulario (indicados por el tipo MIME que especificamos
para el campo Content-Type) son los más comunes, otros tipos de comunicaciones son posibles.
Podríamos haber utilizado los flujos de entrada y salida para intercambiar tipos de datos
arbitrarios con el programa del servidor. La operación POST podría enviar cualquier tipo de
datos; la aplicación del servidor simplemente tiene que saber cómo manejarlos. Una nota final:
si estás escribiendo una aplicación que necesita decodificar datos de formulario, puedes usar
java.net.URLDecoder para deshacer la operación de URLEncoder. Asegúrate de especificar UTF-
8 al llamar a decode().

La HttpURLConnection
Otra información de la solicitud está disponible también desde HttpURLConnection. Podríamos
usar getContentType() y getContentEncoding() para determinar el tipo MIME y la codificación
de la respuesta. También podríamos interrogar los encabezados de respuesta HTTP utilizando
getHeaderField(). (Los encabezados de respuesta HTTP son pares de nombre/valor de
metadatos llevados con la respuesta.) Los métodos de conveniencia pueden recuperar campos
de encabezado formateados como enteros y fechas, getHeaderFieldInt() y
getHeaderFieldDate(), que devuelven un tipo int y un tipo long, respectivamente. La longitud del
contenido y la fecha de última modificación se proporcionan a través de getContentLength() y
getLastModified().

SSL y Comunicaciones Seguras en la Web


Los ejemplos anteriores enviaron un campo llamado "Password" al servidor. Sin embargo, el
estándar HTTP no proporciona encriptación para ocultar nuestros datos. Afortunadamente,
añadir seguridad para operaciones GET y POST como esta es fácil (trivial, de hecho, para el
desarrollador del lado del cliente). Donde esté disponible, simplemente necesitas usar una
forma segura del protocolo HTTP: HTTPS:
HTTPS es una versión del protocolo estándar HTTP ejecutado sobre Secure Sockets Layer (SSL), que utiliza
técnicas de cifrado de clave pública para encriptar las comunicaciones entre el navegador y el servidor. La
mayoría de los navegadores web y servidores vienen actualmente con soporte incorporado para HTTPS
(o sockets SSL sin procesar). Por lo tanto, si tu servidor web admite HTTPS y está configurado para ello,
puedes utilizar un navegador para enviar y recibir datos seguros simplemente especificando el protocolo
https en tus URLs. Hay mucho más por aprender sobre SSL y aspectos relacionados con la seguridad, como
autenticar con quién estás hablando realmente, pero en lo que respecta al cifrado básico de datos, esto
es todo lo que tienes que hacer. No es algo con lo que tu código tenga que lidiar directamente. La edición
estándar de Java JRE incluye soporte para SSL y HTTPS, y a partir de Java 5.0, todas las implementaciones
de Java deben admitir HTTPS, así como HTTP, para conexiones URL.

Aplicaciones Web en Java

Durante los primeros años de Java, las aplicaciones basadas en la web seguían el mismo paradigma básico:
el navegador realiza una solicitud a una URL específica; el servidor genera una página de HTML en
respuesta; y las acciones del usuario llevan al navegador a la siguiente página. En este intercambio, la
mayor parte o todo el trabajo se realiza en el lado del servidor, lo cual parece lógico dado que ahí es donde
suelen residir los datos y los servicios. El problema con este modelo de aplicación es que está
inherentemente limitado por la pérdida de capacidad de respuesta, continuidad y estado experimentados
por el usuario al cargar nuevas "páginas" en el navegador. Es difícil hacer una aplicación basada en web
tan fluida como una aplicación de escritorio cuando el usuario debe saltar a través de una serie de páginas
discretas, y es técnicamente más desafiante mantener los datos de la aplicación a lo largo de esas páginas.
Después de todo, los navegadores web no fueron diseñados para alojar aplicaciones, sino documentos.

Pero mucho ha cambiado en el desarrollo de aplicaciones web en los últimos años. Los estándares para
HTML y JavaScript han madurado hasta el punto en que es práctico, de hecho común, escribir aplicaciones
en las que la mayor parte de la interfaz de usuario y la lógica residen en el lado del cliente, y se realizan
llamadas de fondo al servidor para obtener datos y servicios. En este paradigma, el servidor devuelve
efectivamente solo una "página" de HTML que hace referencia a la mayor parte del JavaScript, CSS y otros
recursos utilizados para renderizar la interfaz de la aplicación. Luego, JavaScript toma el control,
manipulando elementos en la página o creando nuevos dinámicamente utilizando funciones avanzadas
del DOM de HTML para producir la interfaz de usuario.

JavaScript también realiza llamadas asíncronas (en segundo plano) al servidor para obtener datos e
invocar servicios. En los primeros años, los resultados se devolvían como XML, lo que llevó al término
Asynchronous JavaScript and XML (AJAX) para este estilo de interacción. Aunque todavía escuchas ese
término, en la actualidad el formato JavaScript Object Notation (JSON) es más popular que XML, y una
explosión de bibliotecas de JavaScript asíncronas ha tomado el control. Dado que todas las bibliotecas
tienen en común la parte de "JavaScript asíncrono", la mayoría de las veces escuchas a los desarrolladores
(y gerentes de contratación) hablar sobre la biblioteca o el marco particular que utilizan, como React o
Angular.

Este nuevo modelo simplifica y potencia el desarrollo web de muchas maneras. Ya no es necesario que el
cliente trabaje en un régimen de solicitud-respuesta de una sola página, donde las vistas y las solicitudes
se intercambian de ida y vuelta. El cliente ahora es más equivalente a una aplicación de escritorio en el
sentido de que puede responder fluidamente a la entrada del usuario y administrar datos y servicios
remotos sin interrumpir al usuario.

Hasta ahora hemos utilizado el término "aplicación web" de manera genérica, refiriéndonos a cualquier
tipo de aplicación basada en navegador que se encuentra en un servidor web, ya sea una sola página o
una colección de muchas páginas. Ahora vamos a ser más precisos con ese término. En el contexto de la
API de Servlet de Java, una aplicación web es una colección de servlets y servicios web de Java que admiten
clases de Java, contenido como HTML, Java Server Pages (JSP), imágenes u otros medios, e información
de configuración. Para implementarla (instalarla en un servidor web), una aplicación web se empaqueta
en un archivo WAR. Hablaremos en detalle sobre los archivos WAR más adelante, pero basta decir que en
realidad son archivos JAR que contienen todos los archivos de la aplicación junto con alguna información
de implementación. Lo importante es que la estandarización de los archivos WAR significa no solo que el
código Java es portátil, sino también que el proceso de implementar la aplicación en un servidor está
estandarizado.

La mayoría de los archivos WAR tienen en su núcleo un archivo web.xml. Este es un archivo de
configuración XML que describe qué servlets deben implementarse, sus nombres y rutas URL, sus
parámetros de inicialización y una gran cantidad de otra información, incluidos requisitos de seguridad y
autenticación. Sin embargo, en los últimos años, el archivo web.xml se ha vuelto opcional para muchas
aplicaciones debido a la introducción de anotaciones de Java que ocupan el lugar de la configuración XML.
En la mayoría de los casos, ahora puedes implementar tus servlets y servicios web de Java simplemente
anotando las clases con la información necesaria y empaquetándolas en el archivo WAR, o utilizando una
combinación de ambos. Hablaremos sobre esto en detalle más adelante en el capítulo.

Las aplicaciones web, o web apps, también tienen un entorno de ejecución bien definido. Cada aplicación
web tiene su propia ruta "raíz" en el servidor web, lo que significa que todas las URLs que hacen referencia
a sus servlets y archivos comienzan con un prefijo único común (por ejemplo,
http://www.oreilly.com/algoaplicacion/). Los servlets de la aplicación web también están aislados de los
de otras aplicaciones web. Las aplicaciones web no pueden acceder directamente a los archivos de otras
aplicaciones web (aunque pueden permitirlo a través del servidor web, por supuesto). Cada aplicación
web también tiene su propio contexto de servlet. Hablaremos sobre el contexto de servlet con más
detalle, pero en resumen, es un área común para que los servlets dentro de una aplicación compartan
información y obtengan recursos del entorno. El alto grado de aislamiento entre aplicaciones web tiene
como objetivo admitir la implementación y actualización dinámicas de aplicaciones requeridas por los
sistemas empresariales modernos y abordar preocupaciones de seguridad y confiabilidad.

Las aplicaciones web están destinadas a ser aplicaciones relativamente completas y de grano grueso, no
están diseñadas para estar estrechamente acopladas con otras aplicaciones web. Aunque no hay razón
para que las aplicaciones web no puedan cooperar a un alto nivel, para compartir lógica entre
aplicaciones, es posible que desees considerar los servicios web, de los cuales hablaremos más adelante
en este capítulo.

El Ciclo de Vida del Servlet

Saltemos ahora a la API de Servlet y comencemos a construir servlets. Llenaremos los vacíos más tarde
cuando discutamos diversas partes de las APIs y la estructura del archivo WAR con más detalle. La API de
Servlet es muy simple. La clase base Servlet tiene tres métodos del ciclo de vida: init(), service() y destroy(),
junto con algunos métodos para obtener parámetros de configuración y recursos del servlet. Sin embargo,
estos métodos no son utilizados directamente por los desarrolladores. Típicamente, los desarrolladores
implementarán los métodos doGet() y doPost() de la subclase HttpServlet y accederán a recursos
compartidos a través del contexto del servlet, como discutiremos en breve.

Generalmente, solo se instancia una sola instancia de cada clase de servlet implementada por contenedor.
Más precisamente, es una instancia por entrada de servlet en el archivo web.xml, pero hablaremos más
sobre la implementación de servlets en "Contenedores de Servlets" en la página 422. En el pasado, había
una excepción a esa regla cuando se usaba el tipo especial SingleThreadModel de servlet. A partir de la
API de Servlet 2.4, los servlets de un solo hilo han sido obsoletos.

Por defecto, se espera que los servlets manejen solicitudes de manera multihilo; es decir, los métodos de
servicio del servlet pueden ser invocados por varios hilos al mismo tiempo. Esto significa que no deberías
almacenar datos por solicitud o por cliente en variables de instancia de tu objeto servlet. (Por supuesto,
puedes almacenar datos generales relacionados con la operación del servlet, siempre y cuando no
cambien en una base por solicitud). La información del estado por cliente puede ser almacenada en un
objeto de sesión de cliente en el servidor o en una cookie del lado del cliente, que persiste en las
solicitudes del cliente. Hablaremos sobre el estado del cliente más adelante también.
El método service() de un servlet acepta dos parámetros: un objeto "solicitud" del servlet y un objeto
"respuesta" del servlet. Estos proporcionan herramientas para leer la solicitud del cliente y generar salida;
hablaremos sobre ellos (o más bien sus versiones HttpServlet) en detalle en los ejemplos a continuación.

Servlets

El paquete de principal interés para nosotros aquí es javax.servlet.http, que contiene APIs específicas para
servlets que manejan solicitudes HTTP para servidores web. En teoría, puedes escribir servlets para otros
protocolos, pero nadie realmente lo hace, y vamos a hablar de los servlets como si todos estuvieran
relacionados con HTTP.

Observa que el prefijo del paquete javax es similar a lo que vimos con los paquetes de Swing. La API de
Servlet es ciertamente una parte importante de Java, pero no está incluida en el kit de desarrollo base.
Necesitas descargar una biblioteca separada, servlet-api.jar, de un proveedor externo. Apache
proporciona la implementación de referencia de la API de Servlet. Los detalles sobre cómo descargar esta
biblioteca y usarla en la línea de comandos o con el IDE IntelliJ IDEA se pueden encontrar en "Obteniendo
los Ejemplos de Código Web" en la página 454.

La herramienta principal proporcionada por el paquete javax.servlet.http es la clase base HttpServlet. Este
es un servlet abstracto que proporciona algunos detalles de implementación básicos relacionados con el
manejo de una solicitud HTTP. En particular, anula la solicitud genérica de servicio del servlet y la divide
en varios métodos relacionados con HTTP, incluyendo doGet(), doPost(), doPut() y doDelete(). El método
de servicio predeterminado examina la solicitud para determinar de qué tipo es y la envía a uno de estos
métodos, para que puedas anular uno o más de ellos para implementar el comportamiento de protocolo
específico que necesitas.

doGet() y doPost() corresponden a las operaciones estándar HTTP GET y POST. GET es la solicitud estándar
para recuperar un archivo o documento en una URL específica. POST es el método mediante el cual un
cliente envía una cantidad arbitraria de datos al servidor. Los formularios HTML utilizan POST para enviar
datos, al igual que la mayoría de los servicios web.

Para complementar estos métodos, HttpServlet proporciona los métodos doPut() y doDelete(). Estos
métodos corresponden a una parte del protocolo HTTP popular en aplicaciones web que utilizan un estilo
de API REST (REpresentational State Transfer). Proporcionan una forma de cargar y eliminar archivos u
otras entidades como registros de bases de datos. Se supone que doPut() es similar a POST pero con
semántica ligeramente diferente (PUT se supone que reemplaza lógicamente el elemento identificado por
la URL, mientras que POST presenta nuevos datos a él); doDelete() sería lo opuesto.

HttpServlet también implementa otros tres métodos relacionados con HTTP: doHead(), doTrace() y
doOptions(). Normalmente, no necesitas anular estos métodos. doHead() implementa la solicitud HTTP
HEAD, que solicita los encabezados de una solicitud GET sin el cuerpo. HttpServlet implementa esto por
defecto de manera trivial, realizando el método GET y luego enviando solo los encabezados. Es posible
que desees anular doHead() con una implementación más eficiente si puedes proporcionar una como
optimización. doTrace() y doOptions() implementan otras funciones de HTTP que permiten la depuración
y la negociación de capacidades simples entre cliente y servidor. Normalmente, no deberías necesitar
anular estos métodos.

Junto con HttpServlet, javax.servlet.http también incluye subclases de los objetos ServletRequest y
ServletResponse, así como HttpServletRequest y HttpServletResponse. Estas subclases proporcionan,
respectivamente, los flujos de entrada y salida necesarios para leer y escribir datos del cliente. También
proporcionan las APIs para obtener o establecer información de encabezado HTTP y, como veremos,
información de sesión del cliente. En lugar de documentar esto de manera árida, lo mostraremos en el
contexto de algunos ejemplos.

Como de costumbre, comenzaremos con el ejemplo más simple posible.


El Servlet HelloClient

Aquí está nuestra versión de servlet de "¡Hola, Mundo!", HelloClient:

Si deseas probar este servlet de inmediato, salta a "Contenedores de Servlets" en la página 422, donde
explicamos el proceso de implementación de este servlet. Debido a que hemos incluido la anotación
WebServlet en nuestra clase, este servlet no necesita un archivo web.xml para ser implementado. Todo
lo que tienes que hacer es empaquetar el archivo de clase en una carpeta específica dentro de un archivo
WAR (un archivo ZIP sofisticado) y dejarlo en un directorio monitoreado por el servidor Tomcat. Por ahora,
nos centraremos únicamente en el código de ejemplo del servlet en sí, que es bastante simple en este
caso. Los ejemplos de código para esta parte del libro están disponibles en un segundo repositorio en
GitHub. Los detalles para descargar y configurar IntelliJ IDEA para utilizar la biblioteca de servlets
correspondiente se pueden encontrar en "Obtener los Ejemplos de Código Web" en la página 454.

Echemos un vistazo al ejemplo. HelloClient extiende la clase base HttpServlet y anula el método doGet()
para manejar solicitudes simples. En este caso, queremos responder a cualquier solicitud GET enviando
un documento HTML de una línea que diga "¡Hola Cliente!" Primero, le decimos al contenedor qué tipo
de respuesta vamos a generar, utilizando el método setContentType() del objeto HttpServletResponse.
Especificamos el tipo MIME "text/html" para nuestra respuesta HTML. Luego, obtenemos el flujo de salida
usando el método getWriter() e imprimimos el mensaje en él. No es necesario que cerremos
explícitamente el flujo. Hablaremos más sobre la gestión del flujo de salida a lo largo de este capítulo.

Excepciones de Servlets

El método doGet() de nuestro servlet de ejemplo declara que puede lanzar una ServletException. Todos
los métodos de servicio de la API de Servlet pueden lanzar una ServletException para indicar que una
solicitud ha fallado. Se puede construir una ServletException con un mensaje de cadena y un parámetro
Throwable opcional que puede llevar cualquier excepción correspondiente que represente la causa raíz
del problema:

Por defecto, el servidor web determina exactamente qué se muestra al usuario cada vez que se lanza una
ServletException; a menudo hay un "modo de desarrollo" donde se muestra la excepción y su rastro de
pila. La mayoría de los contenedores de servlets (como Tomcat) permiten designar páginas de error
personalizadas, pero eso está más allá del alcance de este capítulo.

Alternativamente, un servlet puede lanzar una UnavailableException, una subclase de ServletException,


para indicar que no puede manejar las solicitudes. Esta excepción puede ser lanzada para indicar que la
condición es permanente o que debe durar un período especificado de segundos.
Tipo de contenido

Antes de obtener el flujo de salida y escribir en él, debemos especificar el tipo de salida que estamos
enviando llamando al método setContentType() del parámetro de respuesta. En este caso, establecemos
el tipo de contenido en text/html, que es el tipo MIME adecuado para un documento HTML. En general,
sin embargo, es posible que un servlet genere cualquier tipo de datos, incluidos audio, video u otro tipo
de documento de texto o binario. Si estuviéramos escribiendo un FileServlet genérico para servir archivos
como un servidor web regular, podríamos inspeccionar la extensión del nombre de archivo y determinar
el tipo MIME a partir de eso o mediante inspección directa de los datos. (¡Este es un buen uso para el
método Files.probeContentType() de java.nio.file.Files!) Para escribir datos binarios, puedes usar el
método getOutputStream() para obtener un OutputStream en lugar de un Writer.

El tipo de contenido se utiliza en la cabecera Content-Type: de la respuesta HTTP del servidor, lo que le
dice al cliente qué esperar incluso antes de comenzar a leer el resultado. Esto permite que tu navegador
web te muestre el cuadro de diálogo "Guardar archivo" cuando haces clic en un archivo ZIP o programa
ejecutable. Cuando se usa la cadena de tipo de contenido en su forma completa para especificar la
codificación de caracteres (por ejemplo, text/html; charset=UTF-8), la información también se utiliza para
que el motor de servlets establezca la codificación de caracteres del flujo de salida PrintWriter. Como
resultado, siempre debes llamar al método setContentType() antes de obtener el escritor con el método
getWriter(). La codificación de caracteres también se puede establecer por separado mediante el método
setCharacterEncoding() de la respuesta del servlet.

La respuesta del servlet

Además de proporcionar el flujo de salida para escribir contenido al cliente, el objeto HttpServletResponse
proporciona métodos para controlar otros aspectos de la respuesta HTTP, incluidos encabezados, códigos
de resultado de errores, redirecciones y el almacenamiento en búfer del contenedor de servlets.

Los encabezados HTTP son pares nombre/valor de metadatos enviados con la respuesta. Puedes agregar
encabezados (estándar o personalizados) a la respuesta con los métodos setHeader() y addHeader() (los
encabezados pueden tener múltiples valores). También hay métodos de conveniencia para establecer
encabezados con valores enteros y de fecha:

Cuando escribes datos hacia el cliente, el contenedor de servlets establece automáticamente el código de
respuesta HTTP en un valor de 200, lo que significa OK. Utilizando el método sendError(), puedes generar
otros códigos de respuesta HTTP. HttpServletResponse contiene constantes predefinidas para todos los
códigos estándar. Aquí tienes algunos de los más comunes:

Cuando generas un error con sendError(), la respuesta ha finalizado y no puedes escribir ningún contenido
real hacia el cliente. Sin embargo, puedes especificar un breve mensaje de error que podría mostrarse al
cliente (consulta la sección "El Ciclo de Vida del Servlet" en la página 411).

Una redirección HTTP es un tipo especial de respuesta que indica al navegador web del cliente que vaya
a una URL diferente. Normalmente esto ocurre rápidamente y sin interacción alguna por parte del usuario.
Puedes enviar una redirección con el método sendRedirect():
Mientras hablamos sobre la respuesta, deberíamos mencionar unas palabras sobre el almacenamiento
en búfer. La mayoría de las respuestas son almacenadas en búfer internamente por el contenedor de
servlets hasta que el método de servicio del servlet haya finalizado o se haya alcanzado un tamaño
máximo preestablecido. Esto permite que el contenedor establezca automáticamente el encabezado
HTTP "content-length", indicando al cliente cuántos datos esperar. Puedes controlar el tamaño de este
búfer con el método setBufferSize(), especificando un tamaño en bytes. Incluso puedes limpiarlo y
comenzar de nuevo si no se ha escrito ningún dato al cliente. Para limpiar el búfer, utiliza isCommitted()
para verificar si se ha enviado algún dato, luego usa resetBuffer() para eliminar los datos si no se ha
enviado ninguno. Si estás enviando mucha información, quizás desees establecer explícitamente la
longitud del contenido con el método setContentLength().

Parámetros del Servlet

Nuestro primer ejemplo mostró cómo aceptar una solicitud básica. Por supuesto, para hacer algo
realmente útil, necesitaremos obtener información del cliente. Afortunadamente, el motor de servlets
maneja esto por nosotros, interpretando tanto los datos codificados en formularios GET como POST del
cliente y proporcionándonoslos a través del simple método getParameter() del servlet request.

GET, POST y "ruta extra"

Hay dos formas comunes de pasar información desde tu navegador web a un servlet o programa CGI. La
más general es "enviar" la información, lo que significa que tu cliente codifica la información y la envía
como un flujo al programa, que la decodifica. El envío se puede usar para cargar grandes cantidades de
datos de formulario u otros datos, incluidos archivos. La otra forma de pasar información es codificarla de
alguna manera en la URL de la solicitud de tu cliente. La forma principal de hacer esto es usar la
codificación GET de parámetros en la cadena de URL. En este caso, el navegador web codifica los
parámetros y los agrega al final de la cadena de URL. El servidor los decodifica y los pasa a la aplicación.

Como describimos anteriormente, la codificación GET lleva los parámetros y los agrega a la URL de manera
de nombre/valor, con el primer parámetro precedido por un signo de interrogación (?) y el resto separado
por ampersands (&). Se espera que toda la cadena esté codificada en URL: cualquier carácter especial
(como espacios, ?, y & en la cadena) se codifican especialmente.

Otra forma de pasar datos en la URL se llama "ruta extra". Esto simplemente significa que cuando el
servidor ha localizado tu servlet o programa CGI como el objetivo de una URL, toma cualquier componente
de ruta restante de la cadena de URL y los entrega como una parte adicional de la URL. Por ejemplo,
considera estas URLs:

Supongamos que el servidor asigna la primera URL al servlet llamado MyServlet. Cuando se le proporciona
la segunda URL, el servidor también invoca a MyServlet, pero considera que /foo/bar es la "ruta extra"
que se puede recuperar a través del método getExtraPath() del servlet request. Esta técnica es útil para
crear nombres de ruta de URL más legibles y significativos, especialmente para contenido centrado en
documentos.

Tanto la codificación GET como POST se pueden usar con formularios HTML en el cliente especificando
get o post en el atributo action de la etiqueta del formulario. El navegador maneja la codificación; en el
lado del servidor, el motor de servlets maneja la decodificación.

El tipo de contenido que utiliza un cliente para enviar datos de formulario a un servlet es: "application/x-
www-form-urlencoded". La API de Servlet analiza automáticamente este tipo de datos y los pone
disponibles a través del método getParameter(). Sin embargo, si no llamas al método getParameter(), los
datos permanecen disponibles, sin procesar, en el flujo de entrada y pueden ser leídos directamente por
el servlet.
GET o POST: ¿Cuál usar?

Para los usuarios, la diferencia principal entre GET y POST es que pueden ver la información GET en la URL
codificada que se muestra en su navegador web. Esto puede ser útil porque el usuario puede copiar y
pegar esa URL (por ejemplo, el resultado de una búsqueda) y enviarla por correo electrónico a un amigo
o marcarla para referencia futura. La información POST no es visible para el usuario y deja de existir
después de ser enviada al servidor. Este comportamiento va de acuerdo con la intención del protocolo de
que GET y POST tengan diferentes semánticas. Por convención, el resultado de una operación GET no
debe tener ningún efecto secundario; es decir, no se supone que cause que el servidor realice operaciones
persistentes (como realizar una compra en un carrito de compras). En teoría, ese es el trabajo de POST.
Es por eso que tu navegador web te advierte sobre volver a enviar datos de formulario si presionas
"recargar" en una página que fue el resultado de un envío de formulario.

El estilo de "ruta extra" sería útil para un servlet que recupera archivos o maneja una variedad de URLs de
una manera legible para humanos. La información de la "ruta extra" a menudo es útil para URLs que el
usuario debe ver o recordar, ya que se ve como cualquier otra ruta.

El Servlet ShowParameters

Nuestro primer ejemplo no hizo mucho. Este siguiente ejemplo imprime los valores de cualquier
parámetro que se recibió. Comenzaremos manejando las solicitudes GET y luego haremos algunas
modificaciones triviales para manejar también las solicitudes POST. Aquí está el código:

Como en el primer ejemplo, anulamos el método `doGet()`. Delegamos la solicitud a un método auxiliar
que hemos creado, llamado `showRequestParameters()`, un método que enumera los parámetros
utilizando el método `getParameterMap()` del objeto de solicitud, el cual devuelve un mapa de nombres
de parámetros a valores y los imprime. Tenga en cuenta que un parámetro puede tener múltiples valores
si se repite en la solicitud del cliente, por lo tanto, el mapa contiene `String[]`. Para que se vea bien,
listamos cada parámetro en HTML con una etiqueta `<li>`.

Como está ahora, nuestro servlet respondería a cualquier URL que contenga una solicitud GET. Vamos a
completarlo agregando nuestro propio formulario a la salida y también aceptando solicitudes mediante
el método POST. Para aceptar publicaciones, anulamos el método `doPost()`. La implementación de
`doPost()` podría simplemente llamar a nuestro método `showRequestParameters()`, pero podemos
simplificarlo aún más. La API nos permite tratar las solicitudes GET y POST de manera intercambiable
porque el motor del servlet maneja la decodificación de los parámetros de la solicitud. Así que
simplemente delegamos la operación `doPost()` a `doGet()`.

Agregue el siguiente método al ejemplo:

Ahora, agreguemos un formulario HTML a la salida. El formulario permite al usuario completar algunos
parámetros y enviarlos al servlet. Agregue esta línea al método `showRequestParameters()` antes de la
llamada a `out.close()`:

El atributo `action` del formulario es la URL de nuestro servlet para que nuestro servlet reciba los datos.
Usamos el método `getRequestURI()` para obtener la ubicación de nuestro servlet. Para el atributo
`method`, hemos especificado una operación POST, pero puedes intentar cambiar la operación a GET para
ver ambos estilos.

Hasta ahora, no hemos hecho nada tremendamente emocionante. En el próximo ejemplo, agregaremos
algo de potencia introduciendo una sesión de usuario para almacenar datos del cliente entre las
solicitudes.

Gestión de la Sesión de Usuario

Una de las características más agradables de la API Servlet es su mecanismo simple para administrar una
sesión de usuario. Por sesión, nos referimos a que el servlet puede mantener información a lo largo de
múltiples páginas y a través de múltiples transacciones mientras navega el usuario; esto también se llama
mantener el estado. Proporcionar continuidad a través de una serie de páginas web es importante en
muchos tipos de aplicaciones, como manejar un proceso de inicio de sesión o rastrear compras en un
carrito de compras. En cierto sentido, los datos de sesión ocupan el lugar de los datos de instancia en el
objeto de su servlet. Le permite almacenar datos entre invocaciones de sus métodos de servicio. Sin tal
mecanismo, su servlet no tendría forma de saber que dos solicitudes provienen del mismo usuario.

El seguimiento de sesión es compatible con el contenedor de servlets; normalmente no tiene que


preocuparse por los detalles de cómo se realiza. Se hace de dos formas: usando cookies en el lado del
cliente o la reescritura de URL. Las cookies en el lado del cliente son un mecanismo estándar de HTTP para
hacer que el navegador web del cliente coopere en almacenar información de estado para usted. Una
cookie es básicamente solo un atributo de nombre/valor que es emitido por el servidor, almacenado en
el cliente y devuelto por el cliente cada vez que accede a un cierto grupo de URLs en un servidor
especificado. Las cookies pueden rastrear una sesión única o múltiples visitas de usuarios.
La reescritura de URL agrega información de seguimiento de sesión a la URL, utilizando codificación de
estilo GET o información de ruta extra. El término "reescritura" se aplica porque el servidor reescribe la
URL antes de que la vea el cliente y absorbe la información adicional antes de que se devuelva al servlet.
Para admitir la reescritura de URL, un servlet debe dar el paso adicional de codificar cualquier URL que
genere en el contenido (por ejemplo, enlaces HTML que pueden volver a la página) utilizando un método
especial del objeto `HttpServletResponse`. Necesitas permitir la reescritura de URL por parte del servidor
si quieres que tu aplicación funcione con navegadores que no admiten cookies o las tienen desactivadas.
Muchos sitios simplemente eligen no funcionar sin cookies.

Para el programador de servlets, la información de estado está disponible a través de un objeto


`HttpSession`, que actúa como una tabla hash para almacenar cualquier objeto que desees llevar a través
de la sesión. Los objetos permanecen en el lado del servidor; se envía un identificador especial al cliente
a través de una cookie o reescritura de URL. A su regreso, el identificador se asigna a una sesión y la sesión
se asocia nuevamente con el servlet.

El Servlet `ShowSession`

Aquí hay un servlet simple que muestra cómo almacenar alguna información de cadena para rastrear una
sesión:
Cuando invocas el servlet, se presenta un formulario que te pide ingresar un nombre y un valor. La cadena
de valor se almacena en un objeto de sesión bajo el nombre proporcionado. Cada vez que se llama al
servlet, muestra la lista de todos los elementos de datos asociados con la sesión. Verás que la sesión crece
a medida que se agregan elementos (en este caso, hasta que reinicies tu navegador web o el servidor).

Los mecanismos básicos son muy similares a nuestro servlet `ShowParameters`. Nuestro método
`doGet()` genera el formulario, el cual apunta de nuevo a nuestro servlet a través de un método POST.
Anulamos `doPost()` para delegar de nuevo a nuestro método `doGet()`, permitiéndole manejar todo.
Una vez en `doGet()`, intentamos obtener el objeto de sesión de usuario del objeto de solicitud utilizando
`getSession()`. El objeto `HttpSession` proporcionado por la solicitud funciona como una tabla hash. Hay
un método `setAttribute()`, que toma un nombre de cadena y un argumento de objeto, y un método
`getAttribute()` correspondiente. En nuestro ejemplo, usamos el método `getAttributeNames()` para
enumerar los valores almacenados actualmente en la sesión e imprimirlos.

Por defecto, `getSession()` crea una sesión si no existe una. Si deseas probar la existencia de una sesión o
controlar explícitamente cuándo se crea una, puedes llamar a la versión sobrecargada `getSession(false)`,
que no crea automáticamente una nueva sesión y devuelve `null` si no hay una sesión. Alternativamente,
puedes verificar si se acaba de crear una sesión con el método `isNew()`. Para borrar una sesión de
inmediato, podemos usar el método `invalidate()`. Después de llamar a `invalidate()` en una sesión, no se
nos permite acceder a ella nuevamente, por lo que establecemos una bandera en nuestro ejemplo y
mostramos el mensaje "Sesión eliminada". Las sesiones también pueden volverse inválidas por sí mismas
debido a su tiempo de espera. Puedes controlar el tiempo de espera de la sesión programáticamente, en
el servidor de aplicaciones o a través del archivo `web.xml` (a través del valor "session-timeout" de la
sección "session config"). En general, esto aparece para la aplicación como una sesión inexistente o una
nueva sesión en la siguiente solicitud.

Las sesiones de usuario son privadas para cada aplicación web y no se comparten entre aplicaciones.

Mencionamos anteriormente que se requiere un paso adicional para admitir la reescritura de URL para
los navegadores web que no admiten cookies. Para hacer esto, debemos asegurarnos de que cualquier
URL que generemos en el contenido pase primero por el método `encodeURL()` de `HttpServletResponse`.
Este método toma una URL de cadena y devuelve una cadena modificada solo si es necesario la reescritura
de URL. Normalmente, cuando las cookies están disponibles, devuelve la misma cadena. En nuestro
ejemplo anterior, podríamos haber codificado la URL del formulario del servidor que se obtuvo de
`getRequestURI()` antes de pasarla al cliente si quisiéramos permitir usuarios sin cookies.
Contenedores de Servlets

Finalmente, ¡es hora de ejecutar todo ese código de ejemplo! Hay muchas herramientas, conocidas como
contenedores, disponibles para implementar servlets. Ni OpenJDK ni el JDK oficial de Oracle vienen con
un contenedor de servlets integrado. Servicios en línea como AWS pueden proporcionar contenedores
rápidos y económicos para hacer que tus servlets estén disponibles para el mundo. Sin embargo, para el
desarrollo, seguramente querrás un entorno local que puedas controlar, cambiar y reiniciar a medida que
exploras la API Servlet.

Como debemos configurar este entorno local nosotros mismos, instalaremos el contenedor de
"implementación de referencia", Apache Tomcat. Instalaremos la versión 9, pero las versiones anteriores
aún admiten todos los conceptos básicos de servlets que hemos discutido hasta ahora.

Como describimos anteriormente, un archivo WAR es un archivo que contiene todas las partes de una
aplicación web: archivos de clase Java para servlets y servicios web, JSP, páginas HTML, imágenes y otros
recursos. El archivo WAR es simplemente un archivo JAR (que es a su vez un archivo ZIP sofisticado) con
directorios especificados para el código Java y un archivo de configuración designado: el archivo
`web.xml`, que le indica al servidor de aplicaciones qué ejecutar y cómo hacerlo. Los archivos WAR
siempre tienen la extensión `.war`, pero se pueden crear y leer con la herramienta `jar` estándar.

El contenido de un archivo WAR típico podría verse así, según lo revela la herramienta `jar`:

Cuando se despliega, el nombre del archivo WAR se convierte, por defecto, en la ruta raíz de la aplicación
web; en este caso, "shoppingcart". Por lo tanto, la URL base para esta aplicación web, si se despliega en
http://www.oreilly.com, sería http://www.oreilly.com/shoppingcart/, y todas las referencias a sus
documentos, imágenes y servlets comienzan con esa ruta. El nivel superior del archivo WAR se convierte
en la raíz de documentos (directorio base) para servir archivos. Nuestro archivo `index.html` aparece en
la URL base que acabamos de mencionar, y nuestra imagen `happybunny.gif` se referencia como
http://www.oreilly.com/shoppingcart/images/happybunny.gif.

El directorio `WEB-INF` (todo en mayúsculas, con guiones) es un directorio especial que contiene toda la
información de despliegue y el código de la aplicación. Este directorio está protegido por el servidor web
y su contenido no es visible para los usuarios externos de la aplicación, incluso si agregas `WEB-INF` a la
URL base. Sin embargo, las clases de tu aplicación pueden cargar archivos adicionales desde esta área
utilizando `getResource()` en el contexto del servlet, por lo que es un lugar seguro para almacenar
recursos de la aplicación. El directorio `WEB-INF` también contiene el archivo `web.xml`, del cual
hablaremos más en la siguiente sección.

Los directorios `WEB-INF/classes` y `WEB-INF/lib` contienen archivos de clases Java y bibliotecas JAR,
respectivamente. El directorio `WEB-INF/classes` se agrega automáticamente al classpath de la aplicación
web, por lo que cualquier archivo de clase colocado aquí (usando las convenciones normales de paquetes
Java) está disponible para la aplicación. Después de eso, cualquier archivo JAR ubicado en `WEB-INF/lib`
se añade al classpath de la aplicación web (lamentablemente, no se especifica el orden en que se añaden).
Puedes colocar tus clases en cualquiera de estas ubicaciones. Durante el desarrollo, a menudo es más fácil
trabajar con el directorio de clases "suelto" y usar el directorio `lib` para clases de soporte y herramientas
de terceros. También es posible instalar archivos JAR directamente en el contenedor de servlets para que
estén disponibles para todas las aplicaciones web que se ejecutan en ese servidor. Esto se hace a menudo
para bibliotecas comunes que serán utilizadas por muchas aplicaciones web. Sin embargo, la ubicación
para colocar las bibliotecas no es estándar y cualquier clase que se despliegue de esta manera no puede
recargarse automáticamente si se cambia, una característica de los archivos WAR que discutiremos más
adelante. La API de Servlet requiere que cada servidor proporcione un directorio para estas JAR de
extensión y que las clases allí sean cargadas por un solo classloader y sean visibles para la aplicación web.

Configuración con web.xml y Anotaciones

El archivo `web.xml` es un archivo de configuración XML que enumera servlets y entidades relacionadas
que se van a desplegar, los nombres relativos (URL paths) bajo los cuales desplegarlos, sus parámetros de
inicialización y sus detalles de despliegue, incluyendo seguridad y autorización. Durante la mayor parte
de la historia de las aplicaciones web de Java, este fue el único mecanismo de configuración de despliegue.
Sin embargo, a partir de la API Servlet 3.0 (Tomcat 7 y posteriores), existen opciones adicionales. Ahora
la mayor parte de la configuración se puede realizar utilizando anotaciones Java. Vimos la anotación
`WebServlet` utilizada en el primer ejemplo, `HelloClient`, para declarar el servlet y especificar su ruta de
URL de despliegue. Utilizando la anotación, podríamos desplegar el servlet en el servidor Tomcat sin
ningún archivo `web.xml`.

Otra opción con la API Servlet 3.0 es desplegar servlets de manera procedural, utilizando código Java en
tiempo de ejecución.

En esta sección describiremos tanto el estilo de configuración XML como el de anotaciones. Para la
mayoría de los propósitos, encontrarás más fácil utilizar las anotaciones, pero hay un par de razones para
comprender también la configuración XML. En primer lugar, el archivo `web.xml` puede utilizarse para
anular o ampliar la configuración de anotaciones codificadas. Utilizando XML, puedes cambiar la
configuración en el momento del despliegue sin recompilar las clases. En general, la configuración en XML
tendrá prioridad sobre las anotaciones. También es posible indicarle al servidor que ignore
completamente las anotaciones, utilizando un atributo llamado `metadata-complete` en el `web.xml`.
Además, puede haber alguna configuración residual, especialmente relacionada con las opciones del
contenedor de servlets, que solo se puede hacer mediante XML.

Supondremos que tienes al menos un conocimiento básico de XML, pero puedes simplemente copiar
estos ejemplos de manera directa y pegarlos. Comencemos con un archivo `web.xml` simple para nuestro
ejemplo de servlet `HelloClient`. Se vería así:

El elemento de nivel superior del documento se llama `<web-app>`. Dentro del `<web-app>` pueden
aparecer muchos tipos de entradas, pero los más básicos son las declaraciones `<servlet>` y los mapeos
de despliegue `<servlet-mapping>`. La etiqueta de declaración `<servlet>` se usa para declarar una
instancia de un servlet y, opcionalmente, para darle parámetros de inicialización y otros parámetros. Se
instancia una vez la clase del servlet por cada etiqueta `<servlet>` que aparezca en el archivo `web.xml`.

Como mínimo, la declaración `<servlet>` requiere dos piezas de información: un `<servlet-name>`, que
sirve como identificador para hacer referencia al servlet en otras partes del archivo `web.xml`, y la
etiqueta `<servlet-class>`, que especifica el nombre de la clase Java del servlet. En este caso, nombramos
al servlet `helloclient1`. Lo nombramos así para enfatizar que podríamos declarar otras instancias del
mismo servlet si quisiéramos, posiblemente dándoles diferentes parámetros de inicialización, etc. El
nombre de la clase para nuestro servlet es, por supuesto, `HelloClient`. En una aplicación real, es probable
que la clase del servlet tenga un nombre completo de paquete, como por ejemplo
`com.oreilly.servlets.HelloClient`.

Una declaración de servlet también puede incluir uno o más parámetros de inicialización, que están
disponibles para el servlet a través del método `getInitParameter()` del objeto `ServletConfig`:

A continuación, tenemos nuestro `<servlet-mapping>`, que asocia la instancia del servlet con una ruta en
el servidor web:

Aquí hemos mapeado nuestro servlet a la ruta `/hello`. (Si se desea, podríamos incluir más patrones de
URL en el mapeo). Si más tarde nombramos nuestro archivo WAR como `learningjava.war` y lo
desplegamos en `www.oreilly.com`, la ruta completa hacia este servlet sería
http://www.oreilly.com/learningjava/hello. Así como podríamos declarar más de una instancia de servlet
con la etiqueta `<servlet>`, podríamos declarar más de un `<servlet-mapping>` para una instancia de
servlet dada. Podríamos, por ejemplo, mapear redundante mente la misma instancia `helloclient1` a las
rutas `/hello` y `/hola`. La etiqueta `<url-pattern>` proporciona formas muy flexibles de especificar las URL
que deben coincidir con un servlet. Hablaremos sobre esto en detalle en la próxima sección.

Finalmente, debemos mencionar que aunque el ejemplo `web.xml` mencionado anteriormente


funcionará en algunos servidores de aplicaciones, técnicamente está incompleto porque le falta
información formal que especifique la versión de XML que está utilizando y la versión del estándar de
archivo `web.xml` con el que cumple. Para que cumpla totalmente con los estándares, agrega una línea
como:

A partir de la API Servlet 2.5, la información de la versión del archivo `web.xml` aprovecha los esquemas
XML. La información adicional se inserta en el elemento `<web-app>`:

Si los dejas fuera, es posible que la aplicación siga funcionando, pero será más difícil para el contenedor
de servlets detectar errores en tu configuración y proporcionarte mensajes de error claros. Algunos
editores inteligentes también aprovechan la información del esquema para ayudar con el resaltado de
sintaxis, autocompletado y otras comodidades.
El equivalente de la declaración y mapeo de servlets anteriores que vimos antes, es nuestra anotación de
una línea:

Aquí, el atributo `urlPatterns` de `WebServlet` nos permite especificar uno o más patrones de URL que
son equivalentes a la declaración `url-pattern` en el `web.xml`.

Mapeos de Patrones de URL

El `<url-pattern>` especificado en el ejemplo anterior era una cadena simple, `/hello`. Para este patrón,
solo una coincidencia exacta de la URL base seguida por `/hello` invocaría nuestro servlet. Sin embargo,
la etiqueta `<url-pattern>` es capaz de manejar patrones más potentes, incluyendo comodines. Por
ejemplo, especificar un `<url-pattern>` de `/hello*` permite que nuestro servlet sea invocado por URL
como http://www.oreilly.com/learningjava/helloworld o .../hellobaby. Incluso puedes especificar
comodines con extensiones (por ejemplo, `*.html` o `*.foo`), lo que significa que el servlet es invocado
para cualquier ruta que termine con esos caracteres.

El uso de comodines puede resultar en más de una coincidencia. Consideremos URLs que terminan en
`/scooby*` y `/scoobydoo*`. ¿Cuál debería coincidir para una URL que termina en .../scoobydoobiedoo?
¿Qué sucede si tenemos un tercer posible partido debido a un mapeo de extensión de comodín? Las reglas
para resolver estos casos son las siguientes:

- Primero, se toma cualquier coincidencia exacta. Por ejemplo, `/hello` coincide con el patrón `/hello` en
nuestro ejemplo, independientemente de cualquier `/hello*` adicional.

- Si no hay coincidencias exactas, el contenedor busca la coincidencia de prefijo más larga. Por lo tanto,
`/scoobydoobiedoo` coincide con el segundo patrón, `/scoobydoo*`, porque es más largo y
presuntamente más específico.

- Si no hay coincidencias allí, el contenedor busca mapeos de comodines de sufijo. Una solicitud que
termina en `.foo` coincide con un `*.foo` en este punto del proceso.

- Finalmente, si no hay coincidencias, el contenedor busca un mapeo predeterminado, de captura todo,


nombrado `/*`. Un servlet mapeado a `/*` recoge cualquier cosa que no haya coincidido en este punto. Si
no hay un mapeo de servlet predeterminado, la solicitud falla con un mensaje de "404 no encontrado".

Despliegue de HelloClient

Una vez que hayas desplegado el servlet HelloClient, debería ser fácil agregar ejemplos al WAR mientras
trabajas con ellos en este capítulo. En esta sección, te mostraremos cómo construir un archivo WAR
manualmente. Seguramente hay una variedad de herramientas disponibles para ayudar a automatizar y
gestionar archivos WAR, pero el enfoque manual es directo y ayuda a comprender mejor el contenido.

Para crear el archivo WAR manualmente, primero creamos los directorios `WEB-INF` y `WEB-INF/classes`.
Si estás utilizando un archivo `web.xml`, colócalo en `WEB-INF`. (Recuerda que el archivo `web.xml` no es
necesario si estás utilizando la anotación `WebServlet` con Tomcat 7 o posterior). Coloca
`HelloClient.class` en `WEB-INF/classes`. Utiliza el comando `jar` para crear `learningjava.war` (`WEB-INF`
en el nivel "superior" del archivo):
También puedes incluir documentos y otros recursos en el WAR agregando sus nombres después del
directorio `WEB-INF`. Este comando produce el archivo `learningjava.war`. Puedes verificar el contenido
utilizando el comando `jar`:

Ahora, todo lo que es necesario es colocar el archivo WAR en la ubicación correcta para tu servidor. Si aún
no lo has hecho, debes descargar e instalar Apache Tomcat. Puedes descargar la versión 9 y encontrar
documentación útil en el sitio web de Apache Tomcat.

La ubicación para los archivos WAR es el directorio `webapps` dentro del directorio de instalación de tu
Tomcat. Coloca tu archivo WAR aquí y luego inicia el servidor. Si Tomcat está configurado con el número
de puerto predeterminado, deberías poder acceder al servlet HelloClient con una de estas dos URL:
`http://localhost:8080/learningjava/hello` o `http://<tuserver>:8080/learningjava/hello`, donde
`<tuserver>` es el nombre o la dirección IP de tu servidor. Si encuentras algún problema, revisa el
directorio de registros (`logs`) de la carpeta de Tomcat para buscar errores.

Recarga de aplicaciones web

Todos los contenedores de servlets deben proporcionar una facilidad para recargar archivos WAR;
muchos admiten la recarga de clases de servlet individuales después de que han sido modificadas. La
recarga de archivos WAR es parte de la especificación de servlets y es especialmente útil durante el
desarrollo. El soporte para recargar aplicaciones web varía de un servidor a otro. Normalmente, todo lo
que debes hacer es colocar un nuevo archivo WAR en lugar del anterior en la ubicación adecuada (por
ejemplo, el directorio `webapps` para Tomcat) y el contenedor cerrará la aplicación antigua y desplegará
la nueva versión. Esto funciona en Tomcat cuando el atributo "autoDeploy" está configurado (viene
activado por defecto) y también en el servidor de aplicaciones WebLogic de Oracle cuando está
configurado en modo de desarrollo.

Algunos servidores, incluido Tomcat, "exploran" (descomprimen) los archivos WAR al desplegarlos en un
directorio dentro del directorio `webapps`, o permiten configurar explícitamente un directorio raíz (o
"contexto") para tu aplicación web desempaquetada a través de sus propios archivos de configuración.
En este modo, es posible que te permitan reemplazar archivos individuales, lo cual puede ser
especialmente útil para ajustar archivos HTML o JSP. Tomcat recarga automáticamente archivos WAR
cuando los cambias (a menos que esté configurado para no hacerlo), así que todo lo que tienes que hacer
es colocar un archivo WAR actualizado sobre el antiguo y se volverá a desplegar según sea necesario. En
algunos casos, puede ser necesario reiniciar el servidor para que todos los cambios surtan efecto. Cuando
haya dudas, apaga y reinicia.

El mundo virtual es, bueno, extenso

Hemos tocado apenas la superficie de todo lo que se puede lograr con Java y la web. Observamos cómo
las facilidades integradas en Java hacen que acceder e interactuar con recursos en línea sea tan simple
como trabajar con archivos. También vimos cómo comenzar a colocar tu propio código Java en el mundo
con servlets. A medida que explores servlets, seguramente te encontrarás con otras bibliotecas de
terceros para agregar a tu proyecto, tal como lo hicimos con el archivo servlet-api.jar. ¡Quizás estés
comenzando a entender lo grande que se ha vuelto el ecosistema de Java!

No solo las bibliotecas y complementos en torno a Java están en expansión. El propio lenguaje Java
continúa creciendo y evolucionando. En el próximo capítulo, veremos cómo estar atentos a las nuevas
características en el horizonte y cómo integrar las características recientemente publicadas en el código
existente.
Capítulo 13

Expansión de Java

El lenguaje Java ya tiene más de 25 años. Ha crecido y cambiado mucho en ese tiempo. Parte del
crecimiento ha sido silencioso e incremental. Otros cambios pueden sentirse bruscos. Incluso el proceso
para introducir cambios en el lenguaje ha evolucionado.

En este capítulo veremos dónde comienzan esos cambios y cómo terminan en una versión real de Java.
Repasaremos el proceso de lanzamiento que discutimos en "Un mapa de Java" en la página 21 y
echaremos un vistazo a algunos de los temas que se están discutiendo para futuras versiones. También
regresaremos al presente y repasaremos cómo actualizar tu código existente con una nueva
característica, y cuándo tiene sentido hacerlo. No todas las nuevas características de Java serán de interés
para todos los desarrolladores de Java. Por otro lado, casi todos los desarrolladores encontrarán algo de
interés en algún lugar del vasto catálogo de capacidades presentes directamente en Java o en sus
numerosas bibliotecas de terceros.

Versiones de Java

Mientras escribimos esta quinta edición, Java 14 está disponible como una versión preliminar. Hemos
estado trabajando con la versión de código abierto del kit de desarrollo, el OpenJDK. Puedes ver versiones
recientes y futuras en la página del Proyecto JDK. Nuevamente, Oracle mantiene el JDK oficial, que puede
ser apropiado para grandes clientes corporativos que buscan soporte pago. Puedes seguir el progreso de
los lanzamientos oficiales en la página de inicio de Java Standard Edition de Oracle. Si tienes curiosidad
sobre exactamente qué características y cambios vienen con cada versión, echa un vistazo a la página de
Notas de lanzamiento del JDK de Oracle.

Después de Java 9, Oracle cambió a un ciclo de lanzamiento cada seis meses para lanzamientos más
pequeños basados en características del lenguaje. Esa cadencia rápida significa que verás actualizaciones
regulares en Java. Puede que esperes con ansias cada nuevo lanzamiento e incorpores sus nuevas
características en tu código de inmediato. O puedes optar por quedarte con una de las versiones de
soporte a largo plazo designadas como Java 8 o Java 11. Como mencionamos antes, no todos los cambios
en Java serán útiles para ti. Pero queremos asegurarnos de que sepas cómo evaluar nuevas características
y estar al tanto de lo que viene a continuación.

JCP y JSRs

Comenzaremos la explicación de qué características se agregan a Java agregando algunos acrónimos más.
El Programa de Proceso de Comunidad de Java (JCP) está diseñado para invitar a la participación pública
en la conformación del mapa de Java. A través de ese proceso, se crean y refinan las Solicitudes de
Especificación de Java (JSRs). Una JSR es simplemente un documento que describe una idea particular y
limitada para la implementación y desarrollo por parte de un equipo de programadores. Por ejemplo, la
JSR 376 describe el Sistema de Módulos de Plataforma Java para manejar mejor las partes necesarias para
construir y desplegar aplicaciones Java. (También puedes buscar todas las JSRs si tienes curiosidad sobre
lo que hay allí, incluyendo ideas que fueron especificadas pero que finalmente fueron retiradas o
rechazadas). Cualquier JSR de suficiente interés podría tener un lugar como característica preliminar en
una próxima versión de Java. Si una idea no está completamente lista para una especificación completa,
podría aparecer como una Propuesta de Mejora del JDK (JEP). No todas las propuestas crecerán más allá
de esa etapa, pero puedes ver que hay un entorno bastante robusto para probar nuevas ideas y llevar a
los ganadores hacia una inclusión eventual.
Si quieres ver qué características van a la siguiente versión, echa un vistazo al sitio de compilación del JDK
de Oracle. Aquí verás JDKs actuales, así como aquellos disponibles para acceso temprano. Las
compilaciones de acceso temprano incluyen notas de lanzamiento sobre lo que está por venir. Pero al
mirar esas versiones de acceso temprano, ten en cuenta el primer descargo de responsabilidad del sitio:
"La funcionalidad de acceso temprano (EA) puede que nunca se incluya en un lanzamiento de
disponibilidad general (GA)." De hecho, los lanzamientos de Java en sí están envueltos en JSRs "paraguas"
como JSR 337 para Java 8. Estas JSRs paraguas son un poco áridas, pero son una descripción autoritativa
de lo que está por venir en el lanzamiento dado.

Expresiones Lambda

La JSR 337, por ejemplo, presagiaba algunos cambios importantes en Java. La edición anterior de este
libro se detuvo en Java 7. El try-with-resources (entre muchas otras características) que utilizamos en el
Capítulo 11 era reciente. Los desarrolladores en ese momento ya estaban buscando otras características
que se habían discutido pero que finalmente no se incluyeron. Una de las adiciones más esperadas era la
idea de las expresiones lambda (JSR 335). Las expresiones lambda te permiten tratar un fragmento de
código como un objeto de primera clase. (En relación con las expresiones lambda, a estos fragmentos de
código se les llama funciones). Si no necesitas usar esa función en ningún otro lugar, esto puede llevar a
programas más concisos y legibles que son más fáciles de entender, una vez que estás familiarizado con
la sintaxis. Al igual que las clases anónimas, las funciones lambda pueden acceder a variables locales que
están en el ámbito donde se escriben. Funcionan muy bien con API orientadas a funciones como el
paquete java.util.stream, también señalado en JSR 335. Estas impresionantes adiciones sí se incluyeron
en Java 8.

Las funciones lambda te permiten abordar problemas con una perspectiva más funcional. La
programación funcional es un estilo más declarativo de programación. Te enfocas en escribir funciones,
métodos con algunas restricciones específicas, en lugar de manipular objetos. No entraremos en detalles
sobre la programación funcional, pero es un paradigma poderoso y que vale la pena explorar mientras
continúas tu viaje de codificación. Incluimos algunos buenos libros para tarea de programación funcional
en "Expansión de Java más allá del núcleo" en la página 437 al final del capítulo.

Adaptación de tu código

Las expresiones lambda parecen bastante interesantes. ¿Qué pasa si queremos usarlas en nuestro propio
código? Esa es una excelente pregunta y se aplicará a cualquier nueva característica. Como hemos
señalado, el cronograma de lanzamientos para Java significa que siempre te enfrentarás a nuevas
versiones. Enfoquemos estas expresiones lambda con la intención de evaluar y potencialmente integrar
nuevas características de Java.

Investigación de la característica

Con cualquier nueva característica, primero deberás entender en qué consiste la característica en sí
misma. Eso podría ser tan simple como un pequeño cambio de sintaxis o tan complejo como una nueva
forma de construir binarios de Java. Nuestras expresiones lambda caen en algún punto intermedio.
Veamos una expresión muy simple y luego úsala en un poco de código.

Entonces, ¿dónde comenzaríamos con las expresiones lambda? Si tienes experiencia en programación en
un lenguaje funcional como Lisp, quizás ya sepas qué son las lambdas y dónde podrían usarse. Si no sabes
mucho sobre el término, podrías buscar en línea. Si la característica ha estado disponible por algún tiempo
(como las expresiones lambda en Java mientras escribimos esta edición del libro a principios de 2020), es
probable que encuentres algunos buenos tutoriales. Si es una característica muy nueva o tus búsquedas
iniciales no están siendo útiles, puedes volver a la JSR. Para las expresiones lambda, nuevamente eso es
JSR 335. La Sección 2 de una JSR es la sección de Solicitud y generalmente contiene algunos consejos
útiles. Aquí está el párrafo de apertura de la sección 2.7 que proporciona una breve descripción de la
característica:
Proponemos extender el Lenguaje Java para admitir expresiones lambda compactas (también
conocidas como clausuras o métodos anónimos). Además, extenderemos el lenguaje para
admitir una conversión conocida como "conversión SAM" para permitir que las expresiones
lambda se utilicen donde se espera una interfaz o clase de método abstracto único, habilitando
la compatibilidad hacia adelante de bibliotecas existentes.

—JSR 335 Sección 2.7

Hay varias palabras clave en solo esas pocas oraciones que podrían ayudarte a buscar más material de
fondo. ¿Qué son las "clausuras"? ¿Qué es la "conversión SAM"? La última oración incluso te da una pista
sobre dónde se usarían las expresiones lambda: en cualquier lugar donde se permita un tipo particular de
interfaz o clase. Ese párrafo ciertamente no es suficiente para comprender completamente las
expresiones lambda por sí solas, pero nuevamente, tiene algunas pistas sobre los temas adecuados para
investigar.

El resto de la JSR debería darte más documentación que puedas leer. Puede incluir contenido que sea
inmediatamente útil, pero más a menudo encontrarás enlaces a material de apoyo, documentos de
diseño de miembros del equipo que trabajan en la JSR, o incluso borradores anteriores de la solicitud
misma para que puedas ver su evolución. También deberías poder encontrar información más concreta
en la documentación de Java para la versión que contiene tu característica (Java 8 en nuestro ejemplo de
lambda). Incluso las compilaciones de acceso temprano tendrán disponible alguna documentación oficial.

Siéntete libre de hacer alguna de esa investigación sobre expresiones lambda en este momento. Lee
algunos de los documentos de soporte de la JSR. Echa un vistazo a los tutoriales de Oracle sobre lambdas
en Java 8. Intenta buscar en línea en sitios como Stack Overflow. Querrás sentirte cómodo encontrando
ejemplos que entiendas de fuentes en las que confíes. Ahora se están lanzando nuevas versiones de Java
cada seis meses. ¡Será útil saber cómo puedes mantenerte actualizado!

Expresiones lambda básicas

Si bien esperamos que hagas algo de esa tarea de investigación, también queremos mostrarte algunos
ejemplos de lo compactas y poderosas que pueden ser las expresiones lambda. La sintaxis básica de una
expresión lambda es simple:

Los "parámetros" son cero o más parámetros nombrados (y posiblemente tipados) que se pasan a la
expresión en el lado derecho del nuevo operador →. La expresión (o bloque de declaraciones dentro del
par usual de llaves) puede devolver un valor o ejecutar algún código. Por ejemplo, aquí tienes una
expresión lambda común de "incremento" para un único parámetro de entrada:

Es importante destacar que esta expresión lambda no altera el valor del parámetro "n". Simplemente
realiza un cálculo. Podrías considerar este ejemplo en particular como una operación "siguiente" para
números enteros. Si tuvieras algún otro contexto que utilizara un método next() para realizar algún
trabajo, podrías suministrar esta expresión lambda. Esto se vuelve más poderoso cuando deseas utilizar
ese mismo contexto para trabajar con otros tipos de objetos como cadenas o fechas. ¿Cuál es la "próxima"
fecha? ¿Es el siguiente día? ¿El próximo año? Con una expresión lambda, puedes proporcionar una versión
personalizada de "próximo" justo donde la necesitas.
Puedes pasar más de un parámetro a tu función. O puedes no pasar ninguno. En la práctica, verás todas
estas variaciones, incluido un atajo popular: si tienes exactamente un parámetro, no necesitas utilizar
paréntesis en el lado izquierdo de la expresión. Las siguientes expresiones son todas válidas:

Consideremos ordenar listas. Si tenemos una lista de números (usaremos la clase contenedora Integer en
este ejemplo), la ordenación es directa:

Pero, ¿qué sucede si quisiéramos los números en orden inverso? Anteriormente, tendríamos que escribir
una clase especial que implemente la interfaz Comparator, o proporcionar una clase interna anónima:

Entiendo, la clase interna anónima funcionó, pero era un poco voluminosa. En su lugar, podríamos usar
una expresión lambda para escribir una versión más compacta:
¡Vaya! Eso es mucho más limpio. Debes entender qué espera como argumentos el método
Collections.sort() y saber que la interfaz Comparator tiene solo un método abstracto (es decir, es una
interfaz de método abstracto único, o SAM; ¿recuerdas la descripción de JSR?). Pero cuando tienes el
entorno adecuado, una expresión lambda puede ser bastante eficiente.

Podríamos tomar estas técnicas y reescribir varios de nuestros ejemplos de "generación de listas" de todo
el libro. Tomemos el fragmento de "Operaciones de archivo" en la página 355 usando objetos java.io.File.
Podríamos ordenar y listar los nombres utilizando los objetos File reales con la ayuda del método
Arrays.asList() (para obtener un Iterable) y luego utilizar una expresión lambda con el método forEach(),
así:

Pudimos obtener nombres de archivos antes sin usar expresiones lambda, pero en muchos casos
podemos escribir un código más conciso con ellas. Por supuesto, debes familiarizarte con la sintaxis de las
expresiones lambda, ¡pero para eso es toda esta práctica!

Referencias de métodos

De hecho, el proceso de ordenar objetos complejos utilizando uno de sus atributos es tan común que hay
un método auxiliar en Java que crea la función adecuada ya en la API. El método estático
Comparator.comparing() puede ayudar a escribir algo similar a nuestra expresión lambda que usa
compareTo() en la sección anterior. Aprovecha las referencias de métodos, un tipo simplificado de
expresión lambda que utiliza métodos existentes de otras clases.

Hay muchos detalles y casos de uso para las referencias de métodos en los que no entraremos aquí, pero
la sintaxis básica y el uso son sencillos. Colocas el separador :: entre el nombre de una clase y el nombre
de un método. El método Comparator.comparing() espera una referencia a un método que pueda ser
usado en los objetos que se están ordenando (es decir, aún debes llamar a métodos apropiados). Al
ordenar nuestros objetos File, podemos usar cualquiera de los métodos getter que devuelvan información
fácil de ordenar, como el nombre o el tamaño del archivo:

¡Eso está bastante limpio! Y podemos ver exactamente lo que se pretende: vamos a ordenar un montón
de archivos comparando sus nombres. Lo cual, por supuesto, es exactamente lo que hicimos con las
lambdas en la sección anterior. Recuerda, no es que el uso de referencias a métodos sea mejor, ya que
siempre pueden ser reemplazadas por una expresión lambda; sino que muchas veces, las referencias a
métodos pueden proporcionar un código más legible una vez que te acostumbras a la nueva sintaxis
(como sucede con las propias expresiones lambda).
Lambdas eventuales

Hemos visto algunos otros ejemplos de código que tienen entornos igualmente restringidos. Piensa en los
muchos controladores de eventos en el Capítulo 10. Varios listeners eran exactamente del tipo de un solo
método abstracto, como la interfaz ActionListener utilizada por JButton o JMenuItem. Cuando sea
apropiado, podemos usar una expresión lambda para simplificar nuestro código de manejo de eventos. A
menudo tenemos manejadores simples y temporales para verificar la capacidad básica de hacer clic en un
botón, así:

Ahora podemos usar una expresión lambda para reducir eso bastante. Hace que escribir este tipo de
código de prueba rápida para varios botones sea mucho más fácil:

¡Genial! Las expresiones lambda pueden proporcionar una buena manera de abordar situaciones donde
se necesita un poco de código dinámico. Por supuesto, no todos los manejadores de eventos se prestarán
a este tipo de conversión. Pero muchos sí lo harán, y la notación más compacta puede ayudar a hacer tu
código más legible también.

Reemplazando Runnable

Otra interfaz popular que se ajusta a este modelo es la interfaz Runnable introducida en el Capítulo 9 y
utilizada nuevamente en el Capítulo 10. Vimos ejemplos de cómo usar tanto clases internas como clases
internas anónimas para crear nuevos objetos Thread. El método SwingUtilities.invokeLater() también
necesitaba una instancia de Runnable como argumento. Podemos usar una expresión lambda en estos
casos también. Recuerda el ejemplo de ProgressPretender de "SwingUtilities and Component Updates"
en la página 333. Ya estábamos dentro del método run() de una clase que implementa la interfaz Runnable
cuando tuvimos que crear una segunda instancia anónima de Runnable para actualizar una etiqueta:

Pero ahora podemos usar una expresión lambda para mantener el enfoque en el trabajo real que el hilo
está realizando:
Nuevamente, un trozo de código mucho más compacto, y con suerte, más legible. No es un cambio
obligatorio, ni mejora el rendimiento de la aplicación, pero si tú (y cualquier miembro de tu equipo en un
entorno laboral) comprenden las expresiones lambda, este código puede aumentar la mantenibilidad y
dejarte más tiempo para trabajar en otros problemas.

Es interesante señalar que, sorprendentemente, Java 8 sigue siendo una de las versiones más utilizadas
de Java según diversas encuestas de la industria hasta 2019.

Ampliando Java más allá del núcleo

Es importante señalar que muchas partes de Java hacen uso de JSRs más allá de las características
fundamentales del lenguaje. Por ejemplo, JSR 369 abarca la especificación Java Servlet 4.0. Quizás
recuerdes, desde "Servlets" en la página 412, que necesitábamos el archivo servlet-api.jar por separado
para compilar y ejecutar los ejemplos de servlet. Al revisar la descripción de JSR 369, vemos que la
especificación 4.0 está diseñada para admitir características encontradas en HTTP/2. Si profundizas en
esas características, una de las adiciones más esperadas es el soporte para el "server push", la capacidad
para que el servidor acelere la entrega de páginas complejas al "empujar" algunos archivos o recursos
antes de su uso real.

Bajo el protocolo HTTP/1.1, una página HTML se enviaría a tu navegador cuando visitaras un sitio. Esta
página, a su vez, indicaría al navegador que solicitara otros recursos como archivos JavaScript, hojas de
estilo, imágenes, etc. Cada uno de estos recursos requeriría una solicitud separada. Las cachés aceleran
parte de este proceso, pero la primera vez que visitas un sitio nuevo, nada está en la caché, por lo que el
tiempo de carga puede ser significativo. HTTP/2 permite que el servidor envíe recursos por adelantado,
aprovechando eficientemente una conexión existente. Esta optimización acelera la entrega de una página
incluso si contiene cosas que no pueden ser almacenadas en caché o no lo están.

Ahora, la transición a HTTP/2 es realmente un gran avance y no todos los sitios lo usarán, ni todos los
navegadores lo admitirán o admitirán todas las opciones. No podemos cubrirlo aquí, pero si el trabajo
relacionado con la web es parte de tu vida diaria, podría valer la pena investigar en línea. De todos modos,
es bueno recordar que puedes consultar el sitio de JCP para ver qué está por venir en Java con respecto
al lenguaje en sí y su ecosistema más amplio.

Conclusión final y próximos pasos

Apenas hemos arañado la superficie de las expresiones lambda y sus partes relacionadas de Java 8,
incluidas las referencias a métodos y la API de Streams. Lamentablemente, como tantos otros temas
interesantes que hemos abordado en este libro, debemos dejar la exploración más detallada a tu
disposición. Afortunadamente, Java 8 lleva muchos, muchos años3 en circulación y abundan los recursos
en línea para estas características. También puedes obtener detalles excelentes sobre las lambdas en
particular y la programación funcional en Java en general en "Java 8 Lambdas" de Richard Warburton
(O'Reilly). En el mundo de los servlets, la versión 4 de la especificación es más reciente, pero aún hay
excelentes recursos en línea que cubren tanto esta especificación como HTTP/2.

¡Pero uf! ¡Lo lograste! Decir que cubrimos mucho terreno es bastante quedado. Esperemos que tengas
una buena base para seguir adelante, aprendiendo más detalles y técnicas avanzadas. Elige un área que
te interese y profundiza un poco más. Si aún te intriga Java en general, intenta conectar partes de este
libro. Por ejemplo, podrías escribir un servlet para responder a solicitudes similares a las realizadas por el
cliente DateAtHost en "El cliente DateAtHost" en la página 384. Podrías intentar usar expresiones
regulares para analizar nuestro protocolo de juego de lanzamiento de manzanas. O podrías construir un
protocolo más sofisticado por completo y enviar bloques binarios a través de la red en lugar de simples
cadenas. Para practicar escribiendo programas más complejos, podrías reescribir algunas de las clases
internas y anónimas en el juego para que sean clases separadas y autónomas, o aprovechar las
expresiones lambda.

Si deseas explorar otras bibliotecas y paquetes de Java mientras te mantienes en algunos de los ejemplos
en los que ya has trabajado, podrías adentrarte en la API Java2D y crear manzanas y árboles con mejor
aspecto. Podrías investigar el formato JSON y probar a reescribir los servlets ShowParameters y
ShowSession para devolver un bloque de JSON válido en lugar de una página HTML. Podrías probar
algunos de los otros objetos de colección, como TreeMap o Stack.

Y si estás listo para aventurarte más, podrías ver cómo Java funciona fuera del escritorio probando algo
de desarrollo para Android. O echar un vistazo a entornos de red muy grandes y la Jakarta Enterprise
Edition de la Eclipse Foundation. ¿Quizás el big data está en tu radar? La Apache Foundation tiene varios
proyectos como Hadoop o Spark. Java tiene sus detractores, pero sigue siendo una parte vibrante y vital
del mundo profesional de los desarrolladores.

Con todas esas opciones frente a ti, estamos listos para concluir la parte principal de nuestro libro. El
Glosario contiene una referencia rápida de muchos términos útiles y temas que hemos cubierto. Y el
Apéndice A explica cómo instalar el editor IntelliJ IDEA y cómo importar y ejecutar los ejemplos de código.
Esperamos que hayas disfrutado de Aprender Java. Esta, la quinta edición de Aprender Java, es realmente
la séptima edición de la serie que comenzó hace más de dos décadas con Explorando Java. Ha sido un
viaje largo y asombroso ver cómo Java se desarrolla en ese tiempo, y agradecemos a aquellos de ustedes
que han estado con nosotros a lo largo de los años. Como siempre, agradecemos tus comentarios para
ayudarnos a seguir mejorando este libro en el futuro. ¿Listo para otra década de Java? ¡Nosotros sí!
APPÉNDICE A

Ejemplos de código e IntelliJ IDEA

Este apéndice te ayudará a ponerte en marcha con los ejemplos de código que se encuentran a lo largo
del libro. Algunos de los pasos aquí mencionados fueron abordados en el Capítulo 2, pero queremos
explicarlos más detalladamente aquí, con instrucciones específicas sobre cómo utilizarlos en la versión
gratuita de la Community Edition de IntelliJ IDEA de JetBrains.

También queremos reiterar que IntelliJ IDEA no es el único entorno de desarrollo integrado (IDE)
compatible con Java disponible. ¡Ni siquiera es el único gratuito! Microsoft's VS Code se puede configurar
rápidamente para admitir Java. Y Eclipse, mantenido por IBM, sigue estando disponible. Y para
principiantes que buscan una herramienta diseñada para introducirlos tanto en la programación Java
como en el mundo de los IDE de Java, pueden echar un vistazo a BlueJ creado por el King's College London.

Obteniendo los ejemplos principales de código

Independientemente del IDE que utilices, querrás obtener los ejemplos de código del libro desde GitHub.
Aunque a menudo incluimos listados completos de código fuente al discutir temas específicos, muchas
veces hemos omitido cosas como declaraciones de importación o paquetes, o la estructura de clase que
los engloba, por brevedad y legibilidad. Los ejemplos de código pretenden ser completos para que puedas
abrirlos en un editor o IDE para revisarlos, o compilarlos y ejecutarlos para reforzar las discusiones en el
libro.

Puedes visitar GitHub en un navegador para explorar los ejemplos individuales sin necesidad de descargar
nada. Simplemente ve al repositorio learnjava5e. (Si ese enlace no funciona, ve a github.com y busca el
término "learnjava5e"). Puede valer la pena echar un vistazo a GitHub en general, ya que se ha convertido
en el principal punto de encuentro para desarrolladores de código abierto e incluso equipos corporativos.
Puedes revisar el historial de un repositorio, reportar errores y discutir problemas relacionados con el
código.

El nombre del sitio se refiere a la herramienta git, un sistema de control de código fuente o gestor de
código fuente que los desarrolladores utilizan para administrar revisiones entre equipos en proyectos de
código. Si tu plataforma aún no tiene disponible el comando git, puedes descargarlo aquí. GitHub tiene su
propio sitio para ayudarte a aprender sobre git en try.github.io. Una vez instalado git, puedes clonar el
proyecto en una carpeta de tu computadora. Puedes trabajar desde ese clon o mantenerlo como una
copia limpia de los ejemplos de código. Si publicamos correcciones o actualizaciones en el futuro, también
puedes sincronizar fácilmente tu carpeta clonada.

También puedes obtener todos los ejemplos descargando la rama principal del proyecto como un archivo
ZIP. (Si el enlace de este documento no es utilizable, busca el botón "Clone or Download" en la página
principal del repositorio). Una vez descargado, simplemente descomprime el archivo en una carpeta
donde puedas encontrar fácilmente los ejemplos. Deberías ver una estructura de carpetas similar a la que
se muestra en la Figura A-1.
Las siguientes secciones cubrirán cómo poner en marcha IntelliJ IDEA y luego importaremos los ejemplos
de código.

Instalación de IntelliJ IDEA

Para empezar, querrás dirigirte al sitio web de JetBrains y descargar una copia de la versión gratuita
Community Edition desde https://oreil.ly/4bexF. El sitio generalmente puede detectar tu plataforma,
pero asegúrate de obtener el archivo binario correcto para tu sistema operativo (o para el sistema donde
planeas instalar y ejecutar IntelliJ IDEA si tienes más de una máquina).

Hay una guía de instalación muy útil con todo lo que necesitas para comenzar, pero aquí resumiremos los
aspectos esenciales para cada plataforma.

Instalación en Linux

En Linux, JetBrains recomienda instalar la aplicación en la carpeta /opt. Sin embargo, puedes instalar
IntelliJ IDEA en una ubicación alternativa si así lo prefieres. Similar al OpenJDK en sí (consulta "Instalación
de OpenJDK en Linux" en la página 29), puedes extraer el archivo tar.gz a tu destino elegido, así:

Para ejecutarlo, busca el archivo de script idea.sh en la carpeta bin donde descomprimiste la descarga.
Tendrás que aceptar el acuerdo de licencia y responder algunas preguntas iniciales, como el esquema de
color que deseas utilizar y qué complementos podrías necesitar. Después de responder a estas preguntas
(que son únicas), deberías ver la pantalla de bienvenida que se muestra en la Figura A-2.
Pasaremos por los pasos para importar los ejemplos de código en "Importación de los ejemplos" en la
página 444.

En macOS, descargarás un archivo .dmg que puedes hacer doble clic para montar y luego arrastrar el
archivo de la aplicación IntelliJ IDEA a tu carpeta de Aplicaciones, tal como harías con otras instalaciones
independientes en macOS. Una vez que se haya copiado ese archivo, puedes iniciar la aplicación y
responder las preguntas de licencia y preferencias. Deberías ver una pantalla similar a la Figura A-3,
aunque es probable que no veas la lista de proyectos abiertos anteriormente a la izquierda. (Si cierras
todas las ventanas activas de IntelliJ IDEA, esta pantalla de bienvenida reaparecerá y la lista a la izquierda
se llenará con tus proyectos).

Pasaremos por los pasos para importar los ejemplos de código en "Importación de los ejemplos" en la
página 444.
Instalación en Windows

La página de descarga de Windows en JetBrains te permite elegir un archivo .zip o un archivo autoextraíble
.exe. Simplemente desempaqueta la versión que hayas descargado. Puedes iniciar la aplicación justo
donde la desempaques; en el primer inicio, te guiará a través de la configuración de IntelliJ IDEA y te
preguntará dónde instalarlo y si quieres accesos directos en el escritorio, entre otras cosas.

Después de que el proceso de instalación termine, podrás ejecutar IntelliJ IDEA. Como en otras
plataformas, tendrás que responder algunas preguntas iniciales y aceptar la licencia. Terminarás con la
misma pantalla de bienvenida, como se muestra en la Figura A-4.

Ahora veremos cómo importar los ejemplos de código fuente para que puedas utilizarlos fácilmente
dentro de IntelliJ IDEA.

Importando los Ejemplos

Antes de examinar el proceso de importación en IntelliJ IDEA, es posible que desees renombrar la carpeta
donde descargaste los ejemplos de código desde GitHub. Si utilizaste el archivo .zip o el proceso de
checkout más simple, es probable que tengas una carpeta llamada learnjava5emaster. Ese es un nombre
perfectamente válido, pero si prefieres algo más amigable (o más corto), adelante y selecciona ese
nombre ahora. Hará más sencillo importar en el IDE. Renombraremos la carpeta como LearningJava.

Ahora regresa a esa pantalla de bienvenida y selecciona la opción "Importar Proyecto". (Si ya has utilizado
IntelliJ IDEA y no ves la pantalla de bienvenida, también puedes seleccionar Archivo → Nuevo → Proyecto
desde Fuentes Existentes...). Navega hasta la carpeta de ejemplos de código, como se muestra en la Figura
A-5. Asegúrate de seleccionar la carpeta principal y no una de las carpetas individuales de los capítulos.
Después de abrir la carpeta de ejemplos, se te pedirá que revises las bibliotecas encontradas, como se
muestra en la Figura A-6. Si aún no hay ninguna, simplemente haz clic en "Siguiente".

Puedes cambiar el nombre del proyecto si así lo deseas en la siguiente pantalla, pero asegúrate de que la
ubicación siga apuntando a la carpeta de ejemplos de código de nivel superior. Nosotros preferimos
nuestro nombre "LearningJava", así que dejamos ambos elementos tal como están, como puedes
observar en la Figura A-7.
Los archivos fuente deberían encontrarse, por lo que adelante y deja marcada la casilla de verificación en
la siguiente pantalla (ver Figura A-8) y haz clic en "Siguiente".

A partir de la versión 2019-2.4, se te pedirá una segunda vez sobre las bibliotecas que se encontraron
(similar a la Figura A-6 anterior). Todavía no hay ninguna asociada con nuestros ejemplos simples, así que
continúa y haz clic en "Siguiente". (Discutimos cómo agregar la biblioteca de servlets necesaria para los
ejemplos en "Obteniendo los Ejemplos de Código Web" en la página 454).

Nuestros ejemplos no aprovechan ninguna de las características de módulo introducidas en Java 9, así
que simplemente mantén marcada la única casilla de verificación en la siguiente pantalla (Figura A-9) y
haz clic en "Siguiente".
A continuación, se te pedirá seleccionar tu SDK (kit de desarrollo de software, en este caso es sinónimo
de la versión de Java). Elegimos la versión de soporte a largo plazo (11), como puedes ver en la Figura A-
10, pero puedes seleccionar la versión 11 o posterior que tengas instalada. (Consulta "Instalación del JDK"
en la página 28 si necesitas recordar cómo descargar e instalar el Java SDK).

Haz clic en "Finalizar" y deberías tener un proyecto de IntelliJ IDEA listo para usar, tal como se muestra en
la Figura A-11.
Ejecutando los ejemplos

Para ejecutar los ejemplos, como se menciona en los Capítulos 2 y 3, puedes usar una terminal o símbolo
del sistema para compilar los ejemplos con `javac` y luego ejecutarlos con `java`. Pero dado que tenemos
IntelliJ IDEA configurado, veamos cómo ejecutar los ejemplos desde el IDE.

Dirígete a través de tu proyecto hasta la carpeta ch02 y haz doble clic en la entrada HelloJava. Deberías
tener ahora una pestaña de código fuente con HelloJava.java, como se muestra en la Figura A-12.

Ahora puedes editar el archivo, por supuesto, pero lo dejaremos como está por el momento. De regreso
en la estructura del proyecto a la izquierda, haz clic derecho en la entrada HelloJava y selecciona la opción
"Run HelloJava.main()"; deberías encontrarla hacia el centro del menú contextual que aparece, como se
ve en la Figura A-13.
Una vez que hayas ejecutado una clase en particular, IntelliJ IDEA generalmente establecerá esa clase
como la acción predeterminada para el botón "play" (reproducción) en la barra de herramientas. Esa es
una forma más rápida de lanzar la misma aplicación nuevamente, perfecta si estás probando una nueva
clase, realizando cambios y probando de nuevo. Sin embargo, si pasas a una nueva clase, necesitarás
regresar y lanzar la nueva clase utilizando el menú contextual del clic derecho. En ese momento, la nueva
clase debería ser la predeterminada para el botón de reproducción.

Nuestra ventana amigable (aunque simple) debería aparecer, como se muestra en la Figura A-14.
¡Felicidades! IntelliJ IDEA está configurado y listo para que comiences a explorar el increíble y gratificante
mundo de la programación en Java. Si no estás interesado en utilizar Java para la programación web,
puedes dejar excluida la carpeta ch12. Sin embargo, si planeas probar los ejemplos de ese capítulo, y
definitivamente te recomendamos que lo hagas, continúa leyendo para agregar la biblioteca requerida.

Obteniendo los Ejemplos de Código Web

Regresa a GitHub en un navegador y busca el segundo repositorio. (Nuevamente, si el enlace no funciona,


simplemente ve a github.com y busca el término "learnjava5e-web"). Este es un repositorio mucho más
pequeño y está configurado exactamente igual que los ejemplos principales. Los hemos separado aquí
para que puedas concentrarte en los primeros ejemplos sin necesidad de bibliotecas adicionales.

Puedes usar git desde la terminal como antes o descargar el archivo ZIP. Si descargas el ZIP,
descomprímelo. Renombraremos la carpeta principal como LearningJavaWeb.

Ahora selecciona Archivo → Nuevo → Proyecto desde Fuentes Existentes... y navega hasta la carpeta de
ejemplos web. Asegúrate de seleccionar la carpeta principal y no la carpeta individual ch12. Ahora
deberías tener un segundo proyecto en IDEA, pero necesitamos algo adicional para los servlets.

Trabajando con Servlets

El Capítulo 12 trata sobre el uso de Java en el mundo de la programación web. Hay mucho que puedes
hacer con Java y la web utilizando únicamente las API disponibles en el JDK. Pero si deseas escribir servlets,
necesitas descargar un contenedor y decirle a IntelliJ IDEA dónde encontrar la biblioteca de servlets.

Como se menciona en "Desplegando HelloClient" en la página 427, debes descargar e instalar Apache
Tomcat. Puedes obtener la última versión y encontrar documentación útil en el sitio web de Apache
Tomcat. También puedes ir directamente a descargar la versión 9 aquí: https://oreil.ly/HWy7I. Puedes
obtener el formato .zip o .tar.gz, el que prefieras. Descomprime el archivo en cualquier carpeta
conveniente donde puedas encontrarlo fácilmente más tarde. Necesitarás la versión 9 si deseas explorar
la característica de server push mencionada en "Expandiendo Java más allá del núcleo" en la página 437,
pero puedes revisar qué versiones de Tomcat admiten qué versiones de la API de Servlet en la página de
"Which Version" de Tomcat.

En IntelliJ IDEA, abre la ventana "Estructura del Proyecto". Puedes hacer clic derecho en el proyecto (la
entrada LearningJavaWeb en nuestro caso) y seleccionar "Abrir Configuración de Módulos" o usar la
opción de menú Archivo → Estructura del Proyecto.... Deberías ver la ventana mostrada en la Figura A-
15. Selecciona la opción "Bibliotecas" en la jerarquía de la izquierda.
Haz clic en el ícono + en la esquina superior izquierda de la columna central y selecciona "Java" como el
tipo de biblioteca que estás agregando. Ahora necesitas navegar hasta donde descargaste y
descomprimiste Tomcat. Necesitamos el archivo servlet-api.jar que se encuentra en la carpeta lib, tal
como se muestra en la Figura A-16.

Ve adelante y haz clic en "Abrir" en ese archivo y luego en "Aceptar" cuando veas el siguiente diálogo que
indica que estamos añadiendo al módulo LearningJava. Deberías terminar con una sección de Bibliotecas
que se vea como en la Figura A-17. Continúa y haz clic en "Aceptar".
Para verificar que la biblioteca de servlets esté instalada correctamente, puedes construir el proyecto
utilizando la opción de menú Build → Build Project. IntelliJ IDEA debería procesar por un momento y luego
informar que la construcción se completó con éxito. Aún necesitas seguir los pasos de implementación en
"Desplegando HelloClient" en la página 427, pero ahora puedes utilizar todas las excelentes funciones del
IDE, como el autocompletado de código, con tus ejemplos de servlets.

Si estás realizando (o eventualmente realizarás) mucha programación web, es posible que desees
considerar la edición de pago "Ultimate" de IntelliJ IDEA. Tiene varias características fantásticas para
trabajar con servlets y tecnologías web relacionadas. Puedes obtener una vista previa de lo que puede
hacer la Edición Ultimate en la sección de ayuda sobre Aplicaciones Web.

También podría gustarte