Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Piensa en Java, 4ta Edicion (Más Importante)
Piensa en Java, 4ta Edicion (Más Importante)
PIENSA EN JAVA
Cuarta Edición
BRUCEECKEL
President, MindView, ¡ne.
Traducción
Vuelapluma
· PEARSON
, Prentice .
, Hall
PIENSA EN JAVA
Bruce EckeJ
PEARSON EDUCACIÓN S.A. , Madrid, 2007
ISBN: 978-84-8966-034-2
DERECHOS RESERVADOS
© 2007 por PEARSON EDUCACIÓN S.A.
Ribera del Loira, 28
28042 Madrid
PIENSA EN JAVA
Bruee Eekel
ISBN: 978-84-8966-034-2
Authorized translation from the English language edition, entitled THINlUNG IN JAVA, 4" Edition by
ECKEL BRUCE, published by Pearsoo Education loe, publishing as Prentiee Hall, Copyright © 2006
EQillPO EDITORIAL
Editor: Miguel Martín-Romo
Técnico editorial: Marta Caicoya
EQUIPO DE PRODUCCIÓN:
Director: José A. CIares
Técnico: María Alvear
Java SE6. . . . .... .. ..... . xx Programación del lado del clicntc ............. 18
La cuarta edición .. .. .. . . .. . .. ..... ... .... xx Programación del lado del servidor . . . . . . . . .. .. 21
Cambios ... . xxi Resumen........................ . ... 22
Sobre el diseño de la cubierta .......... . .... xxii 2 Todo es un objeto ....... . ...... . ........ 23
Agradecimientos. . . . . . . . . . . .......... XXll
Los objetos se manipulan mediante referencias .. 23
Introducción . ..... .. .. .... . ........... . xxv Es necesario crear todos los objetos. . . 24
Prerrequisitos ... .... xxv Los lugares de almacenamiento ... ..... ... . . . 24
Aprendiendo Java ..... .. . . , •• . •. . . . XXVI Caso especial: tipos primitivos. . . . . . . . . . . . . ..25
Objetivos ................ . . . . . . . XXVI Matrices en Java. . . . . . . . ... . .. .. . .. 26
Enseñar con este libro ........... . ........ XXVll Nunca es necesario destruir un objeto ......... 27
Documentación del JDK en HTML . . . XXVI1 Ámbito. .. .. . . . . . . . . . .. . .. . .... . .. 27
Ejercicios ............................. XXVll Ámbito de los objetos .. ............. . .. 27
Fundamentos para Java . .. ........... .... . XXVIl Creación de nuevos tipos de datos: class ..... '.. 28
Código fuente ........ . ..... . . . .. . ..... XXVIII Campos y métodos . . . .... . ... . ... ... 28
Estándares de codificación. . .. xxix Métodos, argumentos y valores de retomo ...... 29
Errores .. ............................... xxx La lista de argumentos ....................... 30
1 Introducción a los objetos . ... ...... . ... . . . 1 Construcción de un programa Java .... 3 1
El progreso de la abstracción ............ •. ... 1 Visibilidad de los nombres. . .... 31
Todo objeto tiene una interfaz ..... . .. ........ 3 Utilización de otros componentes. . ... . .. . 31
Un objeto proporciona servicios ...... . ........ 4 La palabra clave static ......... . . .. 32
La implementación oculta . .......... ......... 5 Nuestro primer programa Java .. 33
Reutili zación de la implementación ..... ... .... 6 Compilación y ejecución. . . . 35
Herencia. . . . . . . . . . . . . . . . . . . . . . .. . ...... 6 Comentarios y documentación embebida ....... 35
Relaciones es-un y es-como-un ....... ... . . .... . 8 Documentación mcdiantc comcntarios .......... 36
Objetos intercambiables con polimorfismo ...... 9 Sintaxis. . . 36
La jerarquía de raíz única. . . .... ...... 11 HTML embebido . ... . ... . .. . .... 37
Contenedores ... .......................... 12 Algunos marcadores de ejemplo ... . ... . . . .37
Tipos paramctrizados (genéricos) . . ... 13 Ejemplo de documentación. . . 39
Creación y vida de los objetos ... . ... ........ 13 Estilo de codificación. .. . . . . ... 39
Tratamiento de excepciones: manejo de errores .. 15 Resumen .............. . ..... ... .. ... .. .. 40
Programación concurrente .................. 15 Ejercicios ...... .. .. . . . ......• .. .......... 40
x Piensa en Java
3 Operadores ... ... .... ... . .. .... . . . .... . 43 Sobrecarga con primitivas. ..... .. .. ... . . 89
Delegación ....... . ....... . .... ... . . 145 Ampliación de la interfaz mediante herencia .. 201
Combinación de la composición Co lisiones de nombres al combinar interfaces ... 202
y de la herencia .... . ............ 147 Adaptaci ón a una interfaz .. ....... 203
Cómo garantizar una limpieza apropiada .. 148 Campos en las interfaces .205
Ocultac ión de nombres ........... . .. . 151 Inicializac ión de campos en las interfaces. .205
Cómo elegir entre la composición y la herencia 152 Anidamiento de interfaces. . . . . . . 206
protected ....... . .............. . .... 153 Interfaces y factorías ............. . . . ...... 208
Upcasting (generalización) ..... . . ..... ..... 154 Resumen ........ ........ .. . . ....... .... 210
¿Por qué "upcasling'? . . .. . . 155
10 Clases internas .. .. . . .. ......... . . .. ... 211
Nueva comparación entre la composición
y la herencia. .. . . .. . . . . . . 155 Creación de clases internas .... ... . ..... 2 11
La palabra clave final ............. 156 El enlace con la clase externa .. .. .... ..... 213
Datos final .. ....... .... . . . . .. ... 156 Utilización de .this y .new . .... . ..... 214
La estructura foreach y los iteradores ... 268 System.out.format( ) .... .. . . .... ... . . ... . . 324
El método basado en adaptadores. . . . . . .. 270 La clase Formatter ..... . . . . . .... ..325
Resumen. . . . ........ . ...... . 273 Especificadores de formato .. . ........ 326
12 Tratamiento de errores Conversiones posibles con Formattcr . . 327
mediante excepciones . ...• ........... .. 277 String.format(). ......... . 329
Expresiones regulares ........ • ..... 331
Conceptos ....................... . • . .... 277
Fundamentos básicos. . .......... . 33 1
Excepciones básicas ...... . . ........ 278
Creación de expresiones regulares ... .... ...... 333
Argumentos de las excepciones. . . 279
Cuantificadores · . 335
Detección de una excepción ................ 279
Pattcrn y Matcher . ... . 336
El bloque try . . . . .. 280
split(). · . 342
Rutinas de tratamiento de excepciones ......... 280
Operaciones de sustitución · . 343
Creación de nuestras propias excepciones .. 281
reset( ) . . .. 344
Excepciones y registro. . . . . . . . . . . . . . . . . .. 283
Expresiones regulares y E/S en Java .... 345
La especificación de la excepción .... . .. .... 286
Análisis de la entrada . . ............. 346
Cómo capturar una excepción .. . . . ..... 286
Delimitadores de Scanner .348
La traza de la pila .. 288
Análisis con expresiones regulares. . ..... 348
Regeneración de una excepción ....... . .. .... 289
Encadenamiento de excepciones ... .... . ..... 29 1
StringTokenizer. ............. . .... 349
Resumen. . ..... .. . . . ....... . . .. .... 350
Excepciones estándar de Java .............. 294
Caso especial: RuntimeException .. . . . . . ..... 294 14 Información de tipos . ........... • ... . ... 351
Realización de tareas de limpieza con finally .. 295 La necesidad de RTTI. ........ . . . ... 35 1
¿Para qué sirve finally? . ........... . .. 296 El objeto Class .... .353
Utilización de finaU)' durante la ej ecución Literales de clase ........ . .357
de la instrucción return. .. 299
Referencias de clase genéricas .. 359
Un error: la excepción perdida .... 300
Nueva sintaxis de proyección ................ 36 1
Restricciones de las excepciones ............ 301
Comprobación antes de una proyección. .361
Constructores ................... .•. . ..303
Utilización de literales de clase · . 367
Localización de excepciones ........•.•.... 307
Instanceof dinámico .. ..... 368
Enfoques alternativos. . .........•.... 308
Recuento recurs ivo. . . . . . . . . . . . . . . . . . . 369
Historia .. .309
Factorías registradas . . . . . ..... ........ 37 1
Perspectivas ... . ......... . .3 10
instanceof y equivalencia de clases .......... 373
Paso de las excepciones a la consola ... 312
Reflexión: infonnación de clases en tiempo de
Conversión de las excepciones comprobadas ejecución ............................ 374
en no comprobadas ...... 3 13
Un extractor de métodos de clases . 375
Directrices relativas a las excepciones . 3 15
Proxies dinámicos. . . . . . . . 378
Resumen ............................... 315
Objetos nulos. . . . . . . . . . . . . . . . . . . . . . . . .. 38 1
13 Cadenas de caracteres ... ............ ... 317 Objetos maqueta y stubs .. .387
Cadenas de caracteres inmutables . . . . . . . . . 317 Interfaces e información de tipos .. 387
Comparación entre la sobrecarga de '+' y Resumen. . .. .... 392
StringBuilder . . . . . . . . . . . . . . . . . 318
Recursión no intencionada .... ........ ..... 32 1 15 Genéricos ..... .. ..................... 393
Operaciones con cadenas de caracteres ....... 322 Co mparación con c++ .................... 394
Formateo de la salida ....... . . ... 324 Genéricos simples ...... .... . . . .• .•.•.. .394
printfO· . . . . . ... .. . ..... 324 Una biblioteca de tuplas .396
Contenido xiii
Una clase que implementa una piJa ..... 398 Utilizac ión del patrón Decorador .. . .... 461
RandornList ... .. 399 Mixins con proxies dinámicos. . .462
lnterfaces genéricas .......... 399 Tipos latentes . .......................... 464
Métodos genéricos .. ... ........... 403 Compensación de la carencia de tipos latentes .. 467
Aprovechamiento de la inferencia del argumento Reflexión ............ . 467
de tipo ...... 404 Aplicación de un método a una secuencia .. . .. 468
Varargs y métodos genéricos. . .... 405 ¿Qué pasa cuando no disponemos de la intcrfaz
Un método genérico para utilizar con correcta? . . . . . . . . . . . . 471
generadores . ............. 406 Simulación de tipos latentes mediante adaptadores472
Un gene rador de propósito general . . .... 407 Utilizando los objetos de función como
Simplificación del uso de las tuplas ..... 408 estrategias .................. 474
Una utilidad Set .... ... ............. 409 Resumen: ¿realmente es tan malo el
Clases internas anónimas . . . . ......... 412 mecanismo de proyección? ..... . ..479
Construcción de modelos complejos ......... 4 13 Lecturas adicionales .. .......... . . 481
El misterio del borrado ............. 4 15 16 Matrices ...... .. .. .................. . . 483
La técnica usada en C++ . . .. ............ 417 Por qué las matrices son especiales . . 483
Compatibi lidad de la migración. .419 Las matrices son objetos de primera clase .... 484
El problema del borrado de tipos . .... . . . . . .4 19 Devolución de una matriz ... . .... 487
El efecto de los límites .... 42 1 Matrices multidimensionales .. 488
Compensación del borrado de tipos .... 424 Matrices y genéricos .......... .... •.... 491
Creación dc instanc ias de tipos. ..425 Creación de datos de prueba .......... 493
Matrices de genéricos ............... . .. 42 7 Array,.fillO . . . . . . . . . . . . . . . ... 493
L~i~s ... . ...................... ~l Generadores de datos ............. 494
Comodines ........... .. ..... ..... 434 Creación de matrices a partir de generadores . ... 498
¿Hasta qué punto es inteligente el compilador? .. 436 Utilidades para matrices . . .. 502
Contravarianza .... ............... 438 Copia en una matriz . ... 502
Comodines no limitados .. 440 Comparación de matrices . ....... . . . .. ... 503
Conversión de captura. . .... 444 Comparaciones de elementos de matriz ... 504
Problemas ..... . ... 445 Ordenación de una matriz .. 507
No pueden usarse primitivas corno parámetros Búsquedas en una matriz ordenada .. 508
de tipo. . . . .. 445
Resumen .................... . ....... 509
Implementación de interfaces parametrizadas . ... 447
Proyecciones de tipos y advertencias ..... 447
17 Análisis detallado de los contenedores . ... 513
Sobrecarga. . .. 449 Taxonomía completa de los contenedores ..... 513
Secuestro de una interfaz por parte de la clase Rclleno de contenedores. . . 514
base . 449 Una solución basada en generador .... ... ..... 515
Tipos autolimitados ........ . .. ......... 450 Generadores de mapas . . . 5 16
Gcnéricos curiosamente recurrentes .. ..... 450 Utilización de clases abstractas ... 519
Autolimitación .... 451 Funcionalidad de las colecciones .. .. . . ..... 525
Covarianza de argumentos. . .. 453 Operaciones opcionales ............. .. .... 528
Seguridad dinámica de los tipos ... . . ....... 456 Operaciones no soportadas . ...... .. ..... 529
Excepciones .. 457 Funcionalidad de List .... . ......... 530
Mixins ........... . .. 458 Conjuntos y orden de a~nacenam iento. . . . .. . 533
Mixins en C++. ............. 459 SortedSet. ......... . 536
Mezclado de clases utilizando interfaces.. . .... 460 Colas ... ............• . •.•.... . . . . •.... 537
xiv Piensa en Java
Colas con prioridad. . . . . .. . . . . · .... 538 Orígenes y destinos de los datos . . . 601
Colas dobles. ....... . .. .. ........ . 539 Modificación del comportamiento de los flujos
Mapas ...... .. ..... . ..... . . . ........... 540 dc datos. ........... . ... 601
Rendimiento . 541 Clases no modificadas .... 602
Mejora de la velocidad con el almacenamiento Entrada de memoria formateada .... .. .. . ..... 604
hash . . . . . . . . ........ . .. 550 Salida básica a archivo ..... . .... . .... . .... 605
Sustituc ión de hashCodeQ ......... . .... . 553 Almacenamiento y recuperación de datos ...... 607
Selección de una implementación .... 558 Lectura y escritura de archivos de acceso
Un marco de trabajo para pruebas de rendimiento 558 aleatorio .. .. 608
Selección entre listas ....................... 561 Flujos de datos canalizados. . ....... .. .... 609
La clase File ...... .. ...... . · ... 587 Archivos Java (lAR) . . . ................ 637
Una utilidad para listados de directorio. . ... 587 Serialización de objetos .. . . .... ... 639
Utilidades de directorio .......... . . ... 590 Localización de la clase ..... 642
Búsqueda y creación de directorios ..... 594 Control en la serialización .. .642
Adición de métodos a lma enumeración ...... 661 Mejora del diseño del código .. ..... ,.. . . 73 0
Sustitución de los métodos de una enumeración . 662 Conceptos básicos sobre hebras .. , , . o • o ••• •• 731
Enumeraciones en las instrucciones switch ... 662 Definición de las tarcas . . ....... . .. .... 731
El misterio de values( ) ....... . .. 663 La clase Thread .732
Implementa, no hereda ..... o • o o o o o ••••• o •• 665 Utilización de Executor .... . 734
Selección aleatoria ... . ......... . , . .. 666 Producción de valores de retomo de las tareas . . 736
Utilización de interfaces con propósitos de Cómo donnir una tarea . , ..... .... . . . ... . 737
organización .. ... . . o • •• ••••••••••• ••• 667 Prioridad. · . 738
Utilización de EnurnSet en lugar de Cesión del control . . .. 740
indicadores ........ o o o o o o • • • 671
Hebras demonio. . , .. 740
Uti lización de EnurnMap ........ 672
Variaciones de código .. 744
ylétodos específicos de constante ............ 673
Terminología . . .. , 748
Cadena de responsabilidad en las enumeraciones 676
Absorción de una hebra ,748
Máquinas de estado con enumeraciones. . . 680
Creación de interfaces de usuario de
Despacho múl tiple .. . ................... 684 rcspuesta rápida . . . .. ......... 750
Cómo despachar con enumeraciones ........... 686 Grupos dc hebras. · , 751
Utilización de méwdos específicos de constante. 688 Captura dc excepciones ... . ... .. . . · . 751
Cómo despachar con mapas En urnMap. ..690 Compartición de recursos .753
Utilización de una matriz 2-D .690 Acceso inapropiado a los recursos .... 754
Reswnen. .... 691 Resolución de la contienda por los recursos
compartidos.. ................... . . 756
20 Anotaciones o o o o o o o o o o o o o o o o o o o o o o o o o o 693
Atomicidad y volatilidad. . . .. . 760
Sintaxis básici:t ... 694 Clases atómicas . .. . . ... 764
Definición de anotaciones. . . .. 694
Secciones críticas , , 765
Meta-anotaciones ........ , ... 695
Sincron ización sobre otros objetos. . .. 770
Escritura de procesadores de anotaciones , .... 696 Almacenamiento local de las hebras ... 77 1
Elementos de anotación. . . 697 Terminación de tareas •• • 0 •••• 772
Restricciones de valor predeterminado. . . . 697 El jardín ornamental .. 772
Generación de archivos externos ... ...... 697 Tcnuinación durantc el bloqueo .. 775
Soluciones alternativas .. 700 Interrupción. . ......... , ... .. . 776
Las anotaciones no soportan la herencia .... . . 700 Comprobación de la existencia de una
Implementación del procesador . 700 interrupción . ... 782
Uti lización de apt para procesar anotaciones .. 703 Cooperación en tre tareas .784
Uti lización del patrón de diseño Visitante waitO y notifyAIIO ... , ... . o o , • .. , 784
con apt ..... , , , , , , , , .. ........ . ... . . 706 notifyO y notifyAIIO, .. ,788
Pruebas unitarias basadas en anotaciones . , . 709 Productores y consumidores. . .... 791
Utilización de @Unit con genéricos .. 716
Productores-consumidores y colas .796
No hace falta ningún "agrupamiento" .. 717
Utilización de canalizaciones para la E/S
Implementación de@Unit ,., ...... . . . . . .... 717 entre tareas . . . . . . . . . . . . . . . . . . . . . . . . . , 800
El iminación del código de prueba . . 723 Interbloqueo . . . . . . , ..... o •••• 801
Resumen. . . . . . . . . . .. , o •••• •• 724 Nuevos componentes de biblioteca ,805
CountDownLatch .. .. 805
21 Concurrencia o o o o o o o o o o o o o o o o o o o o o o o o o o 727
CyclicBarrier .... .. . 807
Las múltiples caras de la concurrencia .. , , . , , . 728
DelayQueue. .... 809
Ejecuc ión más rápida . . . ....... . 728
PriorityBlockingQueue . . • ,.,., . 0 ,., • ... . . 811
xvi Piensa en Java
A Suplementos ..................... . .... 955 B Recursos ...... .. .......... . .... .... .. 959
Suplementos descargables ......... . .... 955 Software ............. .... ... ... . ... 959
Thinking in C: fundamentos para Java .. . . . . 955 Editores y entornos !DE ..... . ... ... . ... 959
Seminario ThinJ..ing in Java . . . . . . . . . . . . . . 955 Libros ........... . ... 959
Seminario CD Hallds-On Java .... 956 Análisis y diseño . .. .. ... 960
Seminario Thinking in Objects .....•... .... 956 Python . ........ . .. .. 962
Thinking in Entelprise Java . ....... . . .. ... . 956 Mi propia lista de libros ........ . ... 962
Thinking in Patlerns (con Java) ............. 957 Indice ............ .. .. ... . . . .... . .... . 963
Seminario Thinking in Patterns ... 957
Consultoría y revisión de diseño .... 957
Prefacio
Originalmente, me enfrenté a Java como si fuera "simplemente otro lenguaje más de programación", lo cual es cierto en
muchos sentidos.
Pero, a medida que fue pasando el tiempo y lo fui estudiando con mayor detalle, comencé a ver que el objetivo fundamen-
tal de este lenguaje era distinto de los demás lenguajes que había visto hasta el momento .
La tarea de programación se basa en gestionar la complejidad: la complejidad del problema que se quiere resolver, sumada
a la complej idad de la máquina en la cual hay que resolverlo. Debido a esta complejidad, la mayoría de los proyectos de
programación terminan fallando. A pesar de lo cual, de todos los lenguajes de programación que conozco, casi ninguno de
ellos había adoptado como principal objetivo de diseño resolver la complej idad inherente al desarrollo y el mantenimiento
de los programas. 1 Por supuesto, muchas decisiones del diseño de lenguajes se realizan teniendo presente esa complejidad,
pero siempre termina por considerarse esencial introducir otros problemas dentro del conj unto de los objetivos.
Inevitablemente, son estos otros problemas los que hacen que los programadores terminen no pudiendo cumplir el objetivo
principalmente con esos lenguajes. Por ejemplo, C++ tenía que ser compatible en sentido descendente con C (para permitir
una fácil migración a los programadores de C), además de ser un lenguaje eficiente. Ambos objetivos resultan muy útiles y
explican parte del éxito de C++, pero también añaden un grado adicional de complejidad que impide a algunos proyectos
finalizar (por supuesto, podemos echar la culpa a los programadores y a los gestores, pero si un lenguaje puede servir de
ayuda detectando los errores que cometemos, ¿por qué no utilizar esa posibilidad?). Otro ejemplo, Visual BASIC (VB) esta-
ba ligado a BASIC, que no había sido diseñado para ser un lenguaje extensible, por lo que todas las extensiones añadidas a
VB han dado como resultado una sintaxis verdaderamente inmanejable. Ped es compatible en sentido descendente con awk,
sed, grep y otras herramientas Unix a las que pretendía sustituir, y como resultado, se le acusa a menudo de generar "códi-
go de sólo escritura" (es decir, después de pasado un tiempo se vuelve completamente ilegible). Por otro lado, C++, VB,
Ped y otros lenguajes como Smalltalk han centrado algo de esfuerzo de diseño en la cuestión de la complejidad, y como
resultado, ha tenido un gran éxito a la hora de resolver ciertos tipos de problemas.
Lo que más me ha impresionado cuando he llegado a entender el lenguaje Java es que dentro del conjunto de objetivos de
diseño establecido por Sun, parece que se hubiera decidido tratar de reducir la complejidad para el programador. Como si
quienes marcaron esos obj etivos hubieran dicho: "Tratemos de reducir el tiempo y la dificultad necesarios para generar códi-
go robusto" . Al principio, este objetivo daba como resultado un código que no se ejecutaba especialmente rápido (aunque
esto ha mejorado a lo largo del tiempo), pero desde luego ha permitido reducir considerablemente el tiempo de desarrollo,
que es inferior en un 50 por ciento o incluso más al tiempo necesario para crear un programa en c ++ equivalente.
Simplemente por esto, ya podemos ahorrar cantidades enormes de tiempo y de dinero, pero Java no se detiene ahí, sino que
trata de hacer transparentes muchas de las complejas tareas que han llegado a ser importantes en el mundo de la programa-
ción, como la utilización de múltiples hebras o la programación de red, empleando para conseguir esa transparencia una
serie de características del lenguaje y de bibliotecas preprogramadas que pueden hacer que muchas tareas lleguen a resultar
sencillas. Finalmente, Java aborda algunos problemas realmente complejos: programas interplataforma, cambios de código
dinámicos e incluso cuestiones de seguridad, todos los cuales representan problemas de una complejidad tal que pueden
hacer fracasar proyectos completos de programación. Por tanto, a pesar de los problemas de prestaciones, las oportunida-
des que Java nos proporciona son inmensas, ya que puede incrementar significativamente nuestra productividad como pro-
gramadores.
Java incrementa el ancho de banda de comunicación entre las personas en todos los sentidos: a la hora de crear los pro-
gramas, a la hora de trabajar en grupo, a la hora de construir interfaces para comunicarse con los usuarios, a la hora de
I Sin embargo, creo que el lenguaje Python es el que más se acerca a ese objetivo. Consulte www.Python. OIg.
xx Piensa en Java
ejecutar los programas en diferentes tipos de máquinas y a la hora de escribir con sencillez aplicaciones que se comuniquen
a través de Internet.
En mi opinión, los resultados de la revolución de las comunicaciones no se percibirán a partir de los efectos de desplazar
grandes cantidades de bits de un sitio a otro, sino que seremos conscientes de la verdadera revolución a medida que veamos
cómo podemos comunicamos con los demás de manera más sencilla, tanto en comunicaciones de persona a persona, como
en grupos repartidos por todo el mundo. Algunos sugieren que la siguiente revolución será la formación de una especie de
mente global derivada de la interconexión de un número suficiente de personas. No sé si Java llegará a ser la herramienta
que fomente dicha revolución, pero esa posibilidad me ha hecho sentir, al menos, que estoy haciendo algo importante al tra-
tar de enseñar este lenguaje.
Java SE6
La redacción de este libro ha sido, en sí misma, un proyecto de proporciones colosales y al que ha habido que dedicar muchí-
simo tiempo. Y antes de que el libro fuera publicado, la versión Java SE6 (cuyo nombre en clave es mustang) apareció en
versión beta. Aunque hay unos cuantos cambios de menor importancia en Java SE6 que mejoran algunos de los ejemplos
incluidos en el libro, el tratamiento de Java SE6 no ha afectado en gran medida al contenido del texto; las principales mejo-
ras de la nueva versión se centran en el anmento de la velocidad y en determinadas funcionalidades de biblioteca que caían
fuera del alcance del texto.
El código incluido en el libro ha sido comprobado con una de las primeras versiones comerciales de Java SE6, por lo que
no creo que vayan a producirse cambios que afecten al contenido del texto. Si hubiera algún cambio importante a la hora de
lanzar oficialmente JavaSE6, ese cambio se verá reflejado en el código fuente del libro, que puede descargarse desde
www.MindView.net.
En la portada del libro se indica que este texto es para "Java SE5/6", lo que significa "escrito para Java SE5 teniendo en
cuenta los significativos cambios que dicha versión ha introducido en el lenguaje, pero siendo el texto igualmente aplicable
a Java SE6".
La cuarta edición
La principal satisfacción a la hora de realizar una nueva edición de un libro es la de poder "corregir" el texto, aplicando todo
aquello que he podido aprender desde que la última edición viera la luz. A menudo, estas lecciones son derivadas de esa
frase que dice: "Aprender es aquello que conseguimos cuando no conseguimos lo que queremos", y escribir una nueva edi-
ción del libro constituye siempre una oportunidad de corregir errores o hacer más amena la lectura. Asimismo, a la hora de
abordar una nueva edición vienen a la mente nuevas ideas fascinantes y la posibilidad de cometer nuevos errores se ve más
que compensada por el placer de descubrir nuevas cosas y la capacidad de expresar las ideas de una forma más adecuada.
Prefacio xxi
Asimismo, siempre se tiene presente, en el fondo de la mente, ese desafio de escribir un libro que los poseedores de las edi-
ciones anteriores estén dispuestos a comprar. Ese desafio me anima siempre a mejorar, reescribir y reorganizar todo lo que
puedo, con el fin de que el libro constituya una experiencia nueva y valiosa para los lectores más fieles.
Cambios
El CD-ROM que se había incluido tradicionalmente como parte del libro no ha sido incluido en esta edición. La parte esen-
cial de dicho CD, el seminario multimedia Thinking in e (creado para MindView por Chuck Allison), está ahora disponible
como presentación Flash descargable. El objetivo de dicho seminario consiste en preparar a aquellos que no estén lo sufi-
cientemente familiarizados con la sintaxis de C, de manera que puedan comprender mejor el material presentado en este
libro. Aunque en dos de los capítulos del libro se cubre en buena medida la sintaxis a un nivel introductorio, puede que no
sean suficientes para aquellas personas que carezcan de los conocimientos previos adecuados, y la presentación Thinking in
e trata de ayudar a dichas personas a alcanzar el nivel necesario.
El capítulo dedicado a la concurrencia, que antes llevaba por título "Programación multihebra", ha sido completamente rees-
crito con el fin de adaptarlo a los cambios principales en las bibliotecas de concurrencia de Java SE5, pero sigue proporcio-
nando información básica sobre las ideas fundamentales en las que la concurrencia se apoya. Sin esas ideas fundamentales,
resulta dificil comprender otras cuestiones más complejas relacionadas con la programación multihebra. He invertido
muchos meses en esta tarea, inmerso en ese mundo denominado "concurrencia" y el resultado final es que el capítulo no
sólo proporciona los fundamentos del tema sino que también se aventura en otros territorios más novedosos.
Existe un nuevo capítulo dedicado a cada una de las principales características nuevas del lenguaje Java SE5, y el resto de
las nuevas características han sido reflejadas en las modificaciones realizadas sobre el material existente. Debido al estudio
continuado que realizo de los patrones de diseño, también se han introducido en todo el libro nuevos patrones.
El libro ha sufrido una considerable reorganización. Buena parte de los cambios se deben a razones pedagógicas, junto con
la perfección de que quizá mi concepto de "capítulo" necesitaba ser revisado. Adicionalmente, siempre he tendido a creer
que un tema tenía que tener "la suficiente envergadura" para justificar el dedicarle un capítulo. Pero luego he visto, espe-
cialmente a la hora de enseñar los patrones de diseño, que las personas que asistían a los seminarios obtenían mejores resul-
tados si se presentaba un único patrón y a continuación se hacía, inmediatamente, un ejercicio, incluso si eso significaba que
yo sólo hablara durante un breve período de tiempo (asimismo, descubrí que esta nueva estructura era más agradable para
el profesor). Por tanto, en esta versión del libro he tratado de descomponer los capítulos según los temas, sin preocuparme
de la longitud final de cada capítulo. Creo que el resultado representa una auténtica mejora.
También he llegado a comprender la enorme importancia que tiene el tema de las pruebas de código. Sin un marco de prue-
bas predefinido, con una serie de pruebas que se ejecuten cada vez que se construya el sistema, no hay forma de saber si el
código es fiable o no. Para conseguir este objetivo en el libro, he creado un marco de pruebas que permite mostrar y vali-
dar la salida de cada programa (dicho marco está escrito en Python, y puede descargarse en www.MindVíew.net. El tema de
las pruebas, en general, se trata en el suplemento disponible en http://www.MindVíew.net/Books/BetterJava. que presenta lo
que creo que son capacidades fundamentales que todos los programadores deberían tener como parte de sus conocimientos
básicos.
Además, he repasado cada uno de los ejemplos del libro preguntándome a mí mismo: "¿Por qué lo hice de esta manera?" .
En la mayoría de los casos, he realizado algunas modificaciones y mejoras, tanto para hacer los ejemplos más coherentes
entre sí, como para demostrar lo que considero que son las reglas prácticas de programación en Java, (al menos dentro de
los límites de un texto introductorio). Para muchos de los ejemplos existentes, se ha realizado un rediseño y una nueva
implementación con cambios significativos con respecto a las versiones anteriores. Aquellos ejemplos que me parecía que
ya no tenían sentido han sido eliminados y se han añadido, asimismo, nuevos ejemplos.
Los lectores de las ediciones anteriores han hecho numerosísimos comentarios muy pertinentes, lo que me llena de satisfac-
ción. Sin embargo, de vez en cuando también me llegan algunas quejas y, por alguna razón, tilla de las más frecuentes es
que "este libro es demasiado voluminoso". En mi opinión, si la única queja es que este libro tiene "demasiadas páginas",
creo que el resultado global es satisfactorio (se me viene a la mente el comentario de aquel emperador de Austria que se
quejaba de la obra de Mozart diciendo que tenía "demasiadas notas"; por supuesto, no trato en absoluto de compararme con
Mozart). Además, debo suponer que ese tipo de quejas proceden de personas que todavía no han llegado a familiarizarse
con la enorme variedad de características del propio lenguaje Java y que no han tenido ocasión de consultar el resto de libros
dedicados a este tema. De todos modos, una de las cosas que he tratado de hacer en esta edición es recortar aquellas partes
xxii Piensa en Java
que han llegado a ser obsoletas, o al menos, no esenciales. En general, se ha repasado todo el texto eliminando lo que ya
había dejado de ser necesario, incluyendo los cambios pertinentes y mejorando el contenido de la mejor manera posible. No
me importa demasiado eliminar algunas partes, porque el material original cOlTespondiente continúa estando en el sitio web
(www.MindVíew.net).graciasa laversióndescargabledelastresprimerasedicionesdel libro.Asimismo. el lector tiene a su
disposición material adicional en suplementos descargables de esta edición.
En cualquier caso, para aquellos lectores que sigan considerando excesivo el tamaño del libro les pido disculpas. Lo crean
o no, he hecho cuanto estaba en mi mano para que ese tamaño fuera el menor posible.
Agradecimientos
En primer lugar, gracias a todos los colegas que han trabajo conmigo a la hora de impartir seminarios, realizar labores de
consultoría y desarrollar proyectos pedagógicos: Dave Bartlett, Bill Venners, Chuck AIlison, Jeremy Meyer y Jamie King.
Agradezco la paciencia que mostráis mientras continúo tratando de desarrollar el mejor modelo para que una serie de per-
sonas independientes como nosotros puedan continuar trabajando juntos.
Recientemente, y gracias sin duda a Internet, he tenido la oportunidad de relacionarme con un número sorprendentemente
grande de personas que me ayudan en mi trabajo, usualmente trabajando desde sus propias oficinas. En el pasado, yo ten-
dría que haber adquirido o alquilado una gran oficina para que todas estas personas pudieran trabajar, pero gracias a Internet,
a los servicios de mensajeros y al teléfono, ahora puedo contar con su ayuda sin esos costes adicionales. Dentro de mis inten-
tos por aprender a "trabajar eficazmente con los demás", todos vosotros me habéis ayudado enormemente y espero poder
continuar aprendiendo a mejorar mi trabajo gracias a los esfuerzos de otros. La ayuda de Paula Steuer ha sido valiosísima a
la hora de tomar mis poco inteligentes prácticas empresariales y transformarlas en algo razonable (gracias por ayudarme
cuando no quiero encargarme de algo concreto, Paula). Jonathan Wilcox, Esq. , se encargó de revisar la estructura de mi
empresa y de eliminar cualquier piedra que pudiera tener un posible escorpión, haciéndonos marchar disciplinadamente a
través del proceso de poner todo en orden desde el punto de vista legal, gracias por tu minuciosidad y tu persistencia.
Sharlynn Cobaugh ha llegado a convertirse en una auténtica experta en edición de sonido y ha sido una de las personas esen-
ciales a la hora de crear los cursos de formación multimedia, además de ayudar en la resolución de muchos otros proble-
mas. Gracias por la perseverancia que has demostrado a la hora de enfrentarte con problemas informáticos complejos. La
gente de Amaio en Praga también ha sido de gran ayuda en numerosos proyectos. Daniel Will-Harris fue mi primera fuen-
Prefacio xxiii
te de inspiración en lo que respecta al proyecto de trabajo a través de Internet y también ha sido imprescindible, por supues-
to, en todas las soluciones de diseño gráfico que he desarrollado.
A lo largo de los años, a través de sus conferencias y seminarios, Gerald Weinberg se ha convertido en mi entrenador y men-
tor extraoficial, por lo que le estoy enormemente agradecido.
Ervin Varga ha proporcionado numerosas correcciones técnicas para la cuarta edición, aunque también hay otras personas
que han ayudado en esta tarea, con diversos capítulos y ej emplos. Ervin ha sido el revisor técnico principal del libro y tam-
bién se encargó de escribir la guía de soluciones para la cuarta edición. Los errores detectados por Ervin y las mejoras que
él ha introducido en el libro han permitido redondear el texto. Su minuciosidad y su atención al detalle resultan sorprenden-
tes y es, con mucho, el mejor revisor técnico que he tenido. Muchas gracias, Ervin.
Mi weblog en la página www.Artima.com de Bill Venners también ha resultado de gran ayuda a la hora de verificar deter-
minadas ideas. Gracias a los lectores que me han ayudado a aclarar los conceptos enviando sus comentarios; entre esos lec-
tores debo citar a James Watson, Howard Lovatt, Michael Barker, y a muchos otros que no menciono por falta de espacio,
en particular a aquellos que me han ayudado en el tema de los genéricos.
Gracias a Mark Welsh por su ayuda continuada.
Evan Cofsky continúa siendo de una gran ayuda, al conocer de memoria todos los arcanos detalles relativos a la configura-
ción y mantenimiento del servidor web basados en Linux, así como a la hora de mantener optimizado y protegido el servi-
dor MindView.
Gracias especiales a mi nuevo amigo el café, que ha permitido aumentar enormemente el entusiasmo por el proyecto. Camp4
Coffee en Crested Butte, Colorado, ha llegado a ser el lugar de reunión normal cada vez que alguien venía a los seminarios
de MindView y proporciona el mejor catering que he visto para los descansos en el seminario. Gracias a mi colega Al Smith
por crear ese café y por convertirlo en un lugar tan extraordinario, que ayuda a hacer de Crested Butte un lugar mucho más
interesante y placentero. Gracias también a todos los camareros de Camp4, que tan servicialmente atienden a sus clientes.
Gracias a la gente de Prentice Hall por seguir atendiendo a todas mis peticiones, y por facilitarme las cosas en todo momen-
to .
Hay varias herramientas que han resultado de extraordinaria utilidad durante el proceso de desarrollo y me siento en deuda
con sus creadores cada vez que las uso. Cygwin (www.cygwin.com) me ha permitido resolver innumerables problemas que
Windows no puede resolver y cada día que pasa más me engancho a esta herrami enta (me hubiera encantado disponer de
ella hace 15 años, cuando tenía mi mente orientada a Gnu Emacs). Eclipse de IBM (www.eclipse.org) representa una mara-
villosa contribución a la comunidad de desarrolladores y cabe esperar que se puedan obtener grandes cosas con esta herra-
mienta a medida que vaya evolucionando. JetBrains IntelliJ Idea continúa abriendo nuevos y creativos caminos dentro del
campo de las herramientas de desarrollo.
Comencé a utilizar Enterprise Architect de Sparxsystems con este libro y se ha convertido rápidamente en mi herramienta
UML favorita. El formateador de código Jalopy de Marco Hunsicker (www.triemax.com) también ha resultado muy útil en
numerosas ocasiones y Marco me ha ayudado extraordinariamente a la hora de configurarlo para mis necesidades concre-
tas. En mi opinión, la herramienta JEdit de Slava Pestov y sus correspondientes plug-ins también resultan útiles en diversos
momentos (wwwjedit.org); esta herramienta es un editor muy adecuado para todos aquellos que se estén iniciando en el
desarrollo de seminarios.
y por supuesto, por si acaso no lo he dejado claro aún, utilizo constantemente Python (www.Python.org) para resolver pro-
blemas, esta herramienta es la criatura de mi colega Guido Van Rossum y de la panda de enloquecidos genios con los que
disfruté enormemente haciendo deporte durante unos cuantos días (a Tim Peters me gustaría decirle que he enmarcado ese
ratón que tomó prestado, al que le he dado el nombre oficial de "TimBotMouse"). Permitidme tan sólo recomendaros que
busquéis otros lugares más sanos para comer. Asimismo, mi agradecimiento a toda la comunidad Python, formada por un
conjunto de gente extraordinaria.
Son muchas las personas que me han hecho llegar sus correcciones y estoy en deuda con todas ellas, pero quiero dar las gra-
cias en particular a (por la primera edición): Kevin Raulerson (encontró numerosísimos errores imperdonables), Bob
Resendes (simplemente increíble), John Pinto, Joe Dante, loe Sharp (fabulosos vuestros comentarios), David Combs (mune-
rosas correcciones de clarificación y de gramática), Dr. Robert Stephenson, lohn Cook, Franklin Chen, Zev Griner, David
Karr, Leander A. Strosehein, Steve Clark, Charles A. Lee, Austin Maher, Dennis P. Roth, Roque Oliveira, Douglas Dunn,
Dejan Ristic, N eil Galarneau, David B. Malkovsky, Steve Wilkinson, y muchos otros. El Profesor Mare Meurrens dedicó
xxiv Piensa en Java
una gran cantidad de esfuerzo a pub licitar y difundir la versión electrónica de la primera edición de este libro en Europa.
Gracias a todos aquellos que me han ayudado a reescribir los ejemplos para utilizar la biblioteca Swing (para la segunda
edición), así como a los que han proporcionado otros tipos de comentarios: Jan Shvarts, Thomas Kirsch, Rahim Adatia,
Rajesh Jain, Ravi Manthena, Banu Rajamani, Jens Brandt, Nitin Shivaram, Malcolm Davis y a todos los demás que me han
manifestado su apoyo.
En la cuarta edición, Chris Grindstaff resultó de gran ayuda durante el desarrollo de la sección SWT y Sean Neville escri-
bió para mí el primer borrador de la sección dedicada a Flex.
Kraig Brockschmidt y Gen Kiyooka son algunas de esas personas inteligentes que he podido conocer en alglm momento de
vida y que han llegado a ser auténticos amigos, habiendo tenido una enorme influencia sobre mí. Son personas poco usua-
les en el sentido de que practican yoga y otras formas de engrandecimiento espiritual, que me resultan particularmente ins-
piradoras e instructivas.
Me resulta sorprendente que el saber de Delphi me ayudara a comprender Java, ya que ambos lenguajes tienen en común
muchos conceptos y decisiones relativas al diseño del lenguaje. Mis amigos de Delphi me ayudaron enormemente a la hora
de entender mejor ese maravilloso entorno de programación. Se trata de Marco Cantu (otro italiano, quizá el ser educado en
latín mejora las aptitudes de uno para los lenguajes de programación), Neil Rubenking (que solía dedicarse al yoga, la comi-
da vegetariana y el Zen hasta que descubrió las computadoras) y por supuesto Zack Urlocker (el jefe de producto original
de Delphi), un antiguo amigo con el que he recorrido el mundo. Todos nosotros estamos en deuda con el magnífico Anders
Hejlsberg, que continúa asombrándonos con C# (lenguaje que, como veremos en el libro, fue una de las principales inspi-
raciones para Java SES).
Los consejos y el apoyo de mi amigo Richard Hale Shaw (al igual que los de Kim) me han sido de gran ayuda. Richard y
yo hemos pasado muchos meses juntos impartiendo seminarios y tratando de mejorar los aspectos pedagógicos con el fin
de que los asistentes disfrutaran de una experiencia perfecta.
El diseño del libro, el diseño de la cubierta y la fotografia de la cubierta han sido realizados por mi amigo Daniel Will-Harris,
renombrado autor y diseñador (www.Will-Harris.com). que ya solía crear sus propios diseños en el colegio, mientras espe-
raba a que se inventaran las computadoras y las herramientas de autoedición, y que ya entonces se quejaba de mi forma de
resolver los problemas de álgebra. Sin embargo, yo me he encargado de preparar para imprenta las páginas, por lo que los
errores de fotocomposición son míos. He utilizado Microsoft® Word XP para Windows a la hora de escribir el libro y de
preparar las páginas para imprenta mediante Adobe Acrobat; este libro fue impreso directamente a partir de los archivos
Acrobat PDF. Como tributo a la era electrónica yo me encontraba fuera del país en el momento de producir la primera y la
segunda ediciones finales del libro; la primera edición fue enviada desde Ciudad del Cabo (Sudáfrica), mientras que la
segunda edición fue enviada desde Praga. La tercera y cuarta ediciones fueron realizadas desde Crested Butte, Colorado. En
la versión en inglés del libro se utilizó el tipo de letra Ceorgia para el texto y los títulos están en Verdana. La letra de la
cubierta original es ¡Te Rennie Mackintosh.
Gracias en especial a todos mis profesores y estudiantes (que también son mis profesores).
Mi gato Molly solía sentarse en mi regazo mientras trabajaba en esta edición, ofreciéndome así mi propio tipo de apoyo
peludo y cálido.
Entre los amigos que también me han dado su apoyo, y a los que debo citar (aunque hay muchos otros a los que no cito por
falta de espacio), me gustaría destacar a: Patty Gast (extraordinaria masaj ista), Andrew Binstock, Steve Sinofsky, JD
Hildebrandt, Tom Keffer, Brian McElhinney, BrinkJey Barr, Bill Gates en Midnight Engineering Magazine, Larry
Constantine y Lucy Lockwood, Gene Wang, Dave Mayer, David Intersimone, Chris y Laura Sttand, Jos Almquists, Brad
Jerbic, Marilyn Cvitanic, Mark Mabry, la familia Robbins, la familia Moelter (y los McMillans), Michael Wilk, Dave Stoner,
los Cranstons, Larry Fogg, Mike Sequeira, Gary Entsminger, Kevin y Sonda Donovan, Joe Lordi, Dave y Brenda Bartlett,
Patti Gast, Blake, Annette & Jade, los Rentschlers, los Sudeks, Dick, Patty, y Lee Eckel, Lynn y Todd, y sus familias. Y por
supuesto, a mamá y papá.
Introd ucción
"El dio al hombre la capacidad de hablar, y de esa capacidad surgió el pensamiento. Que es la
medida del Universo" Prometeo desencadenado, Shelley
Los seres humanos ... eslamos, en buena medida, a merced del lenguaje concreto que nuestra
sociedad haya elegido como medio de expresión. Resulta completamente ilusorio creer que nos
ajustamos a la realidad esencialmente sin utilizar el lenguaje y que el lenguaje es meramente
un medio incidental de resolver problemas específicos de comunicación y reflexión. Lo cierto
es que e/ "mundo real" está en gran parle construido, de manera inconsciente, sobre los hábi-
tos lingüísticos del grupo.
El estado de la Lingüística como ciencia, 1929, Edward Sapir
Como cualquier lenguaje humano, Java proporciona una forma de expresar conceptos. Si tiene éxito, esta forma de expre-
sión será significativamente más fácil y flexible que las alternativas a medida que los problemas crecen en tamaño y en com-
plejidad.
No podemos ver Java sólo como una colección de características, ya que algunas de ellas no tienen sentido aisladas. Sólo
se puede emplear la suma de las partes si se está pensando en el diseño y no simplemente en la codificación. Y para enten-
der Java así, hay que comprender los problemas del lenguaje y de la programación en general. Este libro se ocupa de los
problemas de la programación, porque son problemas, y del método que emplea Java para resolverlos. En consecuencia, el
conjunto de características que el autor explica en cada capítulo se basa en la forma en que él ve cómo puede resolverse un
tipo de problema en particular con este lenguaje. De este modo, el autor pretende conducir, poco a poco, al lector hasta el
punto en que Java se convierta en su lengua materna.
La actitud del autor a lo largo del libro es la de conseguir que el lector construya un modelo mental que le permita desarro-
llar un conocimiento profundo del lenguaje; si se enfrenta a un puzzle, podrá fijarse en el modelo para tratar de deducir la
respuesta.
Prerrequisitos
Este libro supone que el lector está familiarizado con la programación: sabe que un programa es una colección de instruc-
ciones, qué es una subrutina, una función o una macro, conoce las instrucciones de control como "if' y las estructuras de
bucle como "while", etc. Sin embargo, es posible que el lector haya aprendido estos conceptos en muchos sitios, tales como
la programación con un lenguaje de macros o trabajando con una herramienta como Perl. Cuando programe sintiéndose
cómodo con las ideas básicas de la programación, podrá abordar este libro. Por supuesto, el libro será más fác il para los pro-
gramadores de C y más todavía para los de C++, pero tampoco debe autoexcluirse si no tiene experiencia con estos lengua-
jes (aunque tendrá que trabajar duro). Puede descargarse en www.MindView.net el seminario muJtimedia Thinking in e, el
cual le ayudará a aprender más rápidamente los fundamentos necesarios para estudiar Java. No obstante, en el libro se abor-
dan los conceptos de programación orientada a objetos (POO) y los mecanismos de control básicos de Java.
Aunque a menudo se hacen referencias a las características de los lenguajes C y C++ no es necesario profundizar en ellos,
aunque sí ayudarán a todos los programadores a poner a Java en perspectiva con respecto a dichos lenguajes, de los que al
fin y al cabo desciende. Se ha intentado que estas referencias sean simples y sirvan para explicar cualquier cosa con la que
una persona que nunca haya programado en C/C++ no esté familiarizado.
xxvi Piensa en Java
Aprendiendo Java
Casi al mismo tiempo que se publicó mi primer libro, Using C+ + (Osbome/McGraw-I-lill, 1989), comencé a enseñar dicho
lenguaje. Enseñar lenguajes de programación se convirtió en mi profesión; desde 1987 he visto en auditorios de todo el
mundo ver dudar a los asistentes, he visto asimismo caras sorprendidas y expresiones de incredulidad. Cuando empecé a
impartir cursos de formación a grupos pequeños, descubrí algo mientras se hacían ejercicios. Incluso aquellos que sonreían
se quedaban con dudas sobre muchos aspectos. Comprendí al dirigir durante una serie de años la sesión de C++ en la
Software Development Conference (y más tarde la sesión sobre Java), que tanto yo como otros oradores tocábamos dema-
siados temas muy rápidamente. Por ello, tanto debido a la variedad en el nivel de la audiencia como a la forma de presen-
tar el material, se termina perdiendo audiencia. Quizá es pedir demasiado pero dado que soy uno de esos que se resisten a
las conferencias tradicionales (yen la mayoría de los casos, creo que esa resistencia proviene del aburrimiento), quería inten-
tar algo que permitiera tener a todo el mundo enganchado.
Durante algún tiempo, creé varias presentaciones diferentes en poco tiempo, por lo que terminé aprendiendo según el méto-
do de la experimentación e iteración (una técnica que también funciona en el diseño de programas). Desarrollé un curso uti-
lizando todo lo que había aprendido de mi experiencia en la enseñanza. Mi empresa, MindView, Inc. , ahora imparte el
seminario Thinking in Java (piensa en Java); que es nuestro principal seminario de introducción que proporciona los funda-
mentos para nuestros restantes seminarios más avanzados. Puede encontrar información detallada en www.MindView.net. El
seminario de introducción también está disponible en el CD-ROM Hands-On Java. La información se encuentra disponible
en el mismo sitio web.
La respuesta que voy obteniendo en cada seminario me ayuda a cambiar y reenfocar el material hasta que creo que funcio-
na bien como método de enseñanza. Pero este libro no son sólo las notas del seminario; he intentado recopilar el máximo
de información posible en estas páginas y estructurarla de manera que cada tema lleve al siguiente. Más que cualquier otra
cosa, el libro está diseñado para servir al lector solitario que se está enfrentando a un nuevo lenguaje de programación.
Objetivos
Como mi anterior libro, Thinking in C+ +, este libro se ha diseñado con una idea en mente: la forma en que las personas
aprenden un lenguaje. Cuando pienso en un capítulo del libro, pienso en términos de qué hizo que fuera una lección duran-
te un seminario. La infonnación que me proporcionan las personas que asisten a un seminario me ayuda a comprender cuá-
les son las partes complicadas que precisan una mayor explicación. En las áreas en las que fui ambicioso e incluí demasiadas
características a un mismo tiempo, pude comprobar que si incluía muchas características nuevas, tenía que explicarlas yeso
contribuía fácilmente a la confusión del estudiante.
En cada capítulo he intentado enseñar una sola característica o un pequeño grupo de características asociadas, sin que sean
necesarios conceptos que todavía no se hayan presentado. De esta manera, el lector puede asimilar cada pieza en el contex-
to de sus actuales conocimientos.
Mis objetivos en este libro son los siguientes:
1. Presentar el material paso a paso de modo que cada idea pueda entenderse fácilmente antes de pasar a la siguien-
te. Secuenciar cuidadosamente la presentación de las características, de modo que se haya explicado antes de que
se vea en un ejemplo. Por supuesto, esto no siempre es posible, por lo que en dichas situaciones, se proporciona
una breve descripción introductoria.
2. Utilizar ejemplos que sean tan simples y cortos como sea posible. Esto evita en ocasiones acometer problemas
del "mundo real", pero he descubierto que los principiantes suelen estar más contentos cuando pueden compren-
der todos los detalles de un ejemplo que cuando se ven impresionados por el ámbito del problema que resuelve.
También, existe una seria limitación en cuanto a la cantidad de código que se puede absorber en el aula. Por esta
razón, no dudaré en recibir críticas acerca del uso de "ejemplos de juguete", sino que estoy deseando recibirlas
en aras de lograr algo pedagógicamente útil.
3. Dar lo que yo creo que es importante para que se comprenda el lenguaje, en lugar de contar todo lo que yo sé_
Pienso que hay una jerarquía de importancia de la información y que existen hechos que el 95% de los progra-
madores nunca conocerán, detalles que sólo sirven para confundir a las personas y que incrementan su percep-
ción de la complejidad del lenguaje. Tomemos un ejemplo de C, si se memoriza la tabla de precedencia de los
Introducción xxvii
operadores (yo nunca lo he hecho), se puede escribir código inteligente. Pero si se piensa en ello, también con-
fundirá la lectura y el mantenimiento de dicho código, por tanto, hay que olvidarse de la precedencia y emplear
paréntesis cuando las cosas no estén claras.
4. Mantener cada sección enfocada de manera que el tiempo de lectura y el tiempo entre ejercicios, sea pequeño.
Esto no sólo mantiene las mentes de los alumnos más activas cuando se está en un seminario, sino que también
proporciona al lector una mayor sensación de estar avanzando.
5. Proporcionar al alumno una base sólida de modo que pueda comprender los temas los suficientemente bien como
para que desee acudir a cursos o libros más avanzados.
Ejercicios
He descubierto que durante las clases los ejercicios sencillos son excepcionalmente útiles para que el alumno termine de
comprender el tema, por lo que he incluido al final de cada capítulo una serie de ejercicios.
La mayor parte de los ej ercicios son bastante sencillos y están diseñados para que se puedan realizar durante un tiempo razo-
nable de la clase, mientras el profesor observa los progresos, asegurándose de que los estudiantes aprenden el tema. Algunos
son algo más complejos, pero ninguno presenta un reto inalcanzable.
Las soluciones a los ejercicios seleccionados se pueden encontrar en el documento electrónico The Thinking in Java
Annotated So/ution Guide, que se puede adquirir en www.MindVíew.net.
y la sintaxis de e en la que se basa la sintaxis de Java. En las ediciones anteriores del libro se encontraba en el eD
Foundations for Java que se proporcionaba junto con el libro, pero ahora este seminario puede descargarse gratuitamente.
Originalmente, encargué a ehuck Allison que creara Thinking in C como un producto autónomo, pero decidí incluirlo en la
segunda edición de Thinking in C++ y en la segunda y tercera ediciones de Thinking in Java , por la experiencia de haber
estado con personas que llegan a los seminarios sin tener una adecuada formación en la sintaxis básica de e. El razonamien-
to suele ser: "Soy un programador inteligente y no quiero aprender e, sino e ++ o Java, por tanto, me salto el e y paso direc-
tamente a ver el e++/Java". Después de asistir al seminario, lentamente todo el mundo se da cuenta de que el prerrequisito
de conocer la sintaxis de e tiene sus buenas razones de ser.
Las tecnologías han cambiado y han pennitido rehacer Thinking in C como una presentación Flash descargable en lugar de
tener que proporcionarlo en CD. Al proporcionar este seminario en linea, puedo garantizar que todo el mundo pueda comen-
zar con una adecuada preparación.
El seminario Thinking in C también permite atraer hacia el libro a una audiencia importante. Incluso aunque los capítulos
dedicados a operadores y al control de la ejecución cubren las partes fundamentales de Java que proceden de C, el semina-
rio en línea es una buena introducción y precisa del estudiante menos conocimientos previos sobre programación que este
libro.
Código fuente
Todo el código fuente de este libro está disponible gratuitamente y sometido a copyright, distribuido como un paquete único,
visitando el sitio web www.MindView.net. Para asegurarse de que obtiene la versión más actual, éste es el sitio oficial de dis-
tribución del código. Puede distribuir el código en las clases y en cualquier otra situación educativa.
El objetivo principal del copy right es asegurar que el código fuente se cite apropiadamente y evitar así que otros lo publi-
quen sin permiso. No obstante, mientras se cite la fuente, no constituye ningún problema en la mayoría de los medios que
se empleen los ejemplos del libro.
En cada archivo de código fuente se encontrará una referencia a la siguiente nota de copyright:
// ,! Copyright.txt
Thi s eomputer source eode is Copyright ~ 2006 MindView, lnc.
All Rights Reserved .
Estándares de codificación
En el texto del libro, los identificadores (nombres de métodos, variables y clases) se escriben en negrita. La mayoría de las
palabras clave se escriben en negrita, excepto aquellas palabras clave que se usan con mucha frecuencia y ponerlas en negri-
ta podría volverse tedioso, como en el caso de la palabra "e1ass".
En este libro, he utilizado un estilo de codificación particular para los ejemplos. Este estilo sigue el que emplea Sun en prác-
ticamente todo el código que encontrará en su sitio (véase http://java.sun. com/docs/codeconv/index. htmf), y que parece que
soporta la mayoría de los entornos de desarrollo Java. Si ha leído mis otros libros, observará también que el estilo de codi-
ficación de Sun coincide con el mío, lo que me complace, ya que yo no tengo nada que ver con la creación del estilo de
xxx Piensa en Java
Sun. El tema del estilo de fonnato es bueno para conseguir horas de intenso debate, por lo que no vaya intentar dictar un
estilo correcto a través de mis ejemp los; tengo mis propias motivaciones para usar el estilo que uso. Dado que Java es un
lenguaje de programación de fonoato libre, se puede emplear el estilo con el que uno se encuentre a gusto. Una solución
para el tema del esti lo de codificación consiste en utilizar una herramienta como Jalopy (www.triemax.com). la cual me ha
ayudado en el desarrollo de este libro a cambiar el fonoato al que se adaptaba a mí.
Los archi vos de código impresos en el libro se han probado con un sistema automatizado, por lo que deberían ejecutarse sin
crrores de compi lación.
Este libro está basado y se ha comprobado con Java SE5/6. Si necesita obtener infonnación sobre versiones anteriores del
lenguaje que no se cubren en esta ed ición, la ediciones primera y tercera del mismo pueden descargarse gratuitamente en
www.MindView.net.
Errores
No importa cuantas herramientas uti li ce un escritor para detectar los errores, algunos quedan ahí y a menudo son lo que pri-
mero ve el lector. Si descubre cua lquier cosa que piensa que es un error, por favor utili ce el víncu lo que encontrará para este
libro en www.MindView.net y envÍeme el error junto con la corrección que usted crea. Cualqui er ayuda siempre es bienve-
nida.
Introducción
a los objetos
La génesis de la revolución de las computadoras se halla ba en una máquina. La génesis de nuestros lenguajes de programa-
ción tiende entonces a parecerse a dicha máquina.
Pero las computadoras, más que máquinas, pueden considerarse como herramientas que permiten ampliar la mente ("bici-
cletas para la mente", como se enorgullece en decir Steve Jobs), además de un medio de expresión diferente. Como resul-
tado, las herramientas empiezan a parecerse menos a máquinas y más a partes de nuestras mentes, al igual que ocurre con
otras formas de expresión como la escritura, la pintura, la escultura, la an imación y la realización de películas. La progra-
mac ión orientada a objetos (POO) es parte de este movimiento dirigido al uso de las computadoras como un medio de expre-
sión.
Este capítulo presenta los conceptos básicos de la programación orientada a objetos, incluyendo una introducción a los méto-
dos de desarrollo. Este capítulo, y este libro, supone que el lector tiene cierta experiencia en programación, aunque no nece-
sariamente en C. Si cree que necesita una mayor preparación antes de abordar este libro, debería trabajar con el seminario
multimedia sobre C, Thinking in C. que puede descargarse en www.MindView.net.
Este capítulo contiene material de carácter general y suplementario. Muchas personas pueden no sentirse cómodas si se
enfrentan a la programación orientada a objetos sin obtener primero una visión general. Por tanto, aquí se presentan muchos
conceptos que proporcionarán una sólida introducción a la PDO. Sin embargo, otras personas pueden no necesitar tener una
visión general hasta haber visto algunos de los mecanismos primero, estas personas suelen perderse si no se les ofrece algo
de código que puedan manipular. Si usted forma parte de este último glUpO, estará ansioso por ver las especifidades del len-
guaje, por lo que puede saltarse este capítulo, esto no le impedirá aprender a escribir programas ni conocer el lenguaje. Sin
embargo, podrá vo lver aquí cuando lo necesite para completar sus conocimientos, con el fin de comprender por qué son
importantes los obj etos y cómo puede diseñarse con ellos.
El progreso de la abstracción
Todos los lenguaj es de programación proporcionan abstracciones. Puede argumentarse que la complejidad de los problemas
que sea capaz de resolver está directamente relacionada con el tipo (clase) y la calidad de las abstracciones, entendiendo por
"clase", "¿qué es lo que se va a abstraer?", El lenguaj e ensamblador es una pequeña abstracción de la máquina subyacente.
Muchos de los lenguajes denominados "imperativos" que le siguieron (como FORTRAN, BASIC y C) fueron abstraccio-
nes del lenguaje ensamblador. Estos lenguajes constituyen grandes mejoras sobre el lenguaje ensamblador, pero su princi-
pal abstracción requiere que se piense en ténninos de la estructura de la computadora en lugar de en la es tructura del
problema que se está intentado resolver. El programado r debe establecer la asociación entre el modelo de la máquina (en el
"espac io de la solución", que es donde se va a implementar dicha solución, como puede ser una computadora) y el modelo
2 Piensa en Java
del problema que es lo que realmente se quiere resolver (en el "espacio del problema", que es el lugar donde existe el pro-
blema, como por ejemplo en un negocio). El esfuerzo que se requiere para establecer esta correspondencia y el hecho de
que sea extrínseco al lenguaje de programación, da lugar a programas que son dificil es de escribir y caros de mantener, ade-
más del efecto colateral de toda una industria de "métodos de programación",
La alternativa a modelar la maquina es modelar el problema que se está intentado solucionar. Los primeros lenguajes como
LISP y APL eligen vistas parciales del mundo ("todos los problemas pueden reducirse a listas" o "todos los problemas son
algorítmicos", respectivamente). Prolog convierte todos los problemas en cadenas de decisión. Los lenguajes se han creado
para programar basándose en restricciones y para programar de forma exclusiva manipulando símbolos gráficos (aunq ue se
demostró que este caso era demasiado restrictivo). Cada uno de estos métodos puede ser una buena solución para resolver
la clase de problema concreto para el que están diseñados, pero cuando se aplican en otro dominio resultan inadecuados.
El enfoque orientado a objetos trata de ir un paso más allá proporcionando herramientas al programador para representar los
elementos en el espacio del problema. Esta representación es tan general que el programador no está restringido a ningún
tipo de problema en particular. Se hace referencia a los elementos en el espacio del problema denominando "objetos" a sus
representaciones en el espacio de la solución (también se necesitarán otros objetos que no tendrán análogos en el espacio
del problema). La idea es que el programa pueda adaptarse por sí sólo a la jerga del problema añadiendo nuevos tipos de
objetos, de modo que cuando se lea el código que describe la solución, se estén leyendo palabras que también expresen el
problema. Ésta es una abstracción del lenguaje más flexible y potente que cualquiera de las que se hayan hecho anterior-
mente l . Por tanto, la programación orientada a objetos permite describir el problema en ténninos del problema en lugar de
en términos de la computadora en la que se ejecutará la solución. Pero aún existe una conexión con la computadora, ya que
cada objeto es similar a una pequeña computadora (tiene un estado y dispone de operaciones que el programador puede
pedirle que realice). Sin embargo, esto no quiere decir que nos encontremos ante una mala analogía de los objetos del mundo
real, que tienen características y comportamientos.
Alan Kay resumió las cinco características básicas del Smalltalk, el primer lenguaje orientado a objetos que tuvo éxito y uno
de los lenguajes en los que se basa Java. Estas características representan un enfoque puro de la programación orientada a
objetos.
1. Todo es un objeto. Piense en un objeto como en una variable: almacena datos, permite que se le "planteen soli-
citudes", picliéndole que realice operaciones sobre sí mismo. En teoría, puede tomarse cualquier componente con-
ceptual del problema que se está intentado resolver (perros, edificios, servicios, etc.) y representarse como un
objeto del programa.
2. Un programa es un montón de objetos que se dicen entre sí lo que tienen que hacer enviándose mensajes.
Para hacer una solicitud a un objeto, hay que enviar un mensaje a dicho objeto. Más concretamente, puede pen-
sar en que un mensaje es una solicitud para llamar a un método que pertenece a un determinado objeto.
3. Cada objeto tiene su propia memoria formada por otros objetos. Dicho de otra manera, puede crear una nueva
clase de objeto definiendo un paquete que contenga objetos existentes. Por tanto, se puede incrementar la comple-
jidad de un programa acuitándola tras la simplicidad de los objetos.
4. Todo objeto tiene un tipo asociado. Como se dice popularmente, cada objeto es una instancia de una clase, sien-
do "clase" sinónimo de "tipo". La característica distintiva más importante de una clase es "el conjunto de men-
sajes que se le pueden enviar".
5. Todos los objetos de un tipo particular pueden recibir los mismos mensajes. Como veremos más adelante,
esta afirmación es realmente importante. Puesto que un objeto de tipo "círculo" también es un objeto de tipo
"forma", puede garantizarse que un círculo aceptará los mensajes de forma. Esto quiere decir que se puede escri-
bir código para comunicarse con objetos de tipo forma y controlar automáticamente cualquier cosa que se ajuste
a la descripción de una forma. Esta capacidad de suplantación es uno de los conceptos más importantes de la pro-
gramación orientada a objetos.
Booch ofrece una descripción aún más sucinta de objeto:
¡Algunos diseñadores de lenguajes han decidido que la programación orientada a objetos por sí misma no es adecuada para resolver fácilmente todos
los problemas de la programación, y recomiendan combinar varios métodos en lenguajes de programación mll/liparadigma. Consulte MII/I/paradim
Programming in Leda de Timothy Budd (Addison-Wesley, 1995).
1 Introducción a los objetos 3
2 Realmente, esto es poco restrictivo, ya que pueden existir objetos en diferentes máquinas y espacios de direcciones, y también sc pueden almacenar en
disco. En estos casos, debe determinarse la identidad del objeto mediante alguna otra cosa que la dirección de memoria.
3 Algunas personas hacen una distinción, estableciendo que el tipo determina la interfaz micntras que la clase es una implemcntación concreta de dicha
interfaz.
4 Piensa en Java
Tipo Luz
encenderO
Interfaz apagarO
brillarO
atenuarO
La interfaz determina las solicitudes que se pueden hacer a un determinado objeto, por lo que debe existir un código en algu-
na parte que satisfaga dicha solicitud. Esto, junto con los datos ocultos, definen lo que denomina la implementación. Desde
el punto de vista de la programación procedimental, esto no es complicado. Un tipo tiene un método asociado con cada posi-
ble solicitud; cuando se hace una determinada solicitud a un objeto, se llama a dicho método. Este proceso se resume dicien-
do que el programador "envía un mensaje" (hace una solicitud) a un objeto y el objeto sabe lo que tiene que hacer con ese
mensaje (ejecuta el código).
En este ejemplo, el nombre del tipo/clase es Luz, el nombre de este objeto concreto Luz es lz y las solicitudes que se pue-
den hacer a un objeto Luz son encender, apagar, brillar o atenuar. Se ha creado un objeto Luz definiendo una "referencia"
(lz) para dicho objeto e invocando new para hacer una solicitud a un nuevo objeto de dicho tipo. Para enviar un mensaje al
objeto, se define el nombre del objeto y se relaciona con la solicitud del mensaje mediante un punto. Desde el punto de vista
del usuario de una clase predefinida, esto es el no va más de la programación con objetos.
El diagrama anterior sigue el formato del lenguaje UML (Unified Modeling Langr/age, lenguaje de modelado unificado).
Cada clase se representa mediante un recuadro escribiendo el nombre del tipo en la parte superior, los miembros de datos
en la zona intermedia y los métodos (las funciones de dicho objeto que reciben cualquier mensaje que el programador enVÍe
a dicho objeto) en la parte inferior. A menudo, en estos diagramas sólo se muestran el nombre de la clase y los métodos
públicos, 00 incluyéndose la zona iotennedia, como en este caso. Si sólo se está interesado en el nombre de la clase, tam-
poco es necesario incluir la parte inferior.
puede consultar para obtener infonnación sobre cómo imprimir un cheque. Otro objeto o conjunto de objetos puede ser una
interfaz de impresión genérica que sepa todo sobre las diferentes clases de impresoras (pero nada sobre contabilidad; por
ello, probablemente es un candidato para ser comprado en lugar de escribirlo uno mismo). Y un tercer objeto podría utili-
zar los servicios de los otros dos para llevar a cabo su tarea. Por tanto, cada objeto tiene un conjunto cohesivo de servicios
que ofrecer. En un buen diseño orientado a objetos, cada objeto hace una cosa bien sin intentar hacer demasiadas cosas. Esto
además de pernlitir descubrir objetos que pueden adquirirse (el objeto interfaz de impresora), también genera nuevos obje-
tos que se reutilizarán en otros diseños.
Tratar los objetos como proveedores de servicios es una herramienta que simplifica mucho. No sólo es útil durante el pro-
ceso de diseño, sino también cuando alguien intenta comprender su propio código o reutilizar un objeto. Si se es capaz de
ver el valor del objeto basándose en el servicio que proporciona, será mucho más fácil adaptarlo al diseño.
La implementación oculta
Resulta útil descomponer el campo de juego en creadores de clases (aquellos que crean nuevos tipos de datos) y en progra-
madores de clientes4 (los consumidores de clases que emplean los tipos de datos en sus aplicaciones). El objetivo del pro-
gramador cliente es recopilar una caja de herramientas completa de clases que usar para el desarrollo rápido de aplicaciones.
El objetivo del creador de clases es construir una clase que exponga al programador cliente sólo lo que es necesario y man-
tenga todo lo demás oculto. ¿Por qué? Porque si está oculto, el programador cliente no puede acceder a ello, lo que signifi-
ca que el creador de clases puede cambiar la parte oculta a voluntad sin preocuparse del impacto que la modificación pueda
implicar. Nonnalmente, la parte oculta representa las vulnerabilidades internas de un objeto que un programador cliente
poco cuidadoso o poco formado podría corromper fácilmente, por lo que ocultar la implementación reduce los errores en
los programas.
En cualquier relación es importante tener límites que todas las partes implicadas tengan que respetar. Cuando se crea una
biblioteca, se establece una relación con el programador de clientes, que también es un programador, pero que debe cons-
truir su aplicación utilizando su biblioteca, posiblemente con el fin de obtener una biblioteca más grande. Si todos los
miembros de una clase están disponibles para cualquiera, entonces el programador de clientes puede hacer cualquier
cosa con dicha clase y no hay forma de imponer reglas. Incluso cuando prefiera que el programador de clientes no mani-
pule directamente algunos de los miembros de su clase, sin control de acceso no hay manera de impedirlo. Todo está a
la vista del mundo.
Por tanto, la primera razón que justifica el control de acceso es mantener las manos de los programadores cliente apartadas
de las partes que son necesarias para la operación interna de los tipos de datos, pero no de la parte correspondiente a la inter-
faz que los usuarios necesitan para resolver sus problemas concretos. Realmente, es un servicio para los programadores de
clientes porque pueden ver fácilmente lo que es importante para ellos y lo que pueden ignorar.
La segunda razón del control de acceso es permitir al diseñador de bibliotecas cambiar el funcionamiento interno de la
clase sin preocuparse de cómo afectará al programador de clientes. Por ejemplo, desea implementar una clase particular
de una forma sencilla para facilitar el desarrollo y más tarde descubre que tiene que volver a escribirlo para que se eje-
cute más rápidamente. Si la interfaz y la implementación están claramente separadas y protegidas, podrá hacer esto fácil-
mente.
Java emplea tres palabras clave explícitamente para definir los límites en una clase: public, private y protected. Estos
modificadores de acceso detenninan quién puede usar las definiciones del modo siguiente: public indica que el elemento
que le sigue está disponible para todo el mundo. Por otro lado, la palabra clave private, quiere decir que nadie puede acce-
der a dicho elemento excepto usted, el creador del tipo, dentro de los métodos de dicho tipo. private es un muro de ladri-
llos entre usted y el programador de clientes. Si alguien intenta acceder a un miembro private obtendrá un error en tiempo
de compilación. La palabra clave protected actúa como private, con la excepción de que una clase heredada tiene acceso
a los miembros protegidos (protected), pero no a los privados (private). Veremos los temas sobre herencia enseguida.
Java también tiene un acceso "predeterminado", que se emplea cuando no se aplica uno de los modificadores anteriores.
Normalmente, esto se denomina acceso de paquete, ya que las clases pueden acceder a los miembros de otras clases que
pertenecen al mismo paquete (componente de biblioteca), aunque fuera del paquete dichos miembros aparecen como priva-
dos (private).
Reutilización de la implementación
Una vez que se ha creado y probado una clase, idealmente debería representar una unidad de código útil. Pero esta reutili-
zación no siempre es tan fácil de conseguir como era de esperar; se necesita experiencia y perspicacia para generar un dise-
ño de un objeto reutili zable. Pero, una vez que se dispone de tal diseño, parece implorar ser reuti lizado. La reutilización de
códi go es una de las grandes ventajas que proporcionan los lenguajes de programación orientada a objetos.
La fonna más sencilla de reutilizar una clase consiste simplemente en emplear directamente un objeto de dicha clase, aun-
que también se puede colocar un objeto de dicha clase dentro de una clase nueva. Esto es 10 que se denomina "crear un obje-
to miembro". La nueva clase puede estar fonnada por cualquier número y tipo de otros objetos en cualquier combinación
necesaria para conseguir la funcionalidad deseada en dicha nueva clase. Definir una nueva clase a partir de clases existen-
tes se denomina composición (si la composición se realiza de forma dinámica, se llama agregación). A menudo se hace refe-
rencia a la composición como una relación "tiene un", como en "un coche tiene un motor".
Herencia
Por sí misma, la idea de objeto es una buena herramienta. Permite unir datos y funcionalidad por concepto, lo que permite
representar la idea de l problema-espacio apropiada en lugar de forzar el uso de los idiomas de la máqu ina subyacente. Estos
conceptos se expresan como un idades fundamentales en el lenguaje de programación uti lizando la palabra clave c1ass.
Sin embargo, es una pena abordar todo el problema para crear una clase y luego verse forzado a crear una clase nueva que
podría tener una funcionalidad similar. Es mejor, si se puede, tomar la clase existente, clonarla y luego añadir o modificar
lo que sea necesari o al clon. Esto es lo que se logra con la herencia, con la excepción de que la clase original (llamada clase
base. supere/ase o e/ase padre) se modifica, el clon "modificado" (denominado e/ase derivada, clase heredada, subclase o
cy
clase hija) también refleja los cambios.
I derivada I
5 Normalmente, es suficien te grado de detalle para la mayoría de los diagramas y no es necesario especificar si se está usan do una agregación o una com-
posición.
1 Introducción a los objetos 7
La flecha de este diagrama UML apunta de la clase derivada a la clase base. Como veremos, puede haber más de una clase
derivada.
Un tipo hace más que describir las restricciones definidas sobre un conjunto de objetos; también tiene una relación con otros
tipos. Dos tipos pueden tener características y comportamientos en común, pero un tipo puede contener más características
que el otro y también es posible que pueda manejar más mensajes (o manejarlos de forma diferente). La herencia expresa
esta similitud entre tipos utilizando el concepto de tipos base y tipos derivados. Un tipo base contiene todas las caracterÍs-
ticas y comportamientos que los tipos derivados de él comparten. Es recomendable crear un tipo base para representar el
núcleo de las ideas acerca de algunos de los objetos del sistema. A partir de ese tipo base, pueden deducirse otros tipos para
expresar las diferentes fonnas de implementar ese núcleo.
Por ejemplo, una máquina para el reciclado de basura clasifica los desperdicios. El tipo base es "basura" y cada desperdi-
cio tiene un peso, un valor, etc., y puede fragmentarse, mezclarse o descomponerse. A partir de esto, se derivan más tipos
específicos de basura que pueden tener características adicionales (una botella tendrá un color) o comportamientos (el alu-
minio puede modelarse, el acero puede tener propiedades magnéticas). Además, algunos comportamientos pueden ser dife-
rentes (el valor del papel depende de su tipo y condición). Utilizando la herencia, puede construir una jerarquía de tipos que
exprese el problema que está intentando resolver en términos de sus tipos.
Un segundo ejemplo es el clásico ejemplo de la forma, quizá usado en los sistemas de diseño asistido por computadora o en
la simulación de juegos. El tipo base es ""forma" y cada forma tiene un tamaño, un color, una posición, etc. Cada forma puede
dibujarse, borrarse, desplazarse, colorearse, etc. A partir de esto, se derivan (heredan) los tipos específicos de formas (cír-
culo, cuadrado, triángulo, etc.), cada una con sus propias características adicionales y comportamientos. Por ejemplo, cier-
tas fonnas podrán voltearse. AIgtmos comportamientos pueden ser diferentes, como por ejemplo cuando se quiere calcular
su área. La jerarquía de tipos engloba tanto las similitudes con las diferencias entre las formas.
Forma
dibujar()
borrar()
mover()
obtenerColor()
definirColor()
I I
Círculo Cuadrado Triángulo
Representar la solución en los mismos ténninos que el problema es muy útil, porque no se necesitan muchos modelos inter-
medios para pasar de una descripción del problema a una descripción de la solución. Con objetos, la jerarquía de tipos es el
modelo principal, porque se puede pasar directamente de la descripción del sistema en el mundo real a la descripción del
sistema mediante código. A pesar de esto, una de las dificultades que suelen tener los programadores con el diseño orienta-
do a objetos es que es demasiado sencillo ir del principio hasta el final. Una mente formada para ver soluciones complejas
puede, inicialmente, verse desconcertada por esta simplicidad.
Cuando se hereda de un tipo existente, se crea un tipo nuevo. Este tipo nuevo no sólo contiene todos los miembros del tipo
existente (aunque los privados están ocultos y son inaccesibles), sino lo que es más importante, duplica la interfaz de la clase
base; es decir, todos los mensajes que se pueden enviar a los objetos de la clase base también se pueden enviar a los obje-
tos de la clase derivada. Dado que conocemos el tipo de una clase por los mensajes que se le pueden enviar, esto quiere decir
que la clase derivada es del mismo tipo que la clase base. En el ejemplo anterior, "un círculo es una forma". Esta equiva-
lencia de tipos a través de la herencia es uno de los caminos fundamentales para comprender el significado de la programa-
ción orientada a objetos.
Puesto que la clase base y la clase derivada tienen la misma interfaz, debe existir alguna implementación que vaya junto con
dicha interfaz. Es decir, debe disponerse de algún código que se ejecute cuando un objeto recibe un mensaje concreto. Si
8 Piensa en Java
simplemente hereda una clase y no hace nada más, los métodos de la interfaz de la clase base pasan tal cual a la clase deri-
vada, lo que significa que los objetos de la clase derivada no sólo tienen el mismo tipo sino que también tienen el mismo
comportamiento, lo que no es especialmente interesante.
Hay dos formas de diferenciar la nueva clase deri vada de la clase base original. La primera es bastante directa: simplemen-
te, se añaden métodos nuevos a la clase derivada. Estos métodos nuevos no forman parte de la interfaz de la clase base, lo
que significa que ésta simplemente no hacía todo lo que se necesitaba y se le han añadido más métodos. Este sencillo y pri-
mitivo uso de la herencia es, en ocasiones, la solución perfecta del problema que se tiene entre manos. Sin embargo, debe
considerarse siempre la posibilidad de que la clase base pueda también necesitar esos métodos adicionales. Este proceso de
descubrimiento e iteración en un diseño tiene lugar habitualmente en la programación orientada a objetos.
Forma
dibujarO
borrarO
moverO
obtenerColorO
definirColorO
I I
I Círculo Cuadrado Triángulo
VoltearVerticalO
VoltearHorizontalO
Aunque en ocasiones la herencia puede implicar (especialmente en Java, donde la palabra clave para herencia es extends)
que se van a añadir métodos nuevos a la interfaz, no tiene que ser así necesariamente. La segunda y más importante fonna
de diferenciar la nueva clase es cambiando el comportamiento de un método existente de la clase base. Esto es lo que se
denomina sustitución del método.
Forma
dibujarO
borrarO
moverO
obtenerColorO
definirColorO
I I
Círculo Cuadrado Triángulo
Para sustituir un método, basta con crear una nueva defmición para el mismo en la clase derivada. Es decir, se usa el mismo
método de interfaz, pero se quiere que haga algo diferente en el tipo nuevo.
tipo que la clase base, ya que tiene exactamente la misma interfaz. Como resultado, es posible sustituir de forma exacta un
objeto de la clase derivada por uno de la clase base. Se podría pensar que esto es una sustitución pura y a menudo se deno-
mina principio de sustitución. En cierto sentido, ésta es la fonna ideal de tratar la herencia. A menudo, en este caso, la rela-
ción entre la clase base y las clases derivadas se dice que es una relación es-un, porque podemos decir, "un círculo es una
forma". Una manera de probar la herencia es determinando si se puede aplicar la relación es-un entre las clases y tiene sen-
tido.
A veces es necesario añ.adir nuevos elementos de interfaz a un tipo derivado, ampliando la interfaz. El tipo nuevo puede
todavía ser sustituido por el tipo base, pero la sustitución no es perfecta porque el tipo base no puede acceder a los métodos
nuevos. Esto se describe como una relación es-corno-un. El tipo nuevo tiene la interfaz del tipo antiguo pero también con-
tiene otros métodos, por lo que realmente no se puede decir que sean exactos. Por ejemplo, considere un sistema de aire
acondicionado. Suponga que su domicilio está equipado con todo el cableado para controlar el equipo, es decir, dispone de
una interfaz que le permite controlar el aire frío. Imagine que el aparato de aire acondicionado se estropea y lo reemplaza
por una bomba de calor, que puede generar tanto aire caliente como frío. La bomba de calor es-como-un aparato de aire
acondicionado, pero tiene más funciones. Debido a que el sistema de control de su casa está diseñado sólo para controlar el
aire frío, está restringido a la comunicación sólo con el sistema de frío del nuevo objeto. La interfaz del nuevo objeto se ha
ampliado y el sistema existente sólo conoce la interfaz original.
t
I I
Acondicionador Bomba de calor
de aire
enfriarO
enfriarO calentarO
Por supuesto, una vez que uno ve este diseño, está claro que la clase base "sistema de aire acondicionado" no es general y
debería renombrarse como "sistema de control de temperatura" con el fin de poder incluir también el control del aire calien-
te, en esta situación, está claro que el principio de sustitución funcionará. Sin embargo, este diagrama es un ejemplo de lo
que puede ocurrir en el diseño en el mundo real.
Cuando se ve claro que el principio de sustitución (la sustitución pura) es la única fonma de poder hacer las cosas, debe apli-
carse sin dudar. Sin embargo, habrá veces que no estará tan claro y será mejor añadir métodos nuevos a la interfaz de una
clase derivada. La experiencia le proporcionará los conocimientos necesarios para saber qué método emplear en cada caso.
compilación de forma precisa qué parte del código tiene que ejecutar. Éste es el punto clave, cuando se envía el mensaje, el
programador no desea saber qué parte del código se va a ejecutar; el método para dibujar se puede aplicar igualmente a un
círculo, a un cuadrado o a un triángulo y los objetos ejecutarán el código apropiado dependiendo de su tipo específico.
Si no se sabe qué fragmento de código se ej ecutará, entonces se añadirá un subtipo nuevo y el código que se ej ecute puede
ser diferente sin que sea necesario realizar cambios en el método que lo llama. Por tanto, el compilador no puede saber de
forma precisa qué fragmento de código hay qu e ejecutar y ¿qué hace entonces? Por ejemplo, en el siguiente diagrama, el
objeto controladorAves sólo funciona con los objetos genéricos Ave y no sabe exactamente de qué tipo son. Desde la pers-
pectiva del objeto controladorAves esto es adecuado ya que no ti ene que escribir código especial para determinar el tipo
exacto de Ave con el que está trabajando ni el comportamiento de dicha Ave. Entonces, ¿cómo es que cuando se invoca al
método moverO ignorando el tipo específico de Ave, se ejecuta el comportamiento correcto (un Ganso camina, vuela o nada
y un Pingüino camina o nada)?
controladorAves Ave
reubicarO ¿Qué ocurre cuando se moverO
llama a moverO?
t
I I
Ganso Pingüino
moverO moverO
La respuesta es una de las principales novedades de la programación orientada a objetos: el compilador no puede hacer una
llamada a función en el sentido tradicional. La llamada a función generada por un compilador no-POO hace lo que se deno-
mina un acoplamiento temprano, término que es posible que no haya escuchado antes. Significa que el compilador genera
una llamada a un nombre de funció n específico y el sistema de tiempo de ejecución resuelve esta llamada a la dirección
absoluta del código que se va a ejecutar. En la POO, el programa no puede determ inar la dirección del código hasta estar en
tiempo de ejecución, por lo que se hace necesario algún otro esquema cuando se envía un mensaje a un objeto genérico.
Para resolver el problema, los lenguajes orientados a objetos utilizan el concepto de acoplamiento tardío. Cuando se envía
un mensaje a un objeto, el códi go al que se está llamando no se detelmina hasta el tiempo de ejecución. El compilador no
asegura que el método exista, realiza una comprobación de tipos con los argumentos y devuelve un valor, pero no sabe exac-
tamente qué código tiene que ejecutar.
Para realizar el acoplamiento tardío, Java emplea un bit de código especial en lugar de una llamada absoluta. Este código
calcula la dirección del cuerpo del método, utilizando la información almacenada en el objeto (este proceso se estudi a en
detalle en el Capítulo 8, PolimOlfismo). Por tanto, cada objeto puede comportarse de forma di ferente de acuerdo con los con-
tenidos de dicho bit de código especial. Cuando se envía un mensaj e a un objeto, realmente el objeto resuelve lo que tiene
que hacer con dicho mensaje.
En algunos lenguaj es debe establecerse explicitamente que un método tenga la flexibilidad de las propiedades del acopla-
miento tardío (C++ utiliza la palabra clave virtual para ello). En estos lenguaj es, de manera predetenninada, los métodos
no se acoplan de forma dinámica. En Java, el acoplam iento dinámico es el comportamiento predetenn inado y el programa-
dor no tiene que añadi r ninguna palabra clave adicional para definir el polimorfismo.
Considere el ej emplo de las formas. La familia de clases (todas basadas en la misma interfaz uni forme) se ha mostrado en
un diagrama anteriormente en el capítulo. Para demostrar el pol imorfismo, queremos escribir un fragmento de código que
ignore los detalles específicos del tipo y que sólo sea indicado para la clase base. Dicho código se desacopla de la informa-
ción específica del tipo y por tanto es más sencillo de escribir y de comprender. Y, por ejemplo, si se añade un tipo nuevo
como Hexágono a través de la herencia, el código que haya escrito funcionará tanto para el nuevo tipo de Forma como para
los tipos existentes. Por tanto, el programa es ampliable.
Si escribe un método en Java (lo que pronto aprenderá a hacer) como el siguiente:
void hacerAlgo(Forma forma) {
borrar. forma () ;
/ / ...
dibujar . forma () i
1 Introducción a los objetos 11
Este método sirve para cualquier Forma, por lo que es independiente del tipo específico de objeto que se esté dibujando y
borrando. Si alguna otra parte del programa utiliza el método hacerAlgoO:
Circulo circulo = new Circulo() ;
Triangulo triangulo = new Triangulo()j
Linea linea = new Linea () ;
hacerAlgo (c i r cul o ) i
hacerAlgo (tr i angulo ) ;
hacerAlgo (linea ) ;
Las llamadas a JiacerAlgo O funcionarán correctamente, independientemente del tipo exacto del objeto.
De hecho, éste es un buen truco. Considere la línea:
hacerAlgo (circulo ) j
Lo que ocurre aquí es que se está pasando un Circulo en un método que está esperando una Forma. Dado que un Circulo
es una Forma, hacerAlgoO puede tratarlo como tal. Es decir, cualquier mensaje que hacerAlgoO pueda enviar a Forma,
un circulo puede aceptarlo. Por tanto, actuar así es completamente seguro y lógico.
Llamamos a este proceso de tratar un tipo derivado como si fuera un tipo base upcasting (generalización). La palabra sig-
nifica en inglés "proyección hacia arriba" y refleja la fonna en que se dibujan habitualmente los diagramas de herencia,
con el tipo base en la parte superior y las clases derivadas abriéndose en abanico hacia abajo, upcasting es, por tanto, efec-
tuar una proyección sobre un tipo base, ascendiendo por el diagrama de herencia.
"Upcasting" t
.- ______ Jo
B
,-_____J I
o
o
I
:
o Círculo Cuadrado Triángulo
o
Un programa orientado a objetos siempre contiene alguna generalización, porque es la fonna de desvincularse de tener que
conocer el tipo exacto con que se trabaja. Veamos el código de hacerAlgoO:
forma .borrar() i
II
forma.dibu j ar() ;
Observe que no se dice, "si eres un Circulo, hacer esto, si eres un Cuadrado, hacer esto, etc.". Con este tipo de código lo
que se hace es comprobar todos los tipos posibles de Forma, lo que resulta lioso y se necesitaría modificar cada vez que se
añadiera una nueva clase de Forma. En este ejemplo, sólo se dice: "Eres una fonna, te puedo borrarO y dibujarO tenien-
do en cuenta correctamente los detalles".
Lo que más impresiona del código del método hacerAlgoO es que, de alguna manera se hace lo correcto. U amar a
dibujarO para Circulo da lugar a que se ejecute un código diferente que cuando se le llama para un Cuadrado o una
Linea, pero cuando el mensaje dibujarO se envía a una Forma anónima, tiene lugar el comportamiento correcto basán-
dose en el tipo real de la Forma. Esto es impresionante porque, como se ha dicho anteriormente, cuando el compilador
Java está compilando el código de hacerAlgoO, no puede saber de forma exacta con qué tipos está tratando. Por ello, habi-
tualmente se espera que llame a la versión de borrarO y dibujarO para la clase base Forma y no a la versión específica
de Círculo, Cuadrado o Linea. Y sigue ocurriendo lo correcto gracias al polimorfismo. El compilador y el sistema de
tiempo de ejecución controlan los detalles; todo lo que hay que saber es qué ocurre y, lo más importante, cómo diseñar
haciendo uso de ello. Cuando se envía un mensaje a un objeto, el objeto hará lo correcto incluso cuando esté implicado el
proceso de generalización.
excepto e++) la respuesta es afinnativa. Y el nombre de esta clase base es simplemente Object. Resulta que las ventajas
de una jerarquía de raíz única son enormes.
Todos los objetos de una jerarquía de raíz única tienen una interfaz en común, por 10 que en última instancia son del mismo
tipo fundamental. La alternativa (proporcionada por e++) es no saber que todo es del mismo tipo básico. Desde el punto de
vista de la compatibilidad descendente, esto se ajusta al modelo de e mejor y puede pensarse que es menos restrictivo, pero
cuando se quiere hacer programación orientada a objetos pura debe construirse una jerarquía propia con el fin de proporcio-
nar la misma utilidad que se construye en otros lenguajes de programación orientada a objetos. Y en cualquier nueva biblio-
teca de clases que se adquiera, se empleará alguna otra interfaz incompatible. Requiere esfuerzo (y posiblemente herencia
múltiple) hacer funcionar la nueva interfaz en un diseño propio. ¿Merece la pena entonces la "flexibilidad" adicional de
e++? Si la necesita (si dispone ya de una gran cantidad de código en e), entonces es bastante valiosa. Si parte de cero, otras
alternativas como Java a menudo resultan más productivas.
Puede garantizarse que todos los objetos de una jerarquía de raíz única tengan una determinada funcionalidad. Es posible
realizar detenninadas operaciones básicas sobre todos los objetos del sistema. Pueden crearse todos los objetos y el paso de
argumentos se simplifica enonnemente.
Una jerarquía de raíz única facilita mucho la implementación de un depurador de memoria, que es una de las mejoras fun-
damentales de Java sobre C++. y dado que la información sobre el tipo de un objeto está garantizada en todos los objetos,
nunca se encontrará con un objeto cuyo tipo no pueda determinarse. Esto es especialmente importante en las operaciones en
el nivel del sistema, corno por ejemplo el tratamiento de excepciones y para proporcionar un mayor grado de flexibilidad en
la programación.
Contenedores
En general, no se sabe cuántos objetos se van a necesitar para resolver un determinado problema o cuánto tiempo va a lle-
var. Tampoco se sabe cómo se van a almacenar dichos objetos. ¿Cómo se puede saber cuánto espacio hay que crear si no se
conoce dicha infonnación hasta el momento de la ejecución?
La solución a la mayoría de los problemas en el diseño orientado a objetos parece algo poco serio, esta solución consiste en
crear otro tipo de objeto. El nuevo tipo de objeto que resuelve este problema concreto almacena referencias a otros objetos.
Por supuesto, se puede hacer 10 mismo con una matriz, elemento que está disponible en la mayoría de los lenguajes. Pero
este nuevo objeto, denominado contenedor (también se llama colección, pero la biblioteca de Java utiliza dicho término con
un sentido diferente, por lo que en este libro emplearemos el término "contenedor"), se amplía por sí mismo cuando es nece-
sario acomodar cualquier cosa que se quiera introducir en él. Por tanto, no necesitamos saber cuántos objetos pueden alma-
cenarse en un contenedor. Basta con crear un objeto contenedor y dejarle a él que se ocupe de los detalles.
Afortunadamente, los buenos lenguajes de programación orientada a objetos incluyen un conjunto de contenedores como
parte del paquete. En e++, ese conjunto forma parte de la biblioteca estándar e++ y a menudo se le denomina STL
(Standard Template Library, biblioteca estándar de plantillas). Smalltalk tiene un conjunto muy completo de contenedores,
mientras que Java tiene también numerosos contenedores en su biblioteca estándar. En algunas bibliotecas, se considera que
uno o dos contenedores genéricos bastan y sobran para satisfacer todas las necesidades, mientras que en otras (por ejemplo,
en Java) la biblioteca tiene diferentes tipos de contenedores para satisfacer necesidades distintas: varios tipos diferentes de
clases List (para almacenar secuencias), M aps (también denominados matrices asociativas y que se emplean para asociar
objetos con otros objetos), Sets (para almacenar un objeto de cada tipo) y otros componentes como colas, árboles, pilas, etc.
Desde el punto de vista del diseño, lo único que queremos es disponer de un contenedor que pueda manipularse para resol-
ver nuestro problema. Si un mismo tipo de contenedor satisface todas las necesidades, no existe ninguna razón para dispo-
ner de varias clases de contenedor. Sin embargo, existen dos razones por las que sí es necesario poder disponer de diferentes
contenedores. En primer lugar, cada tipo de contenedor proporciona su propio tipo de interfaz y su propio comportamiento
externo. Umi pila tiene una interfaz y un comportamiento distintos que una cola, que a su vez es distinto de un conjunto o
una lista. Es posible que alguno de estos tipos de contenedor proporcione una solución más flexible a nuestro problema que
los restantes tipos. En segundo lugar, contenedores diferentes tienen una eficiencia distinta a la hora de realizar determina-
das operaciones. Por ejemplo, existen dos tipos básicos de contenedores de tipo Lis!: ArrayList (lista matricial) y LinkedList
(lista enlazada). Ambos son secuencias simples que pueden tener interfaces y comportamientos externos idénticos. Pero cier-
tas operaciones pueden llevar asociado un coste radicalmente distinto. La operación de acceder aleatoriamente a los elemen-
tos contenidos en un contenedor de tipo ArrayList es una operación de tiempo constante. Se tarda el mismo tiempo
1 Introducción a los objetos 13
independientemente de cuál sea el elemento que se haya seleccionado. Sin embargo, en un contenedor de tipo LinkedList
resulta muy caro desplazarse a lo largo de la lista para seleccionar aleatoriamente un elemento, y se tarda más tiempo en
localizar un elemento cuanto más atrás esté situado en la lista. Por otro lado, si se quiere insertar un elemento en mitad de
la secuencia, es más barato hacerlo en un contenedor de tipo LinkedList que en otro de tipo ArrayList. Estas y otras ope-
raciones pueden tener una eficiencia diferente dependiendo de la estructura subyacente de la secuencia. Podemos comenzar
construyendo nuestro programa con un contenedor de tipo LinkedList y, a la hora de juzgar las prestaciones, cambiar a otro
de tipo ArrayList. Debido a la abstracción obtenida mediante la interfaz List, podemos cambiar de un tipo de contenedor a
otro con un impacto mínimo en el código.
6Los contenedores no permiten almacenar primitivas, pero la característica de alltobxing de Java SES hace que esta restricción tenga poca importancia.
Hablaremos de esto en detalle más adelante en el libro.
14 Piensa en Java
ser necesario, es preciso eliminarlo, para que se liberen estos recursos y puedan emplearse en alguna otra cosa. En los casos
más simples de programación, el problema de borrar los objetos no resulta demasiado complicado. Creamos el objeto, lo
usamos mientras que es necesario y después lo destruimos. Sin embargo, no es dificil encontrarse situaciones bastante más
complejas que ésta.
Suponga por ejemplo que estamos diseñando un sistema para gestionar el tráfico aéreo de un aeropuerto (el mismo modelo
serviría para gestionar piezas en un almacén o para un sistema de alquiler de vídeos o para una tienda de venta de masco-
tas). A primera vista, el problema parece muy simple: creamos un contenedor para almacenar las aeronaves y luego crea-
mos una nueva aeronave y la insertamos en el contenedor por cada una de las aeronaves que entren en la zona de control
del tráfico aéreo. De cara al borrado, simplemente basta con eliminar el objeto aeronave apropiado en el momento en que
el avión abandone la zona.
Pero es posible que tengamos algún otro sistema en el que queden registrados los datos acerca de los aviones; quizá se trate
de datos que no requieran una atención tan inmediata como la de la función principal de control del tráfico aéreo. Puede que
se trate de un registro de los planes de vuelo de todos los pequeños aeroplanos que salgan del aeropuerto. Entonces, po-
dríamos definir un segundo contenedor para esos aeroplanos y, cada vez que se creara un objeto aeronave, se introduciría
también en este segundo contenedor si se trata de un pequeño aeroplano. Entonces, algún proceso de segundo plano podría
realizar operaciones sobre los objetos almacenados en este segundo contenedor en los momentos de inactividad.
Ahora el problema ya es más complicado: ¿cómo podemos saber cuándo hay que destruir los objetos? Aún cuando nosotros
hayamos terminado de procesar un objeto, puede que alguna otra parte del sistema no lo haya hecho. Este mismo problema
puede surgir en muchas otras situaciones, y puede llegar a resultar enonnemente complejo de resolver en aquellos sistemas
de programación (como C++) en los que es preciso borrar explícitamente un objeto cuando se ha terminado de utilizar.
¿Dónde se almacenan los datos correspondientes a un objeto y cómo se puede controlar el tiempo de vida del mismo? En
C++, se adopta el enfoque de que el control de la eficiencia es el tema más importante, por lo que todas las decisiones que-
dan en manos del programador. Para conseguir la máxima velocidad de ejecución, las características de almacenamiento y
del tiempo de vida del objeto pueden determinarse mientras se está escribiendo el programa, colocando los objetos en la pila
(a estos objetos se los denomina en ocasiones variables automáticas o de ámbito) o en el área de almacenamiento estático.
Esto hace que lo más prioritario sea la velocidad de asignación y liberación del almacenamiento, y este control puede resul-
tar muy útil en muchas situaciones. Sin embargo, perdemos flexibilidad porque es preciso conocer la cantidad, el tiempo de
vida y el tipo exacto de los objetos a la hora de escribir el programa. Si estamos tratando de resolver un problema más gene-
ral, como por ejemplo, un programa de diseño asistido por computadora, un sistema de gestión de almacén o un sistema de
control de tráfico aéreo, esta solución es demasiado restrictiva.
La segunda posibilidad consiste en crear los objetos dinámicamente en un área de memoria denominada cúmulo. Con este
enfoque, no sabemos hasta el momento de la ejecución cuántos objetos van a ser necesarios, cuál va a ser su tiempo de vida
ni cuál es su tipo exacto. Todas estas características se determinan en el momento en que se ejecuta el programa. Si hace
falta un nuevo objeto, simplemente se crea en el cúmulo de memoria, en el preciso instante en que sea necesario. Puesto que
el almacenamiento se gestiona dinámicamente en tiempo de ejecución, la cantidad de tiempo requerida para asignar el alma-
cenamiento en el cúmulo de memoria puede ser bastante mayor que el tiempo necesario para crear un cierto espacio en la
pila. La creación de espacio de almacenamiento en la pila requiere normalmente tilla única instrucción de ensamblador, para
desplazar hacia abajo el puntero de la pila y otra instrucción para volver a desplazarlo hacia arriba. El tiempo necesario
para crear un espacio de almacenamiento en el cúmulo de memoria depende del diseño del mecanismo de almacenamiento.
La solución dinámica se basa en la suposición, generalmente bastante lógica, de que los objetos suelen ser complicados, por
lo que el tiempo adicional requerido para localizar el espacio de almacenamiento y luego liberarlo no tendrá demasiado
impacto sobre el proceso de creación del objeto. Además, el mayor grado de flexibilidad que se obtiene resulta esencial para
resolver los problemas de programación de carácter general.
Java utiliza exclusivamente un mecanismo dinámico de as ignación de memoria7 . Cada vez que se quiere crear un objeto, se
utiliza el operador new para constmir una instancia dinámica del objeto.
Sin embargo, existe otro problema, referido al tiempo de vida de un objeto. En aquellos lenguajes que permiten crear obje-
tos en la pila, el compilador determina cuál es la duración del objeto y puede destruirlo automáticamente. Sin embargo, si
creamos el objeto en el cúmulo de memoria, el compilador no sabe cuál es su tiempo de vida. En un lenguaje como C++,
7 Los tipos primitivos, de los que hablaremos en breve, representun un caso especial.
1 Introducció n a los objetos 15
es preciso determinar mediante programa cuándo debe destruirse el objeto, lo que puede provocar pérdidas de memori a si
no se real iza esta tarea correctamente (y este problema resulta bastante común en los programas C++). lava proporciona una
característica denominada depurador de memoria, que descubre automáti camente cuándo un determinado obj eto ya no está
en uso, en cuyo caso lo destruye. Un depurador de memoria resu lta mucho más cómodo que cualqui er otra solución alter-
nativa, porque reduce el número de problemas que e l programador debe controlar, y reduce también la cantidad de código
que hay que escribir. Además, lo que resulta más importante, el depurador de memoria proporciona un ni vel mucho mayor
de protección contra el insidioso problema de las fugas de memoria, que ha hecho que muchos proyectos en C++ fraca-
saran.
En Java, el depurador de memoria está diseñado para encargarse del problema de liberación de la memoria (aunque esto no
incluye otros aspectos relativos al borrado de un objeto). El depurador de memoria "sabe" cuándo ya no se está usando un
objeto, en cuyo caso libera automáticamente la memori a correspondiente a ese objeto. Esta característica, combinada con el
hecho de que todos los objetos heredan de la clase raíz Object, y con el hecho de que sólo pueden crearse obj etos de una
manera (en el cúmulo de memoria). hace que el proceso de programación en Java sea mucho más simple que en C++, hay
muchas menos decisiones que tomar y muchos menos problemas que resolver.
Programación concurrente
Un concepto fundamental en el campo de la programación es la idea de poder gestionar más de una tarea al mismo tiempo.
Muchos prob lemas de programación requieren que el programa detenga la tarea que estuviera rea liza ndo, resuelva algún
16 Piensa en Java
otro problema y luego vuelva al proceso principal. A lo largo del tiempo, se ha tratado de aplicar diversas soluciones a este
problema. Inicialmente, los programadores que tenían un adecuado conocimiento de bajo nivel de la máquina sobre la que
estaban programando escribían rutinas de servicio de interrupción, y la suspensión del proceso principal se llevaba a cabo
mediante una interrupción hardware. Aunque este esquema funcionaba bien, resultaba complicado y no era portable, por lo
que traducir un programa a un nuevo tipo de máquina resultaba bastante lento y muy caro.
En ocasiones, las interrupciones son necesarias para gestionar las tareas con requisitos críticos de tiempo, pero hay una
amplia clase de problemas en la que tan sólo nos interesa dividir el problema en una serie de fragmentos que se ejecuten por
separado (tareas), de modo que el programa completo pueda tener un mej or tiempo de respuesta. En un programa, estos frag-
mentos que se ejecutan por separado, se denominan hebras y el conjunto general se llama concurrencia. Un ejemplo bas-
tante común de concurrencia es la interfaz de usuario. Utilizando distintas tareas, el usuario apretando un botón puede
obtener una respuesta rápida, en lugar de tener que esperar a que el programa finalice con la tarea que esté actualmente rea-
lizando.
Normalmente, las tareas son sólo una forma de asignar el tiempo disponible en un único procesador. Pero si el sistema ope-
rativo soporta múltiples procesadores, puede asignarse cada tarea a un procesador distinto, en cuyo caso las tareas pueden
ejecutarse realmente en paralelo. Una de las ventajas de incluir los mecanismos de concurrencia en el nivel de lenguaje es
que el programador no tiene que preocuparse de si hay varios procesadores o sólo uno; el programa se divide desde el punto
de vista lógico en una serie de tareas, y si la máquina dispone de más de un procesador, el programa se ejecutará más rápi-
do, sin necesidad de efectuar ningún ajuste especial.
Todo esto hace que la concurrencia parezca algo bastante sencillo, pero existe un problema: los recursos compartidos. Si se
están ejecutando varias tareas que esperan poder acceder al mismo recurso, tendremos un problema de contienda entre las
tareas. Por ejemplo, no puede haber dos procesadores enviando información a una misma impresora. Para resolver el pro-
blema, los recursos que puedan compartirse, como por ejemplo una impresora, deben bloquearse mientras estén siendo uti-
lizados por una tarea. De manera que la forma de funcionar es la siguiente: una tarea bloquea un recurso, completa el trabajo
que tuviera asignado y luego elimina el bloqueo para que alguna otra tarea pueda emplear el recurso.
Los mecanismos de concurrencia en Java están integrados dentro de lenguaje y Java SES ha mejorado significativamente el
soporte de biblioteca para los mecanismos de concurrencia.
Java e Internet
Si Java no es, en defmitiva, más que otro lenguaje informático de programación, podríamos preguntamos por qué es tan
importante y por qué se dice de él que representa una auténtica revolución dentro del campo de la programación. La res-
puesta no resulta obvia para aquéllos que provengan del campo de la programación tradicional. Aunque Java resulta muy
útil para resolver problemas de programación en entornos autónomos, su importancia se debe a que permite resolver los pro-
blemas de programación que surgen en la World Wide Web.
¿Qué es la Web?
Al principio, la Web puede parecer algo misterioso, con todas esas palabras extrañas como "surfear," "presencia web" y
"páginas de inicio". Resulta útil, para entender los conceptos, dar un paso atrás y tratar de comprender lo que la Web es real-
mente, pero para ello es necesario comprender primero lo que son los sistemas cliente/servidor, que constituyen otro campo
de la informática lleno de conceptos bastante confusos.
Informática cliente/servidor
La idea principal en la que se basan los sistemas cliente/servidor es que podemos disponer de un repositorio centralizado de
información· (por ejemplo, algún tipo de datos dentro de una base de datos) que queramos distribuir bajo demanda a una
serie de personas o de computadoras. Uno de los conceptos clave de las arquitecturas cliente/servidor es que el repositorio
de información está centralizado, por lo que puede ser modificado sin que esas modificaciones se propaguen hasta los con-
sumidores de la información. El repositorio de información, el software que distribuye la información y la máquina o máqui-
nas donde esa información y ese software residen se denominan, en conjunto, "servidor". El software que reside en las
máquinas consumidoras, que se comunica con el servidor, que extrae la infonnación, que la procesa y que luego la muestra
en la propia máquina consumidora se denomina cliente.
1 Introducción a los objetos 17
El concepto básico de informática cliente/servidor no es, por tanto, demasiado complicado. Los problemas surgen porque
disponemos de un único servidor tratando de dar servicio a múltiples clientes al mismo ti empo. Generalmente, se utiliza
algún tipo de sistema de gestión de bases de datos de modo que el diseñador "equilibra" la disposición de los datos entre
distintas tablas, con el fin de optimizar el uso de los datos. Además, estos sistemas permiten a menudo que los clientes inser-
ten nueva información dentro de un servidor. Esto quiere decir que es preciso garantizar que los nuevos datos de un cliente
no sobreescriban los nuevos datos de otro cliente, al igual que hay que garantizar que no se pierdan datos en el proceso de
añadirlos a la base de datos (este tipo de mecanismos se denomina procesamiento de transacciones) . A medida que se reali-
zan modificaciones en el software de cliente, es necesario diseñar el software, depurarlo e instalarlo en las máquinas clien-
te, lo que resulta ser más complicado y más caro de lo que en un principio cabría esperar. Resulta especialmente
problemático soportar múltiples tipos de computadoras y de sistemas operativos. Finalmente, es necesario tener en cuenta
también la cuestión crucial del rendimiento: puede que tengamos cientos de clientes enviando cientos de solicitudes al ser-
vidor en un momento dado, por 10 que cualquier pequeño retardo puede llegar a ser verdaderamente crítico. Para min imizar
la latencia, los programadores hacen un gran esfuerzo para tratar de descargar las tareas de procesamiento que en ocasiones
se descargan en la máquina cliente, pero en otras ocasiones se descargan en otras máquinas situadas junto al servidor, utili-
zando un tipo especial de software denominado middleware, (el middleware se utiliza también para mejorar la facilidad de
mantenimiento del sistema).
Esa idea tan simple de distribuir la infonnación tiene tantos niveles de complejidad que el problema global puede parecer
enigmáticamente insoluble. A pesar de lo cual, se trata de un problema crucial: la informática cliente/servidor representa
aproximadamente la mitad de las actividades de programación en la actualidad. Este tipo de arquitectura es responsable de
todo tipo de tareas, desde la introducción de pedidos y la realización de transacciones con tarjetas de crédito basta la distri-
bución de cualquier tipo de datos, como por ejemplo cotizaciones bursátiles, datos científicos, infonnación de organismos
gubernamentales. En el pasado, lo que hemos hecho es desarrollar soluciones individuales para problemas individuales,
inventando una nueva solución en cada ocasión. Esas soluciones eran dificiles de diseñar y de utilizar, y el usuario se veía
obligado a aprender una nueva interfaz en cada caso. De este modo, se llegó a un punto en que era necesario resolver el pro-
blema global de la infonnática cliente/servidor de una vez y para siempre.
verse incorporando la capacidad de ejecutar programas en el extremo cliente, bajo control del explorador. Esto se denomi-
na programación del lado del c1ientc.
Plug-íns
Uno de los avances más significativos en la programación del lado del cliente es el desarrollo de lo que se denomina plug-
in. Se trata de un mecanismo mediante el que un programador puede añadir algún nuevo tipo de funcionalidad a un explo-
rador descargando un fragmento de código que se inserta en el lugar apropiado dentro de l explorador. Ese fragmento de
código le dice al explorador: "A partir de ahora puedes real izar este nuevo tipo de actividad" (sólo es necesario descargar el
1 Introducción a los objetos 19
plug-in una vez). Podemos añadir nuevas fannas de comportamiento, potentes y rápidas, a los exploradores mediante
plug-ins, pero la escritura de unplug-in no resulta nada trivial, y por eso mismo no es conveniente acometer ese tipo de tarea
como parte del proceso de construcción de un sitio \Veb. El va lor de un plug-in para la programación del lado del cliente es
que pernlite a los programadores avanzados desarrollar extensiones y añadírselas a un explorador sin necesidad de pedir per-
miso al fabricante del explorador. De esta fonna , los plug-ins proporcionan una especie de "puerta trasera" que pennite la
creación de nuevos lenguajes de programación del lado del cliente (aunq ue no todos los lenguajes se implementan como
plug-ins).
Lenguajes de script
Los plug-ins dieron como resultado el desarrollo de lenguajes de scrip! para los exploradores. Con un lenguaje de script, e l
código fuente del programa del lado del cliente se integra directamente dentro de la página HTML, y el plllg-in que se encar-
ga de interpretar ese lenguaj e se activa de manera automática en el momento de visualizar la página HTML. Los lenguajes
de script suelen ser razonablemente fáci les de comprender, y como están formados simplemente por texto que se incluye
dentro de la propia página HTML, se cargan muy ráp idamente como parte del acceso al servidor mediante el que se obtie-
ne la página. La desventaja es que el código queda expuesto, ya que cualquiera puede verlo (y copiarlo). Generalmente, sin
embargo, los programadores no llevan a cabo tareas extremadamente sofisticadas con los lenguajes de script, así que este
problema no resulta particularmente grave.
Uno de los lenguajes de scripl que los exploradores web suelen soportar sin necesidad de un p lllg-in es JavaScript (ellen-
guaje JavaScript sólo se asemeja de forma bastante vaga a Java, por lo que hace falta un esfuerzo de aprendizaje adicional
para llegar a dominarlo; recibió el nombre de JavaScript simplemente para aprovechar el impulso inicial de marketing de
Java). Lamentablemente, cada explorador web implementaba originalmente JavaScript de forma distinta a los restantes
ex ploradores web, e incluso en ocasiones, de fonna diferente a otras versiones del mismo explorador. La estandarización de
JavaScript mediante el diseño del lenguaje estándar ECMAScript ha resuelto parcialmente este problema, pero tuvo que
transcurrir bastante ti empo hasta que los distintos exploradores adoptaron el estándar (el problema se complicó porque
Microsoft trataba de conseguir sus propios objetivos presionando en favor de su lenguaje VBScript, que también se aseme-
jaba vagamente a JavaScript). En general, es necesario llevar a cabo la programación de las páginas utilizando una especie
de mínimo común denom inador de JavaScripr, si lo que queremos es que esas páginas puedan visualizarse en todos los tipos
de exploradores. Por su parte, la solución de errores y la dep uración en JavaScript son un auténtico lío. Como prueba de lo
dificil que resulta diseñar un problema complejo con JavaScript, sólo muy recientemente alguien se ha atrevido a crear un a
aplicación compleja basada en él (Google, con GMail), y ese desarrollo requirió una dedicación y una experiencia realmen-
te notables.
Lo que todo esto nos sugiere es que los lenguajes de script que se emplean en los exploradores web están di señados, real-
mente, para resolver tipos específicos de problemas, principalmente el de la creación de in terfaces gráficas de usuario (GUT)
más ricas e interactivas. Sin embargo, un lenguaje de script puede resolver quizá un 80 por ciento de los problemas que
podemos encontrar en la programación del lado del cliente. Es posible que los problemas que el lector quiera resolver es tén
incluidos dentro de ese 80 por ciento. Si esto es así, y teniendo en cuenta que los lenguaj es de scripl permiten realizar los
desarrollos de fonna más fácil y rápida, probablemente sería conveniente ver si se puede resolver un tipo concreto de pro-
blema empleando un lenguaje de script, antes de considerar otras soluciones más complejas, como la programación en Java.
Java
Si un lenguaje de scripl puede resolver el 80 por ciento de los problemas de la programación del lado del cliente, ¿qué pasa
con el otro 20 por ciento, con los "problemas realmente dificiles"? Java representa una solución bastante popular para este
tipo de problemas. No sólo se trata de un potente lenguaje de programación diseñado para ser seguro, inte¡plataforma e inter-
nacional, sino que continuamente está siendo ampliado para proporcionar nuevas características del lenguaje y nuevas
bibliotecas que penniten gestionar de manera elegante una seri e de problemas que resultan bastante dificiles de tratar en los
lenguajes de programación tradi cionales, como por ejemplo la concurrencia, el acceso a bases de datos, la programación en
red y la infonuática distribuida. Java permite reso lver los problemas de programación del lado del cliente utilizando applets
y Java Web Starl.
Un applet es un mini-programa que sólo puede ejecutarse sobre un explorador web. El applet se descarga automáticamen-
te como parte de la página web (de la misma forma que se descarga automáticamente, por ejemplo, un gráfico). Cuando se
acti va el applet, ejecuta un programa. Este meca ni smo de ejecución automática forma parte de la belleza de esta solución:
nos proporciona una fonna de distribuir automáticamente el software de cliente desde el servidor en el mismo momento en
20 Piensa en Java
que el usuario necesita ese software de cliente, y no antes. El usuario obtiene la última versión del software de cliente, libre
de errores y sin necesidad de realizar complejas reinstalaciones. Debido a la forma en que se ha diseñado Java, el progra-
mador sólo tiene que crear un programa simple y ese programa funcionará automáticamente en todas las computadoras que
dispongan de exploradores que incluyan un intérprete integrado de Java (lo que incluye la inmensa mayoría de máquinas).
Puesto que Java es un lenguaje de programación completo podemos llevar a cabo la mayor cantidad de trabajo posible en
el cliente, tanto antes como después de enviar solicitudes al servidor. Por ejemplo, no es necesario enviar una solicitud a tra-
vés de Internet simplemente para descubrir que hemos escrito mal una fecha o algún otro parámetro; asimismo, la compu-
tadora cliente puede encargarse de manera rápida de la tarea de dibujar una serie de datos, en lugar de esperar a que el
servidor genere el gráfico y devuelva una imagen al explorador. De este modo, no sólo aumentan de forma inmediata la velo-
cidad y la capacidad de respuesta, sino que disminuyen también la carga de trabajo de los servidores y el tráfico de red, evi-
tando así que todo Internet se ralentice.
Alternativas
Para ser honestos, los applets Java no han llegado a cumplir con las expectativas iniciales. Después del lanzamiento de Java,
parecía que los applets era lo que más entusiasmaba a todo el mundo, porque iban a permitir finalmente realizar tareas serias
de programación del lado del cliente, iban a mejorar la capacidad de respuesta de las aplicaciones basadas en Internet e iban
a reducir el ancho de banda necesario. Las posibilidades que todo el mundo tenía en mente eran inmensas.
Sin embargo, hoy día nos podemos encontrar con unos applets realmente interesantes en la Web, pero la esperada migra-
ción masiva hacia los applets no llegó nunca a producirse. El principal problema era que la descarga de 10MB necesaria
para instalar el entorno de ejecución JRE (Java Runtime Environment) era demasiado para el usuario medio. El hecho de
que Microsoft decidiera no incluir el entorno JRE dentro de Internet Explorer puede ser lo que acabó por determinar su acia-
go destino. Pero, sea como sea, lo cierto es que los applets Java no han llegado nunca a ser utilizados de forma masiva.
A pesar de todo, los applets y las aplicaciones Java Web Start siguen siendo adecuadas en algunas situaciones. En todos
aquellos casos en que tengamos control sobre las máquinas de usuario, por ejemplo en una gran empresa, resulta razonable
distribuir y actualizar las aplicaciones cliente utilizando este tipo de tecnologías, que nos pueden ahorrar una cantidad con-
siderable de tiempo, esfuerzo y dinero, especialmente cuando es necesario realizar actualizaciones frecuentes.
En el Capítulo 22, Interfaces gráficas de usuario, analizaremos una buena tecnología bastante prometedora, Flex de
Macromedia, que permite crear equivalentes a los applels basados en Flash. Como el reproductor Flash Player está dispo-
nible en más del 98 por ciento de todos los exploradores web (incluyendo Windows, Linux y Mac), puede considerarse como
un estándar de facto. La instalación O actualización de Flash Player es asimismo rápida y fácil. El lenguaje ActionScript está
basado en ECMAScript, por lo que resulta razonablemente familiar, pero Flex permite realizar las tareas de programación
sin preocuparse acerca de las especifidades de los exploradores, por 10 que resulta bastante más atractivo que JavaScript.
Para la programación del lado del cliente se trata de una alternativa que merece la pena considerar.
.NETy C#
Durante un tiempo, el competidor principal de los applets de Java era ActiveX de Microsoft, aunque esta tecnología reque-
ría que en el cliente se estuviera ejecutando el sistema operativo Windows. Desde entonces, Microsoft ha desarrollado un
competidor de Java: la plataforma .NET y el lenguaje de programación C#. La plataforma .NET es, aproximadamente, equi-
valente a la máquina virtual Java (NM, Java Virtual Machine; es la plataforma software en la que se ejecutan los progra-
mas Java) y a las bibliotecas Java, mientras que C# tiene similitudes bastante evidentes con Java. Se trata, ciertamente, del
mejor intento que Microsoft ha llevado a cabo en el área de los lenguajes de programación. Por supuesto, Microsoft partía
con la considerable ventaja de conocer qué cosas habían funcionado de manera adecuada y qué cosas no funcionaban tan
bien en Java, por lo que aprovechó esos conocimientos. Desde su concepción, es la primera vez que Java se ha encontrado
con un verdadero competidor. Como resultado, los diseñadores de Java en Sun han analizado intensivamente C# y las razo-
nes por las que un programador podría sentirse tentado a adoptar ese lenguaje, y han respondido introduciendo significati-
vas mejoras en Java, que han resultado en el lanzamiento de Java SE5.
Actualmente, la debilidad principal y el problema más importante en relación con .NET es si Microsoft pennitirá portarlo
completamente a otras plataformas. Ellos afIrman que no hay ningún problema para esto, y el proyecto Mono (www.go-
mono.com) dispone de una implementación parcial de .NET sobre Linux, pero hasta que la implementación sea completa y
Microsoft decida no recortar ninguna parte de la misma, sigue siendo una apuesta arriesgada adoptar .NET como solución
interplataforma.
Introducción a los objetos 21
con exploradores que dispongan de capacidades diferentes. Los temas de programación del lado del servidor se tratan en
Thinking in Enterprise Java en el sitio web www.MindView.net.
A pesar de todo lo que hemos comentado acerca de Java y de Internet, Java es un lenguaje de programación de propósito
general, que pennite resolver los mismos tipos de problemas que podemos reso lver con otros lenguajes. En este sentido,
la ventaja de Java no radica sólo en su portabilidad, sino también en su programabilidad, su robustez, su amplia biblio-
teca estándar y las numerosas bibliotecas de otros fabricantes que ya están disponibles y que continúan siendo desarro-
lladas.
Resumen
Ya sabemos cuál es el aspecto básico de un programa procedimental: definiciones de datos y llamadas a funciones. Para
comprender uno de esos programas es preciso analizarlo, examinando las llamadas a función y util izando conceptos de bajo
nivel con el fin de crear un modelo mental del programa. Ésta es la razón por la que necesitamos representaciones inter-
medias a la hora de diseñar programas procedimentales: en sí mismos, estos programas tienden a ser confusos, porque se
utiliza una forma de expresarse que está más orientada hacia la computadora que hacia el programa que se trata de resolver.
Como la programación orientada a objetos añade numerosos conceptos nuevos, con respecto a los que podemos encontrar
en un lenguaje procedimental, la intuición nos dice que el programa Java resultante será más complicado que el programa
procedimental equivalente. Sin embargo, la realidad resulta gratamente sorprendente: un programa Java bien escrito es,
generalmente, mucho más simple y mucho más fácil de comprender que Wl programa procedimentaL Lo que podemos ver
al analizar el programa son las definiciones de los objetos que representan los conceptos de nuestro espacio de problema (en
lugar de centrarse en la representación realizada dentro de la máquina),junto con mensajes que se envían a esos objetos para
representar las actividades que tienen lugar en ese espacio de problema. Uno de los atractivos de la programación orienta-
da a objetos, es que con un programa bien diseñado resulta fácil comprender el código sin más que leerlo. Asimismo, suele
haber una cantidad de código bastante menor, porque buena parte de los problemas puede resolverse reuti lizando código de
las bibliotecas existentes.
La programación orientada a objetos y el lenguaje Java no resultan adecuados para todas las situaciones. Es importante eva-
luar cuáles son nuestras necesidades reales y detenninar si Java permitirá satisfacerlas de fonna óptima o si, por el contra-
rio, es mejor emplear algún otro sistema de programación (incluyendo el que en la actualidad estemos usando). Si podemos
estar seguros de que nuestras necesidades van a ser bastante especializadas en un futuro próximo, y si estamos sujetos a res-
tricciones específicas que Java pueda no satisfacer, resulta recomendable investigar otras alternativas (en particular, mi reco-
mendación sería echarle un vistazo a Python; véase www.Python.org). Si decide, a pesar de todo, utilizar el lenguaje Java,
al menos comprenderá, después de efectuado ese análisis, cuáles serán las opciones existentes y por qué resultaba conve-
niente adoptar la decisión que ftnalmente haya tomado.
Todo es un objeto
Aunque está basado en C++, Java es un lenguaje orientado a objetos más "puro".
Tanto e++ como Java son lenguajes híbridos, pero en Java los diseñadores pensaron que esa hibridación no era tan impor-
tante con en C++. Un lenguaje híbrido pennite utilizar múltiples estilos programación; la razón por la que e++ es capaz de
soportar la compatibilidad descendente con el lenguaje C. Puesto que e++ es un superconjunto del lenguaje C. incluye
muchas de las características menos deseables de ese lenguaje, lo que hace que algunos aspectos del e++ sean demasiado
complicados.
El lenguaje Java presupone que el programador sólo quiere realizar programación oricntada a objetos. Esto quiere decir que.
antes de empezar, es preciso cambiar nuestro esquema mental al del mundo de la orientación a objetos (a menos que ya
hayamos efectuado esa transición). La ventaja que se obtiene gracias a este esfuerzo adicional es la capacidad de programar
en un lenguaje que es más fácil de aprender y de utilizar que muchos otros lenguajes orientados a objetos. En este capítulo
veremos los componentes básicos de un programa Java y comprobaremos que (casi) todo en Java es un objeto.
I Este punto puede suscitar enconados debates. Ilay personas que sostienen que "claramente se lrala de un puntero", pero esto esta presuponiendo una
detenninada implemcntación subyacente. Asimismo. las referencias en Ja\a se parecen mucho mas sintacticamcnte a las referencias C++ que a los punte-
ros. En la primera edición de este libro decidi utilizar eltcmlino "descriptor" porque las referencias C++ y las referencias Java tienen diferencias notables.
Yo mismo provenía del mundo del lenguaje C++ y no quería confundir a los programadores de C++. que supon ía que constituirían la gran mayoría de per-
sonas interesadas en el lenguaje Java. En la segunda edición, decidí que "referencia" era eltémlino más comúnmente utilizado. y que cualquiera que pro-
viniera del mundo de C++ iba a enfrentarse a problemas mucho mas graves que la tenninología de las referencias. por lo que no tenía sentido usar una
palabra distinta. Sin embargo. hay personas que están en desacucrdo incluso con el ténnino "referencia". En un detemlinado libro. pude leer quc "rcsultu
completamentc equ ivocado decir que Java soporta el paso por referencia". o que los identificadores de los objetos Java (de acuerdo con el autor del libro)
son en realidad "referencias a objctos". Por lo que (continúa el autor) todo se pasa e" la práctica por valor. Según esle autor, no se efectúa un paso por
referencia, sino quc se "pasa una referencia a objeto por \'<llor". Podríamos discutir acerca de la precisión de estas complicadas explicaciones, pero creo
que el enfoque que he adoptado en este libro simpli fica la compresión del concepto sin generar ningún tipo de problema (los puristas del lenguaje po-
drían sostener que cstoy mintiendo. pero a e~o respondería que lo que estoy haciendo es proporcionar una abstracción apropiada).
24 Piensa en Java
Asimismo, el mando a distancia puede existir de manera independiente, sin necesidad de que exista una televisión. En otras
palabras, el hecho de que dispongamos de una referencia no implica necesariamente que haya un objeto conectado a la
misma. De este modo, si querernos almacenar una palabra o una frase podemos crear una referencia de tipo String:
String s;
Pero con ello sólo habremos creado la referencia a un objeto. Si decidiéramos enviar un mensaje a s en este punto, obten-
dríamos un error porque s no está asociado a nada (no hay televisión). Por tanto, una práctica más segura consiste en inicia-
lizar siempre las referencias en el momento de crearlas:
String s = "asdf" ;
Sin embargo, aquí estamos utilizando una característica especial de Java. Las cadenas de caracteres pueden inicializarse
con un texto entre comillas. Normalmente, será necesario emplear un tipo más general de inicialización para los restantes
objetos.
Esto no sólo dice: "Crea un nuevo objeto String", sino que también proporciona infonnación acerca de cómo crear el obje-
to suministrando una cadena de caracteres inicial.
Por supuesto, Java incluye una plétora de tipos predefinidos, además de String. Lo más importante es que también pode-
mos crear nuestros propios tipos. De hecho, la creación de nuevos tipos es la actividad fundamental en la programación Java,
yeso es precisamente lo que aprenderemos a hacer en el resto del libro.
para el almacenamiento en la pila (eso suponiendo que pudiéramos crear objelOs en la pila en Java, al igual que se
hace en e++).
4. Almace namiento consta nte. Los valores constantes se suelen situar directamente dentro del código de programa, lo
que resulta bastante seguro, ya que nunca varían. En ocasiones, las constantes se almacenan por separado, de fcnna
que se pueden guardar opcionalmente en la memoria de sólo lectura (ROM, read only mem01Y), especialmente en los
sistemas integrados. 2
5. Almacenamiento fuera d e la RAM. Si los datos residen fuera del programa, podrán continuar existiendo mientras
el programa no se esté ejecutando, sin que el programa tenga control sobre los mismos. Los dos ejemplos principa-
les son los objetos stream, en los que los objetos se transfonnan en flujos de bytes, generalmente para enviarlos a
otra máquina, los objetos persislenles en los que los objetos se almacenan en disco para que puedan conservar su
estado incluso después de tenninar el programa. El truco con estos tipos de almacenamiento consiste en transfonnar
los objetos en algo que pueda existir en el otro medio de almacenamiento, y que, sin embargo, pueda recuperarse
para transfonnarlo en un objeto nonnal basado en RAM cuando sea necesario. Java proporciona soporte para lo que
se denomina persistencia ligera y otros mecanismos tales como JDSC e Hibernate proporcionan soporte más sofis-
ticado para almacenar y extraer objetos util izando bases de datos.
void - - - Void
Todos los tipos numéricos tienen signo. por lo que Java no podrá encontrar ningún tipo sin signo,
El tamaño del tipo boolea n no está especificado de manera explícita; tan sólo se define para que sea capaz de aceptar los
valores literales t r ue o false,
Las clases "envoltorio" para los tipos de datos primitivos penniten definir un objeto no primitivo en el cúmulo de memoria
que represente a ese tipo primitivo, Por ejemplo:
'2 Un ejemplo sería el conjunto dc cadenas de caracteres. Todas las cadenas literales y constantes que tengan un valor de tipo carácter se toman de (anna
automática y se asignan a un almacenamiento estático especial.
26 Piensa en Java
ehar e = 'x';
Charaeter eh = new Charaeter (e ) ;
La característica de Java SES denominada allloboxing permite realizar automáticamente la conversión de un tipo primitivo
a un tipo envoltorio:
Charaeter eh = 'x';
y a la inversa:
ehar e = eh;
En un capítulo posterior veremos las razones que existen para utilizar envoltorios con los tipos primitivos.
Matrices en Java
Casi todos los lenguajes de programación soportan algún tipo de matriz. La utilización de matrices en e y e++ es peligro-
sa, porque dichas matrices son sólo bloques de memoria. Si un programa accede a la matriz fuera de su correspondiente blo-
que de memoria o utiliza la memoria antes de la inicialización (lo cual son dos errores de programación comunes), los
resultados serán impredecibles.
Uno de los principales objetivos de Java es la seguridad, por lo que en Java no se presentan muchos de los problemas que
aquejan a los programadores en e y C++. Se garantiza que las matrices Java siempre se inicialicen y que no se pueda acce-
der a ellas fuera de su rango autorizado. Las comprobaciones de rango exigen pagar el precio de gastar una pequeña canti-
dad de memoria adicional para cada matriz, así como de verificar el índice en tiempo de ejecución, pero se supone que el
incremento en productividad y la mejora de la seguridad compensan esas desventajas (y Java puede en ocasiones optimizar
estas operaciones).
Cuando se crea una matriz de objetos, lo que se crea realmente es una matriz de referencias, y cada una de esas matrices se
inicializa automáticamente con un valor especial que tiene su propia palabra clave: nuU. Cuando Java se encuentra un va lor
null, comprende que la referencia en cuestión no está apuntando a ningún objeto. Es necesario asignar un objeto a cada refe-
rencia antes de utilizarla, y si se intenta lIsar una referencia que siga teniendo el valor null, se infonnará del error en tiem-
po de ejecuc ión. De este modo, en Java se evitan los erro res comunes relacionados con las matrices.
También podemos crear una matriz de valores primitivos. De nuevo, el compilador se encarga de garantizar la inicializa-
ción, rellenando con ceros la memoria correspondiente a dicha matriz.
Hablaremos con detalle de las matrices en capítulos posteriores.
2 Todo es un objeto 27
Ámbito
La mayoría de los lenguajes procedimentales incluyen el concepto de ámbito. El ámbito detemlina tanto la visibilidad como
el tiempo de vida de los nombres definidos dentro del mismo. En e, e++ y Java, el ámbito está determinado por la coloca-
ción de las llavesn, de modo que, por ejemplo:
int x = 12;
II x
{
int q = 96;
II están disponibles tanto x como q
}
II sólo está disponible x
II q está "fuera del ámbito"
Una variable definida dentro de un ámbito sólo está disponible hasta que ese ámbito tennina.
Todo texto situado después de los caracteres '11' y hasta el final de la línea es un comentario.
El sa ngrado hace que el código Java sea más fácil de leer. Dado que Java es un lenguaje de fonnato libre. los espacios, tabu-
ladores y retornos de carro adicionales no afectan al programa resultante.
No podemos hacer lo siguiente, a pesar de que sí es correcto en C y e++:
int x 12;
El compilador nos indiearia que la variable x ya ha sido definida. Por tanto, no está permitida la posibilidad en e y e++ de
"ocultar" una variable en un ámbito más grande, porque los diseñadores de Java pensaron que esta característica hacía los
programas más co nfusos.
Esto suscita una cuestión interesante. Si los objetos continúan existiendo en Java perpetuamente, ¿qué es lo que evita llenar
la memoria y hacer que el programa se detenga? Éste es exactamente el tipo de problema que podía ocurrir en C++, y para
resolverlo Java recurre a una especie de varita mágica. Java dispone de lo que se denomina depurador de memoria, que exa-
mina todos los objetos que hayan sido creados con new y determina cuáles no tienen ya ninguna referencia que les apunte.
A continuación, Java libera la memoria correspondiente a esos objetos, de forma que pueda ser utilizada para crear otros
objetos nuevos. Esto significa que nunca es necesario preocuparse de reclamar explícitamente la memoria. Basta con crear
los objetos y, cuando dejen de ser necesarios, se borrarán automáticamente. Esto elimina un cierto tipo de problema de pro-
gramación: las denominadas "fugas de memoria" que se producen cuando, en otros lenguajes, un programador se olvida de
liberar la memoria.
Si bien es verdad que no podemos hacer que ese objeto lleve a cabo ninguna tarea útil (es decir, no le podemos enviar nin-
gún mensaje interesante) hasta que definamos algunos métodos para ese objeto.
Campos y métodos
Cuando se define una clase (y todo lo que se hace en Java es definir clases, crear objetos de esa clase y enviar mensajes a
dichos objetos), podemos incluir dos tipos de elementos dentro de la clase: campos (algunas veces denominados miembros
de datos) y métodos (en ocasiones denominadosfimciones miembro). Un campo es un objeto de cualquier tipo con el que
podemos comunicamos a través de su referencia o bien un tipo primitivo. Si es Wla referencia a un objeto, hay que inicia-
lizar esa referencia para conectarla con un objeto real (usando new, corno hemos visto anterionnente).
Cada objeto mantiene su propio almacenamiento para sus ca mpos; los campos nonnales no son compartidos entre los dis-
tintos objetos. Aquí tiene un ejemplo de una clase con algunos campos definidos:
class DataOnly
int i ¡
double d¡
boolean b¡
Esta clase no hace nada, salvo almacenar una se rie de datos. Sin embargo, podemos crear un objeto de esta clase de la forma
siguiente:
DataOnly data = new DataOnly()¡
Podemos asignar va lores a los campos, pero primero es preciso saber cómo referimos a un miembro de un objeto. Para refe-
rimos a él , es necesario indicar e l nombre de la referencia al objeto, seguida de un punto y seguida del nombre del miem-
bro concreto dentro de l objeto:
objectReference.member
Por ejemplo:
2 Todo es un objeto 29
data.i 47,
data.d 1.1 ;
data.b false;
También es posible que el objeto conten ga otros objetos, que a su vez contengan los datos que queremos modificar. En este
caso, basta con utilizar los pWltos correspondientes, como por ejemplo:
rnyPlane.leftTank.capacity = 100 ;
La clase DataOnly no puede hacer nada más que almacenar datos, ya que no di spone de nin gú n método. Para comprender
cómo funcionan los métodos es preciso entender primero los conceptos de argumentos y valores de retorno, que vamos a
describir en breve.
boolean fa lse
byte (byte)O
shol1 (,hOI1)O
int O
long OL
float O.Of
double O.Od
Los va lores predetenninados son sólo los va lores que Java garantiza cuando se emplea la variable como miembro de lI11a
clase. Esto garantiza que las variables miembro de tipo primitivo siempre sean inicializadas (a lgo que e++ no hace), redu-
ciendo así un a fuente de posibles errores. Sin embargo, este va lor inicial puede que no sea correcto o ni siquiera legal para
el program a que se esté escribi endo. Lo mejor es inicializa r las variabl es siempre de forma explícita.
Esta garantía de inicialización no se ap lica a las variables locales, aquellas que no son campos de clase. Por tanto, si den-
tro de la definición de un método tuviéramos:
int x;
Entonces x adoptaría algún valor arbitrario (como en e y C++), no siendo automáticamente inicializada con el va lor cero.
El programador es el responsable de asignar un valor apropiado antes de usar x. Si nos olvidamos, Java representa de todos
modos una mejora con respecto a C++, ya que se obtiene un error en tiempo de compi lación que nos informa de que la varia-
ble puede no haber sido inicializa (muchos co mpiladores de C++ nos advierten acerca de la existencia de variables no ini-
cializadas, pero en Java esta fa lta de inicialización constituye un error).
El tipo de retomo describe el valor devue lto por e l método después de la invocación . La lista de argumentos proporciona la
lista de los tipos y nombres de la infonnación que hayamos pasado al método. El nombre del método y la lista de argumen-
tos (que f0n11an lo que se denomina signaTUra del método) identifican de fanna univoca al mérodo.
Los métodos pueden crearse en Java únicamente como parte de una clase. Los métodos sólo se pueden invocar para un
objeto 3, y dicho objeto debe ser capaz de ejecutar esa invocación del método. Si tratamos de invocar un método correcto
para un objeto obtendremos un mensaje de crror en tiempo de compi lación. Para invocar un mélOdo para un objeto, hay que
nombrar el objeto seguido de un punto. seguido del nombre del método y de su lista de argumentos, como por ejemplo:
Por ejemplo, suponga que tenemos un método f( ) que no tiene ningún argumento y que devuelve una valor de tipo int.
Entonces, si tuviéramos un objeto denominado a para el que pudiera invocarse f( ) podríamos escribir:
int x = a.f O i
La lista de argumentos
La lista de argumentos del método especifica cuál es la infonnación que se le pasa al método. Como puede suponerse, esta
información (como todo lo demás en Java) adopta la f0n11a de objetos. Por tanto. lo que hay que especificar en la lis ta de
argumentos son los tipos de los objetos que hay pasar y el nombre que hay que utilizar para cada uno. Como en cualquier
otro caso dentro de Java en el que parece que estuviéramos gestionando objetos. lo que en realidad estaremos pasando son
referencias4 . Sin embargo. el tipo de la referencia debe ser correcto. Si el argumento es de tipo Strin g, será preciso pasar un
objeto Strin g. porque de lo contrario el compilador nos daría un error.
Supongamos un cierto método que admite un objeto Strin g como argumento. Para que la compilación se realice correcta-
mente. la definición que habría que incluir dentro de la definición de la clase sería como la siguiente:
int storage (String s )
return s . length () * 2 i
Este método nos dice cuántos bytes se requieren para almacenar la infon118ción contenida en un objeto Strin g concreto (el
tamaiio de cada char en un objeto Stri ng es de 16 bits, o dos bytes, para soponar los caracteres Unicode). El argumento es
de tipo String y se denomina s. Una vez que se pasa s al método, podemos tratarlo como cualquier otro objeto (pode mos
enviarle mensajes). Aquí, lo que se hace es invocar el método lengt h(), que es uno de los métodos definidos para los obje-
tos de tipo Strin g; este método devuelve el número de caracteres que hay en una cadena.
También podemos ve r en el ejemplo cómo se emplea la palabra clave return . Esta palabra clave se encarga de dos cosas:
en primer lugar. quiere decir "sal del método, porque ya he tenninado". en segundo lugar, si el método ha generado un va lor,
ese valor se indica justo a continuación de una instrucción return , en este caso, el valor de retomo se genera evaluando la
expresión s. length () * 2.
Podemos devolver un valor de cualquier lipo que desee mos, pero si no queremos devolver nada, tenemos que especificar-
lo, indicando que el método devuelve un valor de tipo vo id . He aquí algunos ejemplos:
) Los métodos de tipo statie. de los que pronto hablaremos. pueden invocarse para la e/ase, en lugar de para un objeto especifico .
.¡Con la e.xcepeión usual de los tipos de datos "especiales" que antes hemos mencionado: boolean, ehar, byte, short, int , long, nou t y double. En gene-
ral, sin embargo. to que se pasan son objetos, lo que realmente quiere decir que se pasan referencias a objetos.
2 Todo es un objeto 31
Cuando el tipo de retorno es void, la palabra clave return se utiliza sólo para salir del método, por lo que resulta necesaria
una vez que se alcanza el final del método. Podemos volver de un método en cualquier punto, pero si el tipo de retomo es
distinto de "oid, entonces el compilador nos obligará (mediante los apropiados mensajes de error) a devolver un valor del
tipo apropiado, independientemente del lugar en el que salgamos del método.
Llegados a este punto, podría parecer que un programa consiste, simplemente, en una se rie de objetos con métodos que acep-
tan otros objetos como argumentos y envían mensajes a esos otros objetos. Realmente. esa descripción es bastante precisa,
pero en el siguiente capítulo veremos cómo llevar a cabo el trabajo detallado de bajo nivel, tomando decisiones dentro de
un método. Para los propósitos de este capítulo, el envío de mensajes nos resultará más que suficiente.
Para resolver este problema, es preciso eliminar todas las ambigüedades potenciales. Esto se consigue infonnando al com-
pilador Ja va de exactamente qué clases se desea emplear, utilizando para ello la palabra clave importo import le dice al
compilador que cargue un paquete, que es una biblioteca de clases (en otros lenguajes, una biblioteca podría estar compues-
ta por funciones y datos, así como clases, pero recuerde que todo el código Java debe escribirse dentro de una clase).
La mayor parte de las veces utilizaremos componen tes de las bibliotecas estándar Java incluidas con el compilador.
Con estos componentes, no es necesario preocuparse sobre los largos nombres de dominio invertidos, basta con decir, por
ejemplo:
import java .util.ArrayList¡
para infonnar al compilador que queremos utilizar la clase ArrayList de Java. Sin embargo, util contiene di versas clases,
y podernos usar varias de eUas sin declararlas todas ellas explícitamente. Podemos conseguir esto fácilmente utilizando '*'
como comodín:
import java.util.*;
Resulta más común importar una colección de clases de esta fonna que importar las clases individualmente.
Ahora, aunque creáramos dos objetos StaticTest, existiría un único espacio de almacenamiento para StaticTest.i. Ambos
objetos compartirían el mismo campo i. Considere el fragmento de código siguiente:
StaticTest stl new StaticTest() ¡
StaticTest st2 new StaticTest();
En este punto, tanto st1.i corno st2.i tienen el mismo va lor, 47, ya que ambos hacen referencia a la misma posición de
memoria.
5Por supuesto, puesto que los métodos slatic no necesitan que se cree ningún objeto antes de utilizarlos, no pueden acceder directamente a ningún miem-
bro o método que no sea stalic, por el procedimiento de invocar simplemente esos otros miembros sin hacer referencia a un objeto nominado (ya que los
miembros y métodos que no son sto tic deben estar asociados con un objeto concreto).
2 Todo es un objeto 33
Hay dos fannas de hacer referencia a una variable sta tic. Como se indica en el ejemplo anterior, se la puede expresar a tra-
vés de un objeto, escribiendo por ejemplo, st2.i. Podemos hacer referencia a ella directamente a través del nombre de la
clase, lo que no puede hacerse con ningún nombre que no sea de tipo sta tic.
StaticTest.i++¡
El operador ++ incrementa en una unidad la variable, En este punto, tanto st1.i como st2.i tendrán el valor 48.
La fomla preferida de hacer referencia a una variable stane es utilizando el nombre de la clase. Esto no sólo pennite enfa-
tizar la naturaleza de tipo static de la variable, sino que en algunos casos proporciona al compilador mejores oportunidades
para la optimización.
A los métodos statie se les ap lica una lógica similar. Podemos hacer referencia a un método statie a través
de un objeto, al igual que con cualquier otro método, o bien mediante la sintaxis adicional especial NombreClase.meto-
do(). Podemos definir un método stane de manera a si milar:
class Incrementable {
static void increment () { StaticTest. i++; }
Podemos ver que e l método inerement() de lnerementable incrementa el dato i de tipo static utilizando el operador ++.
Podemos invocar increment( l de la forma tipica a través de un objeto:
Incrementable sf = new Incrementable();
sf.increment() ;
0, dado que inerement( ) es un método statie, podemos invocarlo directamente a través de su clase:
Incrementable.increment () ;
Aunque statie, cuando se aplica a un campo, modifica de manera evidente la fonna en que se crean los datos (se crea un
dato global para la clase, a diferencia de los campos que no son sta tic, para los que se crea un campo para cada objeto),
cuando se aplica esa palabra clave a un método no es tan dramático. Un uso importante de static para los métodos consis-
te en pemlitimos invocar esos métodos sin necesidad de crear un objeto. Esta característica resulta esencial, como veremos,
al definir el método main( l, que es el punto de entrada para ejecutar una aplicación.
Al principio de cada archivo de programa, es necesario incluir las instmcciones import necesarias para cargar las clases adi-
cionales que se necesiten para el código incluido en ese archivo. Observe que hemos dicho "clases adicionales". La razón
es que existe una cierta biblioteca de clases que se carga automáticamente en todo archivo Java: java.lang. Inicie el explo-
rador web y consulte la documentación de Sun (si no ha descargado la documentación del kit IDK de hllp://java.slIn.com,
hágalo ahora. 6 Observe que esta documentación no se suministra con el IDK; es necesario descargarla por separado). Si con-
sulta la lista de paquetes, podrá ver todas las diferentes bibliotecas de clases incluidas con Java. Seleccione java.lang; esto
hará que se muestre una lista de todas clases que fonnan parte de esa biblioteca. Puesto que java.lang está implícitamente
6 El compilador Java y la documentación suministrados por Sun tienden a cambiar de manera frecuente , y el mejor lugar para obtenerlos cs directamente
en el sitio web de Sun. Descargandose esos archivos podrá disponer siempre de la versión más reciente.
34 Piensa en Java
en todo archivo de código Java. dichas clases estarán disponibles de manera automática. No existe ninguna clase Date en
java.lang, lo que quiere decir que es preciso importar otra biblioteca para usa r dicha clase. Si no sabe la biblioteca en la que
se encuentra una clase concreta o si quiere ver todas las clases disponibles, puede seleccionar "Tree" en la documentación
de Java. Con eso. podrá localizar todas las clases incluidas con Java. Entonces, utilice la función "Buscar" del explorador
para encontrar Date. Cuando lo haga. podrá ver que aparece como java.util.Date, lo que nos permite deducir que se encuen-
tra en la biblioteca util y que es necesario emplear la instrucción import java.util.* para usar Date.
Si vuelve atrás y selecciona java.lang y luego System , podrá ver que la clase System tiene varios campos; si selecciona
out, descubrirá que se trata de un objeto statie PrintStream. Puesto que es de tipo stane, no es necesario crear ningún obje-
to empleando new. El objeto out está siempre disponible, por lo que podemos usarlo directamente. Lo que puede hacerse
con este objeto out está detenninado por su tipo: PrintStream . En la descripción se muestra PrintStream como un hiper-
vínculo, por lo que al hacer c1ic sobre él podrá acceder a una lista de todos los métodos que se pueden invoca r para
PrintStream. Son bastantes métodos, y hablaremos de ellos más adelante en el libro. Por ahora, el único que nos interesa
es println(), que en la práctica significa "imprime en la consola lo que te vaya pasar y termina con un carácter de avance
de línea". Asi. en cualquier programa Ja va podemos escribir algo corno esto:
System.out.println("Una cadena de caracteres");
cuando queramos mostrar infonnación a través de la consola.
El nombre de la clase coincide con el nombre del archivo. Cuando estemos creando un programa independiente como éste,
una de las clases del archivo deberá tener el mismo nombre que el propio archivo (el compilador se quejará si no lo hace-
mos así). Dicha clase debe contener un método denominado main( ) con la siguiente signarura y tipo de retorno:
public static void main(String[] args)
La palabra clave public quiere decir que el método está disponible para todo el mundo (lo que describiremos en detalle en
el Capínlio 6, Control de acceso). El argumento de main( ) es una matriz de objetos String. En este programa, no se em-
plean los argumentos (args), pero el compilador Java insiste en que se incluya ese parámetro. ya que en él se almacenarán
los argumentos de la línea de comandos.
La línea que imprime la fecha es bastante interesante:
System.out .print ln(new Date(»);
El argumento es un objeto Date que sólo se ha creado para enviar su valor (que se convierte automáticamente al tipo String)
a println(). Una vez finalizada esta inslnlcción, dicho objeto Date resulta innecesario. y el depurador de memoria podrá
encargarse de él en cualquier momento. Nosotros no necesitamos preocupamos de borrar el objeto.
Al examinar la documentación del JDK en hflp://java.sl/tI.com, podemos ver que System tiene muchos métodos que nos
permiten producir muchos efectos interesantes (una de las características más potentes de Java es su amplio conjunto de
bibliotecas estándar). Por ejemplo:
//: object/ShowProperties.java
Compilación y ejecución
Para compilar y ejecutar este programa. y cualquier otro programa de este libro, en primer lugar hay que tener un entorno
de programación Java. Hay disponibles diversos entornos de programación Java de Olros fabricantes, pero en el libro vamos
a suponer que el lector está utilizando el kit de desarrollo JDK (Java Developer's Kit) de Sun, el cual es gratuito. Si está uti-
lizando otro sistema de desarrollo 7 , tendra que consu ltar la documentación de dicho sistema para ver cómo compila r y eje-
cutar los programas.
Acceda a Internet y vaya a hftp://java.sun.com. Allí podrá encontrar infon113ción y vínc ulos que le llevarán a través del pro-
ceso de descarga e instalación del JDK para su platafonna concreta.
Una vez instalado el JDK, y una vez modificada la infonnación de ruta de la computadora para que ésta pueda encontrar
javac y java , descargue y desempaquete el código fuente correspondiente a este libro (puede encontrarlo en
WH'l I'. MindView.nel). Esto creará un subdirectorio para cada capítulo del libro. Acceda a l subdirectorio objects y escriba :
javac HelloDate.java
Este comando no debería producir ninguna respuesta. Si obtiene algún tipo de mensaje de error querrá decir que no ha ins-
talado el JDK correctamente y tendrá que investigar cuál es el problema.
Por el contrario, si lo único que puede ver después de ejecutar el comando es una nueva línea de comandos. podrá escribir:
java HelloDate
Puede utilizar este proceso para com pilar y ejecutar cada uno de los programas de es te libro. Sin embargo, podrá ver que el
código fuente de este libro también di spone de un archivo denominado build.xml en cada capítulo, que contiene comandos
"Ant" para construir automáticamente los archivos correspondientes a ese capítulo. Los arch.ivos de generación y Ant (inc lu-
yendo las instrucciones para descargarlos) se describen más en detalle en el suplemento que podrá encontrar en
hllp:// MindVielV.net/ Books/BellerJava, pero una vez que haya instalado Ant (desde hllp://jakar¡a.apache. org/al1t) basta con
que escriba ' anf en la línea de comandos para compi lar y ejecutar los programas de cada capítulo. Si no ha instalado Ant
roda vía, puede escribir los comandos javac y java a mano.
El segundo tipo de comentario proviene de C++. Se trata de l comentario monolínea que comienza con 11 y continúa ha sta
el fina l de la línea. Este tipo de comentario es bastante cómodo y se suele utili zar a menudo debido a su sencillez. No es
necesari o desplazarse de un sitio a otro del teclado para encontrar e l carácter / y luego el carácter * (e n su lugar, se pu lsa
la misma tecla dos veces), y tampoco es necesario cerrar el c-omentari o. Así que resulta bastante habinlal encontrar comen-
tarios de este estil o:
II Éste es un comentario monolínea.
7 El compilador "jikes" de IHM es una alternativa bastante común, y es significativamente más rápido que Java e de $un (aunque se están construyendo
gnlpoS de archivos utilizando AIIf, la diferencia no es muy significativa). También hay diversos proyectos de código fuente abierto para crear compilado-
res Java, entornos de ejecución y bibliOlecas.
36 Piensa en Java
Sintaxis
Todos los comandos de lavadoc están incluidos en comentari os que ti enen el marcador 1**. Esos comentari os tenninan con
*/ como es habitual. Existen dos fonna s principales de utili zar Javadoc: mediante HTML embebido o medi ante " marcado-
res de documentación". Los marcadores de documentación autónomos so n comandos que comienzan con '@' y se incluyen
al principio de una linea de comentarios (sin embargo, un carácter '*' inicial será ignorado) . Los marcadores de documen-
tación en línea pueden incluirse en cualquier punto de un comentario Javadoc y también com ienzan con '@'. pero están
encerrados entre llaves.
Existen tres "tipos" de documentación de comentarios. que se corresponden con el elemento al que el comentario precede:
una clase, un campo o un método. En otras palabras, el comentario de clase aparece justo antes de la definición de la clase,
el comentario de campo aparece justo antes de la definición de un campo y el comentario de un método apa rece justo antes
de la definición del mismo. He aquí un senci llo ejemplo:
11: object/Documentationl.java
1** Comentario de clase * /
public class Documentationl {
1** Comentario de campo *1
public int i i
1** Comentario de método *1
public void f () {}
/11 , -
Observe que Javadoc só lo procesará la documentación de los come ntarios para los miembros public y
protected . Los comentarios para los miembros private y los miembros con acceso de paquete (véase el Capítulo 6, Control
de acceso) se ignoran, no generándose ninguna salida para ellos (si n embargo, puede utili zar el indicador private para incluir
también la documentación de los miembros private). Esto tiene bastante sentido, ya que sólo los miembros de tipo public y
protected están disponibles fuera del archivo, por lo que esto se ajusta a la perspectiva del programador de clientes.
La salida del código anteri or es un archivo HTML que ti ene el mi smo fonnato estándar que el resto de la documentación
lava, por lo que los usuarios estarán acostumbrados a ese fonnato y podrán na vegar fác ilmente a través de las clases que
2 Todo es un objeto 37
hayamos definido. Merece la pena introducir el ejemplo de código anterior, procesarlo mediante Javadoc y observar el
arch ivo HTML resultante, para ver el tipo de salida que se genera.
HTML embebido
Javadoc pasa los comentarios HTML al documento HTML generado. Esto nos pennite utilizar completamente las caracte-
rísticas HTML; sin embargo, el motivo principal de uso de ese lenguaje es dar formato al código, como por ejemplo en:
/1 : object/Documentation2.java
/ **
* <pre>
* System.out.println (new Date ()) ;
* </pre>
*/
/1/ , -
También podemos usar código HTML como en cualquier otro documento web, para dar faonato al texto nomlal de las des-
cripciones:
ji : object / Documenta t ion3. j ava
/ **
* Se puede <em>inc!uso< / em> insertar una lista :
* <01:;.
* <li:;. Elemento uno
* <li:;. Elemento dos
* <li:;. Elemento tres
* </ 01>
*/
11/ , -
Observe que, dentro del comentario de documentación, los asteriscos situados al principio de cada línea son ignorados por
Javadoc, junto con los espacios iniciales. Javadoc refonnatea todo para que se adapte al estilo estándar de la documenta-
ción. No utilice encabezados como <h l > o <hr> en el HTML embebido, porque Javadoc inserta sus propios encabezados
y los que nosotros incluyamos interferirán con ellos.
Puede utilizarse HTML embebido en todos los tipos de comentarios de documentación : de clase, de campo y de método.
@see
Este marcador pennite hacer referencia a la documentación de otras clases. Javadoc generará el código HTML necesario,
hipervinculando los marcadores @see a los olros fragmentos de documentación. Las fom13s posibles de uso de este marca-
dor son:
@see nombreclase
@see nombreclase-completamente-cualificado
@see nombreclase-completamente-cualificado #nombre-método
Cada uno de estos marcadores añade una entrada "See Also" (Véase también) hipervinculada al archivo de documentación
generado. Javadoc no comprueba los hipervínculos para ver si son válidos.
{@docRoot}
Genera la mta relativa al direclOrio raíz de la doc umentaci ón. Resulta útil para introducir hipervínculos explícitos a páginas
del árbol de documentación.
{@inheritDoc}
Este indicador hereda la documentación de la clase base más próxima de esta clase, insertándola en el comentario del docu-
mento actual.
@version
Tiene la fonna:
®Version información -versi ón
en el que información-versión es cualquier infonnación significativa que queramos incluir. Cuando se añade el indicador
@ version en la línea de comandos Javadoc, la infonnación de vers ión será mostrada de manera especial en la documenta-
c ión HTM L generada.
@author
Tiene la fonna:
@au thor información-autor
donde información-autor es, normalmente, nuestro nombre, aunque también podríamos incluir nuestra dirección de correo
e lectrónico o cualquier otra información apropiada. Cuando se incluye el indicador @ author en la línea de comandos
Javadoc, se inserta la infonnación de autor de manera especial en la documentación HTML generada.
Podemos incluir múltiples marcadores de autor para incluir una lista de autores, pero es preciso poner esos marcadores de
forma consecu tiva. Toda la información de autor se agrupará en un único párrafo dentro del código HTML generado.
@since
Este marcador pem1ite indicar la versión de l código en la que se empezó a utilizar una característica concreta. En la docu-
mentación HTML de Java, se emplea este marcador para indicar qué versión del JDK se está utilizando.
@param
Se utiliza para la documentación de métodos y tiene la fonna:
@param nombre-parámetro descripción
donde nombre-parámetro es el identificador dentro de la lista de parámetros del método y descripción es un texto que
puede continuar en las líneas siguientes. La descripción se considera tenninada cuando se encuentra un nuevo marcador de
documentación. Puede haber múltiples marcadores de este tipo, nonnalmente uno por cada parámetro.
@return
Se utiliza para documentación de métodos y su fonnato es el siguiente:
@return descripción
donde descripción indica el significado del valor de retomo. Puede conti nuar en las lineas siguientes.
@throws
Las excepciones se estudian en el Capítulo 12, Tratamiento de errores mediante ex.cepciones. Por resumir, se trata de obje-
tos que pueden ser "generados" en un método si dicho método falla. Aunque sólo puede generarse un objeto excepción cada
vez que invocamos un método, cada método concreto puede generar diferentes tipos de excepciones, todas las cuales habrá
que describir, por lo que e l formato del marcador de excepción es:
@throws nombre-clase-completamente-cualificado descripción
2 Todo es un objeto 39
@deprecated
Se utili za para indicar característi cas que han quedado obsoletas debido a la introducción de alguna otra característica me-
jorada. Este marca dor indicativo de la obsolescencia recomienda que no se utilice ya esa característica concreta, ya que es
probable que en e l futuro sea eliminada. Un método marcado como @ de precated hace que el compi lador genere una adver-
tenc ia si se usa. En Java SE5, el marcador Javadoc @ depreca ted ha quedado susti tuido por la ano/ación @ Dcpreca ted
(hablaremos de este tema en el Capitulo 20, Anoraciones).
Ejemplo de documentación
He aquí de nuevo nuestro primer programa Ja va, pero esta vez con los co mentarios de documentación inc luidos:
JI : object/HelloDate . java
import java.util .*;
/ * Ou tput: (5 5% match)
HelIo, it's:
Wed Oet 05 14:39:36 MDT 2005
* /// , -
La primera línea del archivo utiliza un a técnica propia del autor del libro que consiste en incluir '//:' como marcador espe-
cial en la línea de comentarios que contiene el nombre del archivo fue ntc. Dicha línea contiene la infonn3ción de ruta del
archivo (object indica este capítulo) seguida del nombre del archivo. La última línea también teml ina con un comentario, y
éste ('///:- ') indica el final del listado de código fuente, lo que pem1ite actualizarlo automáti camente dentro del texto de este
libro después de comprobarlo con un compilado r y ejecutarlo.
El marcador 1* O utp ut: indica el comienzo de la salida que generará este archi vo. Con esta fanna concreta, se puede com-
proba r automáticamente para ve rifi car su precisión. En este caso, el texto (550/0 matc h) indica al sistema de pruebas que la
salida se rá bastante distinta en cada sucesiva ej ecución de l programa, por lo que sólo cabe esperar un 55 por ciento de corre-
lación con la salida que aquí se mu estre. La mayoría de los ejemplos del li bro que generan una salida contendrán dicha sali-
da comentada de esta fonma, para que el lector pueda ver la salida y saber que lo que ha obtenido es correcto.
Estilo de codificación
El estilo descrito en el manual de convenios de código para Java, Cade Convenlions jor rhe Java Programming Language 8,
consiste en poner en mayúsc ula la primera letra del nombre de una clase. Si e l nombre de la clase está compuesto por varias
¡¡Para ahorrar espacio tanto en el libro como en las presentaciones de los seminarios no hemos podido seguir todas las directrices que se marcan en ese
texto. pero el lector podrá ver que el estilo empleado en el libro se ajusta lo máximo posible al estándar recomendado en Java.
40 Piensa en Java
palabras, éstas se escriben juntas (es decir, no se emplean guiones bajos para separarlas) y se pone en mayúscula la prime-
ra letra de cada una de las palabras integrantes, como en:
class AIITheColorsOfTheRainbow { II ...
Para casi todos los demás elementos, como por ejemplo nombres de métodos, campos (variables miembro) y referencias a
objetos, el estilo aceptado es igual que para las clases, salvo porque la primera letra del identificador se escribe en minús-
cula, por ejemplo:
class AIITheColorsOfTheRainbow (
int anlntegerRepresentingColors¡
void changeTheHueOfTheColor(int newHue )
II ...
)
II .. .
Recuerde que el usuario se verá obligado a escribir estos nombres tan largos, así que procure que su longitud no sea exce-
siva.
El código Ja va que podrá encontrar en las bibliotecas Sun también se ajusta a las directrices de ubicación de llaves de aper-
tura y cierre que hemos utilizado en este libro.
Resumen
El objetivo de este capítulo es explicar los conceptos mínimos sobre Java necesarios para entender cómo se escribe un pro-
grama sencillo. Hemos expuesto una panorámica del lenguaje y algunas de las ideas básicas en que se fundamenta. Sin
embargo, los ejemplos incluidos hasta ahora tenían todos ellos la fonna "haga esto, luego lo otro y después lo de más allá".
En los dos capítulos siguientes vamos a presentar los operadores básicos utili zados en los programas Java, y luego mostra-
remos cómo controlar el flujo del programa.
Ejercicios
Normalmente, distribuiremos los ejercicios por todo el capítulo, pero en éste estábamos aprendiendo a escribir programas
básicos, por lo que decidimos dejar los ejercicios para el final.
El número entre paréntesis incluido detrás del número de ejercicio es un indicador de la dificultad del ejercicio dentro de
una escala de 1-10.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tire Thinking in Java Annofafed
Solution Guide, a la venta en wwv.~ MindView. nef.
Ejercicio 1: (2) Cree una clase que contenga una variable int y otra char que no estén inicializadas e imprima sus valo-
res para verificar que Java se encarga de realizar una inicialización predetenninada.
Ejercicio 2: (1) A partir del ejemplo HelloDate.java de este capítulo, cree un programa "helio, world" que simplemen-
te muestre dicha frase. Sólo necesitará un método dentro de la clase, (el método "main" que se ejecuta al
arrancar el programa). Acuérdese de defUlir este método como static y de incluir la lista de argumentos,
incluso aunque no la vaya a utilizar. Compile el programa con javac y ejecútelo con java. Si está utilizan-
do otro entorno de desarrollo distinto de JDK, averigüe cómo compilar y ejecutar programas en dicho
entorno.
Ejercicio 3: (1) Recopile los fragmentos de código relacionados con ATypeNa me y transfónnelos en un programa que
se pueda compilar y ejecutar.
Ejercicio 4: (1) Transforme los fragmentos de código DataOnly en un programa que se pueda com pilar y ejecutar.
Ejercicio 5: (1) Modifique el ejercicio anterior de modo que los va lores de los datos en DataOnly se asignen e impri-
man en maine ).
Ejercicio 6: (2) Escriba un programa que incluya e invoque el método storage( ) definido como fragmento de código
en el capítulo.
2 Todo es un objeto 41
Ejercicio 12: (2) Localice el código para la segunda versión de HelloDate.java, que es el ejemplo simple de documen-
tación mediante comentarios. Ejecute Javadoc con ese archivo y co mpruebe los resultados con su explo-
rado r web.
Ejercicio 13: ( 1) Procese Documentationl.java, Documentalion2.java y Documentation3.java con Javadoc. Veri-
fique la documentación resultante con su explorador web.
Ejercicio 14: ( 1) Añada una lista HTML de elementos a la documentación del eje rcicio anterior.
Ejercicio 15: (1) Tome el programa de l Ejercicio 2 y ailádale comen tarios de documentación. Extraiga esos comentarios
de documentación Javadoc para generar un archi vo HTML y visua líce lo con su exp lorador web.
Ejercicio 16: ( 1) En el Capítulo 5, Inicialización y limpieza, localice el ejemplo Overloading.java y añada documenta-
ción de tipo Javadoc. Extraiga los comentarios de documentación Javadoc para generar un archi vo HTML
y vis ualícelo con su explorador web.
Operadores
Puesto que Java surgió a partir de C++, la mayoría de estos operadores les serán familiares a casi todos los programadores
de C y C++. Java ha añadido también algunas mejoras y ha hecho algunas simplificaciones.
Si está familiarizado con la sintaxis de e o e++, puede pasar rápidamente por este capítulo y el siguiente, buscando aque-
llos aspectos en los que Ja va se diferencie de esos lenguajes. Por el contrario, si le asaltan dudas en estos dos capítulos,
repase el seminario multimedia Thinking in e, que puede descargarlo gratuitamente en www.MindView.nef. Contiene confe-
rencias, presentaciones, ejercicios y soluciones específicamente diseñados para familiarizarse con los fundamentos necesa-
rios para aprender Java.
Puede observar que esto no es sólo un montón de texto que escribir (10 que quiere decir muchos movimientos redundantes
de los dedos), sino que también resulta bastante incómodo de leer. La mayoria de los lenguajes, tanto antes como después
de Java, han adoptado una técnica mucho más sencilla para expresar esta instrucción de uso tan común.
En el Capítulo 6, Control de acceso se presenta el concepto de importación estática añadido a Java SES , y se crea una peque-
ña biblioteca que pennite simplificar la escritura de las instrucciones de impresión. Sin embargo, no es necesario compren-
der los detalJes para comenzar a utilizar dicha biblioteca. Podemos reescribir el programa del último capítulo empleando esa
nueva biblioteca:
jj: operators/HelloDate.java
import java.util.·;
import static net . mindview.util.Print.*;
Aunque la utilización de net.rn indview.util.Pri nt simplifica en0n11emente la mayor parte del código, su uso no está justi-
ficado en todas las ocasiones. Si sólo hay unas pocas instrucciones de impresión en un programa, es preferible no incluir la
instrucción import y escribir el comando completo System.out.println().
Eje rc ic io 1: (1) Escriba un programa que emplee tanto la faona "corta" como la norlnal de la instrucción de impresión.
Precedencia
La precedencia de los operadores define la fom13 en que se evalúa una expresión cuando hay presentes varios operadores.
Java tiene una serie de reglas específicas que determinan el orden de evaluación. La más fácil de recordar es que la mu lti-
plicación y la división se realizan antes que la suma y la resta. Los programadores se olvidan a menudo de las restantes
reglas de precedencia, así que conviene uti lizar paréntesis para que el orden de evaluación esté indicado de manera explíci-
ta. Por ejemplo, observe las instrucciones (1) y (2):
11 : operators/Precedence.java
1* Output:
a = S b = 1
* /// ,-
Estas instrucciones ti enen aspecto similar, pero la sa lida nos muestra que sus significados son bastante distintos, debido a la
utili zación de paréntesis.
Observe que la instrucción System.out.prilltln() incluye el operador '+'. En este contexto, '+' significa "concatenación de
cadenas de caracteres" y, en caso necesario, "conversión de cadenas de caracteres." Cuando el compilador ve un objeto
Stri ng seguido de '+' seguido de un objeto no String, intenta convertirlo a un objeto St ri ng. Como puede ver en la salida,
se ha efectuado correctamente la conversión de int a String pam a y b.
Asignación
La asignación se realiza con el operador =. Su significado es: " Toma el valor del lado derecho, a menudo denominado rva-
lor. y cópialo en e l lado izquierdo (a menudo denominado Ivalor)". Un rvalor es cualquier constante, variable o expresión
que genere un valor, pero un lvalor debe ser una variable detenn inada, designada mediante su nombre (es decir, debe exis-
tir un espacio fisico para almacenar el va lor). Por ejemplo, podemos asignar un valor constante a una variable:
a = 4¡
3 Operadores 45
pero no podemos asignar nada a un valor constante, ya que una constante no puede ser un ¡valor (no podemos escribir
4 = a;).
La asignación de primitivas es bastante sencilla. Puesto que la primitiva almacena el valor real y no una referencia a cual-
quier objeto, cuando se asignan primitivas se asigna el contenido de un lugar a otro. Por ejemplo, si escribimos a = b para
primitivas, el contenido de b se copia en a. Si a continuación modificamos a, el valor de b no se verá afectado por esta modi-
ficación. Como programador es lo que cabría esperar en la mayoría de las situaciones.
Sin embargo, cuando asignamos objetos, la cosa cambia. Cuando manipularnos un objeto lo que manipulamos es la referen-
cia, asi que al as ignar" de un objeto a otro", lo que estamos haciendo en la práctica es copiar una referencia de un lugar a
otro. Esto significa que si escribimos e = d para sendos objetos, lo que al final tendremos es que tanto e corno d apuntan al
objeto al que sólo d apuntaba originalmente. He aquí un ejemplo que ilustra este comportamiento:
11 : operators / Assignment.java
II La asignación de objetos tiene su truco.
import static net.mindview.util.Print.*¡
class Tank {
int level;
1* Output:
1: tl.level: 9, t2.level: 47
2: tI.level: 47, t2.level: 47
3 : tI.level: 27, t2.1evel: 27
* /// , -
La clase Tank es simple, y se crean dos instancias de la misma (11 y t2) dentro de main(). Al campo level de cada objeto
Tank se le asigna un valor distinto, luego se asigna t2 a tl , y después se modifica tI. En muchos lenguajes de programación
esperaríamos que tl y t2 fueran independientes en todo momento, pero como hemos as ignado una referencia, al cambiar el
objeto t1 se modifica también el objeto t2. Esto se debe a que tanto tI como t2 contienen la misma referencia, que está apun-
tada en el mismo objeto (la referencia original contenida en tI, que apuntaba al objeto que tenía un valor de 9, fue sobrees-
crita durante la asignación y se ha perdido a todos los efectos; su objeto será eliminado por el depurador de memoria).
Este fenómeno a menudo se denomina creación de alias, y representa Wla de las características fundamentales del modo en
que Java trabaja con los objetos. Pero, ¿qué sucede si no queremos que las dos referencias apunten al final a un mismo obje-
to? Podemos reescribir la asignación a otro nivel y utilizar:
tI.level = t2.level¡
Esto hace que se mantengan independientes los dos objetos, en lugar de descartar uno de ellos y asociar tI y t2 al mismo
objeto. Más adelante veremos que manipular los campos dentro de los objetos resulta bastante confuso y va en contra de los
principios de un buen diseño orientado a objetos. Se trata de un tema que no es nada trivial, así que tenga siempre presente
que las asignaciones pueden causar sorpresas cuando se manejan objetos.
Ejercicio 2 : (1) Cree una clase que contenga Wl valor float y utilicela para ilustrar el fenómeno de la creación de alias.
46 Piensa en Java
class Letter
char c¡
/ * Output:
1: x.e: a
2: x.e: z
* /// ,-
En muchos lenguajes de programación, el método f( ) haría una copia de su argumento Lettcr dentro del ámbito del méto-
do, pero aquí, una vez más, lo que se está pasando es una referencia. por lo que la línea:
Operadores matemáticos
Los ope radores matemáticos básicos son iguales a los que hay di sponibles en la mayoría de los lenguajes de programación:
suma (+), resta (-), di visión (f), multiplicación (*) y módulo (%, que genera el resto de una divi sión entera). La división
entera trunca en lugar de redondear el resultado.
Java también utili za la notación abreviada de e/e++ que realiza una operación y una asignación al mismo tiempo. Este tipo
de operación se denota medi ante un operador seguido de un signo de igual, y es coheren te con todos los operadores del len-
guaje (allí donde tenga sentido). Por ejemplo, para sumar 4 a la variable x y asignar el resultado a x, uti lice: x += 4.
Este ejemplo muestra el uso de los operadores matemáticos:
11 : eperaters / MathOps.java
JI Ilustra los operadores matemáticos.
import java.util. *;
impert static net . mindvie w. util.Print. * ;
1* Output:
j 59
k 56
j + k 115
j k 3
k I j O
k * j 3304
k % j 56
j %= k ; 3
v 0.5309454
w 0.0534 1 22
v + w 0.5843576
v w 0.47753322
v * w 0 .028358962
48 Piensa en Java
v / w , 9.940527
U += V 10.471473
u v 9.940527
u *= v 5.2778773
u /= v 9 . 940527
*///>
Para generar números, el programa crea en primer lugar un objeto Random . Si se crea un objeto Random sin ningún argu-
mento, Java usa la hora actua l como semilla para el generador de números aleatorios, y esto generaría una salida diferente
en cada ejecución del programa. Sin embargo, en los ejemplos del libro, es importante que la salida que se muestra al final
de los ejemplos sea lo más coherente posible, para poder verificarla con herramientas externas. Proporcionando una semi-
lla (un va lor de inicialización para el generador de números aleatorios que siempre genera la misma secuencia para una
detenninada semilla) al crear el objeto Random, se generarán siempre los mismos números aleatorios en cada ejecución del
programa, por 10 que la salida se podrá verificar. l Para generar una salida más variada, pruebe a eliminar la sem illa en los
ejemplos del libro.
El programa genera varios números aleatorios de distintos tipos con el objeto Random simplemente invocando los méto-
dos nextln!( ) y nextFloa!( ) (también se pueden invocar nextLong() o nexIDouble( ».
El argumento de nextIn!() esta-
blece la cota superior para el número generado. La cota superior es cero, lo cual no resulta deseable debido a la posibilidad
de una división por cero, por lo que sumamos uno al resultado.
Ejercicio 4: (2) Escriba un programa que calcule la velocidad utilizando una distancia constante y un tiempo constante.
tiene un significado obvio. El compilador también podría deducir el uso correcto en:
x = a * -b¡
pero esto podría ser algo confuso para el lector, por lo que a veces resulta más claro escribir:
x = a * (-b) ¡
El menos unario invierte el signo de los datos. El más unario proporciona una simetría con respecto al menos unario, aun-
que no tiene ningún efecto.
Autoincremento y autodecremento
Java, como e, dispone de una serie de abreviaturas. Esas abreviaturas pueden hacer que resulte mucho más fácil escribir el
código; en cuanto a la lectura, pueden simplificarla o complicarla.
Dos de las abreviaturas más utilizadas son los operadores de incremento y decremento (a menudo denominados operadores
de autoincremento y autodecremento). El operador de decremento es -- y significa "disminuir en una unidad". El operador
de incremento es ++ y significa "aumentar en una unidad". Por ejemplo, si a es un valor ¡ot, la expresión ++a es equivalen-
te a (a = a + 1). Los operadores de incremento y decremento no sólo modifican la variable, sino que también generan el
va lor de la misma como resultado.
Hay dos versiones de cada tipo de operador, a menudo denominadas prefija y postfl)·a. Pre-incremenlO significa que el ope-
rador ++ aparece antes de la variable, mientras que post-incremento significa que el operador ++ aparece detrás de la varia-
ble. De fonna similar, pre-decremenlo quiere decir que el operador - - aparece antes de la variable y post-decremento
significa que el operador - - aparece detrás de la variable. Para el pre-incremento y el pre-decremento (es decir, ++a o
- -a), se realiza primero la operación y luego se genera el valor. Para el post-incremento y el post-decremento (es decir,
a++ o - -a), se genera primero el valor y luego se realiza la operación. Por ejemplo:
1 El número 47 se utilizaba como "número mágico" en una universidad en la que estudié, y desde entonces 10 utilizo.
3 Operadores 49
JI : operators / Autolnc.java
/1 Ilustra los operadores ++ y -- o
i mport static net.mindview.util . Print.*;
/ * Output:
i 1
++i 2
i ++ 2
i ,3
- -i 2
i- - 2
i : 1
* /// ,-
Puede ver que pa ra la fanna prefija, se obtiene el valor después de rea li zada la operación, mientras que para la fanna pOS1-
fija , se obtiene el valor antes de que la operación se realice. Estos son los únicos operadores, además de los de asignación,
que tienen efectos colaterales. Modifican el operando en lugar de simplemente utilizar su valor.
El operador de incremento es, precisamente, una de las explicaciones del por qué del nombre e++, que quiere decir "un paso
más allá de C". En una de las primeras presentaciones reali zadas acerca de Java, Bi ll Joy (uno de los creadores de Ja va),
dijo que "Java=C++- _" (C más más menos menos), para sugerir que Java es C++ pero si n todas las complejidades inne-
cesarias, por lo que resulta un lenguaje mucho más simple. A medida que vaya avanzando a lo largo de l libro, podrá ver que
muchas partes son más simples, aunque en algunos otros aspectos Java no resulta mucho más sencillo que C++.
Operadores relacionales
Los operadores relacionales generan un resultado de tipo boolean. Eva lúan la relación existente entre los va lores de los ope-
ra ndos. Una expresión relaciona l produce el valor t ru e si la relación es cierta y false si no es cierta. Los operadores relacio-
nales son: menor que «), mayor que (», menor o igual que «=), mayo r o igual que (>=), equi valente (=) y no equi valente
(!=) . La equival encia y la no equi va lencia funcionan con todas las primitivas, pero las otras comparaciones no funcionan
con el tipo boolean. Puesto que los va lores boolean sólo pueden ser true o fa lse, las relaciones "mayor que" y "menor que"
no tienen sentido.
} / * Output ,
f a l se
t r ue
* /// , -
La instrucción System.out.println(n t = n2) imprimirá el resultado de la comparación booleana que contiene. Parece que
la sal ida debería ser "tTue" y luego "false", dado que ambos objetos Integer son iguales, aunque el contenido de los obje-
tos son los mi smos, las referencias no son iguales. Los operadores = y != comparan referencias a objetos, por lo que la
salida realmente es "false" y luego ·'true". Naturalmente, este comportamiento suele sorprender al principio a los programa-
dores.
¿Qué pasa si queremos comparar si el contenido de los objetos es equivalente? Entonces debemos utilizar el método espe-
cia l equals() disponib le para todos los objetos (no para las primiti vas, que funcionan adecuadamente con = y !~). He aquí
la fonna en que se emplea:
11 : operators / EqualsMethod.java
1* Output:
true
* /// ,-
El resultado es ahora el que esperábamos. Aunque, en rea lidad, las cosas no son tan senci llas. Si creamos nuestra propia
clase, como por ejemplo:
11: operators/EqualsMethod2.java
11 equals {) predeterminado no compara los contenidos.
class Value
int i¡
1* Output:
false
* /// ,-
los resultados vuelven a confundirnos. El resultado es false, Esto se debe a que el comportamiento predetenninado de
equals() consiste en comparar referencias. Por tanto, a menos que sustituyamos equals() en nuestra nue va clase, no obten-
dremos el comportamiento deseado. Lamentablemente, no vamos a aprender a sustituir unos métodos por otros hasta el capí-
tu lo dedicado a la Reutilización de clases, y no veremos cuál es la forma adecuada de definir equ als() hasta el Capítulo 17,
Análisis detallado de los contenedores, pero mientras tanto tener en cuenta el comportamiento de eq ua ls() nos puede aho-
rrar algunos quebraderos de cabeza.
La mayoría de las clases de biblioteca Java implementan equals( ) de modo que compare el contenido de los objetos, en
lugar de sus referencias.
Ejercicio 5: (2) Cree una clase denominada Dog (perro) que contenga dos objetos Strin g: name (nombre) y says
(ladrido). En main(), cree dos objetos perro con los nombres "spot" (que ladre di ciendo " Ruf!1") y
"scruffy" (que ladre dic iendo, "Wurf1 "). Después, muestre sus nombres y el sonido que hacen al ladrar.
3 Operadores 51
Ejercicio 6: (3) Continuando con el Eje rci cio 5, cree una nueva referen cia Dog y asígnela al objeto de nombre "spot".
Realice una comparación utili zando = y cqua ls() para todas las referencias.
Operadores lógicos
Cada uno de los operadores lógicos AND (&&), OR (11) y NOT (!) produce un va lor boolean igua l a Irue o false basá ndo-
se en la relació n lógica de sus argumentos. Este ejemplo utiliza los operadores re lacionales y lógicos:
JI: operators/Bool.java
1/ Operadores relacionales y lógicos.
import java.util. * ;
import static net.mindview util.Print. * ¡
/ 0 Output :
i 58
j 55
i > j is true
i < j is false
i >= j is true
i <= j is false
i j is false
i ! = j is true
(i < 10) && (j < 10) is false
(i < 10) 11 ( j < 10 ) is false
0///,-
Sólo podemos aplica r AND, OR o NOT a valores de tipo boolean . No podemos emplear un valor que no sea boolea" como
si fuera un va lor booleano en una expresión lógica como a diferencia de lo que sucede en e y C++. Puede ver en el ejem-
plo los intentos fallidos de realizar esto, desac ti vados mediante marcas de comentarios '''!' (esta si ntax is de comentarios
penni te la eliminac ión automática de comentarios para facilitar las pruebas). Sin embargo, las expresiones subsiguientes
generan valores de tipo boolean utilizando comparaciones relaciona les y a continuación reali zan operaciones lógicas con
los resu ltados.
Observe que un va lor boolean se convierte autom át icamente a una fonna de tipo tex to aprop iada cuando se les usa en luga-
res donde lo que se espera es un valor de tipo String.
52 Piensa en Java
Puede reemplazar la definición de valores int en el programa anterior por cualquier otro tipo de dato primitivo excepto
boolean . Sin embargo, teni endo en cuenta que la comparación de números en coma flotante es muy estricta, un número que
difiera de cualquier otro, aunque que sea en un valor pequeñísimo seguirá siendo distinto. Asimismo, cualquier número
situado por encima de cero, aunque sea pequeñísimo, seguirá siendo distinto de cero.
Ejercicio 7: (3) Escriba un programa que simule el proceso de lanzar una moneda al aire.
Cortocircuitos
Al tratar con operadores lógicos, nos encontramos con un fenómeno denominado "cortocircuito". Esto quiere decir que la
expresión se evaluará únicamente hasta que la veracidad o la falsedad de la ex presión completa pueda ser detenninada de
forma no ambigua. Como resultado, puede ser que las últimas partes de una expresión lógica no lleguen a evaluarse. He aquí
un ejemplo que ilustra este fenóm eno de cortocircuito.
11 : operators/ShortCircuit.java
II Ilustra el comportamiento de cortocircuito
II al evaluar los operadores lógicos.
import static net . mindview.util.Print. * ;
1* Output:
testl(O)
result: true
test2(2)
result: false
expression is false
* ///,-
Cada una de las comprobaciones realiza una comparación con el argumento y devuelve true o fa lse. También imprime la
información necesaria para que veamos que está siendo invocada. Las pruebas se utilizan en la expresión:
testl (O) && test2 (2) && test3 ( 2 )
Lo natural sería pensar que las tres pruebas llegan a ejecutarse, pero la salida muestra que no es así. La primera de las prue-
bas produce un res ultado tr ue, por lo que continúa con la evaluación de la expresión. Sin embargo, la segunda prueba pro-
duce un resultado false. Dado que esto quiere decir que la expresión completa debe ser false, ¿por qué continuar con la
evaluación del resto de la expresión? Esa evaluación podría consumir una cantidad considerable de recursos. La razón de
que se produzca este tipo de cortocircuito es. de hecho, que podemos mejorar la velocidad del programa si no es necesario
evaluar todas las partes de una expresión lógica.
3 Operadores 53
Literales
Nomlalmente, cuando insertamos un valor literal en un programa, el compilador sabe exactamente qué tipo asignarle.
En ocasiones, sin embargo, puede que ese tipo sea ambiguo. Cuando esto sucede, hay que guiar al compilador afíadiendo
cierta infonnación adicional en la fonna de caracteres asoc iados con el va lor litera l. El código siguiente muestra estos
caracteres:
/1 : operators / Literals.java
import static net.mindview.util .Print.*¡
/ * Output:
il: 101111
i2 : 101111
i 3: 1111111
c: 1111111111111111
b, 1111111
s: 111111111111111
' 111 ,-
Un carácter si tuado al final de un va lor literal pennite establecer su tipo. La L mayúscula o minúscula significa long (sin
embargo, utilizar una I minúscula es confuso, porque puede parecerse al número uno). Una F mayúscula o minúscula sig-
nifica float. Una D mayúscula o minúscula significa double.
Los va lores hexadecimales (base 16), que funcionan con todos los tipos de datos enteros, se denotan mediante el prefijo O,
O OX seguido de 0-9 o a-f en mayúscula o minúscula. Si se intenta inicializar una variable con un va lor mayor que el máx i-
mo que puede contener (independientemente de la fonna numérica del valor), el compilador dará un mensaje de error.
Observe, en el código anterior, los va lores hexadecimales máximos pennitidos para char, byte y short. Si nos excedemos
de éstos, el compilador transfoffilará automáticamente el valor a ¡nt y nos dirá que necesitamos una proyección hacia abajo
para la asignación (definiremos las proyecciones posterionnente en el capítulo). De esta fonua, sabremos que nos hemos
pasado del límite pennitido.
Los valores octales (base 8) se denotan incluyendo un cero inicial en el número y utilizando sólo los dígitos 0-7.
No existe ninguna representación literal para los números binarios en e, e++ o Java. Sin embargo, a la hora de trabajar con
notación hexadecimal y octal, a veces resulta útil mostrar la fOffila binaria de los resultados. Podemos hacer esto fácilmen-
54 Piensa en Java
te con los métodos slalic loBinarySlring( ) de las clases lnleger y Long. Observe que, cuando se pasan tipos mas peque-
ños a Integer.toBinaryString(), el tipo se convierte automáticamente a int.
Ejercicio 8: (2) Dem uestre que las notaciones hexadecimal y octal funcionan con los valores long. Utilice
Long.loBinarySlring( ) para mostrar los resultados.
Notación exponencial
Los exponentes utilizan una notación que a mí personalmente me resulta extraña:
// : operatorsjExponents.java
jI "e" significa "ID elevado a".
1* Output:
1.39E-43
4.7E48
* /// ,-
En el campo de las ciencias y de la ingeniería. °e' hace referencia a la base de los logaritmos nanlrales, que es aproximada-
mente 2,718 (en Java hay disponible un va lor double más preciso, que es Malb .E). Esto se usa en expresiones de exponen-
ciación, como por ejemplo 1. 39 X e- 43 , que significa 1.39 X 2.71S--B . Sin embargo, cuando se inventó el lenguaje de
programación FORTRAN, decidieron que e significaría "diez elevado a", lo cua l es una decisión extraña, ya que FORTRAN
fue diseñado para campos de la ciencia y de la ingeniería, asi que cabría esperar que sus di señadores tendrían en cuenta lo
confuso de introducir esa ambigüedad2 . En cualquier caso, esta costumbre fue también introducida en e, e++ y ahora en
Java. Por tanto, si el lector está habituado a pensa r en e como en la base de los logaritmos naturales, tendrá que hacer una
traducción mental cuando vea una expresión como 1.39 e-43f en Java; ya que qui ere decir 1.39 X 10- 43 .
Observe que no es necesario utilizar el ca rácter sufijo cuando el compilador puede deducir el tipo apropiado. Con :
long n3 = 200;
no existe ninguna ambigüedad, por lo que una L después del 200 sería superfluo. Sin embargo, con:
tloat f4 = le-43f¡ 11 10 elevado a
el compilador nonnalmente considera los números exponenciales como de tipo double, por lo que sin la f final, nos daría
un error en el que nos inform aría de que hay que usar una proyección para convertir el valor double a noat.
Ejercicio 9: (1) Visualice los números más grande y más pequeño que se pueden representar con la notación ex ponen-
cial en el tipo noal y en el tipo double.
2 John Kirkham escribe: "Comencé a escribir programas infonnáticos cn 1962 en FORTRAN 11 en un IBM 1620. Por aquel entonces y a lo largo de las
décadas de 1960 y 1970, FORTRAN era un lenguaje donde todo se escribía en mayúsculas. Probablemente, la razón era que muchos de los disposi livos
de entrada eran antiguas unidades de teletipo que utilizaban el código Baudot de cinco bits que no disponía de minúsculas. La hE" en la notación expo-
nencial era también mayúscula y no se confundía nunca con la base de los logaritmos naturales "e" que siempre se escribe en minúscula. La "E" simple-
mente quería decir exponencial. que era la base para el sistema de numeración que se estaba utilizando, que nonnahncnle era 10. En aquella época, los
programadores también empleaban los números oCUlles. Aunque nunca vi que nadie lo utilizara, si yo hubiera visto un número octal en notación exponen-
cial. habría considerado que cSlaba en base 8. La primera vez que vi un exponencial utiliu·mdo una "e" fue a finales de la década de 1970 y tambien a mí
me pareció confuso: el problema surgió cuando empezaron a utilizarse minúsculas en FORTAN, no al principio. De hecho. disponíamos de funciones que
podian usarse cuando se quisiera empicar la base de los logaritmos naturales, pero todas esas funciones se escribían en mayúsculas.
3 Operadores 55
Operadores de desplazamiento
Los operadores de desplazamiento también sirven para manipular bits. Sólo se les puede utilizar con tipos primitivos
en teros. El operador de desplaza mi ento a la izq ui erda «<) genera como resultado el operando situado a la izquierda del ope-
rado r después de desplazarlo hacia la izquierda el núm ero de bits especificado a la derecha del operador (i nsertando ceros
en los bits de menor peso). El operador de desplaza miento a la derecha con signo (») genera como resultado el operando
situado a la izq ui erda del operador después de desplazarlo hacia la derecha el número de bils es pecificado a la derecha del
operador. El desplaza mi ento a la derecha co n signo» utili za lo que se denomina extensión de signo: si el va lor es positi-
vo, se inse nan ceros en los bits de mayor peso; si el va lor es nega ti vo, se inse rtan UIlOS en los bits de mayor peso. Ja va ha
ailadido también un desplazamiento a la derecha si n signo »>. que utiliza lo que denomina extensión con ceros: indepen-
dientemente del signo. se insertan ceros en los bits de mayor peso. Este operador no existe ni en e ni C++.
Si se desplaza un va lor de tipo charo byte o short, será convertido a int antes de que el desplazamiento tenga lugar y el
resultado será de tipo ¡nt. Sólo se utilizarán los bits de menor peso del lado derecho; esto evita que se realicen despla-
zamientos con un número de posiciones superior al número de bits de un va lor int. Si se es tá operando con un valor long,
se obtendrá un resultado de tipo long y sólo se empleará n los seis bits de menor peso del lado derecho. para así no poder
desplazar más posiciones que el número de bits de un valor long.
Los desplazamient os se pueden combinar con el signo igual «<= o »= o »>=). El Ivalor se sustimye por ell va lor des-
plazado de acuerdo co n lo que el rvalor marque. Existe un problema. sin embargo, con el desplazamiento a la derecha sin
56 Piensa en Java
signo combinado con la asignación. Si se usa con valores de tipo byte o sho rt, no se obtienen los resultados correctos. En
lugar de ello, se tran sfom1an a ¡Dt y luego se desplazan a la derecha, pero a continuación se truncan al volve r a asignar los
valores a sus variables, por lo que se obtiene - 1 en esos casos. El siguiente ejemplo ilustra esta situación:
/1: operatorsJURShift.java
/1 Prueba del desplazamiento a la derecha sin signo.
import stat i c net.mindview.util.Print.*¡
1* Output:
11111111111111111111111111111111
1111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
1111111111111111111111
* /// , -
En el último desplazamiento, el valor resultante no se asigna de nuevo a b, sino que se imprime directamente, obteniéndo-
se el comportamiento correcto.
He aquí un ejemplo que ilustra todos los operadores para el manejo de bits:
11: o perators/BitManipulation .java
/1 Uso de los operadores bit a bit.
import java.util . *¡
import static net.mindview.util.Print.*;
long 1 = rand.nextLong()¡
long ID = rand.nextLong()¡
printBinaryLong ( "-IL", -lL);
printBinaryLong ("+lL", +lL );
long 11 = 9223372036854775807L;
printBinaryLong ("maxpos", 11);
long 11n = -92233720368s477s80BL¡
printBinaryLong ( "maxneg", l1n);
printBinaryLong ( " 1 tl, 1) ¡
printBinaryLong(tl-1 11 , -1);
printBinaryLong (It _1 11 , -1);
printBinaryLong ("m", m) ¡
printBinaryLong (111 & m" 1 & mi;
printBinaryLong (It 1 m" 1 mi;
,
printBinaryLong (11 1 '" m" 1 mi;
printBinaryLong ( " 1 « 5" 1 « 51 ;
printBinaryLong("1 » Sil, 1 » S);
printBinaryLong(1I (-1) » S", {-l} » 5) i
printBinaryLong( 1t 1 »:> 5", 1 :»> S) ¡
printBinaryLong("(-1) »:> S", (-1) »> S)¡
/*Output:
- 1,int: -1, binary:
11111111111111111111111111111111
+1, int: 1, binary:
1
maxpos, int: 2147483647, binary:
1111111111111111111111111111111
maxneg, int: -2147483648, binary:
10000000000000000000000000000000
i, int: -1172028779, binary:
10111010001001000100001010010101
-i, int: 1172028778, binary:
1000101110110111011110101101010
58 Piensa en Java
Los dos métodos del final , printBinarylnt() y printBinaryLong( j, toman un valor int o long, respectivamente, y lo impri-
men en formato binario junto con una cadena de caracteres descriptiva. Además de demostrar el efecto de todos los opera-
dores bit a bit para valores int y long, este ejemplo también muestra los valores mínimo, máximo, + 1 Y - t para int y long
para que vea el aspecto que tienen. Observe que el bit más alto representa el signo: O significa positivo y 1 significa nega-
ti vo. En el ejemplo se muestra la salida de la parte correspondiente a los valores in!.
La representación binaria de los números se denomina complemento a dos con signo.
Ejercicio 11: (3) Comience con un número que tenga un uno binario en la posición más significati va (consejo: utilice
una constante hexadecimal). Emplee el operador de desplazamiento a la derecha con signo, desplace el
valor a través de todas sus posiciones binarias, mostrando cada vez el resultado con
Integer.toBinaryString( ).
Ejercicio 12: (3) Comience con un número cuyos dígitos binarios sean todos iguales a uno. A continuación desplácelo
a la izquierda y utilice el operador de desplazamiento a la derecha sin signo para desplazarl o a través de
todas sus posiciones binarias, visualizando los resultados con lnteger.toBinaryString().
Ejercicio 13: (1) Escriba Wl método que muestre valores char en fonnato binario. Ejecútelo utilizando varios caracte-
res di ferentes.
JI : operators / TernarylfElse.java
import static net.mindview.util.Print.*¡
print(ternary{lO)) i
print(scandardlfElse(9)) ;
print(standardlfElse(lO)) ;
/ * Output :
900
100
900
100
* /1/ ,-
Puede ver que el código de ternary( ) es más compacto de lo que sería si no dispusiéramos del operador temario: la ver-
sión sin operador temario se encuentra en standardlfElse( j. Sin embargo, standardlfElse( j es más fácil de comprender
y además exige escribir muchos caracteres más. Así que asegúrese de ponderar bien las razones a la hora de elegir el ope-
rador ternario; nOffilalmente, puede convenir utilizarlo cuando se quiera configurar una variable con uno de dos valores posi-
bles.
/ * Output:
x , y, z 012
O x, y, z
x, y, z (summed) 3
O
*111 ,-
Observe que la salida de la primera instrucción de impresión es ' 012 ' en lugar de sólo '3', que es lo que obtendria si se estu-
vieran sumando los valores enteros. Esto es porque el compilador Java convierte x, y y z a su representación String y con-
catena esas cadenas de caracteres, en lugar de efectuar primero la suma. La segunda instrucción de impresión convierte la
variable inicial a String, por lo que la conversión a cadena no depende de qué es lo que haya primero. Por último, podemos
ver el uso del operador += para añadir una cadena de caracteres a s, y el uso de paréntesis para controlar el orden de eva-
luación de la expresión, de modo que los valores enteros se sumen realmente antes de la visualización.
Observe el último ejemplo de main() : en ocasiones, se encontrará en los programas un valor String vacío seguido de + y
una primitiva, como fonna de rea lizar la conversión sin necesidad de invocar el método explicito más engorroso,
(Integer.toString(), en este caso).
El programador estaba intentando, claramente, comprobar la equivalencia (-) en lugar de hacer una asignación. En ey
e++ el resultado de esta asignación será siempre true si y es distinto de cero, por lo que probablemente se produzca un bucle
infinito. En Java, el resultado de esta expresión no es de tipo boolean, pero el compilador espera un valor boolean y no rea-
li zará ninguna conversión a partir de un valor int, por lo que dará un error en tiempo de compilación y detectará el proble-
ma antes de que ni siquiera intentemos ejecutar el programa. Por tanto, este error nunca puede producirse en Java (la única
posibilidad de que no se tenga un error de tiempo de compilación, es cuando x e y son de tipo boolean, en cuyo caso x = y
es una expresión legal , aunque en el ejemplo anterior probablemente su li SO se deba a un error).
Un problema similar en e y e++ consiste en utilizar los operadores bit a bit AND y OR en lugar de las versiones lógicas.
Los operadores bit a bit AND y OR uti lizan uno de los caracteres (& o Il mientras que los operadores lógicos AND y OR
utilizan dos (&& y 11). Al igual que con = y~, resulta fácil confundirse y escribir sólo uno de los caracteres en lugar de
dos. En Java, el compi lador vuelve a evitar este tipo de error, porque no pennite emplear un determinado tipo de datos en
un lugar donde no sea correcto hacerlo.
Operadores de proyección
La palabra proyección (casI) hace referencia a la conversión explícita de datos de un tipo a otro. Java cambiará automática-
mente un tipo de datos a otro cada vez que sea apropiado. Por ejemplo, si se asigna un valor entero a una variable de coma
flotante, el compilador convertirá automáticamente el valor int a noat. El mecanismo de conversión nos pennite realizar
esta conversión de manera explícita, o incluso forzarla en situaciones donde nonnalmente no tendría lugar.
Para realizar una proyección, coloque el tipo de datos deseado entre paréntesis a la izquierda del valor que haya que con-
vertir, como en el siguiente ejemplo:
3 Operadores 61
// : operators / Casting.java
Truncamiento y redondeo
Cuando se realizan conversiones de estrechamiento, es necesario presta r atención a los problemas de truncamiento y redon-
deo. Por ejemplo, si efectuamos una proyección de un va lor de coma flotante sobre un valor entero, ¿qué es lo que haría
Java? Por ejemplo, si tenemos el valor 29,7 y lo proyectamos sobre un int, ¿el valor resultante será 30 o 29? La respuesta
a esta pregunta puede verse en el siguiente ejemplo:
/1 : operato rs / CastingNumbers.java
// ¿Qué ocurre c uando se proyecta un val o r float
// o double sobre un val or entero?
import static net.mindview.util.Print.*¡
1* Output:
(int ) above: O
(int ) below: O
(int ) fabove: O
(int ) fbelow: O
* /// ,-
62 Piensa en Java
Asi que la respuesta es que al efectuar la proyección de float o double a un valor entero, siempre se trunca el correspon-
diente número. Si quisiéramos que el resultado se redondeara habría que utilizar los métodos round() de ja\'a.lang.Math:
11 : operators/RoundingNumbers . java
II Redondeo de valores float y double.
import static net.mindview.ut i l.Print .*;
1* Output:
Math . round (above) : 1
Math. round (be low) : O
Math. round (fab ove) : 1
Ma th. round (fbelow ) : O
* ///,-
Puesto que round( ) es parte de ja\'a.lang, no hace falta ninguna instmcción adicional de importación para utilizarlo.
Promoción
Cuando comience a programar en Java, descubrirá que si hace operaciones matemáticas o bit a bit con tipos de datos primi-
tivos más pequeños que ¡nt (es decir, char, byte o short), dichos valores serán promocionados a ¡nt antes de realizar las
operaciones, y el valor resultante será de tipo int. Por tanto, si se quiere asignar el resultado de nuevo al tipo más pequeño,
es necesario emplear una proyección (y, como estamos realizando una asignación a un tipo de menor tamaño, perderemos
infonnación). En general, el tipo de datos de mayo r tamaño dentro de una expresión es el que detennina el tamaño del resul-
tado de esa expresión, si se multiplica un valor float por otro double, el resultado será double; si se suman un valor ¡nt y
uno long, el resultado será long.
Compedio de operadores
El siguiente ejemplo muestra qué tipos de datos primitivos pueden utilizarse con detenninados operadores concretos.
Básicamente, se trata del mismo ejemplo repetido una y otra vez pero empleando diferentes tipos de datos primitivos. El
archivo se compilará sin errores porque las líneas que los incluyen están desactivas mediante comentarios de tipo I/!.
11 : operators/A110ps.java
1I Comprueba todos los ope r adore s con todos los tipos de dat os primitivos
3 Operadores 63
x (char) (x % y);
x (char) (x + y);
x (char) ( x y) ;
x++;
x--¡
x = (char) +y¡
x = (char) -y ¡
II Relacionales y lógicos:
f (x > y);
f (x >= y);
f (x < y);
f (x <= y);
f (x == y);
f(x != y) ;
II! f (!x) ¡
II! f (x && y) ;
II! f(x 11 y) ;
II Operadores bit a bit:
x= (char) -y ¡
x = (char) {x & y};
x = (char) ( x 1 y);
x (char) (x • y);
x (char ) (x « 1);
x (char) (x » 1) ¡
x (char) (x »> 1);
II Asignación compuesta:
X += y;
X y;
X *= y;
x 1= y;
x %= y;
x «= 1 ;
x »= 1 ;
x »>= 1 ;
x &= y;
X A= y¡
x 1= y;
II Proyección:
II! boolean bl = (boolean}x;
byte b = (byte)x;
short s = (short)x¡
int i = {int}x¡
long 1 = (long)x¡
float f = (float)x¡
double d = (double)x¡
f (x >; y ) ;
f (x < y) ;
f (x <= y ) ;
f (x -- y ) ;
f (x != y) ;
// ! f (! x l i
//! f (x && y ) ;
// !f(x 11 y ) ;
1/ Operadores bit a bit:
x (byte) -y;
x (byte) (x & y ) ;
x ( byte ) ( x y) ;
x ( byte ) ( x • y) ;
x ( byte ) ( x « 1) i
x ( byte ) (x » 1) i
x ( byte ) (x »> 1 ) i
JI Asignación compuesta:
x += y;
X y;
X *= y ;
x /; y ;
x %= Yi
X «:::c 1;
x »= 1;
x»>", 1;
x &= y;
x "'= y;
x 1; y;
JI Proyección :
JI ! boolean bl = (boolean)x;
char e = (char ) x;
short s = (short ) x;
int i = ( int ) Xi
long 1 = (l ong ) x;
float f = ( fleat ) X i
dauble d = (double)x;
x (short)-y¡
x Is hort ) Ix & y);
x Ishort) Ix y) ;
x Is hort ) Ix y );
A
x (short) ( x « 1 ) i
x (short) (x » 1) i
x (short) (x »> 1) i
II Asignación compuesta:
X += y;
X -= y;
x *= y;
x 1= y;
x %= y;
x «= 1¡
x »= 1 ¡
x »>= 1;
x &= y¡
x "= Y¡
x 1= y;
II Proyección:
II! boolean bl = (boolean } x¡
char c = (c har }x;
byte b = Ibyte)x;
int i = (i nt ) Xi
long 1 = Ilong ) x;
float f = I float ) x;
double d = Idouble ) x;
x y;
x *= y;
x / = y;
x %= y;
x «= 1;
x :»= 1i
X »>= 1;
x &= Yi
x "'= Yi
x 1= y;
// Proyección:
// ! boolean bl (boolean ) x¡
char e = {char ) x¡
byte b = (byte ) x;
short s = (short ) x¡
long 1 = ( long ) x¡
float f = (float ) x;
double d = {double )x¡
/ 1 Relacionales y lógicos:
f (x > y);
f (x >= y ) i
f (x < y ) ;
f (x <= y ) ;
f (x == y ) ;
f (x != y ) ;
/ / ! f ( !x ) ;
//! f (x && y ) ;
// ! f (x 1 1 y ) ;
JI Operadores b i t a bit:
x - Yi
x x & y;
x x y;
x x y;
x x « 1;
x x » 1;
x x»:> 1;
/1 Asignación compuesta:
X += y;
X y;
X *= y;
x / = y;
x %= y;
x «= 1¡
x »= 1;
x »>= 1;
x &= Yi
x "= Yi
68 Piensa en Java
x 1, y;
II Proyección:
II! boolean bl = (boolean)x ;
char c = (char)x¡
byte b ' (byt e l x;
short s = (short) x¡
int i = (int) x¡
float f = (float ) x¡
double d = {double)x¡
res int no es necesari a una proyecc ión. porque todo es ya de tipo int. Sin embargo, no se crea que todas las operaciones son
seguras. Si se multiplican dos va lores ¡nt que sean lo suficientemente grandes, se producirá un desbordamiento en el resul-
tado, como se ilustra en el siguiente ejemplo:
JJ : operatorsJOverflow . java
JJ ¡Sorpresa! Java permite los desbordamientos.
J* Output :
big = 214 748364 7
bigger = -4
* ///,-
No se obtiene ningún tipo de error o advertencia por pane del compilador, y tampoco se genera ninguna excepción en tiem-
po de ejecución. El lenguaje Ja va es muy bueno, aunque no hasta ese punto.
Las asignaciones compuestas no requieren proyecciones para char, byte o short, aún cuando estén realizando promociones
que provocan los mismos resultados que las operaciones aritméticas directas. Esto resulta quizá algo sorprendente pero, por
otro lado, la posibilidad de no incluir la proyección simplifica el código.
Como puede ver, con la excepción de boolcan, podemos proyectar cualqu ier tipo primitivo sobre cualquier otro tipo primi-
tivo. De nuevo, recalquemos que es preciso tener en cuenta los efectos de las conversiones de estrechamiento a la hora de
realizar proyecciones sobre tipos de menor tamaño~ en caso contrario, podríamos perder información inadvertidamente
durante la proyección.
Ejercicio 14: (3) Escriba un método que tome dos argumentos de tipo String y utilice todas las comparac iones
boolean para comparar las dos cadenas de caracteres e imprimir los resultados. Para las comparaciones
= y !=, realice también la prueba con equals( ). En maine ), invoque el método que haya escrito, utili-
zando varios objetos String diferentes.
Resumen
Si tiene experiencia con algún lenguaje que emplee una sintaxis similar a la de C. podrá ver que los operadores de Ja va son
tan similares que la curva aprendizaje es prácticamente nula. Si este capítulo le ha resultado dificil, asegúrese de echar un
vistazo a la presentación multimedi a Thinking in C. disponible en wW)'l~ MindViel1Wel.
Puede encontrar las sol uciones a lo~ ejercicios seleccionados en el documento elcctrónico The Thi"ki"g in Java Afmolllfed So/mio" Guide, que está dispo-
nible para la vcnta en \\"Il1ul/i"dl'iew.nel.
Control
. ., de
eJecuclon
Al igual que las criaturas sensibles, un programa debe manipular su mundo y tomar decisiones
durante la ejecución. En Java, las decisiones se toman mediante las instrucciones de control de
ejecuc ión.
Java utiliza todas las instrucciones de control de ejecución de e, por lo que si ha programado antes con e o C++, la mayor
parte de la infonnación que vamos a ver en este capítulo le resultará fam il iar. La mayoría de los lenguajes de programación
procedimental disponen de alguna clase de instrucciones de control, y suelen existir solapamientos entre los distintos len-
guajes. En Java, las palabras clave incluyen if-else, while, do-whUe, for, return, break y una instrucción de selección deno-
minada switch. Sin embargo, Java no soporta la despreciada instrucción goto (q ue a pesar de ello en ocasiones representa
la fonna más directa de resolver ciertos tipos de problemas). Se puede continuar realizando un salto de estilo goto, pero está
mucho más restringido que un goto típico.
true y false
Todas las instrucciones condicionales utilizan la veracidad o falsedad de una expresión condicional para determinar la ruta
de ejecución. Un ejemplo de expresión condicional sería a = b. Aquí, se utili za el operador condicional = para ver si el
va lor de a es equivalente al va lor de b. La expresión devuelve true o false. Podemos utilizar cualquiera de los operadores
relacional es que hemos empleado en el capí tulo anterior para escri bir una instrucción condicional. Observe que Java no per-
mite utilizar un número como boolean , a diferencia de lo que sucede en e y e++ (donde la veracidad se asocia con valores
disti ntos ce ro y la falsedad con cero). Si quiere emplear un va lor no boolean dentro de una prueba boolean, como por ejem-
plo if(a), deberá primero convertir el valor al tipo boolean usando una expresión condicional, como por ejemplo if(a != O).
if-else
La instmcción if-else representa la fonna más básica de controlar el flujo de un programa. La cláusula elsc es opcional, por
lo que se puede ver if de dos fonnas distintas:
if (expresión-booleana )
instrucción
o
if (expresión-booleana )
instrucción
else
instrucción
La expresión-booleana debe producir un resultado boolean. La instrucción puede ser una instmcción simple temlinada en
punto y coma o una instrucción compuesta, que es un gmpo de instmcciones simples encerrado entre llaves. All í donde
empleemos la palabra instrucción querremos decir siempre que esa instmcc ión pued e ser simple o compuesta.
Co mo ejemplo de ir-else, he aquí un método teste ) que indica si un cierto valor está por encima, por debajo o es equivalen-
te a un número objetivo:
72 Piensa en Java
//: control/lfElse.java
import static net.mindview.util.Print.*;
/* Output:
1
-1
O
* /// ,-
En la pal1e central de test( ), también puede ver una instnlcción "else if," que no es una nueva palabra clave sino una ins-
tnacción cisc seguida de una nueva instrucción ir.
Aunque Java, como sus antecesores e y C++, es un lenguaje de "fonnato libre" resulta habitual sangrar el cuerpo de las ins-
trucciones de control de flujo, para que el lector pueda detenninar fácilmente dónde comienzan y dónde tenninan .
Iteración
Los bucles de ejecución se controlan mediante whil e, do~w h ile y for, que a veces se clasifican como instrucciones de ite-
ración. Una detenninada instrucción se repite hasta que la expresión-booleana de control se evalúe como falseo La fonna
de un bucle whil e es:
while (expresi6n-booleanal
instrucción
La expresión-booleana se evalúa una vez al principio del bucle y se vuelve a evaluar antes de cada suces iva iteración de la
instrucción.
He aquí un ejemplo si mple que genera números aleatorios hasta que se cumple una detenninada condición.
JJ: controlJWhileTest.java
JJ Ilustra el bucle while.
El metodo condition() utiliza el método random( ) de tipo sta tic de la biblioteca Math, que genera un valor double com-
prendido entre O y 1 (incluye 0, pero no l.) El valor result proviene del operador de comparación <, que genera un resulta-
do de tipo boolean oSi se imprime un va lor boolean , automáticamente se obtiene la cadena de caracteres apropiada "true"
o "false", La expresión condicional para e l bucle wbile dice: "repite las instrucciones del cuerpo mientras que condition()
devuelva truc",
do-while
La forma de do-while es
do
instrucción
while (expresión-booleana ) j
La única diferencia entre ""hile y do-while es que la instrucción del bucle do-while siempre se ejecuta al menos una vez,
incluso aunque la expresión se evalúe como false la primera vez. En un bucle while, si la condición es false la primera
vez, la instrucción nunca llega a ejecutarse. En la práctica, do-while es menos co mún que ",hUe.
for
El bucle for es quizá la forma de iteración más habitualmente utilizada. Este bucle realiza una inicialización antes de la pri-
mera iteración. Después realiza una prueba condicional y, al final de cada iteración, lleva a cabo alguna fOffila de "avance
de paso". La fonna del bucle for es:
for(inicialización; expresión-booleana ; paso)
instrucción
Cualquiera de las expresiones inicialización, expresión-booleana o paso puede estar vacía. La expresión booleana se com-
prueba antes de cada iteración y, en cuanto se evalúe como false, la ejecución continúa en la línea que sigue a la instrucción
for oAl final de cada bucle. se ejecuta el paso.
Los bucles for se suelen utilizar para tareas de "recuento";
/1: control/ListCharacters.java
/1 Ilustra los bucles IIfor ll enumerando
II todas las letras minúsculas ASCII.
/, Output :
value: 97 character: a
value: 98 character: b
value: 99 character: e
value: 100 character: d
value: 101 character: e
value: 102 character: f
value : 103 character: 9
value: 104 character: h
value: 105 character: i
va lue: 106 character: j
* /// ,-
Observe que la variable e se define en el mismo lugar en el que se la utiliza, dentro de la expresión de control correspon-
diente al bucle for, en lugar de definirla al principio de main(). El ámbito de e es la instrucción controlada por foro
74 Piensa en Java
Este programa también emplea la clase "envo ltorio" java.lang.Character. que no sólo envuelve ellipo primili vo char den-
tro de un objeto, sino que también proporciona Olras utilidades. Aquí el mélOdo static isLowcrCase( ) se usa para detectar
si el carácter en cuestión es una letra minúscula.
Los lenguajes procedimental es tradicionales co mo e requieren que se definan todas las variables al comienzo de un bloque,
de modo que cuando el compilador cree un bloque, pueda asignar espacio para esas va riabl es. En Java y C++, se pueden
distribuir las declaraciones de variables por todo el bloque, definiéndolas en el lugar que se las necesite. Esto permite un
estilo más natural de codificación y hace que el código sea más fácil de entender.
Ejercicio 1: (1) Esc riba un programa que imprima los val ores comprendidos entre I y 100.
Ejercicio 2: (2) Escriba un programa que genere 25 va lores int aleatorios. Para cada va lor, utilice una instrucción
if-elsc para clasificarlo como mayor que, menor que o igual a un segundo va lor generado aleatoriamente.
Ejercicio 3: (1) Modifique el Ejercicio 2 para que el código quede rodeado por un bucle while ·'infinito". De este mo-
do, el programa se ejecutará basta que lo interrumpa desde el teclado (nomlalmente, pulsando Contro l-C).
Ejercicio 4: (3) Escriba un programa que utilice dos bucles ror an idados y el operador de módulo (% ) para detectar e
imprimir números primos (números en teros que no son di visibles por ningún número excepto por sí mis-
mos y por 1).
Ejercicio 5: (4) Repita el Ejercicio 10 del capitulo anterior, utilizando el operador ternario y una comprobación de tipo
bit a bit para mostrar los unos y ceros en lugar de lnteger.toBinaryString( ).
El operador coma
Anterionnente en el capítulo, hemos dicho que el operador coma (no el separador coma que se emplea para separar defi-
niciones y argumentos de mélOdos) sólo ti ene un uso en Java: en la ex presión de control de un bucle foroTanto en la parte
correspondiente a la inicialización como en la parte correspondiente al paso de la ex presión de control, podemos incluir una
seri e de instrucciones separadas por comas, y dichas insrrucciones se evaluarán secuencialmente.
Con el operador coma, podemos definir múltiples variables dentro de una instrucción for, pero todas ellas deben ser del
mismo tipo:
11 : control/CornmaOpe rat or . java
1* Output:
i 1 j 11
i 2 j 4
i 3 j 6
i 4 j B
* ///,-
La definición int de la instrucción for cubre tanto a i como a j . La parte de inicialización puede tener cualquier número de
defmiciones de /In mismo tipo. La capacidad de definir variabl es en una ex presión de control es tá limitada a los bucles foro
No puede emplearse esta técnica en ninguna otra de las restantes instrucciones de selección o iteración.
Puede ver que, tanto en la parte de iniciali zación co mo en la de paso, las instmcciones se evalúan en orden secuencial.
Sintaxis foreach
Java SE5 introduce ulla sintaxi s for nueva. más sucinta, para utili zarl a co n matrices y cont enedores (hablaremos más en
detalle sobre este tipo de objetos en los Capítulos 16, Matrices, y 17, Análisis detallado de los contenedores). Esta sintax is
se denomina sintaxisjoreach (para todos), y quiere decir que no es necesario crear una variable int para efeCnl8f un recuen-
to a través de lUla secuencia de elementos: el bucle for se encarga de generar cada elemenlO automáticamente.
4 Control de ejecución 75
Por ejemplo, suponga que tiene una matriz de valores float y que quiere seleccionar cada uno de los elementos de la matri z:
1/ : controljForEachFloat . java
import java.util. *i
/ * Output:
0 .72711575
0.399 82635
0 .5309454
0.0534122
0.16020656
0.57799 757
0 .18847865
0.4170137
0.5 1660204
0 .73734957
* jjj , -
La matriz se rellena utilizando el antiguo bucle fOf , porque debe accederse a ella mediante un índice. Puede ver la sintaxis
foreach en la Iínca:
f or (float x : f)
Esto define una variable x de tipo float y asigna secuencialmente cada elemento de fax.
Cualquier método que devuelve una matriz es buen candidato para emplearlo con la sintaxisforeach. Por ejemplo, la clase
String tiene un método toCharArray() que devuelve una matriz de char, por lo que podemos iterar fácilmente a través de
los caracteres de una matriz:
JI : control/ForEachString . java
1* Output:
A n A f r i e a n s w a 1 1 o w
* jjj ,-
Como podremos ver en el Capítulo 11, Almacenamiento de objetos , la sintaxisjoreach también funciona con cualquier obje-
to que sea de tipo Iterable.
Muchas instrucciones for req uieren ir paso a paso a través de una secuencia de valores enteros como ésta:
for (in t i = o¡ i < 100; i++)
Para este tipo de bloques. la sintaxi sforeach no funcionará a menos que queramos crear primero una matri z de valores int.
Para simplificar esta tarea, he creado un método denominado range() en net.mindview.utiI.Range que genera automática-
mente la matriz apropiada. La intención es que range( ) se utilice como importación de tipo static:
11 : control/ForEachInt . java
import static net.mindview.util.Range.*¡
import static net.mindview.util.Print.*¡
76 Piensa en Java
1* Output :
O 1 2 3 4 5 6 7 8 9
5 6 7 8 9
58111417
* /// ,-
El método range( ) está sobrecargado, lo que quiere decir que puede utilizarse el mismo método con diferentes listas de
argumentos (en breve hablaremos del mecanismo de sobrecarga). La primera fonna sobrecargada de range() empieza en
cero y genera valores hasta el extremo superior del rango, sin incluir éste. La segunda fonTIa comienza en el primer valor y
va hasta un valor menos que el segundo, y la tercera fonna incluye un valor de paso, de modo que los incrementos se rea-
lizan según ese valor. range( ) es una versión muy simple de lo que se denomina generador, que es un concepto del que
hablaremos posteriormente en el libro.
Observe que aunque range() pemlite el uso de la sintaxisforeach en más lugares, mejorando así la legibilidad del código,
es algo menos eficiente, por lo que se está utili zando el programa con el fm de conseguir la máxima velocidad, conviene
que utilice un perfilador, que es una herramienta que mide el rendimiento del código.
Podrá obse rvar también el uso de priotnb() además de print(). El método printnb() no genera un carácter de nueva línea,
por lo que pemlite escribir una línea en sucesivos fragmentos.
La sinta~isfo,.each no sólo ahorra tiempo a la hora de escribir el código. Lo más importante es que facilita la lectura y comu-
nica perfectamente qué es lo que estamos tratando de hacer (obtener cada elemento de la matriz) en lugar de proporcionar
los detalles acerca de cómo lo estamos haciendo ("Estoy creando este índice para poder usarlo en la selección de cada uno
de los elementos de la matri z"). Utilizaremos la sintaxi sforeach siempre que sea posible a lo largo del libro.
return
Diversas palabras clave representan lo que se llama un salIO incondicional, lo que simplemente quiere decir qu e el salto en
el flujo de ejecución se produce sin reali zar previamente comprobación alguna. Dichas palabras clave incluyen return,
break, continue y una forma de saltar a una instrucción etiquetada de fonna similar a la instrucción goto de otros lenguajes.
La palabra clave returD tiene dos objetivos: especifica qué valor devolverá un método (si no tiene un valor de retomo de
tipo void) y hace que la ejecución salga del método actual devolviendo ese valor. Podemos reescribir el método lest( ) pre-
cedente para aprovechar esta característica:
1/ : control/lfElse2.java
import static net.mindview.util.Print.*¡
/ * Output :
1
-1
°*/1/ ;-
No hay necesidad de la cláusula else, porque el método no continuará después de ej ecutar una instrucción return.
Si no inclu ye una instrucción returo en un método que devuel ve un valor void, habrá una instrucción returo implícita al
fina l de ese método, así que no siempre es necesario incluir dicha instmcción. Sin embargo, si el método indica que va a
devolver cualquier otro valor di stinto de void, hay que garantizar que todas las rutas de ejecución del códi go devuel van un
valor.
Ejercicio 6: (2) Modifique los dos métodos teste ) de los dos programas anteriores para que admitan dos argumentos
adicionales, begin y cnd, y para que se compruebe testval para ver si se encuentra dentro del rango com-
prendido entre begin y end (ambos incluidos).
break y continue
También se puede controlar el flujo del bucle dentro del cuerpo de cualquier instrucción de iteración utili zando break y con-
tinue. break provoca la saLida del bucle sin ejecutar el resto de la instmcciones. La instrucción continue detiene la ejecu-
ción de la iteración actual y vuelve al principio del bucle para comenzar con la siguiente iterac ión .
Este programa muestra ejemplos de break y continue dentro de bucles ror y while:
// : control / BreakAndContinue.java
// Ilustra las palabras clave bre ak y continue .
import static net . mindvie w.util . Range .* ;
System. ou t.println {) ;
// Uso de f o reach:
for (int i : range (l OO))
if ( i == 74 ) break; // Fuera de l b ucl e
if {i % 9 ! = O) centi n ue; // Siguiente iteración
System. e ut.print (i + 11 " ) ;
while (t r ue ) {
i+ + ;
i nt j = i * 27 i
i f ( j == 1269 ) b reak ; // Fuera del bucle f er
if ( i % 10 ! = O) centi nue; // Principio del bucle
System.out . print (i + 11 " ) ;
/ * Output:
78 Piensa en Java
o 9 18 27 36 45 54 63 72
o 9 18 27 36 45 54 63 72
10 20 30 40
* /// ,-
En el bucle for, el va lor de i nunca llega a 100. porque la instrucción break hace que el bucle termine cuando ¡ va le 74.
Nommlmente. utilizaremos una instrucción break como ésta sólo si no sabemos cuándo va a cumplirse la condición de ter-
minación. La instrucción continue hace que la ejecución vuelva al principio del bucle de iteración (incrementando por tanto
i) siempre que i no sea divisible por 9. Cuando lo sea, se imprimirá el valor.
El segundo bucle for muestra el uso de la sintaxisforeach y como ésta produce los mismos resultados.
Finalmente, podemos ver un bucle while " infinito" que se estaría ejecutando, en teoría, por siempre. Sin embargo, dentro
del bucle hay una instmcción break que hará que salgamos del bucle. Además, podemos ver que la instrucción continue
devuelve el control al principio del bucle sin ejecutar nada de lo que hay después de dicha instrucción contin ue (por tanto,
la impresión sólo se produce en el segundo bucle cuando el va lor de i es divisible por 10). En la salida, podemos ver que se
imprime el va lor O, porque O % 9 da como resultado O.
Una segunda forma del bucle infutito es ror(;;). El compilador trata tanto while(true) como ror(;;) de la misma fonna , por
lo que podemos uti lizar una de las dos fOffilas según prefiramos.
Ejercicio 7 : ( 1) Modifique el Ejercicio 1 para que el programa temúne usando la palabra clave break con el va lor 99.
Intente utilizar return en su lugar.
El lÍnico lugar en el que una etiqueta resulta útil en Java es justo antes de una instrucción de iteración. Y queremos decir
exactamente justo antes: no resulta conven iente poner ninguna instrucción entre la etiqueta y la iteración. Y la única razón
para colocar una etiqueta en una iteración es si vamos a anidar otra iteración o una instmcción switc h (de la que hablare-
mos enseg uida) dentro de ella. Esto se debe a que las palabras clave break y continue nOn1lalmente sólo intemlmpirán el
bucle actual, pero cuando se las usa como una etiqueta intemllnpen todos los bucles hasta el lugar donde la etiqueta se haya
definido:
labell ,
iteración-externa
iteración-interna
4 Control de ejecución 79
/ / ...
break ; / / 111
/ / ...
con tinue ; // (2 )
/ / ..
continue labell¡ JI (3)
/ / ...
break labell; // 14 1
En (1). la instrucción break hace que se sa lga de la iteración interna y que acabemos en la iteraci ón ex terna. En (2), la
instrucción continue bace que vo lvamos al principio de la iteración interna. Pero en (3), la instrucción continue label) hace
que se salga de la iteración imema y de la iteración ex terna, hasta situarse en labell . Entonces, continúa de hecho con la ite-
ración, pero comenzando en la iteración externa. En (4), la instrucción break labell también hace que nos salgamos de las
dos iteraciones has ta situamos en labell , pero sin volver a entrar en la iteración. De hecho, ambas iteraciones habrán fina-
lizado.
He aq uí un ejemplo de utilización de bucles for :
JI : control/LabeledFor.java
/ / Bucles ter con tlbreak eqtiquetado n y "con tinue etiquetado".
import static net.mindview.util . Print. *¡
if li == 71
print ("continue oucer");
i++; II En caso contrario, i nunca
II se incrementa.
continue outer;
if l i == S I
print ("break outer" ) ;
break outer;
1* Output:
i = O
continue inner
i = 1
continue inner
i = 2
continue
i = 3
break
i = 4
continue inner
i = 5
continue inner
i = 6
continue inner
i = 7
continue outer
i = 8
break outer
*/1/,-
Observe qu e break hace que salgamos del bucle for, y que la expresión de incremento no se ejecuta hasta el final de la pasa-
da a través del bucle fOf o Puesto que break se salta la expresión incremento, el incremento se realiza directamente en el caso
de i = 3. La instrucción continue outer en el caso de i = 7 también lleva al principio del bucle y también se salta el incre-
mento, por lo que en este caso tenemos también que realizar el incremento directamente.
Si no fuera por la instrucción break outer no habría fonna de salir del bucle ex temo desde dentro de un bucle interno, ya
que break por sí misma sólo permite sal.ir del bucle más interno (lo mismo cabría decir de continue).
Por supuesto, en aquellos casos en que salir de un bucle impl.ique salir también del método, basta con ejecutar return.
He aquí una demostración de las instrucciones break y continue etiquetadas con bucles while:
ji : control/LabeledWhile.java
/ / Bucles while con "break etiquetado" y "continue etiquetado".
import static net.mindview.util.Print. * ¡
if ( i == 5 )
print ("break 11 l ;
break¡
if(i == 7)
4 Control de ejecución 81
1* Output:
Outer while loop
i = 1
continue
i = 2
i = 3
continue Quter
Outer while loop
i = 4
i = 5
break
Outer while loop
i = 6
i = 7
break outer
*///,-
Las mismas reglas siguen siendo ciertas para while :
1. Una instrucción contin ue nonnal hace que sa ltemos a la parte superior del bucle más interno y continuemos allí
la ejecución.
2. Una instrucción con tin ue etiquelada hace que sa ltemos hasta la etiqueta y que volvamos a ejecutar el bucle si tua-
do justo después de esa etiqueta.
3. Una instrucción b reak hace que finalice el bucle.
4. Una instrucción break etiquetada hace que finalicen todos los bucles hasta el que tiene la etiqueta, incluyendo
este último.
Es importante recordar que la única razón para utilizar etiquetas en Java es si tenemos bucles anidados y queremos ejecu-
tar una instrucción break o continue a través de más de un nivel.
En el artículo "Goto considered harmfllf' de Dijkstra. la objeción específica que él hacía era contra la utili zac ión de etique-
tas, no de la instrucción goto. Su observación era que el número de errores parecía incrementarse a medida que lo hacía el
número de etiquetas dentro de un programa, y que las etiquetas en las instrucciones goto hacen que los programas sean más
dificiles de analizar. Observe que la etiquetas de Java no presentan este problema, ya que están restringidas en cuanto a su
ubicación y pueden utilizarse para transferi r el control de fonna arbitraria. También merece la pena observar que éste es uno
de esos casos en los que se hace más útil una detemlinada característica del lenguaje reduciendo la potencia de la corres-
pondiente instmcción.
switch
La palabra clave switch a veces se denomina instrucción de selección. La instmcción switch permite seleccionar entre dis-
tintos fragmentos de código basándose en el valor de una expresión entera. Su fonna general es:
switch(selector-entero) {
case valor-entero! instrucción¡ break;
case valor-entero2 instrucción ¡ break;
case valor-entero3 instrucción ¡ break;
case valor-entero4 instrucción¡ break;
case valor-enteroS instrucción ¡ break;
// ...
default: instrucción ;
82 Piensa en Java
SeleCfOr-entero es una expresión que genera un va lor entero. La instrucción switch co mpara el res ultado de selecfOr-enrero
con cada valor-el1fero. Si encuentra un a coi ncidencia, ejecuta la correspondiente instrucción (una sola instmcción o múlti-
ples instmcciones: no hace falta usar lla ves). Si no hay ningun a coi ncidencia, se ejec uta la instrucción de default.
Observará en la defini ción anterior que cada case finali za con una instmcción break, lo que hace que la ejecución salte al
final del cuerpo de la instrucción switch . Ésta es la forma convencional de construir una instrucción switch , pero la instnlc-
ción break es opcional. Si no se inclu ye, se ejec utará el códi go de las instmcciones case si tuadas a continuac ión hasta que
se encuentre una instmcc ión break. Aunque normalmente este comportamiento no es el deseado, puede resultar últil en oca-
siones para los programadores expertos. Observe que la última instrucción, situada después de la cláusula default, no tiene
una instrucción break porque la ejecución continúa justo en el lugar donde break haría que continuara. Podemos incluir,
si n que ello represente un problema, una instmcción break al final de la cláusula default si consideramos que resulta impor-
tante por razones de estilo.
La instmcción s,,'itch es una fonna limpia de implementar selecciones multi vía (es decir, selecciones donde hay que elegir
entre diversas rutas de ejecución), pero requiere de un selector que se evalúe para dar un va lor entero, como int o charo Si
se desea emplear, por ejemplo, una cadena de caracteres o un nllmero en coma flotante como selector, no funcionará en una
instmcción switch. Para los tipos no enteros, es preciso emplear una serie de instm cciones ir. Al final del siguiente capítu-
lo, veremos que la nueva característica enum de Java SE5 ayuda a sua viza r esta restricción, ya que los valores enum están
diseiiados para funcionar adecuadamente con la instrucción switch .
He aquí un ejemplo en el que se crean letras de manera aleatoria y se detennjna si son vocales o consonantes:
//: control/VowelsAndConsonan ts.java
/1 Ilustra la instrucción switch.
import java . util .*;
import static net.mindview . ucil . Print .* ;
/ * Output :
y, 121, Sometimes a vowel
n, 11 0 , consonant
z, 122, consonant
b, 98, consonant
r, 114: consonant
n, 110, consonant
y, 121 : Somecimes a vowel
g, 103, consonant
c, 99, consonanc
f, 102, consonant
o, 1110 vowel
4 Control de ejecución 83
, /// ,-
Puesto que Ra ndom .nextln t(26) genera un valor comprendido entre O y 26, basta con sumar ' a' para generar las letras
minúsculas. Los caracteres encerrados entre comillas simples en las instmcciones case también generan valores enteros que
se emplean para comparación.
Observe cómo las instrucciones case pueden "apilarsc" unas encima de otras para proporcionar múltiples coincidencias para
un detenllinado fragmento de código. Tenga también en cuenta que resulta esencial colocar la instrucción b reak al final de
una cláusula case concreta; en caso contrario, el control no efectuaría el salto requerido y continuaría simplemente proce-
sando el caso siguiente.
En la instrucción :
int e = rand.nextInt(26) + 'a' i
Random. nextl nt( ) genera un valor int aleatorio comprendido entre O y 25, al que se le suma el valor ' a '. Esto quiere decir
que 'a' se convierte automáticamente a int para real izar la suma.
Para imprimir c como carácter, es necesario proyectarlo sobre el tipo char; en caso contrario, generana una salida de tipo
entero.
Ejercicio 8 : (2) Cree una instrucción switch que imprima un mensaje para cada case, y coloque el switch dentro de un
bucle fo r en el que se pruebe cada uno de los va lores de case. Incluya una instrucción break después de
cada case y compruebe los resultados; a conti nuación, elimine las instrucciones brea k y vea lo que suce-
de.
Ejerc icio 9 : (4) Una secllencia de Fibonacci es la secuencia de números 1, 1,2,3,5,8, 13,21,34, etc., donde cada
número (a partir del tercero) es la suma de los dos anteriores. Cree un método que tome un entero como
argumento y muestre esa cantidad de números de Fibonacci comenzando por el principio de la secuencia;
por ejemplo, si ejecuta java Fibonacci 5 (donde Fi bonacci es el nombre de la clase) la salida seria: 1, 1.
2,3,5.
Ejercicio 10: (5) Un nlÍmero vampiro tiene un número par de dígitos y se forma multiplicando una pareja de números
que contengan la mitad del número de dígitos del resultado. Los dígitos se toman del número original en
cualquier orden. No se permiten utilizar parejas de ceros finales. Ent re los ejemplos tendríamos:
1260 ~ 21 • 60
1827 ~ 21 • 87
2187~27' 8 1
Escriba un programa que detennine todos los números vampiro de 4 dígitos (problema sugerido por Dan
Forhan).
Resumen
Este capítulo concluye el estudio de las características fundamentales que podemos encontrar en la mayoría de los lengua-
jes de programación : cálculo, precedencia de operadores, proyección de tipos y mecanismos de selección e iteración. Ahora
esta mos li stos para dar los siguientes pasos, que nos acercarán al mundo de la programación orientada a objetos. El siguien-
te capítulo cubrirá las importantes cuestiones de la inicialización y limpieza de objetos, a lo que seguirá, en el siguiente capi-
rulo, el concepto esencial de ocultación de la implementación.
Pueden encontrarse las soluciones a los ejercicios selecc ionados en el documento electrónico rhe Thi"ki"g in Java Am/Olllled So/mion Cuide. disponible
para la venta en l\w\I:MindViel\:nel.
Inicialización
y limpieza
A medida que se abre paso la revolución infonllática, la programación "no segura" se ha con-
vertido en uno de los mayores culpables del alto coste que tiene el desarrollo de programas.
Dos de las cuestiones relativas a la seguridad son la inicialización y la limpieza. Muchos errores en e se deben a que el pro-
gramador se olvida de inicializar una variable. Esto resulta especialmente habitual con las bibliotecas, cuando los usuarios
no saben cómo inicializar un componente en la biblioteca, e incluso ni siquiera son conscientes de que deban hacerlo. La
limpieza también constituye un problema especial, porque resulta fácil olvidarse de un elemento una vez que se ha tenni-
nado de utilizar, ya que en ese momento deja de preocuparnos. Al no borrarlo. los recursos utilizados por ese elemento que-
dan retenidos y resulta fácil que los recursos se agoten (especialmente la memoria).
C++ introdujo el concepto de constructor, un método especial que se invoca automáticamente cada vez que se crea un ob-
jeto. Java también adoptó el concepto de constructor y además di spone de un depurador de memoria que se encarga de libe-
rar automáticamente los recursos de memoria cuando ya no se los está utilizando. En este capítulo, se examinan las
cuestiones relativas a la inicialización y la limpieza, así como el soporte que Java proporciona para ambas tareas.
class Rock {
Rock () { / f Éste es el constructor
System.out.print(IIRock 11 ) j
new Rack () i
1* Output:
Rack Rack Rack Rock Rock Rack Rock Rack Rack Rack
* /// ,-
Ahora, cuando se crea un objeto:
new Rock() i
se asigna el correspondiente espacio de almacenamiento y se invoca el constmctor. De este modo, se garantiza que el obje-
to está apropiadamente inicializado antes de poder utilizarlo.
Observe que el estilo de codificación consistente en poner la primera letTa de todos los métodos en minúscula no se aplica
a los constructores, ya que el nombre del constructor debe coincidir exactamente con el nombre de la clase.
Un constmctor que no tome ningún argumcnto se denomina conslrucfOr predeterminado. Nonnalmente, la documentación
de Java utiliza el ténnino constructor sin argumentos, pero el término "constructor predeterminado" se ha estado utilizando
durante muchos ailos antes de que Ja va apareciera, por lo que prefiero uti lizar este últin10 ténnino. De todos modos, como
cualquier otro método, el constructor puede también tener argumentos que nos penniten especificar cómo hay que crear el
objeto. Podemos modificar fácilmente el ejemplo anterior para que el constructor tome un argumento:
11 : initialization/SimpleConstructor2.java
II Los constructores pueden tene r argumentos .
class Rack2 (
Rack2(int i)
System.out . print( "Rack 11 + i + 11 ") i
1* Output :
Rock O Rack 1 Rack 2 Rack 3 Rac k 4 Rack S Rack 6 Rock 7
* /// , -
Los argumentos dcl constructor proporcionan una forma de pasar parámetros para la inicialización de un objeto. Por ejem-
plo, si la clase Tree (árbol) tiene un constmctor que toma como argumento tm único número entero que indica la altura del
árbol, podremos crear un objeto Tree como sigue:
Tree t = new Tree (12) i / I árbal de 12 metras
Si Tree(int) es el único constmctor del que di sponemos, el compilador no nos pennitirá crear un objeto Tree de ninguna
otra forma.
Los constructores eliminan una amplia clase de problemas y hacen que el código sea mas fácil de leer. Por ejemplo, en el
fragmento de código anterior, no vemos ninguna llamada ex plícita a ningún método initialize() que esté conceptualmente
separado del acto de creación del objeto. En Java, la creación y la inicialización son conceptos unificados: no es posible
tener la una si n la otra.
El constructor es un tipo de método poco usual porque no ti ene valor de retorno. Existe una clara diferencia entre esta cir-
cunstancia y los métodos que devuelven un valor de retorno vo id, en el sentido de que estos últimos métodos no devue lven
nada. pero seguimos teniendo la opción de hacer que devuel van algo. Los constnlctores no devuel ven nada nunca, y no tene-
mos la opc ión de que se comporten de otro modo (la ex presión De\\' devuel ve una referencia al objeto recién creado, pero
el constructor mismo no tiene un va lor de retomo). Si hubiera valor de retomo y pudiéramos seleccionar cuál es, el compi-
lador necesitaría saber qué hacer con ese va lor de retomo.
Ejercicio 1: (1) Cree una clase que contenga una referencia de tipo String no inicializada. Demuestre que esta referen-
cia la inicializa Java con el va lor null.
5 Inicialización y limpieza 87
Ejercicio 2: (2) Cree una clase con un campo String que se inicialice en el punto donde se defina, y otro campo que-
sea inicializado por el constlUclOr. ¿Cuál es la diferencia entre- las dos técnicas?
Sobrecarga de métodos
U!1<) de las características más importantes en cualquier lenguaje de programación es el uso de nombres. Cuando se crea un
objeto. se proporciona un nombre a un área de almacenamiento. Un método, por su paJ1e, es un nombre que sirve para desig-
nar una acción. Utilizamos nombres para referimos a todos los objetos y metodos. Una serie de nombres bien elegida crea-
rú un sistema que resultará más fácil de entender y modificar por otras personas. En cierto modo, este problema se parece
al acto dL' escribir literanlra: el objetivo es comunicarse con los lectores.
Todos los problemas surgen a la hora de aplicar el concepto de matiz del lenguaje humano a los lenguajes de programación.
A menudo. una misma palabra tiene diferentes significados: es lo que se denomina palabras polisémicas, aunque en el campo
de la programación diríamos que están sobrecargadas. Lo que hacemos normalmente es decir "Lava la camisa", "Lava el
coche" y '"Lava al pelTa": sería absurdo vernos forzados a decir "camisaLava la camisa". "cocheLava el coche" y
"perroLava el pelTa" simplemente para que el oyente no se vea forzado a distinguir cuál es la acción que tiene que realizar.
La mayoria de los lenguajes humanos son redundantes, de modo que podemos seguir detenninando el signiticado aún cuan-
do nos perdamos algunas de las palabras. No necesitamos identificadores unívocos: podemos deducir el significado a par-
tir del contexto.
La mayoría de los lenguajes de programación (y C en panicular) exigen que dispongamos de un identificador unívoco para
cada metodo (a menudo denominados.fimciones en dichos lenguajes). Así que no se pl/ede tener una función denominada
print( ) para imprimir entt:'"ros y otra denominada igualmente print( ) para imprimir números c-n coma flotante, cada una de
las funciones necesitará un nombre distintivo.
En Java (yen e+-t-). hay otro factor que obliga a sobrecargar los nombres de los métodos: cl constructor. Puesto que el nom-
bre del constructor está predetemlinado por el nombre de la clase, sólo puede haber un nombre de constmctor. Pero enton-
ces. ¿qué sucede si queremos crear un objeto utilizando varias formas distintas? Por ejemplo, suponga que construimos una
clase cuyos objetos pueden inicializarse de la fonna nomlal o leyendo la infonnación de un archivo. Harán falta dos cons-
tructores, el constructor predeterminado y otro que tome un objeto String como argumento, a través del cual suministrare-
mos el nombre del archivo que hay que utilizar para inicializar el objeto. Ambos metodos serán constructo res, así que
tendrán el mismo nombre: el nombre de la clasc. Por tanto, la sobrecarga de metodos resulta esencial para poder utilizar el
mismo nombre de metodo con diferentes tipos de argumentos. Y. aunque la sobrecarga de metodos es obligatoria para los
constructores. también resulta útil de manera general y puede ser empleada con cualquier otro método.
He aquí un ejemplo que muestra tanto constmclores sobrecargados como metodos normales sobrecargados:
/ /: initialization/Overloadi ng. java
// Ilustración del mecanismo de sobrecarga
/.1 canto de constructores como de métodos normales.
~mporc scatic net.mindview.u ti l.Prin t.*·
class Tree {
ine height i
Tree () {
print ( 11 Plant i,-¡g a seedling") ¡
height =: O i
Tree(int inicialHeight)
height =: initialHeight¡
print ("Creating new Tree that is " +
height + " feet. tall") i
void info ()
print ("Tre e is + height + 11 feet tall") ¡
void info(String s)
print (s + ": Tree is 11 + height + " feet tall") ¡
88 Piensa en Java
1/ Constructor sobrecargado:
new Tree () ;
/* Output:
Creating new Tree that is O feet tall
Tree is O feet tall
overloaded methad: Tree is O feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded methad: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded methad: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded methad: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded methad: Tree is 4 feet tall
Planting a seedling
./ // >
Con estas definiciones, podemos crear un objeto Tree tanto a partir de una semilla, sin utilizar ningún argumento, como en
fonna de planta criada en vivero, en cuyo caso tendremos que indicar la alnlra que tiene. Para soportar este comportamien-
to, hay un constructor predetenninado y otro que toma como argumento la altura del árbol.
También podemos invocar el método info() de varias formas distintas. Por ejemplo, si queremos imprimir un mensaje adi-
cional, podemos emplear info(String), mientras que utilizaríamos info() cuando no tengamos nada más que decir. Sería bas-
tante extraño proporcionar dos nombres separados a cosas que se corresponden, obviamente. con un mismo concepto.
Afortunadamente, la sobrecarga de métodos nos pennite utilizar el mismo método para ambos.
1* Output :
String: String first, int: 11
int: 99, String: Int first
*///,-
Los dos métodos f() tienen argumentos idénticos, pero el orden es distinto yeso es lo que los hace diferentes.
void testConstVal()
printnb (U 5: n);
f1(S) ;f2(S) ;f3(S) ;f4(S) ;fS(S) ;f6(S) ;f7(S); print();
void testChar () {
char x = 'x' i
printnb ("char: ") i
fl(x) ;f2 (x) ;f3 (x ) ;f4 (x) ;fS(x) ;f6(x) ;f7(x); print();
void testByte () {
byte x = Oi
printnb (ltbyte: ");
f1(x) ;f2 (x ) ;f3(x) ;f4 (x) ;fS(x) ;f6(x) ;f7(x); print();
void testShort () {
short x = O;
printnb (" short: n) i
f1 (x ) ; f2 (x ) ; f3 (x ) ; f4 (x ) ; fS (x) ; f6 (x) ; f7 (x ); print () ;
void testlnt (l {
int x = O;
printnb ( " int: ");
f1(x) ;f 2(x) ;f 3(x) ;f4 (x ) ; fS(x) ;f6(x) ;f7 (x) ; print();
)
void testLong () {
long x = O;
printnb(1I1ong : It);
f1(x) ;f2 (x ) ;f3 (x ) ;f4 (x ) ;fS(x) ;f6(x) ;f7(x); print () ;
void testFloat () {
float x = O;
printnb("float: ");
f1 (x ) ; f2 (x) ; f3 (x ) ; f4 (x ) ; fS (x) ; f6 (x) ; f7 (x); print () ;
void testDouble () {
double x = O;
printnb ("doubl e: ");
f1 (x) ; f2 (x ) ; f3 (x ) ; f4 (x ) ; fS (x ) ; f6 (x) ; f7 (x ); print () ;
/ * Output:
50 f1 (int) f2(int) f3 (int) f4(int) fS ( long ) f6 ( float ) f7 (double )
charo f1 (cha r ) f2 (int) f3 (int) f4 (int) fS ( long ) f6 ( float ) f7 (double)
byte o f1 (byte ) f2 (byte) f3 (short) f4 (i nc ) fS (long) f6 ( float ) f7 (double )
shorto f1(short) f2 (s hort ) f3(short) f4 (in t ) fS(long ) f6 ( float ) f7 (doub le )
into f1 ( int ) f2(int) f3 ( inc) f4 (i nt) fS(long) f6 ( float ) f7 (double)
longo f1 (long) f2 ( long) f3 (long) f4 (long) fS (long) f6 (floa t ) f7 (double)
floato f1(float ) f2(float) f3(float) f4(float) fS(float ) f6 ( float) f7 (double)
doubleo fl(double) f2 (double ) f3(double) f4(double) fS (double) f6(double) f7(double)
*/// 0-
5 Inicialización y limpieza 91
puede ver que el valor constante 5 se trata como ¡ut, por lo que si hay disponible un método sobrecargado que lOma un obje-
to ¡ot. se utilizara dicho mélOdo. En todos los demás casos, si lo que tenemos es un tipo de datos más pequeño que el argu-
mento del método, di cho tipo de datos será promocionado. char produce un efecto ligeramente diferente, ya que, si no se
encuentra una co rrespondencia exacta con cbar, se le promociona a int.
¿Qué sucede si nu estro argumenlO es mayor qu e el argumento esperado por el método sob recargado? Una modificación del
programa ant eri or nos da la respuesta:
11: initialization/Demotion.java
JI Reducción de primitivas y sobrecarga.
import static net.mindview.util.Print.*;
void testDouble()
double x = o;
print {Udouble argument : 11 l i
fl (x) ; f2 ( (float) x) ; f3 ( (long) x) ; f4 ( (int) x ) ;
f5 ( (s h ort) x) ; f6 ( (byte) x ) ; f7 ( (char) x) ;
1* Output :
double argument:
92 Piensa en Java
f1 (double )
f2 (float )
f3 ( long )
f4 (int )
f5 (short }
f6 (byte }
f7 (char )
* /// ,-
Aquí, los métodos admiten valores primitivos más pequeños. Si el argumento es de mayor anchura, entonces será necesario
efectuar una conversión de estrechamiento mediante una proyección. Si no se hace esto, el compilador generará un men-
saje de error.
Esto podría funcionar siempre y cuando el compilador pudiera detenninar inequívocamente el significado a partir del con-
texto, como por ejemplo en int x = f(). Sin embargo, el lenguaje nos permite invocar un método e ignorar el valor de retor-
no, que es una técnica que a menudo se denomina invocar un método por su efecto colateral, ya que no 110S preocupa el
valor de retorno, sino que queremos que tengan lugar los restantes efectos de la invocación al método. Luego entonces, si
invocamos el método de esta fonna:
f () ;
¿Cómo podría Java detenninar qué método f( ) habría que invocar? ¿Y cómo podría determinarlo alguien que estuviera
leyendo el código? Debido a este tipo de problemas, no podemos utilizar los tipos de los valores de retomo para distinguir
los métodos sobrecargados.
Constructores predeterminados
Como se ha mencionado anteriormente, un constructor predetenninado (también denominado "constructor sin argumentos" )
es aquel que no tie ne argumentos y que se uti liza para crear un "objeto predetenninado". Si creamos una clase que no tenga
constructores, el compilador creará automáticamente un constructor predetenninado. Por ejemplo:
11 : i n i t ializa t i o n / Defaul tCo n s tructo r. j a v a
c l ass Bird ()
La expresión
new Bird ()
crea un nuevo objeto y llama al constructor predetemlinado, incluso aunque no se baya definido lino de manera explícita.
Sin ese constructor predeternlinado no dispondríamos de ningún método al que invocar para construir el objeto. Sin embar-
go, si definimos algún constructor (con o sin argumentos), el compilador no sin/enlizará ningún constructor por nosotros:
11 : initialization/ NoSynthesis.java
class Bird2 {
5 Inicialización y limpieza 93
Bü-d2 {int i ) {}
2ird2 1double d I {}
/ I / ,-
Si c:,c ribimos:
:..e.w Bi rd2 ()
el compilador nos indicara que no puede localizar ningílll constmctOr que se corresponda con la instrucción que hemos
escr ito. Cuando no defi nimos explícitamente ningún cons tmctor. es como si el compi lador dijera: "Es necesario ut il izar
algún constmcror. así que déjame definir uno por ti". Pero, si escribimos al menos constructor. el compilador dice: "Has
escrito un constructor, así que tu sabrás 10 que estás haciendo: si 110 has incluido uno predeterminado es porque no quie-
res hacerlo".
Ejercicio 3: (1) Cree ulla clase con un constructor predeterminado (uno que no tome ningún argumento) que imprima
un mensaje. Cree UIl objeto de esa clase.
Ejerc ici o 4: ( 1) Aiiada un constructor sob recargado a l ejercic io anterior que admita un a rgumen to de tipo Strin g e
imprima la correspondiente cadena de cflracteres junto con el mensaje.
Ejercicio 5: (2 ) Cree IIna clase denominada Dog con un método sobrecargado bark( ) (método ··ladrar"). Este método
debe es tar sobrecargado basándose en diversos tipos de datos primi tivos y debe imprimir d iferel1les tipos
de ladridos. gruñidos, etc .. dependiendo de la versión sobrecargada que se invoque. Escriba un método
m aín ( ) que invoque todas las distintas versiones.
Ejercicio 6: ( 1) .\!lodilique e l ejercici o 3merior de modo que dos de tos métodos sobrecargados tengan dos argumen-
tos (de dos tipos di stintos). pero en orden inverso LIno respecto del otro. Verifiquc que estas detíniciones
funcionan.
Ejercicio 7: (1) Crec una c lase ~ in ningún cünstruc(Qr y luego cree un objeto de esa clase en main( ) para veriticnr que
SI.? sintetiza automa¡ieamente el constructor predetcnninado.
Si sól o ha y un único método denominado peel( ), ¿cómo puede ese metoJo saber si está siendo llamado para el objcro a o
para el objeto b?
94 Piensa en Java
Para poder escribir el código en una sintaxis cómoda orientada a objetos, en la que podamos "enviar un mensaje a un obje-
to" , el compilador se encarga de reali zar un cierto trabajo entre bastidores por nosotros. Existe un primer argumento secre-
to pasado al método peel(), y ese argumento es la referencia al objeto que se está manipulando. De este modo, las dos
llamadas a métodos se convierten en algo parecido a:
Banana.peel(a, 1) ¡
Banana.peel(b, 2) i
Esto se realiza internamente y nosotros no podemos escribir estas expresiones y hacer que el compilador las acepte, pero
este ejemplo nos basta para hacernos una idea de lo que sucede en la práctica.
Suponga que nos encontramos dentro de un método y queremos obtener la referencia al objeto aChlal. Puesto que el compi-
lador pasa esa referencia secretamente, no existe ningún identificador para ella. Sin embargo, y para poder acceder a esa
referencia, el lenguaje incluye una palabra clave específica: this. La palabra clave this, que sólo se puedc emplear en méto-
dos que no sean de tipo static devuelve la referencia al objeto para el cual ha sido invocado el método. Podemos tratar esta
referencia del mismo modo que cualquier otra referencia a un objeto. Recuerde que, si está invocando un método de la clase
desde dentro de otro método de esa misma clase, no es necesario utilizar this, si no que simplemente basta con invocar el
método. La referencia this actua l será utilizada automáticamente para el otro método. De este modo, podemos escribir:
jj : initialization j Apricot.java
public class Apricot {
void pick l) { / * ... * /
void pit 11 { pick 11 ; / * .. * / }
// / ,-
Dentro de pite ), podríamos decir Ihis.pick( ) pero no hay ninguna necesidad de hacerlo. ' El compilador se encarga de
hacerlo automáticamente por nosotros. La palabra clave this sólo se usa en aque llos casos especiales en los que es necesa-
rio utilizar explícitamente la referencia al objeto aChla l. Por ejemplo, a menudo se usa en instrucciones return cuando se
quiere devolver la referencia al objeto actual:
jj : initialization/Leaf.java
/ / Uso simple de la palabra clave "this".
void print ()
System.out.println("i = " + i) ¡
j * Output:
i = 3
* ///,-
Puesto que increment() devuelve la referencia al objeto actual a través de la palabra clave this, pueden realizarse fácilmen-
te l11ultiples operaciones con un mismo objeto.
La palabra clave this también resulta úti l para pasar el objeto actual a otro método:
1 Algunas personas escriben obsesivamente this delante de cada llamada a método O referencia a un campo argumentando que eso hace que el código
sea "mas claro y más explicito". Mi consejo es que no lo haga. Existe una razón por la que utilizamos los lenguajes de alto nivel, y esa razón es que estos
lenguajes se encargan de hacer buena parte del trabajo por nosotros. Si incluimos la palabra clave this cuando no es necesario, las personas que lean el
código se sentiran confundidas, ya que los demás programas que hayan leido en cualquier parte 110 wili=an las palabra clave Ihis de manera continua.
Los programadores esperan que this sólo se use allí donde sea necesario. El seguir un estilo de codificación cohercllIe y simple pemlite ahorrar tiempo
y dinero.
5 Inicialización y limpieza 95
class Person {
p'..lblic void eat (Apple apple) {
Apple peeled = apple. get.Peeled () ;
System. out. . prin':.ln ( "Yl.H~1my") ;
class ?eeler {
static Apple peel \Apple apple l
/1 pelar
retur n apple i / / Pela da
c:ass P-.pple {
.ll..pple getPeeled () { return Peeler . peel ( this ) i }
/ * OU tput :
Yummy
* /// :-
El objeto Apple (manzana) necesita invocar Peeler.peel( ), que es un metodo de utilidad ex temo que lleva a cabo una ope-
ración que. por alguna razón, necesita ser ex tema a Apple (quizá ese mélodo ex temo pueda ap lica rse a muchas clases dis-
tintas y no queremos repeti r el código). Para que el objeto pueda pasarse a si mismo al método extcmo. es necesari o emplear
Ihis
Ejercicio 8: (1) Cree ulla clase con dos melodos. Dent ro del primer método invoque a l segu ndo método dos veces: la
primera vez sin util izar this y la segunda utilizando dic ha palabra c lave. Realic e este eje mp lo simpleme n-
te para ver cómo funciona el mecan ismo. no debe utilizar esta forma de invocar a los métodos en la
práctica.
Flower (String ss ) {
print ( "Constructor w/ String arg only, s " + ss ) ;
s :: ss;
void printPetalCount () {
// ! this (11 ) ; JI
¡NO dentro de un no-constructor!
print ( lIpetalCount = " + petalCount + s = "+ s ) ;
/ * Output:
Constructor w/ int arg only, petalCount= 47
String & int args
default constructor (no args)
petalCount = 47 s = hi
*/ // ,-
El constructor Flower(String s, int petals) muestra que, aunque podemos invocar un constructor uti lizando this, no pode-
mos invocar dos. Además, la llamada al constructor debe ser lo primero que hagamos, porque de lo contrario obtendremos
un mensaje de error de compi lación.
Este ejemplo también muestra otro modo de utilización de this. Puesto que el nombre del argu mento s y el nombre del
miembro de datos s son iguales, existe una ambigüedad. Podemos resolverl a utilizando this.s, para dejar claro que estamos
refiriéndonos al miembro de datos. Esta fanna de utilización resulta muy habitual en el código Java y se emplea en nume-
rosos luga res del libro.
En printPctalCount( ) podemos ver que el compilador no nos pennite invocar un constructor desde dentro de cualquier
método que no sea un constructor.
Ejercicio 9: (1) Cree una clase con dos constructores (sobrecargados). Utiliza ndo this, in voq ue el segundo constructor
desde dentro del primero.
El significado de static
Teniendo en mente el significado de la palabra clave this, podemos comprender mejor qué es lo que implica definir un méto-
do como static. Significa que no existirá ningún objeto this para ese método concreto. No se pueden invocar métodos no
static desde dentro de los métodos static2 (aunque la in versa sí es posible), y se puede invocar un método static para la pro-
pia clase, sin especificar ningún objeto. De hecho, esa es la principal aplicación de los métodos static: es como si estuviéra-
mos creando el equivalente de un método global. Sin embargo, los métodos globales no están pennitidos en Java, y el incluir
el método static dentro de una clase pennite a los objetos de esa clase acceder a métodos static y a campos de tipo static.
Algunas personas argumentan que los métodos estáticos no son orientados a objetos, ya que ti enen la semántica de un méto-
do global. Un método estático no envía un mensaje a un objeto, ya que no existe referencia this. Probablemente se trate de
2 El único caso en que esto puede hacerse es cuando se pasa al método stalic una referencia a un objeto (el método stalic también podría crear su propio
objeto). Entonces, a travcs de la referencia (que ahora será. en la práctica, Ihis), se pueden invocar métodos no stlltiC y acccder a campos no static . Pero.
norma lmente si quercmos hacer algo como esto, lo mejor es que escribamos un método no static nonnal y comente.
5 Inicialización y limpieza 97
UIl arg,U1l10;;'1l10 correcto. y si So;;' encll~llIra al guna \t.~Z lIlili 7<lIldo una gran c;:lIltiJad de me IOdos estáticos probablemente Cúll-
\'enQa 'lUL' \"lH:I\'a a Illeditar sobre I:J estrategia que esta ~mpleando. Sin embargo. los melOdos y valores es¡úticos rL'sultan
b<l 5~1 111 e prácti(os. y hay ocasiol1L's cn las que de \'crdad son necesarios. por lo que la cuestión de si se trat;J. Lie \'erdadera
prl.l~r8.lllación orien!:ld<:1 a objetos es mejor dejársela a los teóricos.
Esta c:-. una posible fuellle de C[Tor de programación. porque algullos programadores. especialmente los que provienen Jcl
campo 0('1 C+....... pueden confundirse inicialmente y considerar que finalize( ) es el des/me/o/" de C+-'-. que es una función
ql1C Sé il1\ oca sicl/lp/"I! cuando se destruye un objeto. Es importante entender la distinción qlle existe entre C.,...+ y .lava a este
re...¡p~dü. porqUL' en C . . . + ./05 Ob/CID.' sif! lIIpre se d eSl/"uyell (en un programa libre de errores). mientras que en .lava. el depu-
!"<ldor de Illemoria no siempre procesa los objelOs. Dicho de otro moJo:
(,Significa esto qUe. si el objeto contiene otros objetos. finalize( ) debe Iiber3r explícitamente esos otros objetos '~ En renli-
dnd no: el depurador de memoria se ocupa de libernr toda la mcmoria de objetos. independiL'lltL'mente de cómo estos hayan
sido creados, En resumen. finalize( ) sólo es necesario en aquellos casos ..:-speciales en los que ..:-1 objeto pueda asignar espa-
cio d..:- almacenamiento utilizando alguna técnica distinta de la propia creación de objetos, Algún lector especialmcl1tt:' aten-
to podría argumentar: pero. si roda .laya ..:-s un objeto. (,cómo pu..:-de llegar a producirse esta siulación?
Parece que finalize( ) se ha incluido en el lenguaje debido a la posibilidad de que el programador realice alguna acti\ 'illad
de estilo C. asignando memoria mediante algún mecanismo distinto del normalmente cmpleado en Java. Esto puede suce-
der. principalmente. a través de los métodos Ilari, 'us. que son una f0I111a de in\'ocar desde .laya código esclito en un lengua-
je distinto de .I ay.:! (los métodos natiyos se estudian en el Apéndice B de la segunda edk'ion electrónica de este libro.
disponible en )\"l1'\\', J\lindllt'H'.l1ct), C y C++ son los únicos lenguajes actualmente soportados por los métodos nati\'os. pero
como desde ellos se pueden inyocar subprogramas en otros lenguajt!s. en la práctica pod remos imeocar cualquier cosa que
queramos, Dentro del código no Java, podría invocarse la familia de funciones malloc( ) de C para as ignar esp.:!cio de alma-
cenamiento. y a menos que invoquemos free( ). dicho espacio de almacenamiento no será liberado. provocando una fuga de
memoria. Por supuesto. free( ) es una función de ( y e+ +. por lo que sería necesario im'ocarla mediant..:- Ull método nativo
dentro de finalize( ),
Después de estas explicaciones. el lector probablemente estará pensando que no va a tener que utilizar finalize( ) de forma
demasiado frecuente,3 En efecto. es así: dicho método no es el lugar apropiado para realizar las tareas normales de limpie-
za. Pero. entonces ¿dónde lle\"J a cabo esas ta reas n0n11ales de limpieza?
Por contraste. Java no permite crea r objetos locales. sino que siempre es necesario utilizar new. Pero en Java, no ex iste nin-
gún operador "delete" para liberar el objeto. porque el depurador de memoria se encarga de liberar el espacio de almacena-
miento por nosotros. Por (anta. desdc un punto de vista simplista, podríamos decir que debido a la depuración de memoria
Java no dispone de destntctorcs, Sin embargo. a medida que a\'ancemos en el libro veremos que la presencia de un depura-
dor de memoria no elimina ni la necesidad ni la utilidad de los destructores (y recuerde que no se debe ill\'ocar finalize( )
directamente. por lo que dicho método no constituye una solución). Si queremos que se realice algún tipo de limpieza dis-
tinta de la propia liberación dd espacio de almacenamiento. sigile siendo nt'l'csario invocar explícitamente un método apro-
piJdo L' n Ja\i1. que será el equi\"alente dd destructor de C+ + pero sin la comodidad asociada;) éste,
Reí.'uerde que no están garantizadas ni la depuración de rllemoria ni la finalización, Si la máquina \'jrtllal Java (JVMl no está
próxima J quedarse sin memo ria. puede qUe no pierda tiempo recuperando espacio mediante el mecanismo de depuración
de mcmoria,
La condición de terminación
En general. no podemos confiar en que finalize( ) ~ea invocado y es necesario crear métodos de "limpieza" separados c invo-
carlos explícitamente, Por tanto. parece 4ue tin a lize( ) só lo resulta útil pa ra oscuras tareas de limpieza de memoria que la
mayoría de los programadores nUllca van a tener que utilizar, Sin embargo. existe un caso interesante de uso de tinalize( )
qut.' IW depende de que dicha función sea invocada todas las \-eces, Nos referimos a la verificación de la condiciún dI! tel'-
Illil/(/ciúl(~ de un objeto,
-' Joshua Blo('h.::.; toda\ ía mas tajanll' en ~u Sl"el'i':H1 "EI 'ilt' ¡(J I 1¡1I(/!i~,ldorcs" : "l os tlthllizadores son Itl1pn:del'i h k~, a menudll peligro<;n~ y getler<llmenl.c
inne('e~ano<· . Ellnli rt' JílrdT.tt Pro,'!.lw11llIing LJII,i;lIílg<' Gllide, p. 20 [Addi~ o n - Wl'~k~. 20U I J.
~ La tcmllllll acur'iauu por Bill Vellners IIt "It ·I\ ..~/'lIlIl (! . c(lm l en Utl sC'm inano qUe 1111parttllWs conjull!al1lC'l1te
5 Inicialización y limpieza 99
En el momento en que ya no estemos interesados en un objeto (cuando esté listo para ser borrado) dicho objeto deberá
encontrarse en un estado en el que su memoria debe ser liberada sin riesgo. Por ejemplo, si el objeto representa un archivo
abierto, el programador deberá cerrar dicho archivo antes de que el objeto se vea sometido al proceso de depuración de
memoria. Si alguna parte del objeto no se limpia apropiadamente, tendremos un error en el programa que será muy dificil
de localizar. Podernos utilizar finaHze( ) para descubrir esta condición, incluso aunque dicho método no sea siempre in vo-
cado. Si una de las finali zaciones nos pennite detectar el error, habremos descubierto el problema, que es lo único que real-
mente nos importa.
He aq uí un ejemplo simple de cómo podría emplearse dicho método:
11 : i n itialization/TerminationCondition . java
II USO de finalize() para detectar un objeto
II que no ha sido limpiado apropiadamente.
class Book {
boolean checkedOut = f a l se;
Book (boolean checkOu t ) {
checkedOut = checkOut¡
void check In ()
checkedOut = false¡
1* Output:
Error: checked out
* /// ,-
La condición de terminación es que se supone que todos los objetos Book (libro) deben ser devueltos (check in) antes de
que los procese el depurador de memoria, pero en main() hay un error de programación por el que uno de los libros no es
devuelto. Sin finalize() para verifi car la condición de tenninación, este error puede ser dificil de localizar.
Observe que se utiliza System.gc() para forzar la finalización . Pero, incluso aunque no usáramos ese método, resulta alta-
mente probable que llegáramos a descubrir el objeto Book erróneo ejecutando repetidamente el programa (suponiendo que
el programa asigne un espacio de almacenamiento suficiente como para provocar la ejecución del depurador de memoria).
Por regla general, debemos asum ir que la versión de finalize() de la clase base también estará llevando a cabo alguna tarea
impo rtante, por lo que convendrá invocarla utilizando super. CalDO puede verse en Book.finalize(). En este caso, hemos
desac ti vado esa llamada mediante comentarios, porque requiere utilizar los mecanismos de tratamiento de excepciones de
los que aún no hemos hablado en detalle.
Ejercicio 10: (2) Cree una clase con un método finalize() que imprima un mensaje. En main(), cree un objeto de esa
clase. Explique el comportamiento del programa.
Ejercicio 11: (4) Modifique el ejercicio anterior de modo que siempre se invoque el método finalize().
10C Piensa en Java
Ejercicio 12: (4) Cree una clase denominada Ta nk (tanque) que pueda ser llenado y vaciado, y cuya condición de ter-
minación es que el objeto debe estar vacío en el momento de limpiarlo. Escriba un método finalize() que
\ critique esta condición de tenninación. En maine ), compruebe los posibles casos que pueden producir-
se al utilizar los objetos Tank.
traza, entrando en el objeto al que apunta la referencia y siguiendo a continuación todas las referencias incluidas en ese obje~
too entrando en los objetos a los que esas referencias apuntan, etc., hasta recorrer todo el árbol que se origina en la referen~
cia sinlada en la pila o en almacenamiento estático. Cada objeto a través del cual pasemos seguirá estando vivo. Observe
que no se presenta el problema de los gmpos auto-referenciales: los objetos de esos grupos no serán recorridos durante este
proceso de construcción del árbol, por lo que se puede deducir automáticamente que hay que depurarlos.
En la técnica que acabamos de describir, la NM utiliza un esquema de depuración de memoria adaplal;vo, en el que lo que
hace con los objetos vivos que localice dependerá de la variante del esquema que se esté utilizando actualmente. Una de
esas variantes es parar y copiar, lo que quiere decir que (por razones que luego comentaremos) se detiene primero el pro~
grama (es decir, no se trata de un esquema de depuración de memoria que funcione en segundo plano). Entonces cada obje-
to vivo se copia desde un punto del cúmulo de memoria a otro, dejando detrás todos los objetos muertos. Además, a medida
que se copien los objetos en la nueva zona del cúmulo de memoria, se los empaqueta de modo que ocupen un espacio de
almacenamiento mínimo compactando así el área ocupada (y pennitiendo que se asigne nuevo espacio de almacenamiento
inmediatamente a continuación del área recién descrita, como antes hemos comentado).
Por supuesto, cuando se desplaza un objeto de un sitio a otro, es preciso modificar todas las referencias que apuntan al obje-
to. Las referencias que apunten al objeto desde el cúmulo de memoria o el área de almacenamiento estático pueden modifi-
carse directamente, pero puede que haya otras referencias apuntando a este objeto que sean encontradas posterionnente,
durante el proceso de construcción del árbol. Estas referencias se irán modificando a medida que sean encontradas (imagi~
ne, por ejemplo, que se utilizara lIna tabla para establecer la correspondencia entre las antiguas direcciones y las nuevas).
Hay dos problemas que hacen que estos denominados "depuradores copiadores" sean poco eficientes. El primero es la nece-
sidad de disponer de dos áreas de cúmulo de memoria, para poder mover las secciones de memoria entre una y otra, lo que
en la práctica significa que hace falta el doble de memoria de la necesaria. Algunas máquinas NM resuelven este proble-
ma asignando el cúmulo de memoria de segmento en segmento, según sea necesario, y simplemente copiando de un seg-
mento a otro.
El segundo problema es el propio proceso de copia. Una vez que el programa se estabilice, después de iniciada la ejecución,
puede que no genere ningún objeto muerto o que genere muy pocos. A pesar de eso, el depurador copiador seguirá copian~
do toda la memoria de un sitio a otro, lo que constituye un desperdicio de recursos. Para evitar esto, algunas máquinas JVM
detectan que no se están generando nuevos objetos muertos y conmutan a un esquema di stinto (ésta es la parte "adaptati-
va''). Este otro esquema se denomina marcar y eliminar, y es el que utilizaban de manera continua las anteriores versiones
de la NM de Sun. Para uso gene ral , esta técnica de marcar y eliminar es demasiado lenta, pero resulta, sin embargo, muy
rápida cuando sabemos de antemano que no se están generando objetos muertos.
La técnica de marcar y eliminar sigue la misma lógica de comenzar a partir de la pila y del almacenamiento estático y tra-
zar todas las referencias para encontrar los objetos vivos. Sin embargo, cada vez que se encuentra un objeto vivo, éste se
marca activando un indicador contenido en el mismo, pero sin aplicarle ningún mecanismos de depuración. Sólo cuando el
proceso de marcado ha terminado se produce la limpieza. Durante esa fase, se libera la memoria asignada a los objetos muer-
tos. Sin embargo, hay que observar que no se produce ningún proceso de copia, por lo que si el depurador de memoria deci-
de compactar un cúmulo de memoria fragmentado. tendrá que hacerlo moviendo los objetos de un sitio a otro.
El concepto de " parar y copiar" hace referencia a la idea de que este tipo de depuración de memoria 110 se hace en segundo
plano; en lugar de ello, se detiene el programa mientras tiene lugar la depuración de memoria. En la documentación técni-
ca de Sun podrá encontrar muchas referencias al mecanismo de depuración de memoria donde se dice que se trata de un
proceso de segundo plano de baja prioridad, pero la realidad es que la depuración de memoria no estaba implcmcntada de
esa fonna en las primeras máquinas NM de Sun. En lugar de ello, el depurador de memoria de Sun detenía el programa
cuando detectaba que había poca memoria libre. La técnica de marcar y limpiar también requiere que se detenga el pro-
grama.
Como hemos mencionado anteriormente, en la máquina NM descrita aquí. la memoria se asigna en bloques de gran tam a-
ño. Si asignamos un objeto grande, éste obtendrá su propio bloque. La técnica de detención y copiado estricta requiere que
se copien todos los objetos vivos desde el cúmulo de memoria de origen hasta un nuevo cúmu lo de memoria antes de poder
liberar el primero, lo que implica una gran cantidad de memoria. Utilizando bloques, el mecanismo de depuración de memo-
ria puede nonnalmente copiar los objetos a los bloques muertos a medida que los va depurando. Cada bloque dispone de un
contador de generación para ve r si está vivo. En el caso nonnal , sólo se compactan los bloques creados desde la última pasa-
da de depuración de memoria ; para todos los demás bloques se incrementará el contador de gcneración si han sido refcren~
ciados desde algún sitio. Esto pennite gestionar el caso nOffi1al en el que se dispone de un gran número de objetos temporales
102 Piensa en Java
de corta duración. Periódicamente, se hace una limpieza completa. en )a que los objetos de gran tamaño seguirán sin ser
copiados (simplemente se incrementará su contador de generación) y los bloques que comcngan objclOs pequeños se copia-
rán y compactarán. La maquina JVM monitori za la eficiencia del depurador de memoria y, si este mecanismo se convierte
en una pérdida de tiempo porque todos los objetos son de larga duración, conmuta al mecanismo de marcado y limpieza. De
f0n113 similar, la JVM controla hasta qué punto es efectiva la técnica de marcado y limpieza, y si el cumulo de memoria
comienza a estar fragmentado, conmuta almccanismo de detención y copiado. Aquí es donde entra en acción la parte "adap-
tativa" delmccanismo, al que podríamos describir de manera rimbombante como: " mecanismo adaptativo generacional de
detención-copiado y marcado-limpieza".
Existen varias posibles optimizaciones de la velocidad de una máquina NM. Una especialmente importante afecta a la ope-
ración del cargador y es lo que se denomina compilador jusl-in-lime (liT). Un compilador JlT convierte parcial o totalmen-
te un programa a código máquina nativo. de modo que dicho código no necesite ser interpretado por la JVM , con lo que se
ejecutará mucho más rápido. Cuando debe cargarse una clase (normalmente. la primera vez que queramos crear un objeto
en esa clase) se localiza el archivo .class y se carga en memoria el código intermedio correspondiente a dicha clase. En este
punto, una posible técnica consiste en compilar simplemente todo el código, para generar un código máquina. pero esto tiene
dos desventajas: necesita algo más de tiempo, lo cual (si tenemos en cuenta toda la vida del programa) puede representar
una gran cantidad de recursos adicionales; e incrementa el tamaño del ejecutable (el código intennedio es bastante más COI11-
pacto que el código JlT expandido), y esto puede provocar la aparición del fenómeno de paginación, lo que ralentiza enor-
memente los programas. Otra técnica alternativa es la evalllación lenta, que consiste en que el código intennedio no se
compila para generar código máquina hasta el momento necesario. De este modo, puede que nunca se llegue a compi lar el
código que nunca llegue a ejecutarse. Las tecnologías HotSpot de Java en los kits de desarrollo JDK recientes adoptan una
técnica simi lar, optimizando de manera incremental un fragmento de código cada vez que se ejecuta, por lo que cuanto más
veces se ejecut e, más rápido lo hará.
Inicialización de miembros
Java adopta medidas especiales para garantizar que las variables se inicialicen adecuadamente antes de usarlas. En el caso
de las variables locales de un método, esta garantía se presenta en la forma de errores en tiempo de compilación. Por tamo,
si escribimos:
void f ( ) (
int i¡
i++¡ II Error -- i no inicializada
obtendremos un mensaje de error que dice que i puede no haber sido no inicializada. Por supuesto, el compi lador podría
haber dado a i un valor predetenninado, pero el hecho de que exista una variable local no inicializada es un error del pro-
gramador, y el valor predetenninado estaría encubriendo ese error. Forzando al programador a proporcionar un valor de ini-
cialización. es más probable que se detecte el error.
Sin embargo, si hay un campo de tipo primitivo en una clase, las cosas son algo distintas. Como hemos visto en el Capítulo
2, Todo es 1111 objeto. se garantiza que cada campo primitivo de una clase contendrá un valor inicial. He aquí un programa
que pennite verificar este hecho y mostrar los valores:
11 : initialization/ lnitialValues.java
II Muestra los valores iniciales predeterminados.
import static net.mindview.util.Print.*¡
void printlnitialValues () {
print ( "Data type Initial value") i
print ("boo lean " + tl;
print ("char [" + e + "] 01 ) ;
print ( "byte + b ),
print (" short + s);
print ( " int + i);
print{"long + 1),
print{"float + f ),
print ( lIdouble + d);
print(!lreference + reference);
1* Output:
Data type Initial value
boolea n false
char
byte O
short O
int O
long O
float 0.0
double 0.0
reference null
*/11 ,-
Puede ver que, aún cuando no se han especificado los valores, los campos se inicializan automáticameme (el valor corres-
pondiente a char es cero, lo que se imprime como un espacio). De modo que, al menos, no existe ningún riesgo de llegar a
trabajar con variables no inicializadas.
Cuando definimos una referencia a objeto dentro de una clase sin inicializarse como nuevo objeto, dicha referencia se ini-
cializa con el valor especial null.
Especificación de la inicialización
¿Qué sucede si queremos dar un valor inicial a una variable? Una forma directa de hacerlo consiste, sim plemente, en asig-
nar el va lor en el punto en que deftnamos la variable dentro de la clase (observe que no se puede hacer esto en C++, aun-
que los programadores novatos de C++ siempre lo intentan). En el siguiente fragmento de código, se modifican las
definiciones de los campos de la clase lnitialVa lu es para proporcionar los correspondientes va lores iniciales:
j/ : initialization/lniti alVal ues2 . java
jj Definición de valores iniciales explícitos.
También podemos inicializar objetos no primitivos de la misma fomla. Si Depth es una clase, podemos crear una variable
e in icializarla como sigue:
1/: initialization/Measurement.java
class Depth {}
II .. .
111 >
entonces i se inicializará primero con el va lor O, y luego con el va lor 7. Esto es cierto para todos los tipos primitivos y tam-
bién para las referencias a objetos, incluyendo aquellos a los que se inicialice de manera explíc ita en el punto en el que se
los defina. Por esta razón, el compilador no trata de obligamos a inicializar los elementos dentro del constructor en ningún
sitio concreto o antes de utilizarlos: la inicialización ya está garantizada.
Orden de inicialización
Dentro de una clase, el orden de inicialización se detennina mediante el orden en que se definen las variables en la clase.
Las defllliciones de variables pueden estar dispersas a través de y entre las definiciones de métodos, pero las variables se
inicializan antes de que se pueda invocar cualquier método, incluso el constructor. Por ejemplo:
11 : initialization/OrderOfInitialization.java
II Ilustra el orden de inicialización.
import static net.mindview.util.Print.*;
class House
Window wl new Window(l); II Antes del constructor
House () {
II Mostrar que estamos en el constructor:
print ("House () " ) ;
w3 = new Window(33); II Reinicializar w3
1* Output:
Window(l)
Window (2)
Window(3)
House ()
Window(33)
f ()
* //1 ,-
En l-louse, las definiciones de los objetos Window han sido dispersadas intencionadamente, para demostrar que todos ellos
se inicializan antes de entrar en el constructor o de que suceda cualquier otra cosa. Además. \\'3 se reinicializa dentro del
constructor.
Examinando la sa lida, podemos ver que la referencia a w3 se inicializa dos veces. Una vez antes y otra durante la llamada
al constructo r (el primer objeto será eliminado, por lo que podrá ser procesado por el depurador de memoria más adelante).
Puede que esto no le parezca eficiente a primera vista, pero garantiza una inicialización adecuada: ¿qué sucedería si se defi-
niera un constructor sobrecargado que no inicializara w3 y no hubiera una ü1icialización "predeterminada" para w3 en su
definición?
106 Piensa en Java
class Bowl {
Bowl (int marker)
print {"Bowl (" + marker + " )") ;
class Table (
static Bowl bowll = new Bowl (l) ;
Table () {
print{"Table{) " ) ;
bow12.fl(l) ;
class Cupboard {
Bowl bow13 = new Bowl(3);
static Bowl bow14 = new Bowl (4) ;
Cupboard () {
print (11 Cupboard () " ) i
bow14.fl(2) ;
) / * Output,
Bowl(l)
Bowl (2)
Table ()
f1 (1)
Bowl(4)
Bowl(S)
Bowl(3)
Cupboard ()
f1 (2)
Creat ing new Cupboard() in main
Bowl(3)
Cupboard ()
f1 (2)
creating new Cupboard() in main
Bowl(3)
Cupboard ()
f1 (2)
f2 (1)
f3 ( 1 )
* /// , -
80\\'1 pennite visualizar la creación de una clase, mientras que Table y Cupboard tienen miembros de tipo static de 80\\'1
dispersos por sus co rrespondientes definiciones de clase. Observe que C upboard crea un objeto Bowl bowl3 no estáti co
antes de las definiciones de tipo static.
Examinando la salida, podemos ver que la inicialización de static sólo tiene lugar en caso necesario. Si no se crea un obje-
to Table y nunca se hace referencia a Table.bowll o Table.bowI2, los objetos Bowl estáticos bowll y bowl2 nun ca se
crearán. Sólo se inicializarán cuando se cree el primer objeto Table (cuando tenga lugar el primer acceso statie). Después
de eso. los objetos static no se reinicializan.
El orden de inicialización es el siguiente: primero se inicializan los objetos estáticos, si es que no han sido ya ini cializados
con una previa creación de objeto, y luego se inicializan los objetos no estáticos. Podemos ver que esto es así examinando
la salida del programa. Para examinar main() (un método static), debe cargarse la clase Static\nitialization, después de lo
cual se inicializan sus campos estáticos tab le y cupboard, lo que hace que esas clases se carguen y, como ambas contienen
objetos Bowl estáticos, eso bace que se cargue la clase Bowl . Por tanto, todas las clases de este programa concreto se car-
gan antes de que dé comienzo main(). Éste no es el caso usual , porque en los programas típicos no tendremos todo vincu-
lado entre sí a través de valores estáticos, como sucede en este ejemplo.
Para resumir el proceso de creación de un objeto, considere una clase Dog:
1. Aunque no utilice explícitamente la palabra clave static, el constructor es, en la práctica, un método sta tic. Por
tanto, la primera vez que se crea un objeto de tipo Dog, o la primera vez que se accede a un método estático o a
un campo estáti co de la clase Dog, e l intérprete de Java debe localizar Dog.class, para lo cual analiza la ruta de
clases que en ese momento haya definido (c1asspath).
2. A medida que se carga Dog.c1ass (creando un objeto Class, acerca del cual bablaremos posteriormente) se ejecu-
tan todos sus inicializadores de tipo static. De este modo, la inicialización de tipo static sólo tiene lugar una vez,
cuando se carga por primera vez el objeto Class.
3. Cuando se crea un nuevo objeto con new Dog( ), el proceso de construcción del objeto Dog asigna primero el
suficiente espacio de almacenamiento para el objeto Dog en el cúmulo de memoria.
4. Este espacio de almacenamiento se rellena con ceros, lo que asigna automáticamente sus valores predetermina-
dos a todas las primiti vas del objeto Dog (cero a los números y el equivalente para booloan y cbar); asimismo.
este proceso hace que las referencias queden con el valor null.
5. Se ejecutan las inicializaciones especificadas en el lugar en el que se definan los campos.
6. Se ejecutan los constructores. Como podremos ver en el Capítulo 7, Reutilización de las clases, esto puede impli-
car un gran número de actividades, especialmente cuando estén implicados los mecanismos de berencia.
108 Piensa en Java
class Cup {
Cup (int marker )
print("Cup ( 1I + marker + 11 ) " ) ;
class Cups {
static Cup cupl ¡
static Cup cup2 ¡
static {
cupl new Cup ( l) ;
cup2 new Cup ( 2 ) ;
Cups()
print ( IICups () n);
puede verse a la salida. Asimismo, da igual si se eliminan las marcas de comentario que están desactivando a una y otra de
las lineas marcadas (2) o si se eliminan las marcas de ambas líneas; la inicialización estática tiene lugar una sola vez.
Ejercic io 13: (l) Verifique las afinnaciones contenidas en el párrafo anterior.
Ejercic io 14: ( 1) Cree una clase con un campo estático Strin g que sea inicializado en el punto de definición, y otro
campo que se inicialice mediante el bloque sta tic. Añada un método static que imprima ambos campos y
demuestre que ambos se inicializan antes de usarlos.
cl ass Mug {
Mug ( int marker )
print { IIMug(1I + marker + 11 ) 11 ) ;
Mugs ()
print("Mugs () !I ) ;
Mugs (int i )
print ( IIMugs (int ) 11 ) ;
1* Output:
Inside main ( )
Mug(l )
Mug(2)
mug1 & mug2 initialized
Mugs ()
new Mugs ( ) completed
Mug ( l)
Mug(2 )
mug1 & mug2 initialized
Mugs ( int )
new Mugs ( l ) completed
* // /,-
110 Piensa en Java
parece exac tamente como la cláusula de inicialización es tática, salvo porque falta la palabra clave statie. Esta sintaxis es
necesaria para so portar la inicialización de clases internas anónimas (véase el Capítulo 10, Clases illlernas), pero también
nos permite ga rantiza r que ciertas ope raciones tendrán lugar independientemente de qué co nstmctor ex plícito se invoque.
Examinando la salida, podemos ver que la cláusula de inicia li zación de instancia se ejecuta antes de los dos constructores.
Ejercicio 1 5: ( 1) Cree una clase con un campo String que se inicialice mediante una cláusula de inicialización de ins-
tancia.
Inicialización de matrices
Una matri z es, simplemente, una secuencia de objetos o primitivas que son todos del mismo tipo y que se empaquetan jun-
tos , utilizando un único nombre identificador. Las matrices se definen y usan mediante el operador de indexación I }. Para
definir una referencia de una matri z, basta con incluir unos corchetes vacíos detrás del nombre del tipo:
int[] al;
También puede colocar los corchetes después del identificador para obtener exactamente el mismo resultado:
int al [] ;
Esto concuerda con las expectati vas de los programadores de e y C++. Sin embargo, el primero de los dos estilos es una
sintaxis más adecuada, ya que comunica mejor que el tipo que estamos defmiendo es una " matriz de variab les de tipo int".
Es te estilo es el que emplearemos en el libro.
El compilador no pernlite especificar el tamaño de la matriz. Esto nos retrotrae al problema de las "referencias" anterior-
mente comentado. Todo lo que tenemos en este punto es una referencia a una matriz (habiendo asignado el suficiente espa-
cio de almacenamiento para esa referencia), sin que se haya asignado ningún espacio para el propio objeto matriz. Para crear
espacio de almacenamiento para la matriz, es necesario escribir una expresión de inicialización. Para las matrices, la inicia-
li zación puede hacerse en cualquier lugar del código, pero también podemos utilizar una clase especial de expresión de ini-
cialización que sólo puede emplearse en el punto donde se cree la matriz. Esta inicialización especial es un conjunto de
va lores encerrados entre llaves. En este caso, el compilador se ocupa de la asignación de espacio (el equivalente de utilizar
new) Por ejemplo:
int 1] al : ( 1, 2, 3, 4, 5 );
Pero entonces, ¿por qué íbamos a definir una referencia a una matriz sin definir la propia matri z?
int[l a2;
Bueno, la razón para definir una referencia sin definir la matri z asociada es que en Java es posible asignar una matri z a otra,
por lo que podríamos escribir:
a2 = al;
Lo que estamos haciendo con esto es, en realidad, copiar una referencia, como se ilustra a continuación:
JJ : initializationJArraysOfPrimitives .java
import static net.mindview . ut il.Print.*¡
public class ArraysOfPrimitives {
public static void main (St ring( ] argsl
int [] al : ( 1, 2, 3, 4, 5 );
int [] a2;
a2 = al i
f or(int i O; i e a2.1ength; i++l
5 Inicialización y limpieza 111
a2[í] = a2[í] + 1;
for {int i = O; i < al.length; i++)
print (n al [" + i + "1 = " + al [i] ) i
} / , Output,
al [O] 2
al [1] 3
al [2] 4
al [3] 5
al [4] 6
, /// ,-
Como puede ver, a al se le da un valor de inicialización, pero a a2 no; a a2 se le asigna posterionnente un valor que en este
caso es la referencia a otra matriz. Puesto que a2 y a 1 apuntan ambas a la misma matriz, los cambios que se realicen a tra-
vés de a2 podrán verse en al .
Todas las matrices tienen un miembro intrínseco (independientemente de si son matrices de objetos o matrices de primiti-
vas) que puede consultarse (aunque no modificarse) para determinar cuántos miembros hay en la matriz. Este miembro es
length. Puesto que las matrices en Java, al igual que en C y C++, comienzan a contar a partir del elemento cero, el elemen-
to máximo que se puede indexar es length - 1. Si nos salimos de los límites, e y C++ lo aceptarán en silencio y penniti-
rán que bagamos lo que queramos en la memoria, lo cual es el origen de muchos errores graves. Sin embargo, Java nos
protege de tales problemas provocando un error de tiempo de ejecución (una excepción) si nos salimos de los Iímites. 5
¿Qué sucede si no sabemos cuántos elementos vamos a necesitar en la matriz en el momento de escribir el programa?
Simplemente, bastará con utilizar new para crear los elementos de la matriz. Aquí, new funciona incluso aunque se esté
creando una matriz de primitivas (sin embargo, new no pennite crear una primitiva simple que no fonne parte de una
matri z):
11 : initiali z a ti on/ArrayNew . java
11 Crea ción de matrices con new.
import java.util. *;
import static net.mindview.util.Print.*;
/ * Output :
length of a = 18
[O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O]
, ///,-
El tamaño de la matriz se selecciona aleatoriamente utilizando el método Random.nextlnt(), que genera un valor entre cero
y el que se le pase como argumento. Debido a la aleatoriedad, está claro que la creación de la matriz tiene luga r en tiempo
de ejecución. Además, como la salida del programa muestra que los elementos de matriz de tipos primitivos se inicializan
automáticamente con valores "vacíos" (para las variables numéricas y char, se inicializan con cero mientras que para varia-
bles boolean se inicializan con false).
El método Arrays.toString( ), que fonna parte de la biblioteca estándar java.util, genera una versión imprimible de una
matriz unidimensional.
5 Por supuesto, comprobar cada acceso de una matriz cuesta tiempo y código, y no hay manera de desactivar esas comprobaciones, lo que quiere decir que
los accesos a matrices pueden ser una fuente de ineficiencia en los programas, si se producen en alguna sección critica. En aras de la seguridad en Últemct
y de la productividad de los programadores, los diseñadores en Java pensaron que resultaba convenienle pagar este precio para evitar los errores asocia-
dos con las matrices. Aunque el programador pueda sentirse lenlado de escribir código para tTalar de hacer que Jos accesos a las matrices sean más eficien-
tes, esto es una pérdida de tiempo, porque las optimizaciones automáticas en tiempo de compilación y en tiempo de ejecución se encargan de acelerar los
accesos a las matrices.
112 Piensa en Java
Por supuesto, en este caso la matri z también podía haber sido definida e inicializada en la misma instrucción:
int[] a = new int[rand.nextlnt(20}];
1* Output: (Sample )
length of a = 18
[55,193,36 1 ,461,429,368,200,22,207,288,128,51,89, 309, 278, 498, 361, 20]
* /// ,-
Aquí, incluso después de in vocar new para crear la matriz:
Integer[] a = new Integer(rand.nextInt(201] i
es sólo una matriz de referencias y la inicialización no se completa basta que se inicialice la propia referencia creando un
nuevo objeto Integer (mediante el mecanismo de conversión automática, en este caso):
a[i] = rand . nextInt(500) i
Sin embargo, si nos olvidamos de crear un objeto, obtendremos una excepción en ti empo de ejecución cuando tratemos de
utilizar esa posición vacía de la matriz.
También es posible inicializar matrices de objetos mediante una lista encerrada entre llaves. He aquí dos formas de hacerlo:
11 : initialization/ArrayInit.java
II Inicialización de la matriz.
import java.util. *;
public class ArrayInit {
public static void main(String[] args) {
Integer [] a = (
new Integer (1) ,
new Integer(2) ,
3, II Conversión automática
};
Integer[] b = new Integer[] {
new Integer (1) ,
new Integer (2) ,
3, II Conversión automática
};
System.out.println(Arrays.toString(a» i
System.out.println(Arrays . toString(b» ;
1* Output:
11, 2, 3]
[1, 2, 3]
*///,-
5 Inicialización y limpieza 113
En ambos casos, la coma final de la lista de inicializadores es opcional (esta característica pennite un manteni miento más
fácil de las listas de gran tamaño).
Aunque la primera forma es útil, es más limitada, porque sólo puede emplearse en el punto donde se deflne la matri z.
podemos uti lizar las fonnas segunda y tercera en cualquier lugar, incluso dentro de una llamada a un método. Por ejemplo,
podríamos crear una matriz de objetos String para pasa rl a a otro método main(), con el fin de proporcionar argumentos de
línea de comandos altemativos a ese método main() :
1/ : initialization/DynamicArray.java
1/ Inicialización de la matriz .
class Other {
public static void main (Str ing [) args) {
for (String s : args)
System.out.print(s + " ") i
1* Output:
fiddle de dum
*j jj ,-
La matriz creada para el argumento de Other.main() se crea en el punto correspondiente a la llamada al método, así que
podemos incluso proporcionar argumentos alternativos en el momento de la llamada.
Ejercicio 16: ( 1) Cree una matri z de objetos String y asigne un objeto String a cada elemento. Imprima la matriz utili-
zando un bucle for .
Ejercicio 17: (2) Cree una clase con un constructor que tome un argumento String. Durante la construcción, imprima
el argumento. Cree una matriz de refe rencias a objetos de esta clase, pero sin crear ningún obj eto para asig-
narl o a la matriz. Cuando ejecute el programa, obse rve si se imprimen los mensajes de iniciali zación
correspondi entes a las llamadas al constructor.
Ejercicio 18: ( 1) Co mplete el ejercicio anterior creando objetos que asociar a la matriz de referencias.
class A {}
/ * Output: ( Sample )
47 3.14 11.11
ene tWQ three
A@la46e3 0 A@3e25a5 A@19821f
* /// ,-
Podernos ver que print() admi te una matriz de tipo Object, y recorre la matri z ut ilizando la sintaxisforeach imprimiendo
cada objeto. Las clases de la biblioteca estándar de Java generan una salida más comprensible, pero los objetos de las cIa-
ses que hemos creado aquí imprimen el nombre de la clase, seguido de un signo de '@' y de un a serie de dígitos hexadeci-
males. Por tanto, el comportamiento predeternlinado (si no se define un método toString() para la clase, como veremos
posterionn ente en el libro) consiste en imprimir el nombre de la clase y la dirección del objeto.
Es posible que se encuentre con códi go anterior a Java SES escrito como el anteri or para generar li stas variables de argu-
mentos. Sin embargo, en Java SE5, esta característica largo tiempo demandada ha sido finalmente añadida, por lo que ahora
podemos emplear puntos suspensivos para definir una lista variable de argumentos, como puede ver en pr intArray() :
11: initializationjNewVarArgs.java
I I USO de la sintaxis de matrices para crear listas variables de argumentos.
JI : initialization/OptionalTrailingArguments.java
/ * Output :
requ ired: 1 ane
requ ired : 2 two three
requi red: o
*/// , -
Esto muestra también cómo se pueden utili zar varargs con un tipo especificado distinto de Object. Aquí, todos los varargs
deben ser objetos String. Se puede utilizar cualquier tipo de argumentos en las listas varargs, incluyendo tipos primitivos.
El siguiente ejemplo también muestra que la lista vararg se transforma en una matri z y que, si no hay nada en la lista, se
tratará como una matri z de tamaño cero.
JJ : initiaIizationJVarargType . java
J* Output:
class (Lj ava. lang . Character i Iength 1
class [Lj ava . Iang. Character i length O
class [I Iength 1
class [I Iength O
int [): class [I
* ///, -
El método getClass( ) es parte de Objeet, y lo ana lizaremos en detalle en el Capítulo 14, Información de lipos. Devuelve
la clase de un objeto, y cuando se imprime esa clase, se ve una representación del tipo de la clase en forma de cadena de
caracteres codificada. El carácter inicial ' 1' indica que se trata de una matriz del tipo situado a continuación. La ' 1' indica
una primiti va ¡nt; para comprobarlo, hemos creado una matri z de iut en la última línea y hemos impreso su tipo. Esto per-
mi te comprobar que la utili zación de varargs no depende de la característica de conversión automática, sino que utiliza en
la práctica los tipos primitivos.
Sin embargo, las listas vararg funcionan perfectamente con la característica de conversión automática. Por ejemplo :
116 Pien sa en Java
1/: initialization/AutoboxingVarargs.java
/* Output:
1 2
456789
10 11 12
* /// , -
Observe que se pueden mezclar los tipos en una misma lista de argumentos, y que la característica de conversión automáti-
ca promociona selecti va mente los argumentos ¡nt a Integer.
Las listas vararg compli can el proceso de sobreca rga, aunque éste parezca sufici entemente seguro a primera vista:
// : initialization/OverloadingVarargs.java
System .out.println () ;
f (1) ;
f (2, 1);
f (O) ;
f (aL) ;
II! f{) i 11 No se compilará -- ambigüo
1* Output :
first a b e
seeond 1
seeond 2 1
seeond O
third
* /// ,-
En cada caso, el compil ado r está utili zando la caracterí stica de co nversión auto mática para determinar qué método sobre-
cargado hay que utili zar, e invocará el método que se ajuste de la fonna más específica.
5 Inicialización y limpieza 117
Pero cuando se invoca f() sin argumentos, el compilador no tiene fanna de saber qué método debe llamar. Aunque este error
es comprensible, probablemente sorprenda al programador de programas cliente.
podemos tratar de resolver el problema añadiendo un argumento no vararg a uno de los métodos:
JI : initialization/OverloadingVarargs2.java
/ / {CompileTimeError} (Won' t compile)
1* Output:
first
second
' /// ,-
Generalmente, sólo debe utilizarse una lista variable de argumentos en una única versión de un método sobrecargado. O
bien, considere el no utilizar la lista variable de argumentos en absoluto.
Ejercicio 19: (2) Escriba un método que admita una matriz vararg de tipo Str ing. Verifique que puede pasar una lista
separada por comas de objetos Stri ng o una matriz String[] a este método.
Ejercicio 20 : (1) Cree un método main() que utilice varargs en lugar de la sintaxis main() normal. Imprima todos los
elementos de la matriz args resultante. Pruebe el método con diversos conjuntos de argumentos de línea
de comandos.
Tipos enumerados
Una adición aparentemente poco importante en Java SES es la palabra clave enUDl, que nos facilita mucho las cosas cuan-
do necesitamos agrupar y utilizar un conjunto de tipos enumerados. En el pasado, nos veíamos forzados a crear un conjun-
118 Piensa en Java
to de valores enteros constantes, pero estos conjuntos de valores no suelen casar muy bien con los conjuntos que se necesi-
tan definir y son, por tanto, más arriesgados y dificiles de utilizar. Los tipos enumerados representan una necesidad tan
común que C. e++ y diversos otros lenguajes siempre los han tenido. Antes de Java SES, los programadores de Java esta-
ban obligados a conocer muchos detalles y a tene r mucho cuidado si querían emular apropiadamente el efecto de eoum .
Ahora. Java dispone también de enum , y lo ha implementado de una manera mucho más completa que la que podemos
encontrar en C/C++. He aquí un ejemplo simple :
11 : initialization/ Spiciness.java
public enum Spiciness {
NOT, MILO, MEDIUM, HOT, FLAMING
} /1/ ,-
Esto crea un tipo enumerado denominado Spiciness con cinco valores nominados. Puesto que las instancias de los tipos enu-
merados son constantes, se suelen escribir en mayúsculas por convenio (si hay múltiples palabras en un nombre, se separan
mediante guiones bajos).
Para utilizar un tipo enum, creamos una referencia de ese tipo y la as ignamos una instancia:
11 : initialization/ SimpleEnumUse.java
public class SimpleEnumUse {
public static void main{String[] args )
Spiciness howHot = Spiciness.MEDIUM¡
System.out.println{howHot) i
1* Output:
MEDIUM
* /// ,-
El compilador añade automáticamente una seri e de características útiles cuando creamos un tipo eoum . Por ejemplo, crea
un método toString() para que podamos visuali zar fácilmente el nombre de una instancia enum, y ésa es precisamente la
forma en que la instrucción de impresión anterior nos ha penni tido generar la salida del programa. El compilador también
crea un método ordinal( ) para indicar el orden de declaración de una constante enum concreta, y un método static values( )
que genera una matriz de valores con las constantes enum en el orden en que fueron declaradas:
11 : initialization/EnumOrder.java
public class EnumOrder {
public sta tic void main (S tring [] args ) {
for {Spiciness s : Spiciness.values{))
System.out.println(s + ", ordinal 11 + s.ordinal());
1* Output:
NOT, ordinal O
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4
* /// ,-
Aunque los tipos enumerados enum parecen ser un nuevo tipo de datos, esta palabra clave sólo provoca que el compilador
realice una serie de actividades mientras genera una clase para el tipo enum, por lo que un enum puede tratarse en muchos
sentidos como si fuera una clase de cualquier otro tipo. De hecho, los tipos enum son clases y tienen sus propios métodos.
Una característica especialmente atractiva es la forma en que pueden usarse los tipos enum dentro de las instrucciones
switch :
11 : initialization/Burrito.java
switch(degree)
case NOT: System.out.println(lInot spicy at all . tI) i
break¡
case MILD:
case MEDIUM: System.out.println(lt a little hot.");
break;
case HOT:
cas e FLAMING:
defaul t: System. out. println ( tlmaybe too hot. 11 ) ;
/ * Output:
This burrito is not spicy at all.
This burrito is a little hoto
This bur rito is maybe too hot.
*/// ,-
Puesto que una instrucción switch se emplea para seleccionar dentro de un conjunto limitado de posibilidades, se comple-
menta perfectamente con un tipo en um. Observe cómo los nombres enum indican de una manera mucho más clara qué es
lo que pretende hacer el programa.
En general, podemos utilizar un tipo enum como si fuera otra fOn1la de crear un tipo de datos, y limitamos luego a utilizar
los resultados. En realidad, eso es lo importante, que no es necesario prestar demasiada atención a su uso, porque resulta
bastante simple. Antes de la introducción de enum en Java SE5, era necesario realizar un gran esfuerzo para construir un
tipo enumerado equivalente que se pudiera emplear de fOn1la segura.
Este breve análisis es suficiente para poder comprender y utilizar los tipos enumerados básicos, pero examinaremos estos
tipos enumerados más profundamente en el Capítulo 19, Tipos enumerados.
Ejercicio 21 : ( 1) Cree un tipo enum con los seis lipos de billetes de euro de menor valor. Recorra en bucle los valores
utilizando values() e imprima cada va lor y su orden correspondiente con ordinal().
Ejercicio 22 : (2) Esc riba una instrucción switch para el tipo enum del ejercicio anterior. En cada case, imprima una des-
cripci ón de ese billete concreto.
Resumen
Este aparentemente elaborado mecanismo de inicialización, el constructor, nos indica la importancia crítica que las tareas
de inicialización tienen dentro del lenguaje. Cuando Sjame Stroustrup, el inventor de C++, estaba diseñando ese lenguaje,
una de las primeras cosas en las que se fijó al analizar la productividad en C fue que la inicialización inadecuada de las varia-
bles es responsable de una parte significativa de los problemas de programación. Este tipo de errores son dificiles de loca-
lizar, y lo mismo cabría decir de las tareas de limpieza inapropiadas. Puesto que los constructores nos pem'¡ten garantizar
una inicialización y limpieza adecuadas (el compilador no pennitirá crear un objeto sin las apropiadas llamadas a un cons-
tructor), la seguridad y el control están garantizados.
En C++. la destrucción también es muy importante, porque los objetos creados con new deben ser destruidos explícitamen-
te. En Java, el depurador de memoria libera automáticamente la memoria de los objetos que no son necesarios, por lo que
el método de limpieza equivalente en Java no es necesario en muchas ocasiones (pero cuando lo es, es preciso implemen-
tarlo explícitamente). En aquellos casos donde no se necesite un comportamiento simi lar al de los destructores, el mecanis-
mo de depuración de memoria de Ja va simplifica enOn1lemente la programación y mejora también en gran medida la
120 Piensa en Java
seguridad de la gestión de memoria. Algunos depuradores de memoria pueden incluso limpiar otros recursos, como los
recursos gráficos y los desc riptores de archivos. Sin embargo, el depurador de memori a hace que se incremente el coste de
ejecución, resultando dificil evaluar adecuadamente ese coste, debido a la lentitud que históricamente han tenido los intér-
pretes de Ja va. Aunque a lo largo del tiempo se ha mejorado significativamente la velocidad de Java, un problema de la velo-
cidad ha supuesto un obstáculo a la hora de adoptar este lenguaje en ciertos tipos de problemas de programación.
Debido a que está garantizado que todos los objetos se construyan, los constructores son más complejos de lo que aquí
hemos mencionado . En particular, cuando se crean nuevas clases utilizando los mecanismos de composición o de herencia,
lambién se mantiene la garantía de constnlcción, siendo necesaria una cierta sintaxis adicional para soportar este mecanis-
mo. Hablaremos de la composición, de la herencia y del efecto que ambos mecanismos tienen en los constnlctores en pró-
ximos capítulos.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tlle Thinking in Java AnnOfafed Sofllfion Guide. que esta dispo-
nible para la venta en Inm:MindView.llel.
Control de acceso
Todos los buenos escritores, incluyendo aquellos que escriben software, saben que un cierto trabajo no está tem1inado hasta
después de haber sido reescrito. a menudo muchas veces. Si dejamos un fragmento de código encima de la mesa durante un
tiempo y luego volvemos a él, lo más probable es que veamos una [anna mucho mejor de escribirlo. Ésta es una de las prin-
cipales motivaciones para el trabajo de rediseño, que consiste en reescribir código que ya funciona con el fin de hacerlo más
legible, comprensible y, por tanto, mantenible.'
Sin embargo, existe una cierta tensión en este deseo de modificar y mejorar el código. A menudo, existen consum idores (pro-
gramadores de cliente) que dependen de que ciertos aspectos de nuestro código cont inúen siendo iguales. Por tanto.
nosotros queremos modificar el código, pero ellos quieren que siga siendo igual. Es por eso que una de las principales con-
sideraciones en el diseño orientado a objetos es la de "separar las cosas que cambian de las cosas que pennanecen"'.
Esto es particulannente importante para las bibliotecas. Los consumidores de una biblioteca deben poder confiar en el ele-
mento que están utilizando, y saber que no necesitarán reescribir el código si se publica una nueva vers ión de la biblioteca.
Por otro lado, el creador de la biblioteca debe tener la libertad de realizar modificaciones y mejoras, con la confianza de que
el código del cliente no se verá afectado por esos cambios.
Estos objetivos pueden conseguirse adoptando el convenio adecuado. Por ejemplo, el programador de la biblioteca debe
aceptar no eliminar los métodos existentes a la hora de modificar una clase de la biblioteca, ya que eso haría que dejara de
funcionar el código del programador de clientes. Sin embargo, la sintación inversa es un poco más compleja de resolver. En
el caso de un campo, ¿cómo puede saber el creador de la biblioteca a qué campos han accedido los programadores de clien-
tes? Lo mismo cabe decir de los métodos que sólo forman parte de la implementación de una clase y que no están para ser
usados directamente por el programador de clientes. ¿Qué pasa si el creador de la biblioteca quiere deshacerse de una imple-
mentación anterior y sustituirla por una nueva? Si se modifica alguno de esos miembros, podría dejar de funcionar el códi-
go de algún programa cliente. Por tanto, el creador de la biblioteca tiene las manos atadas y no puede modificar nada.
Para resolver este problema. Java proporciona especificadores de aCceso que penniten al creador de la biblioteca decir qué
cosas están disponibles para el programa cliente y qué cosas no lo están. Los niveles de control de acceso, ordenados de
mayor a menor acceso, son public, protected, acceso de paquete (que no ti enen una palabra clave asociada) y private.
Leyendo el párrafo anterior, podríamos pensar que, como diseñadores de bibliotecas, conviene mantener todas las cosas 10
más "privadas" posible y exponer sólo aquellos métodos que queramos que el programa cliente utilice. Esto es CIerto, aun-
que a menudo resulta an tinatural para aquellas personas acostumbradas a programar en otros lenguajes (especialmente C) y
que están acostumbradas a acceder a todo sin ninguna restricción. Cuando lleguen al final del capítulo, estas personas esta-
rán convencidas de la utilidad de los controles de acceso en Java.
Sin embargo, el concepto de biblioteca de componentes y el control acerca de quién puede acceder a los componentes de
esa biblioteca no es completo. Sigue quedando pendiente la cuestión de cómo empaquetar los componentes para [onnar
1 Consulte RelaclOril/g: Improlling ,he Design 01 Exisling Code , de Martin Fowlcr, el al. (Addison·Wesley, 1999). Ocasionalmente. algunas personas argu·
mentarán en contra de las lareas de rediseño, sugiriendo que tUl código que ya funciona es perfectamente adecuado, por lo que resulta una pérdida dc ticm·
po tratar de rediseñarlo. El problema con esta fomla de pensar es que la parte del león en lo que se refiere al tiempo y al dinero consumidos por un proyecto
no está en la escritura inicial del código. sino en su mantenimiento. Hacer el código más fácil de entender pernlite ahorrar una gran cantidad de dinero.
122 Piensa en Java
una unidad de biblioteca cohesionada. Este aspecto se controla mediante la palabra clave package en Ja va. y los especifi-
cadores de acceso se verán afectados por el hecho de que una clase se encuentra en el mismo paquete o en otro paquete di s-
tinto. Por tanto, para comenzar este capítulo, veamos primero cómo se incluyen componentes de biblioteca en los paquetes.
Con eso, seremos capaces de entender completamente el significado de los especificadores de acceso.
La razón para efectuar estas importaciones es proporcionar un mecanismo para gestionar los espacios de nombres. Los nom-
bres de todos los miembros de las clases están aislados de las clases restantes. Un método f( ) de la clase A no coincidirá
con un método f() que tenga la misma signatura en la clase B. ¿Pero qué sucede con los nombres de las clases? Suponga
que creamos una clase Stack en una máquina que ya disponga de otra clase Stack escrita por alguna otra persona. Esta posi-
bilidad de colisión de los nombres es la que hace que sea tan importante disponer de un control completo de los espacios de
nombres en Java, para poder crear una combinación de identificadores unívoca para cada clase.
La mayoría de los ejemplos que hemos visto hasta ahora en el libro se almacenaban en un único archivo y habían sido di se-
nados para uso local, por lo que no nos hemos preocupado de los nombres de paquete. Lo cierto es que estos ejemplos sí
estaban incluidos en paquetes: e l paquete predeterminado o " innominado". Ciertamente, ésta es una opción viab le y trata-
remos de utilizarla siempre que sea posible en el resto del libro, en aras de la simplicidad. Sin embargo, si lo que pretende-
mos es crear bibliotecas o programas que puedan cooperar con otros programas Java que estén en la misma máq uina,
deberemos tener en cuenta que hay que evitar las posibles colisiones entre nombres de clases.
Cuando se crea un archivo de código fuente para Java, normalmente se le denomina unidad de compilación (y también , en
ocasiones, unidad de traducción). Cada lmidad de compilac ión debe tener un nombre que termine en .java, y dentro de la
unidad de compilación puede haber UDa clase public que debe tener el mi smo nombre del archivo (i ncluyendo el uso de
mayúsculas y minúsc ulas, pero excluyendo la extensión .java de l nombre del archivo). Sólo puede haber una clase public
en cada unidad de compi lación; en caso contrario, el compilador se quejará. Si existen clases adicio nales en esa unidad de
compilación, estarán ocultas para el mundo exterio r al paquete, porque no son public, y simplemente se tratará de clases
"soporte" para la clase public principal.
6 Control de acceso 123
estamos indicando qu e esta unidad de compilación es parte de una biblioteca denominada access. Dicho de otro modo, esta-
mos especificando que el nombre de clase pública situado dentro de esta unidad de compilación debe integrarse bajo el
"paraguas" correspondient e al nombre access, de modo que cualquiera que quiera usa r ese nombre deberá especi fi carlo por
completo o utili za r la palabra clave import en combinación con access, utili za ndo las opciones que ya hemos mencionado
anteriormente (observe que e l co nvenio que se empl ea para los nombres de paquetes Java consiste en emplear letras minús-
cu las, incl uso para las palabras intermedias).
Por ejemp lo, suponga que e l nombre de un archi vo es MyClass.java . Esto quiere decir que sólo puede haber una clase
public en dicho archivo y que el nombre de esa clase debe ser MyClass (respetando e l uso de mayúscu las y minúscu-
las):
11 : access/mypackage/MyClass.java
package access.mypackage;
La palabra clave import permi te que este ejemplo tenga un aspecto mucho más simple:
11 :access/lmportedMyClass.java
import access.mypackage.*¡
2 No hay ninguna característica de Java que nos obligue a utilizar un interprete. Existen compiladores Java de código nativo que generan un único archi*
vo ejecutable.
124 Piensa en Java
package net.mindview.simple¡
Ahora, este nombre de paquete puede uti lizarse como espacio de nombres paraguas para los siguientes dos archivos:
1* Output:
net.mindvie w. simple . Vector
net.mindview . simple . List
' /11 ,-
Cuando el compilador se encuentra con la instrucción import correspondiente a la biblioteca simple, comienza a explorar
todos los directorios especificados por CLASSPATH, en busca del subdirectorio net/mindview/simple, y luego busca los
archi vos compilados con los nombres apropiados (Vector.c1ass para Vector y List.c1ass para List). Observe que tanto las
dos clases C0 l110 los métodos deseados de Vector y List tienen que ser de tipo public.
126 Piensa en Java
La configuración de CLASSPATH resullaba tan en igmática para los usuarios de Java inexpertos (al menos lo era para mi
cuando comencé con el lenguaje) que Sun ha hecho que en el kit JDK de las vcrsiones más recientes de Java se compOrte
de fomla algo más inteligente. Se encontrará, cuando lo instale. que aunque no configure la variable CLASSPATH. podrá
co mpilar y ejec utar programas Java básicos. Sin embargo. para compilar y ejecutar el paq uete de código fuente de este libro
(disponible en 1I'1I'1I'.MindVie1l'. ne¡). necesi tará anadir a la variable CLASSPATH el directorio base del árbol de código.
Ejercicio 1 : (1) Cree una clase dent ro de un paquete. Cree una instancia de esa clase fuera de dicho paquete.
Colisiones
¿Q ué sucede si se importan do s bibliotecas mediante '*' y ambas incluyen los mismos nombres? Por ejemplo, suponga que
un programa hace esto:
impore net.mindview.simple.*;
import java.util.*;
Puesto que java.util.* también contiene una clase Vector. esto provocaría una potencial colisión. Sin embargo, mientras que
no lleguemos a escribir el código que provoque en efecto la colisión, no pasa nada. Resulta bastante conveniente que esto
sea así, ya que de otro modo nos veríamos forzados a escri bir un montón de cosas para evitar colisiones que realmente nunca
iban a suceder.
La colisión sí que se producirá si ahora intentamos constmir un Vector:
Vector v = new Vector {) ;
¿A qué clase Vector se refiere esta línea? El compilad or no puede saberlo, como tampoco puede saberlo el lector. Así que
el compilador generará un error y nos obligará a se r más explíci tos. Si queremos utili zar e l Vecto r Java estándar, por ejem-
plo, deberemos escribir:
java.util.Vector v = new java.util.Vector {) ;
Puesto que esto Uunto con la variable CLASSPATH) especifica completamente la ubicación de la clase Vector deseada, no
existirá en realidad ninguna necesidad de emplea r la instrucción import java.util.*, a menos que vayamos a utilizar algu-
na Olra clase definida en java.util .
Alternativamente, podemos utili zar la instmcción de importación de una única clase para preve nir las colisiones, siempre y
cuando no empleemos los dos nombres qu e entran en colisión dentro de un mismo progra ma (en cuyo caso, no tendremos
más remedi o que especificar completamente los nom bres).
Ejercicio 2 : ( 1) Tome los fragmentos de cód igo de esta sección y transfónn elos en un programa para verificar que se
producen las colisiones que hemos mencionado.
Podemos utilizar estas abreviaturas de impresión para imprimir cualquier cosa, bien con la inserción de una nueva línea
»
(print( o sin una nueva línea (printnb( ».
Como habrá adivinado, este archivo deberá estar ubi cado en un directorio que comience en una de las ubicaciones defini-
das en CLASSPATH y que luego continúe con net/ mindview. Después de compilar, los métodos sta tic print() y printnb()
pueden emplearse en cualquier lugar del sis tema utilizando una instrucción import sta tic:
11: access/PrintTest.java
II Usa los métodos estáticos de impresión de Print.java.
import static net.mindview . util.Print.* ¡
1* Output:
Available frem now on!
100
100
3.14159
* /// , -
Un segundo componente de esta biblioteca pueden ser los métodos range() , que hemos presentado en el Capítulo 4. Con/rol
de la ejecución, y que penniten el uso de la sintaxisforeach para secuencias simples de enteros:
11 : net/mindview/util/Range .java
II Métodos de creación de matrices que se pueden usar sin
II cualificadores, con importaciones estáticas Java SES:
package net.mindview.util¡
return result;
A partir de ahora, cuando desarrolle cualquier nueva utilidad que le parezca interesante la podrá añadir a su propia biblio-
teca. A lo largo del libro podrá ver que cómo añadiremos más componentes a la biblioteca net.mindview.util
Acceso de paquete
En los ejemplos de los capítu los anteriores no hemos utilizado especificadores de acceso. El acceso predetenninado no tiene
asociada ninguna palabra clave, pero comúnmente se hace referencia a él como acceso ele paquete (y también, en ocasio-
nes, "acceso amigable"). Este tipo de acceso significa que todas las demás clases del paquete actual tendrán acceso a ese
miembro, pero para las clases situadas fuera del paquete ese miembro aparecerá como priva te. Puesto que cada unidad de
compilación (cada archivo) sólo puede pertenecer a un mismo paquete, todas las clases dentro de una misma unidad de com-
pilación estarán automáticamente disponibles para las otras mediante el acceso de paquete.
El acceso de paquete nos permite agrupar en un mismo paquete una serie de clases relacionadas para que puedan interac-
tuar fácilmente entre sí. Cuando se colocan las clases juntas en un paquete, garantizando así el acceso mutuo a sus miem-
bros definidos con acceso de paquete, estamos en cierto modo garantizando que el código de ese paquete sea "propiedad"
nuestra. Resulta bastante lógico que sólo el código que sea de nuestra propiedad disponga de acceso de paquete al resto del
código que nos pertenezca. En cierto modo. podríamos decir que el acceso de paquete hace que tenga sentido el agrupar las
clases dentro de un paquete. En muchos lenguajes. la forma en que se hagan las definiciones en los archivos puede ser arbi-
traria. pero en Java nos vemos impelidos a organizarlas de una fomla lógica. Además, podemos aprovechar la definición del
paquete para excluir aquellas clases que no deban tener acceso a las clases que se definan en el paquete actual.
Cada clase se encarga de controlar qué código tiene acceso a sus miembros. El código de los restantes paquetes no puede
presentarse sin más y esperar que le muestren los miembros protected, los miembros con acceso de paquete y los miem-
bros private de una detemlinada clase. La única fomla de conceder acceso a un miembro consiste en:
l. Hacer dicho miembro public. Entonces, todo el mundo podrá acceder a él.
2. Hacer que ese miembro tenga acceso de paquete, por el procedimiento de no incluir ningún especificador de acce-
so, y colocar las otras clases que deban acceder a él dentro del mismo paquete. Entonces, las restantes clases del
paquete podrán acceder a ese miembro.
3. Como veremos en el Capítulo 7, Rculili::ación de clases, cuando se introduce la herencia. una clase heredada puede
acceder tanto a los miembros protectcd como a los miembros pubLic (pero no a los miembros priva te). Esa clase
podrá acceder a los miembros con acceso de paquete sólo si las dos clases se encuentran en el mismo paquete.
Pero, por el momento, vamos a olvidamos de los temas de herencia y del especificador de acceso protected .
4. Proporcionar métodos "de acceso/mutadores" (también denominados métodos "get/set") que pennitan leer y
cambiar el valor. Éste es el enfoque más civilizado en ténninos de programación orientada a objetos, y resulta
fundamental en JavaBeans, como podrá ver en el Capítulo 22, blle/faces gráficas de usuario.
podemos crear un objeto Cookie, dado que su constructor es public y que la clase también es public (más adelante, profun-
dizaremos más en el concepto de clase pubHc). Sin embargo, el miembro bite() es inaccesible desde de Dinner.java ya que
bite() sólo proporciona acceso dentro del paquete dessert, así que el compilador nos impedirá utilizarlo.
El paquete predeterminado
Puede que le sorprenda descubrir que el siguiente código sí que puede compilarse, a pesar de que parece que no cumple con
las reglas:
11: access/Cake.java
II Accede a una clase en una unidad de compilación separada.
class Cake {
public static void main (Stri ng [] args) {
Pie x = new Pie {) i
x. f () ;
1* Output:
Pie. f ()
*j jj,-
class Pie {
void f {) { System.out.println{"Pie.f{) 11 ) i }
} jjj,-
Inicialmente, cabría pensar que estos dos archivos no tienen nada que ver entre sí, a pesar de lo cual Cake es capaz de crear
un objeto Pie y de in vocar su método ro (observe que debe tener ' .' en su variable CLASSPATH para que los archivos se
compilen). Lo que parecería lógico es que Pie y f() tengan acceso de paquete y no estén, por tanto, disponibles para Cake.
Es verdad que tienen acceso de paquete, esa parte de la suposición es correcta. Pero la razón por la que están disponibles en
Cake.java es porque se encuentran en el mismo directorio y no tienen ningún nombre explícito de paquete. Java trata los
archivos de este tipo como si fueran implícitamente parte del "paquete predetem1inado" de ese directorio, y por tanto pro·
porciona acceso de paquete a todos los restantes archivos situados en ese directorio.
El acceso de paquete predetenninado proporciona a menudo un nivel adecuado de ocultación; recuerde que un miembro con
acceso de paquete resulta inaccesible para todos los programas cliente que utilicen esa clase. Esto resulta bastante conve-
niente, ya que el acceso predetenninado es el que nomlalmente se utiliza (y el que se obtiene si nos olvidamos de añadir
especificadores de control de acceso). Por tanto, 10 que normalmente haremos será pensar qué miembros queremos definir
explícitamente como públicos para que los utilicen los programas cliente; como resultado, uno tendería a pensar que la pala-
bra clave private no se utiliza muy a menudo, ya que se pueden realizar los diseños si n ella. Sin embargo, la realidad es que
el uso coherente de priva te tiene una gran importancia, especialmente en el caso de la programación multihebra (como vere-
moS en el Capítulo 21, Concurrencia).
He aquí un ejemplo del uso de priva te:
11 : access/IceCream.java
II Ilustra la palabra clave "private".
class Sundae {
pri vate Sundae() {}
static Sundae makeASundae()
return new Sundae{)¡
Este ejemplo nos pennite ver un caso en el que private resulta muy útil: queremos tener control sobre el modo en que se
crea un objeto y evitar que nadie pueda acceder directamente a un constructor concreto (o a todos eUos). En el ejemplo ante-
rior, no podemos crear un objeto Sundae a través de su constructor; en lugar de ello, tenemos que invocar el método
makeASundae( ) para que se encargue de hacerlo por nosotros 4
Cualquier método del que estemos seguros de que sólo actúa como método "auxiliar" de la clase puede ser defmido como
privado para garantizar que no lo utilicemos accidentalmente en ningún otro lugar del paquete, lo que nos impediría modifi-
car o eliminar el método. Definir un método como privado nos garantiza que podamos modificarlo libremente en el futuro.
Lo mismo sucede para los campos privados definidos dentro de una clase. A menos que tengamos que exponer la imple-
mentación subyacente (lo cual es bastante menos habinlal de lo que podría pensarse), conviene definir todos los campos
como privados. Sin embargo, el hecho de que una referencia a un objeto sea de tipo private en una clase no quiere decir
que algún otro objeto no pueda tener una referencia de tipo publie al mismo objeto (consuhe los suplementos en línea del
libro para conocer más detalles acerca de los problemas de los alias).
4Existe otro efecto en este caso: puesto que el único constructor definido es el predetenninado y éste se ha definido como pl"ivate, se impedini que nadie
herede esta clase (lo cual es un tema del que hablaremos más adelante).
132 Piensa en Java
// : access/ChocolateChip.java
/1 No se puede usar un miembro con acceso de paquete desde otro paquete.
import access.dessert.*;
1* Output:
Cookie constructor
ChocolateChip constructor
* /// ,-
Uno de los aspectos interesantes de la herencia es que, si existe un método bite( ) en la clase Cookie, también existirá en
todas las clases que hereden de Coo ki e. Pero como bite() tiene acceso de paquete y está situado en un paquete di stinto, no
estará disponible para nosotros en el nuevo paquete. Por supuesto, podríamos hacer que ese método fuera público, pero
entonces todo el mundo tendría acceso y puede que no sea eso lo que queramos. S i modificamos la clase Cookie de la forma
siguiente:
1/ : access/cookie2/Cookie.java
package access.cookie2¡
protected void bi te () {
System.out.println{"bite") ;
ahora bite( ) estará d isponible para toda aque lla clase que herede de Cookic:
11: access /ChocolateChip2.java
import access.cookie2.*;
/ * Output :
cookie constructor
ChocolateChip2 constructor
bite
' /11 ,-
Observe que, aunque bite( ) también tiene acceso de paquete, no es de tipo publico
Ejercicio 4 : (2) Demuestre que los métodos protegidos (protected) tienen acceso de paquete pero no son públicos.
Ejercicio 5 : (2) Cree una clase con campos y métodos de tipo p ubli c, private, protected y con acceso de paquete. Cree
un objeto de esa clase y vea los tipos de mensajes de compi lación que se obtienen cuando se intenta acce-
der a todos los miembros de la clase. Tenga en cuenta que las clases que se encuentran en el mismo direc-
torio faonan parte del paquete "predetenrunado",
Ejercicio 6: (1) Cree una clase con datos protegidos. Cree una segunda clase en el mismo archivo con un método que
manipule los datos protegidos de la primera clase.
Interfaz e implementación
El mecanismo de control de acceso se denomina a menudo ocultación de la implementación. El envolver los datos y los
métodos dentro de la clase, en combinación con el mecanismo de ocultación de la implementación se denomina a menudo
encapsulación. 5 El resultado es un tipo de datos con una serie de características y comportamientos.
El mecanismo de control de acceso levanta una serie de fronteras dentro de un tipo de datos por dos razones importantes.
La primera es establecer qué es lo que los programas cliente pueden usar o no. Podemos entonces diseñar como queramos
los mecanismos internos dentro de la clase, sin preocuparnos de que los programas cliente utilicen accidentalmente esos
mecanismos internos como parte de la interfaz que deberían estar empleando.
Esto nos lleva directamente a la segunda de las razones, que consiste en separar la interfaz de la implementación. Si esa
clase se utiliza dentro de un conjunto de programas, pero los programas cliente tan sólo pueden enviar mensajes a la inter-
faz pública, tendremos libertad para modificar cualquier cosa que no sea pública (es decir, los miembros con acceso de
paquete, protegidos o privados) sin miedo de que el código cliente deje de funcionar.
Para que las cosas sean más claras, resulta conveniente a la hora de crear las clases, siul8r los miembros públicos al princi-
pio, seguidos de los miembros protegidos, los miembros con acceso de paquete y los miembros privados. La ventaja es que
el usuario de la clase puede comenzar a leer desde el principio y ver en primer lugar lo que para él es lo más importante (los
miembros de tipo public, que son aquellos a los que podrá accederse desde fuera del archivo), y dejar de leer en cuanto
encuentre los miembros no públicos, que f0n11an parte de la implementación interna.
11 : access/OrganizedByAccess.java
5 Sin embargo, mucha gente utiliza la palabra encapsulación para referirse en exclusiva a la ocultación de la implementación.
134 Piensa en Java
Esto sólo facilita parcialmente la lectura, porque la interfaz y la implementación siguen estando mezcladas. En otras pa-
labras, el lector seguirá pudiendo ver el código fuente (la implementación), porque está incluido en la clase. Además, el sis-
tema de documentación basado en comentarios soportado por Javadoc no concede demasiada importancia a la legibilidad
del código por parte de los programadores de clientes. El mostrar la interfaz al consumidor de una clase es, en realidad, tra-
bajo del explorador de clases, que es una herramienta que se encarga de exarnjnar todas las clases disponibles y mostramos
lo que se puede hacer con ellas (es decir, qué miembros están disponibles) en una forma apropiada. En Java, visualizar la
documentación del IDK con un explorador web nos proporciona el mismo resultado que si utilizáramos un explorador de
clases.
Acceso de clase
En Java, los especificadores de acceso también pueden emplearse para detennjnar qué clases de una biblioteca estarán dis-
ponibles para los usuarios de esa biblioteca. Si queremos que una cierta clase esté disponible para un programa cliente, ten-
dremos que utilizar la palabra clave public en la definición de la propia clase. Con esto se controla si los programas cliente
pueden siquiera crear un objeto de esa clase.
Para controlar el acceso a una clase, el especificador debe situarse delante de la palabra clave class, Podemos escribir:
pUblic class Widget {
Ahora, si el nombre de la biblioteca es access, cualquier programa cliente podrá acceder a Widget mediante la instrucción
import access.Widget¡
o
import access.*¡
Observe que una clase no puede ser private (ya que eso haria que fuera inaccesible para todo el mundo salvo para la pro-
pia clase) ni protected. 6 Así que sólo tenemos dos opciones para especificar el acceso a una clase: acceso de paquete o
public. Si no queremos que nadie tenga acceso a la clase. podemos definir todos los constructores como privados, impidien-
do que nadie cree un objeto de dicha clase salvo nosotros, que podremos hacerlo dentro de un miembro de tipo static de esa
clase. He aquí un ejemplo:
ji : access/Lunch.java
/1 Ilustra los especificadores de acceso a clases. Define una
/1 clase como privada con constructores privados:
class Soupl {
pri vate Soupl () ()
JI (1) Permite la creación a través de un método estático:
public static Soupl makeSoup()
return new Soupl();
class Soup2 {
private Soup2() ()
/1 (2) Crea un objeto estático y devuelve una referencia
JI cuando se solicita. (El patrón "Singleton " ) :
private static Soup2 psi = new Soup2{) i
public static Soup2 access {} {
return psl¡
public void f () {}
void testStatic()
Soupl soup = Soupl.makeSoup();
void testSingleton{)
Soup2. access () . f () ;
}
lit >
Hasta ahora, la mayoría de los métodos devolvían void o un tipo primitivo, por lo que la definición:
public static Soupl makeSoup()
return new Soupl{) ;
puede parecer algo confusa a primera vista. La palabra Soupl antes del nombre del método (makeSoup) dice lo que el
método devuelve. Hasta ahora en el libro, esta palabra normalmente era void, lo que significa que no devuel ve nada. Pero
también podemos devolver una referencia a un objeto, que es lo que estamos haciendo aquí. Este método devuelve una refe-
rencia a un objeto de la clase Soupl.
Las clases Soupl y Soup2 muestran cómo impedir la creación directa de objetos de una clase definiendo todos los cons-
tructores como privados. Recuerde que, si no creamos explícitamente al menos un constructor, se creará automáticamente
el constructor predeterminado (un constructor sin argwnentos). Escribiendo el constructor predetenninado, garantizamos
6 En rea lidad, una e/ase ¡nlerna puede ser privada o protegida, pero se trata de un caso especial. Hablaremos de ello en el Capítulo 10, Clases internas.
136 Piensa en Java
que no sea escrito automáticamente. Definiéndolo como privado nadie podrá crear un objeto de esa clase. Pero entonces,
¿cómo podrá alguien lIsar esta clase? En el ejemplo anterior se muestran dos opciones. En Soup 1. se crea un método static
que crea un nuevo objeto SoupJ y devuelve una referencia al mismo. Esto puede ser úti l si queremos realizar algunas ope-
raciones adicionales con el objeto Soupt antes de devol verlo, o si queremos llevar la cuenta de cuántos objetos Soupl se
han creado (por ejemplo. para restringir el número total de objetos).
Soup2 utiliza lo que se denomina un pmrón de disei)o, de lo que se habla en Thinking in Pallerns (with Java) en
wl1'H'.Minc/View.nef. Este patrón concreto se denomina Solitario (s; ngleron) , porque sólo pennite crear Wl único objeto. El
objeto de la clase Soup2 se crea como un miembro static prívate de Soup2, por lo que existe un objeto y sólo uno, y no se
puede acceder a él salvo a rravés del mérodo público access( ).
Como hemos mencionado anterionnente, si no utili zamos un especificador de acceso para una clase, ésta tend rá acceso de
paquete. Esto significa que cualquier otra clase del paquete podrá crear un objeto de esa clase, pero no se podrán crear obje-
tos de esa clase desde fue ra del paquete (recuerde que todos los archivos de un mismo di rectori o que no tengan declaracio-
nes package exp lícitas fonnan parte, implícitamente, del paquete predetenni nado de dicho directorio). Sin embargo, si un
miembro estático de esa clase es de tipo publie, el programa cliente podrá seguir accediendo a ese miembro estático, aún
cuando no podrá crear un objeto de esa clase.
Ejercicio 8: (4) Siguiendo la fonna del ejemplo Lunch.java. cree una clase denominada ConnectionManager que
gestione una matriz fija de objetos Conncetion. El programa cliente no debe poder crear explícitamente
objetos Conncetíon, sino que sólo debe poder obtenerlos a través de un método estát ico de
ConnectionManager. Cuando ConnectionManager se quede sin objetos, devolverá una referencia oul!.
Pmebe las clases con un programa main() .
Ejercicio 9: (2) Cree el sigui ente archivo en el directorio aeccss/loeal (denrro de su mta CLASSPATH):
11 access/local / PackagedClass.java
package access.local¡
class PackagedClass {
public PackagedClass()
System.out.println("Creating a packaged class") i
Exp lique por qué el compilador crea un error. ¿Se reso lveria el error si la clase Forcign fuera parte del
paquete access. local?
Resumen
En cualquier relación, resulta importante definir una serie de fronteras que sean respetadas por todos los participantes.
Cuando se crea una biblioteca. se establece una relación con el usuario de esa biblioteca (el programador de clientes), que
es un programador como nosotros, aunque lo que hace es utili zar la biblioteca para constru ir una aplicación u otra bibliote-
ca de mayor tamai'io.
Sin una serie de reglas, los programas cliente podrían hacer lo que quisieran con lodos los miembros de una clase, aún cuan·
do nosotros prefiriéramos que no manipularan algunos de los miembros. Todo estaría expuesto a ojos de todo el mundo.
6 Conlrol de acceso 137
En este capítulo hemos visto cómo se construyen bibliotecas de clases: en primer lugar, la fonna en que se puede empaque-
tar un grupo de clases en una biblioteca y. en segundo lugar, la fanna en que las clases pueden controlar el acceso a sus
miembros.
Las esti maciones indican que los proyectos de programación en e empiezan a fallar en algún punto entre las 50.000 y
100.000 líneas de código, porque e tiene un único espacio de nombres yesos nombres empiezan a colisionar, obligando a
realizar un esfuerzo adicional de gestión. En Java. la palabra clave package, el esquema de denominación de paquetes y la
palabra clave import nos proporcionan un control completo sobre los nombres, por lo que puede evitarse fácilmente el pro-
blema de la colisión de nombres.
Existen dos razones para controlar el acceso a los miembros. La primera consiste en evirar que los usuarios puedan hus-
mear en aquellas partes del código que no deben lOcar. Esas partes son necesarias para la operación interna de la clase, pero
no fonnan parte de la interfaz que el programa cliente necesita. Por tanto, definir los métodos y los campos como privados
constituye un servicio para los programadores de clientes, porque así éstos pueden ver fácilmente qué cosas so n impoI1an-
tes para ellos y qué cosas pueden ignorar. Simplifica su comprensión de la clase.
La segunda razón, todavía más importante. para definir mecanismos de control de acceso consiste en pennitir al diseñador
de bibliotecas modificar el funcionamiento interno de una clase sin preocuparse de si ello puede afectar a los programas
cliente. Por ejemplo. podemos diseñar al principio ulla clase de una cierta manera y luego descubrir que una cierta rees-
tructuración del código podría aumentar enomlemente la velocidad. Si la interfaz y la implementación están claramente pro-
tegidas, podemos realizar las modificaciones sin forzar a los programadores de clientes a reescribir su código. Los mecanis-
1110S de control de acceso garantizan que ningún programa cliente dependa de la implementación subyacente de una clase.
Cuando di sponemos de la capacidad de modificar la implementación subyacente, no sólo tenemos la libertad de mejorar
nuestro diselio, sino también la libertad de cometer errores. lndependicntemente del cuidado que pongamos en la planifica-
ción y el diseño, los errores son inevitables. Saber que resulta relativamente seguro cometer esos errores significa que ten-
dremos menos miedo a experimentar, que aprenderemos más rápidamente y que finalizaremos nuestro proyecto con mucha
más antelación.
La interfaz pública de una clase es lo que el usuario puede ver, así que constituye la parte de la clase que más importancia
tiene que definamos "correctamente" durante la fase de análisis del diseño. Pero incluso aquí existen posibilidades de modi-
ficación. Si no definimos correctamente la interfaz a la primera podemos añadir más mélOdos siempre y cuando no elimi-
nemos ningún método que los programas cliente ya hayan utilizado en su código.
Observe que el mecanismo de control de acceso se centra en una relación (yen un tipo de comunicación) entre un creador
de bibliotecas y los clientes externos de esas bibliotecas. Existen muchas situaciones en las que esta relación no está presen-
te. Por ejemplo, imagine que todo el código que escribimos es para nosotros mismos o que estamos trabajando en estrecha
relación con un pequeño grupo de personas y que todo lo que se escriba se va a incluir en un mismo paquete. En esas si-
tuaciones, el lipa de comunicación es diferente, y la rígida adhesión a una serie de reglas de acceso puede que no sea la solu-
ción óp tima. En esos casos, el acceso predetenninado (de paquete) puede que sea perfectamente adecuado.
Las soluciones a los ejercicios seleccionados pueden encontrarse en el documento electrónico The Thillkillg ill JUnI AI/I/Ulafed SO{I/fioll CI/ide. disponible
para la \"cnta en \\'\\'II'./LIil/dl'iell."ef.
Reutilización
de clases
Ésta es la técnica que se ha estado utilizando en lenguajes procedimental es como e, y no ha funcionado muy bien. Como
todo lo demás en Java, la solución que este lenguaje proporciona gira alrededo r del concepto de clase. Reutilizando el códi-
go creamos nuevas clases, pero en lugar de crearlo partiendo de cero, usamos las clases existentes que alguien haya cons-
truido y depurado anteri omlcnte.
El truco estri ba en utilizar las clases sin modificar el código existente. En este capítulo vamos ver dos fonnas de llevar esto
a cabo. La primera de ellas es bastante simpl e. Nos limitamos a crear objetos de una clase ya existente dentro de una nueva
clase. Esto se denomina composición, porq ue la nueva clase está compuesta de objetos de otras clases existentes. Con esto,
simplemente estamos reu tili zando la funcionalidad del códi go, no su fonna.
La segunda técnica es más sutil. Se basa en crear una nueva clase como un tipo de una clase existente. Literalm ente 10 que
hacemos es tomar la fonna de la clase existente y añadirl a código sin modificarla. Esta técnica se denomina herencia, y el
compi lador se encarga de realizar la mayor parte del trabajo. La herencia es una de las piedras angulares de la programa-
ción orientada a objetos y tiene una serie de implicaciones adicionales que analizaremos en el Capítulo 8, Poliformismo.
Resulta que buena parte de la sintaxis y el comportamiento son similares tanto en la composición como en la herencia (lo
que tiene bastante sentido, ya que ambas son fonnas de construir nuevos tipos a partir de otros tipos existentes). En este
capítulo, vam os a presentar ambos mecanismos de reutilización de código.
Sintaxis de la composición
Hemos utilizado el mecanismo de com posición de fonna bastante frecuente en el libro hasta este momento. Simplemente,
basta co n colocar referencias a objetos dentro de las clases. Por ejemplo, suponga que qu eremos construir un objeto que
almacene varios objetos String, un par de primitivas y otro objeto de otra clase. Para los objetos no primitivos, lo que hace-
mos es colocar referencias dentro de la nueva clase, pero sin embargo las primitivas se definen directamente:
11 : reusing/SprinklerSystem.java
II Composición para la reutilización de código.
class WaterSource {
private String S;
WaterSource () {
System . out .println ("Wate rSource () ,, ) ;
s = "Constructed";
1* Output:
WaterSource ( )
valvel = null valve2 null valve3 null valve4 null
i = O f = 0.0 source Constructed
* / //,-
Uno de los métodos definidos en ambas clases es especial : toString(). Todo objeto no primitivo tiene un método toString()
que se in voca en aquellas situaciones especiales en las que el compilador quiere una cadena de caracteres String pero lo que
tiene es un objeto. Así, en la expresión contenida en SprinklerSystem.toString():
11 source = 11 + source ¡
el compilador ve que estamos intentando añadir un objeto String ("source = 11) a un objeto WaterSource. Puesto que sólo
podemos '"añadir" un objeto String a otro objeto String, el compilador dice "¡voy a convertir source en un objeto String
in voca ndo toString() !". Después de hacer esto, puede combinar las dos cadenas de caracteres y pasar el objeto String res ul-
tante a System.out.println() (o, de fonna equi valente, a los métodos estáticos print() y printnb() que hemos definido en
este libro). Siempre que queramos permitir este tipo de comportamiento dentro de una clase que creemos, nos bastará con
escribir un método toString( ).
Las primitivas que son campos de una clase se inicializa n automát icamente con el valor cero, como hemos indicado en el
Capítulo 2, Todo es un objeto. Pero las referencias a objetos se inicializan con el valor null, y si tratamos de invocar méto-
dos para cualquiera de ellas obtendremos una excepción (un eITor en tiempo de ejecución). Afortunadamente, podemos
imprimir una referencia null si n que se genere una excepción.
Tiene bastante sentido que el compilador no cree un objeto predetenninado para todas las referencias, porque eso obligaría
a un gasto adicional de recursos completamente innecesarios en muchos casos. Si queremos inicializar las referencias, pode-
mos hacerlo de las siguientes fonnas:
J. En el lugar en el que los objetos se definan. Esto significa que estarán siempre inicializados antes de que se in vo-
que el constructor.
2. En el constructor correspondiente a esa clase.
3. Justo antes de donde se necesite utili zar el objeto. Esto se suele denominar inicialización diferida. Puede reducir
el gasto adicional de procesamiento en aquellas situaciones en las que la creación de los objetos resulte muy cara
y no sea necesario crear el objeto todas las veces.
4. Utili za ndo la técnica de iniciaUzación de instancia.
El siguiente ejemplo ilustra las cuatro técnicas:
11: reusing / Bath.java
I I Inicialización mediante constructor con composición.
import static net.mindview.util . Print .* ¡
class Soap {
private String Si
7 Reutilización de clases 141
Soap () (
print ("Soap()") i
s = "Constructed";
public String toString () { return s; }
// Inicialización de instancia:
{i= 47;}
public String toString () {
if (54 == nulll / / Inicialización diferida:
54 = IIJoy";
return
"sl + 51 + " \n" +
"52 + 52 + "\n" +
IIs3 + 53 + "\n" +
"54 + 54 + "\nn +
"i = 11 + i + "\n" +
"toy = " + toy + " \n " +
"castille = " + castille;
/* Output:
Inside Bath ()
Soap ()
51 Happy
s2 Happy
53 Joy
54 Joy
i :::: 47
toy = 3.14
castille Constructed
,///,-
Observe que en el constructor 8ath, se ejecuta una instrucción antes de que tenga luga r ninguna de las inicializaciones.
Cuando no se reali za la iniciaLización en el punto de defi nición, sigue sin haber ninguna garantía de que se vaya a reali zar
la inicialización antes de enviar un mensaje a una referencia al objeto, salvo la inevi table excepción en tiempo de ejecución.
Cuando se invoca toString(), as igna un valor a s4 de modo que todos los campos estarán apropiadamente inicializados en
el momento de usarlos.
Ejercicio 1: (2) Cree una clase simple. Dentro de una segunda clase, defina una referencia a un objeto de la segunda
clase. Utilice el mecanismo de inicialización diferida para instanciar este objeto.
142 Piensa en Java
Sintaxis de la herencia
La herencia es una parte esencial de Java (y lOdos los lenguajes orientados a objetos). En la práctica. siempre estamos uti-
liza ndo la herencia cada vez que creamos UDa clase, porque a menos que heredemos explícitamente de alguna otra clase.
es taremos heredando implícitamente de la clase raíz estándar Object de Java.
La si ntaxis de composición es obvia, pero la herencia utiliza una sintaxis especial. Cuando heredamos, lo que hacemos es
decir "esta nu eva clase es similar a esa antigua clase". Especificamos esto en el código situado antes de la llave de abertu-
ra del cuerpo de la clase. utilizando la palabra clave extends seguida del nombre de la clase base. Cuando hacemos esto,
automáticamente obtenemos todos los campos y métodos de la clase base. He aquí una clase:
JI: reusing/Detergent.java
JI Sintaxis y propiedades de la herencia.
impore static net.mindview.util.Print.*¡
class Cleanser {
private String s = "Cleanser";
public void append(String al { s += a¡ }
public void dilute{) ( append(U dilute() ");
public void apply () ( append (" apply () " ); }
public void scrub () ( append (" scrub () " ) ; }
public String toString () { return Si}
public static void main(String[] args)
Cleanser x = new Cleanser();
x . dilute(); x.apply(); x.scrub();
print (x l;
/ * Output:
Cleanser dilute () apply () Detergent . scrub () scrub () f oam()
Testing base class :
Cleanser dilute () apply () scrub()
* /// ,-
Esto nos pem1ite ilustrar una serie de características. En primer lugar, en el método Cleanser append(). se concatenan cade-
nas de caracteres con S utilizando el ope rador +=. que es uno de los operado res, junto con
"han sobrecargado" para que funcionen con cadenas de caracteres.
'+" que los diseñadores de Java
En segu ndo lugar. tanto eleanser co mo Detergent contienen un método maine ). Podemos crear un método maine ) para
cada una de nuestras clases: esta técnica de colocar un método main() en cada clase pcm1ite probar fácilmente cada una de
7 Reutiliza ción de clases 143
ellas. V no es necesario el iminar el método maine ) cua ndo hayamos tenn inado: podemos dejarlo para las pruebas poste-
riores.
Aún cuando tengamos muchas clases en un programa, sólo se invoca rá el método main() de la clase especificada en la línea
de comandos. Por tanto, en este caso, cuando escribimos java Detergent, se invocará Detergent.main( ). Pero también
podemos escribi r java Cleanser para invocar a Cleanser.main( ), aún cuando Cleanser no sea una clase pública. Incluso
aunque una clase tenga acceso de paquete. si el método main() es público, también se podrá acceder a él.
Aquí. podemos ver que Detergcnt.main() llama a C lea nser.main ( ) explícitamente, pasándole los mismos argumentos de
la línea de comandos (sin embargo. podríamos pasarle cualquier matriz de objetos String).
Es importante que todos los métodos de C leanser sean públicos. Recuerde que, si no se indica ningún especificador de acce-
so. los miembros adoptarán de f0n11a predetenninada el acceso de paquete, lo que sólo pennite el acceso a los otros miem-
bros del paquete. Por tanto. dentro de este paquete. cualquiera podría usar esos métodos si no hubiera ningún especificador
de acceso. Detergent. por ejemplo. no tendría ningún problema. Sin embargo. si alguna c lase de algún otro paquete fuera a
heredar de Cleanser, sólo podría acceder a los miembros de tipo public. Así que, para permitir la herencia, como regla gene-
ral deberemos definir todos los campos eomo private y todos los métodos como public (los miembros de tipo protecled
también pemliten el acceso por parte de las clases derivadas; analizaremos este tema más adelante). Por supuesto. en algu-
nos casos particulares será necesario hacer excepciones. pero esta directriz suele resultar bastante útil.
Cleanser dispone de una serie de métodos en su interraz: append( ). dilute( ). apply( ). serub( ) y toString( ). Puesto que
Detergent deril'a de Cleanser (gracias a la palabra clave extends). automáticamente obtendrá todos estos métodos como
parte de su interfaz, aún cuando no los veamos explícitamente definidos en Detergent. Por tanto. podemos considerar la
herencia como un modo de reutilizar la clase.
Como podemos ver en scrub( ), es posible tomar un método que haya sido definido en la clase base y modificarlo. En este
caso, puede también que queramos invocar el método de la clase base desde dentro de la nueva versión. Pero dentro de
scrub( ), no podemos simplemente invocar a scrub( ). ya que eso produciría una llamada recursiva. que no es exactamen-
te 10 que queremos. Para resolver este problema, la palabra clave super de Java hace referencia a la "supcrclase" de la que
ha heredado la clase actual. Por tanto. la expresión supcr.scrub( ) invoca a la versión de la c lase base del método scrub( ).
Cuando se hereda, no estamos limirados a utilizar los métodos de la clase base. También podemos añadir nuevos métodos
a la clase derivada, de la misma forma que los añadiríamos a otra clase: definiéndolos. El método foam( ) es un ejemplo de
esto.
En Detergent.main( ) podemos ver que. para un objeto Detergent, podemos invocar todos los métodos disponibles en
CIeanseO' osi C0l110 en Detergent (es decir, foam()).
Ejemplo 2: (2) Cree una nueva clase que herede de la clase Detergent. Sustinlya el método scrub() y añada un nuevo
método denominado stcrilize( ).
11 : reusing / Cartoon.java
11 Llamadas a constructores durante la herencia.
144 Piensa en Java
class Art
Art () { print ( "Art constructor" ) ; }
1* Output:
Art constructor
Drawing constructor
Cartoon constructor
*/1/,-
Como puede ver, la construcción tiene lugar desde la base hacia "afuera", por lo que la clase base se inicializa antes de que
los constructores de la clase derivada puedan acceder a ella. Incluso aunque no creáramos un constructor para Cartoon() ,
el compilador sintetizaría un constructor predeternlinado que in vocaría al constructor de la clase base.
Ejercicio 3: (2) Demuestre la afinnación anterior.
Ejercicio 4: (2) Demuestre que los constructores de la clase base (a) se in vocan siempre y (b) se invocan antes que los
constructores de la clase deri vada.
Ejercicio 5: (1) Cree dos clases, A y B, con constructores predeterminados (listas de argum ent os vacias) que impriman
un mensaje infornlando de la construcción de cada objeto. Cree una nueva clase llamada e que herede de
A, y cree un miembro de la clase B dentro de C. No cree un constructor para C. Cree un obj eto de la clase
e y observe los resu ltados.
Constructores con argumentos
El ejemplo anterior tiene constructores predetenn inados; es decir, que no tienen argumentos. Resulta fáci l para el compila-
dor invocar estos constructores, porque no existe ninguna duda acerca de qué argumen tos hay que pasar. Si no existe un
constructor predetenninado en la clase base. o si se quiere in vocar un constructor de la clase base que tenga argumentos,
será necesario escri bir explícitamente una llamada al constmctor de la clase base uti lizando la palabra clave super y la lista
de argumentos apropiada:
11: reusing/Chess.java
II Herencia, constructores y argumentos.
import static net.mindview.util.Print.*;
class Game {
Game {int i)
print ( "Game constructor" ) i
Chess (1 {
super(ll) ;
print ( "Chess constructor lt ) ;
/ * Output :
Game constructor
BoardGame constructor
Chess constructor
* /// >
Si no se invoca el constructor de la clase base en BoardGame( ), el compilador se quejará de que no puede localizar un
constructor de la fonna Ga me( ). Además. la llamada al constructor de la clase base debe ser la primera cosa que se haga
en el constructor de la clase derivada (el compilador se encargará de recordárselo si se olvida de ello).
Ejercicio 6 : (1) Utilizando Chess.java. demuestre las afimlaciones del párrafo anterior.
Ejercicio 7 : (1) Modifique el Ejercicio 5 de modo que A y B tengan constructores con argumentos en lugar de cons-
tTuctores predetenninados. Escriba un constructor para e que realice toda la inicialización dentro del cons-
tructor de C.
Ejercic io 8 : (1) Cree una clase base que sólo tenga un constructor no predeterminado y una clase derivada que tenga
un constructor predetenninado (sin argumentos) y otro no predetemlinado, En los constmctores de la clase
derivada, invoque al constructor de la clase base,
Ejercic io 9 : (2) Cree Wla clase denominada Root que contenga una instancia de cada una de las siguientes clases (que
también deberá crear): Componentl, Component2 y Component3. Derive una clase Stem de Root que
también contenga uoa instancia de cada "componente", Todas las clases deben tener constructores prede-
terminados que impriman un mensaje relativo a la clase,
Ejercic io 10: (1) Modifique el ejercicio anterior de modo que cada clase sólo tenga constructores no predetenninados.
Delegación
Una tercera relación, que no está directamente soportada en Java, se denomina delegación. Se eocuentra a caballo entre la
herencia y la composición, por que lo que hacemos es incluir un objeto miembro en la clase que estemos construyendo
(como en la composición), pero al mismo tiempo exponemos todos los métodos del objeto miembro en nuestra nueva clase
(como en la herencia). Por ejemplo, una nave espacial (spaceship) necesita un módulo de control:
11: reusing/SpaceShipControls . java
Sin embargo. un objeto SpaceShip no es realmcnle "un tipo de" objeto SpaccShipControls. aún cuando podamos, por
ejemplo. "deeirle" al objeto SpaceShip que avance hacia adelante (forward ( )). Resu lta más preciso decir que la na ve espa-
cial (el objeto SpaceShip) cOl/liel/e un módulo de control (objeto SpaceShipControls), y que, al mismo tiempo. todos los
métodos de SpaceShipControls deben quedar expuestos en el objeto SpaceShip. El mecanismo de delegació n pemlite
resolver este dilema:
jj : reusingjSpaceShipDelegation.java
jj Métodos delegados:
public void back (i nt velocity )
controls .ba c k (velocit y ) ;
Como puede ver los métodos se redirigen hacia el método controls subyacente, y la interfaz es, por tanto, la misma que con
la herencia. Sin embargo, tenemos más control con la delegación, ya que podemos decidir proporcionar únicamente un sub-
conjunto de los métodos contenidos en el objeto miembro.
Aunque el lenguaje Java no soporta directamente la delegación, las herramientas de desarrollo sí que suelen hacerlo.
Por ejemplo, el ejemplo anterior se ha ge nerado automáti camente utili za ndo el entorno integrado de desarrollo JetBrains
Idea.
Ejercicio 11: (3) Modifique Detergent.java de modo que utilice el mecanismo de delegación .
7 Reutilización de clases 147
class Plate {
Plate (int i)
print ("Plate constructor") i
class Utensil (
Utensil(int i)
print ("Utensil constructor");
/ * Output :
Custom cons tructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
PI ate constructor
DinnerPlate constructor
PlaceSetting constructor
* /// , -
Aunque el compi lador nos obliga a inicializar la clase base y requiere que lo hagamos al principio del constructor. no se ase-
gura de que inicialicemos los objetos miembro, así que es preciso prestar atención a este detalle.
Resulta asombrosa la fonna tan limpia en que las clases quedan separadas. No es necesario siquiera disponer del código
fuente de los métodos para poder reut ilizar ese código. Como mucho, nos basta con limitamos a importar un paquete (esto
es cierto tanto para la herencia como para la composición).
class Shape {
Shape{int i) print (IIShape constructor") ¡
void dispose () { print ("Shape dispose H ) i }
7 Reutilización de clases 149
void dispose () {
print ("Erasing tine: " + start + " + end);
super.dispose() i
e = new Circle(1);
t = new Triangle{1);
print ("Combined constructor");
/* Output:
Shape constructor
Shape constructor
Drawing Line: 0, O
Shape constructor
Drawing Line: 1, 1
Shape constructor
Drawing Line: 2, 4
Shape constructor
Drawing Circle
Shape constructor
Drawing Triangle
Combined constructor
CADSystem.dispose()
Erasing Triangle
Shape dispose
Erasing Circle
Shape dispose
Erasing Line: 2, 4
Shape dispose
Erasing Line: 1, 1
Shape dispose
Erasing Line: O, O
Shape dispose
Shape dispose
*///,-
Todo en este sistema es algún tipo de objeto Shape (que a su vez es un tipo de Object, puesto que hereda implícitamente
de la clase raíz). Cada clase sustituye el método dispose( ) de Shape, además de invocar la versión de dicho método de la
clase base utilizando super. Las clases Shape específicas, Circle, Triangle y Une, tienen constructores que "dibujan" las
formas geométricas correspondientes, aunque cualquier método que se invoque durante la vida del objeto puede llegar a ser
responsable de hacer algo que luego requiera una cierta tarea de limpieza. Cada clase tiene su propio método dispose() para
restaurar todas esas cosas que no están relacionadas con la memoria y dejarlas en el estado en que estaban antes de que el
objeto se creara.
En maine ), hay dos palabras clave que no habíamos visto antes y que no va n a explicarse en delalle hasta el Capitulo le.
Traramiento de errores mediante excepciones: try y finally . La palabra clave try indica que el bloque situado a continua-
ción suyo (delimitado por llaves) es una región protegida, lo que quiere decir que se la otorga un tratamiento especial. Uno
de estos tratamientos especiales consiste en que el código de la cláusula finally sihlada a continuación de esta región prote-
gida siempre se ejecuta, independientemente de cómo se salga de bloque try (con el tratamiento de excepciones, es posible
salir de un bloque try de di versas formas distintas de la normal). Aquí, la cláusula finally dice: "Llama siempre a dispose()
para x, independientemente de lo que suceda".
En el método de limpieza, (dispose(), en este caso), también hay que prestar atención al orden de Llamada de los métodos
de limpieza de la clase base y de los objetos miembro, en caso de que un subobjelo dependa de otro. En general, hay que
seguir la misma forma que imponen los compiladores de C++ para los destructores: primero hay que realizar toda la tarea
de limpieza específica de la clase, en orden inverso a su creación (en generaL esto requiere que los elementos de la clase
base sigan siendo viables). A continuación, se invoca el método de limpieza de la clase base, C0l110 se ilustra en el ejemplo.
Hay muchos casos en los que el tema de la limpieza no constituye un problema, bastando con dejar que el depurador de
memoria realice su tarea. Pero cuando hay que llevar a cabo una limpieza explícita se requieren grandes dosis de diligencia
y atención, porque el depurador de memoria no sirve de gran ayuda en este aspecto. El depurador de memoria puede que no
7 Reutilización de ctases 151
llegue nunca a ser invocado y, en caso de que lo sea, podría reclamar los objetos en el orden que quisiera. No podemos con-
fiar en la depuración de memoria para nada que no sea reclamar las zonas de memoria no utilizadas. Si queremos que las
tareas de limpieza se lleven a cabo. es necesario definir nuestros propios métodos de limpieza y no emplear finalize() .
Ejercicio 12: (3) Aliada una jerarquía adecuada de métodos dispose() a todas las clases del Ejercicio 9.
Ocultación de nombres
Si una clave base de Java tiene un nombre de método varias veces sobrecargado, redefinir dicho nombre de método en la
clase derivada no ocultará ninguna de las versiones de la clase base (a diferencia de lo que sucede en e++). Por tanto, el
mecanis mo de sobrecarga funci ona independientemente de si el método ha sido definido en este nivelo en tina clase base:
// : reusing / Hide . java
// La sobrecarga de un nombre de método de la clase base en una
II clase derivada no oculta las versiones de la clase base.
i mport static net.mindview . util.Print .* ;
c lass Homer {
char doh (char c) {
print ("doh (char) ") ;
return 'd';
class Milhouse {}
public class Hi de {
public sta t ic void main (Str i ng [) args) {
Bart b = new Bart {) i
b . dohlll;
b.doh( ' x ');
b . doh l l.Of l ;
b . doh (new Milhouse ()) i
1* Output :
dohlfloa tl
dOh(c har)
doh Ifloa t I
doh( Milhouse)
*/11 , -
Podemos ver que todos los métodos sobrecargados de Homer están disponibles en Bart. aunque Bart introduce un nuevo
método sobrecargado (si se hiciera esto en C++ se ocultarían los métodos de la clase base). Como veremos en el siguiente
capitulo, lo más común es sobrecargar los métodos del mismo nombre, utili zando exactamente la misma signatura y el
mismo tipo de retomo de la clase de retomo. En caso contrario, el código podría resultar confuso (lo cual es la razón por la
que C-++ oculta todos los métodos de la clase base. para que no cometamos lo que muy probablemente se trata de un error).
Java SES ha añadido al lenguaje la anotación @Override, que no es una palabra clave pero puede usarse como si lo fuera.
Cuando queremos sustituir un método, podemos añadir esta anotación y el compilador generará un mensaje de error si sobre-
ca rgamos accidentalmente e l método en lugar de sustiruirlo:
152 Piensa en Java
class Engine {
public void start () {}
public void rey 11 {)
public void stop 11 {)
class Wheel {
public void inflate (int psi ) {}
class Window {
public void rollup 11 {)
public void rolldown ( ) {)
class Door {
public Window window = new Window()¡
public void open 11 {)
7 Reutilización de clases 153
Puesto que en este caso la composición de un coche fonna parte del análisis del problema (y no simplemente del diseño sub-
yacente), hacer los miembros públicos ayuda al programador de clientes a entender cómo utilizar la clase, y disminuye tam-
bién la complejidad del código que el creador de la clase tiene que desarrollar. Sin embargo, tenga en cuenta que éste es un
caso especial y que, en general, los campos deberían ser privados.
Cuando recurrimos almcc3nismos de herencia, lo que hacemos es tomar una clase existente y definir una ve rsión especial
de la misma. En general, esto quiere decir que estaremos tomando una clase de propósito general y especial izándola para
una necesidad concreta. Si reflexionamos un poco acerca de ello, podremos ver que no tendría ningún sentido componer un
coche utilizando un objeto vehícu lo. ya que un coche no contiene vehícu lo, sino que es un vehículo. La relación eS-1II1 se
expresa mediante la herencia, mientras que la relación tiene-un se expresa mediante la composición.
Ejercicio 14: (1) En Carojava añada un método service() a Engine e invoque este método desde main().
protected
Ahora que ya hemos tomado contacto con la herencia. vemos que la palabra clave protected adquiere su pleno significado.
En un mundo ideal, la palabra clave private resuharía suficiente, pero en los proyectos reales, hay veces en las que quere-
mos ocultar algo a ojos del mundo pero pennitir que accedan a ese algo los miembros de las clases derivadas.
La palabra clave protected es homenaje al pragmatismo, lo que dice es: "Este elemento es privado en lo que respecta al
usuario de la clase, pero está disponible para cualquiera que herede de esta clase y para todo lo demás que se encuentre en
el mismo paquete (protccted también proporciona acceso de paquete)"o.
Aunque es posible crear campos de tipo protected (protegidos), lo mejor es definir los campos como privados; esto nos per-
mitirá conservar siempre el derecho a modificar la implementación subyacente. Entonces, podemos permitir que los here-
deros de nuestra clase dispongan de un método controlado utilizando métodos protected :
JI: reusingfOrc.java
JI La palabra clave protected.
import static net.mindview.util.Print.*¡
clas s Villain
private String name;
protected void set (String nm ) { name = nm; }
public Villain(String name ) { this.name = name;
public String toString () {
return "I'm a Villain and my name is " + name;
154 Piensa en Java
1* Out p ut :
Or e 1 2: l' m a Vi l lain and my name i s Li mburger
Or e 19 : l ' m a Vi l la i n a n d my n a me i 5 Bob
* /// , -
Puede ver que ehange() tiene a cceso a sel() porque es de tipo proleeled. Observe también la forma en que se ha defini-
do el método loSlring() de O re en términos de loSlring() de la clase base.
Ejercicio 15: (2) Cree una clase denlro de un paquete. Esa clase debe estar dentro de un paquete. Esa clase debe conte-
ner un método protected . Fuera del paquete, trate de invocar el método protected y explique los resulta-
dos. Ahora defina otra clase que herede de la anterio r e in voque el método prolected desde un método de
la clase derivada.
Upcasting (generalización)
El aspecto más importante de la herencia no es que proporciona métodos para la nueva clase, sino la relación expresada en-
tre la nueva clase y la clase base. Esta relación puede resumirse dic iendo que " la nueva clase es un lipo de la clase exis-
tente".
Esta descripción no es simplemente una foona elegante de explicar la herencia, sino que está soportada directamente por el
lenguaje. Por ejemplo, considere una clase base denominada [nstrument que represente instrumentos musicales y una clase
derivada denominada Wind (instrumentos de viento). Puesto que la herencia garanti za que todos los métodos de la clase
base estén disponibles también en la clase derivada, cualquier mensaje que enviemos a la clase base puede enviarse también
a la clase derivada. Si la clase Inslrumenl tiene un método play() (tocar el instrumento), también lo tendrán los instrum en-
tos de la clase Wind . Esto significa que podemos dec ir con propiedad que un objeto Wind es también un objeto de tipo
Inslrumon!. El siguiente ejemplo ilustra cómo soporta el compilador esta idea.
11 : reusing / Wind.java
II Herencia y generalización.
class Instrument {
public void play () {}
static void tune (Instrument i ) {
/ / ...
i. play () ;
Al reali zar una proyección de un tipo derivado al tipo base, nos movemos hacia arriba en el diagrama de herencia, y esa
es la razón de que en inglés se utilice el término upcasling (up = arriba, casI = proyección. El upcas ling o generalización
siempre resulta seguro, porque estamos pasando de un tipo más específico a otro más general. Es decir, la clase derivada es
UD superconjunto de la clase base. Puede que la clase derivada contenga más métodos que la clase base, pero debe contener
al menos los métodos de la clase base. Lo único que puede ocurrir con la interfaz de la clase durante la generalización es
que pierda métodos, no que los gane, y ésta es la razón por la que el compilador pennite la generali zación sin efectuar nin-
gún tipo de proyección explícita y sin emplear ninguna notación especial.
También podemos realizar el inverso de la generali zación, que se denomina downcasling (especialización), pero esto lleva
asociado un cierto dilema que examinaremos más en detalle en el siguiente capítulo, y en el Capítulo 14, Información de
tipos.
main( ). cree un objeto Frog y realice una generalización a Amphibian, demostrando que lodos los méto-
dos siguen funcionando.
Ejercicio 17: (1) Modifique el Ejercicio 16 para que el objeto Frog sustituya las definiciones de métodos de la clase
base (propo rcione las nuevas definiciones utilizando las mismas signaturas de métodos). Observe 10 que
sucede en main( ).
Datos final
Muchos lenguajes de programación disponen de alguna fonna de comunicarle al compilador que un elemento de datos es
"constante". Las constantes son útiles por dos razones:
1. Puede tratarse de una constante de tiempo de compilación que nunca va a cambiar.
2. Puede tratarse de un valor inicializado en tiempo de ejecución que no queremos que cambie.
En el caso de una constante de tiempo de compilación, el compilador está autorizado a "compactar" el va lor constante en
todos aquellos cálculos que se le utilice; es decir, el cálculo puede realizarse en tiempo de compilación eliminando así cier-
tos cálculos en tiempo de ejecución. En Java, estos tipos de constantes deben ser primitivas y se expresan con la palabra
clave final . En el momento de definir una de tales constantes, es preciso definir un valor.
Un campo que sea a la vez static y final sólo tendrá una zona de almacenamiento que no puede nunca ser modificada.
Cuando final se utiliza con referencias a objetos en lugar de con primitivas, el significado puede ser confuso. Con una pri-
mitiva, final hace que el valor sea constante, pero con una referencia a objeto lo que final hace constante es la referencia.
Una vez inicializada la referencia a un objeto, nunca se la puede cambiar para que apunte a otro objeto. Sin embargo, el pro-
pio objeto sí que puede ser modificado; Java no proporciona ninguna manera para hacer que el objeto arbitrario sea cons-
tante (podemos, sin embargo, escribir nuestras clases de modo que tengan el efecto de que los objetos sean constantes). Esta
restricción incluye a las matrices, que son también objetos.
He ?quí un ejemplo donde se ilustra el uso de los campos final . Observe que, por convenio, los campos que son a la vez
static y final (es decir, constantes de tiempo de compilación) se escriben en mayúsculas. utilizando guiones bajos para sepa-
rar las palabras.
11: reusing/FinalData.java
II Efecto de final sobre los campos.
import java.util.*;
import static net.mindview.util.Print.*;
class Value {
int i; II Acceso de paquete
public Value(int i) {th is.i i;
1* Output:
fd1, i4 = 15, INT 5 = 18
Creating new FinalData
fd1, i4 15, INT_5 18
fd2, i4 = 13, INT 5 = 18
*111 ,-
Dado que va lueOne y VALUE_TWO son primitivas final con valores definidos en tiempo de compilación, ambas pueden
usarse como constantes de tiempo de compilación y no se diferencian en ningún aspecto importante. VALUE_THREE es
la fonna más típica en que podrá ver definidas dichas constantes: public que se pueden usar fuera del paquete, static para
enfatiza r que sólo hay uo a y final para decir que se trata de una constante. Observe que las primitivas final static con valo-
res iniciales constantes (es decir, constantes en tiempo de compilación) se designan con letras mayúsculas por convenio,
separando las palabras mediante guiones bajos (al igual que las constantes en C, que es el lenguaje en el que surgió este con-
venio).
El que algo sea final no implica necesariamente el que su va lor se conozca en ti empo de compilación. El ejemplo ilustra
está inicializando i4 e lNT_5 en tiempo de ejecución, mediante números generados aleatoriamente. Esta parte del ejemplo
también genera la diferencia entre hacer un va lor final estát ico o no estático. Esta diferencia sólo se hace patente cuando los
valores se inicializan en tiempo de ejecuci ón, ya que el compilador trata de la misma manera los valores de tiempo de com-
pilación (en muchas ocasiones, optimizando el códi go para eliminar esas constan tes). La diferencia se muestra cuando se
ejecuta el programa. Observe que los valores de i4 para fd1 y fd2 son distintos, mientras que el valor para INT_5 no cam-
bia porq ue creemos el segu ndo objeto FinalData. Esto se debe a que es estát ico y se inicializa una sola vez durante la carga
y no cada vez que se crea un nuevo objeto.
Las vari ables v1 a VAL_3 ilustran el significado de una referencia final . Como puede ver en main( ), el hecho de que v2
sea final no quiere decir que se pueda modificar su valor. Puesto que es una referencia, final significa que no se puede aso-
ciar v2 COIl un nuevo objeto. Podemos ver que la afinnación también es cierta para las matrices, que son otro de tipo de refe-
rencia (no hay nin guna fom1a que yo conozca de hacer que las referencias a una matri z sean final). Hacer las referencias
final parece menos úti l que definir las primitivas como final.
158 Piensa en Java
Ejercicio 18: (2) Cree una clase con un campo static final y un campo final y demuestre la diferencia entre los dos.
cIass Poppet {
private int i¡
Poppet (int ii I { i ii; }
Estamos obligados a realizar asignaciones a los valores final utilizando una expresión en el punto de definición de) campo
o bien en cada constructor. De esa fonna, se garantizará que el campo final esté siempre inicializado antes de utilizarlo.
Ejercicio 19: (2) Cree una clase con una referencia final en blanco a un objeto. Realice la inicialización de la referen-
cia final en blanco dentro de todos los constructores. Demuestre que se garantiza que el va lor final esta-
rá inicializado antes de utilizarlo, y que no se puede modificar una vez inicializado.
Argumentos final
Java pennite definir argumentos final declarándolos como tales en la lista de argumentos. Esto significa que dentro del
método no se podrá cambiar aquello a lo que apunte la referencia del argumento:
11 : reusing / FinalArguments.java
Il uso de 11 final" con argumentos de métodos.
class Gizmo {
publie vo id spin () {}
.:~:j wit~c~:IG:zmo g.
el = n e',,' ·; :;izmo {; ; 1/ 8K 9 r.o es :i"o.1
;; . sp:n í;' i
Lo" m~tod05 f( ) Y g( ) muestran lo que sucede cuando los argumento s primitivos son final : Se puede leer el argumento. pero
no 1l10diticarlo. Es ta carac¡el'Ística se utili za principall11ell!e pam pasa r dato s a la:-; clases intemas anónimas. lo cua l es un
tt'll1~! dcl que hablaremos en el C3pítulo 10. Clases imen/Us.
Métodos final
Hay dos razones para utilizar métodos finaL. La primera es "'bloquear" el método para impedir que cualqu ie.r clase quc here-
de dc esta ca mbi e su sign ificado. Esto se hace por razones de discí10 cuando queremos aseguramos de que se retenga el Cl1lll-
pon allliemo de un méwdo duranre la herencia y que ese método pueda se r sustituido.
La'lL'~ul1da razón por la que se ha sugerido en ('.1 pasado la utilización <,1(' los métodos tinal es la eficiencia. En las illlplc-
Illcllfill."ionl!s anteriores de Java, si definíamos un método como final. pemlitíamos al compi lador convenir rodas las Ihllna-
da::- a ese método en llamadas en línea. Cuando el comrilador veía una llamada a un método final. podía (a su discreción)
sal!:lrse el modo 110nnal de inserta r el código correspondientc almccanismos de llamada a l método (insenar los arg umen-
tos en la pi la. saltar al código del método y ejecutarlo, saltar hacia arras y eliminar de la pila los argumentos y tratar el vall)]"
de retorno), para sus titui r en su lugar la llamada al mélodo por Wla copia del propio código contenido en el cuerpo del mé-
todo. Esto e limina el ga~to adicional de recursos asociado a la llamada al método. Por s upuesto. si el método es de gran
!alllai1o. el código empezará a cr..::cer ellomlem~nte y probablemente no detectemos ninguna mejora de velocidad por la uti·
Ii.~:)c ibll de metodos en línea. ya qw: la mejora será insigniti~ante comparada con la call1idad de tiempo invertida dentro del
111CIOdo.
En 111:-. \ersiones más recicmes de J¿l\,a. la máquina virtual (en panicular. la IccnologÍ<I/IOISpOI) puede dete(:tar L'Stas situa-
\.'i~lnes y eliminar el paso adicional de indirección. por lo que ya no es nece sa rio (de becho. se desaconseja por regla gene-
ra l) util izar final para tratar de ayudar al optimizador. Con laya SES/A. lo que debemos hacer es dejar que el compilador y
la .1 V\:1 se encarguell de las cU6tiollCS de eficiencia, y sólo debemos definir un Ill~todo como final si queremos impedir
explkitall1.:nte la ~ustitllción del mélOdo en las clases deri\'adas.!
final y private
Los métodos prhados de una clase son implicit3ment(' de tipo final (finales). Puesto que no Se puede acceder a un método
privndo. es imposible sustituirlo en una clase dc.:-ri\·ada. Podemos añadir el especifícador tionl a un método priva te. pero no
h:.'IhlriÍ ningún efecto adicional.
I:SlC rema puede causar algo de confusión, porque si se trata de sustiruir un métolÍll private (que implícitamente es fimll).
p;lre~e que el mecanismo funciona y e l co mpilador no proporciona ningún mensaje de en'Oc
././ : reus ing / FinalOverridingIll usion. java
l/ Tan sólo pal-ece que podamos sustituir
1/ U~ mét.odo privado o un método privado final.
i~po~t sta:ic net . mindview . util.Prin~ .*·
' o ri<!rda d ti \.'lllpo u1l1:mdo de opllmií'ar pr.:maturamente. Si d sistema funciona ':<' .:s demasiado lento. r<!sulta dndo)ú qu<! lo pueda soh ent<lr \.:ün la
p~lbbra claw final. h"p."'·Milldl iell./Ie! Bouks. Bo!fft'rJal'i't L'Llnticne infom1aóón au::rC!l de Ia~ tccnicas d~ pcrtibdo. qUl' Plledl'n servir d.: ayuJa a la hora
'.k' :ll'el¿omr Jos pwgrama~ .
160 Piensa en Java
class WithFinals {
II Idéntico al uso de IIprivate ll sola:
private final void f () { print {IIWithFinals.f () 11 ) ;
I I También automaaticámente 11 final" :
private void g () { print ( IIWithFinals.g () " ) ¡ }
public void 9 1) {
print {"OverridingPrivate2.g() It) ¡
1* Output:
OverridingPrivate2.f ()
OverridingPrivate2.g ()
*//(,-
La "sustitución de los métodos" sólo puede tener lugar si el método forma parte de la clase base. En otras palabras, es nece-
sario poder generalizar un objeto a su tipo base y poder invocar al mismo método (este tema resultará más claro en el
siguiente capítulo). Si un método es privado, no fonna parte de la interfaz de la clase base. Se trata simplemente de un cier-
to código que está oculto dentro de la clase y que sucede que tiene ese nombre, pero si creamos un método public, protec-
ted o con acceso de paquete con el mismo nombre en la clase derivada, no habrá ninguna conexión con el método que resulte
que tiene el mismo nombre en la clase base. Es decir, no habremos sustituido el método, sino que simplemente habremos
creado otro nuevo. Puesto que un método private es inalcanzable y resulta in visible a efectos prácticos, a lo único que afec-
ta es a la organización del código de la clase en la que haya sido definido.
Ejercicio 20: (1) Demuestre que la anotación @Override resuelve el problema descrito en esta sección.
Ejercicio 21: (1) Cree una clase con un método final. Cree otra clase que herede de la clase an terior y trate de sustituir
ese método.
7 Reutilización de clases 161
Clases final
Cuando decimos que toda una clase es de tipo final (precediendo su definición con la palabra clave final ), lo que estamos
diciendo es que no queremos heredar de esta clase ni pennitir que nadie más lo haga. En otras palabras, por alguna razón,
el diseño de nuestra clase es de tal naturaleza que nunca va a ser necesario efectuar ningún cambio, o bien no queremos que
nadie defi na clases deri vadas por razones de seguridad.
jI : reusing/Jurassic . java
// Definición de una clase completa como final.
class SmallBrain {}
prestaciones. lo que probablemente anula cualquier ganancia proporcionada por final. Este ejemplo tiende a avalar la teo-
ría de que los programadores suelen equivocarse siempre a la hora de decidir dónde hay que optimizar. Resulta un poco
penoso que un diseño tan pobre terminara siendo incluido en la biblioteca estándar para que lOdo el mundo tuviera que
sufrirlo (afortunadamente, la moderna biblioteca de contenedores Java sustituye Vecto r por ArrayList, que se comporta de
una fonna mucho más civilizada; lamentablemente, hoy día se sigue escribiendo código que utili za la antigua biblioteca de
contenedores).
Resulta también interesante observar que Hashtable, otra clase importante de la biblioteca estándar 1.0/ 1.1 de Java no tiene
ningún método tinal. Como ya se ha mencionado, es patente que algunas de las clases fueran diseñadas por personas dis-
tintas (tendrá la ocasión de comprobar que los nombres de los métodos en Hashtable son mucho más breves comparados
con los de Vecto r, lo que constituye otra prueba de esta afirnlación). Esto es, precisamente, el tipo de cosas que no resultan
obvias para los consumidores de una biblioteca de clases. Cuando las cosas no son coherentes, damos más trabajo del nece-
sario a los usuarios, lo cual es otro argumento en favor de las revisiones de diseño y de los programas (observe que la biblio-
teca actual de contenedores Java sustituye Has htable por HashMa p).
class Insect {
private int i = 9;
protected int j;
Insect (1 {
print("i = " + i + j 11 + j);
j = 39;
2 El constructor es también un método eSHi.tico, aún cuando la palabra clave sta tic no sea explícila. Por tanlo. para ser precisos, una clase se carga por pri-
mera vez cuando se accede a cualquiera de sus miembros estáticos.
7 Reutilización de clases 163
print (s ) ;
return 47;
/ * Ou tput:
stati c Insect.xl initialized
s t atic Beetle.x2 initialized
Beetle constructor
i= 9,j=O
Beet l e.k initialized
k = 47
j =39
* /// ,-
Lo primero que sucede cuando se ejecuta Java con Beetle es que se trata de acceder a Beetle.main() (un método estático),
por lo que el cargador localiza el código compilado correspondiente a la clase Beetle (en un archivo denominado
Beetle.class). Durante e l proceso de carga, el cargador observa que tiene una clase base (eso es lo que dice la palabra clave
extends), por lo que procede a cargarlo. Esto sucederá independientemente de si se va a construir un objeto de dicha clase
base (pruebe a desactivar con comentarios la creación del objeto CalDO demostración).
Si la clase base tiene a su vez otra clase base, esa segunda clase base se cargaría, y así sucesivamente. A continuación, se
realiza la inicialización static en la clase base raíz (en este caso, loseet), y luego en la siguiente clase derivada, etc. Esto es
importante, porque la inicialización static de la clase derivada puede depender de que el miembro de la clase base haya sido
inicializado adecuadamente.
En este punto, ya se habrán cargado todas las clases necesarias, por lo que se podrá crear el objeto. En primer lugar, se asig-
nan los valores predetenninados a todas las primitivas de este objeto y se configuran las referencias a objetos con el valor
null (esto sucede en una única pasada, escribiendo ceros binarios en la memoria del objeto). A continuación, se invoca el
constructor de la clase base. En este caso la llamada es automática, pero también podemos especificar la llamada al cons-
tructor de la clase base (como primera operación dentro del conslmctor Beetle()) utilizando super. El constructor de la clase
base pasa a través del mismo proceso y en el mismo orden que el constructor de la clase derivada. Después de que se com-
plete el constructor de la clase base, las variables de instancia se inicializan en orden textual. Finalmente, se ejecuta el resto
del cuerpo del constructor.
Ejercicio 23: (2) Demuestre que el proceso de carga de una clase sólo tiene lugar una vez. Demuestre que la carga puede
ser provocada por la creación de la primera instancia de esa clase o por el acceso a un miembro estático
de la misma.
Ejercicio 24: (2) En Beelle.java, defina una nueva clase que represente un tipo específico de la clase Beetle de la que de-
be heredar, siguiendo el mismo fonnato que las clases ex istentes. Trace y explique los resultados de salida.
Resumen
Tanto la herencia como la composición pemliten crear nuevos tipos a partir de los tipos existentes. La composición reuti-
liza los tipos existentes como parte de la implementación subyacente del nuevo tipo, mientras que la herencia reutiliza la
interfaz.
164 Piensa en Java
Con la herencia, la clase derivada tiene la interfaz de la clase base. así que puede ser generalizada hacia la base, lo cual
resulta crítico para el polimorfismo, como veremos en el siguiente capítulo.
A pesar del gran énfasis que se pone en las cuestiones de herencia cuando hablamos de programación orientada a objetos,
al comenzar un diseilo suele ser preferible la composición (o preferiblemente la delegación) en una primera aproximación,
y utilizar la herencia sólo cuando sea claramente necesario. La composición tiende a ser más flexible. Además. utilizando
la herencia como artificio ailadido a un tipo que se haya incluido como miembro de una clase, se puede modificar el tipo
exacto (y por tanto el comportamiento) de esos objetos miembro en tiempo de ejecución. De este modo, se puede modifi-
car el comportamiento del objeto compuesto en tiempo de ejecución.
A la hora de diseñar un sistema, nuestro objetivo consiste en localizar o crear un conjunto de clases en el que cada clase
tenga un uso específico y no sea ni demasiado grande (que abarque tanta funcionalidad que sea dificil de reutilizar) ni incó-
modamente pequeña (de modo que no se pueda emplear por sí misma o si n añadirla funcionalidad). Cuando los diseños
comienzan a complicarse demasiado, suele resultar útil añadir más objetos descomponiendo los existentes en otros más
pequeños.
Cuando se proponga diseilar un sistema, es importante tener en cuenta que el desarrollo de los programas es un proceso
incremental, al igual que el proceso de aprendizaje humano. El proceso de diseilo necesita de la experimentación; podemos
hacer las tareas de análisis que qu eramos pero es imposible que lleguemos a conocer todas las respuestas en el momento de
arrancar el proyecto. Tendrá mucho más éxito (y podrá probar las cosas antes) si comienza a "hacer crecer" el proyecto como
una criatura orgánica en constante evolución, en lugar de construir todo de una vez, como si fuera un rascacielos de cristal.
La herencia y la composición son dos de las herramientas más fundamentales en la programación orientada a objetos a la
hora de realizar tales experimentos en el curso de un proyecto.
Puede encotHrar las solucioncs a los ejercic ios seleccionados en el documento eleclrónico The Thillking in Jm'a Am/Ofafed So/mio" Gllide. disponible para
la venta en \\'lI'\\'.A findViell'. l1ef.
Polimorfismo
"En ocasiones me he preguntado, ' Sr. Babbage, si introduce en la máquina datos erróneos,
¿podrá suministrar las respuestas correctas? Debo confesar que no soy capaz de comprender
qué tipo de confusión de ideas puede hacer que alguien plantee semejante pregunta".
Charles Babbage ( 1791-187 1)
El polimorfismo es la tercera de las características ese nciales de un lenguaj e de programación ori en tado a objetos, después
de la abstracción de datos y de la herencia.
Proporcio na otra dimensión de separación entre la interraz y la implementación, con el fin de desacoplar el qué co n respec-
to al cómo. El polimorfismo pennite mejorar la organización y la legibilidad de código, así como crea r programas amplia-
bles que puedan "hacerse crecer" no sólo durant e el proceso ori ginal de desa rrollo del proyecto, si no también cada vez que
se desean adherir nu evas características.
Elmccanismo de encapsulación crea nuevos tipos de datos combinando di versas característi cas y comportami entos. La téc-
nica de la ocu ltación de la implementación separa la interfaz de la implementación haciendo que los detalles sea n de tipo
privado. Este tipo de organización mecánica tiene bastante sent ido para las personas que tengan experiencia con la progra-
mación procedimental. Pero el polimorfismo trata la cuesti ón del acoplamiento en ténninos de tipos. En el último capítulo,
hemos visto que la herencia pennite tratar un objeto como si fuera de su propio tipo o como si fuera del tipo base. Esta capa-
cidad resulta crítica, porque pem1ite tratar varios tipos (todos ellos derivados del mismo tipo base) como si fueran un úruco
tipo, pudiéndose utili zar un mismo fragmento de código para procesar de la misma manera todos esos tipos dife-
rentes. La llamada a un método polimórfico pennite que cada tipo exprese su di stinción con respecto a los otros tipos simi-
lares, siempre y cuando ambos deriven del mismo tipo base. Esta distinción se expresa mediante diferencias en el co mpor-
tamiento de los métodos que se pueden invoca r a través de la clase base.
En este capítulo, vamos a estudiar el tema del polimorfismo (también denominado acoplamiento dinámico o acoplamiento
tardío o acoplamiento en tiempo de ejecución) comenzando por los conceptos más básicos y proporcionando ejemp los sim-
ples en los que nos fijáremos tan sólo en el compo rtamie nto polimórfico de los programas.
package polymorphism.music;
class Instrument {
public void play(Note n) {
print (" Instrument. play () ") ;
}
111,-
//: polymorphism/music/Wind.java
package polymorphism . music;
/ * Output:
Wind.playl) MIDDLE_C
*111,-
El método M usic.tun e() acepta una referencia a Instrum ent , pero también a cualquier cosa que se deri ve de Inst r ument.
Podemos ver que esto sucede en main( ), donde se pasa una referencia ' Vind a tun e(), sin que sea necesario efectuar nin-
guna proyección. Esto resulta perfectamente lógico: la interfaz de Instrument debe existir en Wind, porque Wind hereda
de Instrum ent. La generalización de Wind a Instrum ent puede "estrechar" dicha interfaz. pero en ningún caso esa inter-
faz podrá llegar a ser más pequeña que la interfaz completa de Instrum ent.
nu evo método tunee ) para cada clase deri vada de Instrument que incluyéramos en nuestro sistema. Suponga que siguié-
ramos esta forma de razonar y añadi éramos dos instrumentos Stringed (instmmentos de cuerda) y Brass (instrumentos de
metal):
// : polymorphism/music/Music2.java
/1 Sobrecarga en lugar de generalización.
package polymorphism.music;
import static net.mindview.util.Print.*;
/ * Output:
Wind.play() MIDDLE_e
Stringed .play () MIDDLE_C
Brass .play () MIDDLE_C
* /// ,-
Esta solución funciona, pero presenta una desvemaja importante: es necesario escribir métodos específicos del tipo para
cada nueva clase derivada de Instrument que añadamos. Esto significa, en primer lugar, un mayor esfuerzo de programa-
ción, pero también quiere decir que si queremos ailadir un nuevo método como tunee ) o un nuevo tipo de clase derivada
de Instrument, el trabajo adicional necesario es considerable. Si a esto le añadimos el hecho de que el compilador no nos
dará ningún mensaje de error si nos olvidamos de sobrecargar alguno de los métodos, todo el proceso de gestión de los tipos
se vue lve itmlanejable.
¿No sería mucho más fácil , si pudiéramos. limitamos a escribir un único método que tomara la clase base como argumen-
to y no ninguna de las clases derivadas específicas? En otras palabras: ¿no sería mucho más adecuado si pudiéramos olvi-
damos de que hay clases derivadas y escribir el código de manera que sólo se entendiera con la clase base?
Eso es exactamente 10 que el polimorfi smo nos pemlite hacer. Sin embargo, la mayoría de los programadores que proceden
del campo de los lenguajes de programación procedimentales suelen tener problemas a la hora de entender cómo funciona
el polimorfismo.
168 Piensa en Java
Ejercicio 1: (2) Cree una clase Cyele, con subclases Unicyele, Bicycle y Tri cyele. Demuestre que se puede generali-
zar una instancia de cada tipo a Cycle mediante un método rid e( ).
El secreto
La dificultad C011 Music.java puede verse ejecutando el programa. La salida es Wi nd.play(). Se trata claramente de la sali-
da deseada, pero no parece tener se ntido que el programa funcione de esa fonna. Examinemos el método tun e( ):
public static void tune (Instrument i) {
/ / ...
i.play(Note.MIDDLE_CI;
El método recibe una referencia a Instrum ent. De modo que ¿cómo puede el compilador saber que esta referencia a
Instr ument apunta a un objeto Wind en este caso y no a un objeto Br ass o Stringcd? El compi lador no puede saberlo. Para
comprender mejor esta cuestión, resulta útil que examinemos el tema del acoplamiento.
El ejemplo de las fonnas tiene una clase base denominada Shapc (fonna) y varios tipos derivados: Cirele (círculo), Squar e
(cuadrado). Tri a ngle (triángulo), etc. La razón por la que este ejemplo es tan adecuado es porque es fácil decir "un círculo
es un tipo de fom1a" y que el lector lo entienda. El diagrama de herencia muestra las relaciones:
Generaliza ción
en el diagrama
+t Shape
de heren cia t
drawO
._ ._ . ...J
eraseO
I I
t
Circle Square Triangle
Referencia drawO drawO drawO
a Circle eraseO eraseO eraseO
La generalización puede tener lugar en una instmcción tan simple como la siguiente:
Shape s = new Circle();
Aquí, se crea un objeto Circle, y la referencia resultante se asigna inmediatamente a un objeto Shape, (lo que podría pare-
cer un error asignar un ti po a otro); sin embargo, es perfectamente correcto, porque un objeto Circle es una forma (Shape)
debido a la herencia. Por tanto, el compilador aceptará la ins trucción y no generará ningún mensaje de error.
Suponga que invoca uno de los métodos de la clase base (que han sido sustituidos en las clases derivadas):
s.draw() ;
De nuevo. cabría esperar que se invocara el método draw() de Shape porque, después de todo, esto es una referencia a
Shape, asi que ¿cómo podria el compilador hacer cualquier otra cosa? Sin embargo, se invoca el mé todo apropiado
Circle.dr aw( ) debido al acop lamiento tardio (polimorfismo).
El siguiente ejemplo presenta las formas de una manera ligeramente distinta. En primer lugar, vamos a crear una biblioteca
reutilizable de tipos Shape:
//: polymorphism/shape/Shape.java
package polymorphism.shape¡
1/: polymorphism/shapelCircle.java
package polymorphism.shape¡
impore static net.mindview.util.Print.*¡
///, -
/1: polymorphism/shape/Square.java
package polymorphism.shape¡
import static net.mindview.util.Print.*;
JI: pOlymorphism/shape/Triangle.java
package polymorphism.shape¡
import static net.mindview.util.Print.*¡
/1: pOlymorphism/shape/RandomShapeGenerator.java
l/Una fábrica" que genera formas aleatoriamente.
11
package polymorphism.shape¡
import java.util.*;
1/: polymorphism/Shapes.java
II Polimorfismo en Java.
import polymorphism.shape.*¡
1* Output:
Triangle. draw ()
Triangle. draw ()
Square. draw ()
Triangle. draw ()
Square. draw ()
Triangle. draw ()
Square. draw ()
Triangle. draw {}
Circle. draw ()
* ///>
La clase base Shape establece la interfaz común para cualquier otra clase que herede de Shape; en ténninos conceptuales.
representa a todas las formas que puedan dibujarse y borrarse, Cada clase deri vada sust itu ye estas definiciones con el fin de
proporcionar un comportamiento distintivo para cada tipo específico de forma.
8 Polimorfismo 171
Random~napet,;ellerator es una espec ie de "fábrica" que genera una referencia a un objeto Shape aleatoriamente se lec-
cionado cada vez que se invoca su método next( ). Observe que el upcasting se produce en las instrucciones return, cada
una de las cuales toma una referencia a Ci rcle, Square o Triangle y la devu elve desde next() con el tipo de retorno, Shape.
Por tanto, cada vez que se invoca next(). nunca tenemos la oprotunidad de ve r de qué tipo específico se trata. ya que siem-
pre obtenemos una referencia genérica a Shape.
main() contiene una matri z de referencias Shape que se rellena mediante llamadas a RandomShapeGenerato r.next(). En
este punto, sabemos que tenemos objetos Shape, pero no podemos ser más específicos (ni tampoco puede serlo el compi-
lador). Sin embargo, cuando recorremos esta matriz e invocamos draw() para cada objeto, tiene lugar el comportamiento
correspondiente a cada tipo especí flco, como por arte de magia, tal y como puede ver si analiza la salida que se obtiene al
ejecutar el programa.
La razón de crear las fonnas aleatoriamente es que así puede perc ibirse mejor que el compilador no puede tener ningún
conocimiento especial que le pennite hacer las llamada s correctas en tiempo de compilación. Todas las J1amadas a draw()
deben tener lugar mediante el mecanismo de acop lamiento dinámico.
Ejercicio 2: (1) Añada la anotación @Overr ideal ejemplo de procesamiento de fonnas.
Ejercicio 3: (1) Añada un nuevo método a la clase base de S hapes.java que imprima un mensaje, pero sin sustituirlo
en las clases derivadas. Explique lo que sucede. Ahora, sustitúyalo en una de las clases derivadas pero no
en las otras y vea lo que sucede. Finalmente, sustitúya lo en todas las clases deri vadas.
Ejercicio 4: (2) Añada un nuevo tipo de objeto Shape a Shapes.j ava y verifique en main() que el polimorfismo fun-
ciona para el nuevo tipo al igual que para los tipos anteriores.
Ejercicio 5: ( 1) Paniendo del Ejercicio 1, añada un método wheels() a Cycle, que devue lva el número de ruedas.
Modifique ride() para invocar wheels() y verifique que func iona el polimorfismo.
Ampliabilidad
Vol va mos ahora al ejemplo de los instrumentos musicales. Debido al polimorfi smo, podemos añadi r al sistema todos los
nuevos tipos que deseemos sin modificar el método tun e() . En un programa orientado a objetos bien diseñado, la mayoría
de los métodos (o todos ellos) seguirán el método de t u ne() y sólo se comunicarán con la interfaz de la clase base. Ese lipo
Instrument
void playO
String whatO
void adjustO
I
Wind Percussion Stringed
Woodwind Brass
de programas es extensible (ampliable) porque puede añadir nueva funcionalidad heredando nuevos tipos de datos a par~
tir de la clase base común . Los métodos que manipulan la interfaz de la clase base no necesitarán ser modificados para poder
utili zar las nuevas clases.
Considere lo que sucede si tomamos el ejemplo de los illstnullentos y añadimos más métodos a la clase base y una serie de
clases nuevas. Puede ve r el diagra ma correspondiente al final de la página anterior.
Todas estas nuevas clases funcionan correctamente con el método amiguo tune(), sin necesidad de modificarlo. Incluso si
tune() se encontrara en un archivo separado y añadiéramos nuevos métodos a la interfaz de Instrument. tune() seguiría
funcionando correctamente. si n necesidad de recompilarlo. He aquí la implementación del diagra ma:
// : polymorphism/music3/Music3.java
// Un programa ampliable.
package polymorphism.music3¡
impor t polymorphism.music . Note¡
import static net . mindview.util.Print. * ¡
class Instrument {
void play(Note n) { print("Instrument.play{) " + n); }
String what () { return It Instrument lO ¡ }
void adj ust () { print ("Adj usting Instrument") ¡
tune(i) i
new Percussion () I
new Stringed () I
new Brass () ,
new Woodwind ()
);
tuneAll(orchestral;
/ * Output:
wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
woodwind.play() MIDDLE_C
* /// ,-
Los nuevos métodos son what( ), que devuelve una referencia Strin g con una descripción de la clase y adjust(), que pro-
porciona a lguna fanna de ajustar cada instmmento.
En ma in( ), cuando insertamos algo dentro de la matriz orc hestra, se produce automáticamente una generalización a
Jnstrument.
Podemos ver que e l método t une( ) es completamente ignorante de todos los cambios de código que han tenido lugar alre-
dedor suyo, a pesa r de 10 cual sigue fu ncionando perfectamente. Ésta es, exactam ente, la funcionalidad que se supone que
el polimorfismo debe proporcionar. Los cambios en el código no generan ningún problema en aquellas partes del programa
que no deban verse afectadas. Dicho de otra fon11a , el polimorfismo es una técnica importante con la que el programador
puede "separar" las cosas que cambian de las cosas que pennanecen.
Ejercicio 6: ( 1) Modifique Music3.java de modo qu e what() se convierta en el método toString() del objeto raíz
Object. Pruebe a imprimir los objetos (nstr ument utilizando System.out.pr intln() (sin efectuar ningu-
na proyección de tipo).
Ejercicio 7: (2) Ailada un nuevo tipo de objeto lnstrument a Music3.java y verifique que e l polimorfismo funciona
para el nuevo tipo.
Ejercicio 8 : (2) Modifique Music3.java para que genere aleatoriamente objetos [ostrument de la misma forma que 10
bace Shapes.java.
Ejercicio 9: (3) Cree una jerarqu íaa de herencia Rodent: Mouse, Gerbil, Hamster, etc (roedor: ratón, jerbe, hamster,
etc.). En la clase base proporcione los métodos que son comunes para todos los roedores, y sustituya estos
métodos en las clases derivadas para obtener diferentes comportami entos dependiendo del tipo específi co
de roedor. Cree una matri z de objetos Rodeot , rellénela con diferentes tipos específicos de roedores e
invoque los métodos de la clase base para ver lo que sucede.
Ejercicio 10: (3) Cree una clase base con dos métodos. En el primer método, in voque el segundo método. Defina una
c lase que herede de la anterior y sustituya el segundo método. Cree un objeto de la clase derivada, reali-
ce una generalización (lIpcasling) al tipo base y llame al primer método. Explique 10 que sucede.
class Super {
public int field = O¡
public int getField O { return field; }
/ * Output:
sup. field 0, sup getField{) 1
sub. field = 1, sub getField{) 1, sub.getSuperField {) = O
,///:-
Cuando un objeto Sub se generaliza a una referencia Super, los accesos a los campos son resueltos por el compi lador, por
lo que no son polimórficos. En este ejemplo, hay asignado un espacio de almacenamiento distinto para Super.field y
Sub.lield. Por tanto, Sub contiene rea lmente dos campos denominados lield : el suyo propio y el que obti ene a partir de
Super. Sin embargo, cuando se hace referencia al campo field de Super no se genera de fanna predetenninada una refe-
rencia a la versión almacenada en Super; para poder acceder al campo field de Super es necesario escribir explícitamente
super.lield.
Aunque esto último pueda parecer algo confuso. en la práctica no llega a plamearse casi nunca, por una razón: por regla
general, se definen todos los campos como priva te, por lo que no se accede a ellos directamente, sino sólo como efecto
secundario de la invocación a métodos. Además, probablemente nunca le demos el mi smo nombre de la clase base a un
campo de la clase derivada, ya que eso resultaría muy confuso.
Si un método es de tipo stane, no se comporta de fonna polimórfica:
JJ : polymorphismJStaticPolymorphism.java
11 Los métodos estáticos no son polimórficos.
class StaticSuper {
public static String staticGet()
return "Base staticGet () u i
J* Output:
Base staticGet()
Derived dynamicGet()
, /// :-
Los métodos estáticos están asociados con la clase y no con los objetos individuales.
Constructores y polimorfismo
Corno suele suceder, los constructores difieren de los otros tipos de métodos, también en lo que respecta al polimorfismo.
Aunque los constructores no son polirnórficos (se trata realmente de métodos estáticos, pero la declaración static es implí-
cita), tiene gran importancia comprender cuál es la fonna en que funcionan los constructores dentro de las jerarquías com-
plejas y en presencia de polimorfismo. Esta compresión de los fundamentos nos ayudará a evitar errores desagradables.
176 Piensa en Java
class Meal
Meal {l print ( "Meal () " ) ¡
class Bread
Bread () { print ( liBread () 11 ) j
class Cheese
Cheese () { print ( IICheese () " ) ¡
class Le t tuce
Lettuce {) { print {"Lettuce {) " ) j
1* Output:
8 Polimorfismo 177
Meal (1
Lunch!)
portabl eLunch ()
Bread (1
Cheese ()
Lettuce ()
Sandwich ()
. /// ,-
Este ejemplo crea una clase compleja a partir de otras clases y cada una de estas clases dispone de un constructor que se
anuncia a sí mismo. La clase importante es Sandwich, que refleja tres niveles de herencia (cuatro si contamos la herencia
implícita a partir de Object) y tres objetos miembro. Podemos ver en main() la salida cuando se crea un objeto Sandwich.
Esto quiere decir que el orden de llamada a los constnlctores para un objeto complejo es el siguiente:
1. Se invoca al constructor de la clase base. Este paso se repite de fanna recursiva de modo que la raíz de la jerar-
quía se constmye en primer lugar, seguida de la siguiente clase derivada, etc., hasta alcanzar la clase si tuada en
el nivel más profundo de la jerarquía.
2. Los inicial izadores de los miembros se invocan según el orden de declaración.
3. Se invoca el cuerpo del constmctor de la clase derivada.
El orden de las llamadas a los constructores es importante. Cuando utilizamos los mecanismos de herencia, sabemos todo
acerca de la clase base y podemos acceder a los miembros de tipo public y protected de la misma. Esto quiere decir que
debemos poder asumir que todos los demás miembros de la clase base son vá lidos cuando los encontremos en la clase deri-
vada. En UD método nonnal, el proceso de construcción ya ha tenido lugar, de modo que tod os los miembros de todas las
partes del objeto habrán sido construidos. Sin embargo, dentro del constructor debemos poder estar seguros de que todos los
miembros que utilicemos hayan sido construidos. La única fonna de ga rantizar esto es invocando primero al constmctor de
la clase base. Entonces, cuando nos encontremos dentro del constructor de la clase derivada, todos los miembros de la clase
base a los que queremos acceder ya habrán sido inicializados. Saber que todos los miembros son válidos dentro del cons-
tructor es también la razón de que, siempre que sea posible, se deban inicializar todos los objetos miembro (los objetos
incluidos en la clase mediante los mecanismos de composición) en su punto de definición dentro de la clase (por ejemplo.
b, e y I en el ejemplo anterior). Si se ajusta a esta práctica a la hora de programar, le será más fácil garantizar que todos los
miembros de la clase base y objetos miembro del objeto actual hayan sido inicializados. Lamentablemente, este sistema no
nos permite gestionar todos los casos, como veremos en la siguiente sección.
Ejercicio 11: (1) Añada una clase Pickle a Sandwich.j ava.
Herencia y limpieza
Cuando se utili zan los mecanismos de composición y de herencia para crear una nueva clase, la mayor parte de las veces
no tenemos que preocupamos por las tareas de limpieza; los subobjetos pueden nonnalmente dejarse para que los procese
el depurador de memoria. Sin embargo, si hay algún problema relativo a la limpieza, es necesario actuar con diligencia y
crear un método dispose() (éste es el nombre que yo he seleccionado, pero usted puede utili zar cualquier otro que indique
que estamos deshaciéndonos del objeto) en la nueva clase. Y, con la herencia, es necesario sustituir dispose( ) en la clase
deri vada si necesitamos realizar alguna tarea de limpieza especial que tenga que tener lugar como parte de la depuración de
memoria. Cuando se sustituya dispose() en una clase heredada, es importante acordarse de invocar la versión de dispose()
de la clase base, ya que en caso contrario las tareas de limpieza propias de la clase base no se llevarán a cabo. El siguiente
ejemplo ilustra esta situación:
class Characteristic {
private String S;
Characteristic (String s ) {
this.s = S;
178 Piensa en Java
class Description {
private String Si
Description {String s} {
this.s = Si
print ("Creating Description " + s) i
p.dispose() ;
super.dispose() ;
/ * Output:
Creating Characteristic i5 alive
Creating Description Basie Living Creature
LivingC reature ()
Creating Characteristic has heart
Creating Description Animal nat Vegetable
Animal ()
Creating Characteristic can live in water
Creating Description 80th water and land
Amphibian ()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both water and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Crea tu re
disposing Characteristic is alive
*///,-
Cada clase de la jerarquía también contiene objetos miembro de los tipos Characteristic y Description, que también habrá
que borrar. El orden de borrado debe ser el inverso del orden de inicialización, por si acaso uno de los subobjetos depende
del otro. Para los campos, esto quiere decir el inverso del orden de declaración (puesto que los campos se inicializan en el
orden de declaración). Para las clases base (siguiendo la nonna utilizada en e++ para los destructores), debemos real izar
primero las tareas de limpieza de la clase derivada y luego las de la clase base, La razón es que esas tareas de limpieza de
la clase derivada tuvieran que invocar algunos métodos de la clase base que requieran que los componentes de la clase base
continúen siendo accesibles, así que no debemos destruir esos componentes prematuramente. Analizando la salida podemos
ver que se borran todas las partes del objeto Frog en orden inverso al de creación.
A partir de este ejemplo, podemos ver que aunque no siempre es necesario realizar tareas de limpieza, cuando se llevan a
cabo es preciso hacerlo con un gran cuidado y una gran atención.
180 Piensa en Java
Ejerc ic io 12: (3) Modifique el Ejercicio 9 para que se muestre el orden de iniciali zación de las clases base y de las cla-
ses deri vadas. Ahora aiiada objetos miembro a las clases base y deri vadas, y muestre el orden en que se
lleva a cabo la inicialización durante el proceso de construcción.
Observe también en el ejemplo anterior que un objeto Fro g "posee" sus objetos miembro: crea esos objetos miembro y sabe
durante cuálJto tiempo tienen que existir (tanto como dure el objeto F rog), de modo que sabe cuándo invocar el método
dispose() para borrar los objetos miembro. Sin embargo, si uno de estos objetos miembro es compartido con otros objetos,
el problema se vuelve más complejo y no podemos simplemente asumir que basta con invocar dispose(). En estos casos,
puede ser necesario un recuento de referencias para llevar la cuenta del número de obj etos que siguen pudiendo acceder a
un obj eto compartido. He aquí un ejemplo:
//: polymorphism/ReferenceCoun t i ng . java
// Limpieza de objetos miembro compartidos .
import static net.mindview . util.P r int. * ¡
class Shared {
private int refcount = O;
pr i vate static long counter = O;
private fi n a l l ong id = counter++;
public Shared () {
pr i nt ("Crea t ing " + this);
class Compos i ng {
private Shared shared;
private sta t ic long counter = O;
private final long id = coun ter++;
public Composing (Shared shared) {
print ("Creating " + this);
this.shared = shared¡
this . shared . addRef() ;
/ * Output :
Creating Shared O
Creating Comp osing O
Creating Composing 1
8 Polimorfismo 181
creating Composing 2
creating Composing 3
creating Composing 4
disposing Composing O
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared O
* /// ,-
El contador statie long counter lleva la cuenta del número de instancias de Shared que son creadas y también crea un valor
para id. El tipo de eounter es long en lugar de int, para evitar el desbordamiento (se trata sólo de una buena práctica de
programación: es bastante improbable que esos desbordamientos de contadores puedan producirse en ninguno de los ejem-
plos de eSle libro). La variable id es de tipo final porque no esperamos que cambie de valor durante el tiempo de vida del
objeto.
Cuando se asocia el objeto compartido a la clase, hay que acordarse de invocar addRef( ), pero el método dispose( ) lleva-
rá la cuenta del número de referencias y decidirá cuándo hay que proceder con las tareas de limpieza. Esta técnica requiere
un cierta diligencia por nuestra parte, pero si estamos compartiendo objetos que necesiten que se lleve a cabo una determi-
nada tarea de limpieza, no son muchas las opciones que tenemos.
Eje rcicio 13: (3) Añada un método finalize( ) a RefcrcnccCounting.java para verificar la condición de rerminación
(véase el Capítu lo S,inicialización y limpieza).
Ejercici o 14: (4) Modifique el Ejercicio 12 para que uno de los objetos miembro sea un objeto compartido. Utilice el
método de recuento del número de referencias y demuestre que funciona adecuadamente.
class Glyph {
void draw() { print(nGlyph.draw() "); }
Glyph () {
182 Piensa en Java
void draw () {
print ( "RoundGlyph. draw () I radius 11 + radius ) ;
/ * Output:
Glyph () before draw ()
RoundGlyph . draw(), radius = o
Glyph () after draw ()
RoundGlyph. RoundGlyph () ,radius 5
* ///,-
Glyph.draw() está diseñado para ser sustituido, lo que se produce en RoundGlyph. Pero el constructor de Glyph invoca
este método y la llamada tennina en RoundGlyph.draw(), que parece que fuera la intención originaL Pero si examinamos
la sal ida, podemos ver que cuando el constructor de Glyph invoca draw(), el valor de radius no es ni siquiera el valor ini-
cial predetenninado de 1, sino que es O. Esto provocará, probablemente, que se dibuje en la pantalla un punto, o nada en
absoluto, con lo que el programador se quedará contemplándolo tratando de imaginar por qué no funciona el programa.
El orden de inicialización descrito en la sección anterior no está completo del todo, y ahí es donde radica la clave para resol-
ver el misterio. El proceso real de inicialización es:
1. El almacenamiento asignado al objeto se inicializa con ceros binarios antes de que suceda ninguna otra cosa.
2. Los constructores de las clases base se invocan tal y como hemos descrito anteriormente. En este punto se invo-
ca el método sustituido draw( ) (sí, se invoca antes de que llame al constructor de RoundGlyph ) y éste descu-
bre que el valor de radius es cero, debido al Paso 1.
3. Los inicializadores de los miembros se invocan según el orden de declaración.
4. Se invoca el cuerpo del constructor de la clase derivada.
La parte buena de todo esto es que todo se inicializa al menos con cero (o con lo que cero signifique para ese tipo de datos
concreto) y no simplemente con datos aleatorios. Esto incluye las referencias a objetos que han sido incluidas en una clase
a través del mecanismo de composición, que tendrán el valor null. Por tanto, si nos olvidamos de inicializar esa referencia,
se generará una excepción en tiempo de ejecución. Todo lo demás tomará el valor cero, lo que usualmente nos sirve como
pista a la hora de examinar la salida.
Por otro lado, es posible que el programador se quede horrorizado al ver la salida de este programa: hemos hecho algo per-
fectamente lógico, a pesar de lo cual el comportamiento es misteriosamente erróneo, sin que el compilador se haya queja-
do (e++ produce un comportamiento más racional en esta situación). Los errores de este tipo podrían quedar ocultos
fácilmente, necesitándose una gran cantidad de tiempo para descubrirlos.
Como resultado, una buena directriz a la hora de implementar los constructores es: "Haz lo menos posible para garantizar
que el objeto se encuentre en un estado correcto y, siempre que puedas evitarlo, no invoques ningún otro método de esta
clase". Los únicos métodos seguros que se pueden invocar dentro de un constructor son aquellos de tipo final en la clase
8 Polimorfismo 183
base (esto también se aplica a los métodos privados, que son automáticamente de tipo final ). Estos métodos no pueden ser
sustituidos Y no pueden, por tanto, darnos este tipo de so rpresas. Puede que no siempre seamos capaces de seguir esta direc-
triz, pero al menos debemos tratar de cumplirla.
Ejercicio 15: (2) Añada una clase Recta ngularGlyph a PolyCoostructors.java e ilustre el problema descrito en esta
sección.
class Grai n {
public String toString () { rE"turn "Grain" i }
cIass MilI {
Grain p r ocess () { return new Grain () ¡ }
/ * Output:
Grain
Wheat
*// /0-
La diferencia clave entre Java SE5 y las versiones anteriores es que en éstas se obligaría a que la versión sustituida de
process( ) devolviera Grain, en lugar de Wheat, a pesar de que Wheat deriva de Graio y sigue siendo, por tanto. un tipo
de retomo legítimo. Los tipos de retomo covariantes penniten utilizar el tipo de retorno Wheat más específico.
tamiento), mientras que la herencia exige que se conozca un tipo exacto en tiempo de compilación. El siguien te ejemplo
ilustra es to:
jI : polymorphism/Transmogrify.java
/1 Modificación dinámica del comportamiento de un objeto
/1 mediante la composición (el patrón de diseño basado en estados).
import static net.mindview.util.Print.*¡
class Actor {
public void act () {}
class Stage {
private Ac t or actor = new HappyActor {);
public void change () { actor = new SadActor () ;
public void performPlay () { actor. act () ; }
/ * Output :
HappyActor
SadActor
' /1/ , -
Un objeto Stage contiene una referencia a un objeto Actor, que se inicializa para que apunte a un objeto HappyActor. Esto
significa que performPlay() produce un comportamiento concreto. Pero, como una referencia puede redirigirse a un obje-
to distinto en tiempo de ejecución, podríamos almacenar una referencia a un objeto SadActor en actor, y entonces el com-
portamiento producido por performPlay() variaría. Por tanto, obtenemos una mayo r flexibilidad dinámica en liempo de
ejecución (esto se denomina también patrón de diseño basado en estados, consulte Thinking in Patterns (with Java) en
www.MindView.net) . Por contraste, no podemos decidir realizar la herencia de fonna diferente en tiempo de ejecución, el
mecanismo de herencia debe estar perfectamente determinado en tiempo de compilación.
Una regla general sería: "Utilice la herencia para expresar las diferencias en comportamiento y los campos para expresar las
variaciones en el estado". En el ejemplo anterior se utilizan ambos mecanjsmos; definimos mediante herencia dos clases dis-
tintas para expresar la diferencia en el método act() y Stage utiliza la compos ición para permitir que su estado sea mod ifi-
cado. Dicho cambio de estado, en este caso, produce un cambio de comportamiento.
Ejercicio 16: (3) Siguiendo eJ ejempJo de Transmogrify.java, cree una cJase Starship que contenga una referencia
AJertStatus que pueda indicar tres estados distintos. Incluya métodos para verificar los estados.
Sustitución y extensión
Podría parecer que la forma más limpia de crear una jerarquía de herencia sería adoptar un enfoque "puro"; es decir, sólo
los métodos que hayan sido establecidos en la clase base serán sustituidos en la clase derivada, como puede verse en este
diagrama:
8 Polimorfismo 185
Shape
draw()
erase()
I I
Circle Square Triangle
Esto podría decirse que es una relación de tipo "es-un" porque la interfa z de una clase establece lo que dicha clase es. La
herenc ia garant iza que cualqu ier clase derivada tendrá la interfaz de la c lase base y nada más. Si seguimos este diagrama,
las c lases derivadas no tendrán nada más que lo que la interfaz de la clase base ofrezca.
Esto podría considerarse como una sustitución pura, porque podemos sustituir perfectamente un objeto de la clase base o
un objeto de una clase deri vada y no nos hace falta conocer ninguna infomláción adicional acerca de las subcla ses a la hora
de utilizarlas:
Habla con Shape --------------------,.. Circle, Square, Une o un nuevo tipo de Shape
Mensaje
Relación ~es·u n "
En otras palabras, la clase base puede rec ibir cualquier men saje que enviemos a la clase deri vada, porque las dos tienen
exactamente la mism a interfa z. Debido a esto lo que tenemos que hacer es generalizar a partir de la clase derivada, sin
tener que preocuparnos de ver cuál es e l tipo exacto del objeto con el que estemos tratando. Todo se maneja medi ante el
pol imo rfi smo.
Cuando vemos las cosas de esta fom18, debe parece r que las relac iones puras de tipo "es-un" so n la fonna más lógica de
implementar las cosas, y que cualquier otro tipo de diseño resulta confuso por comparación. Pero esta forma de pensar es
un error. Tan pronto comencemos a pensar de esta foona, miraremos a nuestro alrededor y descubriremos que ampliar la
interfaz (mediante la palabra clave extends) es la perfecta so lución para un problema concreto. Este tipo de so lución podría
denominarse relación de tipo "es-coma-un", porque la clase deri vada es como la clase base: tiene la misma interfaz ele men-
tal y tiene, además, ot ras características que req ui eren método s adicionales para implementarlas:
Uselul
Suponga qu e esto
voi d I()
representa una
void g()
interfaz compleja
MoreUselul "Es-coma-un"
void I()
void g()
void u()
void vO Ampliación
void w() de la interfaz
186 Piensa en Java
Aunque este enfoque también resulta útil y lógico (dependiendo de la situación) tiene una desventaja. La parte ampliada de
la interfaz en la clase derivada no está disponible en la clase base, por lo que, una vez que efectuemos una generalización
no podremos invocar los nuevos métodos:
Si no estamos haci endo generalizaciones, no debe haber ningún problema, pero a menudo nos encontraremos en situacio-
nes en las que necesitamos descubrir el tipo exacto del objeto para poder acceder a los métodos ampliados de dicho tipo. En
la siguiente sección se explica cómo hacer esto.
cIass UsefuI {
public void f 1) {}
public void g 1) {}
x[O].f();
X[l].g () ;
/ 1 Tiempo de compilación: método no encontrado en Useful :
II ! x[l].u () ;
(( MoreUseful)x[l] ) . u () i // Especialización / RTTI
(( MoreUseful ) x[O J) .u () ; 1/ Excepción generada
}
111 ,-
Como en el diagrama anterior, MoreUseful amplía la interfaz de Usefu). Pero. como se tTata de una clase heredada tam-
bién puede generalizarse a Useful. Podemos ve r esta generalización en acción durante la inicialización de la matriz x en
m.iu(). Puesto que ambos objetos de la matriz son de clase Useful, podemos enviar los métodos f() Y g() a ambos. mien-
tras que si tratamos de invocar u() (que sólo existe en MoreUseful), ob tendremos un mensaje de error en tiempo de com-
pilación .
Si queremos acceder a la interfaz ampliada de un objeto MoreUseful, podemos tratar de efectuar una especialización. Si se
trata del tipo correcto. la operación tendrá éxito. En caso contrario, obtendremos una excepción ClassCastException. No
es necesario escribir ningtin código especial para esta excepción. ya que indica un error del programador que puede produ-
cirse en cualquier lugar del programa. La etiqueta de comentario {ThrowsExcept-ion} le dice al sistema de construcción de
los ejemplos de este libro que cabe esperar que este programa genere una excepción al ejecutarse.
El mecanismo RTTI es más comp lejo de 10 que este ejemplo de proyección simple pennite intuir. Por ejemplo, existe una
fonna de ver cuál es el tipo con el que estamos tratando antes de efectuar la especialización. El Capítulo 14, Información
de (ipos está dedicado al estudio de los diferentes aspectos de la infoonación de tipos en tiempo de ejecución en Java.
Ejercicio 17: (2) Utilizando la jerarquía Cyele del Ejercicio 1, aliada un método balance() a Unicyele y Bicyele, pero
no a Tricycle. Cree instancias de los tres tipos y generalícelas para formar una matriz de objetos Cyclc.
Trate de invocar balance() en cada elemento de la matriz y observe los resultados. Rea lice una especia-
li zación e invoque balance( ) y observe lo que sucede.
Resumen
Polimorfismo significa "diferentes f0n11as". En la programación orientada a objetos, tenemos una misma interfaz definida
en la clase base y diferentes fonnas que utilizan dicha interfaz: las diferentes versiones de los métodos dinámicamente aco-
pIados.
Hemos visto en este capítulo que resulta imposib le comprender. o incluso crear, un ejemplo de polimorfismo sin utilizar la
abstracción de datos y la herencia. El polimorfismo es una característica que no puede anali zarse de manera aislada (a dife-
rencia, por ejemplo, del análisis de la instrucción switch), sino que funciona de manera concertada, como parte del esque-
ma global de relaciones de clases.
Para usar el polimorfismo, y por tanto las técnicas de orientación a objetos, de manera efectiva en los programas, es nece-
sario ampliar nuestra visión del concepto de programación, para incluir no sólo los miembros de una clase indi vidual, sino
tambi én los aspectos comunes de las distintas c lases y las relaciones que tienen entre sÍ. Aunque esto requiere un esfuerzo
significativo, se trata de un esfuerzo que merece la pena. Los resultados serán una mayor velocidad a la hora de desarrollar
programas, una mejor organizac ión del código y la posibilidad de di sponer de programas ampliables, y un mantenimiento
del código más eficiente.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico rhe rhillkillg in Jav(I Annotafed So/lIti01l Guide, disponible para
la venia en \IWII~ AfilldVhT\I ·. /¡e/.
Interfaces
Las interfaces y las clases abstractas proporcionan una fonna más estructurada de separar la
interfaz de la implementación.
Dichos mecanismos no son tan comunes en los lenguajes de programación. C++-, por ejemplo, sólo tiene soporte indirecto
para estos conceptos. El hecho de que existan palabras clave del lenguaje en Java para estos conceptos indica que esas ideas
fueron consideradas lo suficientemente importantes como para proporcionar un soporte directo.
En primer lugar, vamos a exa minar el concepto de clase abstracta, que es una clase de ténnino medio entre una clase nor-
mal y una interfaz. Aunque nuestro primer impulso pudiera ser crear una interfaz, la clase abstracta constituye una herra-
mienta importante y necesaria para construir clases que tengan algunos métodos no implementados. No siempre podemos
utili zar una interfaz pura.
I Para los programadores de C++, se trata del anftlogo a lasjimciones ¡'irrita/es puras de C++.
190 Piensa en Java
generará un mensaje de error. De es ta fonna. el compilador garal1liza la pureza de la clase abstracta y no es necesario preo·
cupa rse de si se la va a utilizar correctamente.
Si definimos un a clase heredada de una clase abstracta y queremos co nstruir objelos del nuevo tipo. deberemos proporcio·
nar defini ciones de métodos para todos los métodos abstractos de la clase base. Si no lo hacemos (y podemos decidir no
hacerlo). el1lonces la clase derivada será también abstracta. y el compilador nos obligará a calificar esa clase con la palab ra
clave abstrael.
Resulta posible definir una clase como abstracta sin incluir ningún método abstracto. Esto resulta útil cuando tenemos una
clase en la que no tiene sentido ten er ningún método abstracto y. sin embargo. queremos evitar que se generen instancias de
dicha clase.
La clase Instrument de l capítulo ant eri or puede lransfonnarse fácilmente en una clase abstracta. Sólo algunos de los méto·
dos serán abstra cTOs, ya que definir una clase como abstracta no obli ga a que todos los métodos sean abstractos. He aquí el
ejempl o modificado:
abstraet lnstrument
Woodwind Brass
He aquí el ejemplo de la orquesta modificado para uti lizar clases y métodos abstractos:
/1 : interfaces / musie4 / Musie4.java
1/ Clases y métodos abstractos.
package interfaees.musie4¡
import polymorphism.musie.Note¡
import statie net.mindview.util.Print.*;
} / * Output,
Wind.play () MIDDLE_C
Percussion.play () MIDDLE_C
Stringed.play () MIDDLE_C
Brass.play () MIDDLE_C
Woodwind.play () MIDDLE_C
* /// , -
Podemos ver que no se ha efecnlado ningún cambio, salvo en la clase base.
Resulta útil crear clases y métodos abstractos porque hacen que la abstracción de una clase sea explícita, e ¡nfannan tanto
al usuario como al compilador acerca de cómo se pretende que se utilice esa clase. Las clases abstractas también resultan
útiles como herramientas de rediseño, ya que permiten mover fácilmente los métodos comunes hacia arriba en la jerarquía
de herencia.
Ejercicio 1: (1) Modifique el Ejercicio 9 del capítulo anterior de modo que Rodent sea una clase abs tracta. Defina los
métodos de Rodent como abstractos siempre que sea posible.
Ejercicio 2: (1) Cree una clase abstracta sin incluir ningún método abstracto y verifique que no pueden crearse instan-
cias de esa clase.
Ejercicio 3: (2) Cree una clase base con un método print( ) abstracto que se sustituye en una clase deri vada. La ver-
sión sustituida del método debe imprimir el va lor de una variable int definida en la clase derivada. En el
punto de defin ición de esta vari able, proporcione un va lor di stinto de cero. En el constructor de la clase
base, llame a este método. En main( ), cree un objeto del tipo derivado y luego invoque su método
print( ). Explique los resultados.
Ejercicio 4: (3) Cree una clase abstracta sin métodos. Defllla una clase derivada y anádale un método. Cree un méto-
do estático que tome una referencia a la clase base, especialícelo para que apunte a la clase derivada e
invoque el método. En main( ), demuestre que este mecanismo funciona. Ahora, incluya la declaración
abstracta del método en la clase base, eliminando así la necesidad de la especialización.
Interfaces
La palabra clave interface lleva el concepto de abstracción un paso más allá. La palabra clave abstract pennite crear uno
o más métodos no definidos dentro de una clase: proporcionamos parte de la interfaz, pero sin proporcionar la implementa-
ción correspondiente. La implementación se proporciona de las clases que hereden de la clase actual. La palabra clave inter-
face produce una clase com pletamente abstracta, que no proporciona ninguna implementación en absoluto. Las interfaces
pemúten al creador determinar los nombres de los métodos, las listas de argumentos y los tipos de retomo, pero si n especi-
ficar ningún cuerpo de nin gull método. Una interfaz proporciona simplemente una fomla, sin ninguna implementación.
Lo que las interfaces hacen es decir: "Todas las clases que implementen esta interfaz concreta tendrán este aspecto". Por
tanto. cualquier código que utili ce una interfaz concreta sabrá qué métodos pueden invocarse para di cha interfaz yeso es
todo. Por tanto, la interfaz se utiliza para establecer un "protocolo" entre las clases (algunos lenguaj es de programación
orientados a objetos di sponen de una palabra clave denominada pr%col para hacer lo mi smo).
Sin embargo, una interfaz es algo más que simplemente una clase abstracta llevada hasta el extremo, ya que permite
rea li zar una varian te del mecanismo de " herencia múltiple" creando una clase que pueda generalizarse a más de un tipo
base.
Para crear una interfaz, utilice la palabra clave interface en lugar de class. Al igual que COIl una clase, puede añadir la pala-
bra clave public antes de interface (pero sólo si dicha interfaz está definida en un archivo del mi smo nombre). Si no inclui-
mos la palabra clave public, obtendremos un acceso de tipo paquete, porque la interfaz sólo será utilizable dentro del mismo
paquete. Una interfaz también puede contener campos, pero esos campos serán im plíci tamente de tipo static y final.
Para definir una clase que se adapte a una interfaz concreta (o a un gmpo de interfaces concretas), utilice la palabra clave
implements que quiere decir: " La interfaz especifica cuál es el aspecto, pero ahora vamos a decir cómo fimciono". Por lo
demás, la defini ción de la clase derivada se asemeja al mecanismo normal de herencia. El diagrama para el ejemplo de los
instnlmentos musicales sería el siguiente:
9 Interfaces 193
Instrument
void play();
String what();
void adjust();
Woodwind Brass
Podemos ver en las clases Woodwind y Brass que una vez que hemos implementado la interfaz, la implementación pasa a
ser una clase normal que puede ampliarse de la forma usual.
Podemos declarar explícitamente los métodos de una interfaz como public, pero esos métodos serán públicos aún cuando
no lo especifiquemos. Por tanto, cuando implementemos una interfaz, los métodos de esa interfaz deben estar definidos
como públicos. En caso contrario, se revertiría de f0n113 predeterminada al acceso de tipo paquete, con lo que estaríamos
reduciendo la accesibilidad de los métodos durante la herencia, cosa que el compilador de Java no permite.
Podemos ver esto en la versión modificada del ejemplo Instrumeot. Observe que todos los métodos de la interfaz son estric-
tam en te una declaración, que es lo único que el compilador pemite. Además, ninguno de los métodos de Instrument se
declara corno public, pero de lodos modos son públicos de manera automática:
11 : interfaces / musicS / MusicS . java
II Interfaces .
package interfaces.mus icS¡
i mport p olymorphism.music.Not e ;
i mport stati c net . mindview util . Print .*
1* Output:
Wind.play() MIDDLE_C
Percussion.play(} MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
* ///,-
En esta versión del ejemplo hemos hecho otro cambio: el método \Vhat() ha sido cambiado a toString(), dado que esa era
la fOnTIa en que se estaba utilizando el método. Puesto que toString() forma parte de la clase raíz Object, no necesita apa-
recer en la interfaz.
El resto del código funciona de la misma manera. Observe que no importa si estamos generalizando a una clase "nonnar'
denominada Instrument. a una clase abstracta llamada lnstrurncnt, o a una interfaz denominada Instrument. El compor-
9 Interfaces 195
tamiento es siempre el mismo. De hecho. podemos ver en el método lune( ) que no existe ninguna c\idencia acerca de si
Instrumcn t es una clase "nannal", una clase abstracta o una interfaz.
Ejercicio 5: (2) Cree una interfaz que contenga Ires métodos en su propio paquete. Implemente la interfaz en un paque-
te diferente.
Ejercicio 6: (2) Demuestre que lodos los métodos de una ¡merfaz son automáticamente públicos.
Ejercicio 7: ( 1) Mod ifique el Ejercicio 9 del Capítu lo 8. Polimorfismo, para que Roden! sea una interfaz.
Eje rcicio 8: (2) En polymorphism'sandwich.java. cree una interfaz denominada FastFood (con los métodos apro-
piados) y cambie Sandwich de modo que también implemente FastFood .
Eje rcicio 9: (3) Rediseñe Music5.jav3 moviendo los métodos comunes de \Vind . Percussion y Stringed a una clase
abstracta.
Eje rcicio 10: (3) Modifique MusicS.java añadiendo una interfaz Pla)'ab le. Mueva la declaración de playO de
lnstrument a Playable. Afiada Playable a las clases derivadas incluyéndola en la lista implements.
Modifique tun e() de modo que acepte un objeto Playable en lugar de un objeto Instrument.
Desacoplamiento completo
Cuando un método funciona con una clase en lu ga r de con una interfaz, estamos limitados a utili za r dicha clase o sus sub-
clases. Si quisiéramos ap li car ese método a una clase que no se encontrara en esa jerarquía, no podríamos. Las interfaces
relajan esta restri cción considerablemen te. Como resu ltado, permiten escribir código más reutil izab le.
Por ejemplo. supo nga que disponemos de una clase Processor que ti ene sendos métodos Dame () y process( ) que toman
una cierta entrada, la modifican y generan una salida. La clase base se puede amp li ar para crear diferentes tipos de objetos
Processo r . En este caso, los subtipos de Processor modifican objetos de tipo String (observe que los tipos de retomo pue-
den ser covariantes, pero no los tipos de argumentos):
11: interfaces/classprocessor/Apply.java
package interfaces.classprocessor¡
import java.util.*¡
import static net.mindview util.Print.*¡
class Processor {
public String name()
return getClass () . getSimpleName () ;
1* Output:
Using Processor Upcase
DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
Using Processor Downcase
disagreement witb beliefs ls by definition incorrect
Using Processor Splitter
[Disagreement, wich, beliefs, is, by, definition, incorrect]
* /// ,-
El método Apply.process() toma cualquier tipo de objeto Processor y lo aplica a un objeto Object, imprimiendo después
los resultados. La creación de un método que se comporte de fonna diferente dependiendo del objeto argumento que se le
pase es lo que se denomina el patrón de diseño basado en estrategias. El método contiene la parte fija del algoritmo que hay
que implementar, mientras que la estrategia cOOliene la parte que varia. La estrategia es el objeto que pasamos, y que con-
tiene el código que hay que ejecutar. Aquí, el objeto Processor es la estrategia y en main( ) podemos ver cómo se aplican
tres estrategias diferentes a la cadena de caracteres s.
El método split( ) es parte de la clase String; toma el objeto String y lo divide utilizando el argumento como frontera, y
devolviendo una matri z Stringll . Se utiUza aquí como forma abreviada de crear una matriz de objetos String.
Ahora suponga que descubrimos un conjunto de filtros electrónicos que pudieran encajar en nuestro método Apply.process( ):
/ / : interfaces / filters / Waveform.java
package interfaces.filters;
/1: interfacesjfilters/HighPass.java
package interfaces.filtersi
La primera fo nna en que podemos reutilizar el códi go es si los programadores de clientes pueden escribir sus clases para
que se adapten a la interfaz, como por ejemplo:
//: interfaces/interfaceproeessor/StringProcessor.java
package interfaces.interfaeeprocessor;
import java.util.*;
1* Output:
Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MACE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor Splitter
[If, she, weighs, the, same, as, a, duck, , she's, made, of, wood]
* //1 , -
Sin embargo, a menudo nos encontramos en una situación en la que no podemos modificar las clases que queremos usar.
En el caso de los filtros electrónicos. por ejemplo, la correspondiente biblioteca la hemos descubierto, en lugar de desarro-
llarla. En estos casos, podemos utilizar el patrón de diseiio adaprador. Con dicho patrón de diseño, lo que hacemos es escri-
bir código para lOmar la interfaz de la que disponemos y producir la que necesitamos, como por ejemplo:
//: interfaces/interfaceprocessor/FilterProcessor.java
package interfaces.interfaceprocessor;
import interfaces.fiIters.*¡
} / * Output,
Using Processor LowPass
Waveform o
Using Processor HighPass
Waveform o
Using Processor BandPass
Waveform O
* /// >
En esta aplicación, el patrón de di seño de adaptación, el constmctorFilterAdapter, toma la interfaz que tenemos (Filter)
y produce un objeto que tiene la interfaz Processor que necesitamos. Observe también la utilización del mecanismo de dele-
gación en la clase FilterAdapter.
Desacoplar la interfaz de la implementación pennite aplicar las interfaces a múltiples implementaciones diferentes, con lo
que el código es más reutilizable.
Ejercicio 11: (4) Cree una clase con un método que tome como argumento un objeto String y produzca un resultado en
el que se intercambie cada pareja de caracteres contenida en el argumento. Adapte la clase para que fun-
cione con interfaceprocessor.Apply.process().
I interfaz 2 1 ...
.......
I intet n I
interface CanFight
void fight () i
interface CanSwim
void swim ( ) i
interface CanFly {
200 Piensa en Java
voidfly(l;
class ActionCharacter {
public void fight 11 {}
.2 Este ejemplo muestra cómo las interfaces evitan el denominado ··problema del rambo", que se presenta en el mecanismo de herencia mú lti ple de C++.
9 Interfaces 201
/1 : interfaces/HorrorShow.java
JI Ampliación de una interfaz mediante herencia.
interface Monster
void menace () ;
interface Lechal
void kill () ;
La siI1laxis empleada en Vampire sólo funciona cuando se heredan interfaces. Nonna lmente, sólo podemos utilizar extends
con una única clase. pero extends puede hacer referencia a múltiples interfaces base a la hora de construir una nueva inter-
faz. Como puede ver, los nombres de interfaz es tá simplemente separados por comas.
Ejercicio 14: (2) Cree tres interfaces, cada una de ellas con dos métodos. Defina mediante herencia una nueva interfaz
que combine las tres, añadiendo un nuevo método. Cree una clase implerncnrando la nueva interfaz y que
también herede de una clase concreta. A continuación, escriba cuatro métodos, cada uno de los cuales tome
una de las cuatro interfaces C01110 argumento. En main(), cree un objeto de esa clase y páselo a cada uno
de los métodos.
Ejercicio 15: (2) Modifique el ejercicio anterior creando una clase abstracta y haciendo que la clase derivada herede de
ella.
La dificultad surge porque los mecanismos de anulación, de implementación y de sobrecarga se entremezclan de forma com-
pleja. Asimismo, los métodos sob recargados no pueden diferir sólo en cuanto al tipo de retorno. Si quitarnos la marca de
comentario de las dos últimas líneas, los mensajes de error nos informan del problema:
lnterfaceCollisionjava:23: f( ) in e CanllO( implemenl f( ) in 11 ; attempting lO use incompatible relurn type
foune!: inr
required: void
lntelfaceCollisionjava:24: Inlelfaces /3 ane! 11 are incompatible; bOlh dejinef( ), b1ll \Vith different relUrn
Iype
Asimismo, utilizar los mismos nombres de método en diferentes interfaces que vayan a ser combinadas suele aumentar,
generalmente, la confusión en lo que respecta a la legibilidad del código. Trate de evi tar la utilización de nombres de méto-
do idénticos.
9 Interfaces 203
cb.append(" ti);
return 10; /1 Número de caracteres añadidos
/ * Output :
Yazeruyac
Fowenucor
Goeazimom
Raeuuacio
Nuoadesiw
Hageaikux
Ruqicibui
Numasetih
204 Piensa en Java
Kuuuuozog
Waqizeyoy
*///,-
La interfaz Readablc sólo requiere que se implemente un método read( ). Dentro de read( ), añadimos la infonnación al
argumento C harBuffer (hay varias formas de hacer esto, consulte la documentación de CharB uffer), o devolvemos -]
cuando ya no haya más datos de entrada.
Supongamos que disponemos de una clase base que aún no implementa Readable. en este caso, ¿cómo podemos hacer que
funcione con Scanner? He aquí un ejemplo de una clase que genera números en coma flotante aleatorios.
ji: interfaces/RandornDoubles.java
import java.util. *¡
1* Output:
0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732
0.5166020801268457 0.2678662084200585 0 . 2613610344283964
*///,-
De nuevo. podemos utili zar el patrón de di seño adaptador, pero en este caso la clase adaptada puede crearse heredando e
implementando la interfaz Readable. Por tanto, si utilizamos la heren cia pseudo-múltiple proporcionada por la palabra clave
interface, produciremos una nueva clase que será a la vez Ra ndomDoubles y Readable:
11: interfaces/AdaptedRandornDoubles.java
11 Creación de un adaptador mediante herencia.
import java.nio . *;
import java.util. *¡
1* Output:
0.7271157860730044 0.5309454508634242 0 .1 6020656493302599 0.18847866977771732
0.5166020801268457 0 . 2678662084200585 0 . 2613610344283964
* /// ,-
Puesto que podemos añadir de esta fomla Ulla interfaz a cualquier clase ex istente, podemos deducir que un método que tom e
como argumen to una interfaz nos permitirá adaptar cualquier c lase para que funcione con dicho método. Aquí radica la ver-
dadera potencia de utiliza r interfaces en lugar de clases.
9 Interfaces 205
Ejerci cio 16: (3) Cree una clase que genere una secuencia de ca racteres. Adapte esta clase para que pueda utilizarse
COlll0 entrada a un objeto Scanner.
/ * Output:
8
-32032247016559954
-8.5939291E18
5.779976127815049
*///,-
Los campos, por supuesto, no fonnan parte de la interfaz. Los valores se almacenan en el área de almacenamiento estático
correspondiente a dicha interfaz.
Anidamiento de interfaces
Las interfaces pueden anidarse dentro de clases y dentro de otras interfaces. 3 Esto nos revela una serie de características
in teresantes:
//: interfaces/nesting/Nestinglnterfaces.java
package interfaces.nesting;
class A
interface B
void f () ;
public interface C {
void f () ;
private interface O {
void f () ;
] Grac ias a Martin Danner por hacer una pregunta a este respecto en un seminario.
9 Interfaces 207
interface E {
interface G
void f () ;
JI "public" redundante:
public interface H {
void f () ;
void 9 () ;
JI No puede ser private dentro de una interfaz:
li t private interface 1 {}
tra que también puede implementarse como clase pública. Sin embargo. A.Dlmp2 sólo puede utili zarse co mo ella misma.
No se nos pennite mencionar el hecho de que implementa la interfaz privada D, por lo que implementar interfaces privadas
es una fomla de forzar la defini ción de los métodos de dicha interfaz sin añadi r ninguna información de tipos (es decir, sin
permitir ninguna generalización).
El método getD() nos reve la un dato adicional acerca de las interfaces privadas: se trata de un método público que devuel~
ve una referencia a un interfaz privada. ¿Qué podemos hacer con el valor de retomo de este método? En main(), podemos
ver varios intentos de utili za r el va lor de re tomo, todos los cua les fallan. La única cosa que funciona es entregar el valor de
retomo a un objeto que tenga penniso para usarlo. qu e en este caso es orro objeto A, a través del método receiveD( ).
La interfaz E mues tra que podemos ani dar unas interfaces dentro de otras. Sin embargo, las reglas acerca de las interfaces.
en particular, qu e lodos los elementos de la interfaz tienen que ser públicos, se imponen aquí de manera estricta, por lo que
una interfaz anidada dentro de otra será automáticamente pública y no puede nun ca definirse como privada.
Nestinglnterfaces muestra las diversas fonnas en que pueden implementarse las interfaces anidadas. En particular, obser~
ve que, cuando implementamos una interfaz, no estamos obligados a implemenrar ninguna de las interfaces anidadas de n~
tro de ella. Asimismo, las interfaces pri vadas no pueden impl ementarse fuera de sus clases definitorias.
Inicia lmente, pudiera parecer qu e estas característi cas sólo se hubieran miad ido para gara nti zar la coherencia si ntác tica, pero
mi ex periencia es que una vez que se conoce una característica siempre se descubren ocasiones en las que puede resultar
útil.
Interfaces y factorías
El objeto principal de una interfaz es pem1itir la existencia de múltiples implement aciones, y una fonna típica de producir
objetos que encajen con una interfaz es el denominado patrón de diselio de método factoría. En lugar de llamar a un cons~
tructor directamente. invocamos un método de creación en un objeto factoría que produce una impl ementación de la inter~
faz; de esta forma. en teoría, nuestro cód igo estará completamente ais lado de la implementac ión de la interfaz, haciendo así
posib le intercambiar de manera transparente una implementación por otra. He aquí un ejemplo que muestra la estructura de l
método factoría:
// : interfaces/Factories.java
import static net.mindview.util.Print.*;
interface Service
void methodl () ;
void method2 () j
interface ServiceFactory
Service getService();
/ * Output:
Imp lementacianl methadl
Implementatian! mechod2
Implementation2 methadl
Imp lementation2 method2
*///,-
Sin el método factoría, nuestro código tendría que especificar en algún lugar el tipo exacto de objeto Service que se estu-
viera creando, para poder invocar el constnlctor apropiado.
¿Para qué sirve añadir este nivel adicional de indirecci ón? Una razón común es para crear un marco de trabajo para el de-
sarrollo. Suponga que estam os creando un sistema para juegos que pennita, por ejemplo, jugar tanto al ajedrez como a las
damas en un mismo tablero.
JI: interfaces/Games.java
II Un marco de trabajo para juegos utilizando métodos factoría.
import static net.mindview.util . Print.*¡
/ * Output:
Checkers move O
Checkers move 1
Checkers move 2
Chess move O
Chess move 1
Chess move 2
Chess move 3
* /// ,-
Si la clase Carnes representa un fragmenlO complejo de código, esta técnica permite reutilizar dicho código con diferentes
tipos de juegos. Podemos fácilmente imaginar otros juegos más elaborados que pudieran beneficiarse a la hora de desarro-
llar este diseño.
En el siguiente capítulo, veremos una fonna más elegante de implementar las factorías utili zando clases internas anónjmas.
Ejercicio 18: (2) Cree una interfaz Cycle, con implementaciones Unicycle, Bicycle y Tricycle. Cree fac torias para cada
tipo de Cycle y el código necesario que utilicen estas factorías.
Ejercicio 19: (3) Cree un marco de trabajo utilizando métodos factoría que pennita simular las operaciones de lanzar
una moneda y lanzar un dado.
Resumen
Resulta bastante tentador concluir que las interfaces resultan útiles y que, por tanto, siempre son preferibles a las clases con-
cretas. Por supuesto, casi siempre que creemos una clase, podemos crear en su lugar una interfaz y una factoría.
Mucha gente ha caido en esta tentación creando interfaces y factorías siempre que era posible. La lógica subyacente a este
enfoque es que a lo mejor podemos necesitar en el futuro una implementación diferente, por lo que añadimos siempre dicho
nivel de abstracción. Esta técnica ha llegado a convertirse en una especie de optimización de diseño prematura.
La realidad es que todas las abstracciones deben estar motivadas por una necesidad real. Las interfaces deben ser algo que
utilicemos cuando sea necesari o para optimizar el código, en lugar de incluir ese nivel ad icional de indirección en todas par-
tes, ya que ello hace que aumente la complejidad. Esa complejidad adicional es significativa, y bacer que alguien trate de
comprender ese código tan complejo sólo para descubrir al final que hemos añadido las interfaces "por si acaso" y sin una
razón rea l, esa persona sospechará, con motivo, de todos los diseños que rea licemos.
Una directriz apropiada es la que señala que las clases resultan preferibles a las ¡melfaces. Comience con clases y, si está
claro que las interfaces son necesarias, rediseñe el código. Las interfaces son una herramienta muy conveniente, pero está
bastante generalizada la tendencia a utili zarlas en demasía.
Puede encontrar las soluciones a los ejercicios se leccionados en el documento electrónico Tire Thinking in Jam Annotated So/lItion Guide, disponible para
la venta en wv,'w.J."indl1elOlet.
Clases internas
Resulta posible situar la definición de una clase dentro de la definición de otra. Dichas clases se
llaman clases internas.
Las clases internas constituyen una característica muy interesante, porque nos peml ite agrupar clases relacionadas y contro-
lar la visibilidad mutua de esas clases. Sin embargo, es importante comprender que las clases internas son algo tota lmente
distinto almccanismo de composición de l qu e ya hemos hablado.
A primera vista, las clases internas parecen un simple mecanismo de ocultación de código: colocamos las clases dentro de
otras clases. Sin embargo, como veremos, las clases internas sirven pa ra algo más que eso: la clase interna conoce los deta-
lles de la clase contenedora y puede comunicarse con ella. Asimismo, el tipo de código que puede escribirse con las clases
internas es más elegante y claro (aunque no en todas las ocasiones, por supuesto).
Inicialmente, las clases internas pueden parecer extrañas y se requiere cierto tiempo para llegar a sentirse có modo al utili-
zarlas en los di seilos. La necesidad de las clases internas no siempre resulta obvia, pero después de describir la sintaxis bási-
ca y la se mántica de las clases internas, la sección "¿Para qué sirven las clases internas?" debería permitir que el lector se
haga una idea de los beneficios de emplear este tipo de clases.
Después de dicha sección, el resto del capítulo contiene un análisis más detallado de la sintaxis de las clases internas. Estas
características se proporcionan con el fin de cubrir por completo el lenguaje, pero puede que no tengamos que usarlas nunca,
o al menos no al principio. Asi pues, puede que el lector sólo necesite consultar las partes iniciales del capítulo dejando los
análisis más detallados como material de referencia.
cla ss Destination {
privace String label;
Destination(String whereTo )
label = whereTo¡
/ * Output:
Tasmania
* /// , -
Las clases internas utili zadas dentro de ship( ) parecen c lases n0n11ales. Aquí, la única diferencia práctica es que los nom-
bres están anidados dentro de Pareell. Pronto veremos que esta diferencia no es la única.
Lo más nOlmal es que una clase ex terna tenga un método que devuelva una referencia a una clase interna, como puede verse
en los métodos to( ) y contents( ):
ji : innerclasses/Parce12.java
/1 Devolución de una referencia a una clase interna.
class Destination {
private String label¡
Destination{String whereTo)
label = whereTo¡
1* Output:
Tasmania
* /// ,-
Si queremos construir un objeto de la clase interna en cualquier lugar que no sea dentro de un método 110 estático de la clase
externa, debemos especificar el tipo de dicho objeto como NombreClaseExterna.NombreClaseJl7terna, corno puede verse en
main( ).
10 Clases internas 213
Ejercicio 1: ( 1) Escriba una clase den ominada Outer que contenga una clase interna llamada Jon er . Añada un méto-
do a Outer que de vuel va un objeto de tipo Inn er . En main(), cree e inicialice una referencia a un objeto
Jon er .
i nterface Selector {
boolean end () ;
Object current () ;
v o id next ( ) ;
1- Output:
0 1234 5 6 7 8 9
' /11 ,-
1 Esto difiere significati vamente del diseno de clases anidadas en C++, que simplemente se trata de un mecanismo de ocultación de nombres. No hay nin~
giln enlace al objeto contenedor ni nillgtin tipo de penni sos implícitos en C++.
214 Piensa en Java
La secuenc ia Sequence es simplemente una matriz de tamaño fijo de objetos Object con una clase envoltorio. In vocamos
add( ) para aiiadir un nuevo objelO al final de la secuencia (si queda sitio). Para extraer cada uno de los objetos de la secuen_
cia, hay una interfaz denominada Selector. Éste es un ejemplo del patrón de diseño iterador del que hablaremos más en deta-
lle posterionnente en el libro. Un Selector pennite ver si nos encontramos al final de la secuencia [ende )], acceder al objeto
acnlal [current()] y desplazarse al objeto siguiente [next()] de la secuencia. Como Selector es una interfaz, otras clases pue-
den implementar la interfaz a su manera y otros métodos pueden tomar la interfaz como argumento, para crear código de
propósito más general.
Aquí. SequenceSelector es una clase privada que proporciona la funcionalidad Selector. En main(), podemos ver la crea-
ción de una secuencia, seguida de la adición de una serie de objetos de tipo String. A continuación, se genera un objeto
Selector con una llamada a selector(), y este objeto se utiliza para desplazarse a tra vés de la secuencia y seleccionar cada
elemento.
A primera vista, la creación de SequenceSelector se asemeja a la de cualquier otra clase interna. Pero examinemos el ejem-
plo en más detalle. Observe que cada uno de los métodos [end(), current() y next()] bace referencia a items, que es una
referencia que no forma parte de SequenceSelector, sino que se encuentra en un campo privado dentro de la clase contene-
dora. Sin embargo, la clase interna puede acceder a los métodos y campos de la clase contenedora como si fueran de su pro-
piedad. Esta característica resulta muy cómoda, como puede verse en el ejemplo anterior.
Así pues, una clase interna tiene acceso automático a los miembros de la clase contenedora. ¿Cómo puede suceder esto? La
clase interna captura en secreto una referencia al objeto concreto de la clase contenedora que sea responsable de su crea-
ción. Entonces, cuando hacemos referencia a un miembro de la clase contenedora, dicha referencia se utiliza para seleccio-
nar dicho miembro. Afortunadamente, el com pilador se encarga de resolver todos estos detalles por nosotros, pero resulta
evidente que sólo podrá crearse un objeto de la clase interna en asociación con otro objeto de la clase contenedora (cuando,
como vere mos pronto, la clase interna sea no estática). La construcción del objeto de la clase interna necesita de una refe-
rencia al objeto de la clase contenedora y el compilador se quejará si no puede acceder a dicha referencia. La mayor parte
de las veces todo este mecanismo funciona sin que el programador tenga que intervenir para nada.
Ejercicio 2: (1) Cree una clase que almacene un objeto String y que disponga de un método toString( ) que muestre
esa cadena de caracteres. Añada varias instancias de la nueva clase a un objeto Sequence y luego visualí-
celas.
Ejercicio 3: (1) Modifique el Ejercicio 1 para que Outer tenga un campo private String (inicializado por el construc-
tor) e lnner tenga un método toString( ) que muestre este campo. Cree un objeto de tipo (nner y visua-
licelo.
dti.outer () .f () ;
} 1* Output,
Do tThis. f ()
* 111 ,-
Algunas veces, necesitamos decir a un objeto que cree otro objeto de una de sus clases internas. Para hacer esto es necesa-
rio proporcionar una referencia al objeto de la clase externa en la expresión oew, utilizando la sintaxis .ne\\', como en el
siguiente ejemplo:
1/ : innerclasses/DotNew.java
// Creación de una clase interna directamente utilizando la sintaxis .new.
Para crear un objeto de la clase intern a directamente, no se utiliza esta misma fa mla haciendo referencia al nombre de la
clase externa DotNew como cabría esperar, sino que en su lugar es necesari o utili zar un objeto de la clase ex terna para crear
un objeto de la clase intern a. como podemos ver en el ejemplo anterior. Esto resuelve también las cuestiones re lativas a los
ámbitos de los nombres en la c lase interna, por lo que nun ca escri biríamos (porque, de hecho, no se puede) dO.De\\'
DotNew.lnner( ).
No es posible crea r un objeto de la clase interna a menos que ya se di sponga de un objeto de la c lase ex tern a. Esto se debe
a que el objeto de la clase interna se conecta de manera transparente al de la clase ex tern a que lo haya creado. Sin embar-
go, si defini mos una clase anidada, (una clase intern a estática), entonces no será necesa ri a la refe renc ia al objeto de la clase
externa.
A continuación puede ver có mo se aplicaría .Den' a l eje mplo " Parcer':
11 : innerclasses/Parcel3.java
JI Utilización de .new para crear instancias de clases internas.
c lass Destination {
private String label¡
Destination (String whereTo ) { label = whereTo¡ }
String readLabel () { return label ¡ }
class Parcel4 {
private class PCont en t s implements Contents {
private i n t i = 11;
public int val ue () { return i; }
que protected también proporciona acceso de paquete) y las clases que hereden de Parcel4. Esto quiere decir que el pro-
gramador de clientes tiene un conocimiento de estos miembros y un acceso a los mismos restringido. De hecho, no pode-
mos ni siquiera realizar una especialización a una clase interna privada (ni a una clase interna protegida, a menos que
estemos usando una c lase que herede de ella), porque no se puede acceder al nombre, como podemos ver en class
TestParcel. Por tanto, las clases internas privadas proporcionan una forma para que los diseñadores de clases eviten com-
pletamente las dependencias de la codificación de tipos y oculten totalmente los detalles relativos a la implementación.
Además. la extensión de una interfaz resulta inútil desde la perspectiva del programador de clientes, ya que éste no puede
acceder a ningún método adicional que no forme parte de la interfaz pública. Esto también proporciona una oportunidad
para que el compilador de Java genere código más eficiente.
Ejercicio 6 : (2) Cree una interfaz con al menos un método, dentro de su propio paquete. Cree una clase en un paque-
te separado. Añada una clase interna protegida que implemente la interfaz. En un tercer paquete, defina
una clase que herede de la anterior y, dentro de un método, devuelva un objeto de la clase interna protegi-
da, efecnlando una generalización a la interfaz durante el retorno.
Ejercicio 7: (2) Cree una clase con un campo privado y un método privado. Cree una clase interna con un método que
modifique el campo de la clase externa e invoque e l método de la clase externa. En un segundo método
de la clase externa, cree un objeto de la clase interna e invoque su método, mostrando a continuación el
efecto que esto tenga sobre el objeto de la clase externa.
Ejercicio 8: (2) Detennine si una clase externa tiene acceso a los elementos privados de su clase interna.
La clase PDestination es parte de destination( ) en lugar de ser parte de Parcel5. Por tanto, no se puede acceder a
PDestination fuera de destination(). Observe la generalización que tiene lugar en la instrucción return: lo único que sale
de destination( ) es una referencia a Destination, que es la clase base. Por supuesto, el hecho de que el nombre de la clase
PDestination se coloque dentro de destination( ) no quiere decir que PDestinatioD no sea un objeto válido una vez que
destination( ) tennina.
Podemos utilizar el identificador de clase PDestination para nombrar cada clase interna dentro de un mismo subdirectorio
sin que se produzcan colisiones de clases.
El siguiente ejemplo muestra cómo podemos anidar clases dentro de un ámbito arbitrario.
jj : innerclasses/Parcel6.java
ji Anidamiento de una clase dentro de un ámbito.
La clase TrackingSlip está anidada dentro del ámbito de una instrucción ir. Esto 00 quiere decir que la clase se cree con-
dicionalmente; esa clase se compila con todo el resto del código. Sin embargo, la clase no está disponible fuera del ámbito
en que está definida. Por lo demás, se asemeja a una clase nornlal.
Ejercicio 9 : (1) Cree una interfaz con al menos un método e implemente dicha interfaz definiendo una clase intema
dentro de un método que devuelva una referencia a la interfaz.
Ejercicio 10: (1) Repita el ejercicio anterior, pero definiendo la clase interna dentro de un ámbito en el interior de un
método.
Ejercicio 11: (2) Cree una clase interna privada que implemente una interfaz pública. Escriba un método que devuel va
una referencia a una instancia de la clase interna privada, generalizada a la interfaz. Demuestre que la clase
interna está completamente oculta, tratando de realizar una especialización sobre la misma.
10 Clases internas 219
El método contents( ) combina la creación del va lor de retorno con la definición de la clase que representa dicho valor de
retorno. Además, la clase es anónima, es decir, no tiene nombre. Para complicar aún más las cosas, parece como si estuvié-
ramos empeza ndo a crear un objeto Contents, y que entonces, antes de lJegar al punto y co ma, dij éramos "Un momento:
voy a introducir una definición de clase".
Lo que esta extrai'ia sintax is significa es: "Crea un objeto de una clase anónima que herede de Contents". La referencia
devuelta por la expresión De\\' se generalizará automáticamente a una referencia de tipo Contents. La sintax is de la clase
interna anónima es una abreviatura de:
11: innerclasses/Parce17b.java
II Versión expandida de Parce17.java
public class Parce17b {
class MyContents implements Contents
private int i = 11;
public int value () { return i i }
Es decir, simplemente pasamos e l argumento apropiado al constructor de la clase base. C0l110 sucede aquí con la x que se
pasa en oew Wr apping(x). Aunque se trata de una c lase nonnal con una implementación, W r ap ping se está usando tamo
bién como "interfaz" con sus clases derivadas:
JI : innerclasses / Wrapping.java
public class Wrapping {
prívate int i;
public Wrapping ( int x ) { i = x; }
public int value () { return i; }
/// ,-
Como puede observar. \ Vr a p p in g liene un constructo r que requiere un argumento, para que las cosas sean un poco más inte-
resantes.
El punto y coma si mado al final de la c lase intema anónima no marca el final del cuerpo de la c lase, sino el final de la expre-
sión que co nten ga a la clase anónima. Por tanto, es una utili zac ión idéntica al uso del punto y coma en cua lquier otro lugar.
También se puede realiza r la inicialización cuando se definen los campos en una clase anónima:
// : innerclasses / ParceI9 . java
// Una clase interna anónima que realiza la
// inicialización . Versión más breve de ParceIS.java.
Si estamos definiendo una clase interna anónima y queremos usar un objeto que está definido fuera de la clase interna anó~
nima. el com pilador requiere que la referencia al argumento sea fin a l, como puede verse en e l argumento de d es tin a tion().
Si nos olvidamos de hacer eslO, obtendremos un mensaje de error en tiempo de co mpilac ión.
Mientras que es temos simplemente reali za ndo una asignación a un campo, la técnica empleada e n es te ejempl o resulta ade-
cuada. ¿Pero qué sucede si necesitamos reali zar algún tipo de actividad similar a la de los constmclO res? No podemos dis-
poner de un constmctor nominado dentro de una clase anónima (ya que la c lase no tiene ningún nombre), pero con el
mecani smo de inicialización de instancia. podemos, en la práctica, crear un constructor para una clase interna anónima. de
la fomla siguiente:
11 : innerclasses / AnonymousConstructor.java
II Creación de un constructor para una clase interna anónima.
import static net.mindview.util.Print.*;
/ * Output :
Base constructor, i = 47
rnside instance initializer
In anonymous fe)
* /// >
En este caso, la variable i no tenía porqué haber sido final. Aunque se pasa i al constructor base de la c lase anónima. nunca
se utili za esa variable dentro de la clase anónima.
He aq ui un ejemplo con inicialización de instancia. Observe que los argumentos de destination( ) deben ser de tipo final ,
puesto que se los usa dentro de la clase anónima:
ji : innerclasses/ParcellO . java
JI USO de "inicialización de instancia" para realizar
II la construcción de una clase interna anónima .
};
1* Output :
Over budget!
* /1/ , -
Dentro del inicializador de instanc ia, podemos ver código qu e no podría ejecutarse como parte de un inicializador de campo
(es dec ir, la instrucción if). Por tanto, en la práctica, un inicializador de instan cia es el constructor de una clase interna anó-
nima. Por supuesto, esta solución está limitada : no se pueden sobrecargar los inicializadores de instancia, así que sólo pode-
mos disponer de uno de estos constructores.
Las clases imemas anónimas están en cierta medida limitadas si las comparamos con el mecani smo nonnal de herenci a, por-
que tiene n que extender Ull a clase o implementar una interfaz, pero no pueden hacer ambas cosas al mismo tiempo. Y, si
implementam os una interfaz. sólo podemos implementar una.
222 Piensa en Java
Ejercicio 12: ( 1) Repita el Ejerci cio 7 ut ili zando una clase interna anónima.
Ejercicio 13: ( 1) Repita e l Ejercicio 9 utilizando una clase interna anónima.
Ejercicio 14: ( 1) Modifique interfaces/Ho r rorShow.j.v. para impl ementar Da nge rousMo nster y Va mpire utilizando
clases anónimas.
Ejercicio 15: (2) Cree una clase con un constructor no predetemli nado (uno que tenga argumentos) y sin ning ún cons·
tmctor predetenn inado (es decir, un constructor sin argumenlos). Cree una segunda clase que tenga un
método que devuel va una referenc ia a un objeto de la primera clase. Cree el objeto qu e hay que devolver
defini endo Ulla clase intema anón ima que herede de la primera clase.
interface Service
void methodl () i
void method2 () i
interface ServiceFactory
Service getService( ) i
1* Output:
Implementationl methodl
rmplementationl method2
Implementation2 methodl
Implementation2 method2
* / 1/,-
Ahora. los constructores de hnplementationl e Implementation2 pueden ser pri vados y no hay ninguna necesidad de crear
una clase nominada C01110 facrofi a. Además, a menudo sólo hace falta un úni co objeto fa ctoría , de modo qu e aquí lo hemos
creado C0l110 un campo estáti co en la implementac ión de Service. Asimismo, la sintax is resultante es más clara.
Tam bién podemos mejorar e l ej empl o interfaces/Games.java utilizando clases internas anónimas:
JI: innerclasses/Games . java
II Utilización de clases internas anónimas con el marco de trabajo Game.
import static net.mindview . util . Print .*
/* Output:
224 Piensa en Java
Checkers move O
Checkers move 1
Checkers move 2
Chess move O
Chess move 1
Chess move 2
Chess move 3
, /// ,-
Recuerde el consejo que hemos dado al final del último capítulo: U,Uice las clases con preferencia a las in/elfaces. Si su
diseño necesita una interfaz, ya se dará cuenta de ello. En caso contrario, no emplee una interfaz a menos que se vea obli·
gado.
Ejercicio 16: (1) Modifique la sol ución del Ejercicio 18 del Capitulo 9, Imelfaces para utilizar c lases internas anónimas.
Ejercic io 17: (1) Modifique la solución del Ejercicio 19 del Capitulo 9, In/e/faces para utilizar clases internas anónimas.
Clases anidadas
Si no es necesario disponer de una conexión entre el objeto de la clase ¡ntema y el objeto de la clase extema, podemos defi-
nir la clase interna como estática. Esto es lo que comúnmente se denomina una clase anidada. 2 Para comprende r el signi fi-
cado de la palabra clave static cuando se la aplica a las clases internas, hay que recordar que el objeto de una clase intema
normal mantiene implícitamente una referencia al objeto de la clase contenedora que lo ha creado. Sin embargo, esto no es
cierto cuando definimos una clase interna corno estática. Por tanto, una clase anidada significa:
1. Que no es necesario un objeto de la clase externa para crear un objeto de la clase anidada.
2. Que no se puede acceder a un objeto no estático de la clase externa desde un objeto de una clase anidada.
Las clases anidadas difieren de las clases internas ordinarias también en otro aspecto. Los campos y los métodos en las cla-
ses internas l10rnlales sólo pueden encontrarse en el nivel externo de una clase, por lo que las clases internas nomlales no
pueden tener datos estáticos, campos estáticos o clases anidadas. Si.n embargo, las clases anidadas pueden tener cualquiera
de estos elementos:
ji : innerclasses/Parcel11.java
ji Clases anidadas (clases internas estáticas).
2 Guarda cierto parecido con las clases anidadas de C++, salvo porque dichas clases no penniten acceder a miembros privados a dilercncia de lo que suee-
de en Java.
10 Clases internas 225
1* Output:
Howdy!
*//1,-
Resulta bastante útil anidar una clase dentro de una interfaz cuando queremos crear un código común que baya que em-
plear con todas las diferentes implementaciones de dicha interfaz.
Anteriornlente en el libro ya sugeríamos incluir un método main( ) en todas las clases. con el fin de utilizarlo como meca-
nismo de prueba de estas clases. Una desventaja de esta técnica es la cantidad de código compilado adicional con la que hay
que trabajar. Si esto representa un problema, pruebe a utilizar una clase anidada para incluir el código de prueba:
JI : innerclassesJTestBed.java
JI Inclusión del código de prueba en una clase anidada .
JI {main : TestBed$Tester}
1* Output:
f ()
* /// ,-
Esto genera una clase separada denominada Tcs tB cd$Tester (para ejecutar el programa, escribiríamos java
TestBedSTestcr ). Puede utilizar esta clase para las pmebas, pero no necesitará incluirla en el producto final , bastará con
borrar TestBed$Tester.class antes de crear el producto definitivo.
Ejercicio 20 : (1) Cree una interfaz que contenga una clase anidada. Implemente esta interfaz y cree una instancia de la
clase anidada.
Ejercicio 21: (2) Cree una interfaz que contenga una clase anidada en la que haya un método estático que invoque los
métodos de la interfaz y muestre los resultados. Implemente la interfaz y pase al método una instancia de
la implementación.
elass MNA
private void f () {}
class A
pri vate void 9 () {}
publ ie elass B {
void h() (
g ();
f ();
Puede ver que en MNA.A. B. los métodos g() y f() son invocables sin necesidad de ninguna cualificación (a pesar del hecho
de que son privados). Este ejemplo también ilust ra la sintaxis necesaria para crear objetos de clases internas múltiplemente
anidadas cuando se crean los objetos en una clase diferente. La sintaxis ·'.new·' genera el ámbito correcto, por lo que no hace
falta cualificar el nombre de la clase dentro de la llamada al constmctor.
Cada clase in/erna puede heredar de una implementación de manera independiente. Por tamo, la clase
interna no está limitada por el hecho de si la clase externa ya está heredando de una implementación.
Sin la capac idad de las clases internas para heredar, en la práctica, de más de una clase concreta o abstracta, algunos pro-
blemas de diseño y de programación serían intratables. Por tanto, una forma de contemplar las clases internas es decir que
representan el resto de la solución del problema de la herencia múltiple. Las interfaces resuelven parte de l problema. pero
las clases internas permiten en la práctica una "herencia de múltiples implementaciones". En otras palabras, las clases inter-
nas nos pemliten en la práctica heredar de varios elementos que no sean in terfaces.
Para analizar esto con mayor detalle, piense en una situación en la que tuviéramos dos interfaces que deban de alguna forma
ser implementadas dentro de una clase. Debido a la flexibilidad de las interfaces, tenemos dos opciones: una única clase o
una clase interna.
/1: innerclasses/MultiInterfaces.java
II Dos formas de implementar múltiples interfaces con una clase.
package innerclassesi
interface A {}
interface 8 {}
class X implements A, B {}
class y implements A
8 makeB (1 {
JI Clase interna anónima:
return new 8 (1 {};
Por supuesto. esto presupone qu e la estrucrura del código tenga sentido en ambos casos desde el punto de vista lógico. Sin
embargo. nonnalmente dispondremos de algún tipo de directriz, extraída de la propia naturaleza del problema, que nos indi·
cará si debemos utilizar una única clase o una clase interna. pero en ausencia de cualquier otra restricción, la técn ica utili-
zada en el ejemplo anterior no presenta muchas diferencias desde e l punto de vista de la imp lementación. Ambas so luciones
funcionan adecuadamente.
Sin embargo. si ten emos clases abstractas o concretas en lugar de interfaces, nos veremos obligados a utilizar clases inter-
nas si nuestra c lase debe imp lementar de alguna forma las otras clases de las que se quiere heredar:
/1: innerclasses/Multilmplementation.java
II Con clases abstractas o concretas, las clases
II internas son la única forma de producir el efecto
II de la "herencia de múltiples implementaciones".
package innerclasses¡
class D {}
abstract class E {}
class Z extends D {
E makeE () ( return new E l) {}; }
Si no necesitáramos resolver el problema de la '"herencia de múltiples implementaciones", podríamos escribir el resto del
programa sin necesidad de utilizar clases internas. Pero con las clases internas tenemos. además. las siguientes característi-
cas adicionales:
1. La clase interna puede tener múltiples instancias, cada una con su propia infonnación de estado que es indepen-
diente de la infornlación contenida en el objeto de la clase externa.
2. En una única clase externa, podemos tener varias clases internas, cada una de las cuales implementa la misma
interfaz o hereda de la mis ma clase en una fornla diferente. En breve mostraremos un ejemp lo de este caso.
3. El punto de creación del objeto de la clase interna no está ligado a la creación del objeto de la clase externa.
4. No existe ninguna relación de tipo "es-un" potencialmente confusa con la clase interna, se trata de una entidad
separada.
Por eje mplo, si Seq uence.java no utilizara clases internas, estaríamos ob ligados a decir que "un objeto Sequ ence es un obje-
to Selecto r ", y sólo podría existir un objeto Selec to r para cada objeto Sequ ence concreto. Sin embargo, podríamos fácil-
mente pensar en definir un segundo método, reve rseSelecto r( ), que produjera un objeto Selecto r que se desp lazara en
sentido inverso a través de la secuencia. Este tipo de flexibilidad sólo está disponible con las clases internas.
Ejercicio 22 : (2) Implemente reve rseSelecto r ( ) en Sequ ence.j ava.
Ejercicio 23 : (4) Cree una interfaz U con tres métodos. Cree una clase A con un método que genere una referencia a U_
definiendo una clase interna anónima. Cree una segunda clase B que contenga una matriz de U. B debe
tener un método que acepte y almacene una referencia a U en la matriz, un segundo método que configu-
re una referencia en la matriz (especificada mediante el argumento del método) con el valor null, y un ter-
ce r método que se desplace a través de la matriz e invoque los mé todos de U. En main (), cree un grupo
de objetos A y un único B. Rellene e l objeto B con referencias a U generadas por los objetos A. Utilice el
objeto B para realizar llamadas a todos los objetos A. Elimine algunas de las referencia s U del objeto 8.
10 Clases internas 229
Cierres y retrollamada
Un cierre (closure) es un objeto invocable que retiene infol1l13ción acerca del ámbito en que fue creado. Teniendo en cuen-
ta esta definición, podemos ver que una clase intem8 es un cierre orientado a objetos, porque no contiene simplemente cada
elemento de infonnación del objeto de la clase externa ("e l ámbito en que fue creado"). sino que almacena de manera auto-
mática una re ferencia que apunta al propio objeto de la clase externa, en el cual tiene penniso para manipular lodos los
mi embros. incluso aunque sea privados.
Uno de los argumentos más sólidos que se proporcionaron para incluir algún mecanismo de punteros en Java era el de per-
mitir las re/rol/amadas (callbacks). Con una retrollamada, se proporciona a algún o tro objeto un elemento de infonnación
que le pennite llamar al objeto o ri ginal en un momento posterior. Se trata de un concepto muy potente, como veremos más
adelante. Sin embargo, si se implementa una retrollamada utilizando un puntero, nos veremos forzados a confiar en que el
programador se comporte correctamente y no haga un mal uso del puntero. Como hemos visto hasta e l momento, el lengua-
je Java tiende a ser bastante más precavido. por lo que 110 se ban incluido punteros en el lenguaje.
El cierre proporcionado por la clase interna es una buena solución, bastante más flexible y segura que la basada en punte-
roS. Veamos un ejemplo:
11 :i nnerclasses/Callbacks .java
II Utili za c i ón de clases internas para las retrollamadas
package innerclassesi
import static net . mindview . util .Print .*i
interface Incrementable
void increment() i
class Mylncrement {
public void increment () print ("Other operation") i
static void f(Mylncrement mi) { mi. increment () ; }
Incrementable getCallbackReference(}
return new Closure() i
230 Piensa en Java
elass Caller {
private Inerementable eallbaekReferenee;
Caller (Inerementable ebh) { eallbaekReferenee cbh; }
void 90 () { eallbaekReference. inerement (); }
/ * Output:
Other operation
1
1
2
Other operatíon
2
Other operatíon
3
* /// ,-
Esto muestra también una di stinción ad icional entre el hecho de implementar una interfaz en una clase externa y el hecho
de hacerlo en una clase interna. Calleel es claramente la solución más simple en ténninos de código. Callee2 hereda de
Mylncrement, que ya dispone de un método increment( ) diferente que lleva a cabo alguna tarea que no está relacionada
con la que la interfaz Incrementable espera. Cuando se hereda Mylncremcnt en Callee2, increment( ) no puede ser sus-
tituido para que lo utilice Incrementable. por lo que estamos obligados a proporcionar una implementación separada
mediante una clase interna. Observe también que cuando se crea una clase interna no se añade nada a la interfaz de la clase
ex terna ni se la modifica de nin guna manera.
Todo en Callee2 es privado salvo getCallbackReference(). Para permitir algún tipo de conexión con el mundo exterior, la
interfaz Incrementable resulta esencial. Con este ejemplo podemos ve r que las interfaces penniten una completa separa-
ción cntre la interfaz y la implementación.
La clase interna C losure implementa In crementable para proporcionar un engarce con Callee2 , pero un engarce que sea
lo suficientemente seguro. Cualquiera que obtenga la referencia a Incrementable sólo puede por supuesto invocar incre-
ment() y no tiene a su dispos ición ninguna otra posibilidad (a diferencia de un puntero, que nos penni tiría hacer cualquier
cosa).
Caller toma una referencia a Incrementable en su constructor (aunque la captura de la refere ncia de retrollamada podría
tener lugar en cualquier instante) y luego en algún momento posterior utiliza la referencia para efectuar una retro llamada a
la clase Callee.
El va lor de las retrollamadas radica en su flexibil idad; podemos decidir de manera dinámica qué métodos van a ser invoca-
do en tiempo de ejecución. Las ven tajas de esta manera de proceder resultarán evidentes en el Capítul o 22, lntelfaces grá-
ficas de usuario, en el que emplearemos de manera intensiva las retrolJamadas para implementar la funcionalidad GUl
(Graphical User/I/te/face).
Un marco de lrabajo de una aplicación es una clase o un conjunto de clases diseñado para resolver un tipo concreto de pro-
blema. Para aplicar un marco de trabajo de una aplicación, lo que nonnalmente se hace es heredar de una o más clases y
sustituir algunos de los métodos. El código que escribamos en los métodos sustituidos sirve para personalizar la solución
general proporcionada por dicho marco de trabajo de la aplicación, con el fin de resolver nuestros problemas específicos.
Se trata de un ejemplo del patrón de disclio basado en el método de plantillas (véase Thinking in Pallerns (with Java) en
u'l\'w.A1indView.l1el). El método basado en plantillas contiene la estrucntra básica del algoritmo, e invoca uno o más méto-
dos sustituibles con el fin de completar la acción que el algoritmo dictamina. Los patrones de diseño separan las cosas que
no cambian de las cosas que sí que sufren modificación yen este caso el método basado en plantillas es la parte que penna-
nece invariable, mientras que los métodos sustituibles son los elementos que se modifican.
Un marco de control es un tipo particular de marco de trabajo de apJjcación, que está dominado por la necesidad de respon-
der a cierto suceso. Los sistemas que se dedican principalmente a responder a sucesos se denominan sistemas dirigidos por
s/lcesos. Un problema bastante común en la programación de aplicaciones es la interfaz gráfica de usuario (GUI), que está
casi completamente dirigida por sucesos. Como veremos en el Capítulo 22, ¡me'laces gráficas de usuario, la biblioteca
Swing de Java es un marco de control que resuelve de manera elegante e l problema de las interfaces GUI y que utili za de
manera intensiva las clases internas.
Para ver la forma en que las clases internas permiten crear y utili zar de manera se ncilla marcos de control , considere un
marco de control cuyo trabajo consista en ejecutar sucesos cada vez que dichos sucesos estén " listos". Aunque " listos"
podría sign ificar cualqu ier cosa, en este caso nos basaremos en el dato de la hora actual. El ejemplo que sigue es un marco
de control que no contiene ninguna infonnación específica acerca de qué es aquello que está controlando. Dicha infonna-
ción se suministra mediante el mecanismo de herencia, cuando se implementa la parte del algoritmo correspondiente al
método .ction().
En primer lugar, he aquí la interfaz que describe los sucesos de control. Se trata de una clase abstracta, en lugar de una ver-
dadera interfaz, porque el comportamiento predeterminado consiste en llevar a cabo el control dependiendo del instante
actual. Por tanto, parte de la implementación se incluye aquí:
11 : innerclasses/controller/Event.java
II Los métodos comunes para cualquier suceso de control.
package innerclasses.controller¡
mas en más detalle en el Capínllo 11 , Almacenamiento de objetos. Pero ahora lo único que necesitamos saber es que add()
añade un objeto Event al final de la lista List, que size() devuelve el número de elementos de List, que la sintaxisforeach
pennüe extraer objetos Event sucesivos de List, y que remove() elimina el objeto Event especificado de List.
11 : innerclasses / controller / Controller.java
II El marco de trabajo reutilizable para sistemas de control.
package innerclasses.controller¡
import java.util.*;
El método run() recorre en bucle una copia de eventList, buscando un objeto Event que esté listo para ser ejecutado. Para
cada uno que encuentra, imprime infonnación utilizando el método toString( ) del objeto, invoca el método action( ) y
luego elimina el objeto Event de la lista.
Observe que en este diseño, hasta ahora, no sabemos nada acerca de qué es exactamente lo que un objeto Event hace. Y
éste es precisamente el aspecto fundamental del diseño: la manera en que "separa las cosas que cambian de las cosas que
permanecen iguales". O, por utilizar un tém1ino que a mi personalmente me gusta, el "vector de cambio" está compuesto
por las diferentes acciones de los objetos Event, y podemos expresar diferentes acciones creando distintas subclases de
Event.
Aquí es donde entran en juego las clases internas. Estas clases nos permiten dos cosas:
1. La implementación completa de un marco de control se crea en una única clase, encapsulando de esa forma lodas
aquellas características distintivas de dicha implementación. Las clases internas se usan para expresar los múlti-
ples tipos distintos de acciones [actionOJ necesarias para resolver el problema.
2. Las clases internas evitan que esta implementación sea demasiado confusa, ya que podemos acceder fácilmente
a cualquiera de los miembros de la clase externa. Sin esta capacidad, el código podría llegar a ser tan complejo
que terminaríamos tratando de buscar una alternativa.
Considere una implementación concreta del marco de control di seilado para regular las funciones de un invernadero. 4 Cada
acción es totalmente distinta: encender y apagar las luces, iniciar y detener el riego, apagar y encender los termostatos, hacer
sona r alarn18s y reinicializar el sistema. Pero el marco de control está diseñado de tal manera que se aíslan fácilmente estas
distintas secciones del código. Las clases internas permiten disponer de múltiples versiones derivadas de la misma clase
base, Event, dentro de una misma clase. Para cada tipo de acción, heredamos una nueva clase interna ·Event y escribimos
el código de control en la implementación de action().
Como puede supo ner por los marcos de trabajo para aplicaciones, la clase GreenhouseControls hereda de ControUer:
11 : innerclasses/GreenhouseControls.java
11 Genera una aplicación específica del sistema
4 Por alguna razón, este problema siempre me ha resultado bastante grato de resolver; proviene de mi anterior libro C+ +/l1Side & Ow, pero Java permite
obtener una solución más elegante.
10 Clases internas 233
}
// Un ejemplo de action {) que inserta un
// nuevo ejemplar de 51 misma en la línea de sucesos:
public class Bell extends Event {
public Bell (long delayTime) ( super (delayTime) ¡
public void action () {
addEvent(new Bell(delayTime));
La siguie nte clase configura el sistema creando un objeto GreenhouseControls y añadiendo di versos tipos de objetos
Event. Esto es un ejemplo del patrón de diseño Command: cada objeto de eventList es una solicitud encapsulada en forma
de objeto:
1/ : innerclasses / GreenhouseController.java
1/ Configurar y ejecutar el sistema de control de invernadero.
11 {Args, sooo }
i mport innerclasses.controller .* ¡
1* Output :
Bing!
Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Greenhouse water is off
Thermostat on day setting
Restarting system
Terminating
* 1// ,-
Esta clase inicializa el sistema, para que añada todos los sucesos apropiados. El suceso Restart se ejecuta repetidamente y
carga cada vez la lista eventList en el objeto GreenhouseControls. Si proporcionamos un argumento de línea de coman-
dos que indique los milisegundos, Restart tennillará el programa después de ese número de milisegundos especificado (esto
se usa para las pruebas).
Por supuesto, resulta más flexible leer los sucesos de un arclüvo en lugar de leerlos en el código. Uno de los ejercicios del
Capítulo 18, E/S, pide, precisamente, que modifiquemos este ejemplo para hacer eso.
Este ejemplo debería pemútir apreciar cuál es el va lor de las clases internas, especialmente cuando se las usa dentro de un
marco de control. Sin embargo. en el Capítulo 22, ¡me/faces gráficas de usuario, veremos de qué [onna tan elegante se uti-
li zan las clases internas para definir las acciones de una interfaz gráfica de usuario. Al tenninar ese capítulo, espero haber-
le convencido de la utilidad de ese tipo de clases.
Ejercicio 24: (2) En GreenhouseControls.java , añada una serie de clases internas Event que pennitan encender y apa-
gar una serie de ventiladores. Configure GreenhouseController.java para utilizar estos nuevos objetos
Event.
236 Piensa en Java
Ejercicio 25: (3) Herede de GreenhouseControls en CreenhouseControls.jav3 para ai1adir clases internas Event
que pennitan encender y apagar una seri e de vapori zadores. Escriba una nueva ve rs ión de
GreenhouseController.java para util izar estos nuevos objetos Event.
Puede ver que Inheritlnncr sólo amplía la clase interna, no la ex terna. Pero cuando llega el momento de crear un construc·
tor, el predeterminado no sirve y no podemos limitarnos a pasar una referencia a un objeto contenedor. Además, es necesa·
rio utihzar la si ntax is:
enclosingClassReference.super() ¡
dentro del const ructor. Esto proporciona la referencia necesaria y el programa pod rá así compilarse.
Ejercicio 26: (2) Cree una clase con una clase interna que tenga un constructor no predetenninado (uno que tom e argu-
mentos). Cree una segunda clase con una clase interna que herede de la primera clase interna.
class E99 {
private Yolk y¡
protected class Yolk {
public Yolk() { print{"Egg Yolk() "); }
}
public Egg () {
print ("New Egg () " ) ;
y = new Yolk () ;
10 Clases internas 237
/ * Output:
New Egg ()
Eg9. Yolk ()
*///,-
El compilador sintetiza automática ment e el constructor predeterminado, y éste invoca al constructor predeterminado de la
clase base. Podríamos pensa r que puesto que se está creando un objeto BigEgg, se utili zará la versión "sustituida" de Yolk,
pero esto no es así, como podemos ver ana lizando la salida.
Este ejemplo muestra qu e no hay ningún mecanismo mági co adicional relacionado con las clases internas que entre en
acc ión al heredar de la clase externa. Las dos clases internas son entidades completamente separadas, cada una con su pro-
pio espacio de nombres. Sin embargo. lo que sigue siendo posible es heredar explícitamente de la clase interna:
JI: innerclassesjBigEgg2.java
JI Herencia correcta de una clase interna.
import static net . mindview . util.Print.*¡
class Egg2 {
protected class Yolk {
public Yolk () { print (" Egg2 . Yolk () "); }
public void fl) {print("Egg2.Yolk.f()");}
1* Output:
Egg2 . Yolk ()
New Egg2 ()
Egg2 . Yolk ()
BigEgg2 . Yolk ()
BigEgg2 . Yolk.f()
*/ / / >
Allora, BigEgg2.Yolk amplía explícitamente extends Egg2.Yolk y sustituye sus métodos. El método insertYolk( l pennite
que BigEgg2 generalice uno de sus propios objetos Yolk a la referencia y en Egg2 , por lo que g( l invoca y.f( l, se utili za
la vers ión sustituida de f() . La segund a llamada a Egg2.Yolk() es la llamada que el constructor de la clase base hace al
constructor de BigEgg2. Yolk. Como puede ver, cuando se llama a g( l se utili za la versión sustituida de f( l·
238 Piensa en Java
interface Counter
int next () ;
1* Output :
LocalCounter ()
Counter ()
10 Clases internas 239
Local inner O
Local inner 1
Local inner 2
Local inner 3
Local inner 4
Anonyrnous inner 5
Al1onyrnous inner 6
Anonymous inner 7
Anonymous inner 8
Anonymous inner 9
* /// ,-
Counter devuelve el siguiente valor de una secuencia. Está implementado como una clase local y como una clase interna
anónima, teniendo ambas los mismos comportamientos y capacidades. Puesto que el nombre de la clase interna local no es
accesible fuera del método, la única justificación para utilizar una clase interna local en lugar de una clase interna anónima
es que necesitemos un constructor nominado y/o un constructor sobrecargado, ya que una clase interna anónima sólo puede
utilizar un mecanismo de inicialización de instancia.
Otra razón para definir una clase interna local en lugar de una clase interna anónima es que necesitemos construir más de
un objeto de dicha clase.
Resumen
Las interfaces y las clases internas son conceptos bastante más sofisticados que los que se pueden encontrar en muchos len-
guajes de programación orientada a objetos; por ejemplo, no podremos encontrar nada similar en C++. Ambos conceptos
resuelven, conjuntamente, el mismo problema que C++ trata de resolver con su mecanismo de herencia múltiple. Sin embar-
go, el mecanismo de herencia múltiple en C++ resulta bastante dificil de uti lizar, mientras que las interfaces y las clases
internas de Java son, por comparación, mucho más accesibles.
Aunque estas funcionalidades son, por sí mismas, razonablemente sencillas, el uso de las mismas es una de las cuestio-
nes fundamenta les de diseño, de fonna similar a lo que ocurre con el polimorfismo. Con el tiempo, aprenderá a reconocer
S Por otro lado, '$ ' es un mctacarácter de la shell Unix, por lo que en ocasiones podemos encontramos con problemas a la hora de listar los archivos .class.
Resulta un tamo extraño este problema, dado que el lenguaje Java ha sido definido por Sun, una empresa volcada en el mercado Unix. Supongo que no
tuvieron en cuenta el problema, pensando en quc los programadores se centrarian principahneme en los archivos de código fuente.
240 Piensa en Java
aquellas situaciones en las que debe utili zarse una interfaz o una clase interna. o ambas cosas. Pero almenas, en este pUnto
del libro, sí que el lector debería sentirse cómodo con la sintaxis y la semántica aplicables. A medida que vaya viendo cómo
se aplican estas funcionalidades, tenninará por interiorizarlas.
Puede encontrm las soluciones a los ejercicios seleccionados en el documento electrónico rile Thinkil/g ;1/ )OI'Q Anllotaled SOllllioll GlIide, disponible para
la venta en II'ww.MilldView."el.
Al macenamiento
de objetos
Es un programa bastante simple que sólo dispone de una cantidad de objetos con tiempos de
vida conocidos.
En general, los programas siempre crearán nuevos objetos basándose en algunos criterios que sólo serán conocidos en tiem-
po de ejecución. Antes de ese momento, no podemos saber la cantidad ni el tipo exacto de los objetos que necesitamos. Para
resolver cualquie r problema general de programación, necesitamos poder crear cualquier número de objetos en cualquier
momento y en cualquier lugar. Por tanto, no podemos limitarnos a crear una referencia nominada para almacenar cada uno
de los objetos:
MiTipo unaReferencia¡
! Diversos lenguajes como PerL Payton y Ruby tienen soporte nativo para los contenedores.
242 Piensa en Java
11: holding/ApplesAndOrangesWichoutGenerics.java
1I Ejemplo simple de contenedor (produce advertencias del compilador).
II {ThrowsException}
import java . util.*;
class Apple
private static long counter;
prívate final l ong id = counter++¡
public long id () { reCurn id; }
elass Orange {}
Hablaremos más en detalle de las anotaciones Java SE5 en el Capítulo 20, Anotaciones.
Las clases Apple y Orange son diferentes; no tienen nada en común salvo que ambas heredan de Object (recuerde que si
no se indica explícitamente de qué clase se está heredando, se hereda automáticamente de Object). Puesto que ArrayList
almacena objetos de tipo Objcct, no sólo se pueden aliad ir objetos Apple a este contenedor utilizando el método add() de
ArrayList, si no que también se pueden añadir objetos Orange sin que se produzca ningún tipo de advertencia ni en ti em ~
po de compilación ni en tiempo de ejecución. Cuando luego tratamos de extraer lo que pensamos que son objetos Apple uti~
li zando el método gct() de ArrayList, obtenemos una referencia a un objeto de tipo Object que deberemos proyectar sobre
un objeto Apple. En tonces, necesitaremos encerrar toda la expresión entre paréntesis para forzar la evaluación de la proyec~
ción antes de invocar el método id( ) de Apple; en caso contrario. se producirá un erro r de sintaxis.
2 Éste es uno de Jos casos en los que la sobrecarga de operadores resultaría convenieOfe. Las clases contenedoras de C++ y C# producen una sintaxis [mIS
limpia utilizando la sobrecarga de operadores.
11 Almacenamiento de objetos 243
En tiempo de ejecución, a l intentar proyectar el objeto Orange sobre un objeto Apple. obtendremos un error en la farnla de
la excepción que antes hemos mencionado.
En el Capínilo 20, Genéricos, veremos que la creación de clases utilizando los genéricos de Java puede resultar muy com-
pleja. Sin embargo, la aplicación de c lases genéricas predefinidas suele resultar sencilla. Por ejemplo. para defmir un con-
tenedor ArrayList en el que almacenar objetos Apple, tenemos que indicar ArrayList<App le> en lugar de sólo Arra:yList.
Los corchetes angulares rodean los parámetros de lipo (puede haber más de uno) , que especifican el tipo o tipos que pue-
den almacena rse en esa instancia del contenedor.
Con los genéricos ev itamos, en tiempo de compilación, que se puedan introducir objetos de tipo incorrecto en un contene-
dor.} He aquí de nuevo el ejemplo utilizando genéricos:
ji : holding/ApplesAndOrangesWithGenerics . java
import java.util. *¡
/ * Output:
Ahora el compilador evitará que introduzcamos un objeto Orange en apples, convirtiendo el error en tiempo de ejecución
en un error de tiempo de compi lación.
Observe también que la operación de proyección ya no es necesaria a la hora de extraer los elementos de la lista. Puesto que
la lista conoce qué tipos de objetos almacena, ella misma se encarga de realizar la proyección por nosotros cuando invoca-
mos get(). Por tanto. con los genéricos no sólo podemos estar seguros de que el compilador comprobará el tipo de los obje-
tos que introduzcamos en un contenedor, sino que también obtendremos una si ntaxis más limpia a la hora de utilizar los
objetos almacenados en el contenedor.
El ejemplo muestra también que, si no necesitamos utilizar el índice de cada elemento, podemos utiliza r la sintaxi sjoreach
para seleccionar cada elemento de la lista.
No estamos limitados a almacenar el tipo exacto de objeto dentro de un contenedor cuando especificamos dicho tipo corno
un parámetro genérico. La operación de generalización (upcasting) funciona de igual fonna con los genéricos que con los
demás tipos:
11 : holding / GenericsAndUpcasting.java
import java.util.*;
) Al final del Capitulo 15. Gené/'icos, se incluye una explicación sobre la gravedad de este problema. Sin embargo, dicho capítulo también explica que los
genéricos de Java resultan útiles para otras cosas además de definir contenedores que sean seguros con respecto al tipo de los datos.
244 Piensa en Java
1* Output: (Samplel
GrannySmith@7d772e
Gala@11b86e7
Fuji @35ce36
Braeburn@757aef
* /// ,-
Por tanto, podemos aiiadir un subtipo de Apple a un contenedor que hayamos especificado que va a almacenar objetos
Apple.
La salida se produce utilizando el método toString() predetenninado de Object, que imprime el nombre de la clase segui-
do de una representación hexadecimal sin signo del código hash del objeto (generado por el método hashCode( )l. Veremos
más detalles acerca de los códigos hash en el Capítulo 17, Análisis detallado de los contenedores.
Ejercicio 1: (2) Cree una nueva clase llamada Gerbil con una variable int gerbilNumber que se inicializa en el cons-
tructor. Añada un método denominado hop( ) que muestre el valor almacenado en esa variable entera. Cree
una lista ArrayList y añada objetos Gerbil a la lista. Aho ra, utilice el método get() para desplazarse a
través de la li sta e invoque el método hop( ) para cada objeto Gerbil.
Conceptos básicos
La biblioteca de contenedores Java toma la idea de "almacenamiento de los objetos" y la divide en dos conceptos di stintos,
expresados mediante las interfaces básicas de la biblioteca:
1. Collection : una secuencia de elementos individuales a los que se apli ca una o más reglas. Un contenedor List
debe almacenar los elementos en la fom1a en la que fueron insertados, un contenedor Set no puede tener elemen-
tos duplicados y un contenedor Queue produce los elementos en el orden determinado por una disciplina de cofa
(que nonnalmente es el mismo orden en el que fueron insertados).
2. Map : un grupo de parejas de objetos clave-valor, que permite efectuar búsquedas de va lores utilizando una clase.
Un contenedor ArrayList pennite buscar un objeto utilizando un número, por lo que en un cierto sentido sirve
para asociar números con los objelOs. Un mapa permite buscar un objeto utilizando otro objeto. También se le
denomina matriz asociativa, (porque asocia objetos con otros objetos) o diccionario (porque se utili za para bus-
car un objeto valor mediante un objeto clave. de la misma fonTIa que buscamos una definición utili zando una
palabra). Los contenedores Map son herramientas de programación muy potentes.
Aunque no siempre es posible. deberíamos tratar de escribir la mayor parte del código para que se comunique con estas
interfaces; asi mi smo, el único lugar en el que deberíamos especificar el tipo concreto que estemos usando es el lugar de la
creación del contenedor. Así, podemos crear un contenedor List de la fonna siguiente:
List<Apple> apples = new ArrayList<Apple> ();
Observe que el contenedor ArrayList ha sido general izado a un contenedor List, por contraste con la fonna en que lo había-
mos tratado en los ejemplos anteriores. La idea de utilizar la interfaz es que, si posteriormente queremos cambiar la imple-
mentación, lo úni co que tendremos que hacer es efectuar la modifi cación en el punto de creación, de la fomla siguiente:
List<Apple> apples = new LinkedList<Apple>(};
11 Almacenamiento de objetos 245
Así, nonna lmente crearemos un objeto de una clase concreta. lo generalizaremos a la correspondiente interfaz y luego uti-
lizaremos la interfaz en e l resto de l código.
Esta técnica no siempre sirve. porque algunas c lases di sponen de funcionalidad adicionaL Por ejemplo, LinkedList tiene
métodos adicionales que no fonllan parte de la interfaz List mientras que TreeMap tiene métodos que no se encuentran en
la interfaz Map. Si nec esita mos usar esos métodos, no podremos efectuar la operación de generalización a la interfaz más
generaL
La interfaz Collection generaliza la idea de secuencia: una forma de almacenar un grupo de objetos. He aquí un ejemplo
simple que rellena un contenedor CoUection (representado aquí mediante un contenedor ArrayList) con objetos Illteger y
luego imprime cada elemento en el contenedor resultante:
11 : holding/SimpleCollection java
import java . util. * ¡
1* Output:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
. /// ,-
Puesto que este ejemplo sólo utiliza métodos Collection, cualquier objeto que herede de una clase de Collection funciona-
rá, pero ArrayList representa e l tipo más básico de secuencia.
El nombre del método add() sugiere que ese método introduce un nuevo elemento en el contenedor Collection. Sin embar-
go, la documentación indica expresamente que add() "garanti za que este objeto Collection contenga el e lemento especifi-
cado". Esto es así para pennitir la existencia de contenedores Set, que añaden el elemento sólo si éste no se encuentra ya en
el contenedor. Con un objeto ArrayList, o con cualquier tipo de List, add( ) siempre significa " insertar e l e lemento", por-
que las listas no se preocupan de si ex isten duplicados.
Todas las colecciones pueden recorrerse utilizando la sintaxi sforeach, como hemos mostrado aquí. Más adelante en el capí-
tulo ap renderemos acerca de un co ncepto más flexibl e denominado I!erador.
Ejercicio 2: (1) Modifique SimpleCollection.java para utilizar un contenedor Set para c.
Ejercicio 3: (2) Modifique innerclasses/Sequence.java de modo que se pueda añadir cualquier número de elementos.
El constructor de una colección puede aceptar otra colección con el fin de utilizarla para inicializarse a sí misma, así que
podemos emplear Arrays.asLisl( ) para generar la entrada para el constructor. Sin embargo, Collections.addAII( ) se eje.
cuta mucho más rápido y resulta igualmente sencillo construir el objeto Collection sin ningún elemento y luego invocar
Collections.addAII( ), por lo que ésta es la técnica que más se suele utili za r.
El método miembro Colleclion.addAII( ) sólo puede tomar como argumento otro objeto Colleclion, por lo que no es tan
flexible como Arrays.asList( ) o Collections.addAII( J, que utili zan listas de argumentos variables.
También es posible utilizar directamente la salida de Arrays.asListO como una lista, pero la representación subyacente en
este caso es la matri z, que no se puede cambiar de tamaño. Si tratamos de añadir o borrar elementos en dicha lista, eso impli.
caría cambiar el tamaño de la matriz, por lo que se obtiene un error "Unsupported Operation" en tiempo de ejecución.
Una limitación de Arrays.asList() es que dicho método trata de adivinar cuál es el tipo de lista resultante, sin prestar aten·
ción a la variable a la que la estemos asignando. En ocasiones, esto puede causar un problema :
11 : holding/AsListlnference.java
I I Arrays.asList( ) determina el tipo en sí mismo.
import java.util.*¡
elass Snow {}
class Powder extends Snow {}
class Light extends Powder { }
class Heavy extends Powder {}
class Crusty extends Snow { }
class Slush extends Snow {}
II No se compilará:
11 ListcSnow> snow2 = Arrays.asList {
11 new Light{ ) , new Heavy ( ) ) ;
11 El compilador dirá:
11 found java.util.List<Powder>
11 required: java.util.ListcSnow>
Al tratar de crear snow2, Arrays.asList( ) sólo di spone del tipo Powder, por lo que crea un objeto List<Powder> en lugar
de List<Snow>, mientras que Collections.addAII() funciona correctamente, porque sa be, a partir del primer argumento,
cuál es el tipo de destino.
Como puede ver por la creación de snow4, es posible insertar una " indicación" en mitad de Arrays.asList(), para decirle
al compilador cuál debe ría ser el tipo de destino real para el objeto List producido por Arrays.asList(). Esto se denomina
especificación explícita del tipo de argumento.
Los mapas son más complejos co mo tendremos oportunidad de ver, y la biblioteca estándar de Java no proporciona ningu-
na fonna para inicializarlos de manera automática. sa lvo a partir de los contenidos de otro objeto Map .
Impresión de contenedores
Es necesario utilizar Arrays.toString( ) para generar una representación imprimible de una matri z, pero los contenedores
se imprimen adecuadamente sin ninguna medida especial. He aquí un ejemplo que también nos va a pennitir presentar los
contenedores básicos de Java:
11: holding/PrintingContaine r s.java
II Los contenedores se imprimen automáticamente.
import java.util. * ;
import static net.mindview . util.Print.*¡
1* Output:
[rat, cat, dog, dog]
[rat, cat, dog, dog]
[dog, cat, rat]
[cat, dog, rat]
[rat, cat, dog]
{dog~S pot, cat=Rags, rat~Fuzzy}
{cat ~Rags, dog=Spot, rat~Fuzzy}
{rat~ Fuzzy, cat~Rags, dog=Spot}
*///,-
Este ejemplo muestra las dos categorías principales en la biblioteca de contenedores de Java. La distinción entre ambas se
basa en el número de elementos que se almacenan en cada "posición" del contenedor. La categoría Collection sólo almace-
248 Piensa en Java
na un e lemento en cada posición; esta categoría incluye el objeto List, que almacena un gnlpo de elementos en una secuen.
cia especificada, el objeto Set, que no pemlite ailadir un elemento idéntico a otro que ya se encuentre dentro del conjunto y
el objeto Queue. que sólo pennite insertar objetos en un "extremo" del contenedor y extraer los objclOs del arra "extremo"
(en lo que a este ejemplo respecta se trataría simplemente de otra forma de manipu lar una secuenc ia, por lo que no lo hemos
incluido). Un objeto Map, por su parte, almacena dos objetos, una clave y un valor asociado, en cada posición.
A la salida, podemos ver que el comportamiento de impresión predeterminado (que se implementa mediante el método
toString() de cada contenedor) genera resultados razonablemente legibles. Una colección se imprime rodeada de corche.
tes, estando cada elemento separado por una coma. Un mapa estará rodeado de llaves, asociándose las claves y los valores
mediante un signo igual (las claves a la izquierda y los va lores a la derecha).
El primer método fill() funciona con todos los tipos de colección, cada uno de los cuales se encarga de implementar el meto.
do add() para incluir nuevos elementos.
ArrayL ist y LinkedList son tipos de listas y, como puede ver a la salida, ambos almacenan los elementos en el mi smo orden
en el que fueron insertados. La diferencia entre estos dos tipos de objeto no está sólo en la ve locidad de ciertos tipos de ope.
raciones. sino también en que la clase LinkedList contiene más operaciones que ArrayList. Hablaremos más en detalle de
estas operaciones posterionnente en el capítulo.
HashSet, TreeSet y Linkedl-lashSet son tipos de conjuntos. La salida muestra que los objetos Set sólo pemliten almace-
nar una copia de cada elemento (no se puede introducir dos veces el mismo elemento), pero tamb ién muestra que las dife·
rentes implementaciones de Set almacenan los elementos de manera distinta. HashSet almacena los e lementos utilizando
una técnica bastante compleja que ana li zaremos en el Capítulo 17, Análisis detallado de los cOlllenedores, lo único que nece·
sitamos saber en este momento es que dicha técnica representa la fonlla más rápida de extraer elementos y, como resultado,
el orden de almacenamiento puede parecer bastante extraño (a menudo, lo único que nos preocupa es si un cierto objeto
fom13 parte de un co njunto, y no el orden en que aparecen los objetos). Si el orden de almacenamiento fuera impar·
tante, podemos utilizar un objeto TreeSet, que mantiene los objetos en orden de comparación ascendente, o un objeto
LinkedHashSet, que mantiene los objetos en el orden en que fueron "'adidos.
Un mapa (también denominado matriz asociativa) pennite buscar un objeto utilizando una clave, como si fuera una base de
datos simple. El objeto asociado se denomina va/Dr. Si tenemos un mapa que asocia los estados ameri canos con sus capita-
les y queremos saber la capital de Ohio, podemos buscarla utilizando "Ohio" como clave, de fonna muy similar al proceso
de acceso a una matriz uti li za ndo un índice. Debido a este comportamiento, un objeto mapa sólo admite una co pia de cada
clave (no se puede introducir dos veces la misma clave).
Map.put(key, val"e) añade un valor (el elemento deseado) y lo asocia con una clave (aquello con lo que buscaremos el
elemento). Map.get(key) devuelve el va lor asociado con una clave. En el ejemplo anterior sólo se añaden parejas de clave-
valor, sin real izar ninguna búsqueda. Ilustraremos el proceso de búsqueda más ade lante.
Observe que no es necesario especificar (n i preocuparse por ello) el tamaño del mapa, porque éste cambia de tamaño auto-
máticamente. Asimismo, los mapas sabe n cómo imprimirse, mostrando la asociación existente entre claves y valores. En el
orden en que se malllienen las claves y va lores dentro de un objeto Map no es e l orden de inserción, porque la implemen·
lación HashMap utiliza un algoritmo muy rápido que se encarga de controlar dicho orden.
En el ejemplo se utilizan las tres versiones básicas de Map: HashMap, TreeMap y LinkedHashMap. Al igual que
Has hSet, HashMap proporciona la técnica de búsqueda más rápida, no almacenando los elementos en ningún orden apa-
rente. Un objeto Treel\1ap mantiene las claves en un orden de comparación ascendente, mientras que LinkedHashMap tiene
las claves en orden de inserción sin deja r, por ello, de ser igual de rápido que HashMap a la hora de realizar búsquedas.
Ejercicio 4: (3) Cree una clase generadora que devuelva nombres de personajes (como objetos String) de su pelicula
favo rita cada vez que invoque next( ), y que vue lva al principio de la lista de personajes una vez que haya
acabado con todos los nombres. Uti lice este generador para rellenar una matriz, un ArrayList, un
LinkedList, un HashSet, un LinkedHashSet y un TreeSet, y luego imprima cada contenedor.
List
Las listas garantizan que los elementos se mantengan en una secuenci a concreta. La interfaz List añade varios métodos a
Collection que penniten la inse rción y la eliminación de elementos en mitad de una lista.
11 Almacenamiento de objetos 249
/ * OUtput:
1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug)
2: (Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Hamster]
3, true
4, Cymric 2
5, -1
6, false
7, true
B, [Rat, Manx, Mutt, Pug, Cymric, Pug]
9, [Rat, Manx, Mutt, Mouse, Pug, Cymric, Pug]
subList: [Manx, Mutt, Mouse)
10: true
sorted subList: [Manx, Mouse, Mutt]
11: true
shuffled subList: [Mouse, Manx, Mutt1
12: true
sub: (Mouse, Pug]
13, [Mouse, Pug]
14, [Rat, Mouse, Mutt, Pug, Cymric, Pug1
15, [Rat, Mutt, Cymric, Pug)
16, [Rat, Mouse, Cymric, Pug}
17, [Rat, Mouse, Mouse, Pug, Cymric, PugJ
lB, false
19, []
20, true
21, [Manx, Cymric, Rat, EgyptianMau]
22: EgyptianMau
23, 14
* ///,-
Hemos numerado las lineas de impresión para poder establecer la relación de la salida con el código fuente. La primera línea
de salida muestra la lista original de objetos Peto A diferencia de las matrices. un objeto List pennite añadir o eliminar ele-
mentos después de haberlo creado y el objeto cambia automáticamente de tamaño. Ésa es su característica fundamental: se
trata de una secuencia modificable. En la linea de salida 2 podemos ver el resultado de arladir un objeto Hamster. El obje-
to se ha añadido al final de la lista.
Podemos averiguar si un objeto se encuentra dentro de la lista utilizando el método contains(). Si queremos eliminar un
objeto. podemos pasar la referencia a dicho objeto al método remove( j. Asimismo, si disponemos de una referencia a
un objeto, podemos ver en qué número de indice está al macenado ese objeto dentro de la lista utili zando indexOf( j, como
puede verse en la linea de sa lida 4.
A la hora de determinar si un elemento fonlla parte de una lista, a la hora de descubrir el índice de un elemento y a la hora
de eliminar un elemento de una lista a partir de su referencia, se utiliza el método equals() (que forma parte de la clase raíz
Objectj. Cada objeto Pet se define como un objeto único, por lo que, aunque ex istan dos objetos Cymric en la li sta, si crea-
mos un nue vo objeto Cym ri c y lo pasamos a indexOf( j, el resultado será -1 (indicando que no se ha encontrando el obje-
11 Almacenamiento de objetos 251
10): asimismo. si tratamos de eliminar el ObjclO con remove( ), e l va lor devuelto será false . Para otras clases, el método
equals( ) puede esta r definido de forma diferente; dos objetos String. por ejemplo, serán iguales si los co ntenidos de las
cadenas de ca racteres son idénti cos. Así que, para evi tarnos sorpresas, es importante ser consc iente de que e l co mportami en-
to de un objeto List va ría dependiendo del comportamie nt o del método equals().
En las líneas de sa lida 7 y 8, podemos ver que se puede eliminar perfec tamente un objeto que se corresponda exactamente
con otro obje to de la lista.
También resulta posible insertar un elemento en mitad de la lista , C0l110 puede verse en la línea de salida 9 y en el código
que la precede. pero esta operación nos pemlite resaltar un potencial problema de rendimi ento: para un objeto LinkedList,
la inserción y e liminación en mitad de una lista son operaciones muy poco costosas (salvo por, en este caso, e l propio acce-
so aleatorio en mitad de la lista), mientras que para un obje to ArrayList se trata de una operación bastante cos tosa. ¿Quiere
esto decir que nunca deberíamos insertar elementos en mitad de una lista ArrayList, y que por el contrario. deberíamos
emplear un objeto LinkedList en caso de tener que llevar a cabo esa operación? De ninguna manera: simplemente signi fi-
ca que debemos tener en cuenta el potencial problema, y que si co menzamos a hacer numerosas inserciones en mitad de un
objeto ArrayList y nuestro programa comien=a a ralenti:orse, podernos sospechar que el posible culpable es la implemen-
tación de la lista concreta que hemos elegido (la mejor fomla de descubrir uno de estos cuellos de botella, como podrá ver
en el suplemento http://MindVielt:net/ Books/BetterJO\'lI, co nsis te en utili zar un perfilador). El de la optimización es un pro-
blema bastante complicado, y lo mejor es no preocuparse por é l hasta que veamos que es abso lutamente necesario (au nque
comprender los posibles problemas siempre resulta útil).
El método subList() pem1Íte crear fácilmente una sublista a partir de otra lista de mayor tamaño, y esto produce de fonna
natural un resultado true cuando se pasa la subli sta a containsAIl( ) para ver si los elementos se encuentran en esa lista de
mayor lamal10. También merece la pena recalcar que e l orden no es importante: puede ver en las líneas de salida 11 y 12
que al invocar los métodos CoUections.sort() y Collections,shuftle() (que ordenan y a leatorizan, respecti vamente, los ele-
mentos) con la subli sta sub , el resultado de containsAII() no se ve afectado. subList() genera una lista respaldada por la
lista origi nal. Por tanto, los cambios efectuados en la lista devue lta se verán reflejados en la lista origi nal, y viceversa.
El método retainAII() es, en la práctica, una operaci ón de " intersección de conjuntos", que en este caso conserva todos los
elementos de copy que se encuentren tambi én en sub. De nuevo, e l comportamiento resultante dependerá del método
equals( ).
La línea de salida 14 muestra el resultado de eliminar un e lemento utili zando su número índice, lo cual resulta bastante más
directo que elimi narl o mediante la referencia al objelO, ya que no es necesario preocuparse acerca del comportamiento de
equals( ) cuando se utilizan índices.
El método removeAII() también opera de manera distinta dependiendo del método equals(). Como su propio nombre
sugiere. se encarga de elimi nar de la Lista todos los objetos que estén en e l argumento de tipo List.
El nombre del método set( ) no resulta muy adec uado, debido a la posible confusión con la clase Set, un mejor nombre
habría sido "replace" (sustituir) porque este método se encarga de sustituir e l elemento situado en el índice indicado (e l pri-
mer argumento) con el segundo argumento.
La línea de salida 17 muestra que, para las listas. existe un método addAII() sob recargado que nos permite insertar la nueva
lista en mitad de la lista ori gi nal , en lugar de limitamos a añadirla al final con el método addAII() incluido en Collection .
Las lineas de salida 18-20 muestran el efecto de los métodos isEmpty( ) y eloar( ).
Las líneas de salida 22 y 23 muestran cómo puede conve rtirse cualquier objeto Collection en una matri z utili za ndo
tOArray( ). Se trata de un método sobrecargado, la versión sin argu mentos devuelve una matriz de Object. pero si se pasa
una matriz del tipo de destino a la versión sobrecargada, se generará una matri z del tipo especificado (suponi endo que los
mecanismos de comprobac ión de tipos no detecten ningún error). Si la matriz utilizada como argumento resulta demasiado
pequeña pa ra al macenar todos los objetos de la lista List (como sucede en este ejemplo), toArray() crea rá un a nueva matriz
del tamaI10 apropiado. Los objetos Pet ti enen un método id(), pudiendo ver en el ejemplo cómo se invoca di cho método
para uno de los objetos de la matriz resultante.
Ejercicio 5; Modifique ListFeatures.java para luili zar enteros (recuerde la característica de o/iloboxing) en lugar de
objetos Pet, y explique las diferencias que haya en los resultados.
Ejercicio 6; (2) Modifique ListFeatures,java para utili zar cadenas de caracteres en lugar de objetos Pet, y explique
las diferencias que haya en los resultados.
252 Piensa en Java
Ejercicio 7: (3) Cree una clase y construya luego una matriz inicializada de objetos de dicha clase. Rellene una liSta a
partir de la matriz. Cree un subconj unto de la lista uti lizando subList(), y luego e limine dicho Subco njun.
to de la lista.
Iterator
En cualquier conte nedor. tenemos que tene r una forma de inse rtar elementos y de vo lver a ex traerlos. Después de todo, esa
es la función princ ipal de un contenedor: almacenar cosas. En una lista, add( ) es una de las fo rm as de inse rtar elementos y
gct( ) es una de las fonnas de extraerlos.
Si queremos razonar a UD nivel más a lto, nos encontramos con un prob lema: necesitamos desarrollar el programa con el tipo
exacto de contenedor para poder utilizarlo. Esto puede parecer una desventaja a primera vista. pero ¿q ué sucede si escribi.
mas código para una lista y posterionnente descub rim os que sería conveniente ap licar ese mismo código a un conjunto?
Suponga que quisiéramos escribir desde el principio, un fragmento de código de propósi to general, que no supiera con que
tipo de contenedor está trabajando, para poderlo ut il izar con diferentes tipos de contenedores sin reescribir dicho código.
¿Cómo podríamos hacer esto?
Podemos util izar el concepto de Iterador (otro patrón de di seño) para conseguir este grado de abstracción. Un iterador es
un objeto cuyo trabajo consiste en desplazarse a través de una secuencia y seleccionar cada UIlO de los objetos que la como
ponen, sin que el programa cliente tenga que preocuparse acerca de la estmctura subyacente a dicha sec uencia. Además, un
iterador es lo que usualmente se denomina un objeto ligero: un objeto que resulta barato crear. Por esa ra zón, a menudo nos
encont ramos con rest ricciones aparentemente ex trañas que afectan a los iterado res; por ejemplo, un objeto Iterator de Java
sólo se puede desp lazar en una dirección. No so n muchas las cosas que podemos hacer con un objeto Iterator salvo:
1. Pedi r a una colección que nos devuelva un iterador utili zando un método iterator(). Dicho iterador estará pre-
parado para devolver el primer elemento de la secuencia.
2. Obtener el siguiente objeto de la secuencia mediante nexl().
3. Ver si hay más objetos en la secuencia utilizando e l método hasNexl().
4. Eliminar el último elemento devuelto por el ilerador mediante remove( ).
Para ver cómo funciona, podemos volver a utilizar las herramientas de la clase Pet que hemos tomado prestadas del Capitulo
14, Información de tipos.
11 : holding / Simplelteration.java
import typeinfo.pets.*;
import java.util.*;
System.out.println() ;
1I Un enfoque más simple, siempre que sea posible:
for {Pet p : pets)
System . out . print (p. id ( l + It: ti + P + "l ;
System.out.println() ;
I1 Un iterador también permite eliminar elementos:
it = pets.iterator {);
for(int i = Di i < 6; i++l {
it.next() ;
it.remove() ;
System.out .println{pets) i
11 Almacenamiento de objetos 253
/* Output:
O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 8:Cymric 9:Rat lO:EgyptianMau
11:Hamster
O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 8:Cymric 9:Rat lO:EgyptianMau
11 :Hamster
[Pug, Manx, Cymric, Rat, EgyptianMau, Hamster]
* // /,-
Con un objeto Iterator, no necesitamos preocuparnos acerca del número de elementos que haya en el contenedor. Los méto-
dos hasNoxt( ) y next() se encargan de dicha tarea por nosotros.
Si simplemente nos estamos desplazando hacia adelante por la lista y no pretendemos modificar el propio objeto List, la
sintaxis/oreach resulta más sucinta.
Un ¡teTador pennite también eliminar el último elemento generado por next( ), lo que quiere decir que es necesario invocar
a next( ) antes de llamar a remove( ).~
Esta idea de toma r un contenedor de objetos y recorrerlo para realizar una cierta operación con cada uno resulta muy poten-
te y haremos un extenso uso de ella a lo largo de todo el libro.
Ahora consideremos la creación de un metodo display() que sea neutral con respecto al contenedor utilizado:
11 : holding/CrossContainerlteration.java
import typeinfo.pets.*¡
import java . util .*¡
System.out.println () i
1* Output:
O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
4: Pug 6:Pug 3:Mutt l:Manx 5:Cymri c 7:Manx 2:Cymric O:Rat
5 :Cymric 2:Cymric 7:Manx l:Manx 3:Mutt 6:Pug 4:Pug O:Rat
* /// ,-
Observe que display() no contiene ninguna infonnación acerca del tipo de secuencia que está recorriendo. lo cual nos mues-
tra la verdadera potencia del objeto 1tcrator: la capacidad de separar la operación de recorrer una secuencia de la estructu-
ra subyacente de recorrer dicha secuencia. Por esta razón, decimos en ocasiones que los ¡teradores unifican el acceso a los
contenedores.
4 remo\C() es un método "opciona l" (existen otros métodos también opcionales), lo que significa que no todas las implementaciones de It era lor deben
implementarlo. Este tema se trata en el Capitulo 17, Alláli.~is detallado de los cOIl/elledo/'es. Los contenedores de la biblioteca estándar de Java si imple-
mentan cl método rcmove{ l. por lo que no es necesario preocuparse de este lema hasta que lleguemos a este capitulo.
254 Piensa en Java
Ejercicio 8 : (1) Modifique el Ejercicio l para que utilice un iterador para recorrer la lista mientras se invoca hop().
Ejercicio 9 : (4) Modifique innercl asses/Sequ cnce.j ava para que Sequ ence funcione con un objeto !terator en lugar
de un objeto Selector.
Ejercicio 10: (2) Modifique el Ejercicio 9 del Capitulo 8, PolimO/jismo para utili zar un objeto Ar r ayList para almace_
nar los objetos Rod ent y un iterador para recorrer la secuencia de objetos Rodent.
Ejercicio 11: (2) Escriba un método que uti lice un iterador para recorrer una colección e imprima el resultado de
toStrin g() para cada objeto del contenedor. Rellene lodos los diferentes tipos de colecciones con una serie
de objetos y aplique el método que haya diseliado a cada contenedor.
Listlterator
ListIterator es un subtipo más potente de Iterator que sólo se genera mediante las clases List. Mientras que Iterator sólo
se puede desplazar hacia adelante, Listlterator es bidireccional. También puede generar los índices de los elementos
siguiente y anterior, en re lación al lugar de la lista hacia el que el ¡terador está apuntando, permite sustituir el último ele·
mento listado uti lizando el método set( ). Podemos generar un iterador Listlterator que apun te al principio de la lista iuvo·
cando listIterator(), y también podemos crear un iterador Listlterator que com ience apuntando a un índice n de la lista
invocando Iistlterator(n). He aquí un ejemplo que ilustra estas capacidades:
// : holding/Listlteration.java
import typeinfo.pets. * ¡
import java.util. * ;
System.out.println(pets)¡
1* Output:
Rat, 1, O¡ Manx, 2, 1; Cymric, 3, 2¡ Mutt, 4, 3¡ Pug, 5, 4¡
Cymric, 6, 5; Pug, 7, 6¡ Manx, S, 7;
76543210
[Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx)
[Rat, Manx, Cymr ic, Cymric, Rat, EgyptianMau, Hamster, EgyptianMau]
El método Pets.randomPet() se utili za para sustimir todos los objetos Pet de la lista desde la posición 3 en adelante.
Ejercicio 12: (3) Cree y rellene un objeto List<Integer>. Cree un segundo objeto List<In teger> del mismo tamaño que
el primero y utilice sendos objetos Listlterator para leer los elementos de la primera lista e insertarlos en
la segu nda en orden inve rso (pruebe a explorar varias fonnas distintas de resolve r este problema).
11 Almacenamienlo de objetos 255
LinkedList
LinkedList también implementa la interfaz List básica, como ArrayList, pero realiza ciertas operaciones de famla más efi-
ciente que Ar ra yL ist (la inserción y la eliminación en la mitad de la li sta). A la inversa, resulta menos eficiente para las
operaciones de acceso aleatorio.
LinkcdList también incluye métodos que pemliten lIsa r este tipo de objetos como una pila, como una cola o como una cola
bidireccional.
Algunos de estos métodos son alias o li geramente variantes de los otros, con el fin de disponer de nombres que resulten más
familiares dentro del contexto de un uso específico (en particular en el caso de los objetos Queue, que se lIsan para imple-
mentar colas). Por ejemplo, getFirst() y element() son idénticos: devuelven la cabecera (primer elememo) de la lista sin
eliminarlo Y generan la excepción NoSuchElementException si la lista está vaCÍa. peek() es una variante de estos méto-
dos que devuelve nuJl si la lista está vacía.
rem oveFi rst() y remove() también son idénticos: eliminan y devuel ven la cabecera de la lista , generando la excepción
NoSuchE lement Excc ption para una lista vacía; poll() es una variante que devuelve nuJl si la lista está vacía.
addFirst() inserta un elemento al principio de la lista.
offer() es el mismo método que add () y addLast(). Todos ellos añaden un elemento al final de la lista.
rem oveLas t() e limina y devuelve el último elemento de la lista.
He aquí un ejemplo que muestra las similitudes y diferencias básicas entre todas estas funcionalidades. Este ejemplo no repi-
te aquellos comportamientos que ya han sido ilustrados en ListFcatures.java:
1/: holding/LinkedListFeatures.java
impor t typeinfo.pets.*¡
import java.util.*¡
import static net.mindview.util.Print.*¡
pets.addLast(new Hamster(}} i
print("After addLast(}: It + petsl;
print ("pets. removeLast () : 11 + pets. removeLast () ) ;
1* Output:
[Rat, Manx, Cymric, Mutt, Pug)
pets.getFirst(): Rat
256 Piensa en Java
pets.element(): Rat
pets.peek(): Rat
pets.remove(): Rat
pets.removeFirst(): Manx
pets.poll(): Cymric
[Mutt, PugJ
After addFirst (): (Rat, Mutt, PugJ
After offer(): [Rat, Mutt, Pug, CymricJ
After add(): [Rat, Mutt, Pug, Cymric, Pug]
After addLast(): [Rat, Mutt, Pug, Cymric, Pug, HamsterJ
pets.removeLast(): Hamster
* ///>
El resu ltado de Pets .• rr.yList( ) se entrega al constructor de LinkedList con el fID de rellenar la lista enlazada. Si analiza
la interfaz Queue, podrá ver los métodos element(), offer(), peek( l, poll() y remove() que han sido aüadidos a
LinkedList para poder disponer de la implementación de una cola. Más ade lante en el capítulo se incluyen ejemplos Com-
pletos del manejo de colas.
Ejercicio 13: (3) En el ejemplo innerclasscs/GreenhouseController.java, la clase Controllcr utiliza un objeto
ArrayList. Cambie el código para util izar en su lugar un objeto LinkedList y emp lee un iterador para
recorrer el co njunto de sucesos.
Ejercicio 14: (3) Cree un objeto vacío LinkedList< lnteger>. Ut ilizando un iterador ListIterator. añada valores ente-
ros a la lista insel1ándolos siempre en mitad de la misma.
Stack
Una pila (sTack) se denomina en ocasiones "contenedor de tipo LLFO" (Iasl-in,first-o lll, el último en en trar es el primero en
salir). El último elemento que pongamos en la "parte superior" de la pila será el primero que tengamos que sacar de la
misma, como si se tratara de una pila de platos en una cafetería.
LinkedList tiene métodos que implementan de fonna directa la funcionalidad de pila, por lo que también podríamos usar
una lista enlazada LinkcdList en lugar de definir una clase con las característ icas de una pila. Sin embargo, definir una clase
a propósito permite en ocasiones clarificar las cosas:
11: net/mindview/util/Stack . java
II Definición de una pila a partir de una lista enlazada.
package net.mindview.util;
import java.util . LinkedList;
He aquí una se ncilla demos trac ión de esta nueva clase St~lck :
1/: holding/StackTest.java
import net.mindview.util.*¡
/ * Output :
fleas has dog My
, /// ,-
Si quiere utilizar esta clase Stack en su propio código, tendrá que especificar completamente el paquete (o ca mbiar el nOI11-
bre de la clase) cuando cree una pila: en caso contrario, probablemente entre en colisión con la clase Stack del paquete
java.lItil. Por ejemplo, si importamos java.util.* en e l ejemplo anterior. deberemos usar los nombres de los paquetes para
evitar las colisiones.
11: holding/StackCollision . java
import net.mindview . util.*¡
1* Output:
fleas has dog My
f leas has dog My
' / / / >
Las dos clases Stack tienen la misma interfaz. pero no existe ninguna interfaz común Stack en java.util, probable mente
porque la clase original java.utiI.Stack. que es taba di señada de una fonna inadecuada, ya tenía ocu pado el nombre. Aunque
java.utilStack existe, LinkedList permite obtener una clase Stack mejor, por lo que resulta preferible la técnica basada en
net.mindview.utiI.Stack.
Tamb ién podemos controlar la selección de la implementación Stack Hpreferida" utili zando una instmcción de importación
explícita:
import net.mindview.util.Stack¡
Ahora cualquier referenci a a Stack hará que se selecc ione la versión de net.mindview.util , mientras que para seleccionar
java.util.Stack es necesario emplear una cualificación completa.
Ejercicio 15: (4) Las pilas se utilizan a menudo para evaluar expresiones en lenguajes de programación. Utili zando
net.mindview.utiI.Stack, evalúe la siguiente expresión, donde '+' significa "introducir la letra siguiente
en la pila" mientras que '-' significa " extraer la parte superior de la fila e imprimirlo":
"+U+n+c---+e+r+t---+a-+i-+n+t+y---+ -+r+u--+I+e+s---"
258 Piensa en Java
Set
Los objetos de tipo Set (conjuntos) no penniten almacenar más de una instancia de cada objeto. Si tratamos de añadir más
de una instancia de un mismo objeto, Set impide la duplicación. El uso más común de Set cons iste en comprobar la pene~
llencia, para poder preguntar de una manera sencilla si un detenninado objeto se encuentra dentro de un conjunto. Debido
a esto, la operación más importante de un conjunto suele ser la de búsqueda, así que resulta habinJaI seleccionar la imple~
mentación HashSet, que está optimizada para realizar búsquedas rápidamente.
Set tiene la misma interfaz que Collection , por lo que no existe ninguna funcionalidad adicional, a diferencia de los dos
tipos distintos de List. En lugar de ello, Set es exactamente un objeto ColleetioD, salvo porque tiene un comportamiento
distinto (éste es un ejemplo ideal del uso de los mecanismos de herencia y de polimorfismo: penniten expresar diferentes
comportamientos). Un objeto Set detemlina la pertenencia basándose en el "valor" de un objeto, lo cual constituye un tema
relativamente complejo del que hablaremos en el Capítulo 17, Análisis detallado de los conrenedores.
He aquí un ejemplo que utiliza un conjunto HashSet con objetos Integer:
11: holding/SetOflnteger.java
import java . util. *i
1* Out put:
[15, 8, 23, 16, 7, 22, 9, 21, 6, 1, 29, 14, 24, 4, 19, 26, 11, 18, 3, 12, 27, 17, 2, 13,
28, 20, 25, 10, 5, O]
,/ // ,-
En el ejemplo, se añaden diez mil números aleatorios entre O y 29 al conjunto, por lo que cabe imaginar que cada valor ten-
drá muchos duplicados. A pesar de ello, podemos ver que sólo aparece una instancia de cada valor en los resultados.
Observará también que la salida no tiene ningún orden específico. Esto se debe a que HashSet utiliza el mecanismo de hash
para ace lerar las operaciones; este mecanismo se analiza en detalle en el Capítulo 17, Análisis detallado de los contenedo-
res. El orden mantenido por un conjunto HashSet es diferente del que se mantiene en un TreeSet o en un LinkedHashSet,
ya que cada implementación almacena los elementos de forma distinta. TreeSet mantiene los elementos ordenados en una
estructura de datos de tipo de árbol rojo-negro, mientras que HashSet utiliza una función de hasll. LinkedHashSet también
emplea una función hash para acelerar las búsquedas, pero parece mantener los elementos en orden de inserción utilizando
una lista enlazada.
Si queremos que los resultados estén ordenados, una posible técnica consiste en utilizar un conjunto TreeSet en lugar de
HasbSet:
11: holding/SortedSetOflnteger.java
import java.util. *;
1* Output:
[O, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1 0, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
, /// ,-
11 Almacenamiento de objetos 259
Una de las operaciones más comunes que tendremos que realizar es comprobar la pertenencia al conjunto de un determina-
do miembro usando contains( ). pero hay otras operaciones que nos recuerdan a los diagramas Venn que enseñan en el cole-
gio:
1/ : holdingfSetOperations.java
import java.util. * ;
import static net.mindview.util.Print.*
/ * Output :
H: true
N: false
set2 in setl: true
seU, ID, K, e, B, L, G, 1, M, A, F, J, El
set2 in setl: false
set2 removed from setl: [D, C, a, G, M, A, F, El
'X y Z' added to setl: [Z, D, C, a, G, M, A, F, Y, X, El
*1110-
Los nombres de los métodos resultan bastante descriptivos, y existen unos cuantos métodos adicionales que podrá encon-
trar en el JDK.
Generar ulla lista de elementos diferentes puede resultar muy útil en detenninadas ocasiones. Por ejemplo, suponga que qui-
siera enumerar todas las palabras contenidas en el archivo SetOperations.java anterior. Con la utilidad net.mindview.
TextFile que presentaremos más adelante en el libro, podremos abrir un archivo y almacenar su contenido en un objeto Set:
11: holding/UniqueWords.java
import java.util .*;
import net.mindview.util.*;
1* Output:
(A, S, e, eollections, D, E, F, G, H, HashSet, I, J, K, L, M, N, Output, Print, Set,
SetOperations, String, X, Y, Z, add, addAII, added, args, class, contains, containsAll,
false, frem, holding, impert, in, java, main, mindview, net, new, print, public, remove,
removeAll , removed, set!, set2, split, static, to, true, util, voidl
* 111 ,-
260 Piensa en Java
TextFile hereda de List<Strin g>. El constnlctor de TextFile abre el arch ivo y lo descompone en palabras de acuerdo Con
la expresión regular "\\ W+", que significa " una o más letras" (las expresiones regulares se presentan en el Capítulo !J.
Cadenas de caracteres). El resultado se entrega al constructor de TreeSet. que aii.ade el contenido del objelO List al conjun.
lo. Puesto que se lIata de un objeto TreeSet. el resultado está ordenado. En este caso, la reordenación se realiza /exicogra.
.ficomente de modo que las letras mayúsculas y minúsculas se encuentran en gnlpos separados. Si desea reali zar una
ordenación alfa bética, puede pasar el comparador Slring,CASE_INSENS ITIVE_ORDER (un comparador es un objeto
que establece un orden) al constructor TreeSet:
/1 : holding / UniqueWordsAlphabetic . java
11 Generación de un listado alf a bético.
import java . util .* ;
import net . mindview.util .* ;
1* Output :
(A, add, addAll, added, args, B, C, c l ass, Coll ections, conta i ns, containsAll, D, E, F,
f alse, f rom , G, H, HashS e t, holdi ng, I , i mpor t , i n , J , java, K, L, M, ma i n, mi n d view, N,
net, new, Output, Pr int, public, remove, removeAll, removed , Set, set1 , set2,
SetOperations, split, static, String , to, true , util, void, X, Y, Z)
* /// ,-
Los comparadores se anali zarán en el Capítulo 16, Mafl·;ces.
Ejercicio 16: (5) Cree un obj eto Sel con todas las vocales, Ut il izando el archivo UniqueWords,java, cuente y muestre
el número de vocales en cada palabra de entrada, y muestre también el número total de vocales en el archi·
va de entrada.
Map
La posibilidad de estab lecer correspondencias entre unos objetos y otros puede ser enonnemente potente a la hora de resol·
ver ciertos problemas de programación. Por ejemplo, consideremos un programa que pemlite examinar la aleator iedad de
la clase Random. Idealmente. Random debería producir la distribución perfectamente aleatoria de números, pero para com·
probar si es to es así debería generar numerosos números aleatorios y llevar la cuenta de cuáles caen dentro de cada uno de
los rangos definidos. Un obj eto Map nos pennite resolver fác ilmente el problema ; en este caso, la clase será el número ge ne·
rada por Random y el va lor será el número de veces que ese número ha aparecido:
1/ : holding / Statistics . java
11 Ejemplo simple de HashMap .
import java.util . *;
/ * Output:
{15=497, 4=481, 19=464, 8=468, 11=531, 16=533, 18=478, 3=508, 7=471, 12=521, 17=509,
2=489, 13=506, 9=549, 6=519, 1=502, 14=477, 10=513, 5=503, 0 =481)
' 111 ,-
En main(), la característi ca de oUloboxing convie rte e l va lor int aleatoriamente generado en una referencia a Integer qu e
puede utilizarse con el mapa HashMap (no pueden utili zarse primiti vas en los contenedores). El método get() devuelve
null si la clave no se encuentra ya en el contenedor (lo que quiere decir que es la primera vez que se ha encontrado ese
número concreto) . En caso contrario, el método gct() devuelve el va lor Intcger asociado con esa clave. el cual tendremos
que incrementar (de nuevo, la ca racterística de auroboxing simplifica la ex presión, pero en la práctica se lleva n a cabo las
necesarias conversiones hacia y desde Integer).
He aquí un ejemplo que nos pennite utilizar la descripción de String para buscar objelOs Peto También nos muestra cómo
podemos comprobar si un determinado objelO Map contiene una c lave o un va lor utilizando los métodos containsKey( ) y
containsValue( ):
11 ,holding/PetMap.java
import typeinfo.pets.·¡
import java.util.·;
import static net.mindview.util.Print.*¡
/ * Output:
{My Cat=Cat Molly, My Hamster=Hamster Bosco, My Dog=Oog Ginger}
Dog Ginger
true
true
' 111 ,-
Los mapas, al igual que las matrices y las colecciones, pueden cxpa ndirse fácilmente para que sean multidimcnsionales:
basta con definir un obj cto Map c uyos va lores sean también mapas (y los va lores de esos olros mapas pueden se r, a su vez,
otros contenedores o inc luso otros mapas). Así, resulta bastante fácil combinar los contenedores para generar estmctliras de
datos muy potentes. Por ejem plo, suponga que queremos llevar una lista de personas que tien en múltiples masco tas, en ese
caso. lo único que necesitaremos es un objeto Map< Person, List< Pet»:
11 , holding / MapOfList.java
package holding;
import typeinfo.pets.*¡
import java.util. * ¡
import static net.mindview . util.Print. · ¡
/* Output:
People: [Person Luke, Person Marilyn, Person Isaac, Person Dawn, Person Kate]
Pets: [[Rat Fuzzy, Rat Fizzy], (Pug Louie aka Louis Snorkelstein Dupree, Cat Stanford aka
Stinky el Negro, Cat Pinkolal, [Rat Freckly], [Cymric MOlly, Mutt Spot] , [Cat Shackleton,
Cat Elsie May, 009 Margrett]]
Person Luke has:
Rat Fuzzy
Rat Fizzy
Person Marilyn has:
Pug Louie aka Louis Snorkelstein Dupree
Cat Stanford aka Stinky el Negro
Cat Pinkola
Pers o n Isaac has:
Rat Freckly
Person Dawn has:
Cymric Molly
Mutt Spot
Person Kate has:
Cat Shackleton
Cat Elsie May
Dog Margrett
* ///,-
Un mapa puede devo lver un objeto Set con sus claves, un objeto CoUection con sus valores o un objeto Set con las parejas
clave-valor que tiene almacenadas. El método keySct() genera un conjunto de todas las claves de petPeople, que se utili-
za en la instrucción foreach para iterar a través del mapa.
Ejercicio 17: (2) Tome la clase Gcrbil del Ejercicio I y cambie el ejemplo para incluirla en su lugar en un objeto Map,
asociando el nombre de cada objeto Gerbil (por ejemplo, "Fuzzy" o "Spot") como si fuera una cadena de
caracteres (la clave) de cada objeto Gerbil (el valor) que incluyamos en la tabla. Genere un iterador para
el conjunto de claves keySet() y utilicelo para desplazarse a lo largo del mapa, buscando el objeto Gerbil
correspondiente a cada clave, imprimiendo la clave e invocando el método hop() del objeto Gerbil.
Ejercicio 18: (3) Rellene un mapa HashMap con parejas clave-valor. Imprima los resultados para mostrar la ordena-
ción según el código hash. Ordene las parejas, extraiga la clave e introduzca e l resultado en un mapa
LinkedHashMap. Demuestre que se mantiene el orden de inserción.
Ejercicio 19: (2) Repita el ejercicio anterior con sendos conjuntos HashSet y LinkedHashSet.
Ejercicio 20: (3) Modifique el Ejercicio 16 para llevar la cuenta de cuántas veces ha aparecido cada vocal.
11 Almacenamiento de objetos 263
Eje rcici o 21 : (3) Utilizando Ma p<Strin g,ln teger >, siga el ejemplo de Uni qu eWords.java para crear un programa que
lleve la cuenta del número de apariciones de cada palabra en un archi vo. Ordene los resultados uti lizando
Collecti ons.so rt() proporcionando como segundo argumento Strin g.CASE_INSENS ITI VE_ O RD ER
(para obtener una ordenación alfabét ica), y muestre los resultados.
Ejercicio 22: (5) Modifique el ejercicio anterior para que ut il ice una clase que contenga un campo de tipo Stri ng y un
campo contador para almacenar cada una de las di ferentes palabras, así como un conjunto Set de estos
objetos con el fin de mantener la lista de palabras.
Ejercicio 23 : (4) Partiendo de Statistics.j a va, cree un programa que ejecute la prueba repetidamente y compru ebe si hay
algún número que tienda a aparecer más que los otros en los resultados .
Ejercicio 24: (2) Rellene un mapa LinkedH as hM a p con claves de tipo Strin g y objetos del tipo que prefiera. Ahora
extraiga las parejas, ordénelas según las claves y vue lva a insertarlas en el mapa.
Ejercicio 25 : (3) Cree un objeto M ap <String,A rrayL ist<lntcger». Uti lice net.mind view.Tex t File para abri r un
archivo de texto y lea el archivo de palabra en palabra (utilice " \\W+" como segundo argumento del cons-
tructor Te.tFile). Cuente las palabras a med ida que lee el archivo, y para cada palabra de un archivo, anote
en la matriz ArrayList<lnteger> el contador asociado con dicha palabra; esto es, en la práctica, la ubica-
ción dentro del archivo en la que encontró dicha palabra.
Ejercicio 26 : (4) Tome el mapa resultan te del ejercicio anterior y ordene de nuevo las palabras, tal como aparecían en
el archivo origina l.
Queue
Una cola (queue) es normalmente un contenedor de tipo FIFO (jirst-in,first-out, el primero en entrar es el primero en sa lir).
En otras palabras, lo que hacemos es insertar elementos por un o de los extremos y extraerlos por el otro, y el orden en que
insertemos los e lementos coincidirá con el orden en que estos serán extraídos. Las colas se utilizan comúnmente como un
mecanismo fiab le para transferir objetos desde un área de un programa a otro. Las colas son especialmente importantes en
la programación concurrente, corno veremos en el Capítulo 21, Concurrencia, porque permüen transferir objetos con segu-
ridad de una a otra ta rea.
LinkedList di spone de métodos para soportar el comportamiento de una cola e implementa la interfaz Q ueue, por lo que
un objeto Li nkcd List puede utilizarse como implementación de Q ueu • . Generalizando un objeto LinkedList a Q ueue, este
ejemplo uti liza los métodos específicos de gestión de colas de la interfaz Queue:
11: holding /QueueDemo .java
II Generalización de un objeto LinkedList a un objeto Queue .
import java.util. * ¡
1* Output:
264 Piensa en Java
8 1 1 1 5 14 3 1 O 1
B ron t o s a u r u s
* ///,-
offe . . () es uno de los métodos específicos de Queue; este método inserta un elemento al final de la cola, siempre que sea
posible. o bien devuehe el valor fa lse. Tanto peek() como element() devuelven la cabecera de la cola sin eliminarla, pero
peek() devuel ve null si la cola está vacia y element() genera NoSuch Element Exccption . Tanto poll() como remOVe()
eliminan y de vuelven la cabecera de la cola, pero poll() devue lve null si la cola está vacía, mientras que removc() genera
NoS ueh Element Exce plion.
La característica de allloboxil1g convierte automáticamente el resultado int de ne xtlnt() en el objeto Int ege r requerido por
q ueue. y el va lor ehar e en el objeto C har ae ter requerido por q c. La interfaz Queue limita el acceso a los métodos de
Li nkedList de modo que sólo están disponibles los métodos apropiados. con lo que estaremos menos tentados de utilizar
los métodos de LinkedList (aqui, podríamos proyectar de nuevo qu eue para obtener un objeto LinkedList, pero al menos
nos resultará bastante más complicado uti lizar esos métodos).
Observe que los métodos específicos de Queue propo rcionan una funcionalidad completa y autónoma. Es decir, podemos dis-
poner de una cola ut ilizable sin ninguno de los métodos que se encuentran en Collection . que es de donde se ha heredado.
Ejercicio 27: (2) Esc ri ba una clase denominada Command que contenga un objeto Strin g y que tenga un método ope-
r ati on( ) que imprima la cadena de caracteres. Escriba una segunda clase con un método que rellene un
objeto Queue con objetos Command y devuelva la cola rellena. Pase el objeto Queue relleno a un méto-
do de una tercera clase que consuma los objetos de la cola e invoque sus métodos oper ation ( ).
PriorityQueue
El mecanismo FIFO (Firsl-ill. firsl-ou t) describe la disciplina de gestión de colas más común. La disciplina de gestión de
colas es lo que decide. dado un grupo de elementos existentes en la cola. cuál es el que va a continuación. La disciplina
HFO dice que el siguiente elemento es aquel que haya estado esperando durante más tiempo.
Por el contrario. una cola COI1 prioridad implica que el elemento que va a continuación será aquel que tenga una necesidad
mayor (la prioridad más alta). Por ejemplo, en un aeropuerto, puede que un cliente que está en medio de la cola pase a ser
atendido inmed iatamente si su avión está a punto de salir. Si construimos un sistema de mensaje ría, algunos mensajes serán
más importantes que otros y será necesario tratar esos mensajes antes, independientemente de cuándo hayan llegado. El con-
tenedor Pr iori tyQ ueue ha sido ailadido en Java SES para proporcionar una implementación automática de este tipo de com-
portamiento.
Cuando ofrecemos. como método offer( ). un objeto a una cola de tipo Priorit)'Q ueue, dicho objeto se ordena dentro de la
cola. 5 El mecanismo de ordenación predetcnninado utili za el orden lIatural de los objetos de la cola, pero podemos modi-
ficar dicho elemento proporcionando nuestro propio objeto Compar ator. La clase Priori tyQ ueue garantiza que cuando se
invoquen los métodos peek(), poll( ) o ,'emove(), el elemen to que se obtenga será aquél que tenga la prioridad más alta.
Resulta trivial implementar una cola de tipo Priori tyQ ueue que funcione con tipos predefinidos como Illteger. Str ing o
C ha r aeter. En el siguiente ejemplo, el primer conjunto de valores son los valores aleatorios del ejemplo anterior. con lo
cual podemos ver cómo se los extrae de manera diferente de la cola PriorityQueue:
11 : holding/PriorityQueueDemo.java
impore java.util.*;
~ Esto depli!ndc. de hecho. de ta implemcntación. Lo:>. algoritmos de colas con prioridad suelcn realizar la ordenación durantc la inserción (mantC'ni~IllI\.'
una estnJctura de memoria quc se conoce con el nombre de cúmulo). pcro también puedc perfectamente seleccionarse el elemento más impol1ante C'n ~I
momento de la extracción . La elección del algoritmo podría tener su importancia si la prioridad de los objeto:>. puede modificarse mientra s estos C:>.tán ópe-
rando en la cola.
11 Almacenamiento de objetos 265
1* Output :
O 1 1 1 1 1 3 5 8 14
1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25
25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1
A A B e e e D D E E E F H H 1 1 L N N o o o o s s S T T U U U W
wU U U T T S S S o o o o N N L 1 1 H H F E E E D o e e e B A A
A B e o E F H 1 L N o S T U W
, /// ,-
Como puede ver, se permiten los duplicados. los valores menores tienen la prioridad más alta (en el caso de String.
los espacios también cuentan como va lores y tienen una prioridad superi or a la de las letras). Para ver cómo podemos
verificar la ordenac ión propo rcio nando nuestro propio objeto compa rador, la tercera llamada al co nS(nlclor de
PriorityQueue<lnteger> y la segunda llamada a PriorítyQueue<String> utilizan el comparador de orden inverso genera-
do por Collectiol1s,revcrseOrder( ) (añadido en Ja va SE5).
La última sección aiiadc un conjunto HashSet para el iminar los objetos Character duplicados, simplemente con el fin de
hacer las cosas un poco más interesantes.
Integer. String y Charader funcionan con PriorityQuclIc porque estas clases ya tienen un orden natural predefinido. Si
quere mos utilizar nuestra propia clase en una cola PriorityQueue. deberemos incluir una funcionalidad adicional para gene-
rar una ordenación natural. o bien proporcionar nuestro objeto Comparator. En el Capíhl lo 17. Análisis de/allado de los
cOllfenedores se proporciona un ejemplo más sofi sticado en el que se ilustra este mecani smo.
Ejercicio 28 : (2) Rellene la cola PriorityQueue (ut ili za ndo offer()) con va lores de tipo Double creados utilizando
java.utiI.Random. y luego elimine los elementos con poll( ) y visualícelos.
Ejercicio 29: (2) Cree una clase simple que herede de Object y que no contenga ningún nombre y demuestre qu e se
pueden aiiadir múltiples elementos de di cha clase a una cola PriorityQuclIe. Este tema se explicará en
detalle en el Capintlo 17. Análisis detallado de los contenedores.
266 Piensa en Java
System.out.println() ;
1* Output:
O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
4:Pug 6:Pug 3:Mutt l:Manx 5:Cymric 7:Manx 2:Cymríc O:Rat
fi Algunas personas defienden la creación automática de una interfaz para toda posible combi nación de métodos en una clase: en ocasiones, defienden que
se haga esto para lodas las clases. En mi opinión. una interfaz debería tener un sign ificado mayor que una mera duplicación mecánica de combi naciones
de métodos, por lo que suelo preferir esperar y ver qué valor añadiría una interfaz antes de crearla.
11 Almacenamiento de objetos 267
1* Output:
O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug S : Cymric 6:Pug 7:Manx
O:Rat l : Manx 2:Cymric 3 : Mutt 4 : Pug S:Cymric 6:Pug 7 : Manx
- /// , -
El método remo\'c() es una "operación opcional", de la que hablaremos más delante en el Capítulo 17, Análisis detallado de
los conrenedores. Aquí, no es necesa rio implementarlo y, si lo invocamos, generará una excepción.
En este eje mplo, podemos ver que si implementamos Collection. también implementamos iterator(); además, vemos que
implementar únicamente iterator() sólo requiere un esfuerzo ligeramente menor que heredar de AbstractCollection . Sin
embargo, si nuestra clase ya hereda de otra clase, no podemos heredar de AbstractCollcction. En tal caso, para implemen-
268 Piensa en Java
tar Collection sería necesario implementar todos los métodos de la inte rfaz. En este caso. resultaría mucho más senci llo
heredar y añadir la posibilidad de crear un ¡terador:
11 : ho lding / NonCollectionSequenee.java
import typeinfo.pets.*¡
import j ava.util.*;
class PetSequence
protected Pet(] pets Pets.createArray (8 ) ¡
};
1* Output:
O:Rat l:Manx 2:Cymric 3 :Mutt 4:Pug 5:Cymric 6 : Pug 7:Manx
*/ // ,-
Generar un objeto Iterator es la forma con acop lamiento más débil para conectar una secuencia con un método que consu-
ma esa secuencia; además, se imponen muchas menos restricciones a la clase correspondi ente a la sec uencia que si imple-
mentamos Collectio n.
Ejercicio 30: (5) Modifique CollectionSeq uence.j.va para que no herede de AbstractCollection , sino que implemen-
te Collection.
/ * Output:
' Take' 'the' ' long' 'way' 'home '
* /// ,-
11 Almacenamiento de objetos 269
Como es es una colección, este código dem uestra que todos los objetos Collectioll permiten emplear la estmcturajoreach.
La razón de que este método funcione es que en Java SES se ha introducido una nueva interfaz denominada Iterable que
contiene un método iterator( ) para generar un objeto Iterator. y la interfaz Iterable es 10 que la cstrucrura foreach utilí ·
za para desp lazarse a través de una secuencia. Por tanto, si creamos cualquier clase que implemente Iterable, dicha clase
podrá se r utilizada en un a instmcciónJoreach:
// : holding / lterableClass. j ava
/1 Anything Iterable works with foreach.
i mport java.util.*;
/ * Output:
And that is how we know the Earth to be banana-shaped.
* /// ,-
El método iterator() devuelve una instancia de una implementación interna anónima de Iterator<String> que dev uelve
cada palabra de la matri z. En main(), podemos ver que lterableClass funciona perfectamente en una instrucciónforeach.
En Java SES, hay varias clases que se han definido co mo Iterable, principalment e todas las clases de tipo Collection (pero
no las de tipo Map ). Por ejemplo, este códi go muestra todas las variables del entorno del sistema operativo:
// : holding / EnvironmentVariables. j ava
i mport java.util.*¡
7 Esto no estaba disponible antes de Java SE5. porque se pensaba que estaba acoplado de una manera demasiado estrecha con el sistema operativo, lo que
violaba la regla dc "cscribir los programas una vez y ejecutarlos en cualquier lugac'·. El hecho de que sc haya incluido ahora sugiere que los diseñadores
de Java han dccidido ser más pragmáticos.
270 Piensa en Java
1* Out put :
1 2 3 A B e
* /// , -
Al tratar de pasar una matriz como argumento de tipo Iter able el programa falla. No hay ningún tipo de conversión auto-
mática a Iterable; sino que debe realizarse de forma manual.
Ejercicio 3\: (3) Modifique polyrnorphism/shapelRa ndomShapeGenerator.java para hacerlo de tipo Iterable.
Tendrá que añadir un constructor que admita el número de elementos que queremos que el iterador gene-
re antes de pararse. Verifique que el programa funciona.
)
);
; * Output:
To be or not ta be
be ta not or be To
* /1/ ,-
,i nos limitamos a poner el objeto ral dentro de la instmcción/oreac/¡, obtenemos el iterador directo (predeterminado). Pero
i invocamos reverscd() sobre el objeto, el comportamiento será distinto.
Jtiliza ndo esta técnica, podemos añadir dos métodos adaptadores al ejemplo lterableClass.java :
/1: holding / MultilterableClass.java
/1 Adición de varios métodos adaptadores.
import java.util. *¡
System.out.println () i
for (String s mic.randomized (»
System.out.print (s + 11 11 ) i
System . out.println () i
f o r (String s : mie l
System.out.print (s + 11 11 ) i
/ * Output:
banana-shaped. be to Earth the know we how lS that And
is banana-shaped. Earth that how the be And we know to
And that lS how we know the Earth to be banana-shaped.
* /// ,-
Observe qu e el segundo método, random(). no crea su propio Iterator sino que se limita a devolver el correspondiente a
la lista List mezclada .
Puede ver a la salida que el método Collcctions.shuffle() no afecta a la matri z original, sino que sólo mezcla las referen-
cias en shuffled. Esto es así porque el método randomized() empaqueta en el nue vo objeto Arra)'List el res ultado de
Arrays.asList(). Si mezcláramos directamente la lista generada por Arrays.asList() se modificaría la matri z subyacente.
como puede ver a continuación :
/1 : holding / ModifyingArraysAsList.java
import java.util .* ¡
1* Output:
Before shuffling: (1, 2, 3, 4, 5, 6, 7, 8, 9, la]
After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9)
array: [1, 2 , 3, 4, 5, 6, 7, 8, 9, la]
Before shuffling: (1, 2, 3, 4, 5, 6, 7, 8, 9, la]
After shuffling: (9, 1, 6, 3 , 7, 2, 5, ID, 4, 8]
array' [9, 1, 6, 3, 7, 2, 5, 10, 4, 8)
* /// ,-
En el primer caso, la salida de Arrays,asList( j se entrega al constructor de ArrayList( j , y esto crea una lista ArrayList
que hace referencia a los elementos de ia. Mezclar estas referencias no modifica la matri z. Sin embargo, si uti li zamos direc-
tamente el resultado de Arrays.asList(ia), el mezclado moditica el orden de ia. Es importante tener en clIenta que
Arrays.asLis t() genera un objeto List que utili za la matri z subyacente corno su implememación fisica. Si hacemos algo a
ese objeto List que lo modifique y no queremos que la matri z original se vea afectada, entonces será necesario reali zar una
copia en otro contenedor.
Ejercicio 32: (2) Siguiendo el ejemplo de MultilterableClass, a"ada métodos reversed() y randomized() a
NonCollcctionSequencc,java. Haga también que No nCollectionSequence implemente Iterable y mues-
tre que las distin tas técnicas funcionan en las instnlcciones foreach.
11 Almacenamiento de objetos 273
Resumen
Java proporciona varias fannas de almacenar objetos:
1. Las matrices asocian índices numéricos con los objetos. Almacenan objetos de un tipo conoc ido, así que no es
necesario proyectar el resultado sobre ningún otro tipo a la hora de buscar un objeto. Pueden ser multidimcnsio-
nales y también almacenar tipos primitivos. Sin embargo, su tamaiio no puede modificarse después de haberlas
creado.
2. Las colecciones (Collection) almacenan elementos independientes, mientras que los mapas (Ma p ) almacenan
parejas asoc iadas. Utilizando los genéricos de Java, especificamos el tipo de objeto que hay que almacenar en los
contenedores, con el fin de no introduci r un tipo incorrecto en un contenedor y también para no tener que efec-
tuar una proyección de los elementos en el momento de extraerlos de un contenedor. Tanto las colecciones como
los mapas cambian automáticamente de tamaño a medida que añadimos más elementos. Los contenedores no per-
miten almacenar tipos primilivos, pero el mecanjsmo de autoboxillg se encarga de producir las primitivas a los
tipos envoltorio almacenados en el contenedor.
3. Al igual que una matriz, una lista (List) también asocia índices numéricos con objetos; por tanto, las matrices y
las listas son contenedores ordenados.
4. Ut ilice una lista ArrayList si tiene que realizar numerosos accesos aleato rios; pero, si lo que va a hacer es un
gra n número de inserciones y de borrados en mitad de la lista, utili ce LinkedList.
S. El comportamiento de las colas (Queue) y de las pilas se obtiene mediante LinkedList.
6. Un mapa (M ap ) es una fonna de asocia r los objetos no con valores enteros. sino con olros objelos. Los mapas
HashMap están diseñados para un acceso rápido, mientras que un mapa TreeMap mantiene sus claves ordena-
das y no es tan rápido como HashMap. Un mapa LinkedHashMap mantiene sus elementos en orden de inser-
ción, pero proporciona un acceso rápido con mecanismos de ¡lCIS/¡.
7. Los conjuntos (S el) sólo acep lan objetos no duplicados. Los conjuntos Hash.Sel proporcionan las búsquedas más
rápidas. miemras que TreeSet mantiene los elementos en orden. LinkedHashSet mantiene los elementos en
orden de inserción.
8. No hay necesidad de utilizar las clases antiguas Vector, Hashtable y Stack en los nuevos programas.
Resulta útil examinar un diagrama simplificado de los contenedores Java (sin las clases abstractas ni los componentes anti-
guos). En este diagrama se incluyen únicamente las interfaces y clases que podemos enCOlHrar de fonna habitual.
Los recuadros punteados representan interfaces, mientras que los recuadros de línea continua son clases normales (concre.
tas). Las líneas de puntos con flecha s huecas indican que una clase concreta está implementando una interfaz. Las flechas
rellenas muestran que una clase puede generar objetos de la clase a la que apunta la flecha. Por ejemplo, cualquier objeto
Collection puede generar un objeto Iterator, y un objeto List puede generar un objeto Li,lIterator (además de un objeto
Iterator nonma!. ya que List hereda de Collection).
He aquí un ejemplo que muestra la diferencia en métodos entre las distintas clases. El código concreto se ha tomado del
Capítulo 15. Genéricos; simplemente lo incluimos aquí para poder generar la correspondiente salida. La salida también
muestra las interfaces que se implementan en cada clase o interfaz.
jj: holdingjContainerMethods.java
import net.rnindview.util.*;
j * Output: (Sample)
Collection: {add, addAII, clear, contains, containsAII, equals, hashCode, isEmpty, itera-
tor, remove, removeAII, retainAII, size, toArray]
Interfaces in Collection: [Iterable]
Set extends Collection , adds: []
Interfaces in Set: [Collectionl
HashSe t extends Set, adds: [J
Interfaces in HashSet: [Set, Cloneable, Serializablel
LinkedHashSet extends HashSet, adds: []
Interfaces in LinkedHashSet: (Set, Cloneable, Serializablel
TreeSet extends Set, adds: [pollLast, navigableHeadSet, descendingIterator, lower,
headSet, ceiling, pollFirst, subSet, navigableTailSet, comparator, first, floor, last,
navigableSubSet, higher, tailSet]
Interfaces in TreeSet: [NavigableSet, Cloneable, SerializableJ
List extends Collection, adds: [listlterator, indexOf, get, subList, set, lastIndexOf]
Interfaces in List: (Collection]
ArrayList extends List, adds: [ensureCapacity, trirnToSize]
Interfaces in ArrayList: [List, RandomAccess, Cloneable, Serializable]
LinkedList extends List, adds: [pollLast, offer, descendingIterator, addFirst, peekLast,
removeFirst, peekFirst, removeLast, getLast, pollPirst, pop, polI, addLast,
removeFirstOccurrence, getFirst, element, peek, offerLast, push, offerFirst,
removeLastOccurrence]
Interfaces in LinkedList: [List, Deque, Cloneable, Serializable]
Queue extends Collection, adds: [offer, element, peek, polI]
Interfaces in Queue: (Collectionl
PriorityQueue extends Queue, adds: [comparator]
Interfaces in PriorityQueue: [Serializablel
Map: (clear, containsKey, containsValue, entrySet, equals, get, hashCode, isEmpty,
keySet, put, putAII, remove, size, valuesl
HashMap eXi:.ends Map, adds: [J
Interfaces in HashMap: [Map, Cloneable, Serializable]
LinkedHashMap extends Has~~ap, adds : []
Interfaces in LinkedHashMap: [Map]
SortedMap extends Map, adds: [subMap, comparator, firstKey, last Key, headMap, tailMapJ
Interfaces in SortedMap: [Map]
TreeMap extends Map, adds: [descendingEntrySet, subMap, pollLastEntry, lastKey,
floorEntry, lastEntry, lowerKey, navigableHeadMap, navigableTailMap, descendingKeySet,
tailMap, ceilingEntry, higherKey, pollFirstEntry, comparator, firstKey, fl oor Key,
higherEntry, firstEntry, navigableSubMap, headMap, lowerEntry, ceilingKey]
Interfaces in TreeMap: [NavigableMap, Cloneable, Serializable]
. /// ,-
11 Almacenamiento de objetos 275
Como puede ver. todos los conjuntos, excepto TreeSet, tienen exactamente la misma interfaz que Collection. List y
Collection difieren significativamente, aunque List requiere métodos que se encuentran en Collection . Por otro lado, los
métodos de la interfaz Queue son autónomos; los métodos de Collection no son necesarios para crear una implementación
de Queue funcional. Finalmente, la única intersección en tre Map y Collection es el hecho de que un mapa puede generar
colecciones uti lizando los métodos entrySet() y values( ).
Observe la interfaz de marcado java.util.RandomAccess, que está asociada a ArrayList perno no a LinkedList. Esta inter-
faz proporciona información para aquellos algoritmos que quieran modificar dinámicamente su comportamiento depend ien-
do del uso de un objeto List concreto.
Es verdad que es ta organización parece un poco extraña en lo que respecta a las jerarquías orientadas a objetos. Sin embar-
go, a medida que conozca más detalles acerca de los contenedores en java.util (en particular, en el Capitulo 17, Análisis
detallado de los contenedores), verá que existen otros problemas más importantes que esa estructura de herencia ligeramen-
te extraña. Las bibliotecas de contenedores han constituido siempre un problema de diseño realmente dificil ; resol ver estos
problemas implica tratar de satisfacer un conjunto de fuerzas qu e a menudo se oponen entre sí. Como consecuencia, debe-
mos preparamos para llegar a cienos compromisos en algunos momentos.
A pesar de estos problemas, los contenedores de Java son helTamientas fundamenta les que se pueden utilizar de fonna coti-
diana para hacer nuestros programas más simples, más potentes y más efectivos. Puede que tardemos un poco en aCOSUlnl-
bramas a algunos aspectos de la biblioteca, pero lo más probable es que el lector comience rápidamente a adquirir y utilizar
las clases que componen esta biblioteca.
Puede encontrar las solucioncs a los ejercicios se leccionado~ en el documento electrónico rile Thinláng in JaI'tl AnnOfafed So/utioll Guide, disponible para
la venta en WII'lI'.Alindr'iew.net.
Tratamiento de
errores • mediante
excepciones
El momento ideal de detectar un error es en tiempo de compilación, antes incluso de poder ejecutar e l programa. Sin embar-
go, no IOdos los errores pueden detectarse en tiempo de compilación. El resto de los problemas deberán ser gestionados en
tiempo de ejecución, utilizando algún lipo de fanna lidad que pemlita que quien ha originado el error pase la infonnación
apropiada a un receptor que sabrá cómo hacerse cargo de las dificultades ap ropiadamente.
Una de las fannas más potentes de incrementar la robustez del código es disponer de un mecanismo avanzado de recupera-
ción de errores. La recupe ración de errores es una de las preocupaciones principales de todos los programas que escribimos,
pero resulta especialmente importante en Java. donde lino de los objetivos principales consiste en crear componentes de pro-
gramas para que otros los utilicen. Para crear lIn s;sfema robusfo, cada componente f;ene que ser robusto. Al proporcionar
un modelo coherente de informe de elTores utilizando excepciones, Java pennite que los componentes comuniquen los pro-
blemas de manera fiable al cód igo cliente.
Los objetivos del tratamiento de excepciones en Java son simplificar la creación de programas fiables de gran envergadura
utili zando menos código de lo habitual y llevar esto a cabo con una mayor confianza en que nuestra apl icación no va a
encontrarse con errores no probados. El tema de las excepciones no resulta demasiado difici l de aprender y e l mecanismo
de excepciones es una de esas características que proporcionan beneficios inmediatos y de gran importancia a cualquier pro-
yecto.
Puesto que el tratamiento de excepciones es la única forma oficial en la que Java infonna acerca de los CITO res, y dado que
dicho mecanismo de tratamiento de excepciones es impuesto por el compi lador Java, no son demasiados los ejemplos
que podríamos escribir en este libro sin antes estudiar el tratamiento de excepciones. En este capítulo, se presenta el códi-
go que es necesario escribir para gestionar las excepciones adecuadamente, mostrándose también cómo podemos ge nerar
nuestras propias excepciones si alguno de nuestros métodos se mete en problemas.
Conceptos
El lenguaje e y otros le nguaj es anteriores di sponían a menudo de múltiples esquemas de tralamiento de elTores. y dichos
esquemas se solían estab lecer por convenio y no como parte del lenguaje de programación. NOITna(mente, lo que se hacía
era devolver un valor especial o co nfigurar una va riable ind icadora, y se suponía que el receptor debía examinar el valor o
la variable y detennmar que algo iba mal. Sin embargo, a med ida que fuero n pasando los años. se descubrió que los progra-
madores que uti lizaban una biblioteca tendían a pensar en sí mismos como si fueran inmunes al erro r; era casi como si dije-
ran: "Sí, puede que a otros se les presenten elTores, pero en mi código no hay errores". Por tanto, de f0IT118 bastante natura l,
los programadores tendían a no comprobar las condiciones del elTo r (y además, en ocasiones, esas condiciones de error eran
demasiado tontas como para comprobarlas ]). Si fuéramos tan exhaustivos como para comprobar si se ha producido un error
cada vez que invocamos un método, el código se convertiría en una pesad illa ilegible. Puesto que los programadores
podían, a pesar de todo, construir sus sistemas con estos lenguajes. se resistían a admitir la realidad: que esta técnica de ges-
tión de errores representaba una limitación importante a la hora de crear programas de gran envergadura que fueran robus_
tos y mantenibles.
La solución consiste en eliminar la naturaleza casual del tratamiento de errores e imponer una cierta fonnalidad. Este modo
de proceder tiene, en la práctica, una larga historia, porque las implementaciones de los mecanismos del tratamiento de
excepciones se remontan a los sistemas operativos de la década de 1960, e incluso a la instmcci6n "on erro r goto" de
BASIe. Pero el mecanismo de tratamiento de excepciones de C++ estaba basado en Ada, y el de Java se basa principalmen_
te en C++ (aunque se asemeja más al de Object Pascal).
La palabra "excepción" hace referencia a algo que no tiene lugar de la forma acostumbrada. En el lugar donde aparece un
problema, puede que no sepamos qué hacer con él, pero de lo que sí podemos estar seguros es de que no podemos conti.
nuar como si no hubiera pasado nada; es necesario pararse y alguien, en algún lugar, tiene que saber cómo responder al error.
Sin embargo. no disponemos de suficiente infonnación en el contexto actual como para poder corregir el problema, así que
lo que hacemos es pasar dicho problema a otro contexto superior en el que haya alguie n cualificado para tomar la decisión
adecuada.
El otro beneficio importante derivado de las excepciones es que tienden a reducir la complejidad del código de gestión de
errores. Sin las excepciones, es necesario comp robar si se ha producido un error concreto y resolverlo en múltiples lugares
del programa. Sin embargo, con las excepciones ya no hace falta comprobar los errores en el punto donde se produce la lla-
mada a un método, ya que la excepción garantiza que alguien detecte el error. Además, sólo hace falta tratar el problema en
un único sit io, en lo que se denomina la rutina de Iratamiento de excepciones. Esto nos ahorra código y permite también
separar el código que describe lo que queremos hacer durante la ejecución normal, de ese otro código que se ejecuta cuan-
do las cosas van mal. En general, la lectura, la escritura y la depuración del código se bacen mucho más claras con las excep-
ciones que cuando se utiliza la antigua fonna de tratar los errores.
Excepciones básicas
Una condición excepcional es un problema que impide la continuación del método o ámbito actuales. Resulta importante
distinguir las condiciones excepcionales de los problemas nonnales, en los que disponemos de la suficiente información
dentro del contexto actual, como para poder resolver de alguna manera la dificultad con que nos hayamos encontrado. Con
una condición excepcional, no podemos continuar con el procesamiento, porque no disponemos de la infonnación necesa-
ria para tratar con el problema en el contexto actual. Lo único que podemos hacer es saltar fuera del contexto actual y pasar
dicho problema a otro contexto de orden superior. Esto es lo que sucede cuando generamos una excepción.
La división representa un ejemplo sencillo: si estamos a punto de dividir por cero, merece la pena comprobar dicha condi-
ción, pero ¿qué implica que el denominador sea cero? Puede que sepamos, en el contexto del problema que estemos inten-
tando resolver en ese método concreto, cómo tratar con un denominador cero. Pero si se trata de un valor inesperado, no
podremos tratar con él, y deberemos por tanto generar una excepción en lugar de continuar con la ruta de ejecución en la
que estuviéramos.
Cuando generamos una excepción suceden varias cosas. En primer lugar, se crea el objeto excepción de la misma fonna que
cualquier otro objeto Java: en el cúmulo utilizando la instrucción new. A continuación. se detiene la ruta actual de ejecución
(aquella que ya no podemos continuar) y se extrae del contexto actua l la referencia al objeto excepción. En este punto, el
mecanismo de tratamiento de excepciones se hace cargo del problema y comienza a buscar un lugar apropiado donde con·
tinuar ejecutando el programa. Dicho lugar apropiado es la rutina de Iratamiento de excepciones, cuya tarea consiste en
recuperarse del problema de modo que el programa pueda intentar hacer otra cosa o simplemente continuar con lo que estu-
viera haciendo.
Como ejemplo simple de generación de excepciones vamos a considerar una referencia a un objeto denominada t. Es posi·
ble que nos pasen una referencia que no haya sido inicializada, así que podríamos tratar de comprobarlo antes de invocar
un método en el que se utilice dicha referencia al objeto. Podemos enviar la infomlación acerca del error a otro contexto de
orden superior, creando un objeto que represente dicha información y extrayéndolo del contexto actual. Este proceso se
denomina generar una e.xcepción. He aquí el aspecto que tendría en el código:
Hit :: null)
throw new NullPointerException();
12 Tratamiento de errores mediante excepciones 279
Esto genera la excepción, lo que nos pemlite, en el contexto actual, olvidarnos del problema: dicho problema será gestiona-
do de manera transparente en algún otro lugar. En breve veremos dónde exactamente se gestiona la excepción.
Las excepciones nos penniten pensar en cada cosa que hagamos como si se tratara de una transacción, encargándose las
excepciones de proteger esas transacciones: " .. .la premisa fundamental de las transacciones es que hacía falta un mecanis-
mo de tratamiento de excepciones en la informática distribuida. Las transacciones son el equivalente informático de los con-
tratoS legales. Si algo va mal, detenemos todos los cálculos"2. También podemos pensar en las transacciones como si se
tratara de un sistema integrado para deshacer acciones, porque (con un cierto cuidado) podemos establecer varios puntos de
recuperación en cualquier programa. Si una parte del programa falla, la excepción se encarga de "deshacer" las operaciones
basta un punto estable conocido dentro del programa.
Uno de los aspectos más importantes de las excepciones es, que si sucede algo realmente grave, no penniten que el progra-
ma continúe con la ruta de ejecución ordinaria. Este tema ha constituido un grave problema en lenguajes como e y C++;
especialmente en C, donde no había ninguna fonna de impedir que el programa continuara por una ruta de ejecución en caso
de aparecer un problema, de modo que era posible ignorar los problemas durante un largo tiempo y acabar en un estado
totalmente inadecuado. Las excepciones nos penniten (entre otras cosas) obligar al programa a detenerse y a infonnamos
de qué es lo que ha pasado o, idealmente, obligar al programa a resolver el problema y volver a un estado estable.
Esta cadena de caracteres puede posteriormente extraerse utilizando varios métodos, como tendremos oportunidad de ver.
La palabra clave throw produce una serie de resultados interesantes. Después de crear un objeto excepción mediante new,
le proporcionamos la referencia resultante a throw. En la práctica, el objeto es "devuelto" desde el método, aun cuando
dicho tipo de objeto no es normalmente lo que estaba diseñado que el método devolviera. Una fonna bastante simplificado-
ra de considerar el mecanismo de tratamiento de excepciones es como si fuera un tipo distinto de mecanismo de retomo,
aunque no debemos caer en la tentación de llevar esa analogía demasiado lejos, porque podríamos meternos en problemas.
También podemos salir de ámbitos de ejecución ordinarios generando una excepción. En cualquiera de los casos, se devuel-
ve un objeto excepción y se sale del método o ámbito donde la excepción se haya producido.
Las similitudes con el proceso normal de devolución de resultados por parte de un método tenninan aquí, porque e/lugar
a/ que se vuelve es completamente distinto de aquel al que volvemos en una llamada normal a Ull método (volvemos a una
rutina apropiada de tratamiento de excepciones que puede estar alejada muchos niveles dentro de la pila del lugar en el que
se ha generado la excepción).
Además, podemos generar cualquier tipo de objeto Throwable, que es la clase raíz de las excepciones. Normalmente, lo
que haremos será generar una clase de excepción distinta para cada tipo diferente de error. La información acerca del error
está representada tanto dentro del objeto excepción como, implícitamente, en el nombre de la clase de excepción, por lo
que alguien en el contexto de orden superior puede determinar qué es lo que hay que hacer con la excepción (a menudo,
la única infonuación es el tipo de excepción, no almacenándose ninguna información significativa dentro del objeto ex-
cepción).
2 Jim Gray. ganador del premio Tunng Award por las contribuciones que su equipo ha realizado al tema de las transacciones. Las palabras eslán tomadas
de una entrevista publicada en www.acmq/leue.org.
280 Piensa en Java
El bloque try
Si nos encontram os dentro de un método y generamos una excepción (o si otro método al que invoquemos dentro de éste
genera una excepción), dicho método temlinará al generarse la excepción. Si no queremos generar (con throw) la excep_
ción para sal ir del método. podemos definir un bloque especial denrro de dicho método para capturar la excepción. Este blo-
que se denomina Moque Iry (proba r) porque lo que hacemos en la práctica es "probar" dentro de ese bloque las diversas
ll amadas a métodos. El bloque try es un ámbito de ejecución ordinari o precedido por la palabra clave try:
try {
JI Código que podría generar excepciones
Si qui siéramos comprobar cuidadosamente los errores en un lenguaje de programación que no tu vie ra mecanismos de trata-
miento de excepciones, tendríamos que rodear todas las llamadas a métodos con cód igo de preparación y de comprobación
de errores, incluso si in vocáramos el mismo método en varias ocasiones. Con e l tratamiento de excepciones incluimos todo
dentro del bloque t ry y capturamos lOdas las excepciones en un único lugar. Esto significa que el código es mu cho más fácil
de escribir y de leer, porque e l objetivo del código no se ve confundido con los mecanismos de comprobación de errores.
// etc",
Cada clá usula catch (nuina de tratamiento de excepciones) es como un pequei'io método que toma un único argumento de
un tipo concreto. El identifi cador (idl , id2 , etc.) puede utilizarse dentro de la mtina de tratamiento de excepciones exacta-
mente igual que un argumento de un método. En ocasiones. nunca utilizamos e l identificador, porque el tipo de la excep-
ción nos proporciona ya suficiente infomlación como para poder tratar con e ll a, pero a pesa r de todo el identificador debe
estar preseme.
Las rutinas de tratamiento de excepciones deben aparecer inmediatamente después del bloque try. Si se genera una excep-
ción, el mecanismo de tratamiento de excepciones trata de buscar la primera mtina de tratamiento cuyo argumento se ajus-
te a l tipo de la excepción. A con tinuación, entra en la clá usul a ca tch, y la excepción se considera tratada. La búsqueda de
rutinas de tratamiento se detiene en cuanto finalizada la cláusu la catch . Sólo se ejecutará la c láusula catch que se ajuste al
tipo de la excepción; estas cláusula s no son como las instmcciones switch, en las que hace fa lta una instmcción break des-
pués de cada cláusula case para impedir que las restantes cláusul as se ejecuten.
Observe que, dentro del bloque try, puede haber di stintas llamadas a metodos que generen la misma excepción, pero sólo
hace falta una úni ca rutina de tratamiento.
Terminación y reanudación
Existen dos modelos básicos en la teoría de tratamiento de excepciones. Java so pona el modelo denominado de termina-
ción ,3 en el que asumimos que e l error es tan crítico que no hay forma de volver a l lugar en e l que se generó la excepción.
Quienquiera que generara la excepción, decidió que 110 había ninguna manera de salvar la situación, por lo que no desea que
volvamos a ese punto en el que la excepción fue generada.
La alternativa se denomina reanudación. Esta alternativa implica que la mtina de tratamiento de excepciones debe hacer
algo para rectificar la si tuación, después de 10 cual se vuelve a intentar ejecutar el método fallido , presumiendo que esa
segunda vez tendrá éxito. Si queremos utili za r la técnica de reanudación. quiere decir que esperamos podemos con tinuar co n
la ejecución después de que la excepción sea tratada.
Si quiere disponer de un comportamiento de reanudación en Java, no genere una excepción cuando se encuentre con error.
En lugar de ello. invoque un método que canija el problema. Alternativamente, coloque el bhJque tr)' dentro de un bucle
while que continlle vo lviendo a entrar en el bloque t ry hasta que el resultado sea sarisfactorio.
Históricamente, los programadores qu e utilizaban sistemas operativos donde se admitía el tratamiento de excepciones con
reanudación terminaron uti lizando código basado en el mecanismo de tenllinación y evitando emplear la reanudación. Por
tan to. aunque la reanudación pueda parecer atractiva a primera vista, no resulta demasiado útil en la práctica. La razón prin-
cipal es. probablemente, el acop lamiento resultante: las mtinas de tratamiento con reanudación necesitan saber dónde se ha
generado la excepción, y necesitan también contener código no genérico específico de cada ubicación donde la excepción
se genere. Esto hace que el código sea difícil de escribir y de mantener. especialmente en sis temas grandes en los que las
excepciones puedan generarse en muchos puntos di stintos.
/ * Out put:
Throw SimpleException fram f()
Caught it!
' /// ,-
El compilador crea un constmctor predetemlinado, que de forma automática (e invisible) invoca al constructor predetermi-
nado de la clase base. Por supuesto, en este caso no obtenemos un constmctor SimpleException(String), pero en la prácti-
ca dicho constructor 110 se usa demasiado. Como veremos, lo más importante acerca de una excepción es el nombre de la
clase, por lo que la mayor parte de las veces ulla excepción como la que aquí se muestra se rá perfectamente adecuada.
282 Piensa en Java
Aqui, el resu ltado se imp rim e en la consola, donde se captura y se compru eba automáticamente utili za ndo el sistema de
visualización de la sa lida de programas de este libro. Sin embargo. también podríam os enviar la infomlación de error a la
salida de error estándar escribiendo en System. er r . Nomlalmentc, suele se r mejor envia r aquí la infonnación de error que
a S~'stem.out. que puede estar redirigido. Si se envía la salida a System.er r, no sera redirigida junto con System.out. por
lo que es más probable que el usuario vea la infonnación.
También pode mos crea r una clase de excepción que ten ga un constructor con un argumento St ri ng:
JI : exceptions/FullConstructors.java
/* Output:
Throwing MyException fram f()
MyException
at FullConstl"uctors. f (FullConstructors . java: 11)
at FullConstructors.main(FullConstructors.java:19)
Throwing MyException from g()
MyExcepeion: Originated in g()
ae FullConstructors.g(FullConstructors.java:15)
at FullConstructors main(FullConstructors .java:2 4)
El código añadido es de pequeño tamano: do; eonstmetores qoe definen la fomla en que se crea MyException . En el segun-
do const ru ctor, se in voca explícilamente el constructor de la clase base con un argum ento String utilizando la palabra cla\e
super.
En las rutinas de tratamiento, se invoca uno de los metodos Throwable (de donde se hereda Ex«plio n): prinIStackTrace() .
Como puede \'er a la salida. esto genera infonnación acerca de la secuencia de Inétodos que fueron ¡mocados hasta llegar
al punt o en que se generó la excepción. Aquí, la información se envía a System.out. sie ndo automáticamente captu rada y
mostrada a la salida . Sin embargo, si invocamos la vers ión predetenninada:
e.printStackTrace() ;
Ejercicio 1: (2) Cree una clase con un método main() que genere un objeto de la clase Exception dentro de un blo-
que tr)'. Proporcione al co nstmctor de Exception un argumento String. Capture la excepción dentro de
una cláusula catch e imprima el argumento String. Aii.ada ulla cláusula finaUy e imprima un mensaje para
demostrar que pasó por alli.
Ejercicio 2: (1) Defina una referencia a un objeto e inicialícela con nul!. Trate de invocar un método a través de esta
referencia. Ahora rodee el código con una cláusula try-catch para capturar la excepción.
Ejercicio 3: (1) Escriba código para generar y ca pnlfar Ulla excepción ArraylndexOutOffioundsException (indice de
matri z fuera de límites).
Ejercicio 4: (2) Cree su propia clase de excepción utilizando la palabra clave extends. Escriba un constructor para
dicha clase que tome un argumento String y lo almacene dentro del objeto como una referencia de tipo
String. Escriba un método que muestre la cadena de caracteres almacenada. Cree una cláusula try-catch
para probar la nueva excepción.
Ejercicio 5: (3) Defina un comportamiento de tipo reanudación utiliza ndo un bucle while que se repita hasta que se
dej e de generar una excepción.
Excepciones y registro
También podemos registrar la sa lida utili zando java.util.logging. Aunque los detalles comp letos acerca de los mecanismos
de registro se presentan en el suplemento que puede encontrarse en la dirección htfp:/IMindView. netIBooksIBetterJava, los
mecanismos de registro básicos son lo suficientemente sencillos como para poder utilizarlos aquí.
11 : exceptions / LoggingExceptions.java
II Una excepción que proporciona la información a través de un registro .
import java.util.logging .*;
import java.io.*;
Caught LoggingException
284 Piensa e n Java
Caught LoggingException
*///,-
El método estático Logger.getLoggcr() crea un objeto Logger asociado con el argumento String (usualment e, el nombre
del paqucle y la clase a la que se refieren los errores) que envía su salida a System.err. La forma más fácil de escribir en
un objeto Logger consiste simple mente en invoca r el método asociado con el nivel de mensaj e de regi stro: aquí utilizamos
severe(). Para producir el objeto String para el mensaje de registro. convendría di sponer de la traza de la pila correspon·
diente al lugar donde se generó la excepción, pero printStackTrace() no genera un objeto String de fonna predetennina_
da. Para obtener un objeto String, necesitamos usar e l método sobrecargado printStackTrace() que toma un objeto
java.io.PrintWriter como argumento (todo esto se ex plicará con detalle en el Capítulo 18, E/S). Si entregamos al construc·
tor de Print\Vriter un objeto java.io.StringWritcr, la salida puede extraerse como un objeto String invocando toString().
Aunque la técnica utili zada por LoggingException resulta muy cómoda. porque integra toda la infraestructura de registro
dentro de la propia excepción, y funciona por tamo automáticamente sin intervención del programador de clientes, lo más
común es que nos veamos en la situación de capturar y regi strar la excepción generada por algún otro. por lo que es nece·
sa rio generar el mensaje de regi stro en la propia mtina de tratamiento de excepciones:
11: exceptions/LoggingExceptions2.java
/1 Registro de l as excepciones capturadas .
import java .util.logging.*¡
import java.io. *¡
11: exceptions/ExtraFeatures.java
JI Mejora de las clases de excepción.
import static net.mindview.util.Print.*;
this.x = Xi
try
g();
catch (MyException2 el {
e.printStackTrace(System.out) i
}
try {
h();
catch (MyException2 el {
e.printStackTrace(System.out) i
System.out.println{ue.valO = " + e.val(» i
/* Output:
Throwing MyException2 from f()
MyException2: Detail Message: O null
at ExtraFeatures.f(ExtraFeatures.java:22)
at ExtraFeatures.main(ExtraFeatures.java:34)
Throwing MyException2 from g()
MyException2 : Detail Message: O Originated in g()
at ExtraFeatures.g(ExtraFeatures.java:26l
at ExtraFeatures.main(ExtraFeatures.java:39)
Throwing MyException2 from h()
MyException2: Detail Message: 47 Originated in h()
at ExtraFeatures.h(ExtraFeatures.java:30l
at ExtraFeatures.main(ExtraFeatures.java:44)
e. val () = 47
*///,-
Se ha añadido un campo x junto con un mélOdo qu e lee di cho va lor y un constructor adicional qu e lo inicializa. Además,
Throwable.getMessagc( ) ha sido sustituido para generar un mensaje de detalle más interesante. getMcssage() es un méto-
do simi lar a toString() que se utiliza para las clases de excepción.
286 Piensa en Java
Puesto que una excepción no es más que otro tipo de objeto, podemos continuar este proceso de mejora de nuestras clases
de ex tensión. Recuerde, sin emba rgo. que todo este trabajo adic ional puede no servir para nada en los programas de c lien~
te que utili cen nuestros paquetes, ya que dichos programas puede que simplemente miren cuál es la excepción que se ha
generado y nada más (ésa es la forma en la que se utili zan la mayoría de las excepciones de la biblioteca Java).
Ejercicio 6: ( 1) Cree dos clases de excepción, cada una de las cuales realice su propia tarea de registro automáticamen~
te. Demuestre que dichas clases funcionan.
Ejercicio 7: (1) Modifique el Ejercicio 3 para que la cláusula ealeh registre los resultados.
La especificación de la excepción
En Java, debemos tratar de informar al programador de clientes, que invoque nuestros métodos, acerca de las excepciones
que puedan ser generadas por cada método. Esto resulta conveni ente porque quien realiza la in vocación puede saber así
exactamente qué código necesita escribir para caphlrar todas las excepciones potenciales. Por supu esto, si el código fuente
está disponible, el programador de clientes podría examinarlo y buscar las instrucciones throw, pero puede qu e una biblio-
teca se di stribuya sin el correspondiente código fuente. Para evitar que esto constituya un problema, Java proporciona una
si ntaxis (y nos obliga a usar esa sintaxis) para permitirnos informar al programado r de clientes acerca de cuál es son las
excepciones que el método genera, de modo que el programador pueda tratarlas. Se trata de la especificación de excepcio-
nes que forma parte de la declaración del método y que aparece después de la lista de argumentos.
La especificación de excepciones utili za una palabra clave adicional, throws, seguida de todos los tipos potenciales de
excepciones. Por tanto, la definición de un método podría tener el aspecto siguiente:
void f() throws TooBig. TooSmall, DivZero { j j ...
significa que el método no genera ninguna excepción (salvo por las excepciones heredadas de RuntimeException , qu e pue ~
den generarse en cualquier lugar sin necesidad de que exista una especi ficación de excepciones; hablaremos de estas excep~
ciones más adelante) .
No podemos proporcionar información falsa acerca de la especificación de excepciones. Si el código dentro del método pro-
voca excepciones y ese método no las trata, el compilador lo detectará y nos infonnará de que debemos tratar la excepción
o indicar, mediante una especificación de excepción, que dicha excepción puede ser devuelta por el método. Al imponer que
se utilicen especificaciones de excepción en todos los lugares, Java garanti za en tiempo de compilación que exista una cier-
ta corrección en la definición de las excepciones.
Sólo hay una manera de que podamos proporcionar infonnación falsa: podemos declarar que generamos una excepción que
en realidad no vayamos a genera r. El compilador aceptará lo que le digamos y forzará a los usuarios de nuestro método a
tratar esa excepción como si rea lmente fuera a ser generada. La única ventaja de hacer esto es di sponer por adelantado de
una forma de añadi r posteriormente la excepción; si más adelante decidimos comenzar a generar esa excepción, no tendre-
mos que real izar cambios en el código existente. También resulta importante esta característi ca para crear interfaces y cIa-
ses bases abstractas cuyas clases derivadas o implementaciones puedan necesitar generar excepciones.
Las excepciones que se comprueban y se imponen en tiempo de compilación se denominan excepciones comprobadas.
Ejercicio 8: (1) Escriba una clase con un método que genere una excepción del tipo creado en el Ejercicio 4. Trate de
compilarlo si n incluir una especificación de excepción para ver qué es lo que di ce el compilador. Añada
la especificación de exce pción apropiada. Pruebe la clase y su correspondiente excepción dentro de una
cláusula try-ealeh .
c atch ( Exception e ) (
System.out.println ( "Caught an exception");
Esto pennitirá capturar cualquier excepción, por lo que si usamos este mecanismo convendrá ponerlo al final de la lista de
rutinas de tratamiento, con el fin de evitar que se impida entrar en acción a otras rutinas de tratamiento de excepciones que
puedan estar situadas a continuación.
puesto que la clase Exception es la base de todas las clases de excepc ión que tienen importancia para el programador, no
vamos a obtener demasiada infonnación específica acerca de la excepción concreta que hayamos capturado, pero podemos
invocar los métodos incluidos en su lipo base Throwable:
String getMessage()
String gctLocalizedMessage()
Obtiene el mensaje de detalle o un mensaje ajustado para la configuración de idioma uti lizada.
String toString()
Devuelve una descripción corta del objeto Throwable, incluyendo el mensaje de detalle, si es que existe uno.
voi d printStackTrace( )
void printStackTracc(PrintStream)
void pri ntStackTraceGava.io.PrintWriter)
Imprime el obje to Throwable y la traza de pila de llamadas de dicho objeto. La pila de llamadas muestra la sec uencia de
llamadas a métodos que nos han conducido hasta el lugar donde la excepción fue generada. La primera versión imprime en
la salida de error es tánda r, mientras que la segu nda y la tercera im primen en cualquier salida que elijamos (en e l Capitulo
18, E/S, veremos por qué ex isten dos tipos de !lujos de salida).
Th rowa ble filllnStackTrace()
Registra infomlación dentro de este objeto Throwable acerca del estado actual de los marcos de la pila. Resulta útil cuan-
do una aplicación está volviendo a generar un error o una excepción (hablaremos de esto en breve).
Además, también tenemos acceso a otros métodos del tipo base de Throwablc que es Object (que es e l tipo base de todos
los objetos). El que más útil puede resultar para las excepciones es getClass(), que devuelve un objeto que representa la
clase de este objeto. A su vez, podemos consultar este objeto Class para obtene r su nombre mediante getNarnc(), que inclu-
ye infonnación o getSimpleName(), que sólo devuelve el nombre de la clase.
He aqui un ejemplo que muestra el uso de los métodos básicos de Exception :
// : exceptions / ExceptionMethods.java
/1 Ilustración de los métodos de Exception.
import static net.mindview.util.Print.*;
1* Output:
Caught Exception
g etMessage() :My Exception
getLocalizedMessage () : My Exception
toString() :java.lang.Exception: My Exception
288 Piensa en Java
printStackTrace() :
java . lang.Exception: My Exception
at ExceptionMethods.main(ExceptionMethods.java:8)
Podemos ve r que los métodos proporcionan sucesivamente más infomlación. de hecho, cada uno de ellos es un supercon_
junto del anterior.
Ejercicio 9: (2) Cree tres nuevos tipos de excepciones. Escriba una clase co n un método que genera las tres. En
main(), llame al método pero utilice una única clá usula catch que penllita capturar los tres tipos de excep_
ciones.
la traza de la pila
También puede accederse directam ente a la información proporcionada por printStackTrace() , utilizando
getStackTrace() . Este método devuelve una matriz de elementos de traza de la pila, cada un o de los cuales representa un
marco de pila. El elemento cero es la parte superior de la pila y se corresponde con la última in vocac ión de método de la
secuencia (el punto en que este objeto Throwable fue generado). El último elemento de la matriz (la parte inferior de la
pila) es la primera invocación de método de la secuencia. Este progra ma proporciona una ilustración simple:
// : exceptions/WhoCalled.java
// Acceso mediante programa a la información de traza de la pila.
/ * Output:
f
main
f
g
main
Aquí, nos limitamos a Impnm ir el nombre del método, pero también podríamos imprimir el objeto completo
StackTraceElement , que contiene información adic iona l.
12 Tratamiento de errores mediante excepciones 289
La regeneración de una excepción hace que ésta pase a las rutinas de tratamiento de excepciones del siguiente co ntexto de
ni ve l superior. Las c láusulas catch adicional es inc luidas en el mismo bloque try seguirán ignorándose. Además, se preser-
va toda la infonnación acerca del objeto de excepción, por lo que la nnina de tratamiento en el contexto de ni vel superior
que capnlre ese tipo excepción específico podrá extraer toda la información de dicho objeto.
Si nos limitamos a regenerar la excepción actual. la información que imprimamos acerca de dicha excepc ión en
printStackTrace() será la relativa al origen de la excepción, no la relati va al lugar donde la hayamos regenerado. Si que-
remos insertar nueva infonnación de traza de la pila podemos hacerlo invocando fillInStackTrace(), que devuelve un obje-
to Throwable que habrá generado insertando la infonnación de pila actual en el antiguo objeto de excepción. He aquí un
ejemplo:
jj : exceptions/Ret h r owing . java
11 Ilustración de fil l InStackTra ce()
try
hll;
catch(Exception el
Sys t em.out . println(lImain : printStackTrace() 11) i
e . printStackTrace(System . out) ;
290 Piensa en Java
/ * Output:
originating the exception in f ()
Inside g {) ,e.printStackTrace ()
j ava.lang.Exception: thrown froID f ()
at Rethrowing.f {Rethrowing.java:7 )
at Rethrowing.g (Rethrowing.java:ll )
at Rethrowing.main(Rethrowing.java:29)
main: printStackTrace ()
java .1an9. Exception: thrown froID f ()
at Rethrowing.f (Rethrowing.java:7 )
at Rethrowing.g (Rethrowing . java:ll l
at Rethrowing.main (Rethrowing. java:29 )
originating che exception in f ()
Inside h () ,e.printStackTrace ()
java.lang.Exception : thrown fr oro f ()
at Rethrowing.f (Rethrowing.java:7 )
at Rechrowing.h (Rethrowing.java:2 0)
at Rethrowing.main(Rethrowing.java:35 )
maln: printStackTrace {)
java.lang.Exception: thrown fram f ()
at Rethrowing.h(Rethrowing.java:24)
at Rethrowing.main(Rethrowing.java:35 )
/1 : exceptions / RethrowNew.java
/1 Regeneración de un objeto distinto de aquél que fue capturado.
catch(TwoException e)
System.out .println (
12 Tratamiento de errores mediante excepciones 291
1* Output:
originating the exception in f()
Caught in inner try, e.printStackTrace()
OneException : thrown from f()
at RethrowNew. f {RethrowNew.java: 15)
at RethrowNew. main {RethrowNew. java: 20)
Caught in outer try, e.printStackTrace()
TwoException: fram inner try
at RethrowNew. main {RethrowNew. java: 25)
* /// ,-
La excepción final sólo sabe que pro viene del bloque try interno y no de f().
Nunca hay que preocuparse acerca de borrar la excepción anterior, ni ningw1a otra excepción. Se trata de objetos basados
en el cúmulo de memoria que se crean con new, por lo que el depurador de memoria se encargará automáticamente de
borrarlos.
Encadenamiento de excepciones
A menudo, nos interesa capturar una excepción y generar otra, pero manteniendo la informac ión acerca de la excepción de
origen; este procedimiento se denomina encadenamiento de excepciones. Antes de la aparición del JDK 1.4, los programa-
dores tenían que escribir su propio código para preservar la información de excepción original, pero ahora todas las subcla-
ses de Throwable tienen la opción de admitir un objeto causa dentro de su constnlctor. Lo que se pretende es que la causa
represente la excepción original, y al pasa rla lo que hacemos es mantener la traza de la pila corres pondiente al origen de la
excepción, incluso aunque estemos generando una nueva excepción.
Resulta interesante observar que las úni cas subclases de Throwable que proporcionan el argumento causa en el constnlC-
tor son las tres clases de excepción fundamental es Error (utilizada por la máquina virtual JVM para informar acerca de los
errores del sistema), Exception y RuntimeException . Si queremos encadenar cualquier otro tipo de excepción, tenemos
que hacerlo utilizando el método initCause( ), en lugar del constructor.
He aquí un ejemplo que nos pennite añadir dinámicamente campos a un objeto DynamicFields en tiempo de ejecución:
11 : exceptions/DynamicFields.java
II Una clase que añade dinámicamente campos a s í misma.
II Ilustra el mecanismo de encadenamiento de excepciones.
import static net.mindview.util.Print.*¡
private int
getFieldNumber(String id ) throws NoSuchFieldException {
int fieldNum = hasField {id ) i
if ( fieldNum == - 1 )
throw new NoSuchFieldException () ;
return fieldNuID;
public Object
getField(String id) throws NoSuchFieldException
return fields [getFieldNumber(id)] [1];
1* Output:
null: null
null: null
nu ll: null
d: A value for d
number: 47
number2: 48
Ejercicio 10: (2) Cree una clase con dos métodos. f( 1 y g( l. En g( l. genere una excepción de un nuevo tipo det,nido
por el usuario. En f(). invoque a g(), capture su excepción y, en la cláusula catch. genere una excepción
diferente (de un segundo tipo también definido por el usuario). Compruebe el código en main( ).
Ejercicio 11: (1) Repita el ejercicio anterior, pero dentro de la cláusula catch , envuelva la excepción g() dentro de una
excepción RuntimeException.
Puede resultar un poco atenador pensar que debemos comprobar si todas las referencias que se pasan a un método son igua-
les a ouU (dado que no podemos saber de antemano si el que ha realizado la invocación nos ha pasado una referencia váli-
da). Afortunadamente, no es necesario realizar esa comprobación manualmente; dicha comprobación forma parte del
sistema de comprobación estándar en tiempo de ejecución que Java aplica automáticamente. y si se realiza cualquier llama-
da a una referencia null, Java generará automáticamente la excepción NullPointerException . Por tanto, el anterior frag-
mento de código resulta siempre superfluo, aunque si que puede resultar interesante realizar otras comprobaciones para
protegerse frente a la aparición de una excepción NuLlPointerException.
Existe un conjunto completo de tipos de excepción que cae dentro de esta categoría. Se trata de excepciones que siempre
son generadas de fomla automática por Java y que no es necesario incluir en las especificaciones de excepciones.
Afortunadamente, todas estas excepciones están agrupadas. dependiendo todas ellas de una única clase base denominada
RuntimeException, que constituye un ejemplo perfecto de herencia: establece una familia de tipos que tienen detem1Ína-
das características y comportamientos en común. Asimismo, nunca es necesario escribir una especificación de excepción
que diga que un método puede generar RuntimeException (o cualquier tipo heredado de RuntimeException), porque se
trata de excepciones no comprobadas. Puesto que estas excepciones indican errores de programación, nonnalmente no se
suele capnlrar una excepción RuntimeException. sino que el sistema las trata automáticamente. Si nos viéramos obligados
a comprobar la aparición de este tipo de excepciones, el código sería enormemente lioso. Pero. aunque nonnalmenre no
vamos a capturar excepciones RuntimeException. sí que podemos generar este tipo de excepciones en nuestros propios
paquetes.
¿Qué sucede cuando no capturamos estas excepciones? Puesto que el compilador no obliga a incluir especificaciones de
excepción para estas excepciones, resulta bastante posible que lila excepción RuntimeException ascienda por toda la jerar-
12 Tratamiento de errores mediante excepciones 295
qu ía de métodos sin ser captllrada. hasta llegar al método maine ). Para ve r lo que sucede en este caso, trale de ejecutar el
siguiente ejemplo:
11 : exceptions / NeverCaught . java
II Lo que sucede al ignorar una excepción RuntimeException .
11 {ThrowsException}
public class NeverCaught {
static void f () {
throw new RuntimeException( "From f () ti) i
static void 9 () {
f II ;
Como puede ver, RuntimeException (o cualquier cosa que herede de ella) es un caso especial. ya que el compilador no
requiere que incluyamos una especificación de excepción para estos tipos. La salida se envía a System .crr:
Exception in thread "main" java .lang. RuntimeException: From f ()
at NeverCaught.f{NeverCaught.java:7)
at NeverCaught.g(NeverCaught.java:lOl
at NeverCaught.main(NeverCaught.java:13)
Por tanto, la respuesta es: si una excepción RuntimeException llega hasta main() sin ser capturada. se invoca
printStackTracc() para dicha excepción en el momento de sa lir del programa.
Recuerde que sólo las excepciones de tipo RuntimeException (y sus subclases) pueden ser ignoradas en nuestros progra-
mas, ya que el compilador obliga exhaustivamente a tratar todas las excepciones comprobadas. El razonamiento que expli-
ca esta fonna de actuar es que RuntimeException representa un error de programación, que es:
1. Un error que no podemos anticipar. Por ejemplo, una referencia oull que escapa a nuestro contTo!.
2. Un error que nosotros, como programadores, deberíamos haber comprobado en nuestro código (como por ejem-
plo una excepción ArraylndexOutOfBoundsExccption , que indica que deberíamos haber prestado atención al
tamaño de una matriz). Una excepción que tiene lugar como consecuencia del punto l suele convertirse en un
problema del tipo especificado en el punto 2.
Como puede ver, resulta enonnemente beneficioso disponer de excepciones en este caso, ya que nos ayudan en el proceso
de depuración .
Es interesante observar que el mecanismo de tratamiento de excepciones de Java no tiene un único objetivo. Por supuesto,
está di se ñado para tratar esos molestos errores de ejecución que tienen lugar debido a la acción de fuerzas que escapan al
control de nuestro código, pero también resulta esencial para ciertos tipos de errores de programación que el compilador no
puede detectar.
Ejercicio 12: (3) Modifique innerclasses/Sequence.java para que genere una excepción apropiada si tratamos de intro-
ducir demasiados elementos.
" El mecanismo de tratamiento de excepciones de e++ no dispone de la cláusula finally, porque depende de la utilización de destructores para llevar a
cabo estas tarcas de limpieza.
296 Piensa en Java
try {
// La región protegida: actividades peligrosas
// que pueden generar A, B o e
catch lA al) {
11 Rutina de tratamiento para la situación A
catch (B b1) {
11 Rutina de tratamiento para la situación B
catch (C el) {
11 Rutina de tratamiento para la situación e
final l y {
11 Actividades que tienen lugar en todas las ocasiones
Para demostrar que la cláusula fin aLl y siempre se ejecuta, pruebe a ejecutar este programa:
//: exceptions/FinallyWorks.java
II La cláusula finally siempre se ejecuta.
class ThreeException extends Exception {}
finally {
System.out.println(nIn finally clause n );
if (count == 2) break; II fuera del bucle "while"
1* Output:
ThreeException
In finally clause
No exception
In finally clause
*111,-
Analizando la salida, podemos ver que la cláusula fi nall y se ejecuta se haya generado o no una excepción.
Este programa también nos indica cómo podemos tratar con el hecho de que las excepciones en Java no nos penniten con-
tinuar con la ejecución a partir del punto donde se generó la excepción, como ya hemos indicado anteriormente. Si inclui-
mos nuestro bloque try en un bucle, podremos establecer una condición que habrá que satisfacer antes de continuar con el
programa. También podemos añadir un contador estático o algún otro tipo de elemento para pennitir que el bucle pruebe
con varias técnicas diferentes antes de darse por vencido. De esta forma, podemos proporcionar un mayor nivel de robustez
a nuestros programas.
S Un destructor es una función que siempre se invoca cuando un objeto deja de ser utilizado. Siempre sabemos exactamente dónde y cuándo se invoca al
destructor. C++ dispone dc llamadas automáticas a destnLctores, mientras que e#, que se parece más a Java, dispone de un mecanismo que hace po~ibl e
que tenga lugar la destrucción automática .
12 Tratamiento de errores mediante excepciones 297
el bloq ue try. Pero Ja va dispone de un depurador de memoria, por lo que la liberación de memoria casi nunca es un proble-
ma. Asimismo, no dispone de ningún destructor al que in vocar. Por tanto, ¿cuándo es necesario utili zar finally en Java?
La cláusula finally es necesaria cuando tenernos que restaurar a su estado original alguna afro cosa distinta de la propia
memoria. Se trata de algún tipo de tarea de limpieza que se encargue, por ejemplo, de cerrar un archivo abierto o una cone-
xión de red, de borrar algo que hayamos dibujado en la pantalla o incluso de accionar un conmutador en el mundo exterior,
tal como se ilustra en el siguiente ejemplo:
/1 : exceptions/Switch.java
import static net.mindview util . Print.*¡
1/ : exceptions/OnOffExcept ion2.java
public class OnOffException2 extends Exception {} ///:-
1* Output:
on
o ff
, /// , -
Nuestro objetivo es asegurarnos de que el conmutador esté cerrado cuando se complete la ejecución de main(), por lo que
situamos sw.off() al final del bloque try y al final de cada rutina de tratamiento de excepciones. Pero es posible que se gene-
re alguna excepción que no sea capturada aquí, en cuyo caso no se ejecutaría sw.off(). Sin embargo, con finally podemos
incluir el código de limpieza del bloque try en un único lugar:
JI : exceptionsfWithFinally . java
// Finally garantiza que se ejecuten las tareas de limpieza .
/ * Output:
Aquí, la llamada a sw.off( ) se ha desplazado incluyéndola en un único lugar, donde se garantiza que será reali zada indepen·
dientemente de lo que suceda.
Incluso en aquellos casos en qu e la excepción no es capturada en el conjunto actual de cláusulas catch, finally se ejecuta·
ría antes de que el mecanismo de tratamiento de excepciones continúe buscando una rutina de tratamiento adecuada en el
siguiente ni vel de orden superior:
// : exceptions/AlwaysFinally.java
// Finally siempre se ejecuta .
import static net.mindview.util.Print. * ¡
catch (FourException e) {
System.out.println(
" Caught FourException in 1st try block");
finally {
System.out.println("finally in 1st try block");
/ * Output :
Entering first try block
Entering second try block
finally in 2nd try block
Caught FourException in 1st try block
finally in 1st try block
* /// , -
La instrucción finally también se ejecutará en aquellas situaciones donde estén implicadas instrucciones break y continuc.
Observe que la cláusula finally junto con las instrucciones break y continue etiquetadas elimina la necesidad de una ins~
trucción goto en Java.
12 Tratamiento de errores mediante excepciones 299
Ejercicio 13: (2) Modifique el Ejercicio 9 a¡;adiendo una cláusula fin.U y. Verifique que la cláusula finally se ejecuta,
incl uso cuando se genera una excepción NulLPointcrExce ption .
Ejercicio 14: (2) Demuestre que OnOffSwitch.java puede fallar, generando una excepción RuntimeException dentro
del bloque try.
Ejercicio 15: (2) Demuestre que WithFinally,java no falla , generando una excepción RuntimeException dentro del
bloque try.
1* Output:
Initialization that requires cleanup
Poi nt 1
Performing cleanup
Initialization that requires cleanup
Po int 1
Po int 2
Performing cleanup
Initialization that requires cleanup
Po int 1
Point 2
Po int 3
Perfo r mi ng cleanup
Initiali za tion that requires c l eanup
Po int 1
Po int 2
Point 3
End
Performi ng cleanup
* /// , -
Podemos ver, en la salida del ejemplo, que no importa desde dónde vol vamos, ya que siempre se ejecuta la c láu sula finally .
300 Piensa en Java
Ejercic io 16: (2) Modifique reusing/CA DSystem.java para demostrar que si volvemos desde un punto siruado en la
mitad de una estmctura try- fin all y se seg uirán ejecutando adecuadamente las tareas de limpieza.
Eje rcicio 17: (3) Modifique polymorphism/Frog.j ava para que utilice la estrucrura try-fi nally con el fin de garantizar
que se lleven a cabo las tareas de limpieza y demuestre que esto funciona incluso si ejecutamos una ins-
tmcción retu r n en mitad de la estructura try-finall y.
catch (Exception e ) {
System.out.println (e ) i
1* Output:
A trivial exception
- /// ,-
Podemos ve r, analizando la salida, que no existe ninguna prueba de que se haya producido la excepción
Vcry lmport ant Exception, que es simplemente sustituida por la excepción HoHumE xce ption en la cláusula finall y. Se
trata de un fallo imponante, puesto que implica que puede pe rderse completamente una excepción, y además puede perder-
se de una fomla bastante más sutil y dificil de de tectar que en el ejemplo anterior. Por co ntraste, e++ considera como un
erro r de programación que se genere una segunda excepc ión antes de que la primera haya sido tratada. Quizá, una futura
versión de Java solventará este problema (por otro lado, no rma lmente lo que haremos será encerrar cualqu ier mé todo que
genere una excepción, C01110 es el caso de dispose( ) en el ejemplo anterior, dentro de una cláusula try-ca tch).
12 Tratamiento de errores mediante excepciones 301
Una forma todavía más simple de perder una excepción consiste en volver con return desde dentro de una cláusula finally :
11: exceptions/ExceptionSilencer.java
Si ejecutamos este programa, veremos que no produce ninguna salida, a pesar de que se ha generado una excepción.
Ejercicio 18: (3) Añada un segundo nivel de pérdida de excepciones a LostMessage.java para que la propia excepción
HoHum Exception sea sustituida por una tercera excepción.
Ejercicio 19: (2) Solucione el problema de LostMessage.java protegiendo la llamada contenida en la cláusula finally .
catchlFoul el {
System. out. println ( " Foul") ¡
catch (RainedOut el {
System. out _printIn ("Rained out ") i
catch(Baseball Exception e) {
System.out .printIn ( "Generic baseball exception") i
}
///,-
En Inning, podemos ver que tanto el constructor como el método event() especifican que generan una excepción, pero que
nunca lo hacen. Esto es legal, porque nos pem1ite obligar al usuario a capturar cualquier excepción que podamos añadir en
las versiones sustituidas de event(). La misma idea puede aplicarse a los métodos abstractos como podemos ver en atB at().
La interfaz Storm es interesante porque contiene un método (eve nt()) que está definido en Inning, y otro método que no
lo está. Ambos métodos generan un nuevo tipo de excepción, Rain edOu t. Cuando Storm ylnning amplía (ex tend s) Inning
e implementa (implements) Storm, podemos ver que el método event() de Storm /la puede cambiar la interfaz de excep-
ciones de cvcnt() definida en Inning. De nuevo, esto tiene bastante sentido porque en caso contrario nunca sabríamos si
estamos capturando el objeto correcto a la hora de trabajar con la clase base. Por supuesto, si un método descrito en una
interfaz no se encuentra en la clase base, como es el caso de r ainHard(), no existe ningún problema en cuanto a las excep·
ciones que genere.
12 Tratamiento de errores mediante excepciones 303
La restricción relativa a las excepciones no se aplica a los constructores. Podemos \'cr en Stormylnning que un construc-
tor puede generar todo aquello que desee. independientemente de lo que genere el constructor de la clase base. Sin embar-
go. puesto que siempre hay que invocar el constructor de la clase base de una forma o de otra (aquí se invoca el constructor
predetenninado de manera automática). el conslructor de la clase derivada deberá declarar todas las excepciones del cons-
tructor de la clase base en su propia especificación de excepciones.
Un cons tructor de la clase derivada no puede capturar las excepciones generadas por su constructor de la clase base.
La razón por la que Stormyln nin g.walk( ) no podrá compilarse es que genera una excepción. mientras que lnning.walk()
no lo hace. Si se pennitiera esto. entonces podríamos escribir código que in vocara a lnning.walk() y que no tuviera que
tratar ninguna excepción, pero entonces, cuando efectuáramos una sustitución y empleáramos un objeto de una clase deri-
vada de Inning. podrían generarse excepciones. con Jo que nuestro código fallaria. Obligando a los métodos de la clase deri-
vada a adaptarse a las especificaciones de excepciones de los métodos de la clase base. se mantiene la posibilidad de sustituir
los objetos.
El método sustituido eVf'nt() muestra que la versión de un método definido en la clase derivada puede elegir no generar
nin guna excepción, incluso a pesa r de que la versión de la clase sí las genere. De nuevo, no pasa nada por hacer esto, ya que
no dejarán de funcionar aquellos programas que se hayan escrito bajo la suposición de que la versión de la clase base gene-
ra excepciones. Podemos aplicar una lógica similar atBat( ), que genera PopFoul. una excepción que deriva de la excep-
ción Fo ul generada por la versión de la clase base de atBat(). De esta fonna, si escribimos código que funcione con Inning
y que invoque at8at( ). deberemos capturar la excepción Fou!. Puesto que PopFoul deri va de Foul. la rutina de tratamien-
10 de excepciones también pennitirá capturar PopFoul .
El último punto de interés se encuentra en main(). Aquí, podemos ver que. si estamos tratando co n un objeto que sea exac-
lamente del tipo Stormylnning, el compilador nos obligará a capturar únicamente las excepciones que sean específicas de
esa clase, pero si efectuamos una generali zación al tipo base. entonces el compilador (cOlTectamente) nos obligará a captu-
rar las excepciones de l tipo base. Todas estas restricciones penniten obtener un código de tratamiento de excepciones mucho
más robusto 6 .
Aunque es el compilador el que se encarga de imponer las especificaciones de excepciones en los casos de herencia, esas
especificaciones de excepciones no fomlan parte de la signatura de un método. que está compuesta sólo por el nombre del
método y los tipos de argumentos. Por tanto, no es posible sobrecargar los métodos basándose solamente en las especifica-
ciones de excepciones. Además, el hecho de que exista un a especificac ión de excepción en la ve rsión de la clase base de un
método no quiere decir que dicha especificación deba existir en la versión de la clase derivada del método. Esto difiere bas-
tante de las reglas nomlales de herencia, según las cuales un método de la clase base deberá también existir en la clase deri-
vada. Dicho de otra fonna, la "interfaz de especificación de excepciones" de un método concreto puede estrecharse durante
la herencia y cuando se realizan sustituc iones. pero lo que no puede es ensancharse~ se trata, precisamente. de la regla opues-
ta a la que se ap lica a la interfaz de una clase durante la herencia.
Ejerci cio 20 : (3) Modifique Storm ylnning.j ava añadiendo un tipo excepción UmpireArgument y una serie de méto-
dos que generen esta excepción. Compruebe la jerarquía modificada.
Constructores
Es importante que siempre nos hagamos la pregunta siguiente: "Si se produce una excepción, ¿se limpiará todo apropiada-
mente?"' La mayor parte de las veces, podemos eslar razonablemente seguros, pero con los constructores existe un proble-
ma. El constructor sitúa los objetos en un estado inicial seguro, pero puede realizar alguna operac ión (como por ejemplo
abrir un archivo) que no revierta hasta que el usuario termine con el objeto e invoque un método de limpieza especial. Si
generamos una excepción desde dentro de un constructor, puede que estas tareas de limpieza no se lleven a cabo apropia-
damente. Esto quiere decir que debemos tener un especial cuidado a la hora de escribir los constructores.
Podíamos pensar que la cláusula finally es una solución. Pero las cosas no son tan simples. porque finaUy lleva a cabo las
tareas de limpieza lodas las \'eces. Si un constructor fa lla en mitad de la ejecución, puede que no haya tenido tiempo de crear
alguna parte del objeto que será limpiado en la cláusu la linally.
6 El estándar ISO e++ ha añndido unas restricciones similares. que obligan a que las excepciones de los métodos derivados sean iguales a las excepcio-
nes gen~ntdas por los métodos de la clase base. o al menos a que deri\'en de ellas. Éste es uno de los casos en los que C++ es capaz de comprobar las espe-
cificaciones de excepciones en tiempo de compIlación.
304 Piensa en Java
En el siguiente ejemplo. se crea una clase denominada InputFile que abre un archivo y pemlite leerlo línea a línea. Utiliza
las clases FileReader y BufferedR •• der de la biblioteca estándar E/S de Java que se analizará en el Capítulo 18, E/S Estas
clases son lo suficientemente simples como para que el lector no tenga ningún problema a la hora de comprender los fun_
damentos de su utilización:
11: exceptions/lnputFile.java
II Hay que prestar a las excepciones en los constructores.
import java . io.*;
throw e¡ II Regenerar
finally (
II ¡ j i No cerrarlo aquí!!!
return s;
El constructor de InputFile toma un argumento String, que representa el nombre del archivo que queremos abrir. Dentro
de un bloque Iry, crea un objeto FileReader utili zando el nombre del archivo. Un objeto "ileReader no resulta particular-
mente útil hasta que lo empleemos para crear otro objeto BufferedReader (para lectura con buffer). Uno de los beneficios
de InputFile es que combina las dos acciones.
Si el constnlctor de FileReader falla, generará una excepción FileNolFoundException , que indicará que no se ha encon-
trado el archivo. Éste es el único caso en el cual no querernos cerrar el archivo, ya que no hemos llegado a poder abrirlo.
Cualquier aIra cláusula catch deberá cerrar el archivo, porque estará abierto en el momento de entrar en dicha cláusula
catch (por supuesto, el asunto se comp lica s i hay más de un método que pueda generar una excepción
FileNolFoundExceplion. En dicho caso, normalmente, habrá que descomponer las cosas en varios bloques Iry). El rnéto-
12 Tratamiento de errores mediante excepciones 305
do c1ose() puede generar una excepción, así qu e lo encerramos dentro de una ctáusula try y tratamos de capulrar la excep-
ción aú n cuando ese método se encuentre dentro del bloque de otra cláusula catch: para el compi lador de Java se trata sim-
plemente de un par adicional de símbolos de llave. Después de realizar las operaciones locales, la excepción se vuelve a
generar. lo cual resulta apropiado porque este constructor ha fallado y no queremos que el método que ha hecho la in voca-
ción asuma que el objeto se ha creado apropiadamente y es válido.
En este ejem plo, la cláusula finally no es. en modo alguno, el lugar donde ce rrar el archivo con c1osc(), ya que eso haría
que el archi vo se cerrara cuando el constructor completara su ejecución. Lo que queremos es que el archi vo continúe abier-
to mientras dure la vida útil del objeto InputFile.
El método getLinc() devuelve un objeto String que contiene la siguiente linea del archivo. Dicho método invoca a
readLinc(), que puede generar una excepción, pero dicha excepción es capturada, por lo que gctLinc( ) no genera excep-
ción alguna. Uno de los problemas de diseño relati vos a las excepciones es el de si debemos tratar una excepción comple-
tam ente en es te nivel. si sólo debemos tratarla parcialmente y pasar la misma excepción (u otra distinta) al ni vel siguiente,
o si debemos pasar la excepción directamente al siguiente nivel. Pasar la excepción directamente, siempre que sea apropia-
do, puede simplificar bastante el programa. En nu estro caso, el método getUnc() cOl/vierfe la excepc ión al tipo
RuntimeException para indicar que se ha producido un error de programación.
El método dispose() debe ser llamado por el usuario cuando ya no se necesite el método InputFile. Esto hará que se libe-
ren los recursos del sistema (como por ejemplo los descriptores de archivo) que estén siendo utilizados por los objetos
BuffercdReader y/o FileReader. Evidentemente, no queremos hacer esto hasta que hayamos tenninado de utili zar el obje-
to InputFile. Podríamos pensar en incluir dicha funcionalidad en un método finalize() , pero como hemos dicho en el
Capítulo 5, Inicialización y limpieza. no siempre podemos estar seg uros de que se vaya a llamar a finalize() (e, incluso si
estuviéramos seguros de que va a ser llamado, lo que no sabemos es cuándo). Ésta es una de las desve ntajas de Java: las
tareas de limpieza (excepn13ndo las de memoria) no ti enen lugar automáticamente, por lo que es preciso infonllar a los pro-
gramadores de clientes de que ellos son los responsables.
La fonna más segura de utilizar una clase que pueda ge nerar una excepción durante la construcción y que requiera que se
lleven a cabo tareas de limpieza consiste en emplear bloques try anidados:
11: exceptions jCleanup . java
ji Forma de garantizar la apropiada limpieza de un recurso.
catch(Exception el
System.out . println("InputFi l e construction failed lt } i
1* Output:
dispose() successful
, /// ,-
Examine cu idadosamente la lógica utilizada: la construcc ión del objeto InputFile se encuentra en su propio bloque try. Si
di cha construcción falla, se entra la clá usula catch extema y no se invoca el método dispose(). Sin embargo, si la construc-
ción tiene éxito, entonces hay que asegurarse de que el objeto se limpie, por lo que inmediatamente después de la construc-
ción creamos un nuevo bloque try. La cláusula finally que lle va a cabo las tareas de limpieza está asociada con el bloque
306 Piensa en Java
try interno; de esta fomla , la cláusul a finally no se ejec uta si la construcción fa lla, mientras que siempre se ejecuta si la
construcción ti ene éx ito.
Esta técnica general de limpieza debe utili zarse aún cuando el constmctor no genere ninguna excepción. La regla básica es:
justo después de crear un objeto que requiera limpieza, Lncluya una estmctura try-finally:
11 : exceptions / CleanupIdiom.java
II Cada objeto eliminable debe estar seguido por try-finally
II Sección 2:
II Si la construcción no puede fallar, podemos agrupar los objetos:
NeedsCleanup nc2 new NeedsCleanup{)¡
NeedsCleanup nc3 = new NeedsCleanup();
try (
11
finally
nC3.dispose {) ; II Orden inverso al de construcción
nc2.dispose ( ) ;
II Sección 3:
II Si la construcción puede fallar, hay que proteger cada uno:
try (
NeedsCleanup2 nc4 = new NeedsCleanup2{);
try (
NeedsCleanup2 ncS = new NeedsCleanup2();
try (
11
finally
ncS.dispose() i
/ * Output:
NeedsCleanup 1 disposed
NeedsCleanup 3 disposed
NeedsCleanup 2 disposed
NeedsCleanup 5 disposed
NeedsCleanup 4 disposed
*/// ,-
En main( ), la sección l nos resulta bastante se ncilla de entender: incluimos una estructura tr y-fi nall y después de un obje-
to eliminable. Si la constmcción del objeto no puede fallar, no es necesario incluir ninguna cláusula catch . En la sección 2,
podemos ver que los objetos con constructores que no pueden fallar pueden agruparse tanto para las tareas de construcción
como para las de limpieza.
La sección 3 muestra cómo tratar con aquellos objetos cuyos constmctores pueden fallar y que necesitan limpieza. Para
poder manejar adecuadamente esta situación, las cosas se complican, po rque es necesario rodear cada construcción con su
propia estructura try-catch, y cada construcción de objeto debe ir seguida de un tr y-fin all y para garantizar la limpieza.
Lo com plicado del tratamiento de excepciones en este caso es un buen argumento en favor de la creación de constructores
que no puedan fallar, aunque lamentablemente esto no siempre es posible.
Observe que si dispose() puede generar una excepción, entonces serán necesarios bloques tr)' adicionales. Básicamente, lo
que debemos hacer es pensar con cuidado en todas las posibi 1idades y protegernos frente a cada lIna.
Ejercicio 21: (2) Demuestre que un constTuctor de una clase derivada no puede capturar excepciones generadas por su
constnlClOr de la clase base.
Ejercicio 22 : (2) Cree una clase denominada FailingCo nstr uctor con un constructor que pueda fa\lar en mitad del pro-
ceso de constmcción y generar una excepción. En ma in(). escriba el código que pemlita protegerse apro-
piadamente frente a este faUo.
Ejercicio 23 : (4) Afiada una clase con un método d ispose( ) al ejercicio anterior. Modifique FailingCo nstructor para
que el constnlctor cree uno de estos objetos eliminables como un objeto miembro, después de lo cual el
constructor puede generar una excepción y crear un segundo objeto miembro eliminable. Escriba el códi-
go necesario para protegerse adecuadamente contra los fallos y verifique en ma in() que están cubiertas
todas las posibles situaciones de fa\lo.
Ejercicio 24: (3) Allada un método dispose( ) a la clase Faili ngConstr uctor y escriba el código necesario para utilizar
adecuadamente esta clase.
Localización de excepciones
Cuando se genera una excepción, el sistema de tratamiento de excepciones busca entre las mtinas de tratamiento "más cer-
canas", en el orden en que fueron escritas. Cuando encuentra una correspondencia, se considera que la excepción ha sido
tratada y no se continúa con el proceso de búsqueda.
Localizar la excepción correcta no requiere que haya una correspondencia perfecta entre la excepción y su rutina de trata-
miento. Todo objeto de una clase derivada se corresponderá con una rutina de tratamiento correspondiente a la clase base,
como se muestra en este ejemplo:
// : exceptions/Human.java
/ / Captura de jerarquías de excepciones.
1* Output:
Caught Sneeze
Caught Annoyance
* /// , -
La excepción Sneeze será capturada por la primera cláusula catch con la que se corresponda, que será por supuesto la pri-
mera. Sin embargo, si eliminamos la primera cláusula catch, dejando sólo la cláusula catch correspondiente a Annoyance,
el código seguirá funcionando porque se está capturando la clase base de Sneeze. Dicho de otra forma, catch(Annoyance
a) penniti rá capturar Ulla excepción Annoyance o cualquier clase derivada de ella. Esto resulta útil porque si dec idimos
añadir más excepciones derivadas a un método, no será necesario cambiar el código de los programas cliente, siempre y
cuando el cliente capnlre las excepciones de la clase base.
Si tratamos de "enmascarar" las excepciones de la clase derivada, incluyendo primero la cláusula catc h correspondiente a
la clase base, como en el siguiente ejemplo:
try (
throw new Sneeze();
catch (Annoyance al {
// ...
catch (Sneeze s)
//
el compilador nos dará un mensaje de error, ya que verá que la cláusula catch correspondiente a Sncezc nunca puede eje-
cutarse.
Ejercicio 25: (2) Cree una jerarquía de excepciones en tres niveles. Ahora cree una clase base A con un método que
genere una excepción de la base de nuestra jerarquía. Herede una clase B de A y sustituya el método para
que genere una excepción en el nivel dos de la jerarquía. Repita el proceso, heredando una clase e de B.
En main(), cree un objeto e y generalícelo a A, invoque el método a continuación.
Enfoques alternativos
Un sistema de tratamiento de excepciones es un mecanismo especial que permite a nuestros programas abandon ar la ejecu-
ción de la secuencia nonna! de instrucciones. Ese mecanismo especial se utiliza cuando tiene lugar una "condi ción excep-
cionar', tal que la ejecución nonnal ya no es posible o deseable. Las excepciones represe ntan condiciones que el método
actual no es capaz de ges tionar. La razón por la que se desarrollaron los sistemas de tratamiento de excepciones es porque
la técnica de gestionar cada posible condición de error producida por cada llamada a fun ción era demasiado onerosa, lo que
hacía que los programadores no la implementaran. Como resultado, se terminaba ignorando los errores en los programas.
Merece la pena recalcar que incrementar la comodidad de los programadores a la hora de tratar los errores fue una de las
principales motivac iones para desa rrotlar los sistemas de tratam iento de excepciones.
12 Tratamiento de errores mediante excepciones 309
Una de las directrices de mayor importancia en el tratamiento de excepciones es "no captures una excepción a menos que
sepa qué hacer con ella". De hecho, uno de los objetivos más importantes del tratamjento de excepciones es quitar el códi-
go de tratamiento de errores del punto en el que los errores se producen. Esto nos pennite concentramos en lo que quere-
mos conseguir en cada sección del código, dejando la manera de tratar con los problemas para ulla sección separada del
mismo código. Como resultado, el código principal no se ve oscurecido por la lógica de tratamiento de errores, con lo que
resulta mucho más fácil de co mprender y de mantener. Los mecanismos de tratamiento de excepciones también tienden a
reducir la cantidad de código dedicado a estas tareas. pemlitiendo que una rutina de tratamiento dé servicio a múltip les luga-
res posibles de generación de errores.
Las excepciones comprobadas complican un poco este escenario, porque nos fuerzan a añadir cláusulas catch en lugares en
los que puede que no estemos listos para gestionar un error. Esto puede dar como resultado que no se traten ciertas excep-
ciones:
try {
II ... hacer algo útil
} catch IObligatoryException e l {} / / Glub!
Los programadores (incluido yo en la primera edición de este libro) tienden a hacer lo más simple y a capturar la excepción
olvidándose luego de tratarla; esto se hace a menudo de forma inadvertida, pero una vez que lo hacemos, el compilador se
queda satisfecho, de modo que si no nos acordamos de revisar y corregir el código, esa excepción se pierde. La excepción
tiene lugar, pero desaparece todo rastro de la misma una vez que ha sido capturada y se deja sin tratar. Puesto que el com-
pilador nos fuerza a escribir código desde el principio para tratar la excepción, incluir una rutina de tratamiento vacía pare-
ce la solución más simple, aunque en realidad es lo peor que podemos hacer.
Horrorizado al damlc cuenta de que yo había hecho precisa mente esto, en la segunda edición del libro "corregí" el proble-
ma imprimiendo la traza de la pila dentro de la mtina de tratamiento (como puede verse apropiadamente en varios ejemplos
de este capítulo). Aunque resulta útil trazar el comportamiento de las excepciones, esta forma de proceder sigue indicando
que realmente no sabemos qué hacer con esa excepción en dicho punto del código. En esta sección, vamos a estudiar algu-
nos de los problemas y algunas de las complicaciones derivadas de las excepciones comprobadas, y vamos a repasar las
opciones que tenemos a la hora de tratar con ellas.
El tema parece bastante simple, pero no sólo resulta complicado sino que también es motivo de controversia. Hay personas
que sostienen finnemente los argumentos de ambos bandos y que piensan que la respuesta correcta (es decir, la suya) resul-
ta tremendamente obvia. En mi opinión, la razón de que den estas posiciones tan vehementes es que resulta bastante obvia
la ventaja que se obtiene al pasar de un lenguaje con un pobre tratamiento de los tipos, como el previo ANSI e a un lengua-
je fuertemente tipado con tipos estáticos (es decir, comprobados en tiempo de compilación) como e++ o Java. Cuando se
hace esa transición (como hice yo mi smo), las ve ntajas resultan tan evidentes que puede parecer que la comprobación está-
tica de tipos es siempre la mejor respuesta a la mayoría de los problemas. Mi esperanza con las líneas que siguen es que, al
relatar mi propia evolución, el lector pueda ver que el valor absoluto de la comprobación estática de tipos es cuestionable;
obviamente, resulta muy útil la mayor parte de las veces, pero hay un línea tremendamente difusa a partir de la cual esa com-
probación estática de tipos comienza a ser un estorbo y un problema (una de mis citas favoritas es la que dice "todos los
modelos son erróneos, aunque algunos de ellos resultan útiles").
Historia
Los sistemas de tratamiento de excepciones tienen su origen en sistemas como PLII y Mesa, y posteriormente se incorpo-
raron en CLU, Smalltalk, Modula-3, Ada, Eiffel, C++, Python, Java y los lenguajes post-Java como Ruby y C#. El di seño
de Java es similar a C++-, excepto en aquellos lugares en los que los diseñadores de Java pensaron que la técnica usada en
C++ podría causar problemas.
Para proporcionar a los programadores un marco de trabajo que estuvieran más di spuestos a utilizar para el tratamiento y la
recuperación de errores, el sistema de tratamiento de excepciones se añadió a e++ bastante tarde en el proceso de estanda-
rización, promovido por Bjame Stroustrup, el autor original del lenguaje. El modelo de las excepciones de e++- proviene
principalmente de CLU. Sin embargo, en aquel entonces existían otros lenguajes que también soportaban el tratamiento de
excepciones: Ada, Smalltalk (ambos tienen excepciones, pero no tienen especificaciones de excepciones) y Modula-3 (que
incluía tanto las excepciones como las especificaciones).
310 Piensa en Java
En su artículo pionero 7 sobre el tema, Liskov y Snyder observaron que uno de los principales defectos de los lenguajes tipo
e, que infomlan acerca de los errores de manera transitoria es que:
..... toda im'ocaciólI debe ir seguida de una prueba incondicional para determinar cuál ha sido el
resultado. Este requisito hace que se desarrollen programas dificiles de leer y que probablemente tam-
bién 5011 poco ejicie11les, lo que tiende a desanimar a los programadores a la hora de .,·eliali:ar y tra-
tar las excepciones. "
Por tanto. uno de los motivos originales para desarrollar sistemas de tratamiento de excepciones era eliminar este requisito,
pero con las excepciones comprobadas en Java nos encontramos precisamente con este tipo de código. Los autores conti-
núan diciendo:
.. ... si se requiere que se asocie el texto de tilla rutina de tratamiento a la invocación que genera la
excepción, el resultado serán programas poco legibles en los que las expresiones estarán descompues-
tas debido a la presencia de las rutinas de tratamiento. "
Siguiendo el enfoque adoptado en CLU, Stroustrup afinnó, al diseñar las excepciones de e ++, que el objetivo era reducir
la cantidad de código requerida para recuperarse de los errores. En mi opinión, estaba partiendo de la observación de que
los programadores no solían escribir código de tratamiento de errores en e debido a que la cantidad y la colocación de dicho
código en los programas era muy dificil de manejar y tendía a distraer del objetivo principal del programa. Como resulta-
do, los programadores solian abordar el problema de la misma manera que en e. ignorando los errores en el código y utili-
zando depuradores para localizar los problemas. Para usar las excepciones, había que convencer a estos programadores de
e de que escribieran código "adiciona!", que normalmente no escribirían. Por tanto, para hacer que pasen a adoptar una
fonna más eficiente de tratar los errores, la cantidad de código que esos programadores deben "ai'iadir" no debe ser excesi-
va. Resulta importante tener presente este objetivo de diseño inicial a la bora de eliminar los efectos que las excepciones
comprobadas tienen en Java.
e++ tomó prestado de CLU una idea adicional: la especificación de excepción, mediante la cual se enuncian programática-
mente en la signatura del método las excepciones que pueden generarse como resultado de la llamada al método. La espe-
cificación de excepciones tiene, en realidad, dos objetivos. Puede querer decir: "Puedo generar esta excepción en mi código,
encárgate de tratarla", Pero también puede significar: "Estoy ignorando esta excepción que puede producirse como resulta-
do de mi código, encárgate de tratarla'". Hasta ahora, nos estamos centrando en la parte que dice "encárgate de tratarla" a la
hora de examinar la mecánica y las sintaxis de las excepciones, pero lo que en este momento concreto nos interesa es el
hecho de que a menudo ignoramos las excepciones que se producen en nuestro código, y eso es precisamente lo que la espe-
cificación de excepciones puede indicar.
En C++, la especificación de excepciones no fornla parte de la infonnación de tipo de una función (la signatura). La única
comprobación que se realiza en tiempo de compilación consiste en garantizar que las especificaciones de excepciones se
utilizan de manera coherente, por ejemplo, si una función o un método generan excepciones, entonces las versiones sobre-
cargadas o derivadas también deberán generar esas excepciones. A diferencia de Java, sin embargo, no se realiza ninguna
comprobación en tiempo de compilación para detenninar si la función o método va a generar en realidad dicha excepción.
o si la especificación de excepciones está completa (es decir, si describe con precisión todas las excepciones que puedan ser
generadas). Esa va lidación sí que se produce, pero sólo en tiempo de ejecución. Si se genera una excepción que viola la
especificación de excepciones, el programa e++ invocará la función de la biblioteca estándar unexpected().
Resulta interesante observar que. debido al uso de plantillas, las especificaciones de excepciones no se utilizan en absoluto
en la biblioteca estándar de C++. En Java, existen una serie de restricciones que afectan a la forma en que pueden emplear-
se los genéricos Java con las especificaciones de excepciones.
Perspectivas
En primer lugar. merece la pena observar que es el lenguaje Java el que ha inventado las excepciones comprobadas (inspi-
radas claramente en las especificaciones de excepciones de e++ y en el hecho de que los programadores e++ no suelen ocu-
parse de las mismas). Sin embargo, se trata de un experimento que ningún lenguaje subsiguiente ha incorporado.
7 Barbara Liskov y Alan Snydcr. Exceplion Handling in CLU. IEEE Transactions on Sonware Enginecring. Vol. SE-5, No. 6. Noviembre 1979. Este ant-
culo no c~ta disponible en Internet. sino sólo en copia impresa. por lo que tendrá que encargar una copia a tra\cs de su biblioteca.
12 Tratamiento de errores mediante excepciones 311
En segundo lugar, las excepciones comprobadas parecen ser algo "evidentemente bueno" cuando se las contempla dentro
de ejemplos de nivel introductorio y en pequeños programas. Según algunos autores, las dificultades más sutiles comienzan
a aparecer en el momento en que los programas crecen de tamaño. Por supuesto. el tamaiio de los programas no suele incre-
mentarse de manera espectacular de la noche a la mañana. sino que lo más normal es que los programas vayan creciendo de
tamaño poco a poco. Los lenguajes que puedan no ser adecuados para proyectos de gran envergadura, se utilizan sin pro-
blema para proyectos de pequeño tamaño. Pero esos proyectos crecen y. en algún punto, nos damos cuenta de que las cosas
que antes eran manejables ahora son relativamente dificiles. A eso es a lo que me refería al comentar que los mecanismos
de comprobación de tipos pueden llegar a hacerse engorrosos: en particular. cuando esos mecanismos se combinan con el
concepto de excepciones comprobadas.
El tamaño del programa parece ser una de las cuestiones principales. Y esto es, en sí mismo, un problema porque la mayo-
ría de los análisis tienden a utilizar como ilustración programas de pequeilo tamaí1o. Uno de los diseñadores de C# escribió
que:
"El examen de programas de pequeiio lamaiio nos I/e\'a a la conclusión de que imponer el uso de espe-
cificaciones de excepciones podría mejorar tanto la productividad del desarrollador como la calidad
de código, pero la experiencia con los grandes proyecfOs de desarrollo software sugiere un resultado
completamente distinto: 11110 menor productividad y un incremento en la calidad del código que es,
como mucho. poco significativo, .. 8
En referencias a las excepciones no capturadas, los creadores de CLU escribían:
"Pensamos que era poco realista exigir al programador que proporcionara rutinas de tratamiento en
aquellas situaciones en las que no es posible llevar a cabo ninguna acción con verdadero significa-
do. "9
A la hora de explicar por qué una declaración de función sin ninguna especificación significa que la función pueda generar
cualquier excepción en lugar de ninguna excepción. Stroustrup escribe:
"Sin embargo, eso requeriria que se incluyeran especificaciones de excepción para casi todas lasfim-
ciones. haría que fuera necesario efectuar muchas recompilaciones y d{ficultaría la cooperación con
el software escrito en otros lenguajes, Esto animaría a los programadores a subvertir los mecanis-
mos de tratamiento de excepciones y a escribir código espúreo para suprimir las excepciones.
Proporcionarla un falso sentido de seguridad a las personas que no se hubieran dado cuenta de la
excepción, .. 10
Precisamente, con las excepciones comprobadas en Java podemos ver que se produce precisamente esta reacción: tratar de
subvertir las excepciones.
Martin Fowler (autor de UAfL Distilled, Refactoring y Analysis Patlerns) escribía en cierta ocasión lo siguiente:
·· ... en conjunto. creo que las excepciones son buenas. pero las excepciones compmbadas en Java
callsan más problemas de los que resuelven. .,
Actualmente, lo que opino es que el paso más importante dado por Java fue unificar el modelo de información de errores,
de modo que de todos los errores se informa utilizando excepciones, Esto no sucedía en C++. porque. debido a la compati-
bilidad descendente con C, seguía estando disponible el modelo de limitarse a ignorar los elTores, Pero, cuando disponemos
de un mecanismo de infonne de errores coherente con excepciones, las excepciones pueden utilizarse si se desea y. en caso
contrario, se propagarán al siguiente nivel superior (la consola u otro programa contenedor). Cuando Java modificó el mode-
lo e++ para que las excepciones fueran la única fonna de infom1ar de los errores, la imposición <tdicional relativa a las
excepciones comprobadas puede que haya dejado de ser tan necesaria.
En el pasado, creía firmemente que tanto las excepciones comprobadas como la comprobación estática de tipos resultaban
esenciales para el desarrollo de programas robustos. Sin embargo, tanto la infoll11ación proporcionada por otros programa-
10 Bjame Strous{mp. rIJe c++ Progmmming Language. 3 ~ EdilicJl/ (Addison-\Vcs ley. 1997), p. 376.
312 Piensa en Java
dores como mi experiencia directa 11 con lenguajes que 5011 más dinámicos que estáticos, me han hecho pensar que las mayo~
res ventajas provienen en realidad de:
t. Un modelo unificado de infonne de errores basado en excepciones. independientemente de si el programador está
obligado por el compilador a tratarlas.
2. Mecanismos de comprobación de tipos, independientemente de cuándo terna lugar esa comprobación. Es decir.
en tanto que se imponga un uso apropiado de los tipos, a menudo no importa si eso sucede en tiempo de compi.
lación o de ejecución.
Además, se puede aumentar muy significativamente la productividad si se reducen las restricciones impuestas al programa.
dor en tiempo de compilación. De hecho, se necesita algo de reflexión junto con los genéricos. para compensar la naturale_
za demasiado restrictiva del mecanismo estático de tipos, C0l110 podrá ver en una serie de ejemplos a lo largo del libro.
Algunas personas me han dicho que lo que digo constituye una auténtica blasfemia y que al expresar estas ideas mi reputa-
ción quedará irremediablemente destruida, la civilización se vendrá abajo y un mayor porcentaje de los proyectos de pro-
gramación fallarán . La creencia de que el compilador puede ser la salvación de nuestro proyecto, al indicarnos los errores
en tiempo de compilación, se basa en evidencias bastante fuertes , pero es todavía más importante que nos demos cuenta de
las limitaciones que afectan a lo que el compilador es capaz de hacer. En el suplemento disponible en http://MindVielt'.
l1et/Books/BetterJava, se hace un gran hincapié en el valor que tienen los procesos automatizados de construcción de pro-
gramas y las pruebas de las unidades componentes de un programa, mecanismos ambos que nos ayudan mucho más que el
tratar de convertirlo todo en un error sintáctico. Merece la pena recordar que:
"Un buen lenguaje de programación es aquél que ayuda a los programadores a escribir buenos pro-
gramas. Ningún lenguaje de programación podrá impedir siempre que sus usuarios escriban malos
programas. ,. 12
En cua lqu ier caso, la probabilidad de que las excepciones comprobadas sea n eliminadas de Java parece muy baja. Sería un
cambio de lenguaje demasiado radical y los que se oponen a esa eliminación dentro de Sun parecen tener bastante fuerza.
Sun tiene una larga historia (y una oonna) de ser siempre compatible en sentido descendente; de hecho, casi todo el softwa-
re de SUD se ejecuta en lodo el hardware Sun, independientemente de lo antiguo que éste sea. Sin embargo, si llega a tener
la se nsación de que algunas excepciones comprobadas están empezando a ser engorrosas, y especialmente si se encuentra
con tinuamente en la obligación de capturar excepciones para luego no saber qué hacer con ellas, existen ciertas alternati vas.
11 Ind irectamente con Smalltalk a traves de conversaciones con muchos programadores con experiencia en dicho lenguaje; di reCiamente con pylhl1n
( lI'WII'.PylllOll.Ol'g).
12 Kees Kosler. diseñador del lenguaje COL. citado por Benrand Meyer, diseñador del lenguaje Eiffcl. wlI1\celj.com/eljlvllnllbm/riglu/.
12 Tratamiento de errores mediante excepciones 313
Observe que main( ) es un método que también puede tener una especificación de excepciones, y en es te ejemplo el tipo de
la excepción es Exception , que es la c lase raíz de todas las excepciones comprobadas. Pasando la excepción a la consola,
nos vemos liberados de la obligación de incluir cláusulas try-catch dentro del cuerpo de main( ) (lamentablemente, la E/S
de archivo es sign ifica ti vamente más compleja de lo que parece en este ejemplo, así que no se anime en exceso hasta que
baya leido el Capimlo 18, E/S).
Eje rcicio 26: (1) Cambie la cadena de caracteres que especifica el nombre del archivo en MainException,java para pro-
porcionar un nombre de archivo que no exista. Ejecute el programa y anote el resul tado.
Esto parece una sol ución ideal si queremos "desactivar" la excepción comprobada: no la perdemos y no tenemos porqué
incluirla en la especificación de excepciones de nuestro método; además, debido al encadenamiento de excepciones no per-
demos ninguna información relativa a la excepción original.
Esta técnica proporciona la opción de ignorar la excepción y dejarla progresar por la pila de llamadas, si n vemos obligados
a escribir cláusulas try-catch y/o especificaciones de excepciones. Sin embargo, seguimos pudiendo capturar y tratar la
excepción específica, usando getCause(), como se muestra a continuación:
11: exceptions/TurnOffChecking.java
II IIDesactivación" de excepciones comprobadas.
import java.io.*¡
import static net.mindview.util.Print.*¡
class WrapCheckedException {
void throwRuntimeException (int type) {
try (
switch(typel
case o: throw new FileNotFoundException() ¡
case 1: throw new IOException() ¡
case 2: throw new RuntimeException("Where am I?") i
default: return¡
/* Output :
FileNot FoundException: java . io . FileNot FoundExcep tion
IOException: java.io.IOException
Throwable : java . lang . RuntimeEx ception : Wher e am I ?
SomeOtherException : SomeOtherEx cep t ion
* ///>
WrapCheckedExccption.throwRuntimeException( ) contiene código que genera diferentes tipos de excepciones. Éstas
se capturan y se envuel ven dentro de objetos RuntimeException , asi qu e se convierten en la "causa" de dichas excepcio-
nes.
En TurnOffChecking, podemos ver que es posible in vocar throwRuntimeException( ) sin ningún bloque try porque el
método no genera ninguna excepción comprobada. Sin embargo, cuando estemos listos para capturar las excepciones, segui-
remos teniendo la posibilidad de capturar cualquier excepción que queramos poniendo nuestro código dentro de un bloque
try. Comenzamos capturando todas las excepciones que sabemos explícitamente que pueden emerger del código incluido
dentro del bloque try; en este caso. se captura primero SomeOtherException . Finalmente, se captura RuntimeException
y se genera con throw el resultado de getCa use() (la excepción envuelta). Esto extrae las excepciones de origen, que pue-
den se r entonces tratadas en sus propias cláusulas catch.
La técnica de envolver una excepción comprobada en otra de tipo RuntimeException se utili zará siempre que sea apropi a-
do a lo largo del resto del libro. Otra solución consiste en crear nuestra propia subclase de RuntirneException . De esta
forma, no es necesari o capturarla, pero alguien puede hacerlo si así lo desea.
Ejercicio 27 : ( 1) Modifique el Ejercicio 3 para convertir la excepción en otra de tipo RuntimeException .
Ejercicio 28: ( 1) Modifique el Ejercicio 4 de modo que la clase de excepción personali zada herede de
RuntimeException, y muestre que el compi lador nos pem1ite no incluir el bloque try.
Ejercicio 29: ( 1) Modifique todos los tipos de excepción de Stormylnning.java de modo que extiendan
RuntimeException, y muestre que no son necesarias especificaciones de excepción ni bloques try.
Elimine los comentarios 'II! ' y muestre cómo pueden compi larse los métodos sin especificaciones.
Ejercicio 30: (2) Modifique Human.java de modo que las excepciones hereden de RuntimeException. Modifique
maine ) de modo que se utilice la técnica de TurnOffChecking.java para tratar los diferentes tipos de
excepc iones.
12 Tratamiento de errores mediante excepciones 315
Resumen
Las excepciones son una parte fundamental de la programación Java; no es mucho lo que puede hacerse si no se sabe cómo
trabajar con ellas. Por esa razón, hemos decidido introducir las excepciones en este punto del libro; hay muchas bibliotecas
(corno las de E/S, mencionadas anterionnente) que no pueden emplearse sin tratar las excepciones.
Una de las ventajas del tratamiento de excepciones es que nos permite concentramos en un cierto lugar en el problema que
estemos tratando de resolver, y tratar con los errores relati vos a dicho código en otro lugar. Y, aunque las excepciones se
suelen explicar como herramientas que nos penniten informar acerca de los errores y recuperarnos de ellos en tiempo de
ejecución, no es tan claro con cuánta frecuencia se implementa ese aspecto de "recuperación", como tampoco está muy claro
si resulta siempre posible. Mi percepción es que la recuperación es posible en no más del I O por cierto de los casos, e inclu-
so en esas sihlaciones sólo consiste en devolver la pila a un estado estable conocido, más que reanudar el procesamiento del
programa. Pero, sea esto verdad o no, lo importante es que el valor fundamental de las excepciones radica en la función de
"informe de errores". El hecho de que Java insista en que se ¡nfoone de todos los errores mediante las excepciones es lo que
le proporciona a este lenguaje una gran ventaja respecto a otros lenguajes como C++. que permite infonnar de los errores
de distintas manera o incluso no infonnar en absoluto. Disponer de un sistema coherente de infom1e de errores implica que
no tenemos ya por qué hacemos la pregunta de "¿se nos está colando algún error por alguna parte?" cada vez que escriba-
mos un fragmento de código (siempre y cuando, no capturemos las excepciones para luego dejarlas sin tratar).
Como podrá ver en futuros capítulos, al pennitirnos olvidarnos de esta cuestión (aunque sea generando una excepción de
tipo RuntimeException), los esfuerzos de diseño e implementación pueden centrarse en otras cuestiones más interesantes
y complejas.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tlle Thinking in Java Annorated So/mion Guide, disponible para
la venta en \n~'W.MindVie\\'.nel.
Cadenas de
caracteres
Esto resulta especialmente cierto en los sistemas web, en los que Java se ut ili za ampliamente. En este capítul o, vamos a exa-
minar más en detalle la que constituye, ciertamente, la clase más comúnmente utilizada de todo el lenguaje, String, junto
con sus utilidades y clases asoc iadas.
/* Out put:
howdy
HOWDY
howdy
- 111>
Cuando se pasa q a upcase( ) se trata en realidad de una copia de la referen cia a q. El objeto al que esta referencia está
conectado pennanece en una única ubicación física. Las referencias se copian a medida que se las pasa de un si lio a a Iro.
Examinando la definición de upcasc( ), podemos ver que la refe rencia que se le pasa ti ene un nombre s, y que di cha refe-
rencia existe sólo mientras que se ejecuta el cuerpo de upcase(). Cuando se completa upcase( ), la referencia local s desa-
parece. upcasc( ) devuelve el resultado, que es la cadena original con todos los caracteres en mayúscula. Por supuesto, lo
que se devue lve en rea lidad es una referencia al resultado. Pero lo cierto es que la referencia que se devuelve apunta a un
nuevo objeto, dejándose sin modi ficar el objeto al que apuntaba la referencia q original.
Este comportamiento es n0011ahnente el que deseamos. Suponga que escribimos:
318 Piensa en Java
String s = "asdf";
String x = Immutable.upcase(s};
¿Realmente queremos que el método upcase( ) mod(fique el argumento? Para el lector del código, los argumentos suelen
aparecer como fragmentos de infon11ación proporcionados al método, no como algo que haya que modificar. Esta garantía
es impol1ante. ya que hace que el código sea más fácil de escribir y comprender.
J* Output:
abcmangodef47
* ///,-
Queremos tratar de imaginar cuál sería la forma en que este mecanismo funciona. El objeto String "abe" podría tener un
método append( ) que creara un nuveo objeto String que contuviera "abe" concatenado con el contenido de mango. El
nuevo objeto String crearía entonces otro objeto String que añadiera "der', etc.
Esto podría funcionar. pero requiere la creación de un montón de objetos String simplemente para componer esta nueva
cadena de caracteres. lo que conduciría a que hubiera varios objetos String intermedios a los que habría que aplicar poste-
riormente los mecanismos de depuración de memoria. Sospecho que los diseñadores de Java intentaron en primer lugar esta
solución (lo cual constituye una de las lecciones aplicándose al diseño software: en realidad no sabemos nada acerca de un
sistema hasta que lo probamos con código y obtenemos algo que funcione). También sospecho que descubrieron que esta
solución presentaba un rendimiento inaceptable.
Para ver lo que sucede en realidad podemos descompilar el código anterior utilizando la herramienta javap, que está inclui-
da en el JDK. He aquí la línea de comandos necesaria:
javap -e Concatenation
El indicador -c genera el código intennedio NM. Después de quitar las partes que no nos interesan y editar un poco los
resultados. he aquí el código intemledio relevante:
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args slze~l
o: ldc #2; J/String mango
I C++ pernlite al programador sobrecargar los operadores a voluntad. Como esto puede ser a menudo bastante complicado (I'éase el Capínilo 10 de
Thinkillg in C+ +. rEdición. Prenticc Hall , 2000), los diseñadores de Java consideraron que era una característica "indeseable" que no habia que incluir
en Java. Resulta gracioso que al final terminaran ellos mismos recurriendo a la sobrecarga de operadores y, lo que todavía resulta mas irónico, se da la cir-
cunstancia de que la sobrecarga de operadores resultaría mucho mas fácil en Java que en C++. Esto es lo que sucede en Python (I'éa~e WlVW.Prr}¡oll.org)
y CII, que disponen de mecanismos tanto de depuración de memoria como de sobrecarga sencilla de los operadores.
13 Cadenas de caracteres 319
2: astare 1
3: new #3; I/class StringBuilder
6, dup
7: invokespecial #4; //StringBuilder."<init>lt: ()
10: ldc #5; I/String abe
12: invokevirtual #6; / /StringBuilder append: (Stri ng )
15: aload 1
16: invokevirtual #6¡ / /StringBuilder append: (String)
19, lde #7; //String def
21: invokevirtual #6; / /StringBuilder append: (String)
24: bipush 47
26: invokevirtual #8; //StringBuilder append: (I)
29: invokevirtual #9; / /StringBuilder. toString: ()
32: astore_2
33: getstatic #10; / /F ield System. out: PrintStream;
36: aload 2
37: invokevirtual #11; JI PrintStream.println: (St ringl
40: return
Si tiene expe riencia con el lenguaje ensamblado r, puede que este código le resulte fami liar: las instrucciones como dup e
invokevirtual son los equivalentes en la máquina virtual Ja\'3 (NM) al lenguaje ensamblador. Si nunca ha visto lenguaje
ensamblador. no se preocupe: lo que hay que observar es que el compi lador introduce la clase java.lang.StringBuilder. No
había ninguna mención de StringBuilder en el código fuente. pero el compi lador ha decidido utilizarlo por su cuenta, por-
que es mucho más eficiente.
En este caso, el compilador crea un objeto StringBuilder para crear la cadena de caracteres s. y llama a append( ) cuatro
veces, una para cada uno de los fragmentos. Finalmente, in voca a toString() para generar el resultado, que almacena (con
a,lore_2) como ,.
Antes de dar por supuesto que lo que hay que hacer es uti lizar cadenas de caracteres por todas partes y que el compilador
se encargará de que todo sea eficiente. examinemos un poco más en detalle lo que el compilador está haciendo. He aquí un
ejemplo que genera un resultado de tipo String de dos maneras: utilizando cadenas de caracteres y realizando la codifica-
ción de fonna manual con StringBuilder:
jj: stringsjWhitherStringBuilder.java
Ahora, si ejecutamos javap -c WitherSlringBuilder. podemos ver el código (simplificado) para los dos métodos diferen-
tes. En primer lugar, impLicit( ):
public java.lang.String implicit(java.lang.String[]);
Code:
O, lde #2; / /String
2, astore - 2
3, iconst O
4,
-
istore - 3
5, iload_ 3
320 Piensa en Java
6: aload 1
7: arraylength
8: if_icmpge 38
11: new #3; I/class StringBuilder
14: dup
15: invokespecial #4; // StringBuilder."<init:>":()
18: alead 2
19: invokevirtual #5; // StringBuilder.append: ()
22: alead 1
23: iload 3
24: aaload
25: invokevirtual #5; // StringBuilder.append: ()
28: invokevirtual #6 i / / StringBuilder. toString: ()
31: astare 2
32: iinc 3, 1
35: goto 5
38: aload 2
39: areturn
Observe las líneas 8: y 35: , que forman un bucle. La línea 8: rea li za una "comparación entera de tipo mayor o igual que"
con los operandos de la pila y salta a la línea 38: cuando el bucle se ha terminado. La línea 35: es una instrucción de salto
que vuelve al principio del bucle, en la línea 5:. Lo más importante que hay que observar es que la construcción del objeto
StringBuilder tiene lugar dentro de este bucle, lo que quiere decir que obtenemos un nuevo objeto StringBuilder cada vez
que pasemos a través del bucle.
He aqui el código intennedio correspondiente a explicit():
public java.lang.String explicit(java.lang.String[]);
Cede:
O: new #3; / /cla ss StringBuilder
3, dup
4: invokespecial #4; / / StringBuilder." <ini t:>" : ()
7: astore 2
8: iconst O
9: istore 3
10: iload 3
11: aload 1
12: arraylength
13: lf lcmpge 30
16: alead 2
17: aload 1
18 : iload 3
19: aaload
20: invokevirtual #5; /1 StringBuilder.append: ()
23, pop
24: iinc 3, 1
27: goto 10
30: aload 2
31: invokevirtual #6; jj StringBuilder.toString: ()
34: areturn
No sólo es el código de bucle más corto y más simple, sino que además el método sólo crea un único objeto StringBuilder.
La creación de un objeto StringBuilder explícito también nos pemlite preasignar su tamaño si d isponemos de infonnación
adiciona l acerca de lo grande que debe ser, con lo cual no es necesario vo lver a reasignar constantemente el buffel:
Por tanto, cuando creamos un método toString( ), si las operaciones son lo suficientemente simples como para que el com·
pilador pueda figurarse el sólo cómo hacerlas, podemos generalmente confiar en que el compilador construirá el resultado
de una fonna razonab le. Pero si hay bucles, conviene utili zar explícitamente un objeto StringBuUder en el método
toString( ), como se hace a continuación:
11 : stringsjUsingStringBuilder.java
13 Cadenas de caracteres 321
/ * Output:
[58, 55, 93, 61, 6 1, 29, 68, O, 22, 7, 88, 28, 51, 89, 9, 78, 98, 61, 20, 58, 16, 40,
11, 22, 4]
*///,-
Observe que cada parte del res ultado se añade con una instmcción append( ). Si tratamos de seguir un atajo y hacer algo
como append(a + ": " + e), el compilador saldrá a la palestra y comenzará a construir de nuevo más objetos StringBuilder.
Si tiene duda acerca de qué técnica utilizar, siempre puede ejecutar javap para comprobar los resultados.
Aunque StringBuilder dispone de un conjunto completo de métodos, incluyendo insert(), replace(), substring( ) e inclu-
so reverse( l, los que generalmente se usan son append( ) y toString( ). Observe el uso de delcte( ) para eliminar la últi-
ma coma y el último espacio antes de añadir el corchete de cierre.
StringBuilder fue introducido en Java SE5. Antes de esta versión, Java utili zaba StringBuffer, que garantizaba la seguri-
dad en lo que respecta a las hebras de programación (véase el Cap ítulo 21 , Concurrencia) y era, por tanto, significativa men-
te más caro en términos de recursos de procesamiento. Por tanto, las operaciones de manejo de caracteres en Java SE5/6
deberían ser más rápidas.
Ejercicio 1: (2) Analice SprinklerSystem.toString( ) en reusing/SprinklerSystem.java para descubrir si escri bir el
método toString( ) con un método StringBuilder explícito pennitiría abarrar operaciones de creación de
objetos StringBuilder.
Recursión no intencionada
Puesto que los contenedores estándar Java (al igual que todas las demás clases) heredan en último término de Object, todos
ellos contienen un método toString( ). Este método ha sido sustituido para que los contenedores puedan generar una repre-
sentación de tipo String de sí mismos, incluyendo los objetos que almacenan. ArrayList.toString( ), por ejemplo, recorre
los elementos del objeto Arr.yList y llama a toString() para cada uno de ellos:
ji : stringsjArrayListDisplay.java
impo rt generics.coffee.*¡
import java.util.*¡
/* Output:
322 Piensa en Java
El compilador ve un objeto String seguido de un símbolo '+ ' y algo que no es un objeto Slring, por lo que trata de con-
vertir Ihis a String. El compilador reali za esta conversión llamando a toString(), que es lo que produce una llamada recur-
siva.
Si queremos impri mi r la dirección del objeto, la solución es llamar al método toString() de Object. y hace precisamente
eso. Por tanto, en lugar de especi ficar Ihis, lo que tendríamos que escribir es super.toString().
Ejercicio 2: (1) Corrija el error de lnfiniteRecursion.java.
getChars( ), gelBytes( ) El principio y el final del que hay que copiar, Copia caracteres o bytes en una matriz ex·
la matriz a la que hay que copiar, un índice a tema.
la matriz de dest ino.
13 Cadenas de caracte res 323
equals( ), Un objeto String con el que comparar. Una comprobación de igualdad de los conteni-
equals-lgnoreCase( ) dos de dos objetos String.
compareTo( ) Un objeto St rin g con el que comparar. El resultado es negativo, cero o positivo
dependiendo de la ordenación lexicogrática
del objeto String y del argumento. ¡Las
mayúsculas y minusculas no so n iguales!
contains( ) Un objeto CharSequence que buscar. E! resultado es true si el argumento está con-
tenido en el objeto String.
contentEquals( ) Un objeto CbarSequence o St rin gB uffer con El resultado es true si hay una co rresponden-
el que comparar.. cia exacta con el argumento.
equals lgnoreCase( ) Un objeto String con el que comparar.. El resultado es true si los contenidos son
iguales, sin tener en cuenta la diferencia entre
mayúsculas y minúsculas.
rcgionM atcbes( ) Desplazamiento dentro de este objeto String, Un resultado booleano que indica si la región
el otro objeto Stri ng junto con su desplaza- se corresponde.
miento y la longitud para comparar. La
sobrecarga aiiade la posibilidad de "ignorar
mayúsculas y minúsculas".
starlSWith( ) Objeto String con el que puede comenzar. La Un resultado booleano que indica si el objeto
sobreca rga aiiade el desplazamiemo dentro de String comienza con el argumento.
un argumento.
endsWith( ) Objeto Slring que puede ser sufijo de este Un resu ltado booleano que indica si el argu-
objeto String. mento es un sufijo.
indexOf( ), Sobrecargado: char, char e índice de inicio, Devuel ve - 1 si el argumento no se encuen tra
lastlndexOf( ) String. String e índice de inicio. dentro de este objeto String; en caso contra-
rio, devuelve el índice donde empieza el argu-
mento. lastlndexOf( ) busca hacia atrás el
final.
substring( ) Sobrecargado: índice de micio: Índice de ini- Devuelve un nuevo objeto String que conlie-
(también subScquencc( » cio + índice de fin. ne el conjunto de caracteres especificado.
concat( ) El objeto String que haya quc concatenar. Devuel ve un nuevo objeto StriDg que cOlllie-
ne los caracteres del objeto String original
seguidos de los caracteres del argumento.
replace( ) El antiguo canlcter que hay que buscar, el Devuelve un nuevo objeto String en el que se
nuevo carácter con el que hay que reempla- han efectuado las sustituciones. Utiliza el anti-
zarlo. También puede reemp lazar un objeto gua objeto String si no se encuentra ninguna
CharSequeoce con otro objeto correspondencia.
CharSequence.
toLowerCase( ) Devuelve un lluevo objeto String en el que se
toUpperCase( ) habrán cambiado todas las letras a minúsculas
o mayúsculas. Utiliza el antiguo objeto String
si no es necesario realizar ningún ca mbio.
trim( ) Devuelve un nuevo objeto String eliminando
los espacios en blanco de ambos extremos.
Utiliza el antiguo objeto String si no es nece-
sario realizar ningún cambio.
324 Piensa en Java
valueDr( l Sobrecargado: Object, charl! . charll y des- Devuclvc un objeto String que contiene una
plazamiento y recuento, boolean. charo int. representación en fonna de caracteres del
long, noat. double. argumento.
Puede ver que todo método de Strin g devuelve, cuidadosamente. un nuevo objeto Strin g cuando es necesario cambiar los
contenidos. Observe también que, si los contenidos no necesitan modificarse. el método se limita a devol ver una referencia
al objeto String original. Esto ahorra espacio de almacenamiento y recursos de procesamiento.
Los métodos Strin g en los que están implicadas expres;ones regulares se explican más adelante en este capítulo.
Formateo de la salida
Una de las características más esperadas que ha sido finalmente incorporada a Java SES es el formateo de la sa lida al esti-
lo de la instrucción printf() de C. No sólo pemúte esto simplificar el código de salida. sino que también proporciona a los
desarrolladores Java una gran capacidad de control sobre el fomlato a la alineación de esa sa lida.:!
printfO
La función printf() de e no ensambla las cadenas de caracteres en la forma en que lo hace Java, sino que toma una única
cadena deformato e inserta valores en ella. efectuando el f0n11ateo a medida que lo hace. En lugar de utili za r el operador
sobrecargado .+. (que el lenguaje C no sobrecarga) para concatenar ellexto entrecomillado y las varia bles. printf( ) utili-
za marcadores especiales para indicar dónde deben insertarse los datos. Los argumentos que se insenan en la cadena de for-
mato se indican a continuación mediante una lista separada por comas. Por ejemplo:
printf("Row 1: [%d %fl\n", x, y ) ¡
En tiempo de ejecución. el valor de x se inserta en °/od y el va lor de y se inserta en % f. Estos contenedores se denominan
especificadores de formato Y. además de decirnos dónde se debe insertar el valor, también nos infonnan del tipo de va ria-
ble que hay que insertar y de cómo hay que fonnatearla. Por ejemplo. el marcador '% d' anterior dice que x es un entero.
mientras que ' Olor dice que y es un valor de punto flotante (float o double).
System,out.format( )
Java SES introdujo el mélodo format( l, disponible para los objelos PrintStream o PrintWriter (de los que hablaremos
más en deta lle en el Capítulo 18. EIS). enlre los que se incluye System.o"t. El método format() está modelado basándose
en la función printf() de C. Existe incluso un método printf( ) que pueden utilizar aquellos que se sientan nos tálgicos, que
simplemente invoca format( ). He aquí un ejemplo simple:
JJ : strings / SimpleFormat. j ava
2 Mark Welsh ha ayudado en la creación de esta sección. asi como en la sección "Análi~is de la entrada".
13 Cadenas de caracteres 325
1* Output :
Row 1, [5 5. 332542J
Row 1, [5 5.3325421
Row 1, [5 5. 332542J
* /// ,-
Puede ver que format() y printf() son equivalentes. En ambos casos, hay una única cadena de ronnata, seguida de un argu-
mento para cada especificador de fonnato .
La clase Formatter
Toda la nueva funcionalidad de fonnateo de Java es gestionada por la clase Formatter del paquete java.utU. Podemos COll-
siderarFormatter como una especie de traductor que convierte la cadena de fonnato y los datos al resultado deseado.
Cuando se crea un objeto Formatter, se le indica a dónde queremos que se manden los resultados. pasando esa infon11a-
ción al constmctor:
//: strings/Turtle.java
import java.io.*;
import java.util.*;
/* Output:
Tommy The Turtle is at (O, O)
Terry The Turtle is at (4,8)
Tommy The Turtle is at (3,4)
Terry The Turtle is at (2,5)
Tommy The Turtle is at (3,3)
Terry The Turtle is at (3,3)
* /// >
Toda la salida representada por tommy va a System.out mientras que la representa por terry va a un alias de Systcm.out.
El constructor está sobrecargado para admitir di versas ubicaciones de sa lida, pero las más útil es son PrintStream (como en
el ejemplo), OulputStre.m y File. Veremos más detalles sobre esto en el Capitu lo 18, En/rada/salida.
Ejercicio 3: (1) Modifique Turtle.java de modo que envie toda la salida a System.err.
326 Piensa en Java
El ejemplo anterior uti liza un nuevo especificador de fommto, -%s'. Este marcador indica un argumento de tipo Stri ng y
es un ejemplo del tipo más simple de especificador de fonnato : uno que sólo tiene un tipo de conversión.
Especificadores de formato
Para controlar el espaciado y la alineación cuando se insertan los datos, hacen falta especificadores de fomlato más elabo-
rados. He aquí la sintaxis más general:
%[i nd ice_argumentoJ [indicadores] [anchura] [.precisión] conversión
A menudo, será necesario controlar el tamaño mínimo de un campo. Esto puede rea lizarse especificando una anchura. El
objeto Form atte r garantiza que un campo tenga al menos una anchura de un cierto número de caracteres, rellenándolo con
espacios en caso necesario. De manera predeterminada, los datos se justifican a la derecha, pero esto puede cambiarse inclu-
yendo ;-' en la sección de indicadores.
Lo contrario de la anchura es la precisión, que se utili za para especificar un máximo. A diferencia de la anchura, que es
aplicable a todos los tipos de conversión de datos y se comporta de la misma manera con cada uno de elJos, precisión tiene
un significado distinto para los diferentes tipos. Para las cadenas de caracteres, la precisión especifica el número máximo
de caracteres del objeto Str ing que hay que imprimir. Para los números en coma flotante , precisión especifica el número de
posiciones decimales que hay que mostrar (el valor predeterminado es 6), efecntando un redondeo si hay más dígitos o aña-
diendo más ceros al final si hay pocos. Puesto que los enteros no tienen parte fraccionaria, precisión no es aplicable a ellos
y se generará una excepción si se utili za el argumento de precisión con un tipo de conversión entero.
El siguiente ejemplo utili za especificadores de formato para imprimir una factura de la compra:
// : strings/Receipt.java
import java . util .*¡
public void print (St ring name, int qty, double price) {
f.format ( "%-15.1Ss %5d %lO .2f \n", name, qty, price);
total += price;
1* Output:
Item Qty Price
25.06
Como puede ver, el objeto Fo rmatter propo rciona un considerable grado de control entre el espaciado y la alineación, con
una notación bastante concisa. Aquí, las cadenas de fannato se copian simplemente con el fin de producir el espaciado apro-
piado.
Ejercicio 4: (3) Modifique Receipt.java para que todas las anchuras estén controladas por un único conjunto de va lo-
res constantes. El objetivo es poder cambiar fácilmente una anchura modificando un único valor en un
determinado lugar.
Caracteres de conversión
b Valor booleano
s Cadena de caracteres
char u = 'a I ¡
System.out.println(tlu = 'a' ti) ¡
f. format ( tls: %s\n", u);
JJ f.format("d: %d\n", u) i
f.format( " c: %c\n", u) ¡
f.format("b: %b\n", u) ¡
11 f.formatl"f, %f\n", u);
JI f.format("e: %e\n", u) ¡
JI f.format ( "x: %x\n", u);
f.format{"h: %h\n", u);
int v = 121;
System. out. println ("v 121") ;
328 Piensa en Java
double x = 179.543;
System.out.println(lI x = 179.543 " );
1/ f.format("d: %d\n " , x);
1/ f.format("c : %c\n", x);
f.format("b: %b\n", xl;
f.format{"s: %s\n", xl;
f.format {"f: %-f \ n", x l ;
f.format(lIe: %e\nll, x l ;
II f.format ( lIx: %x \ n ll , x l ;
f.format ( "h: %-h \ n", x l ¡
boolean z = false;
System.out.println ( "z = false" ) ;
1/ f.format ( "d: %d \ n", z ) ;
II f.format ( "c: %c \ n", z ) ;
f.format ( "b: %b \ n", z ) ;
f . format ( "s: %s \ n", z ) ;
/1 f.format l "L H \ n", z ) ;
II f.format ( "e: %e \ n", z ) ;
II f.format ( "x: %x \ n", z ) ;
f.format {"h: %h \ n", z ) ;
1* Output: (Sample )
u = 'a'
s: a
e: a
b: true
13 Cadenas de caracteres 329
h, 61
v = 121
d, 121
e: y
b: true
s: 121
x: 79
h, 79
w = new Biglnteger("SOOOOOOOOOOOOO" )
d, 50000000000000
b: true
s, 50000000000000
x, 2d79883d2000
h: 8842ala7
x = 179.543
b: true
s: 179.543
f, 179.543000
e: 1 . 795430e+02
h: lef462c
y = new Conversion ()
b: true
s: Conversion@9cab16
h: 9cab16
z = false
b: false
s: false
h, 4d5
, /// ,-
Las líneas comentadas muestran conversiones que no so n vá lidas para ese tipo de variables concreto, ejecutarlas harían que
se generará una excepción.
Observe que la conve rsión ' b' funciona para cada una de las variables anteriores. Aunque es vá lida para cualquier tipo de
argumento. puede que no se comporte C0l110 cabría esperar. Para las primitivas boolean o los objetos de tipo boolean, el
resultado será true o false, según corresponda. Sin embargo, para cua lquier otro argumento, siempre que el tipo de argu-
mento no sea nulJ. el resultado será siempre truco Incluso el va lor numérico de cero, que es sinónimo de false en muchos
lenguajes (incluyendo C), generará true. de modo que tenga cuidado cuando utilice esta conversión con tipos no booleanos.
Existen otros tipos de conversión y otras opciones de especificador de fonnato más extraños. Puede consultarlos en la docu-
mentación del JDK para la clase Formatter.
Ejercicio 5: (5) Para cada uno de los tipos básicos de conversión de la tabla anterior, escriba la expresión de formateo
más compleja posible. Es decir, utilice todos los posib les especificadores de fornlato disponibles para
dicho tipo de conversión.
String.format( )
Java SE5 también ha tomado prestado de e la idea de sprintf(), que es un método que se utiliza para crear cadenas de
caracteres. String.format( ) es un método estático que toma los mismos argumentos que el método format() de Formatter
pero devuelve un objeto String. Puede resultar útil cuando sólo se necesita invocar una vez a format( ):
11 : strings/DatabaseException.java
1* Output:
DatabaseException: (t3, q7) Wri te failed
, ///,-
Entre bastidores, tod o lo que Striog.format() hace es iostanciar el objeto Forma!!er y pasarle los argumentos que haya-
mos proporcionado, pero utili zar este método puede resultar a menudo más claro y más fácil que hacerlo de fonna manual.
result.append{ u\n U) ;
return resul t .toSt ri ng() i
/ , Output: (Sample)
00000 , CA FE BA BE 00 00 00 31 00 52 DA 00 05 00 22 07
00010 , 00 23 DA 00 02 00 22 08 00 24 07 00 25 DA 00 26
00020, 00 27 DA 00 28 00 29 DA 00 02 00 2A 08 00 2B DA
00030, 00 2C 00 2D 08 00 2E DA 00 02 00 2F 09 00 30 00
0004 O, 31 08 00 32 DA 00 33 00 34 DA 00 15 00 35 DA 00
00050, 36 00 37 07 00 38 DA 00 12 00 39 OA 00 33 00 3A
* ///,-
Para abrir y leer el archi vo binario, este programa presenta otra ut ilidad que se presentará en el Capítulo 18, En/radal
salida: net.mindview.utiI.BinaryFile. El método read( ) devuelve el archi vo completo como una matri z de tipo byte.
13 Cadenas de caracteres 331
Ejercicio 6 : (2) Cree una clase que contenga campos inl. long, noal y double, Cree un método loSlring( ) para esta
clase que utilice String.format(), y demuestre que la clase funciona correctamente.
Expresiones regulares
Las expresiones regulares han sido durante mucho tiempo parte integrante de las utilidades estándar Unix como sed yawk,
y de lenguajes como Pylhon y Perl (algunas personas piensan incluso que las expresiones regu lares son la principal razón
del éxito de Perl) . Las herramientas de manipulación de cadenas de caracteres estaban anterionnente delegadas a las clases
Slring. Slrin gB uffer y StringTokenizer de Java. que disponian de funcionalidades relativamente simples si las compara-
mos con las expresiones regulares.
Las expresiones regulares son herramientas de procesamiento de texto potentes y flexibles. Nos pCn11iten especificar.
mediante programas, patrones complejos de texto que pueden buscarse en una cadena de entrada. Una vez descubiertos estos
patrones, podemos reaccIOnar a su aparición de la fonna que deseemos. Aunque la sintaxis de las expresiones regulares
puede resultar intimidante al principio. proporcionan un lenguaje compacto y dinámico que puede emplearse para resolver
todo tipo de tareas de procesamiento, comparación, selección, edición y verificación de cadenas de una fonna general.
Fundamentos básicos
Una expresión regu lar es una fonna de describir cadenas de ca racteres en ténnmos generales, de modo que podemos decir:
"Si una cadena de caracteres contiene estos elementos, entonces se corresponde con lo que estoy buscando", Por ejemplo,
para decir que un número puede estar o no precedido por un signo menos, escribimos el signo menos seguido de un signo
de interrogación de la fonna siguiente:
-?
Para describir un entero. diremos que está compuesto de uno o más digitos. En las expresiones regulares, un dígito se des-
cribe mediante '\d', Si tiene experiencia con las expresiones regulares en otros lenguajes, obsenrará inmediatamente la dife-
rencia en la fomla de gestionar las barras inclinadas, En otros lenguajes, '\\' significa: '"Quiero insertar una barra incli nada
a la izquierda normal y corriente (literal) en la expresión regular, No le asignes ningún significado especial", En Java, '11'
significa: "Estoy insertando una barra inclinada de expresión regu lar, por lo que el siguiente carácter tiene un significado
especia!". Por ejemplo, si queremos indicar un dígito, la cadena de la expresión regular será '\\d' , Si deseamos insertar una
barra inclinada literal, tendremos que escribir '\\\\', Sin embargo, elementos tales como de nueva línea y de tabulación uti-
lizan una barra inclinada simple: '\n\t',
Para indicar " una o más apariciones de la expresión precedente'\ se utiliza un símbolo '+', Por tanto, para decir "posible-
mente un signo menos seguido de uno o más dígitos", escribiríamos:
-?\\d+
La fonna más simple de utilizar las expresiones regulares consiste en utilizar la funcionalidad incluida dentro de la clase
String. Por ejemplo, podemos comprobar si un objeto String se corresponde con la expresión regular anterior:
ji: strings/lntegerMatch.java
/ * Output:
true
true
false
true
*/ / / >
332 Pien sa en Java
Las primeras dos ex pres iones se corres pond en, pero la tercera comi enza con un '+', que es un número legí timo pero que no
se aj usta a la ex presión regul ar. Por tanto, necesitamos un a fonna de decir "puede comenza r con un + o un -". En las expre-
siones regulares. los parén tesis tienen el efecto de agrupar una ex pres ión. y la barra verti cal '1' signifi ca OR (di syunción).
Por lanto.
( - 1\\ + ) 7
qui ere decir que esta parte de la cadena de caracteres puede se r un .~ . o un '+' o nada (debido al '? ' ). Pues to qu e el carác-
ter '+ ' ti ene un signifi cado especial en las ex presiones regul ares. es necesario introducir la secuencia de esca pe '\\' para que
aparezca como un ca rácter normal dentro de la expresión.
Una herram ienta útil de ex presiones regul ares incorporada en String es split(), que signjfi ca " partir esta cadena de carac-
teres juslO donde se produ zcan las co rrespondencias con la ex presión regul ar indicada".
/f : strings f Splitting.java
import java.util. * ¡
/ * Output :
[Then" when, you, have, found, the, shrubbery" you, must, cut, down , the, migh tiest,
tree, in, the, foresto .. , with .. . , a, herring!]
[Then, when, you, have, found, the, shrubbery, you, must, cut, down, the, mightiest,
tree, in, the, forest, with, a, herring]
(The, whe, you have found the shrubbery, you must cut dow, the mightiest tree i, the
forest. .. wi th. .. a herring!]
* /// , -
En primer lugar, observe que puede utili zar caracteres nomlales como expresiones regulares, una expresión regul ar no tiene
porqué co nt ener ca racteres especiales. como podemos ver en la primera llamada a split(), que simple mente efectúa la par-
ti ción de acuerdo con los espacios en blanco.
La segunda y tercera llamadas a split() utili zan '\W ', que represent a caracteres que no pertenezcan a palabras (la versión
en minúsculas, " w', representa un carácter perteneciente a una palabra); podrá ver que los signos de puntuación han sido
eliminados en el segundo caso . La tercera llamada a split( ) dice. " la letra n seguida de uno o más caracteres que no perte-
nezcan a palabras". Podrá ver que los patrones de di visión no aparece n en el resultado.
Una versión sobrecargada de String.split( ) nos pennite limitar el número de di visiones que hayan de producirse.
La última de las herramieOlas de expresiones regulares incorporada en String es la de sustitución . Podemos sustiulir la pri-
mera apari ción o todas elJas:
// : strings / Replacing . java
import static net.mindvie w.util.Print. * ¡
print(s.replaceAll("shrubbery!treelherring","banana ll ) i
/ * Output:
Then, when yau have located the shrubbery, you must cut down the mightiest tree in the
forest ... with . .. a herring!
Then, when you have found the banana, you must cut down the mightiest banana in the
forest ... with ... a banana!
* /// , -
La primera expresión se corresponde con la letra f seguida de uno o más caracteres de palabras (observe que el carácter w
está en minúscula esta vez). Sólo sustituye la primera correspondencia que encuentra, por lo que la palabra "found" ha sido
sustituida por la palabra "Iocated".
La segunda exp resión se corresponde con cualquiera de las tres pa labras separadas por las barras verticales que representan
la operación OR, y sustituye todas las correspondencias que encuentra.
Más adelante veremos que las expresiones regulares que no son de tipo Strin g disponen de herramientas de sustitución
más potentes; por ejemplo, se pueden in vocar métodos para llevar a cabo las sustituciones. Las expresiones regulares que
no son de tipo String también son significativamente más eficientes cuando hace falta utilizar la ex presión regular más de
un a vez.
Ejercicio 7: (S) Utilizando la documentación de java. util.regex.Pattern como referencia, escriba y pruebe una expre-
sión regu lar de prueba que compruebe una frase para ver si comienza con una let ra mayúscula y tenn ina
con un punto.
Ejercicio 8: (2) Divida la cadena Splitting.knights por las pa labras "the" o "you".
Ejercicio 9: (4) Utilizando la documentación de j ava. util.regex.Pattern como referencia, sustituya todas las vocales
de Splitting. knights por guiones bajos.
Caracteres
B El ca rácter específico B
II Tabulador
In Nueva línea
Ir Retomo de carro
Ir Avance de página
le Escape
La potencia de las expresiones regu lares comienza a hacerse patente cuando se definen clases de caracteres. He aquí algu-
nas fonnas típ icas de crear clases de caracteres, junto con algunas clases predefinidas:
334 Piensa en Java
Clases de caracteres
Is Un carácter de espaciado (espacio. tabulador, nueva linea, avance de pág ina. retomo de carro)
Lo que se muestra aquí es sólo un ejemplo; consultando la págma de documentación del JDK correspondiente a
java.u til.regex. Pattern podrá conocer todos los posibles patrones de expresiones regulares.
Operadores lógicos
XV X seguido de Y
XIY XoY
Localizadores de contorno
, Comi enzo de línea
$ Fin de linea
lb Frontera de palabra
\B Frontera de no palabra
Por ejemplo, cada una de las siguientes ex presiones pennite localiza r la secuencia de caracteres "Rudo lph":
jj , strings j Rudolph.java
/ * Ou tput:
t rue
true
true
t r ue
* /// , -
Por supuesto, el objetivo no debe ser crear la expresión regular más complicada sino la que sea más simple y baste para rea-
lizar la tarea que tengamos entre manos. Una vez que comience a escribir expresiones regulares, podrá ver cómo a menudo
conviene referirse a los ejemplos de código escritos anterionnente, para facilitar la escritura de nuevas expresiones regu-
lares.
Cuantificadores
Un cuantificador describe la forma en que un patrón absorbe el texto de entrada:
• Avaricioso: los cuantificadores son avariciosos a menos que se los modifique de alguna manera. Un expresión
avariciosa trata de encontrar el máximo número posible de correspondencias para el patrón indicado. Una causa
bastante común de problemas consiste en suponer que el patrón sólo se corresponderá con el primer grupo de
caracteres, cuando lo cierto es que se trata de un patrón avaricioso y continuará procesando texto hasta que baya
logrado establecer una correspondencia con la cadena de caracteres más larga posible.
• Reluctante: especificado con un signo de interrogación, este cuantificador hace que la correspondencia se esta-
blezca con el número mínimo de caracteres necesario para satisfacer el patrón. También se denomina perezoso,
de correspondencia mínima o no avaricioso.
• Posesivo: en la actualidad, este lipo de cuantificador sólo está di sponible en Java (no en otros lenguajes) y es más
avanzado, por lo que es posible que no lo utilice al principio. A medida que se aplica una expresión regular a una
cadena de caracteres, la expresión genera múltiples estados para poder retroceder si la correspondencia falla. Los
cuantificadores posesivos no conservan dicbos estados intennedios, evitando así el retroceso. Pueden utilizarse
para impedir que una expresión regular quede fuera de control y también para hacer que se ejecute de manera más
eficiente.
Recuerde que la expresión 'X' necesitará a menudo encerrarse entre paréntesis para que funcione de la fomla deseada. Por
ejemplo:
abe ...
podría parecer que se debería corresponder con la secuencia 'abc' una o más veces, y si la aplicamos a la cadena de entra-
da ' abcabcabc' , obtendremos de hecho tres correspondencias. Sin embargo, lo que la expresión dice en realidad es: "loca-
li za ' ab' seguido de una o más apariciones de 'c '''. Para buscar correspondencias con la cadena completa 'abc' una o más
veces, debemos decir:
336 Piensa en Java
(abc)+
Resulta bastante fácil equivocarse al utilizar expresiones regulares; se trata de un lenguaje completamente ortogonal a Ja va,
que funciona sobre éste y que presenta diferencias con el lenguaje de programación.
CharSequence
La interfaz denominada C harScquence establece una definición generalizada de una secuencia de caracteres abstraída de
las clase CharBuffer, String, StringBuffer o StringBuilder:
interface CharSequence
charAt{int i) i
length () ;
subSequence(int start, int end) i
toString() ¡
Dichas clases implementan esta interfaz. Muchas operaciones con expresiones regu lares toman argumentos de tipo
CharSequence.
Pattern y Matcher
En general, lo que se hace es compilar objetos de expres ión regular en lugar de emplea r las utilidades String, que so n bas-
tante limitadas. Para ello, importamos java.util.regex, y luego compilamos una expresión regu lar utilizando el método
static Patter n.compile( j . Esto genera un objeto Pattern basado en su argumento Strin g. Para util izar el objeto Pattern, lo
que se hace es invocar el método matcher( ), pasándole la cadena de caracteres que queremos buscar. El método matcher( )
genera un objeto Matcher, que tiene un conjunto de operaciones de entre las cuales podemos elegir (puede consultar todas
las operaciones en la documentación del JDK correspondiente a .util.regex.Matcher). Por ejemplo, el método replaceAII()
sustituye todas las correspondencias por el argumento que se proporcione.
Vamos a ver un primer ejemplo: la clase sigui ente puede utili zarse para probar expresiones regulares con una cadena de
entrada. El primer argumento de la línea de comandos es la cadena de entrada en la que hay que buscar las corresponden-
cias, seguida de una o más expresiones regulares que haya que aplicar a la entrada. En Un ix/Linux, las exp resiones regula-
res deben estar entrecomilladas en la línea de comandos. Este programa puede resultar úti l para probar expresiones regulares
mientras las construimos con el fin de comprobar que esas expresiones establecen las correspondencias deseadas.
11 : strings/TestRegularExpression.java
II Permite probar con facilidad expresiones regulares.
II {Args : abcabcabcdefabc abc+ "(abc)+" " (abc ){2,}" }
11 11
import java.util.regex. *¡
import static net.mindview.util.Print.*¡
} l ' Output,
Input: "abcahcahcdefabc"
Regular expression: "ahcabcabcdefahc"
Match "ahcabcabcdefabc" at positions 0-14
Regular expression: "abc+"
Match "abe" at positions 0-2
Match "abe" at positions 3-5
Match lIabc" at positions 6-8
Match "abe" at positions 12-14
Regular expression: " (abe) +"
Match lIabcabcabc" at positions 0-8
Match "abe" at positions 12-14
Regular expression: " (abc){2 ,}u
Match "abcahcabc " at positions 0-8
' 111 ,-
Un objeto Pattcrn representa la versión compi lada de una expresión regular. Como hemos visto en el ejemplo anterior,
podemos utilizar el método matcher() y la cadena de entrada para generar un objeto Matcher a partir del objeto Pattern
compilado. Pattern también tiene un método estático:
static boolean matches{String regex, CharSequence input)
para comprobar si regex se corresponde con el objeto input de tipo CharSequence utilizado como entrada, y un método
split() que genera una matriz de tipo String después de descomponer la entTada según las correspondencias es tabl ecidas
con la exp resión regular regex.
Podemos generar un objeto Matcher invocando Pattern.matcher() con la cadena de entrada como argum ento. Después el
objeto Matchcr se utiliza para acceder a los resultados, utili za ndo métodos para evaluar si se establecen o no diferentes
tipos de correspondencias:
boolean matches()
boolean lookingAt()
boolean f ind ()
boolean find(int start)
El método matches( ) tendrá éxito si el patrón se corresponde con la cadena de entrada completa. mientras que lookingAt( )
tendrá éx ito si la cadena de entrada, comenzando por el pri ncipio, pe rmite establecer una correpondencia con el patrón.
Ejercicio 10: (2) Para la frase ·'Java now has expresiones regulares" evalúe si las siguientes expresiones pennitirán loca-
lizar la correspondencia:
. . Java
\8reg. *
n.w\s+h(ali)s
s?
s'
s+
S(4}
S(l} .
s(a,3}
find( )
Matchcr.find() puede utilizarse para descubrir múltiples correspondencias de patrón en el objeto CharSequence al cual se
aplique, Por ejemp lo:
338 Piensa en Java
JI: strings/Finding.java
import java.util.regex.*¡
import static nec.mindview.util.Print.*;
/ * Output:
Evening is full of the linnet s wings
Evening vening ening ning ing ng 9 i5 is s full full ull 11 1 of of
f the the he e linnet linnet innet nnet net et t s s wings w~ngs ings ngs 9S s
* /// ,-
El patrón '\\W+' divide la entrada en palabras. find() es como un iterador, que se desplaza hacia adelante a través de la cade-
na de caracteres de entrada. Sin embargo, la segunda versión de find() puede aceptar un argumento entero que le dice cuál
es la posic ión del carácter en el que debe comenzar la búsqueda; esta versión rein icializa la posición de búsqueda con el
valor de su argumento, como puede ver analizando la salida.
Grupos
Los grupos son expresiones regulares delimitadas por paréntesis y a las que luego se puede hacer referencia utilizando su
número de grupo. El grupo O indica la expresión completa, el grupo 1 es el primer grupo entre paréntesis, etc. Por tanto, en
A(B(C))D
1* Output :
[the slithy taves] [the] [sl ithy taves] [slithy] [taves]
[in the wabe.l [in] [t he wabe.] [the] [wabe.l
[were the borogoves,] [werel [the borogoves,] [the] [borogoves,]
[mame raths out grabe .] [mame] [raths out grabe .] [raths] [out grabe .
[Jabberwo ck, my son,] [Jabberwock,] [my son,] [rny] [son,]
[cl aws that catc h. ] [c laws] [that catc h.] [that] [catch . ]
[bird , and shun] [bird,] [and shun] [and] [shunl
[The frum ious Bandersnatch.] [Thel [frumious Bandersnatch.] [ frumi ousl [Bandersnatch.]
*///,-
Este poema es la primera parte de "Jabberwocky", de Lewis Carroll extraído del libro A través del espejo. Puede ver que el
patrón de expresión regular tiene una serie de grupos entre paréntesis, compuestos de cualquier número de caracteres que
no sea de espaciado ('18+' ) seguido de cualquier número de caracteres de espaciado ('Is+'). El objetivo es capturar las tres
últimas palabras de cada línea, el final de una línea está delimitado por '$'. Sin embargo, el comportamiento normal con-
siste en hacer corresponder "$' con el final de la secuencia de entrada comp leta, por lo que es necesario decir expLícitamen-
te a la expresión regular que preste atención a los caracteres de nueva línea desde dentro de la entrada. Esto se consigue con
el indicador de patrones '(?m)' al principio de la secuencia (los indicadores de patrones los veremos enseguida).
Ejercicio 12: (5) Moditique Groups.java para contar todas las palabras que no empiecen con una letra mayúscul a.
start( ) y end( )
Después de una operación de establecimiento de correspondencias que haya pennitido encontrar al menos una correspon-
dencia, start() devuelve el índice de inicio de la correspondencia anterior, mientras que cnd() devuelve el índice del últi-
mo carácter de la correspondencia mas uno. Al invocar start() o end() después de una operación de localización de
correspondencias que no haya tenido éxito (o antes de intentar una operación de localización de correspondencias), se gene-
ra la excepción lllegalStateException. El siguiente programa también ilustra los métodos matcbes() y lookingAt( )3
jj : stringsjStartEnd.java
import java. uti l.regex.*¡
import static net.mindview.util.Print.*¡
1 El texto indicado es una cita de uno de los discursos del Comandante Taggart en Gala-cy Quest.
340 Piensa en Java
print (message);
1* Output:
input : As long as there is injustice, whenever a
\w *ere\w*
findO 'there' start ::: 11 end = 16
\w *ever
find() 'whenever' start 31 end = 39
input : Targathian baby cries out, wherever a distress
\w*ere\w*
find () 'wherever' start 27 end 35
\w*ever
find () 'wherever' start 27 end 35
T\w+
find () 1 Targathian' start O end = 10
lookingAt() start = O end 10
input: signal sounds among the stars . . . We'll be there.
\w*ere\w *
find() ' there' start = 43 end = 48
input: This fine ship, and this fine crew . . .
T\w+
find () 'This ' start = O end = 4
lookingAt{) start = O end = 4
input : Never give up! Never surrender!
\w *ever
f ind () 'Never' start O end = 5
f ind () 1 Never' start 15 end = 20
13 Cadenas de caracteres 341
Indicadores de Pattern
Hay un método compile( ) altemativo que acepta indi cadores que afectan al co mportamiento de búsqueda de co rresponden-
cias:
Pattern Pattern.compile{String regex, int flag)
donde flag puede ser una de las siguientes constantes de la clase Pattero :
l'attc rn .CANO N_EQ Se cons idera que dos caracteres se corresponden si y sólo si sus descomposiciones canónicas
completas 10 hacen. Por ejemplo, la expresión " u003F' se corresponderá con la cadena o?,
cuando se especifique este indicador. De manera predeterminada. la búsqueda de correspon-
dencias no tiene en cuen ta la equivalencia canónica.
Patte rn.CASE_INSENS ITl VE Por omisión. la búsqueda de correspondencias sin distinción de mayúsculas y minúsculas presu-
(?i) pone que sólo se están util izando caracteres del conjunto de caracteres US-ASC II. Este indicador
permite establecer una correspondencia con el patrón sin tener cn cuenta mayúsculas o minúscu-
las. Puede habilitarse la búsqueda de correspondencias Unicode sin distinción de mayúsculas y
minúsc ulas especificando el indicador UN ICOD E_ CASE en conjunción con este indicador.
Patte rn .COM M ENTS En es te modo. se ignoran los caracteres de espaciado, y también se ignoran los comentarios
(?x) incnlstados que comicncen con # hasta el final de la línea. También puede habilitarse el modo
de líneas Unix mediante la expresión de indicador incnlstado.
Pattern.DOTALL En este modo, la expresión'.' se corresponde con cualquier carácter, incluyendo el tenninador de
(?s) línea. Por omisión, la expresión'.' no se corresponde con los tenninadores de línea.
Pattern.M ULTI U NE
- -En el modo multilínea. las expresiones 'A' y 'S' se corresponden con el principio y el final de una
(? m) línea. respectivamente. 'A' también se corresponde con el principio de la cadena de entrada y 'S'
lo hace con el final de la cadena de entrada. De manera predetenninada. estas expresiones sólo se
corresponden con el principio y el final de la cadena de entrada completa.
Patte rn.UN ICOD E_CASE La correspondencia sin distinción de mayúsculas y mi núsculas, si está habi litada por el indicador
(?u) CASE_lNSENSITIV E. se realiza de manera coherente con el estándar Unicode. De manera pre-
detenninada, la correspondencia sin distinción de mayúsculas y minúsculas presupone que sólo se
están buscando correspondencias con ca racteres del conjunto de caracteres US-ASCI 1.
Pancrn.UNIX _ LI NES En este modo, sólo se reconoce el tenni nador de línea '\n ' en el comportamien to de ;.', ,'" y '$ '.
(?d)
4No sé por qué dieron este nombre a dicho método ni a qué refiere ese nombre. Pero resulta reconfortante saber que quienquiera que sea que inventa esos
nombres de métodos tan poco intuitivos continúa empleado en SUIl y que su aparente politica de no revisar los diseños de código sigue estando vigente.
Perdón por el sarcanno, pero es que este tipo de cosas empiezan a cansar después de unos cuamos años.
342 Piensa en Java
1* Output:
java
Java
J AVA
* /// ,-
Esto crea un patrón que se corresponderá con las líneas que comiencen por "java," "Java," "JAVA," etc., y que intentará bus-
car una correspondencia con cada línea que fonne parte de un conjunto multilínea (correspondencias que comienzan al prin-
cipio de la secuencia de caracteres y a continuación de cada terminador de línea contenido dentro de la secuencia de
caracteres). Observe que el método group() sólo devuel ve la porción con la que se ha establecido la correspondencia.
split( )
split() divide una cadena de caracteres de entrada en una matri z de objetos Str ing, utili zando como delimitador la expre-
sión regular.
String[] split {CharSequence input )
String[] split {CharSequence input, int limit )
Ésta es una forma cómoda de descomponer el texto de entrada utili zando una frontera di visori a común :
11 : strings /Sp litDemo.java
import j ava.util.regex.*;
import java . util.*.
import static net . mindview . util.Print. * ¡
1* Output :
[This, unusual us e , o f e xclamation, pointsl
[This, unusual use, of exclamation! !points]
* /// ,-
13 Cadenas de caracteres 343
La segunda forma de split( ) limita el número de di visiones que pueden tener luga r.
Ejercicio 14: (1) Escriba de nuevo SplitDemo utilizando String.split( ).
Operaciones de sustitución
Las expresiones regulares resultan especialmente útiles para sustituir texto. He aquí los métodos disponibles:
rcplaceFirst(String rcplacement) sustituye por replacement la primera parte que se corresponde de la cadena de caracte-
rcS.
replaceAII(StriDg replacement) sustituye por replacemeDt todas aquellas partes que se correspondan en la cadena de
caracteres de entrada.
appendReplacement(StringBuffer sbuf, String replacement) reali za sustituciones paso a paso en sbuf, en lugar de susti-
nJir sólo la primera o todas ellas, como sucede con replaceFirst() y rcplaceAII(), respecti vamente. Éste es un método muy
importante, porque permite invocar métodos y realizar otros tipos de procesamiento para generar la cadena de sustitución
replacement (replaceFirst() y replaceAII() sólo pueden utilizar cadenas de caracteres fija s para la sustitución). Con este
método, podernos separar los grupos mediante programa y crear potentes rutinas de sustitución.
appendTail(StringBuffcr sbuf, String replacement) se invoca después de UDa o más invocaciones del método
appendReplacement() para copiar el resto de la cadena de caracteres de entrada.
He aquí un ejemplo que muestra el uso de todas las operaciones de sustitución. El bloque de texto comentado al principio
del programa es extraído y procesado con expresiones regulares para usarlo como entrada en el resto del ejemplo:
/1: strings/TheReplacements.java
import java.util.regex. * ¡
import net.mindview.util . *¡
import static net.mindview.util.Print. * ¡
/ * Output:
Here's a block of text to use as input to
the regular expression matcher. Note that we'll
first extract the block of text by looking for
the special delimiters, then process the
extracted block.
H{VOWEL1 ) rE' s A blOck Of tExt tO UsE As InpUt tO
thE rEgUlAr ExprEssIOn mAtchEr. NOtE thAt wE'll
fIrst ExtrAct thE blOck Of tExt by lOOkIng fOr
thE spEcIAl dEllmItErs, thEn prOcEss thE
ExtrActEd blOck.
*// / ,-
El archivo se abre y se lee utilizando la clase TextFiJe de la biblioteca net.mindview.util (el código correspondiente se mos-
trará en el Capitulo 18, E/S). El método estático read() lee el archivo completo y lo devuelve como objeto Slrin g. mlnput
se crea para corresponderse con todo el texto (observe los paréntesis de agrupamiento) comprendido entre '{*! ' y ' !*/'.
Después, los conjuntos de más de dos espacios seguidos se reducen a un único espacio y se eliminan todos los espacios situa-
dos al principio de cada línea (para hacer esto en todas las líneas y no sólo al principio de la cadena de entrada, es necesa-
rio habilitar el modo multilínea). Estas dos sustituciones se realizan con el método equivalente (pero más cómodo, en este
caso) replaceAII() que fom1a parte de Slring. Observe que, como cada sustitución sólo se emplea una vez en el programa,
no se produce ningún coste adicional por hacerlo así en lugar de precompilar la operación en fonna de un objeto Pattern .
replaceFirst() sólo sustituye la primera correspondencia que encuentre. Además, las cadenas de sustitución en
replacefirst() y replaceAII() son simplemente literales, por lo que si queremos realizar algún procesamiento en cada sus-
titución, no nos sirven de ninguna ayuda. En dicho caso, tendremos que utilizar appendReplacement(), que nos pennite
escribir cualquier código que queramos para reali zar la sustitución. En el ejemplo anterior, se selecciona y procesa un grupo
(en este caso, poniendo en mayúscula la vocal encontrada por la expresión regular) a medida que se constmye el objeto sbuf
resultante. Nonnalmente, lo que haremos será recorrer toda la entrada y hacer todas las sustituciones y luego invocar a
appendTail(), pero si queremos simular replaceFirst() (o una operación de "sustitución de n apariciones"), basta con hacer
la sustitución una vez y luego invocar appendTail() para insertar el resto de la infonnación en sbuf.
appendReplacement() también nos permite hacer referencia directamente en la cadena de sustitución a los grupos capul-
radas mediante la notación "$g", donde 'g' es el número de grupo. Sin embargo, este método sólo sirve para tareas de pro-
cesamiento simples y no nos varía los resultados deseados en el programa anterior.
reset( )
Podemos aplicar un objeto Matcher existente a una nueva secuencia de caracteres utili zando los métodos reset():
// : strings / Resetting . java
import java.util.regex.*;
/ * Output:
13 Cadenas de caracteres 345
1* Output: (Sample)
O, strings: 4
L simple: 10
2, the: 28
3, Ssct: 26
4, class: 7
5, static: 9
6, String: 26
7, throws: 41
8, System: 6
9, System: 6
10, compile: 24
I I , through: 15
12, the: 23
13, the: 36
14, String: 8
15, System : 8
16, start: 31
' 111,-
El archivo se abre como un objeto net.mindview.util.TcxtFilc (del que hablaremos en el Capitulo 18. E/S). que lee las li-
neas del archivo en un contenedor tipo ArrayList. Esto significa que podemos utilizar la sintaxi s foreach para iterar a tra-
vés de las líneas almacenadas en el objeto TextFile.
346 Piensa en Java
Aunque es posible crear un nuevo objelO Matcher dentro del bucle for, resulta ligeramente más óptimo crear un objeto
vacio Matcher fuera del bucle y utili zar el método reset() para asignar cada linea de la entrada al objeto Matcher. El resul_
tado se analiza con tind().
Los argumentos de prueba abren el archivo JGrep.java para leerlo como entrada y buscan las palabras que comiencen por
¡Ssct¡ .
Puede aprender mucho más acerca de las expresiones regulares en A1astering Regular E).pressions, 2 u Edición, por Jeffrey
E. F. Friedl (O'Reilly, 2002). Hay también numerosas introducciones a las expresiones regulares en Internet, y también se
puede encontrar a menudo infonnación útil en la documentación de lenguajes tales como Perl y Python.
Ejercicio 15: (5) Modifique JGrep.java para aceptar indicadores como argumentos (por ejemplo, Pattern.CASE_
INSENS ITIVE, Pattern.M ULT ILl NE).
Ejercicio 16: (5) Modifique JGr.p.java para aceptar un nombre de directorio o un nombre de archivo como argumen-
to (si se proporciona un directorio, la búsqueda debe extenderse a todos los arcluvos de directono).
Consejo: puede generar una lista de nombres de archivo con:
File (] files = new File (" . ") .listFiles () ;
Ejercicio 17: (8) Escriba un programa que lca un archivo de código fuente Java (tendrá que proporcionar el nombre del
archivo en la línea de comandos) y muestre todos los comentarios.
Ejercicio 18: (8) Escriba UD programa que lea un archivo de código fuente Java (tendrá que propo rcionar el nombre del
archivo en la línea de comandos) y muestre todos los literales de cadena presentes en el código.
Ejercicio 19: (8) Utilizando los resultados de los dos ejercicios anteriores, escriba un programa que examine el código
fuente Java y genere todos los nombres de clases utilizados en un programa concreto.
Análisis de la entrada
Hasta ahora, resultaba relativamente complicado leer datos de un archivo de texto legible o desde la entrada estandar. La
solución usual consiste en leer una línea de texto, extraer los elementos y luego utilizar los diversos métodos de análisis sin-
táctico de Integer, Double, etc., para analizar los datos:
11: strings/SimpleRead.java
impert java.io.·;
/ '* Output:
What i5 your name?
Sir Robin of Camelat
How old are you? What i5 your favorite double?
(input: <age> <double»
22 1.61803
Hi Sir Robin o f Camelat .
In 5 years you will b e 27.
.
My favorite double i 5 0 . 809015 .
/// ,-
El campo input utili za clases de java.io, que no vamos a presentar oficialmente basta el Capítulo 18. E/S. Un objeto
StringReadcr transfonna un dato de tipo String en un flujo de datos legible y este objeto se emplea para crear un obj eto
BufferedRcader porque BufferedReader tiene un método readLine(). El resultado es que el objeto input puede leerse de
linea en línea, como si fuera la entrada estándar procedente de la consola.
readLine( ) se utili za para obtener la cadena de caracteres correspondiente a cada línea de entrada. Resulta bastante cómo-
do de utili zar cuando queremos obtener un dato de entrada por cada linea de datos, pero si hay dos valores de entrada que
se encuentran en una misma línea, las cosas empiezan a complicarse: la línea debe dividirse para poder analizar cada dato
de entrada por separado. Aquí, la división tiene lugar al crear numArray. pero observe que el método splil( ) fue introdu-
cido en J2SE lA, por lo que en las versiones anteriores era necesario hacer alguna otra cosa.
La clase Scanner, miad ida en Java SES, elimina buena parte de la complejidad relacionada con el análisis de la entrada:
11 : strings/BetterRead . java
import java . util. * ;
1* OutPUt:
What is your name?
Sir Robin of Camelot
How old are you? What i5 your favorite double?
(input: <age> <double»
22
1.61803
Hi Sir Robin of Camelot .
In 5 years you will be 27 .
My favorite double is 0 . 809015 .
' /// ,-
348 Piensa en Java
El construc tor de Scanner admite casi cualquie r lipo de objeto de entrada, incluido el objeto File (que también veremos en
el Capítulo 18, E/S), un objeto InputStream , un objeto String o. como en este caso un objeto Readable, que es una inler-
faz introducida en Java SE5 para describir "algo que dispone de un método read( )". El objeto BufferedReader del ejem-
plo anterior cae dentro de esta catego ría.
Con Scanner. los pasos de entrada. extracción de elementos y análisis si ntáctico están implementados mediante diferentes
lipos de métodos de "continuación", Un método next() devuelve el siguiente elemento String y ex isten elementos si mila-
res para todos los tipos primitivos (excepto char). así como para BigDecimal y Biglnteger. Todos estos métodos se blo-
quean, lo que significa que sólo temlinan después de que ha ya un eleme nto de datos completo di spo nible como entrada.
También existen los métodos "hasNext"' correspondientes que devuelven true si el siguiente elemento de entrada es del tipo
co rrecto.
Una interesante diferencia entre los dos ejemplos anteriores es la falta de un bloque try para las excecpiones IOException
en BetterRead.java. Una de las suposiciones hechas por el objeto Scanner es que IOExccption marca el final de la entra-
da , por lo que estas excepciones son capturadas y hechas desaparecer por e l objeto Scanner. Sin embargo, la excepción más
reciente está di sponible a través del método ioException( ), por lo que podemos examinarla en caso necesario.
Ejercicio 20: (2) Cree una clase que contenga campos inl, long, noat, double y String. Cree un constructor para esta
c lase que tenga un único argumento String, y anal ice dicha cadena de caracteres para rellenar los diferen-
tes campos. Anada un método toString() y demuestre que la clase funciona correctamente.
Delimitadores de Scanner
De manera predetenl1inada, un objeto Scanner divide los elementos de entrada según los caracteres de espaciado, pero tam-
bién podemos especificar nuestro patrón delimitador en forma de una expresión regular:
11 : strings/ScannerDelimiter.java
import java.util.*;
1* Output:
12
42
78
99
42
*///,-
Es te ejemplo utiliza comas (rodeadas por cantidades arbitrarias de espacios) como los delimitadores a la hora de leer la cade-
na de caracteres dada. Esta misma técnica puede utili za rse para leer desde archivos delimitados por comas. Además del
método useDelimiterO. que sirve para fijar el patrón de delimiración. existe también otro mélOdo denominado delimiterO.
que devuelve el objeto Pattern que esté siendo acnl3lmcl1le uti li zado como delimitador.
/* Output:
Threat en 02/10/2005 from 58 . 27.82.161
Threat en 02/11/2005 from 204.45.234 . 40
Threat en 02/11/2005 from 58 . 27 . 82.161
Threat en 02/12/2005 from 58 . 27 . 82.161
Threat en 02/12/2005 from 58 . 27.82.161
*/1/,-
Cuando utili zamos next( ) con un patrón específico, se trata de hacer co rresponder di cho patrón con el siguiente elemento
de entrada. El resultado es devuelto por el método match ( ) y, como podemos ver en el ejempl o, el mecanismo funciona
exactament e igual que las búsqu edas con ex presiones regulares que hemos visto anterionnente.
Exis te un problema a la hora de anali za r con ex presiones regul ares. El patrón se hace co rres ponder ún ica mente con el
siguien te elemento de entrada, por lo qu e si el patrón contiene un delimitador nunca se encontrará una correspondencia.
StringTokenizer
Anl es de que hubi era di sponibles expresiones regulares (en J2SE 1.4) o la clase Scanner (en Java SES), la fo nna de di vidir
una cadena en sus partes componentes era ex traer los elementos con Strin gTokenizer . Pero ahora res ulta mucho más fác il
y más suci nto hacer lo mismo con ex presiones regulares o la clase Sca nn er . He aquí ull a comparac ión simple de
Strin gTo keni zcr con las otras dos técni cas:
JJ: stringsJReplacingStringTokenizer.java
impert java.util.*;
1* Output:
But l ' m not dead yet! 1 feel happy!
350 Piensa en Java
Resumen
En el pasado, el sopone que Java tenía para la manipulación de cadenas de caracteres era rudimentario, pero en las edicio_
nes recientes del lenguaje hemos podido ver cómo se han ido adoptando funcionalidades más sofi sticadas, copiadas de otros
lenguajes. Actualmente. el soporte para cadenas de caracteres es razonablemen te completo, aunque en ocasiones es necesa-
rio prestar algo de atención a los deta lles de eficiencia, como por ejemplo haciendo un uso apropiado de StringBuilder.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico The Thinking ill Jm'o Allllotared SOlulioll Cuide, disponible pard
la venia en 1\'W\\,. ¡\findl'ie\\: /lel .
Información
de tipos
El mecanismo RTTI (Run/ime Iype informa/ion, información de tipos en tiempo de ejecuc ión)
nos pennite averiguar y uti lizar la informac ión acerca de los tipos de los datos mientras un
programa se está ejecutando.
Este mecanismo nos libera de la rest ricción de efectuar operac iones orientadas a tipos únicamente en tiempo de compila-
ción, y pennite construir algunos programas de gran potencia. La necesidad de RTTI pennite descubrir una gran cantidad
de cuestiones del di seño orientado a objetos de gran interés (y a menudo bastante intrigantes) y hace que surjan cuestiones
fundamentales acerca de cuál tiene que ser la manera de estructurar los programas.
En este capítulo se examinan las formas en que Java permite averiguar la información acerca de los objetos y las clases en
tiempo de ejecución. Estos mecanismos adoptan dos formas diferentes: RTTI " tradiciona)", que supone que tenemos lodos
los tipos disponibl es en tiempo de compilación y el mecanismos de reflexión que pennite desc ubrir y utilizar la información
sobre clases exclusivamente en tiempo de ejecución.
La necesidad de RTTI
Considere el ya familiar ejemplo de una jerarquía de clases que utiliza los mecanismos de polimorfismo. El tipo genérico
es la clase base Shape y los tipos derivados específicos son Circle. Square y Triangle:
Shape
draw( )
I I
I Circle
I Square
I Triangle
I
Se trala de un diagrama de jerarquía de clases típico, con la clase base en la parte superior y las clases derivadas distribu-
yéndose en senl ido descendente. El objetivo nom1al de la programación orientada a objetos es que el código manipule refe-
renci as a tipo base (en este caso, Shape), de 1110do que si decidimos ampliar el programa aiiadielldo una nueva clase (como
por ejemplo Rhomboid. derivada de Shape). el grueso del código no se vea afectado. En este ej emplo, el método de aco-
plamiento dinámico de la interfaz Shapc es draw(), por lo que la intención es que el programador de clientes invoque a
draw( ) a través de una referencia genérica a Shape. En todas las clases derivadas. el método draw( ) se susti ulye y, pues-
to que es un método con acoplamiento dinámico, obtendremos el co mportamiento adecuado aún cuando se invoque al méto-
do sustiuno a través de una referencia genérica a Shape. Esto es, ni más ni menos, polimorfismo.
Por tanto, lo que hacemos en general es crear un objeto específico (Circle. Squarc o Triangle), generalizarlo a Shape (olvi-
dando el tipo específico de objeto) y utilizar esa referencia anónima a Shape en el reslO del programa.
Podemos codificar la jerarqu ía Shape de la siguiente manera:
352 Piensa en Java
/* Output :
Circle . draw ()
Square. draw ()
Triangle. draw ()
*///,-
La clase base contiene un método draw() que utili za indirectamente toString() para imprimir un identificador de la clase,
pasando this a System.out.println() (observe que toString() se declara como abstracto para obligar a las clases herederas
a sustituirlo, y para imped ir la instantación de un objeto Shape simple). Si un objeto aparece en una expresión de concate-
nación de cadenas (donde están involucrados '+' y objetos String), se invoca automáti camente el método toString() para
generar una representación de tipo Slring de dicho objelo. Cada un a de las clases derivadas sustituye el método toString()
(de Object) de modo que draw() termine (po limórficamente) imprimiendo algo distinto en cada caso.
En este ejemplo, la generalización tiene lugar cuando se coloca la fonna geométrica en el contenedor List<S hape>. Durante
la generalización a Shape, el hecho de que los objetos sean lipos específicos de Shape se pierde. Para la matriz se (rata sim-
plemente de objetos Shape.
En el momento en que se extrae un elemento de la matriz, el contenedor (q ue en la práctica almacena todos los elementos
como si fueran de tipo Object) proyecta automáticamente el resultado sobre un objeto Shape. Éste es el tipo más básico del
mecanismo RTTI , porque todas las proyecciones de tipos se comprueban en tiempo de ejecución para comprobar Su correc-
ción. Eso es lo que RTTI significa: el tipo de los objetos se identifica en tiempo de ejecución .
En este caso, la proyección RTTl sólo es parcial : el objeto Object se proyecta sobre Shape, y no so bre Circle, Sq ua re o
Trian gle. Eso es debido a que lo único que sabemos en este punto es que el contenedor List<Shape> está lleno de objetos
Shape. En tiempo de compilac ión, esto se impone mediante el contenedor y el sistema genéri co de Java, pero en tiempo de
ejecución es la proyección la que garanti za que esto sea así.
Ahora es cuando entra en acción el polimorfismo y se detennina el código exacto que ejecutará el objeto Shape vie ndo
si la referencia corresponde a un objeto Circle, Square o Triangle. Y, en general, as í es como deben ser las cosas. Lo que
queremos es que la mayor parte de nuestro código sepa lo menos posible acerca de los tipos específicos de los objetos.
debiendo limitarse a trata r con la representación general de una fami lia de objetos (en este caso, Shape). Como resu ltado.
el código será más fácil de escribir, de leer y de mantener, y los diseños serán más senci llos de implementar, comprender y
14 Información de tipos 353
modificar. Por ello. el polimosrfismo es UIlO de los objetivos generales que se persiguen con la programación orientada a
objetos.
¿Pero qué sucede si tenemos un problema especial de programación que resulta más fácil de resolver si conocemos el tipo
exacto de una referencia genérica? Por ejemplo, suponga que queremos pem1itir a nuestros usuarios que resalten todas las
fom1as geométricas de un cierto tipo concreto, asignándolas un color especial, de esta fonn8. pueden localizar todos los
triángulos de la pantalla resaltándolos. 0 , por ejemplo. imagine que nuestro método necesita "rotar" una lista de fomlas geo-
métricas. pero que no tiene sentido rotar un círculo, por lo que preferimos saltarnos los círculos al implementar la rotación
de todas las formas. Con RTT l, podemos preguntar a una referencia de tipo Shape cuál es el tipo exacto al que está apun-
tand o. lo que nos permite seleccionar y aislar los casos especiales.
El objeto Class
Para comprender cómo funciona el mecanismo RTTI en Java. primero tenemos que saber cómo se represe nta la infonna-
ción de tipos en tiempo de ejecución. Esto se lleva a cabo mediante un tipo de objeto especial denominado objeto Class. que
contiene infonnación acerca de la clase. De hecho. el objeto Class se utiliza para crear todos los objetos ""nonnales" de una
clase. Java implementa el mecanismo RTIl utilizando el objeto C lass. incluso si lo que estamos haciendo es algo como una
proyecc ión de tipos. La clase C lass también pennite otra se rie de fonnas de utilización de RTTI.
Existe un objeto Class para cada clase que fonne parte del programa. En otras palabras, cada vez que escribimos y compi-
lamos una nueva clase. también se crea un determinado objeto Class (y ese objeto se almacena en un archivo .class de nOI11-
bre idéntico). Para crear un objeto de esa clase. la máquina virtual Java (JVM) que esté ejecutando el programa utiliza un
subsistema denominado cargador de clases.
El subsistema cargador de clases puede comprender, en la práctica, una cadena de cargadores de clases. pero sólo existe un
cargador de clases primordial, que forma parte de la implementación de la NM. El cargador de clases primordial carga las
que se denominan clases de confian:::a, que incluyen las clases de las interfaces API de Java, y esa carga se reali za normal-
mente desde el di sco local. Usualmente no es necesario tener cargadores de clases adicionales en la cadena, pero si tenemos
necesidades especiales (como por ejemplo, cargar clases de alguna manera especial para dar soporte a aplicaciones de ser-
vidor web, o descargar clases a través de una red). entonces disponemos de una manera de enlazar cargadores de clases adi-
cionales.
Todas las clases se carga n en la JVM dinámicamente, cuando se utiliza la clase por primera vez. Esto sucede cuando el pro-
grama hace referencia a un miembro estático de dicha clase. Resu lta que el constructor también es un método estático de
una clase, aún cuando no se utilice la palabra clave sta tic para el constructor. Por tanto, el crear un nuevo objeto de di cha
clase utilizando el operador Dew también cuenta como una referencia a un miembro estático de la clase.
Por tanto. los programas Java no se cargan por completo antes de comenzar la ejecución, sino que se van cargando los dis-
tintos fragmentos del programa a medida que so n necesarios. Esto difiere de muchos lenguajes tradic ionales. El mecani s-
mos de carga permite conseguir un tipo de comportamiento que resulta muy dificil, o incluso imposible, de obtener con un
lenguaje estático de carga como pueda ser C++.
El cargador de clases compnteba primero si el objeto C lass de dicho tipo está cargado. Si no lo está, el cargador de clases
predetem1inado localiza el archivo .class con dicho nombre (un cargador de clases adicional podría. por ejemplo. extraer el
código intenuedio de una base de datos en lugar de hacerlo de un archivo). A medida que se carga e l código intennedio
correspondiente a la clase, dicho código se verifica para garantizar que no esté corrompido y que no incluya código Java
mal fonnado (ésta es una de las líneas de defensa de los mecanismos de seguridad de Java).
Una vez que e l objeto C lass de dicho tipo se encuentra en memoria se le utiliza para crear todos los objetos de dicho tipo.
He aquí un programa que ilustra esta fom1a de actuar:
JI : typeinfo / SweetShop.java
JI Examen de la forma en que funciona el cargador de clases.
import static net.mindview .uti l.Print.*;
class Candy {
static { print ("Loading Candy"); }
354 Piensa en Java
class Gum {
static { print ("Loading Gum"); }
class Cookie {
static { print (IILoading Cookie ll ) ; }
/ * Output :
inside main
Loading Candy
After creating Candy
Loading Gum
After Class. forName ( "Gum")
Loading Cookie
After creating Cookie
* ///, -
Cada una de las clases de Candy, Gum y eookic tiene una cláusula statie que se ejecuta cuando se carga la clase por pri-
mera vez. Se imprimirá la infonnación para decimos cuándo tiene lugar la carga de esa clase. En maine ), las creaciones de
objetos están mezcladas con instmcciones de impresión, como ayuda para detemlÍnar el instante de la carga.
Podemos ver, a partir de la salida, que cada objeto Class sólo se carga cuando es necesario, y que la inicinlizHción de tipo
static se realiza durante la carga de la clase. Una línea particulal111ente interesante es:
Class. forName ( "Gum " ) ;
Todos los objetos elass pertenecen a la clase elass. Un objeto elass es como cualquier otro objeto, por lo que se puede
obtener y manipular Wl3 referencia a él (es to es lo que hace el cargador). Una de las fonnas de obtener una referencia al
objeto Class es el método estático forName(), que toma un argumento de tipo String que contiene el nombre textua l (¡tenga
cuidado con la ortografía y el uso de mayúsculas!) de la clase concreta de la cual se quiera obtener una referencia. Elméto-
do devuelve una referencia de tipo Class, que en este ejemplo se ignora; la llamada a forName() se rcali za debido a su efec-
10 secundario que consiste en cargar la clase Gum si no está ya cargada. Durante el proceso de carga se ejecuta la cláusula
sta tic de Gum .
En el ejemplo anterio r, si elass.forName( ) falla porque no puede encontrar la clase que estemos intentando cargar. gene-
rará una excepción ClassNotFoundException. Aqui, simplemente nos limitamos a infonnar del problema y a continuar.
pero en otros programas más sofisticados podríamos intentar resolver el problema dentro de la mtina de tratamiento de
excepciones.
Siempre que queramos utilizar la infonnación de tipos en tiempo de ejecución, debemos primero obtener una referencia al
objeto elass apropiado. elass.forName( ) es una fo mla cómoda de hacer esto, porque no necesitamos un objeto de di cho
tipo para obtener la referencia Class. Sin embargo, si ya disponemos de un objeto del tipo que nos interesa. podemos ext raer
la referencia Class invocando un método que fomJa parte de la clase raíz Objeet: gctelass( ). Este mecanismo de vuel\'e la
referencia Class que representa el tipo concreto de objeto. Class dispone de muchos métodos interesantes; el sigu iente es
un ejemplo que ilustra algunos de ellos:
14 Información de tipos 355
11 : typeinfo ltoys/ToyTest.java
11 Prueba de la clase Class.
package typeinfo.toys;
import static net.mindview.util.Print. * ;
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy
1/ Desactive con un comentario el constructor predeterminado
11 siguiente para ver NoSuchMethodError de (*1 * )
Toy () {}
Toy l int i l {}
printlnfo(c) i
for (Class face : c.getlnterfaces()
printlnfo(face) ;
Class up = c.getSuperclass() ¡
Object obj = null¡
try {
11 Requiere un constructor predeterminado:
obj = up.newlnstance()¡
cateh (InstantiationException el {
print ("Cannot instantiate");
System. exi t (l) ;
eatch(IllegalAccessException el {
print(IICannot access") ¡
System.exit(1) ¡
printlnfo(obj .getClass(»;
1* Output:
Class name: typeinfo. tays. FancyToy is interface? [false]
Simple name: FancyToy
Canonical name : typeinfo.toys.FancyToy
Class name: typeinfo. tays. HasBatteries i5 interface? [true]
Simple name: HasBatterie5
356 Piensa en Java
Ejercici o 8 : (5) Escriba un método que tome un objeto e imprima de manera recursiva todas las clases presentes en la
jerarquia de ese objeto.
Eje rcici o 9 : (5) Modifique el ejercicio anterio r de modo que utilice C lass.getDeclaredFields( ) con el fin de mostrar
también infom13ción acerca de los campos contenidos en cada clase.
Eje rcici o 10: (3) Escriba un programa para determinar si una matriz de char es un tipo primitivo o un verdadero obje~
too
Literales de clase
Java proporciona una segunda fom13 de generar la referencia al objeto Class: el Iileral de clase. En el programa anterior,
dicho literal de clase tendría el aspecto:
FancyToy.class;
lo que no sólo es más simple. sino también más seguro ya que se comprueba en tiempo de compilación (y no necesita, por
tanto, colocarse dentro de un bloque try). Asimismo, puesto que elimina la llamada al melado forNal11c( ). es también más
eficiente.
Los literales de c lase funcionan tanto con las clases nonnales COlT'IO con las interfaces. matrices y tipos primitivos. Además,
existe un campo estándar denominado TVPE en cada un a de las clases envoltorio de los tipos primitivos. El campo TYPE
produce una referencia a l objeto C lass correspondiente al tipo primitivo asociado, de modo que:
char.cJass C haracter.TYPE
byte.class Byte.TYPE
int.cJass Integer.TYPE
long.cJass Long.TYPE
float.class Float.TYPE
double.cJass Double.TYPE
\'oid.cJass Void.TYPE
-
En mi opinión, es mejor utilizar las versiones ".class" siemp re que se pueda. ya que son más coherentes con las clases nor-
males.
Es interesante observar que al crear una referencia a un objeto Class utilizando ·'.class·' no se inicializa automáticamente el
objeto C lass . La preparación de una clase para su uso consta, en realidad. de tres pasos diferentes :
1. Carga, que es realizada por el cargador de clases. Este proceso localiza el código intennedio (que usualmente se
encuentra en el disco. dentro de la ruta de clases, aunque no tiene porque ser necesariamente así) y crea un obje-
to Class a partir de dicho código intem1edio.
2. A1onlClje. La fase de montaje verifica el código intenncdio de la clase. asigna el campo de almacenamiento para
los campos estáticos y. en caso necesario, resuelve todas las referencias que esta clase haga a otras clases.
3. Inicialización. Si hay una superclase, es preciso inicializarla, ejecutando los inicializadores de tipo static y los
bloques de inicialización de tipo static.
La inicialización se retarda hasta que produce la primera referencia a un melodo estático (e l cons tructor es implícitamen te
de tipo sta tic) o a un campo estát ico no cons tante:
358 Piensa en Java
//: typeinfojClasslnitialization.java
import java.util. *;
class Initable
statie final int staticFinal = 47;
statie final int staticFina12 =
Classlnitialization.rand.nextlnt(lOOOl;
static {
System . out .println ("Initializing Initable");
class Initable2 {
statie int staticNonFinal = 147;
sta tic {
System.out.println(lIInitializing Initable2");
class Initable3 {
static int staticNonFinal = 74;
statie {
System.out.println(ltlnitializing Initable3");
1* Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
* ///,-
En la práctica, la inicialización es lo más tardía posible. Analizando la creación de la referencia ¡nHable, podemos ver que
usar sim plemelllc la sintaxis .class para obtener una referencia a la clase no provoca la inicialización. Sin embargo.
Class.forName( ) inicializa la clase inmediatamente para generar la referencia C lass, como puede ver analizando la crea-
ción de i"ilable3 .
Si un va lor final estático es una "constante de tiempo de compilación", tal como Jnitable.staticFinal, dicho valor puede
leerse sin que ello haga que la clase Initable se inicialice. Sin embargo, definir un campo como estático y final no garanti-
14 Información de tipos 359
za este comportamiento: al acceder a Initable.sta tic Fina l2 se fuerza a la inicial ización de clase, porque dicho campo no
puede ser una constante de tiempo de compi lación.
Si un campo estático no es de tipo fin al, acceder al mismo requiere siempre que se ejecute la fase de montaje (para asignar
el espacio de almacenamiento para e l campo) y también la de inicialización (para inicializar dicho espacio de almacena-
mienw) antes de que el va lor pueda ser leído, como puede ver analizando el acceso a Ini table2 .staticNo n Final.
La referencia de clase nonnal no genera ninguna advertencia de compilación. Sin embargo, puede ver que la referen cia de
clase nomlal puede reasignarse a cualqui er otro objew Class, mientras que la referencia de clase genérica sólo puede asig-
narse a su tipo declarado. Utilizando la sintaxis genérica, permilimos que el compilador imponga comprobaciones adicio-
nales de los tipos.
¿Qué sucede si queremos relajar un poco las restricciones? lnicialmente, parece que deberíamos ser capaces de hacer algo
como lo siguiente:
Esto parece tener sentid o. porque Integer hereda de Number. Sin embargo, este método no funciona, porque el objeto Class
Integer no es una subclase del objeto C lass Number (puede parece r que esta di stinción re sulta demasiado su til ; la analiza-
remos co n más detall e en el Ca pítulo 15 . Genéricos).
Para relajar las restri cciones al utili zar referencias Class genéricas, yo personalmente empleo el comodín , que fonna parte
de los ge néricos de Java. El símbolo del comodín es ' ?', e indica "cualquier cosa". Por tanto. podemos añadir comodines a
la referencia Class de l ejemplo anterior y genera r los mismos resultados:
jI : typeinfo / WildcardClassReferences.java
En Java SE5, se prefiere utilizar Class<?> en lugar de Class, aún cuando ambos son equivalemes y la referencia Class nor-
mal. como hemos visto, no genera ninguna advertencia del compilador. La ventaja de Class<?> es que indica que no es ta-
mos pasando una referencia de clase no específica simplemente por accidente o por ignorancia, sino que hemos elegido la
versión no especifica.
360 Piensa en Java
Para crear una referencia C lass que esté restringida a un detemlinado tipo o a cualquiera de sus stlbripos, podemos combi-
nar un comodín con la palabra clave ex tends para crear un limite. Por lanlO, en luga r de decir simplemente
C lass<N umb er>. lo que diríamos seria:
11: typeinfo/BoundedClassReferences.java
class Countedlnteger
private static long counter;
private final long id = counter++;
public String toString() ( return Long. toString (i d ) ¡
return resul t;
1* Output:
[O, 1, 2, 3, 4, S, 6, 7, 8, 9, lO, 11, 12, 13, 14J
* /// ,-
Observe que esta clase debe asumir que cualquier tipo con el que trabaje dispondrá de un constnlctor predetenninado (uno
que no tenga argumentos), obteniéndose una excepción si no es éste el caso. El compilador no genera ningún tipo de adver-
tencia para este programa.
Cuando utilizamos la sintaxis genérica para los objetos Class sucede algo interesante: ncwInsta nce( ) devolverá el tipo
exacto del objeto. en lugar de simplemente un objeto básico O bj ec t como vimos en ToyTes t.java. Esto resulta un tanto limi-
tado :
14 Información de tipos 361
class Building {}
class House extends Building {}
J. La proyección clás ica, por ejemplo, "(Shape);' que utili za RTTl para asegurarse de que la proyección es correc-
ta. Esto ge nerará ClassCastException si se ha realizado una proyección incorrecta.
2. El objeto Class representativo del objeto. Podemos consultar el objeto Class para obtener información útil en
tiempo de ejecución.
En C++. la proyección clásica "(Shape)" no utiliza mecanismos RTTI. Simplemente le dice al compilador que trate el obje-
to como si fuera del tipo indicado. En Java, sí realiza la comprobación de tipos. esta proyección se denomina a menudo
"especialización segura en lo que respecta a tipos". La razón de utilizar el ténnino "especialización" se basa en la disposi-
ción históricamente utilizada en los diagramas de jerarquías de clases. Si la proyección de Circle sobre Shape es una gene-
rali zación, entonces la proyección de Shape sobre Circle es un a especialización. Sin embargo, puesto que el compilador
sabe que un objeto Circle es también de tipo Shape, permite que se realicen libremente asignaciones de generalización, si n
que sea obligatorio incluir una sintaxis de proyección específica. El compilador no puede saber, dado un objeto Shape, de
qué tipo concreto es ese objeto; podría ser exactamente de Shape, o podría ser un subtipo de Shape, como Circle, Square,
Triangle o algún olro tipo. En ti empo de compilación, el compilador sólo ve un objeto Shape. Por tanto, no nos permitirá
que realicemos una asignación de especialización sin utilizar una proyección específica. con la que le decimos al compila-
dor que disponemos de información adicional que nos permite saber que se trata de un tipo concreto (el compilador com-
probará si dicha especialización es razonable, por lo que no nos permitirá efectuar especializaciones sobre un tipo que no
sea realmente una subclase del anterior).
Existe un tercer mecanismo de RTTI en Java. Se trata de la palabra clave instanceof, que nos dice si un objeto es una ins-
tancia de un tipo concreto. Devuelve un valor de tipo boolean, as í que esta palabra clave se utiliza en fomla de pregunta,
como en el fragmento siguiente:
if(x instanceof Oog)
(( Dog ) x ) .bark( ) ;
La instrucción ir comprueba si el objeto x pertenece a la clase Dog antes de proyectar x sobre Dog. Es importante utilizar
instanceof antes de una especialización cuando no dispongamos de otra in[onnación que nos indique el tipo del objeto; en
caso contrario, obtendremos una excepción ClassCastException .
Nonnalmente, lo que estaremos tratando de localiza r es un determinado tipo (por ejemplo, para pintar de púrpura todos los
triángul os), pero podemos fácilmente seleccionar todos los objetos utilizando instanceof. Por ejemplo, suponga que dispo-
nemos de una familia de clases para describir mascotas, Pet, (y sus propietarios, una característica que nos será útil en un
ejemplo posterior). Cada individuo (I ndividual ) de la jerarquia tiene un identificador id y un nombre opcional. Aunque las
clases que siguen heredan de Individual, existen ciertas complejidades en la clase Individual, por lo que mostraremos y
explicaremos di cho código en el Capítulo 17, Análisis detallado de los contenedores. En realidad, no es imprescindible ana-
li zar el código de Individual en este momento; lo único qu e necesitamos saber es que podemos crear un individuo con o
sin nombre, y que cada objeto Individual tiene un metodo ¡d() que devuelve un identificador unívoco (creado mediante un
simple recuent o de los objetos). También hay un método toString( ); si no se proporciona un nombre para un objeto
Individual, toString() sólo genera el nombre simple del tipo.
He aqui la jerarquía de clases que hereda de Individual :
// : typeinfo/pets/Person.java
package typeinfo.pets;
// , typeinfo/pets/Dog.java
14 Información de tipos 363
11 , typeinfo/pets/Mutt.java
package typeinfo.pets i
return result¡
}
111>
El método abstracto getTypes() deja para las clases derivadas la tarea de obtener la lista de objetos Class (esto es una
variante del patrón de diseño basado en el método de las plantillas). Observe que el tipo de clase se especifica como "cual-
quier cosa derivada de Pet", por lo que newlnstance() produce un objeto Pet sin requerir ninguna proyección.
randomPet() realiza una indexación aleatoria en el contenedor de tipo List y utiliza el objeto Class seleccionado para ge-
nerar una nueva instancia de dicha clase con Class.newlnstance(). El método createArray() utiliza randomPet( ) para
rellenar una matriz y arrayList() emplea a su vez createArray() .
Podemos obtener dos tipos de excepciones al llamar a newInstance(). Analizando el ejemplo, podrá ver que es ta s excep-
ciones se tratan en las cláusulas catch que siguen al bloque try. De nuevo, los nombres de las excepciones son casi autoex-
plicativos e indican cuál es el problema (1llegalAccessException está relacionado con una vio lación del mecanismo de
seguridad de Java, en este caso si el constructor predetenninado es de tipo private).
Cuando derivamos una subclase de PetCreator, lo único que necesi tamos su ministrar es el contenedor List de todos los
tipos de mascotas que queremos crear mediante randomPet() y los otros métodos. El método getTypes() normalmente
devolverá, simplemente, una referencia a un lista estática. He aquí una implementación utilizando forName():
11 : typeinfo/pets/ForNameCreator.java
package typeinfo.pets¡
import java.util.*¡
IItypeinfo.pets. Pug ll ,
"typeinfo. pets. EgyptianMau" ,
typeinfo. pets. Manx
11 11 ,
"typeinfo.pets.Cymric ll ,
"typeinfo.pets.Rat",
typeinfo. pets. Mouse" ,
11
IItypeinfo.pets.Hamster"
};
@SuppressWarnings (lIunchec ked ll )
priva te static void loader() {
try {
for(String name : typeNames)
types.add(
(Class<? extends Pet»Class.forName{name)) ¡
catch(ClassNotFoundException el
throw new RuntimeException {e);
static { loader() ¡ }
public List<Class<? extends Pet» types () {return types¡}
111 ,-
El método loader() crea la lista de objetos Class utilizando Class.forName(). Esto puede generar la excepción
ClassNotFoundException, lo cual tiene bastante sen tido, ya que estamos pasándole un objeto String que no puede validar-
se en tiempo de compilación. Puesto que los objetos Pet se encuentran en el paquete typeinfo, es necesario utilizar el nom-
bre del paquete al hacer referencia a las clases.
Para generar una lista con tipos de objetos Class, se necesita una proyecc ión, lo cual produce una advertencia en tiempo de
compilación. El método loader( ) se define por separado y luego se incluye dentro de una cláusula de inicialización estáti-
ca, porque la anotación @S uppressWarnings no puede insertarse directamente en la cláusula de inicialización estática.
366 Piensa en Java
Para recontar el número de mascotas, necesitamos un a herramienta que vaya controlando el número de los diferentes tipos
de mascotas. Un contenedor Map resulta perfecto para esta tarea; las claves con los nombres de los tipos de objetos Pet y
los valores son enteros que almacenan el número de objetos Pet de cada tipo. De esta fanna , podemos preguntar cosas como
"¿Cuántos objetos Hamster hay?". Podemos utilizar instanceof para recontar las mascotas:
//: typeinfo/PetCount.java
/1 Utilización de instanceof.
import typeinfo.pecs. * ;
import java.util. * ¡
import static net . mindview util.Print.*¡
} / * Output o
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster
EgyptianMau Mutt Mutt cymric Mouse Pug Mouse Cymric
{ Pug =3, Cat=9, Hamster=l, Cymric=7, Mouse=2, Mutt=3, Rodent=5,
Pet=20, Manx=7, EgyptianMau=7, 00g=6, Rat=2}
*/// 0-
En countPets(), se rellena aleatoriamente un a matri z con objetos Pet utilizand o un objeto PetCreator. Después, cada obj e-
to Pet de la matriz se comprueba y se recuenta util izando instanceof.
Existe una pequeña restri cción en la utili zación de instanceof: podemos comparar únicamente con un tipo nominado, y no
con un objeto Class. En el ej emplo anterior, podría parecer que resulta tedioso esc ribir todas esas expresiones instanceof,
y efectivamente lo es. Pero no hay ninguna forma inteligente de automatizar instanceof creando una matriz de objetos Class
y realizando la co mparación de di chos objetos (aunque, si sigue leyendo, verá que existe una alternati va). Sin embargo, esta
restricción no es tan grave como pudiera parecer, porque más adelante veremos que si un di seño nos exige escribir una gran
cantidad de expresiones instanceof probablemente eso signifique que el di seño no está bien hecho.
1* Output:
[class typeinfo.pets.Mutt, class typeinfo.pets.Pug, class typeinf o.pets.EgyptianMau, class
typeinfo . pets . Manx, class typeinfo.pets . Cymric, class typeinfo.pets.Rat, class
typeinfo.pets . Mouse, class typeinfo.pets.Hamster]
*/// 0-
En el ejemplo PetCount3.java, que veremos más adelante, necesitamos precargar un contenedor Map con todos los tipos
de Pel (no sólo con aquellos que hayan de ser generados aleatoriamente), por lo que es necesaria la lista allTypes Lisl, que
incluye lodos los tipos. La lista typcs es la parte de allTypes (creada mediante List.subList()) que incluye los tipos exac-
tos de mascota, por lo que es la que se utili za para la generación aleatoria de objetos Pet.
Esta vez, la creación de types no necesita estar rodeada por un bloque try, puesto que se evalúa en tiempo de compilación
y no generará ninguna excepción a diferencia de Class.forName( ).
Ahora tenemos dos implementaciones de PetCreator en la biblioteca typeinfo.pels. Para definir la seglmda como imple-
mentación predetenninada, podemos crear un envoltorio que utilice LiteralPetCreator:
368 Piensa en Java
//: typeinfo/pets/Pets.java
// Envoltorio para generar un PetCreator predeterminado.
package typeinfo.pets;
import java.util. *¡
Instanceof dinámico
El método Class.islnstance() proporciona una ronlla para probar dinám icamente el tipo de un objeto. Por tanto, podemos
el iminar todas esas tediosas instrucciones instanceof de PetCount.java :
//: typeinfo/PetCount3.java
// Utilizaci6n de islnstance()
import typeinfo.pets.*¡
import java.util.*¡
import net.mindview.util.*;
import static net.mindview.util.Print.*¡
result.delete{result.length()-2, result.length(»;
result. append ( "} ") ;
return result.toString();
petCount.count(pet) ;
print ();
print(petCount) ;
/ * Output:
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat
EgyptianMau Hamster EgyptianMau Mutt Mutt Cymric Mouse Pug Mouse Cymric
{Pet =20, Dog=6, Cat=9, Rodent=5, Mutt=3, Pug=3, EgyptianMau=2,
Manx=7, Cymric=5, Rat=2, Mouse=2, Hamster=l}
* 1I lo-
Para contar todos los tipos diferentes de objetos Pet, se precarga el mapa PetCounter Map con los tipos de
LiteraIPetCreator.aIlTypes. Esto utili za la clase net.míndvíew.utíl.MapData, que toma un objeto Iterable (la li sta
allTypes) y un valor constante (cero, en este caso) y rellena el mapa con claves tomadas de allTypes y valores iguales a
cero). Sin precargar el contenedor de tipo Map, lo que haríamos sería contar los tipos que se generan aleatoriamente y no
los tipos base como Pet y Cato
Como puede ver, el método islnstance() ha eliminado la necesidad de utili za r expresiones instanceof. Además, esto signi-
fica que podemos añadir nuevos tipos de Pet si mplemente cambiando la matriz LiteraIPetCreator.types; el resto del pro-
grama no necesita modificación (al revés de lo que sucedía al utili zar expresiones instanceof).
El método toString( ) ha sido sobrecargado para obtener una salida más legible que siga correspondiendo con la salida típi-
ca que podemos ver a la hora de imprimir un contenedor de tipo Map .
Recuento recursivo
El mapa en PetCounO.PetCounter estaba precargado con todas las diferentes clases de objetos Peto En lugar de sobrecar-
gar el mapa, podemos utili za r Class.ísAssignableFrom() y crear una herramienta de propósito general que no esté limita-
da a recontar objetos Pet:
11 : net/mindview/util/TypeCounter.java
11 Recuenta instancias de una familia de tipos .
package net.mindview.util;
import java . util . *;
if(!baseType.isAssignableFrom(type) )
throw new RuntimeException(obj + incorrect type:
+ type + ", should be type or subtype of "
+ baseType) i
countClass(type) ;
El método count() obtiene el objeto Class de su argumento y utili za isAssignableFrom() para reali zar una comprobación
en tiempo de ejecución con el fin de veri ficar que el objeto que se le haya pasado pertenece verdaderamente a la jerarquía
de clases que nos interesa. countClass() incrementa primero el contador correspondiente al tipo exacto de la clase. Después,
si baseType es asignable desde la superclase, se invoca a countClass( ) recursivamente en la superclase.
11 : typeinfo/PetCount4.java
import typeinfo.pets .* ;
import net . mindview.util .* ;
import static net.mindview.util . Print.*;
print ();
print (counter) ;
1* Output: (Sample)
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat
EgyptianMau Hamster EgyptianMau Mutt Mutt Cymric Mouse Pug Mous e Cymr ic
{Mouse=2 , 00g =6 , Manx=7 , EgyptianMau=2, Rodent=S, Pug =3,
Mutt=3, Cymric=S, Cat=9, Hamste r =l , Pet=20, Rat=2}
* /// , -
Como puede ver anal izando la salida, se cuentan ambos tipos base así como los tipos exactos.
Ejercicio 11: (2) Añada Gerbil a la biblioteca typeinfo.pets y modifique todos los ejemplos del capítulo para adaptar-
los a esta nueva clase.
Ejercicio 12: (3) Utilice TypeCounter con la clase CoffeeGenerator.java del Capitulo 15, Genéricos.
14 Información de tipos 371
Factorías registradas
Unos de los problemas a la hora de crear objetos de la jerarquía Pet es el hecho de que cada vez que añadimos un nuevo
lipa de objelo Pel a la jerarquía lenemos que acordamos de añadirlo a las entradas de LiteraIPetC r eator.java. En aquellos
sistemas donde tengamos que añadir un gran número de clases de fanna habitual. esto puede llegar a se r problemático.
podríamos pensa r en añadir un inicializador estático a cada subclase, de modo que el inicializador añadiera su clase a una
lista que se conservara en algún lugar. Desafortunadamente, los inicializadores estáticos sólo se invocan cuando se carga por
primera vez la clase, así que tenemos el típico problema de la gallina y el huevo: el generador no tiene la clase en su lista,
por lo que nunca puede crear un objeto de esa clase, así que la clase no se cargará y no podrá ser incluida en la lista.
Básicame nte, podemos obligarnos a crear la lista nosotros mismos de manera manual (a menos que queramos escribir una
herramienla que analice el código fuenle y luego genere y compile la liSia). Por lanto. lo mejor que podemos hacer. proba-
blemente, es colocar la lista en algún lugar central lo suficientemente obvio. Seguramente, el mejor lugar será la clase base
de la jerarquía de clases que nos interese.
El otro cambio que vamos a hacer aquí es diferir la creación del objeto, dejándoselo a la propia clase, utilizando el parrón
de diseño denominado método de/acloría. Un método de factoría puede invocarse polimórficamente y se encarga de crear
por nosotros un objeto del tipo apropiado. En esta versión muy simple, el método factoría es el método create() de la inter-
faz Factor y:
11: typeinfo/factory/Factory . java
package typeinfo.factory;
public interface Factory<:T> { T create (); } 111:-
El parámelro genérico T pennite a create() devolver un lipa diferenle por cada implemenlación de Factory. ESlo hace uso
también de los tipos de retomo covariantes.
En este ejemplo, la clase base Part contiene un contenedor List de objetos factoría. Las factorías correspondientes a los
lipos que deben generarse medianle el método createRan dom( ) se " registran" ante la clase base añadiéndolos a la liSia
partFactori es:
11 : typeinfo / RegisteredFactories.java
1/ Registro de factorías de clases en la clase base.
import typeinfo.factory.*;
import java.util .* ;
class Part {
public String toString () {
return getClass() .getSimpleName{);
/ * Output:
GeneratorBelt
CabinAirFilter
Gene ratorBelt
AirFilter
PowerSteeringBelt
CabinAirF ilter
FuelFilter
PowerSteeringBelt
powerSteeringBelt
FuelFilter
*///,-
No todas las clases de la jerarquía deben instanciarse; en este caso, Filter y Belt son simplemente clasificadores, por lo que
no se crea ninguna instancia de ninguno de ellos, sino sólo de sus subclases. Si una clase debe ser creada por
createRandom(), contendrá una clase Factory ¡ntema. La única forma de reutilizar e l nombre Factory, como hemos vis to
antes, es mediante la cualificación typeinfo.factory.Factory.
Aunque podemos utilizar Colleetions.addAIl( ) para añadir las factorías a la lista, el compilador se quejará, generando una
advertencia relati va a la "creación de una matriz genérica" (lo que se supone que es imposible, como veremos en el Capítulo
15, Genéricos), por lo que hemos preferido invocar add(). El método createRandom() selecciona aleatoriamente un obje-
to factoría de partFactories e invoca su método create() para generar un nuevo objeto Part.
Ejercicio 14: (4) Un constructor es un tipo de método de factoría. Modifique RegisteredFactories.j ava para que en
lugar de utilizar una factoría explícita, el objeto clase se almacene en el contenedor List, uti li zándose
newlnstanee() para crear cada objeto.
Ejercicio 15: (4) Implemente un nuevo PetCre.tor utíli zando factorías registradas y modifique el método envoltorio de
la sección "Utili zación de literales de clase" para que emplee este nuevo objeto en lugar de los otros dos.
Haga los cambios necesarios para que el resto de los ejemplos que utilicen Pets.java sigan funcionando
correctamente.
Ejercicio 16: (4) Modifique la jerarquía Coffee del Capítulo 15, Genéricos, para utilizar jerarquías registradas.
c1ass Base {}
c1ass Derived extends Base {}
/ * Output:
Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base. islnstance (x) true
Derived. islnstance (x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass () .equals(Base.class ) true
x.getClass () .equals (Derived.class » false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base. islnstance (x ) true
Derived.islnstance(x) true
x.getClass() == Base.class false
x.get Class() == Derived.class true
x .getClass () . equals (Base. class )) false
x.getClass() .equals (Derived.class )) true
* ///,-
El método teste ) realiza una comprobación de tipos con su argumento, utilizando ambas fonnas de instanceof. Después,
obtiene la referencia al objeto Class y emplea = y equals() para comprobar la igualdad de los objetos Class . Como cabría
esperar, instaneeoC e islnstanee() producen exactamente los mismos resultados, al igual que equals() y =. Pero las prue-
bas muestran que se obtienen diferentes conclusiones. Basándose en el concepto de tipos, instanceof dice: "¿Perteneces a
esta clase o a una clase derivada de ésta?". Sin embargo. si comparamos los objetos Class utilizando =, no entran en juego
los conceptos de herencia: o son tipos exactamente iguales o no lo son.
cisamente esto. El primero de esos casos es la programación basada en componentes. en la que constnlimos los proyectos
utilizando herramientas RAD (Rapid Applica/ion Deve/opmen/. desarrollo rápido de aplicaciones) dentro de un entorno IDE
(Integrafed Developmenr Environmem, entorno integrado de desarrollo). que fanna parte de una herramienta de generación
de aplicaciones. Se trata de un enfoque visual para la creación de programas, mediante el que se desplazan hasta un fonnu-
lario una serie de iconos que representan componentes. Estos componentes se configuran entonces estableciendo algunos de
SUS va lores durante el desa rrollo. Esta configuración en tiempo de diseño requiere que todos los componentes sean instan-
ciables, que expongan hacia el exterior partes de sí mismos y que pennitan que sus propiedades se lean y se modifiquen.
Además. los componentes que gestionan sucesos GUI (Graphica/ User IIl/elface) deben exponer la información acerca de
los métodos apropiados, de modo que el entamo lOE pueda ayudar al programador a la hora de sustituir dichos métodos de
tratamiento de sucesos. La reflexión proporciona el mecanismo para detectar los métodos di sponibles y generar los nom-
bres de los métodos. Java proporciona una estrucrura para la programación basada en componentes mediante JavaBeans
(este tema se describe en el Capitulo 22, Imelfaces gráficas de usuario).
Otra razón importante para descubrir la infonnación de clases en tiempo de ejecución es tener la posibilidad de crear y eje-
cutar objetos en platafomlas remotas, a través de una red. Esto se denomina invocación remota de melodos (RM-I, RemOle
Method Invocation), y pennite a un programa Java tener objetos distribuidos entre muchas máquinas. Esta distribución
puede tener lugar por diversas razones. Por ejemplo, quizá estemos realizando una tarea que requiera cálculos intensivos y,
para acelerar las cosas, podemos intentar descomponerla y asignar panes del trabajo a las máquinas que estén inactivas. En
otras situaciones, puede que queramos colocar el código que gestiona tipos concretos de tareas (por ejemplo, "reglas de
negocio" en una arquitectura cliente/servidor multinivel) en una máquina concreta, de modo que la máquina se convierta en
un repositorio común que describa dichas acciones y que pueda ser fáci lmente modificado para que los cambios afecten a
todo el sistema (se trata de un concepto bastante interesante, ya que la máquina existe exclusivamente para facilitar la modi-
ficación del software). En la misma línea, la informática d istribuida también soporta la utilización de hardware especia-
lizado que puede resultar adecuado para una tarea concreta, por ejemplo, en inversiones de matrices. pero inapropiado o
demasiado caro para la programación de propósito general.
La clase C lass sopona el concepto de reflexión, junto con la biblioteca java.lang.rcOcct que contiene las clases Field,
Mcthod y Constructor (cada una de las cuales implementa la interfaz Member). Los objetos de es tos tipos son creados por
la máquina JVM en tiempo de ejecución para representar el miembro correspondiente de la clase desconocida. Entonces,
podemos utilizar los objetos Constructor (constructores) para crear nuevos objetos, los métodos get( ) y set() para leer y
modificar los campos asociados con los objetos Field y el método invoke() para invocar un método asociado con un obje-
to Method. Además, podemos invocar los métodos de utilidad getFields( ). getMethods( ). gctConstructors( ), etc., con
el fin de obtener como resultado matrices de objetos que representen los campos, métodos y constructores (puede averiguar
más detalles examinando la clase Class en la documentación del JDK). Así, la información de clase para objetos anónimos
puede determinarse completamente en tiempo de ejecución y no es necesario tener ninguna infonnación en tiempo de com-
pilación.
Es importante comprender que no hay ninguna especie de mecanismo mágico en la reflexión. Cuando se utili za la refl ex ión
para interactuar con un objeto de un tipo desconocido, la máquina NM simplemente examinará el objeto y comprobará que
pertenece a una clase concreta (al igual que con el mecanismo RTTl normal). Antes de poder hacer nada con él, es necesa-
rio cargar el objeto Class. Por tanto, el archivo .class para ese tipo concreto deberá seguir estando disponible para la JVM,
bien en la máquina local o a través de la red. Por tanto, la verdadera diferencia entre RTf 1 y la reflexión es que, con la RTf!.
el compilador abre y examina el archivo .c1ass en tiempo de compilación. Dicho de otra fonna, podemos invocar tod os Jos
métodos de un objeto de la forma "nornlal". Con el mecanismo de reflexión, el archivo .c1ass no está disponible en tiempo
de compilación, sino que el que lo abre y examina es el entorno de tiempo de ejecución.
y requiere mucho ti empo ]. Afortunadamente, el meca nismo de reflexión proporciona una fonna de escribir una herramien_
tas simple que nos muestre automáticamente la interfaz completa. He aquí la fonna en que funciona :
jj: typeinfojShowMethods.java
jj Utilización de la reflexión para mostrar todos los métodos de una clase,
jj incluso aunque los métodos estén definidos en la clase base.
II {Args, ShowMethods}
import java.lang.reflect. * ;
import java.util.regex.*;
import static net.mindview.util.Print.*;
int lines = O;
try {
Class<?> c = Class.forName{args[O]);
Method(] methods = e . getMethods() i
Construetor(] ctors = e.getConstruetors();
if(args.length == 1 ) {
for(Method method methods)
print{
p. mateher (met hod. toString () ) . replaeeAll (" " ) ) i
for{Construetor etor : ctors)
print {p . mateher (etor. toString () ) . replaceAll (II") ) ¡
lines = methods.length + ctors.length¡
else {
for(Method method : methods)
if(rnethod.toStringll .indexOf(args[l]) != -1) {
print(
p. matcher (method. toString () ) . replaceAll (" 11) ) ¡
lines++¡
eateh(ClassNotFoundExeeption e)
print ( liNo such class: " + el ¡
I Especialmente en el pasado. Sin embargo. SUIl ha mejorado enonnemente su documentación HTML sobre Java, por lo que ahora es más fácil consultar
los métodos de las clases base.
14 Información de tipos 377
} /* Output,
public static void main(String[])
public native int hashCode()
public final native Class getClass ()
public final void wait(long,int ) throws InterruptedException
public final void wait() throws InterruptedEx ception
public final native void wait (long) throws InterruptedException
public boolean equals{Object)
public String toString( )
public fina l native void notify{)
public final native void notifyAll()
public ShowMethods()
*/ / / >
Los métodos getMethods( ) y getConstructors( ) de Class devuel ven una matri z de tipo Method y una matriz de tipo
Constructor, respectivamente. Cada una de estas clases tiene métodos adicionales para diseccionar los nombres, argumen-
toS y va lores de retomo de los métodos que representan. Pero también podemos utili zar toString(), como se hace en el ejem-
plo, para producir una cadena de ca racteres con toda la signatura del método. El res to del código extrae la información de
la línea de comandos, determina si una signatura concreta se corresponde con la cadena buscada (ut ilizando indexOf( » y
elim ina los cualificadores de los nombres utilizando expresiones regulares (presentadas en el Capítulo 13, Cadenas de
caracteres).
El resultado producido por Class.forName() no puede ser conocido en tiempo de compilación, y por tanto toda la infonna-
ción de signaturas de métodos se está ex traye ndo en tiempo de ejecución. Si analiza la documentación del JDK sobre el
mecanismo de reflexión, verá que existe el suficiente soporte como para poder realizar una invocación de un método sobre
un objeto que sea totalmente desconocido en ti empo de compilación (más adelante en el libro se proporcionan ejemplos de
esto). Aunque inicialmente pueda parecer que no vamos a llegar nunca a necesitar esta funcionalidad , el va lor de los meca-
nismo de reflexión puede resultar ciertamente so rprendente.
La salida anterior se genera mediante la linea de comandos:
java ShowMethods ShowMethods
Puede ver que la salida incluye un constructor predetenninado público, aún cuando no se haya definido ningún conslmctor.
El co nstmctor que vemos es el que el compilador sinteti za de forma automát ica. Si luego ejecutamos ShowMethods con
una clase no pública (es decir, acceso de paquete), el constructor predeterminado sintetizado no aparecerá en la salida. El
constructor predetenninado sinteti zado recibe automáticamente el mismo acceso que la clase.
Otro experimento interesante consiste en invocar java ShowMethods java.lang.String con un argumento adicional de tipo
char, int, String, etc.
Esta herramienta puede ahorramos mucho tiempo mientras programamos, en aq uellos casos en los que no reco rdemos si
una clase dispone de un método concreto y no tengamos ganas de examinar el índice o la jerarquía de clases en la docu men-
tación del JDK, o bien si no sabemos, por ejemplo, si dicha clase puede hace r algo con, por ejemplo, objetos de tipo Color.
El Ca pítulo 22, In/e/faces gráficas de usuario , contiene una vers ión GUl de este programa (personalizada para extraer infor-
mación para componentes Swing), por lo que puede dejar ese programa ejecutándose mientras esté escri biendo código para
poder realizar búsquedas rápidas.
Ejercicio 17: (2) Modifique la ex presión regular de ShowMethods.java para eliminar también las palabras clave
native y final (consejo: utilice el operador OR "').
Ejercicio 18: (1) Defina ShowMethods como una clase no pública y verifique que el constructor predetenninado sinte-
tizado no aparece a la salida.
Ejercicio 19: (4) En ToyTest.java, utilice la ren exión para crear un objeto Toy utilizando el constructor no predetenni-
nado.
Ejercicio 20: (5) Examine la interfaz de java.lang.Class en la documentación del IDK que podrá encontrar en
hllp:l/java.sun.com. Esc riba un programa que tome el nombre de una clase como un argumento de la línea
de comandos, y luego utilice los métodos Class para volcar toda la infonnación di sponible para esa clase.
Compruebe el programa con una cJase de la biblioteca estándar y con una clase que usted mismo defina.
378 Piensa en Java
Proxies dinámicos
El patrón de diseño Proxy es uno de los patrones de diseño básicos. Se trata de un objeto que insertamos en lugar del obje·
te ureal" para proporcionar operaciones adicionales o diferentes, estos objetos nonnalmente se comunican con un objeto
"real", de manera que un proxy actúa típicamente como un intennediario. He aquí un ejemplo trivial para mostrar la estmc·
tura de un proxy:
jj : typeinfo/SimpleProxyDemo.java
import static net.mindview . util.Print.*;
interface Interface {
void doSomething() ¡
void somethingElse(String arg) ¡
class SimpleProxyDemo {
public static void consumer (Interface iface) (
iface.doSomething() ;
iface. somethingElse ( "bonobo lt ) ;
1* Output:
doSomething
somethingElse bonobo
SimpleProxy doSomething
doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo
, /// ,-
Puesto que consumer() acepta una Interface, no puede saber si está conteniendo un objeto real RealObj ect o un Proxy,
porque ambos implementan Interface. Pero el Proxy, que se ha insertado entre el cliente y el objeto ReaIObjec!, reali za
operaciones y luego invoca el método idéntico de RealObjec!.
14 Información de tipos 379
Un prOJ.y puede ser útil siempre que queramos incluir operaciones adicionales en un lugar distinto que el propio "objeto
fea!" , y especialmente cuando queramos poder cambiar fácilmente entre una situación en la que se usen esas operaciones
adicionales Y otra en la que no se empleen, y viceversa (el objeto de utilizar patrones de diseño consiste en encapsular los
cambios, así que sólo si se tienen que efectuar modificaciones para justificar el uso de un patrón). Por ejemplo, ¿qué suce-
de si quisiéramos controlar las Llamadas a los métodos del objeto RealObject , o medir la ca rga de procesamiento asociada
a dichas llamadas? Este tipo de código no conviene incorporarlo en la aplicación, por lo que un proxy nos pennite añadirlo
y eliminarlo fácilmente.
El concepto pro.\y dinámico de Java lleva el concepto de pro.\ y un paso más allá, tanto porque crea el objeto dinámicamen-
te cuanto porque gestiona dinámicamente las llamadas a los métodos para los cuales hemos insertado un pro.\ y. Todas las
llamadas realizadas a un proxy dinámico se redirigen a un único gestor de invocaciones, cuya tarea consiste en descubrir
qué es cada llamada y en decidir qué hacer con ella. He aquí el programa SimpleProxyDemo.java reescrito para utilizar un
prD.\y dinámico:
JI : typein f o/SimpleDynamicProxy . java
import java .lang.reflect. *¡
publ i c Object
invoke(Object proxy, Method method, Obj e ct[) args)
throws Throwable {
System.out.println(" **** proxy: " + proxy.getClass( ) +
", methad: 11 + methad + " , args : 11 + args) ¡
if(args != null)
for {Object arg : args)
System . out .println(" 11 + arg) ¡
return methad .invoke(proxied, args);
class SimpleDynamicProxy {
public static void consumer{Interface iface) {
iface.doSomething{) ¡
iface.somethingElse{lbonobo") ¡
somethingElse bonobo
, /// ,-
Para crear un proxy dinámico se invoca el método estáti co Proxy.ncwProxyInstance(), que requiere un cargador de clases
(generalment e, podemos pasa rle un cargador de clases de un objeto que ya haya sido cargado), una lista de interfaces (no
clases ni clases abstractas) que queramos que el proxy implemente y una implementación de la interfaz InvocationHandler
(gestor de in vocaciones). El prmy dinámico rediri girá todas las llamadas al gestor de invocaciones, de modo que al cons·
tmclOr para el gcslO r de in vocac iones usualmente se le entrega la refe rencia al objeto " real" para que pueda redirigirle las
solicintdes una vez que haya terminado de llevar a cabo su tarea intermediaria.
Al método invoke( ) se le pasa el obj eto prmy. en caso de qu e necesitemos distinguir de dónde viene la solicitud, (aun-
que en muchos casos esto no nos preocupará). Sin embargo, tenga cuidado cuando in voq ue métodos del pro.\y dentro de
invoke(), porque las llamadas a tra vés de la interfaz se redirigen a través del proxy.
~n general. lo que haremos será reali za r la operación intermediario y luego usa r Method.invoke() para red irigir la solici-
Jd hacia el objeto real pasá ndole los argume nt os necesarios. Puede que esto parezca a primera vista algo limitado, co mo si
ólo se pudieran realizar operaciones genéricas. Sin embargo, podemos filtrar ciertas llamadas a métodos, dejando pasa r las
tras directamente:
11: typeinfo/SelectingMethods . java
II Búsqueda de métodos concretos en un proxy dinámico.
import java.lang.reflect. * ;
import static net.mindview.util.Print .* ;
public Object
invoke{Object proxy, Method method, Object(] args)
throws Throwable {
if (method . getName () . equals ("interesting") )
print ( nproxy detected the interesting method");
return method. invoke (proxied , args) i
interface SomeMethods
void boringl();
void boring2();
void interesting(String arg);
void boring3 () ;
class SelectingMethods (
public static void main(String(] args) {
SomeMethods proxy= (SomeMethods)Proxy.newProxylnstance (
SomeMethods . class . getClassLoader() ,
new Class[] { SomeMethods.class },
new MethodSelector(new Implementation() )) ;
14 Información de tipos 381
proxy.boringl() i
proxy.boring2() ;
proxy. interesting ("bonoba") i
proxy.boring3 {) ;
/ * Output:
boringl
boring2
Proxy detected the interesting methad
interesting banaba
boring3
* /// ,-
Aquí. simplemente examinamos los nombres de los métodos, pero también podríamos examinar los aspeclOs de la signatu-
ra del método, incluso podríamos buscar va lores concretos de los argumentos.
El proxy dinámico no es una herramienta para utilizarla todos los días. pero pennite reso lver ciertos tipos de problemas muy
elegantemente. Puede obtener más detalles acerca del patrón de diseño Pro.\y y de otros patrones de diseño en Thinking in
Palterns (,'éase H'H'wA1indView.net) y Design Palterns, de Erich Gamma el al. (Addison- Wesley, 1995).
Ejercicio 21: (3) Modifique SimpleProx yDemo.java para que mida los tiempos de llamada a los métodos.
Ejercicio 22: (3) Modifique SimpleDynamicProxy.java para que mida los tiempos de llamada a los métodos.
Eje rci cio 23: (3) Dentro de invoke( ) en SimpleDyna mic Prox y.ja va, trate de imprimir el argumento proxy y explique
lo que sucede.
P royecto :2 Escriba un sistema utilizando proxies dinám icos para implementar transacciones, donde el proxy se encar-
gue de confirmar /0 transacción si la llamada realizada al objeto real tiene éxito (no genera ninguna excep-
ción), debiendo anular /a transacción si la llamada falla. La continnación y anu lación deben funcionar
como un archivo de texto ex temo, que se encuentra fuera del control de las excepciones Java. Tendrá que
prestar atención a la atomicidad de las operaciones.
Objetos nulos
Cuando se utili za el valor predetenninado null para indicar la ausencia de un objeto, es preciso comprobar si las referencias
son iguales a null cada vez que se utiliza. Esta labor puede llegar a ser muy tediosa y el código resultante es muy comple-
jo. El problema es que nuH no tiene ningún comportamiento propio, salvo generar una excepción NullPointerExcepti on si
se intenta hacer algo con el valor. Algunas veces. resulta úti l introducir la idea de un objeto nu/0 3, que aceptará mensajes en
lugar del objelO al cual "represema", pero que devo lverá valores indicando que no existe ahí ningún objeto "real". De esta
fomla, podcmos asumir que todos los objetos son vá lidos y no tenemos porqué desperd iciar tiempo de programación com-
probando la igualdad con nuJl (y leyendo el código resultante).
Aunque resulta di vertido imaginarse un lenguaje de programación que cree automáticamente objetos nulos por nosotros, en
la práctica no tiene sentido usarlos en todas partes; en ocasiones, será adecuado realizar las comprobaciones de valor l1ull,
en otros casos podremos asumir razonablemente que no vamos a encontrarnos con el valor null, e incluso, en otras ocasio-
nes, será perfectamente aceptable detectar las aberraciones a través de NullPoin terExceptio n. El lugar donde los objetos
nulos parecen ser más útiles es en "el lugar más próximo a los datos", con objetos que representen entidades cn el espacio
del problema. Como ejemplo simple, muchos sistemas dispondrán de una clase Per sono y hay situaciones en el código en
las que no di sponemos de una persona real (o sí di sponemos de ella. pero no tenemos todavía toda la infomlación acerca de
dicha persona), por lo que tradicionalmente utilizaríamos una referencia null y comprobaríamos si las referencias son nulas.
En lugar de ello, podemos crear un objeto nulo, pero aún cuando el objeto nulo responderá a lodos los mensajes
2 Los proyeclOs son sugerencias que pueden utilizarse. por cjcmplo. como uabajo!> dc clasc. Las soluc iones a los proyectos no se incluyen en la guía de
soluciones .
.3Descubieno por Bobby Woolfy Bmce Andcrson. Puede verse como un caso especial del patrón de diseño basado en eslr(l/egill. Una variante del ObjelO
Nlllo es el patrón de disello de t/erador Nlllo. que hace que la interacción a lrav¿s de los nodos en una jerarquía complle~la sea transparente para el clien-
te (el cliellle puede elllonces utilizar la misma lógica para iterar a través de lajer.:¡rquía compuesta y a traves de los nodos hoja).
382 Pien sa en Java
a los que el objeto " real" respondería, sigue siendo necesario disponer de una manera de co mprobar si hay valores nul os.
La forma más simple de hacer esto consiste en crear una interfaz de marcado:
//: net/mindview/util/Null . java
package net . mindview.util¡
public interface Null {) 111,-
Esto permite a insta nceof detectar el objeto nulo, y lo más importante, no requi ere que afiadamos un mérodo is Null() a
todas nuestras clases (lo cual sería, después de todo. simplemente otra forma de utilizar mecanismos RTT I, ¿por qué no uti-
li zar la funcionalidad integrada en su lugar?).
// : typeinfo/Person.java
// Una clase con un objeto Null .
import net.mindview.util.*¡
class Person {
public final String first¡
public final String last;
public final String address¡
II etc.
public Person(String first, String last, String address){
this.first = first¡
this.last = last¡
this.address = address¡
class Position {
private String title¡
private Person person¡
public Position (String jobTitle, Person employeel {
title = jobTitle¡
person = employee;
if(person == null)
person = Person.NULL¡
4 Esa tendencia a implementar la solución más simple posible es una de las recomendaciones de Extreme Programmillg (XP).
384 Piensa en Java
1* Output:
[Position: President Person: Me Last The Top, Lonely At, Position: CTO NullPerson,
Position: Marketing Manager NullPerson, Position: Product Manager NullPerson, Position:
Project Lead Person: Janet Planner The Burbs, Position: Software Engineer Person: Bob
Coder Bright Light City , Position: Software Engineer NullPerson, Position: Software
Engineer NullPerson, Position: Software Engineer NullPerson, Position: Test Engineer
NullPerson, Position: Technical Writer NullPerson]
* /// >
Observe que sigue siendo necesario comprobar la existencia de objetos nulos en algunos lugares, lo cual no difiere Illucho
de comprobar la igualdad con el valor null, pero en otros lugares (como en las conversiones toString( ), en este caso).
no es necesario realizar comprobaciones adicionales; podemos limitarnos a asumir que todas las referencias a objetos son
válidas.
Si estamos trabajando con interfaces en lugar de con clases concretas. es posible utilizar un objeto DynamicProxy para crear
automáticamente los objetos nulos. Suponga que tenemos una interfaz Robot que define un nombre. un modelo y una lista
List<Operation> que describe lo que el Robot es capaz de hacer. Operation contiene una descripción y un comando (es
un tipo del patrón de disetio basado en comandos):
11 : typeinfo/Operation.java
}
/l/o-
14 Información de tipos 385
Esto incorpora tambi én una clase ani dada para realizar las pruebas.
Ahora podemos crear un objeto Robot espec iali zado:
/1: typeinfojSnowRemovalRobot . java
import java.util. * ;
},
n e w Operatian ()
public String description ()
return name + " can chip ice";
1;
/ * Output:
Robot na me: Slusher
Robot model : SnowBot Series 11
Slusher can shove l snow
Slusher shoveling snow
Slushe r can chip ice
Slushe r chipping ice
Slusher can clear the roof
Slushe r clearing roof
*/// , -
Existirá n presum iblemente muchos tipos diferentes de objetos Robot , y lo que nos gustaría es que cada objeto nulo hiciera
algo especial para cada tipo de Robot; en este caso, incorporar infonnación acerca del tipo exacto de Robot que el objeto
nul o representa. Esta información será capturada por el pro.\y dinámico:
11 : t ypeinfo/NullRobot.java
/1 Util i zac i ón de un p r oxy d inámico para c rear un objeto nulo .
386 Piensa en Java
import java.lang.reflect.*;
import java.util.*;
import net.mindview.util.*¡
public Object
invoke{Object proxy, Method method, Object[] args)
throws Throwable {
return method. invoke {proxied, args) ¡
1* Output :
Robot name: SnowBee
Robot model: SnowBot Series 11
SnowBee can shovel snow
SnowBee shoveling snow
SnowBee can chip ice
SnowBee chipping ice
SnowBee can clear the roof
SnowBee clearing roof
[Null Robot)
Robot name: SnowRemovalRobot NullRobot
Robot model: SnowRemovalRobot NullRobot
* /// >
Cuando se necesita un objeto Robot nulo, simplemente se invoca newNullRobot( ), pasándole al método el tipo de Robot
para el que queremos que actúe como proxy. El prox)' satisface los requisitos de las interfaces Robot y Null. y proporciona
el nombre específico para el que actúa como pro.\)'.
14 Información de tipos 387
public interface A {
void f 1) ;
} 111 ·-
Esta mterfaz se implementa a continuación y podemos '"er fácilmente cómo saltamos los controles para obtener el tipo real
de implementación:
11 : typeinfo / lnterfaceVio lation.java
11 Sorteando una interfaz.
import typeinfo.interfacea.*;
class B implements A
public void f 1) {}
public void 9 1) {}
1* Output:
Uti li zando RTfl, descubrimos que a ha sido implementado como R. Proyectando sobre R, podemos invocar un método que
no se encuentre en A.
388 Piensa en Java
Esto es perfectamellle legal y aceptable. pero puede que no queramos que los programadores de clientes hagan esto, ya que
esto les da la opornmidad para acoplarse más estrechamente con nuestro código de lo que querríamos. En otras palabras,
podríamos pensar que la palabra clave interface nos está protegiendo. pero en realidad no es así, y el hecho de que utili cc-
mas B para implementar A en este caso es algo de dominio público. 5
Una solución consiste simplemente en decir que los programadores serán los responsab les si deciden utilizar la clase real
en lugar de la interfaz. Esto es probablemente razonable en muchos casos, pero si ese " probablemente" no es suficiente, Con-
viene apli car otros controles más estrictos.
La técnica más sencilla consiste en uti lizar acceso de paquete para la implemclllación, de modo que los clientes situados
fuera del paquete no puedan verla:
jj : typeinfo/packageaccess/HiddenC.java
package typeinfo.packageaccess;
import typeinfo.interfacea .* ¡
import static net.mindview.util.Print.*;
class C implements A {
public void f () { print ( "public C. f () " ) ;
public void g{) { print("public C.g()");
void u() { print("package C.u()"); )
protected void v () { print ( "protected c. v () " ) ;
priva te void w() { print("private C .W {) H) ¡ }
5 El caso mas famoso cs el sistema operativo Windo\Vs. que tenía una API publica con la que se suponía que había que desarrollar programas y un conjun-
to no publicado pero visible de funciones que podiamos descubrir e invocar. Para resolver los problemas. los programadores utilizaban las funcione s ocut-
tas de la API , lo que forzó a Microsoft a mumcnerlas como si flleran pane de la API pública. Esto se convini6 en una fuente de grandes costes y de enonne
trabajo para la empresa.
14 Información de tipos 389
callHiddenMethod(a, "u");
callHiddenMethod (a, "v");
callHiddenMethod (a, "w");
/* Output:
public c. f ()
typeinfo.packageaccess.C
public c . 9 ()
package c. u ()
protected C. v ()
prívate C. w ()
*///,-
Como puede ver, sigue siendo posible meterse en las entrañas e invocar lodos los métodos utili zando el mecanismo de refle-
xi ón. ¡Incluso los Inétodos pri vados! Si se conoce e l nombre del método, se puede in vocar setAccessible(tru e) sobre el obje-
to Meth od para hacerlo ¡nvocable, como podemos ver en caIlHiddenMethod().
Podríamos pensar que es posible impedi r esto distribuyendo sólo el código compilado, pero no es un a solución. Basta con
ejecutar j a va p, que es el descompilador incluido en el J DK. He aqu í la línea de comandos necesari a:
j avap -pri vate e
El indi cador -pri vate especifica que deben mostrarse todos los m iembros, incluso los privados. He aquí la salida que se
obtiene:
class typeinfo.packageaccess.C extends
java.lang.Object impl e ments typeinfo.interfacea.A
typeinfo . packageaccess . C{) ;
public void f () ;
public void g();
void u () ;
protected void v() i
private void w() i
Por tanto, cua lquiera puede obtener los nombres y signaturas de los métodos más pri vad os e invoca rl os.
¿Qué sucede si implementamos la interfaz con un a clase int ern a privada? He aqu í un ej empl o:
11: typeinfo/lnnerlmplementation . java
II Las clases interna privadas no pueden ocultarse del mecanismo de
II reflexión.
import typeinfo.interfacea.*;
import static net . mindview.util.Print.*¡
class InnerA {
private static class e implements A {
public void f (1 { print ( " public c. f () "1 ;
public void 9 (1 { print ("public c . 9 () ") ;
void u() ( print("package e.u()"I ; )
protected void v () { print ("protected c . v () ") ;
private void w() { print("private C.w() "); }
1* Oucput :
public e. f l)
I nne rA$C
p u blic e . g 11
package C. u()
protected C. v ()
private C.w ()
' jjj >
Esta solución no nos ha penni tido ocultar nada a ojos del mecanismo de reflexión. ¿Qué sucedería con una clase anónima?
11 : typeinfo / Anonyrnouslmplementation.java
II Las c lases internas anónimas no pueden ocultarse del mecanismo de
II reflex i ón.
import typeinfo . interfacea . *¡
import stat i c net . mindvi ew.util.Print.*¡
class AnonyrnousA {
public static A makeA ()
return new A () {
publi c void f 11 ( print I "public e. f 11 ,, ) ;
public void g il ( print I "publi c e . g il " ) ;
v o id u 11 ( print I "package e. u 1) " ) ; }
protected void v () { print ( "prot ected C. v () " ) ¡
private void w() { print ( "private C.w () " ) i }
};
1* Output:
public e . f l)
AnonymousA$l
pub lic e . g l)
package C. u ()
protected C. v ()
private C. w()
' //j , -
14 Información de tipos 391
Parece que no existe ningu na faffila de impedir que el mecanismo de reflexión entre e invoque los métodos qu e no tienen
acceso público. Esto también se cumple para los campos. inc luso para los campos pri vados:
JI : typeinfo / ModifyingPrivateFields.java
import java.lang.reflect.*i
class WithPrivateFinalField
private int i = 1;
private final String s = "1 'm totally safe" i
private String 52 = "Am 1 safe?" i
public String toString () {
return 11 i = 11 + i + ", 11 + S + 11 + 52;
/ * Outp ut :
i = 1, 1 'm totally safe, Am I saf e?
f .getInt (pf) , 1
i = 47, I ' m totally sa f e, Am I safe?
f .get (pf) : 1 'm totally sa f e
i = 47, I ' m totally safe, Am I safe?
f.get(pf) : Am 1 safe?
i = 47, I' m tota ll y safe, No , you 're not !
*/// , -
Sin embargo, los campos de tipo final sí que están protegidos frente a los cambios. El sistema de tiempo de ejecución acep-
ta los intentos de cambio sin quejarse, pero no se produce cambio alguno.
En general , todas estas violaciones de acceso no constituyen un problema grave. Si alguien utiliza una de estas técnicas para
invocar métodos que se han marcado como privados o con acceso de paquete (lo cual indica claramente que no deberían
invocarse), entonces es dificil que esas personas puedan quejarse si decidimos cambiar posterionnente algunos aspectos de
esos métodos. Por otro lado, el hecho de que siempre exista una puerta trasera para entrar en una clase nos pennite resolver
ciertos tipos de problemas que en otro caso serían dificiles o imposibles, y los beneficios del mecanismo de reflexión son,
por regla general, incuestionables.
Ejercicio 25: (2) Defina una clase que contenga métodos privados, protegidos y con acceso de paquete. Escriba código
para acceder a dichos métodos desde fuera del paquete de la clase.
392 Piensa en Java
Resumen
RTTI nos pennite descubrir la infonnación de tipos a partir de una referencia anónima a una clase base. Es por ello que se
presta a una inadecuada utilización por los usuarios menos expertos, ya que resulta más fácil de comprender que las llama-
das polimórficas a métodos. Para las personas que tienen experiencia previa en lenguajes procedimentales, resulta difícil
organizar los programas en conjuntos de instrucciones switch. Este tipo de estmctura puede implementarse fácilmente con
RTTI perdiéndose así el importante valor que el polimorfismo añade al desa rrollo y el mantenimiento del código. La inten-
ción de la programación orientada a objetos es utili zar llamadas polimórficas a métodos siempre que se pueda y RTTI sólo
cuando no haya más remedio.
Sin embargo, las llamadas polimórficas a métodos, tal como está prevista en el lenguaje, requiere que tengamos control de
la definición de la clase base, porque en algún punto dentro del proceso de extensión del programa podemos llegar a descu-
brir que la clase base no incluye el método que necesitamos. Si la clase base proviene de una biblioteca desalTollada por
algún otro programador, una so lución es RTTI: podemos heredar un nuevo tipo y ailadir el método adicional que necesita-
mos. En el resto del código podemos entonces detectar ese tipo concreto que hemos añadido y llamar a ese método espe-
cial. Esto no destmye el polimorfismo y la extensibilidad del programa, porque el aiiadir un nuevo tipo no requiere que
andemos a la caza de instmcciones switch en nuestro programa. Sin emba rgo, cuando añadamos código que dependa de la
nueva funcionalidad aiiadida, nos veremos obligados a util izar RTTI para detectar el tipo concreto que hayamos definido.
Añadir una funcionalidad en una clase base puede implicar que, a cambio de obtener exclusivamente un beneficio en esa
clase concreta, todas las demás clases derivadas de la misma deberán ca.rgar con un esqueleto de método completamente
carente de significado. Esto hace que la interfaz sea menos clara y resulta bastante molesto para aque llos que se ven obli-
gados a sustituir métodos abstractos cuando derivan otra clase a partir de esa clase base. Por ejemplo, considere una jerar-
quía de clases que representa instrumentos musicales. Suponga que desea limpiar las válvulas de las boquillas de todos los
instrumentos apropiados de su orquesta. Una opción es incluir un método IimpiarValvula() en la clase base Instrumento,
pero esto resulta confuso, porque implicaría que los instrumentos de Percusión, Cuerda y Electrónicos también tienen
boquillas y vá lvulas. RTTI proporciona una solución mucho más razonable, porque nos pennile colocar el método en la
clase específica donde resulta aprop iado (Viento, en este caso). AlmLsmo tiempo, podemos descubrir que existe una solu-
ción más lógica, que en este caso consistiría en incluir un método prepararlnstrumento(). Sin embargo, puede que no vea-
mos esa solución cuando estemos tratando por primera vez de resolver el problema y, como consecuencia, podríamos asumir
erróneamente que es necesario utilizar RTTI.
Finalmente, RTTI pennite en ocasiones resolver problemas de eficiencia. Suponga que nuestro código utili za apropiadamen-
te el polimorfismo, pero resulta que uno de los objetos reacciona a este código de propósito general de una manera terrible-
mente poco eficiente. Podemos detectar ese tipo concreto de objeto utilizando RTTI y escribir código específico para
mejorar la eficiencia. No caiga en la tentación, sin embargo, de estructurar sus programas demasiado pronto pensando en la
eficiencia. Se trata de una trampa bastante tentadora. Lo mejor es conseguir primero que el programa funcione y luego deci-
dir si está funcionando lo suficientemente rápido. Sólo entonces deberemos abordar los problemas de eficiencia con una
herramienta de perfilado (consulte el suplemento en htrp://MindView.net/Books/Be((erJava).
También hemos visto que el mecanismo de reflexión abre un nuevo mundo de posibilidades de programación, pemlitiendo
un esti lo de programaci ón mucho más dinámico. Existen programadores para los que la naturaleza dinámica del mecanis-
mo de reflexión resulta bastante perturbadora. El hecho de que podamos hacer cosas que sólo pueden comprobarse en tiem-
po de ejecución y de las que sólo se puede informar mediante el mecanismo de excepciones, parece, para las mentes
cómodamente acostumbradas a la seguridad de las comprobaciones estáticas de tipos, algo bastante pernicioso. Algunas per-
so nas sostienen incluso, que el introducir la posibilidad de una excepción en tiempo de ejecución es una indicación clara de
que dicho tipo de código debe evitarse. En mi opinión, esta sensación de seg uridad no es más que una ilusión, siempre hay
cosas que pueden suceder en tiempo de ejecución y que pueden gene rar excepciones, incluso en un programa que no con-
tenga ningún bloque try ni ninguna especificación de excepción. En lugar de ello, en mi opinión, la existencia de un mode-
lo coherente de infonnación de errores nos permite escribi r código dinámico utilizando los mecanismos de reflexión. Por
supuesto, merece la pena tratar de escribir código que pueda comprobarse estáticamente ... siempre que se pueda. Pero creo
que el código dinámico es una de las características más importantes que diferencia a Java de otros lenguajes como C++.
Ejercicio 26: (3) Implemente un método IimpiarV.lvula() como el desc rito en este resumen.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tll e Thillki"g il1 1am AI1/1o/ated SO/liriO" Gllide, disponible para
la venta en l\'\\'W.A lind J'ie\\:l1et.
Genéricos
Los métodos y clases ordinarios funcionan con tipos específicos: con tipos primitivos o con
clases. Si lo que queremos es escribir código que pueda utilizarse con un tipo más amplio
de tipos, esta rigidez puede resultar demasiado restrictiva. I
Una de las fonnas en que los lenguajes orientados a objetos penniten la generalización es a través del polimorfismo. Por
ejemplo, podemos escribir un método que tome un objeto de una clase base como argumento, y luego utilice dicho método
con cualquier clase derivada de dicha clase base. Con ello, el método será algo más general y podrá ser utilizado en más
lugares. Lo mismo cabe decir dentro de las clases: en cualquier lugar donde utilicemos un tipo específico, un tipo base pro-
porcionará mayor flexibilidad. Por supuesto, podemos extender todas las clases salvo aquellas que hayan sido definidas
como finales 2 , po r lo que esta flexibilidad se obtiene de manera automática la mayor parte de las veces.
En ocasiones, limitarse a una única jerarquía puede resultar demasiado restrictivo. Si el argumento de un método es una
interfaz en lugar de una clase, las limitaciones se relajan de modo que ahora se incluirán todas aquellas clases que imple-
menten la interfaz, incluyendo clases que todavía no hayan sido desarrolladas. Esto proporciona al programador de clientes
la opción de implementar una interfaz para adaptarse a nuestra clase o método. Con esto, las interfaces nos penlliten esta-
blecer un vínculo entre jerarquías de clases, siempre y cuando tengamos la opc ión de crear una nueva clase para implemen-
tar ese vínculo.
Algunas veces, incluso una interfaz resulta demasiado restrictiva. Las interfaces siguen requiriendo que nuestro código fun-
cione con esa interfaz concreta. Podríamos escribir código todavía más gene ral si el lenguaje nos permitiera decir que ese
código funciona con "algún tipo no especificado", en lugar de con una interfaz o clase específicas.
En esto se basa el concepto de genéricos, un cambio de los más significativos en Java SES. Los genéri cos implementan e l
concepto de tipos parametrizados, que penlliten crear componentes (especialmente contenedores) que resultan fáciles de
util.izar con múltiples tipos. El término "genérico" significa " peI1eneciente o apropiado para grandes grupos de clases". La
intención original de los genéricos en los lenguajes de programación era dotar al programador de la mayor capacidad expre-
siva posible a la hora de escribir clases o métodos, relajando las rest ricciones que afectan a los tipos con los que esas clases
o métodos pueden funcionar. Como veremos en este capítulo, la implementación de los genéricos en Java no tiene un alcan-
ce tan grande; de hecho, podríamos cuestionamos si el término "genérico" resulta siquiera apropiado para esta funcionali-
dad de Java.
Si no ha visto antes ningún mecanismo de tipos parametrizados, los genéricos de Java le parecerán, probablemente, una
mejora sustancial del lenguaje. Cuando se crea una instancia de un tipo parametrizado, el lenguaje se encarga de realizar las
proyecciones de los tipos por nosotros y la corrección de los tipos se garantiza en tiempo de compilación. Evidentemente,
parece que este mecanismo es toda una mejora.
Sin embargo, si el lector ya tiene experiencia con algún mecanismo de tipos parametrizados, como por ejemplo en C++,
encontrará que no se pueden hacer con los genéricos de Java todas las cosas que cabría esperar. Mientras que utilizar un tipo
genérico desarrollado por alguna otra persona resulta bastante sencillo, a la hora de crear nuestros propios genéricos nos
I Quiero dar las gracias a Angelika Langer por su liSIa de preguntas frecuentes Jm'G Geller¡cs FAQ (véase II'wl\:!allge,:camefOl.de), así como por sus otros
escritos (hechos en colaboración con Klaus Kreft). Esos trabajos han resultado enonnementc valiosos de cara a la preparación de este capítulo.
encontraremos con diversas sorpresas. Uno de los aspeclOs que trataremos de explicar en este capínllo son los motivos por
los que la funciona lidad se ha imp lementado en Java en la manera en que se ha hecho.
No queremos decir que los genéricos de Java sean inútiles. En muchos casos, consiguen que el código sea más directo e
incluso más elegante. Pero, si el leclOr ha utilizado ante riomlente algún lenguaje donde esté implemenrada una versión más
pura de los genéricos, puede que la solución de Java le desilusione. En este capítulo. vamos a examinar tanto las fortalezas
como las debilidades de los genéricos de Java, con el fin de que el lec tor pueda utilizar esta nueva funcionalidad de mane-
ra más efectiva.
Genéricos simples
Una de las razones iniciales más fuertes para introducir los genéricos era crear clases de contenedores, de las que ya hemos
hablado en el Capítulo 11 , Almacenamiellfo de objetos (hablaremos más acerca de estas clases en el Capítulo 17. Análisis
detallado de los contenedores). Un contenedor es un lugar en el que almacenar objetos mientras trabajamos con ellos.
Aunque esto también es cierto para las matrices, los contenedores tienden a ser más fl exibles y sus características son dis-
tintas a las de las matrices simples. Casi todos los programas requieren que almacenemos un grupo de objetos mientras los
utilizamos, por lo que los contenedores son una de las bibliotecas de clases más inherentemente reutilizables.
Examinemos una clase que almacena un único objeto. Por supuesto. la clase podría especificar el tipo exacto del objeto de
la fonna siguiente :
11 : generics/Holderl.java
class Automobile {}
private Object a¡
public Holder2(Object al { this . a = a; }
public void set(Object al { this.a = a¡ }
public Object get () { return a¡ }
public static void main(String[] args) {
Holder2 h2 = new Holder2{new Automobile());
Automobile a = (Automobile)h2.get();
h2.set("Not an Automobile");
String s = (String) h2 .get ();
h2 . set{1); // Se transforma automáticamente en Integer
Integer x = (Intege r )h2 .get () ;
Ahora, la clase Holder2 puede almacenar cualquier cosa y, en este ejemplo, un único objeto Hold er2 almacena tres tipos
distintos de objetos.
Hay algunos casos en los que queremos que un contenedor almacene múltiples tipos de objetos, pero lo más normal es que
sólo coloquemos un tipo de objeto en cada contenedor. Una de las principales motivaciones de los genéricos consiste en
especificar el tipo de objeto que un contenedor almacena, y hacer que dic ha especificación quede respaldada por el compi-
lador.
Por tanto, en lugar de emplear Object, lo que querríamos es poder utilizar un tipo no especificado, lo que podremos deci-
dir en algún momento posterior. Para hacer esto, incluimos un parámetro de tipo entre corchetes angulares después del nom-
bre de la clase y luego, al utili zar la clase, sustituimos ese parámetro por un tipo real. Para nuestra clase contenedora an terior,
la técni ca consistiría en lo siguiente, donde T es el parámetro de tipo:
11: generics/Holder3.java
Ahora, cuando creemos un objeto Holder3, deberemos especificar el tipo que queramos almacenar en el mismo, utilizando
la sintaxis de corchetes angulares, como puede verse en main(). Sólo podemos introducir en el contenedor objetos de dicho
tipo (o de alguno de sus subtipos, ya que el principio de sustinlción sigue funcionando con los genéricos). Y al ex traer del
contenedor un va lor, dicho valor tendrá automáticamente el tipo correcto.
Ésta es la idea fundamental de los genéricos de Java: le decimos al compi lador qué tipo queremos usar y el compilador se
encarga de los detalles.
En genera l, podemos tratar los genéricos como si fueran otro tipo más que en lo único que se diferencia de los tipos nonna-
les es en que tiene parámetros de tipo. Pero, como veremos en breve, podemos ut ilizar los genéricos simplemeOle nombrán-
dolos junto con su lista de argumentos de tipo.
Ejercicio 1: (1) Utilice HolderJ con la biblioteca typeinfo.pets para demostrar que un objeto Holder3 que se haya
especificado para almacenar un tipo base, también puede almacenar un tipo derivado.
Ejercicio 2: (1) Cree una clase contenedora que almacene tres objetos del mismo tipo, junto con los métodos para
almacenar y extraer dichos objetos y un constructor para inicializar los tres.
396 Piensa en Java
}
;;;,-
Para utili zar una tupla, simplemente definimos la tupla de la longitud adecuada como valor de retomo para nuestra función
y luego la cream os y la devolvemos en la instrucción return :
1/: generics/TupleTest . java
import net . mindview.util .* ¡
elass Amphibian {}
elass Vehiele {}
static
FourTuple<Vehicle,Amphibian,String,Integer> h()
return
new FourTuple<Vehicle,Amphibian,St ri ng,Integer>(
new Vehicle(), new Amphibian (}, "h i n, 47) i
static
FiveTuple<Vehicle,Amphibian,String,Integer,Double> k()
return new
FiveTuple<Vehicle,Amphibian,Str i ng, Integer, Double> (
new Vehicle(), new Amphibian(), "hi " , 47, 11 . 1) ¡
System.out.println(ttsi) i
JI ttsi.first = "there"¡ // Error de compilación: final
System.out.println(g()) ;
System.out.println(h()) ;
System.out.println{k()) i
/ * Output:
stun!
on
Phasers
*///,-
La clase interna Node también es un genérico y ti ene su propio parámetro de tipo.
Este ejempl o hace uso de un indicador de fin para detemünar cuándo la pila está vacía. El indicador de fin se crea en e l
momento de construir el contenedor LinkedStack, y cada vez que se invoca push( ) se crea un nuevo objeto Node<T> y
se enlaza con el objeto Node<T> anterior. Cuando se invoca a pop(). siempre se devuelve top.item, y luego se descarta el
objeto Node<T> actua l y nos desplazamos al sigui ente, excepto cuando encontramos el indicador de fin , en cuyo caso no
noS desplazamos. De esa forma, si el cliente continúa in vocando pope ), obtendrá como respuesta valores nuH para indicar
que la pila está vaCÍa.
Ejercicio 5: (2) Elimine el parámetro de tipo en la clase Node y modifique el resto del código en LinkedStack.java
para demostrar que una clase interna ti ene acceso a los parámetros de tipo genérico de su clase ex terna.
RandomList
Como ejemplo adicional de contenedor, suponga que querernos di sponer de un tipo especial de lista que seleccione aleato-
riamente uno de sus elementos cada vez que invoquemos select( ). Al hacer esto, podemos co nstruir una herramienta que
funcione para todos los objetos, así que utili zamos genéricos:
// : generics /RandornList.java
import java . util.*;
/ * Output :
brown over fox quick quick dog brown The brown lazy brown
* /// ,-
Ejercicio 6: ( 1) Utilice RandornList con dos tipos adicionales además del que se muestra en main( ).
Interfaces genéricas
Los genéricos también funcionan con las interfaces. Por ejemplo, un generador es una clase qu e crea objetos. En la prácti-
ca, una especiali zac ión del patrón de diseño basado en el método defactoria, pero cuando pedimos a un generador que cree
400 Piensa en Java
un nuevo objeto no le pasamos ningún argumento, al contrario de lo que sucede con un método de factoría. El generador
sabe cómo crear nuevos objetos sin ninguna infonnación adicional.
Típicamente, un ge nerador simplemente define un método, el método que produce nuevos objetos. Aquí, lo denominaremos
next( ) y lo incluiremos en las utilidades estándar:
1/: net/mindview/util/Generator.java
II Una interfaz genérica.
package net.mindview.util;
public interface Generator<T::> { T next (); } 11/:-
El tipo de retomo de next() se parametriza como T . Como puede ver, la utilización de genéricos con interfaces no es dife-
rente de la util izac ión de genéricos con clases.
Para ilustra r la implementación de un objeto Generator, necesitaremos al gunas clases. He aquí una jerarquía de ejemplo:
11: generics/coffee/Coffee.java
package generics.coffee;
11: generics/coffee/Latte.java
package generics.coffee;
public class Latte extends Coffee {} 111:-
//: generics/coffee/Mocha.java
package generics.coffee;
public class Mocha extends Coffee {} 11/ :-
/1: generics/coffee/Cappuccino.java
package generics.coffee;
public class Cappuccino extends Coffee {} 111:-
11: generics/coffee/Americano.java
package generics.coffee¡
public class Americano extends Coffee {} /1/:-
//: generics/coffee/Breve.java
package generics . coffee;
public class Breve extends Cof fee {} /1/:-
Ahora, podemos implementar un objeto Generator<Coffee> que genera aleatori amente diferentes tipos de objetos Coffee:
11: generics/coffee/CoffeeGenerator.java
/1 Generar diferentes tipos de objetos Coffee:
package generics.coffee;
import java.util.*;
import net.mindview.util.*¡
1* Output:
Americano O
Latte 1
Americano 2
Mocha 3
Mocha 4
Breve 5
Americano 6
Latte 7
Cappuccino 8
Cappuccino 9
*111,-
La inte rfaz Generator parametrizada garanti za que next( ) devuel va el tipo definido en el parámetro. CoffeeGenerator
también implementa la interfaz Iterable, por lo que se le puede usar en una instrucciónforeach. Sin embargo, requiere un
"indicador de fin" para saber cuándo parar, y esto se crea utilizando el segundo constructor.
He aquí una segunda implementación de Generator<T>, que esta vez se utili za para generar números de Fibonacci:
/1 : generics/Fibonacci.java
II Generar una secuencia de Fibonacc i.
import net.mindview.util.*;
/ * Output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
* jjj ,-
Aunque estamos trabajando con va lores int tanto dentro como fuera de la clase, el parámetro de tipo es Intege r . Esto nos
plantea una de las limitaciones de los gené ricos de Java. No se pueden utilizar primitivas como parámetros de tipo. Sin
embargo, Java SES ha añadi do, afortunadamente, la fu ncionalidad de conversión automática entre primitivas y tipos envol-
torio, para poder efectuar las conversiones fácilmente. Podemos ver el efecto en este ejemplo porque los va lores in t se uti-
li zan, en general, en la clase de manera transparente.
Podemos ir un paso más allá y crear un generador de Fibonacci de tipo Iterable. Una opción consiste en reimp lementar la
clase y añadir la interfaz Iterabl e. pero no siempre tenemos control sobre el código original, y no merece la pena reesc ribir
código a menos que nos veamos obligados a hacerlo. En lugar de ello, podemos crear un adaptador para obtener la interfaz
deseada; este patrón de diseño ya fue presentado anteriomlente en el libro.
Los adaptadores pueden implementase de múltiples fomlas. Por ejemplo, podemos utili zar el mecanismo de herencia para
generar la clase adaptada:
11 : generics / IterableFibonacci.java
11 Adaptar la clase Fibonacci para hacerla de tipo Iterable.
import java.util.*¡
1* Output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584
* jjj ,-
Para utili zar IterableFibonacci en una instrucción foreach, hay que proporcionar al constructor un límite para que
hasNexl() sepa cuándo devolver false.
Ejercicio 7: (2) Uti lice el mecanismo de composición en lugar del mecanismo de herencia para adaptar Fibonacci con
el fm de hacerla de tipo Iterable.
Ejercicio 8: (2) Siguiendo la fonna del ejemplo CoITee, cree una jerarquía de personajes (SloryCharacler) de su pelí-
cula favorita, diviéndo los en buenos (GoodGu ys) y malos (BadGuys). Cree un generador para
Slor yC haracler , siguiendo la fonna de CoffeeGeneralor.
15 Genéricos 403
Métodos genéricos
Hasta ahora, hemos estado analizando la parametrización de clases enteras, pero también podemos parametrizar métodos de
una clase. La propia clase puede ser o no genérica; esto no influ ye en la posibilidad de disponer de métodos genéricos.
Un método genéri co permite que el método va ríe independientemente de la clase. Como directriz, deberemos usar los méto-
dos genéri cos "siempre que podamos". En otras palabras : si es posible hacer que un método sea genérico, en lugar de que
lo sea la clase completa, probablemente el programa sea más claro si hacemos genérico el método. Además, si un método
es estáti co, no tiene acceso a los parámetros genéricos de tipo de la clase, por lo que si esa genericidad es necesaria en el
método, deberemos definirlo como un método genérico.
Para definir un método genético, simplemente colocamos una lista de parámetros genéricos delante del va lor retorno del
modo siguient e:
jj: genericsjGenericMethods.java
} / * Output,
java .lang.String
java . lang.lnteger
java.lang.Double
java.lang.Float
java . lang.Character
GenericMethods
*///,-
La clase GenericMethods no está paralnetrizada, aunque es perfectamente posible parametrizar simultáneamente tanto una
clase como sus métodos. Pero en este caso, solo el método f() tiene un parámetro de tipo, indicado por la lista de paráme-
tros antes del tipo de retorno del método.
Observe que con una clase genérica es preciso especificar los parámetros de tipo en el momento de instanciar la clase. Pero
con un método genérico, usualmente no hace falta especificar los tipos de parámetro, porque el compi lador puede detenni-
nar esos tipos por nosotros. Este mecanismo se denomina inferencia del argumento de tipo. Por tanto, las llamadas a f()
parecen llamadas a método nonnales, y en la práctica f() se comporta como si estuviera infinitamente sobrecargado. El
método admitirá incluso un argumento del tipo GenericMethods.
Para las llamadas a f() que usen tipos primitivos entra en acción el mecanismo de conversión de tipos automática, envol-
viendo de manera transparente los tipos primitivos en sus objetos asociados. De hecho, los métodos genéricos y el mecanis-
mo de conversión automática de tipos penniten eliminar parte del código que anterionnente requería utilizar conversiones
de tipos manuales.
Ejercicio 9: ( 1) Modifique GenericMethods.java de modo que f( ) acepte tres argumentos, cada uno de los cuales
tiene que ser de un tipo parametrizado distinto.
Ejercicio 10: ( 1) Modifique el ejercicio anterior de modo que uno de los argumentos de f() no sea parametrizado.
404 Piensa en Java
II Ejemplos,
public static veid main (String [] args) {
Map<String, List<String» sls = New.map{) i
List<String> ls = New.list();
LinkedList<String> lIs = New.lList( ) i
Set<String> ss = New.set() ¡
Queue<String> qs = New.queue( );
}
111 ,-
En main() podemos ver ejemplos de cómo se emplea esta herramienta: la inferencia del argumento de lipa elimina la nece-
sidad de repetir la lista de parámetros genéricos. Podemos ap licar esto a holding/MapOfList.java :
// : generics / Simp1erPets.java
impert typeinfo.pets . *i
impert java.util. *¡
impert net.mindview.uti1.*;
Aunque se trata de un ejemplo interesante del mecanismo de inferencia del argumento de tipo, resulta dificil detennioar las
ventajas de este mecanismo. A la persona qu e lea el código la obligamos a analizar y a comprender esta biblioteca adicio-
nal y sus implicaciones, por lo que sería igual de productivo dejar la definición original (que es bastante repetiti va) precisa-
mente para simplificar. Sin embargo, si la biblioteca estándar de Java incluyera algo similar a la utilidad New.j ava que
hemos prese ntado, tendría bastante se ntido utili zarla.
El mecanismo de inferencia de tipos no funciona más que en las asignaciones. Si pasamos el res ultado de una llamada a un
método, tal como New.rna p (), como argumento a otro método, el compi lador no intentará realizar una inferencia de tipos.
En lu gar de ello, lo que hará es tratar la llamada al método como si el valor de retomo se asignara a una variable de tipo
Object. He aquí un ejemplo con el que se gene ra un error de compilación:
JJ :genericsJLimitsOflnference.java
import typeinfo.pets. * ;
import java.util .* ;
Ejerc icio 11 : ( 1) Pruebe New.j ava creando sus propias clases y verificando que New func ione adecuadamente con las
mismas.
Por supuesto, esto elimina la ventaja de utilizar la clase New para reducir la cantidad de texto tecleado, pero esta sintaxis
adicional sólo será necesaria cuando no es tem os esc ribiendo una instrucción de asignación.
Eje rcicio 12: (1) Repita el ejercicio anterior utilizando la especificación explicita de tipos.
/ * Output:
[A]
[A, B, e]
[, A, B, e, D, E, F, F, H, 1, J, K, L, M, N, O, P, Q, R, S, T, U, V, w, x, Y, z]
* /// ,-
El método makeLis t( ) mostrado aquí tiene la misma funcionalidad que el método java.utiI.Arr ays.asLis t() de la bibl io-
teca estándar.
/ * Output:
Americano O
Latte 1
Americano 2
Mocha 3
1, 1, 2, 3, 5, 8, 13, 21, 34 , 55, 89, 144,
* /// ,-
Observe que el método generico fill( ) puede aplicarse de fomla transparente a contenedores y generadores de objetos
Coffee e Intege r.
15 Gené ricos 407
Ejercicio 13: (4) Sobrecargue el método fill () de modo que los argumentos y tipos de retorno sean los su btipos especi-
ficos de Collection : List, Queue y Set. De esta fonna, no pe rdemos el tipo de contenedor. ¿Podemos uti-
liza r el mecanismo de sobreca rga para distinguir entre List y LinkedList?
/ * Output:
CountedObj ect O
CountedObj ect 1
CountedObject 2
CountedObject 3
CountedObject 4
*/// 0-
Podemos ver cómo el método genérico reduce la cantidad de texto necesaria para crear el objeto Generator. Los genéricos
de Ja va nos fuerzan a pasar de todos modos el objeto C lass, por lo que también podríamos utili za rlo para la inferencia de
tipos en el método ereate( ).
Ejercicio 14: ( 1) Modifique BasieGeneratorDemo.java para utilizar la fonna explicita de creación del objeto
Generator (es decir, utilice el constructor explícito en lugar del método genérico create()j.
statie
FourTuple<Vehicle,Amphibian,String,Integer> h()
return tuple{new Vehicle () , new Amphibian(), tlhi" , 47) i
statie
Fi veTuple<Vehicle. Amphibian I String, Integer, Double> k () {
return tuple(new Vehicle(), new Amphibian{),
tlhí" , 47, 11.1);
Los primeros tres métodos dupli can el primer argumento copiando sus referencias en un nuevo objeto HashSet, de modo
que los conjuntos utilizados como argumentos no se modifican directamente. El valor de retorno será, por tanto, un nuevo
objeto Seto
Los cuatro métodos representan las operac iones matemáticas de conjuntos: union() devuelve un objeto Set que contiene la
combinación de los dos argumentos, intersection( ) devuelve un objeto Set que contiene los elementos comunes a los dos
argumentos, difference( ) resta los elementos subsel de superset y complement( ) devuelve un objeto Sel con todos los
elementos que no formen parte de la intersección. Para crear un ejemplo simple que muestre los efectos de estos métodos,
he aqu í una enumeración que contiene di ferentes nombres de acuarelas:
11: generics/watercolors/Watercolors.java
package generics.watercolors;
complement{setl, set2));
/* Output: (Sample)
setl, IBRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET, CERULEAN_BLUE HUE,
PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUEJ
set2, ICERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN,
VIRIDIAN_HUE, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER, BURNT_UMBERJ
union(setl, set2): [SAP_GREEN, ROSE_MADDER, YELLOW OCHRE, PERMANENT GREEN, BURNT UMBER,
COBALT_BLUE_HUE, VIOLET, BRILLIANT_R ED, RAW_UMBER, ULTRAMARINE, BURNT_SIENNA, CRIMSON,
CERULEAN_BLUE_HUE, PHTHALO_BLUE, MAGENTA, VIRIDIAN_HUEJ
intersection(setl, set2): [ULTRAMARINE, PERMANENT_GREEN, COBALT BLUE_HUE, PHTHALO_BLUE,
CERULEAN_BLUE_HUE, VIRIDIAN_HUEJ
difference(setl, subset): [ROSE_MADDER, CRIMSON, VIOLET, MAGENTA, BRILLIANT_REOl
differencelset2, subsetl, lRAW_UMBER, SAP_GREEN, YELLOW_OCHRE, BURNT SIENNA, BURNT_UMBERJ
complement(se tl, set2): [SAP_GREEN, ROSE_MADDER, YELLOW_OCHRE, BURNT_UMBER, VIOLET, BRI-
LLIANT_RED, RAW_UMBER, BURNT_SIENNA, CRIMSON, MAGENTAJ
* jjj,-
Analiza ndo la salida. puede ver el resultado de cada una de las operaciones.
El siguienre ejemplo utili za Sets.difference( ) para mostrar las diferencias de métodos entre diversas clases Collection y
Map de java.util:
jI: net/mindview/util/ContainerMethodDifferences.java
package net.mindview . util¡
import java.lang.reflect. * ;
import java .util.·;
difference(LinkedHashSet.class, HashSet.class) ¡
difference(TreeSet.class, Set.class} ¡
difference(List.class, Collection.class};
difference(ArrayList.class, List.class) ¡
difference(LinkedList.class, List.class) i
difference(Queue.class, Collection.class);
difference(PriorityQueue . class, Queue.class) ¡
System.out.println("Map: 11 + methodSet(Map.class)};
difference(HashMap.class, Map.class } i
difference(LinkedHashMap.class, HashMap.class) ;
difference (Sorte dMap.class, Map.class);
difference(TreeMap.class, Map.class ) ¡
La salida de este programa fue utili zada en la sección "Resumen" de l Capítulo 11 , Almacenamiento de objetos.
Ejercicio 17: (4) Analice la documentación del JDK correspondiente a EnumSet. Verá que hay definido un método
clone(), clonar. Sin embargo, no podemos efectuar una clonación a partir de la referencia a la interfaz Set
que se pasa en Sets.java. ¿Podría modificar Sets.java para tratar tanto el caso general de una interfaz Set,
tal como se muestra, como el caso especial de un objeto EnumSet, utilizando c1one( ) en lugar de crear
un nuevo objeto HashSet?
class Customer {
private static long counter = 1;
private final long id = counter++¡
private Customer() {}
public String toString () { return "Customer + id; }
/1 A methad to produce Generator objects:
public static Generator<Customer> generator()
return new Generator<Customer>(} (
public Customer next () { return new Customer () ;
};
class Teller {
private static long counter = 1;
private final long id = counter++i
pri vate Teller () {}
public String toString () { return "Teller 11 + id¡ }
JI Un único objeto Generator:
public static Generator<Teller> generator
new Generator<Teller> () {
public Teller next () { return new Teller () ¡
};
/ ' Output :
Teller 3 serves Customer 1
Teller 2 serves Customer 2
Teller 3 serves Customer 3
Telle r 1 serves Customer 4
Teller 1 serves Customer 5
Teller 3 serves Customer 6
Teller 1 serves Customer 7
Tel ler 2 serves Customer 8
Teller 3 serves Customer 9
Teller 3 serves Customer 10
Teller 2 serves Customer 11
Teller 4 serves Customer 12
Teller 2 serves Customer 13
Teller 1 serves Customer 14
Teller 1 serves Customer 15
,///,-
Tanto C ustomer como Teller tienen constructores privados, lo que nos obliga a utili zar objetos Generator. Custorner tiene
un metodo generator( ) que genera un nuevo método Generator<C ustomer> cada vez que lo invocamos. Puede que no
necesitemos múltiples objetos Generator, y TeIler crea un único objeto generator público. Si analiza main( ) verá que
ambas técnicas se uti lizan en los métodos fill( ).
Puesto que tanto el método generator( ) de Customer como el objeto Generator de Teller son estáticos, no pueden for-
mar pal1e de una interfaz, así que no hay fonDa de hacer genérica esta fu nción concreta. A pesar de ello func iona razona-
blemente bien con el método fill( ).
Examinaremos otras versiones de este problema de ve rsiones de colas en el Capítulo 21, Concurrencia.
Ejercicio 18: (3) Siguiendo la fOffila de BankTeller.java, cree un ejemplo donde el pez grande (BigFish) se coma al
chico (LittleFish) en el océano (Ocean).
class Product {
private final int id;
private String description¡
private double price;
public Product{int IDnumber, String descr, double price) (
id = IDnumber¡
description = descr i
this.price = price;
System.out.println(toString()) ;
class CheckoutStand {}
15 Genéricos 415
/ * Output:
25 8, Test, price: $400.99
861 , Test, price: $160 . 99
868 , Test, price: $417.99
207 , Test, pr i ce: $268.99
55 1, Test, price: $114.99
27 8, Test, price: $804 . 99
520 , Test, priee: $554.99
140 , Test, priee: $530 . 99
* /// ,-
Como puede ver en Store.toString(), el resultado son muchos niveles de contenedores que, a pesar de la complejidad, resul-
tan manejables y son seguros en lo que al tratamiento de tipos se refiere. Lo más impresionante es que no resulta demasia-
do complej o, desde el punto de vista intelectual, construir dicho tipo de modelos.
Ejercicio 19: (2) Siguiendo la fonna de Store.java, construya un modelo de un buque de carga donde se utilicen conte-
nedores metálicos para las mercancías.
/ * Output:
t rue
* /1/ ,-
416 Piensa en Java
ArrayList<String> y ArrayList< lnteger> son claramente de tipos distintos. Los diferentes tipos se comportan de fonna
distinta, y si tratamos de almacenar un objeto Integer en un contenedor ArrayList<String>, obtendremos un comporta-
miento diferente (la operación falla) que si tratamos de almacenar ese objero Intcger en un contenedor ArrayList<lnteger>
(la operación sí está permitida). Sin embargo, el programa anterior sugiere que ambos tipos son iguales.
He aquí atTo ejemplo que aumenta todavía más la confusión:
// : generics/Lostlnformation.java
import java.util.*;
class Frob {}
cIass Fnorkle {}
cIass Quark<Q> {}
cIass ParticIe<POSITION,MOMENTUM> {}
} / * Output,
[E]
[K, V]
[O]
[POSITION, MOMENTUMI
* /// ,-
De acuerdo con la documentación del JDK, Class.getTypeParameters( ) "devuelve un a matriz de objetos TypeVariable
que representan las vari ab les de tipo definidas en la declaración genéri ca .. ." Esto parece sugerir que podríamos ser capa-
ces de averiguar cuáles son los tipos de parámetro. Sin embargo, como podemos ver analizando la salida, lo único que pode-
mos averiguar son los contenedores utilizados como va riables para los parámetros, lo cual no constiulye una información
muy interesante.
La cruda realidad es que:
No hay información disponible acerca de los tipos de parámetros genéricos dentro del código genérico.
Por tanto, podemos llegar a detemlinar cosas como el identificador del parámetro de tipo y los límites del tipo genérico, pero
no podemos llegar a detenninar los parámetros de tipo reales utilizados para crear una instancia concreta. Este hecho, que
resulta especialmente frustrante para los que tienen experiencia previa con e++, constituye el problema fundamental al que
hay que enfrentarse cuando se trabaja con genéricos de Java.
Los genéricos de Java se implementan utili zando el mecanismo de borrado. Esto significa que toda la illfomlación especí-
fica de tipos se borra cuando se utili za un genérico. Dentro de un genérico, la única cosa que sabemos es que estam os usan-
do un objeto. Por tanto, List<String> y List<Integer> SOIl , de hecho, el mismo tipo en tiempo de ejecución. Ambas fonnas
se "borran" sustiulyéndolas por su tipo de origen List. Entender este mecanismo de borrado y cómo hay que tratar con él
constituye uno de los principales problemas a la hora de aprender el concepto de genéricos en Java, éste es precisamente el
problema que analizaremos a lo largo de esta sección.
15 Gené ricos 417
class HasF
public :
void f () caut « tlHasF;: f ( ) " « endl; }
);
int main () {
HasF h f ;
Manipulator<HasF> manipulator (hf);
manipul a tor.manipulate{ ) ;
1* Output:
HasF, ,f ()
((( , -
La clase M a nipulator almacena un objeto de tipo T. Lo interesante es el método manipulate( ) que invoca un método f()
so bre obj . ¿Cómo puede saber que el método f( ) existe para el parámetro de tipo T ? El compilado r C++ efectúa la com-
probación cuando instanciamos la plantilla, por lo que el punto de instantac ión de M anipulator<HasF > comprueba que
HasF ti ene un método f( ). Si no fuera así, se obtendría un error en ti empo de compilación, preservándose por tanto la segu-
ridad referente a los tipos.
Escribir este tipo de cód igo en C++ resulta sencillo, porque cuando se instanc ia una plan tilla, e l cód igo de la planti lla cono-
ce el tipo de sus parámetros de plantilla. Los genéri cos de Ja va son di stintos. He aquí la traducción de HasF:
JJ : generics J HasF.java
manipulator.manipulate() ;
)
///,-
Debido al mecanismo de borrado. el compi lador de Java no puede relacionar el requi sito de que manipulate( ) debe Ser
ca paz de invocar f( l so bre obj con el hecho de que HasF' tiene un método f( l. Para poder invocar f( l. debemos ayudar a
la clase genérica, proporcionándola un lí"úte que indique al compilador que sólo debe aceptar los tipos que se confonnen
con dicho límite. Para esto se utiliza la palabra cla ve extends. Una vez que se incluye el límite. sí que se puede realizar la
compi lación:
1/: generics/Manipulator2.java
11: generics/Manipulator3.java
class Manipulator3 {
private HasF obj¡
public Manipulator3 (HasF x) { obj = X¡
public void manipulate () { obj. f () ¡
///,-
Esto nos plantea una cuestión importante: los genéricos sólo son útiles cuando deseamos utili za r parámetros de tipo que sean
más "genéricos" que un tipo específico (y todos sus subtipos); en otras palabras, cuando queramos escribir código que fun·
cione con múltiples clases. Como resultado, los parámetros de tipo y su apl icación dentro de un fragmento útil de código
genérico se rán nonnalmente más complejos que una sim ple sustitución de clases. Sin embargo, no debemos concluir por
ello que cualquier cosa de la fom13 <T extends UasF> no tiene nin gú n sentido. Por ejemp lo, si una clase ti ene un método
que devue lve T. entonces los genéricos son útiles, porque pennitirán devolver el tipo exacto:
11: generics/ReturnGenericType.java
Compatibilidad de la migración
Para eliminar cualquier potencial co nfusión ace rca del mecan ismo de borrado de tipos. es necesario entender claramente que
no se trala de una característica del lenguaje. Se trata de un compro mi so en la implementac ión de los genéricos de Java,
compromiso que es necesario porque los genéricos no han fonnado parte del lenguaje desde e l principio. Este compromiso
puede crearnos algunos quebraderos de cabeza, por lo que es necesario acostumbrarse a él lo antes posible y emender a qué
se debe.
Si los genéricos hubieran fonnado parte de Java 1.0, esta funcionalidad no se habría implementado utili zando el mecanis-
mo de bo rrado de tipos, si no que se habría empl eado el mecanismo de la reificación para retener los parámetros de tipo como
entidades de primera clase, de modo que se ríam os ca paces de realizar, co n los parámetros de tipo, operac iones de refl ex ión
y operaciones del lenguaje basadas en tipos. Veremos posterionnente en el capítulo que el mecanismo de borrado reduce el
aspecto "genérico" de los genéricos. Los genéricos siguen siendo útil es en Java, pero lo que pasa es que no son tan útiles
como podrían ser, y la razón de ello es precisamente el mecanismo de borrado de tipos.
En una implementación basada en dicho mecanismo, los tipos genéricos se tratan como tipos de segu nda clase qu e no pue-
den utilizarse en algunos contextos importantes. Los tipos genéricos sólo es tán presentes durante la co mprobación estática
de tipos, después de lo cua l todo tipo genérico del programa se borra, sustituyéndolo por un tipo límite no genérico. Por
ejemplo, las anotaciones de tipos como List<T> se borran sustituyéndolas por List, y las variables de tipos nonuales se
borran sustituyéndolas por Object a menos que se especifique un límite.
La principal moti vación para el mecanismo de borrado de tipos es que permite utilizar clientes de código genérico con
bibliotecas no genéricas, y viceversa. Esto se denomina a menudo compatibilidad de la migración. En un mundo ideal, exis-
tiria un punto de partida en el que lodo hubiera sido hecho genérico a la vez. En la realidad, incluso aunque los programa-
dores sólo estén escribiendo código genérico, se verán forzados a tratar con bibliotecas no genéricas que hayan sido escritas
antes de la aparición de Java SE5. Los autores de esas bibliotecas puede que no lleguen nunca a tener ningún motivo para
hacer su código más genérico, o puede si mplemente que tarden algún tiempo en ponerse manos a la obra.
Por ello, los genéricos de Java no sólo deben soportar la compatibilidad descendente (el código y los archivos de clase exis-
tentes siguen siendo legales y continúan significando lo que antes significaban) sino que también tienen que soportar la com-
patibilidad de migración, de modo que las bibliotecas puedan llegar a ser genéricas a su propio ritmo y de modo también
que, cuando una biblioteca se reescriba en forma genérica, no haga que dejen de funcionar el código y las aplicaciones que
dependen de ella. Después de decidir que el objeto era éste, los di seliadores de Java y di versos grupos que estaban trabajan-
do en el problema, decidi ero n que el borrado de tipos era la única solución fac tible. El mecanismo de borrado de tipos per-
mite esta migración hacia el código genérico, al conseguir que el código no genérico pueda coexistir con el que sí lo es.
Por ejemplo, suponga que una aplicación utiliza dos bibliotecas, X e Y, y que Y utiliza la biblioteca Z. Con la aparición de
Java SE5, los creadores de esta aplicación y de estas bibliotecas probablemente terminen por efectuar una migración hacia
código genérico. Sin embargo, cada uno de esos diseñadores tendrá diferentes motivaciones y diferent es res tricciones en lo
que respecta a dicha migración. Para conseguir la compatibilidad de migración, cada biblioteca y aplicación tiene que ser
independiente de todas las demás en lo que respecta a la utilización de genéricos. Por tanto, no deben ser capaces de detec-
tar si las otras bibliotecas están utilizando genéricos o no. En consecuencia, la evidencia de que una biblioteca concreta está
usando genéricos debe ser "borrada".
Sin algún tipo de ruta de migración, todas las bibliotecas que hubieran sido di seIiadas a lo largo del tiempo correrian el ries-
go de no poder ser utilizadas por los desarrolladores que decidieran comenzar a utilizar los genéricos de Java. Pero como
las bibliotecas son la parte del lenguaje de programación que mayor impacto tiene sobre la productividad, este coste no
resultaba aceptable. Si el mecanismo de borrado de tipos era la mejor ruta de migración posible o la única existente, es algo
que sólo el tiempo nos dirá.
El cos te del borrado de tipos es signifi cativo. Los tipos genéricos no pueden utili zarse en operaciones que hagan referencia
explícita a tipos de tiempo de ejecución; como ejemplo de estas operaciones podemos citar las proyecciones de tipos. las
operaciones instanceof y las expresiones ne"'. Como toda la infonnación de tipos acerca de los parámetros se pierde, cada
vez que escribamos código genérico debemos estar perpetuamente aco rdándonos de que la idea de que di sponemos de infor~
mación de tipos es solo aparente. Por tanto, cuando escribimos un fragmento de código como éste:
class Foo<T>
T var;
el códi go de la clase Foo debería saber que ahora está trabajando con un objeto Cato La sintaxis sugiere de manera directa
que el tipo T está siendo sustituido a lo largo de toda la clase. Pero en realidad no es así y debemos siempre tener presente,
cuando estemos escribiendo el código para la clase, que se trata simplemente de un objeto de tipo Object.
Además, el borrado de tipos y la compatibilidad de migración significan que el uso de genéricos no se impone en aquellas
ocasiones en que sería bueno que se impusiera:
11: generics/ErasureAndlnherítance.java
class GenericBase<T> {
prívate T element;
public void set (T arg) { arg = element;
publ ic T get () { return element; }
Observe que esta anotación se coloca en el método que genera la advertencia, en lugar de en la clase completa. Es mejor
"enfocar" 10 máx imo posible a la hora de desacti var una advertencia, para no ocultar accidentalmente un problema real al
desactivar las advertencias en un contex to demasiado amplio.
Presumiblemente, el error producido por Derived3 indica que el compilador espera una clase base pura.
Añadamos a esto el esfuerzo adiciona l de gestionar los límites cuando queramos tratar el parámetro de tipo como algo más
que simplemente un objeto de tipo Object y la conclusión de todo ello es que hace falta un esfuerzo mucho mayor con unas
15 Genéricos 421
ventajas mucho menores que cuando se utili za n tipos parametrizados en lenguajes tales como C++, Ada o Eiffel. Esto no
quiere decir que dichos lenguajes ten gan en general más ventajas qu e Java a la hora de abordar la mayoría de los problemas
de programac ión, sino simplemente que sus mecanismos de tipos parametrizados son más fle xibles y potentes qu e los de
Java.
1* Output:
[nu ll, null, null, null, null, null, nul1, null, null]
. /// ,-
Aunque kind se almacena corno Class<T>, el mecanismo de borrado de tipos significa que en realidad se está almacenan-
do simplemente como un objeto Class si n ningún parámetro. Por tanto, cuando hacemos algo con ese objeto, como por
ejemplo crear una matri z, Array.ncwInstance( ) no di spone en la prácti ca de la in[oflnac ión de tipos que está implícita en
kind ; como co nsec uen cia, no puede producir el resu ltado específico, lo que obliga a real izar una proyección de tipo que
genera una advertencia que 110 se puede co rregir.
Observe que la util izació n de Array.newlnstance() es la técnica recomendada para la creación de matrices dentro de gené-
neos.
Si crea mos un contenedor en lugar de una matriz, las cosas son di stintas:
11 :generics/ListMaker.java
import java.util.*¡
El compi lador 110 genera ningun a advert encia, aún cuando sabemos perfec tamente (debido al mecanismo de borrado de
tipos) que la <T> en ncw ArrayList<T>() dentro de create() se elimina: en tiempo de ejecución no hay ninguna <T> den-
tro de la clase, por lo que parece que no tiene ningún significado. Pero si hacemos caso de esta idea y cambiamos la expre-
sión a ne\\' ArrayList( ), e l compilador generará un a advertenc ia.
¿Carece realmente de significado en este caso? ¿Qué pasaría si pusiéramos algunos obj etos en la lista antes de devolverla,
Como en el siguiente ejemplo?:
422 Piensa en Java
11 : generics/FilledListMaker.java
import java.util. *;
1* Output:
[HelIo, HelIo, HelIo, HelIo]
* ///,-
Aunque el compilador es incapaz de tener ninguna información acerca de T en create(), sigue pudiendo garantizar (en tiem-
po de compilación) que lo que pongamos dentro de result es de tipo T , de modo que concuerde con ArrayList<T>. Por
tanto, aún cuando el mecanismo de borrado de tipos elimine la información acerca del tipo real dentro de un método o de
una clase, el compilador sigue pudiendo garanti zar la coherencia interna en lo que respecta a la forma en que se utili za el
tipo dentro del método o de la clase.
Puesto que el mecanismo de borrado de tipos elimina la información de tipos en el cuerpo de un método, lo que importa en
tiempo de ejecución son los limites: los puntos en los que los objetos entran y salen de un método. Estos son los puntos
en los que el compilador realiza las comprobaciones de tipos en tiempo de compilación e inserta código de proyección de
tipos. Considere el siguiente ejemplo no genérico:
11: generics/simpleHolder.java
7: astare 1
8: aload 1
9, ldc #5; I/ String Item
11: invokevirtual #6; JI Método set: {Object;)V
14: aload 1
15: invokevirtual #7; // Método get: () Object¡
18: checkcast #8; j jclass java/ langjString
21: astore_2
22: return
Los métodos sel( ) y get( ) simplemente almacenan y producen el valor, y la precisión de tipos se comprueba en el lugar
donde se produce la llamada a gel( ).
Ahora vamos a incorporar genéricos al código anterior:
JI : generics / GenericHolder.java
La necesidad de efectuar una proyección de tipos para get( ) ha desaparecido, pero también sabemos que el valor pasado a
set() sufre una comprobación de tipos en tiempo de compilación. He aquí el código intermedio relevante:
El código resultante es idéntico. El trabajo adicional de comprobar el tipo entrante en set( ) es nulo, ya que es el compila-
dor quien se encarga de reali zarlo. Y la proyección de tipos para el valor saliente de get() sigue estando ahí, pero ahora no
tenemos que bacerlo nosotros explícitamente; el compilador se encarga de insertar esa proyección automáticamente, de
modo que el código que escribamos (y que tengamos que leer) estará mucho más libre de "ruido".
424 Piensa en Java
Puesto que get() y set() generan el mismo códi go intennedi o, toda la acción referida a los genéricos tiene lugar en los lími-
tes: en concreto, se tra ta de la comprobación adicional en tiempo de compilación para los valores entrantes y de la proyec-
ción de tipos que se inserta para los valores salientes. Para contrarrestar la confusión en lo que respecta al tema del
mecanismo de borrado de tipos, recuerde siempre que "los límites son los luga res en los que la acción se produce".
class Building {}
class House extends Building {}
1* Output :
true
true
false
true
*111 ,-
15 Genéricos 425
El compilador ga ranti za que el marcador de tipos se corresponda con el argumento genéri co.
Ejerc icio 21 : (4) Modifique ClassTypeCapture.java añadiendo un contenedor Ma p<Strin g,C lass<?», un métod o
add Ty pe(Strin g typename, Class<?> kind) y un método createNew(String typename). createNew( )
ge nerará un a nueva instancia de la clase asoc iada con la cadena de caracteres que se le proporcione como
argumento, o producirá un mensaj e de error.
in t main ()
Foo<Bar> tb¡
Foo<int> fi¡ // . . . y funciona con primi t ivas
// / >
La solución en Java co nsiste en pasar un objeto factoría y utili zarlo para crear la nueva instancia. Un objeto factoría muy
adecuado es el propio objetó Class, por lo que si uti lizamos un marcador de tipos, podemos emplear newInstance( ) para
crear un nuevo objeto de dicho tipo:
// : generics / InstantiateGenericType . java
import s t atic net.mindv i ew . util.Print .* ¡
c lass ClassAsFactory<T> {
T x¡
public ClassAsFactory (Class<T> kind l {
try (
x = kind.newInstance () ;
cat ch (Except i on e l {
throw new RuntimeException (e ) i
class Employee (J
public class InstantiateGenericType
publ ic static void main (String[] args )
ClassAs Factory<Employee> fe =
n ew ClassAsFactory<Employee> (Employee . class ) ;
p r int ( "ClassAsFactory<Employee> succeeded" ) ¡
try (
ClassAsFactory<Integer> ti =
n ew ClassAsFactory<Integer> (Integer.class ) ¡
426 Piensa en Java
catch ( Exception e) (
print(UClassAsFactory<Integer> failed" ) ;
J* Output:
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
* /// ,-
Este ejemplo se puede compilar, pero falla si utilizamos ClassAsFactory<lnteger> porque Integer no dispone de ningún
constmctor predetemlinado. Como el error no se detecta en tiempo de compilación, la gente de Sun desaconseja utilizar esta
técnica. Lo que sugieren, en su lugar, es que se utilice una factoría explicita y que se restrinja el tipo de modo que sólo admi-
ta una clase que implemente dicha factoría:
JJ : generics/FactoryConstraint.java
interface FactoryI<T>
T create () ¡
class Foo2<T> {
private T x¡
public <F extends FactoryI<T» Foo2(F factory ) {
x = factory.create() i
}
/ / ...
class Widget {
public static class Factory implements FactoryI<Widget> {
public Widget create ()
return new Widget( ) ¡
abstract T create() i
class X {}
/ * Output:
Ejercicio 22: (6) Utilice un marcador de tipos junto con el mecallismo de reflexión para crear un método que emplee la
versión con argumentos de newlnstance() con el fin de crear un objeto de una clase con un constructor
que tenga argumentos.
Ejercicio 23: (1) Modifique FactoryConstraint.java para que create() adm ita un argumento.
Ejercicio 24: (3) Modifique el Ejercicio 21 para que los objetos factoría se almacenen en el mapa en lugar de en
Class<?>.
Matrices de genéricos
Como hemos visto en Erased.java, no se pueden crear matrices de genéricos. La solución consiste en utili zar un contene-
dor ArrayList en lodos aquellos lugares donde necesitemos crear una matriz de genéricos:
// : generics/ListOfGenerics.java
import java.util.*;
class Generic<T> {}
la misma estrucnlra (tamaño de cada posición de la matriz y disposición de la matri z) independientemente del tipo que alma-
cene, parece que deberíamos poder crear una matriz de objetos Object y proyectarla sobre el tipo de matriz deseado. De
hecho, esta solución podrá compilarse, pero no se podrá ejecutar, ya que se generará una excepción ClassCastException :
JI: generics/ArrayOfGeneric.java
El problema es que las matrices controlan su tipo real y ese tipo se establece en el momento de creación de la matriz. Por
tanto, aunque gia haya sido proyectado sobre Generic<lnteger>[], dicha infonnación sólo existe en tiempo de compilación
(y sin la anotación @SuppressWarning., obtendremos Ulla advertencia debido a dicha proyección). En tiempo de ejecu-
ción, sigue siendo Ulla matriz de tipo Object, yeso hace que se produzcan problemas. La única forma de crear adecuada-
mente una matriz de un tipo genérico es crear una nueva matriz del tipo resultante del borrado de tipos y efectuar un a
proyección de tipos con dicha matriz.
Veamos un ejemplo algo más sofisticado. Considere un envoltorio genérico simple para una matri z:
11: generics/GenericArray.java
Como antes, no podemos decir TII array ~ new TI'zJ, por lo que creamos una matri z de objetos y la proyectamos.
15 Genéricos 429
El método rep() devuelve una matriz TII , que en main( ) debe ser una matriz (ntegerll para gai, pero si llamamos a ese
método y tratamos de capturar el resultado como una referencia a Integerll . obtenemos una excepción
ClassCastException, de nuevo debido a que el tipo real en tiempo de ejecución es Objectll .
Si compilamos GenericArray.java después de desactivar mediante comentarios la anotación @SuppressWarnings, el
compilador genera una advertencia:
Note: GenericArray.java uses unchecked or unsafe opera t ions .
Note: Recompile with -Xlint:unchecked for details.
En este caso, hemos obtenido una única advertencia y parece que se refiere a la operación de proyección de tipos. Pero si
queremos aseguramos, debemos realizar la compilación con -Xlint:unchecked :
Gener i cArray.java : 7 : warning : [unchecked) unchecked cast
found : java . lang . Object(]
required : T[J
array (T [] ) new Object (sz] ;
1 warning
Ciertamente, el compilador se está quejando sobre la proyección de tipos. Puesto que las advertencias introducen ruido den-
tro de l proceso de diseño, lo mejor que podemos hacer, una vez que verifiquemos que se produce una advertencia, es de-
sactivarla utilizando @S uppressWarnings. De esa forma, cuando aparezca algu na otra advertencia podremos investigarla
adecuadamente.
Debido al mecanismo de borrado de tipos, el tipo en tiempo de ejecución de la matriz sólo puede ser Objectll . Si lo pro-
yectamos inmediatamente sobre TI J, entonces el tipo real de la matriz se perderá en tiempo de compilación y el compilador
podría no ap licar algunas compro baciones de potenciales errores. Debido a esto, es mejor utilizar una matriz Object ll den-
tro de la colección y añadir una proyección a T cuando la usemos como elemento de la matriz. Veamos cómo se apl icaría
esta solución con el ejemplo GenericArray.java:
11 : generics/GenericArray2.java
@SuppressWarnings ( "unchecked")
pUblic T get(int index) { return (T)array[index] i }
@SuppressWarnings ("unchecked 11)
public T [1 rep [) (
return (T[])array; II Advertencia: proyección no comprobada
1* Output : (Sample )
0 12 3 4 5 678 9
430 Piensa en Java
El testigo de tipos Class<T> se pasa al constructor para compensar el mecanismo de borrado de tipos, con el fin de poder
crear el tipo real de matriz que necesitemos, aunque el mensaje de advertencia referido a la proyección de tipos deberá ser
suprimido mediante @S uppressWarnings. Una vez que obtengamos el tipo real, podemos devolverlo y obtener los resul-
tados deseados como podemos ver en main(). El tipo de la matriz en tiempo de ejecución es el tipo exacto TII.
Lamentablemente, si examinamos el código fuente en las bibliotecas estándar de Java SES, podremos ver que existen por
todas partes proyecciones de matrices de tipo Object a tipos parametrizados. Por ejemplo, he aquí el constructor Arra)'List
que utiliza como argumento un contenedor Collection, después de simplificar y limpiar el código un poco:
public ArrayList (Collection e) {
size = c.size () ¡
elementData = (E[] )new Object(size];
c .toArray(elementData) ¡
Si examina el código de ArrayLisLjava, podrá encontrar multitud de estas proyecciones. ¿Y qué es lo que sucede cuando
compilamos este código?
Note: ArrayList.java uses unchecked or unsafe operations .
Note: Recompile with -Xlint:unchecked for details.
15 Genéricos 431
Como puede ver, las bibliotecas estándar generan una multitud de advertencias. Si el lecto r ha trabajado antes con e, yespe-
cialmente con el e anterior al estándar ANSI, recordará un efecto muy concreto de las advertencias: en el momento en que
descubrimos que las podemos ignorar, las ignoramos completamente. Por esa razón, lo mejor es que intentemos que el com-
pilador no genere ningún tipo de mensaje, a menos que el programador deba hacer algo con ese mensaje.
En su bitácora web,3 Neal Gafter (uno de los principales desarrolladores de Java SES) apunta que le daba bastante pereza
reescribir las bibliotecas Java, y que los buenos programadores no deben imitar lo que él hizo. Neal también señala que le
hubiera resultado imposible corregir parte del código de la biblioteca Java sin modificar la interfaz existente. Por tanto, aun-
que en los archivos de código fuente de la biblioteca Java aparezcan ciertas técnicas, eso no quiere decir que esa sea la fonna
correcta de bacer las cosas. Cuando examine el código de biblioteca no de por sentado que se trate de un ejemplo que haya
de seguir en su propio código.
Límites
Ya hemos presentado brevemente los límites anterionnente en el capítulo. Los límites nos pernliten imponer restricciones a
los tipos de parámetros que pueden utilizarse con los genéricos. Aunque esto nos pennite imponer reglas acerca de los tipos
a los que pueden aplicarse los genéricos, un efecto quizá más importante es que podemos invocar métodos pertenecientes a
los tipos definidos como límite.
Puesto que el mecanismo de borrado de tipos elimina la infonnación de tipos, los únicos métodos que podemos in vocar para
un parámetro genérico al que no se le hayan impuesto límites son aquellos disponibles para Object. Sin embargo, si somos
capaces de restringir el parámetro para que se corresponda con un subconj unto de tipos, entonces podemos invocar todos
los métodos de dicho subconjunto. Para implementar esta restricción, el mecanismo de genéricos en Java utili za la palabra
clave extends. Es importante comprender que extends tiene un significado bastante distinto al normal dentro del contexto
de los límites de genéricos. El siguiente ejemplo ilustra los fundamentos básicos de los mecanismos de límites:
11 : generics/BasicBounds.java
JI Múltiples límites:
class ColoredDimension<T extends Dimension & HasColor>
T item;
ColoredDimension(T item) { this.item = item; }
T getltem () { return i tem; }
java.awt.Color color() { return item.getColor();
int getX() { return item.x; }
int gety() { return item.y; }
int getZ () { return i tem. Z i }
3 hllp://gafter.hlogspol.com/!004/09/pll==/ing-lhrough-erasure-answer.hlml
432 Piensa en Java
cIass Bounded
extends Dimension implements HasColor, Weight (
public java. awt. Color getColor () { return null i
public int weight () { return O; }
class Holdltem<T> {
T item;
Holdltem(T item) ( this.item item; }
T getltem() ( return item; )
interface SuperPower {}
interface XRayVision extends SuperPower {
void seeTh r oughWalls{);
Observe que los comodines (de los que hab laremos a continuación) están limitados a un único límite.
Ejercicio 25 : (2) Cree dos interfaces y una clase que implemente ambas. Cree dos métodos genéricos, uno cuyo argu-
mento de parámetro esté limitado por la primera interfaz y otro cuyo argumento de parámetro esté limita-
do por la segunda interfaz. Cree una instancia de la clase que implementa ambas interfaces y demuestre
que se puede utilizar con ambos métodos genéricos.
Comodines
Ya hemos visto algunos usos simples de los comodines (símbolos de interrogación dentro de las expresiones de argumentos
genéricos) en el Capítulo 11 , Almacenamiento de objetos y en el Capítulo 14. Información de tipos. En esta sección vamos
a explorar esta cuestión con más detalle.
Empezaremos con un ejemplo que demuestra un comportamiento concreto de las matrices. Podemos asignar una matri z de
un tipo derivado a una referencia de matri z del tipo base:
11 : generics/CovariantArrays . java
class Frui t {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
} 1* Output,
java.lang.ArraySto reException: Frui t
j ava.lang .ArrayStoreExceptio n: Orange
* 111 ,-
La primera línea de main() crea una matriz de objetos Apple y la asigna una referencia a una matri z de objetos Fruit. Esto
ti ene bastante sentido, ya que Apple es un tipo de Fruit, por lo que una matriz de Apple tiene que ser una matriz de Fruit.
Sin embargo, si el tipo real de la matriz es Apple(J, sólo deberíamos poder insertar en la matriz un objeto Apple o un sub-
tipo de Ap ple. lo que de hecho funciona tanto en tiempo de compilación como en tiempo de ejecución. Pero observe que el
compilador nos pemlite insertar un objeto Fruit dentro de la matri z. Esto tiene sentido para el compilador, porque dispone
de una referenc ia a Fruitll ; como dispone de esa referencia, ¿por qué no debería pennitir colocar en la matriz un objeto
Fruit. o cualquier cosa que descienda de Fruit, como por ejemplo Orange? Por tanto. la operación se permite en tiempo
de compi lación . Sin embargo, el mecanismo de tiempo de ejecuci ón para las matrices sabe que está tratando con una matriz
Applell y genera una excepción cuando se inserta un tipo incorrecto de la matriz.
El tém1ino "generalización" resulta confuso dentro de este contexto. Lo que estamos realmente haciendo es asignar una
matriz a otra. El comportamiento de las matrices es tal que pennite almacenar otros objetos, pero como podemos reali zar
una ge neralización, resulta claro que los objetos matri z pueden preservar las reglas acerca del tipo de objetos que contienen.
Es como si las matri ces fueran conscientes de qué es lo que están almacenando, por lo que entre las comprobaciones reali-
zadas en tiempo de compilación y las rea lizadas en tiempo de ejecución, no podemos tratar de abusar del mecanismo de
matrices.
Esta fonna de comportarse de las matrices no resulta tan terrible, porque al final sí que detectamos en tie mpo de ejecución
que hemos insertado un tipo incorrecto. Pero un o de los objeLivos principales de los genéricos era precisamente mover esos
mecanismos de detección de errores a tiempo de compilación. Por tanto, ¿qu é sucede si tratamos de utiliza r contenedores
genéricos en lugar de matrices?
11 : generics / NonCovariantGenerics.java
II {CompileTimeError} (Won t compile )
I
i mport java.util.*¡
Examinando la documentación de ArrayList, podemos comprobar que el compilador no es tan inte ligente. Mientras que
add( ) lOma un argumento del tipo de parámetro genérico, contains( ) e indexOf( ) toman argumentos de tipo Object. Por
tanto. cuando especificamos un contenedor ArrayList<? extcnds Fruit>. el argumento de add() se convierte en '? extends
Frui!' . A partir de dicha descripción, el compilador no puede saber qué tipo especifico de Fruit se requiere, por lo que no
aceptará ningún tipo de Fruit. No importa si primero generalizamos el objeto App le a Fruit: el compilador se negará a invo-
car un método (como add(» si hay un comodín en la lista de argumentos.
Con contains( ) e indexOf( ), los argumentos son de tipo Object, por lo que no hay ningún comodín implicado y el com-
pilador sí que pemlite la llamada. Esto significa que es responsabilidad del diseñador de clases genéricas definir qué clases
son "seguras" y utilizar el tipo Object para sus argumentos. Para prohibir una llamada cuando el tipo se utilice con como-
dines, utilice el parámerro de tipo dentro de la lista de argumentos.
Podemos ilustrar esto con una clase Holder muy simple:
11 : generics/Holder.java
/* Output: (Sample)
java.lang.ClassCastExCeption: Apple cannot be cast to Orange
true
*111,-
Holder tiene un método set() que toma un argumento T, un método get() que devuelve T, y un mélOdo equals() que toma
un argumento de tipo Object. Como ya hemos visto, si creamos un contenedor Holder<Apple>, no podemos generalizar-
lo a Holder<Fruit>, pero sí que podemos generalizarlo a Holder<? extends Frui!>. Si invocamos get(), sólo devuelve un
objeto Fruit, que es lo único que el compilador sabe, teniendo en cuenta que el límite que hemos hecho es "cualquier cosa
que extienda Fruif'. Si el programador tiene más infonnación acerca de los objetos implicados, puede efectuar una predic-
ción sobre un tipo específico de Fruit y no se generará ninguna advertencia, aunque correremos el riesgo de que se genere
una excepción ClassCastException. El método set( ) no funcionará ni con un objeto Apple ni con un objeto Fruit, porque
el argumento de set( ) es también "? Extends Fruit", lo que significa que puede ser cualquier cosa y el compi lador no puede
verificar que "cualquier cosa" sea segura en lo que a tipos respecta.
Sin embargo, el método equals( ) funciona correctamente, porque toma un objeto Object en lugar de T como argumemo.
Por tanto, el compilador sólo presta atención a los tipos de objetos que se pasan a los métodos y que se devuelven desde
éstos. El compilador no analiza el código para ver si efectuamos ninguna escritura o lectura real.
438 Piensa en Java
Contravarianza
También es posible proceder en sentido inverso y uti lizar comodines de superripo. Con esto, lo que expresamos es que el
comodín está limitado por cualqu ier clase base de una clase concreta, espec ifi cando <? super MyClass> o incluso utilizan-
do un parámetro de tipo: <? super T> (aunque no se puede dar un límite de supertipo a un parámetro genérico, es decir, no
podemos escribir <T super MyClass». Esto nos pennite pasar con seguridad un objeto de un cierto a un tipo genérico. De
este modo , con los comodines de supertipo podemos escribir dentro de un contenedor de tipo Collection:
11 :generics/SuperTypeWildcards.java
import java.util.*¡
static void f2 () (
writeWithWildcard(apples, new Apple (») i
writeWithWildcard(fruit, new Apple {» ;
podemos ver esto en f2( ), donde sigue siendo posible insertar un objeto Apple en una lista List<Apple>, como antes, pero
ahora resulta posible insertar un objeto Apple en una lista List<Fruit>, tal como cabría esperar.
pod ríamos reali zar este mismo tipo de análisis como revisión del tema de la covari anza y de los comodines:
jI : generics/GenericReading . java
import java . util. * ;
static void f3 () {
CovariantReadercFruit> fruitRead e r =
new CovariantReadercFruit>() i
Fruit f fruitReader.readCovariant(fruit);
Fruit a = fruitReader . readCovariant(apples) i
Como antes, el primer método readExact( ) utili za el tipo concreto. Por tanto, si utilizamos el tipo concreto sin ningún
comodín, podemos escribir y leer de dicho tipo concreto en una lista. Además, para el valor de retomo, el método genérico
estático readExact( ) "se adapta" efectivamente a cada llamada a método y devuelve un objeto Apple de una lista
List<Apple> y un objeto Fruit de una lista List<Fruit>, como puede verse en fJ() . Por tanto, si podemos resolver el pro-
blema con un método genéri co estático, no necesitamos recurrir a la covarianza si únicamente nos estamos limitando a leer.
Sin embargo, si tenemos una clase genérica, el parámetro se establece para la clase en el momento de crear una instancia de
dicha clase. Como podemos ver en f2(), la instancia fruitReader puede leer un objeto Fruit de una lista List<Fruit>, pues-
to que ese es su tipo concreto. Pero una lista List<Apple> también debería producir objetos Fruit y el objeto fruitReader
no permite esto.
Para corregir el pro blema, el método CovariantReader.readCovariant( ) lOma una lista List<? extends T >, de modo
que resulta seguro lee r un objeto T de di cha lista (sabemos que todo lo que hay en esa lista es cuando menos de tipo T,
440 Piensa en Java
y posiblemente algo derivado de T). En f3( ) podemos ver que ahora si es posible leer un objeto Fruit de una lista
List<Apple>.
Ejercicio 28: (4) Cree una clase genérica Genericl<T> con un único método que tome un argumento de tipo T. Cree
una segunda clase genérica Generic2<T> con un único método que devuelva un argumento de tipo T.
Escriba un método genérico con un argumento contra variante de la primera clase genérica que invoque al
método de dicha clase. Escriba un segundo método con un argumento covanante de la segunda clase gené-
rica que invoque al método de dicha clase. Pruebe el diseño utilizando la biblioteca typeinfo.pets.
Comodines no limitados
El comodín no limitado <?> parece que quiere significar "cualquier cosa", por lo que utilizar un comodin no limitado pare-
ce eq ui valente a emplear un tipo normal. Ciertamente, el compilador parece aceptar, a primera vista, esta interpretación:
11 :generics/unboundedWildcardsl.java
import java.util.*¡
Hay muchos casos como los de este ejemplo en que al compi lador no le importa si utilizamos un tipo normal o <?>. En
dichos casos, <?> parece completamente superfluo. Sin embargo, sigue teniendo sentido utilizar este comodín, porque lo
que dicho comodín dice en la práctica es "he escrito este código teniendo presente los genéricos de Java y lo que estoy uti-
lizando aquí no es un tipo 110n11al, aunque en este caso el parámetro genérico puede referirse a cualquier tipo".
15 Genéricos 441
Un segundo ejemplo muestra un uso importante de los comodines no limitados. Cuando estemos tratando con múltiples
parámetros genéricos, a menudo es importante pennitir que un parámetro sea de cualquier tipo al mismo tiempo que se asig-
na un tipo concreto al otro parámetro:
1/ : generics/UnboundedWildcards2 . java
i mport java.util.*;
Pero de nuevo, cuando lo que tenemos son todos comodines no limitados, como por ejemplo en Map<?,?>, el compilador
parece no efectuar distinciones con respecto a un mapa nonnal y corriente. Además, UnboundedWildcardsl.java muestra
que el compilador trata List<?> y List<? extends Object> de manera diferente.
Lo que resulta más confuso es qu e el compilador no siempre diferencia entre, por ejemplo, List y List<?>, por lo que am bas
expresiones podrían parecer equiva lentes. De hecho, puesto que los argumentos genéricos se ven sometidos al mecanismo
de borrado de tipos, List<?> podría parecer equivalente a List<Object>, y List es de hecho equiva lente a List<Object>.
Sin emba rgo, estas afinnaciones no son completamente ciertas. List quiere decir en la práctica "una lista simple que alma-
cena cualquier tipo de objeto Object", mientras que List<?> significa " una lista no simple de algún lipo específico, aunque
no sabemos qué tipo es ese".
¿En qué casos se preocupa el compilarlor de las diferen cias entre los tipos nonnales y los tipos que incluyen comodines no
limitados? El sigui ente ejemplo utiliza la clase Holder<T> que hemos de finido anterionnente. Contiene métodos que toman
Holder como argumentos, pero de distintas maneras: como tipo nonna!, con un parámetro de un tipo específico y co n un
parámetro con un comodín no limirado:
11 : generics / wildcards.java
/1 Exploració n del significado de l os c omodines.
static <T>
T wildSubtype{Holder<? extends T> holder, T arg) {
1/ holder.set(arg); II Error,
II set(capture of ? extends T) en
II Holder<capture of ? extends T>
1I no se puede aplicar a (T)
T t = holder.get();
return t i
static <T>
void wildSupertype(Holder<? super T> holder, T arg) {
holder.set(arg) ;
liT t = holder. get (); II Error,
I1 Tipos incompatibles: encontrado Object, requerido T
rawArgs{raw, lng};
rawArgs{qualified, lng);
rawArgs{unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg{qualified, lng);
unboundedArg{unbounded, lng);
unboundedArg(bounded, ln9);
En wildSubtype( ), las restricciones en el tipo de Holder se relajan para incluir un contenedor Holder de cualquier COSa
que extienda T. De nuevo, esto significa que T podría ser Fruit, mientras que holder podría ser perfectamente el contene-
dor Holder<Apple>. Para impedir que se inse rte un objeto Orange en un contenedor Holder<Apple>, la llamada a set( )
(o cualqui er método que tome un argumento referido al parámetro de tipo) no está permitida. Sin embargo, seguimos sabien-
do que cualquier cosa que extraigamos de un contenedor Holder<? extends Fruit> será al menos de tipo Fr uit, por lo que
sí está pem1itido invocar get() (o cualquier método que genere un valor de retomo que se corresponda con el parámetro de
tipo).
Los comodines de supertipo se mu estran en wildSupertype(), que tiene el comportam iento opuesto a wildSubtype( ): hol-
der puede ser un co ntenedor que almacene cualquier tipo que sea una clase base de T. Por tanto, set( ) puede aceptar un
objeto T , puesto qu e cualquier cosa que fu ncione con un tipo base también funcionará, polimórficamente. con un tipo deri-
vado (y por tanto con T). Sin embargo. no resu lta útil tratar de invocar get( ). porque el tipo almacenado por holder puede
ser cualqu ier supertipo, de modo que el único tipo seguro es Object.
Este ejem plo también muestra las limitaciones relativas a lo que se puede y no se puede hacer respecto a un parámetro no
limitado. El metodo que ilustra esta situación es unbounded() : no se puede invocar get() o set( ) con T porque no dispo-
nemos de ningún T.
En main( ), podemos ver cuáles de estos métodos pueden aceptar cada uno de estos métodos sin errores ni advertencias.
Para garanti zar la compati bilidad de mi gración, rawArgs() adm ite todas las diferentes variac iones de Holder sin generar
advertencias. El método unbound edArg() también acepta todos los tipos, aunque, como hemos indicado anteriormente, los
ges tiona de manera distinta dentro del cue rpo del método.
Si pasamos una referen cia Holder normal a un método que tome un tipo genérico "exacto" (comodi nes) obtendremos una
advertencia, porque el argumento exacto está esperando infonnación que no existe en el tipo nonnaJ. Y si pasamos una refe-
rencia no limitada a exactt ( ), no existe la sufi ciente información de tipos como para establecer el tipo de retorno.
Pode mos ver que exact2( ) tiene el mayor número de restricciones, ya que desea di sponer exactamente de un Holder<T>
y de un argumento de tipo T , y debido a ello genera errores o advertencias a menos que le ent reguem os los argumentos exac-
tos. En ocasiones, esto es perfectamente adm isible, pero se trata de una restricción excesiva; en ese caso, podemos utilizar
comodines, dependiendo de si queremos obtener va lores de retorno de un tipo detenninado a partir de nuestro argumento
genérico (como puede verse en wildSubtype() o de si queremos pasar argumentos con un tipo detenminado a nuestro argu-
mento genérico (como puede verse en wildSupertype( ».
Por tanto, la ventaja de uti lizar tipos exactos en lugar de tipos con comodín es que podemos hacer más cosas con los pará-
metros genéri cos. Sin embargo, utilizar comodines nos permite aceptar como argumentos un rango más amp lio de tipos
parametrizados. El programador deberá decidir cuál es el compromjso más adecuado examinando caso por caso.
Conversión de captura
Hay una situación concreta que exige el uso de <?> en lugar de un tipo normal. Si se pasa un tipo normal a un método que
utilice <?>, al co mpilador le resulta posible inferir el parámetro de tipo real, de modo que el método puede a su vez invo-
car otro método que uti lice el tipo exacto. El siguiente ejemplo muestra esta técnica que se denomina conversión de caplll-
ra porque el tipo no especificado con comodín se captura y se convierte en un tipo exacto. Aquí, los comentarios acerca de
las advertencias sólo se ap lica n si se elimina la anotación @S uppressWarnings :
// : generics /CaptureConversion.java
@SuppressWarnings ( tlunchecked ti )
public static void main(String[] args )
Holder raw = new Holder <Integer > (1) i
15 Genéricos 445
/ * Output:
Int eger
Obj ect
Double
, /// ,-
Los parámetros de tipo f1 () son todos exactos, sin comodines ni límites. En f2 (), el parámetro Holder es un comodin no
limitado, por lo que podría parecer que es desconoc ido en la práctica. Sin embargo, dentro de n ( ) , se invoca f1 () Y f1 ()
requiere un parámetro conocido. Lo que está sucediendo es que el tipo de parámetro se captllra en el proceso de invocación
de n(), de modo que puede utilizarse en la llamada a f1 ().
El lector podría preguntarse si esta técnica podría util izarse para escribi r, pero eso requeriría que pasáramos un tipo especí-
fico junto con Holder <?>. La conversión de captura sólo funciona en aque llas situac iones donde necesitamos, dentro del
método, trabajar con e l tipo exacto. Observe que no se puede devolver T desde n ( ), porque T es desco nocido para n ( ).
La conversión de captura res ulta interesante, pero bastante limitada.
Ejercicio 29: (5) Cree un método genérico que tome como argumento un contenedor Holder< List<?». Detennine qué
métodos puede o no invocar para el contenedor Holder y para la li sta List. Repita el ejercicio para un argu-
mento de tipo List<Holder <?».
Problemas
Esta sección analiza di versos tipos de problemas que surgen cuando se intentan utilizar los genéricos de Ja va .
1* Output:
O 1 2 3 4
' /// ,-
446 Piensa en Java
Observe que el mecanismo de conversión automática admite incluso la utili zación de la sintaxi sforeach para generar valo-
res int.
En general, esta solución funciona adecuadamente, ya que podemos almacenar y extraer apropiadamente valores primitivos
enteros. Se producen ciertas conversiones, pero éstas se llevan a cabo de fonna transparente. Sin embargo, si las cuestiones
de rendimiento son un problema, podemos uti lizar una versión especializada de los contenedores adaptada para tipos pri-
mitivos; un ejemplo de versión de código fuente abierto para este tipo de contenedores especializados es org.apache.
cornmons.collections.primitives.
He aquí otro enfoque, que crea un conjunto de bytes:
J/: generics/ByteSet.java
import java.util.*;
/1: generics/PrimitiveGenericTest.java
import net.mindview.util.*;
1* Output:
YNzbrnyGcF
OWZnTcQrGs
15 Genéricos 447
eGZMmJMROE
suEcUOneOE
dLsmwHLGEa
hKcx rEqUCB
bklnaMesht
7052
6665
2654
3909
520 2
2209
5458
* /// ,-
Pueslo que RandomGenerator.lnteger implementa Generator<Jnteger>, cabria esperar que el mecan ismo de conversión
automática se encargara de convertir el valor de ncxt( ) de Intcgcr a int. Sin embargo, el mecanismo de conversión auto-
mática no se ap lica a las matrices, así que esta solución no funciona .
Ejercicio 30 : (2) Cree un contenedor Holder para cada uno de los tipos envoltorio de las primitivas y demuestre que el
mecanismo de conversión automática funciona para los métodos sct() y get() de cada instancia.
interface Payable<T~ {}
class FixedSizeStack<T~ {
private int index : o;
private Object[] storage;
public FixedSizeStack(int size)
storage : new Object[size];
448 Piensa en Java
1* Output:
J 1 H G F E D e B A
* /// ,-
Sin la anotación @SuppressWarnings, el compilador generaría una advertencia de "proyección de tipos no comprobada"
para pop(). Debido al mecanismo de borrado de tipos, no puede saber si la proyección de tipos es segura, y el método pop()
no realiza en la práctica ninguna proyección. T se borra para sustituirla por su primer límite que es Object de manera pre·
detenninada, por lo que pop() está, de hecho, proyectando un objeto de tipo Objecl sobre Objecl.
Hay veces en que los genéri cos no elim inan la necesidad de efectuar la proyección, y esto genera una advertencia por parte
del compilador, que es inapropiada. Por ejemplo:
11: generics/NeedCasting.java
import java.io.*;
import java.util. *;
Como veremos en el siguiente capitulo, readObjecl( ) no puede saber lo que está leyendo, por lo que devuelve un objeto
que habrá que proyectar. Pero cuando desactivamos mediante comentarios la anotación @SuppressWarnings y compila·
mos el programa, se obtiene una advertencia:
Note: NeedCasting.java uses unchecked or unsafe operations.
Note: Recompile with - Xlint:unchecked for details.
Estamos obligados a realizar la proyección y a pesar de ello se nos dice que no podemos hacerlo. Para resolver el proble-
ma, debemos utilizar una nueva forma de proyección de tipos introducida en Java SES, que es la proyección de tipos a tra·
vés de una clase genérica:
11 : generics/classCasting.java
import java.io.*;
import java.util.*;
15 Genéricos 449
Sobrecarga
El siguiente ejemplo no se podrá compilar, aunque parece que se trata de algo bastante razonable:
jj : genericsjUseList.java
ji {compileTimeError} (no se compilará)
import java.util.*¡
i mplements Comparable<ComparablePet~
publi c int compareTo (ComparablePet arg ) { return Oi }
} /1/ , -
Resulta razonable tratar de enfocar mejor el tipo con el que pueda compararse una subclase de ComparablePet. Por ejem-
plo, un objeto Cat sólo debería ser Comparable con otros objetos Cat:
11 : generics / Hijackedlnterface.java
II {Comp i leTimeErro r } (no se compilará )
/1 O simplemente :
Tipos autolimitados
Existe una sintaxis que provoca bastante confusión y que aparece con bastante asiduidad en el caso de los genéricos de Java.
He aquÍ el aspecto:
c lass SelfBounded <T extends SelfBo unded <T» { II ...
Esto es algo comparable a tener dos espejos enfrentados, lo que genera una especia de reflexión infinita. La clase
Selffiounded toma un argumento genéri co T, por su parte, T está restringido por lIn límite, y dicho límite es Selffiounded,
con T como argumento.
Este tipo de sintaxis resulta dificil de entender cuando se ve por primera vez, y nos pennite enfa tizar el hecho de que la pala-
bra clave extends, cuando se utiliza con los límites de los genéricos, es completamente distinta que cuando se la usa para
crear subclases.
Se trata de un tipo genéri co normal con métodos que aceptan y generan objetos del tipo especificado en el parámetro, junto
con un método que opera sobre el campo almacenado (aunque úni camente reali za operaciones de tipo Object con ese
campo).
Podemos utili zar BasicHolder en un genético curiosamente recurrente:
// : generics/CRGWithBasicHolder.java
/ * Output:
Subtype
,///,-
Observe en este ejemplo algo muy importante: la nueva clase Subtype toma argumentos y va lores de retomo de tipo
Subtype, no simplemente de la clase base BasicHolder. Ésta es la esencia de los genéricos curiosamente recurrentes: la
clase derivada sustituye a la clase base en sus parámetros. Esto significa que la clase base genérica se convierte en una cier-
ta clase de plantilla para describir la funcionalidad común de todas sus clases deri vadas, pero esta funcionalidad utili zará el
tipo derivado en todos los argumentos y tipos de retomo. En otras palabras, se utilizará el tipo exacto en lugar del tipo base
en la clase resultante. Por tanto, en Subtype, tanto el argumento de sel() como el tipo de retomo de gel( ) son exactamen-
te Subl)'pe.
Autolimitación
El contenedor BasicHolder puede utilizar cualquier tipo como su parámetro genérico, como puede verse aquí :
//: generics/Unconstrained.java
class Other ()
452 Piensa en Java
1* Output :
Other
* jjj,-
La autolimitación realiza el paso adicional de obligar a que el genenco se utilice como su propio argumento límite.
Examinemos cómo puede utilizarse y cómo no puede utili zarse la clase resultante:
11: generics/SelfBounding.java
class O {}
II No se puede hacer esto:
II class E extends SelfBounded<D> {}
II Error de compilación: el parámetro de tipo D no está dentro de su límite
II Sin embargo, podemos hacer esto, así que no se puede forzar la sintaxis :
class F extends SelfBounded {}
Lo que la autolimitación hace es requerir el uso de la clase en una relación de herencia como ésta:
class A extends SelfBounded<A> {}
Esto nos fuerza a pasar la clase que estemos definiendo como parámetro a la clase base.
¿Cuál es el valor añadido que obtenemos al autolimitar el parámetro? El parámetro de tipo debe ser el mismo que la clase
que se esté definiendo. Corno podemos ver en la definición de la clase B, también podemos heredar de una clase
15 Genéricos 453
SelfBounded que utilice un parámetro SelfBounded, aunque el uso predominante parece ser e l que podernos ver en la clase
A. El intento de definir E demuestra que no se puede utilizar un parámetro de tipo que no sea SelfBounded.
Lamentablemente, F se compila sin ninguna advertencia, por lo que la sintaxis de autolimitación no se puede imponer. Si
fuera realmen te importante, se necesitaría una herramienta externa para garanti zar que los tipos nomla les no se utilizan en
lugar de los tipos parametrizados.
Observe que se puede eliminar la restricc ión y todas las c lases se seguirán compilando, pero entonces E también se compi-
laría:
JI : generics/NotSelfBounded . java
elass D2 ()
// Ahora esto es OK:
class E2 extends NotSelfBounded<D2> {} ///:-
Así que, obviamente, la restricción de autolimitación sólo sirve para imponer la relación de herencia. Si se utiliza la autoli-
mitación, sabemos que el parámetro de tipo utilizado por la clase será e l mismo tipo básico que la clase que esté utilizando
dicho parámetro. Esto obliga a cualquiera que utilice dicha clase a ajustarse a ese fornlato.
También resulta posible utilizar la autolimitación en los métodos genéricos:
// : generics/SelfBoundingMethods.java
Covarianza de argumentos
El valor de los tipos auto limitados es que producen tipos de argumentos ca variantes, es decir, tipos de argumentos de los
métodos que varían con el fin de ajustarse a las subclases.
Aunque los tipos autolimitados también producen tipos de retomo que coinciden con el tipo de la subclase, esto no es tan
importante, porque Java SES ya introduce la funcionaljdad de lipos de retorno covariantes:
// : generics/CovariantReturnTypes.java
class Base {}
454 Piensa en Java
interface OrdinaryGetter
Base get ();
El método get() en DerivedGetter sustituye a get() en OrdinaryGetter y devuelve un tipo que se deriva del tipo devuel·
to por OrdinaryGetter.get() . Aunque se trata de algo perfectamente lógico (un método de un tipo derivado debería poder
devolver el tipo más específico que el método del tipo base que esté sustituyendo). no podía hacerse en las versiones ante-
riores de Java.
Un genérico autolimitado produce, de hecho, el tipo exacto derivado como va lor de retomo, como podemos ver aquí con el
método get( ):
jj: genericsjGenericsAndReturnTypes.java
Observe que este código no podria haberse compilado de no haberse incluido los tipos de retomo covariantes en Java SE5.
Sin embargo, en el código no genérico, los tipos de argumento no pueden variar con los subtipos:
jj: genericsjOrdinaryArguments.java
class OrdinarySetter {
void set (Base base) {
System . out . println ("OrdinarySetter. set (Base) ") ¡
/ * Output:
DerivedSetter.set(Derived)
Ordina rySetter.set {Base )
'111,-
Tanto set(derived) como set(base) son legales, por lo que DerivedSetter.set() no está sustitu yendo OrdinarySetter.set(),
sino que está sobrecargando dicho método. Analizando la salida, puede ver que hay dos métodos en DerivedSetter, por lo
que la versión de la clase base sigue estando disponible, lo que confim1a que se ha producido una sobrecarga del método.
Sin embargo, con los tipos autolimitados, sólo hay un único método en la clase derivada y dicho método loma el tipo deri-
vado como argumento, no el tipo base:
1/ : generics/SelfBoundingAndCovariantArguments .java
El compilador no reconoce el intento de pasar el tipo de base como argumento a set(), porque no existe ningún método co n
dicha signatura. El argumento ha sido, en la práctica, sustituido.
Sin el mecanismo de autolimitación, entra en acción el mecanismo nonnal de herencia y lo que obtenemos es una sobrecar-
ga, al igual que sucede en el caso no genérico:
//: generics/PlainGenericlnheritance.java
/ * Output :
456 Piensa en Java
DerivedGS.set {Derived l
GenericSetter.set (Base )
' 111 ,-
Este código se asemeja a OrdinaryArguments.java ; en dicho ejemplo, DerivedSetter hereda de OrdinarySetter que con-
tiene un conjunto set(Base). Aquí, DerivcdGS hereda de GenericSetter<Base> que también contiene un conjunto
set(Base), creado por el genérico. Y al igual que en OrdinaryA rguments.java, podemos ver analizando la sali da que
DerivedGS contiene dos versiones sobrecargadas de set(). Sin el mecanismo de la autolimitación, lo que se hace es sobre-
cargar los tipos de argumentos. Si se utiliza la 3ulolimitación, se tennina disponiendo de una única versión de un método,
que admite el tipo exaclO de argumento.
Ejercicio 34: (4) Cree un tipo genérico autolimitado que contenga un método abstracto que admita un argumento con el
tipo de l parámetro genérico y genere un va lor de retomo con el tipo del parámetro genérico. En un méto-
do no abstracto de la clase, invoque dicho método abstracto y devuelva su resultado. Defina otra clase que
herede de l tipo autolimitado y compruebe el funcionamiento de la clase resultante.
/ * Outpu t:
java.lan g . ClassCa st Exception: Attempt to in s e rt class t ypein f o . pets . Cat eleme nt i nta
co l l e ction with e l e ment type class typ e info .pe ts . Dog
*/// , -
Cuando ejecutamos el programa, vemos que la inserción de un objeto Cat no provoca ninguna queja por parte dogsl , pero
dogs2 genera inmediatamente una excepción al tratar de insertar un tipo incorrecto. También podemos ver que resulta per-
fectamente posible introducir objetos de un tipo derivado dentro de un contenedor comprobado donde se esté haciendo la
com probación según el tipo base.
Ejercicio 35: (1) Modifique CheckedList.java para que utilice las clases Coffee definidas en este capitulo.
Excepciones
Debido al mecanismo de borrado de tipos, la utili zación de genéricos con excepciones es extremadamente limitada. Una
cláusula catch no puede tratar una excepción de un tipo genérico, porque es necesario conocer el tipo exacto de la excep-
ción tanto en tiempo de compilación como en tiempo de ejecución. Asimismo, una clase genérica no puede heredar directa
ni indirectamente de Throwable (esto impide que tratemos de definir excepciones genéricas que no puedan ser atrapadas).
Sin embargo. los parámetros de tipo sí que pueden usarse en la cláusula throws de la declaración de un método. Esto nos
pennite escribir código genérico que varíe según el tipo de una excepción comprobada:
JJ: g e nerics/ ThrowGene ric Exc ept i on . java
impert java . util .* ;
if (count < O)
throw new Failure2();
ProcessRunner<Integer,Failure2> runner2 =
new ProcessRunner<Integer,Failure2>();
for(int i = O; i < 3; i++)
runner2.add(new Processor2 ());
try (
System.out.println{runner2.processAll{) ;
catch(Failure2 el {
System.out.println(e) ;
Un objeto Processor ejecuta un método process( ) y puede generar una excepción de tipo E. El resultado de process( )
se almacena en el contenedor List<T> resultCollector (esto se denomina parámetro de recolección). Un obje-
to ProcessRunner tiene un método processAll( ) que ejecuta todo objeto Process que almacene y devuelve el objeto
resultCoUector.
Si no pudiéramos parametrizar las excepciones generadas, no podríamos escribir este código en forma genérica. debido a la
existencia de las excepciones comprobadas.
Ejercicio 36: (2) Ailada una segunda excepción parametrizada a la clase Processor y demuestre que las excecpiones
pueden vari ar independientemente.
Mixins
El ténnino mixin parece haber adquirido distintos significados a lo largo del tiempo, pero el concepto fundamental se refie·
re a la mezcla (mixing) de capacidades de múltiples clases con el fin de producir una clase resultante que represente a todos
los tipos del mixin. Este tipo de labor suele realizarse en el último minuto, lo que hace que resu lte bastante conveniente para
ensamblar fácilmente unas clases con otras.
Una de las ventajas de los mixins es que permiten aplicar coherentemente una serie de características y comportamientos a
múltiples clase. Como ventaja añadida, si queremos cambiar algo en una clase mixin, dichos cambios se aplicarán a todas
las clases a las que se les haya aplicado el mixin. Debido a esto, los mixins parecen orientados a lo que se denomina progra-
mación orientada a aspectos (AOP, aspect-oriented programming), y diversos autores sugieren precisamente que se utilice
el concepto de aspectos para resolver el problema de los mixins.
15 Genéricos 459
Mixins en C++
Uno de los argumentos más sólidos en favor de la utilización de los mecanismos de herencia múltiples en e++ es, precisa-
mente, e l poder utilizar mixins. Sin embargo, un enfoque más interesante y más elegame a la hora de tratar los mixins es e l
que se basa en la utilización de tipos parametrizados; según este enfoque. un mixin es una clase que hereda de su paráme-
tro de tipo. En C++, podemos crear mixins fáci lmente debido a que e++ recuerda el tipo de SlI S parámetros de plantilla.
He aquí un ejemplo de e++ con dos tipos de mixin: uno qu e pennite añadir la propiedad de di sponer de una marca tempo-
ral y otro que añade un número de sen e para cada instancia de objeto:
JI: generics/Mixins.cpp
#include <string>
#inc!ude <ctime>
#include <iostream>
using namespace std¡
class Basic {
string value¡
public:
vaid set (string val) { value val; )
string get () { return value¡
};
int main () {
TimeStamped<SerialNumbered<Basic> > mixinl, mixin2¡
mixinl.set("test string 1");
mixin2 . set ( " test string 2 t' ) ;
caut « mixin1. get () « " " « mixin1 . getStamp () «
" " « mixin1. getSerialNumber () « endl;
caut « mixin2. get () « " " « mixin2. getStamp () «
" " « mixin2. getSeria lNumber () « endl;
1* Output: (Samp le)
test string 1 1129840250 1
test string 2 1129840250 2
* /// ,-
En maine ), el tipo resultame de mixin I y mixin2 tiene todos los métodos de los tipos que se han usado en la mezc la.
Podemos considerar un mixin como una especie de func ión que establece una correspondencia entre clases existentes y una
se rie de nu evas subclases. Observe lo fácil que resulta crear un mixin utilizando esta técnica; básicamente, nos limitamos a
decir lo que queremos y e l co mpilador se encarga del resto:
TimeStamped<SerialNumbered<Basic> > mixin1, mixin2;
460 Piensa en Java
Lamentablemente, los genéricos de Java no penniten esto. El mecanismo de borrado de tipos hace que se pierda la infonna-
ción acerca del tipo de la clase base, por lo que una clase genérica no puede heredar directamente de un parámetro genérico.
interface Basic {
public void set(String val);
public String get() ¡
/* Output: {Sample}
test string 1 1132437151359 1
cest string 2 1132437151359 2
*///0-
La clase Mi~iD está utilizando, básicamente, el mecanismo de delegación, por lo que cada uno de los tipos mezclados
requiere un campo en Mixin , y es necesario escribir todos los métodos que se precisan en Mixin para redirigir las llamadas
al objeto apropiado. Este ejemplo utiliza clases triviales, pero en un mixin más complejo el código puede llegar a crecer de
tamaño de fonn3 bastante rápida. 4
Ejercicio 37: (2) Añada una nueva clase mixin Colored a Mixins.java, mézclela en Mixio y demuestre que funciona.
class Basic {
private String value¡
public void set (String va l ) { value val; }
public String get () { return value ¡
4 Observe que algunos entornos de programación, como Eclipse e lntelliJ Idea, generan automáticamente el código de delegación.
5 Los patrones se tratan en Thinking in Pal1ems (with Java), que puede encontrar en www.MindView.ner. Consulte también Design Partems, de Erieh
Gamma er al. (Addison- Wesley, 1995).
462 Piensa en Java
super{basic) ;
timeStamp = new Date{) .getTime ();
@SuppressWarnings (Hunchecked!l)
public static Object newlnstance{TwoTuple ... pairs)
Class [] interfaces = new Class [pairs . length) ;
for(int i = O; i <: pairs . length; i++} {
interfaces [i] = (Class) pairs (i] . second¡
Cl assLoader el =
pairs(O) . first . getClass() . getClassLoader() ¡
return Proxy . newProxylnstanee(
el, interfaces, new MixinProxy(pairs)) ¡
/ * Output : (Sample)
Hello
11325 1 9137015
1
, /// , -
Puesto que el único que incluye todos los tipos mezclados es el tipo dinámico y no es tipo estático, esta solución sigue sin
ser tan elegante como la utilizada en C++, ya que nos vemos obligados a realizar una especialización sobre el tipo apropia-
do antes de que podamos invoca r ningún método del mismo. Sin embargo, esta solución está significativamente más próxi-
ma que las anteriores a lo que es el concepto de un verdadero mixi11.
Es mucho el trabajo que se ha realizado para poder soportar mixins en Java, incluyendo la creación de una extensión de l len-
guaje (el lenguaje Jam) que se ha definido específicamente con el objeto de soportar los mixins.
Ejercicio 39: (1) Añada una nueva clase mixin Colo red a DynamicProxyM ixin.java, mézclela en mixin, y demuestre
que funciona.
464 Piensa en Java
Tipos latentes
Al principio de este capitulo hemos introducido la idea de escribir código que pueda ser aplicado de la forma más general
posible. Para hacer esto, necesitamos fonnas de relajar las restricciones que afectan a los tipos con los que nuestro código
puede trabajar. sin por ello perder los beneficios proporcionados por el mecanismo de comprobación estática de tipos. Por
eso, somos capaces de escribir código que puede utilizarse, sin modificaciones, en el número mayor de siruaciones; en otras
palabras, código '"más genérico".
Los genéricos de Java parecen dar un paso adicional en esta dirección. Cuando escribimos o utilizamos genéricos que sim-
plemente se emplean para almacenar objetos, el código funciona con cualquier tipo de datos (salvo con las primitivas, aun-
que como hemos visto, el mecanismo de conversión automática solventa este problema). Dicho de otra manera, los
genéricos de "almacenamiento" son capaces de decir: "No me importa de qué tipo eres". El código al que no le preocupa el
tipo de dato con el que funciona puede aplicarse, ciertamente, en cualquier situación y es, por tanto, bastante "genérico".
Como también hemos visto, surge un problema en el momento en el que queremos realizar manjpulaciones con los tipos
genéricos (distintas de la invocación de métodos de ObjecI), porque el mecanismo de borrado de tipos requiere que espe-
cifiquemos los límites que pueden usarse para los tipos genéricos, con el fin de poder invocar de manera segura métodos
específicos para los objetos genéricos dentro del código. Ésta es una limitación significativa al concepto de "genérico",
porque es necesario restringir los tipos genéricos de modo que puedan heredar de clases concretas o implementar interfaces
particulares. En algunos casos, puede que tenninemos generando en lugar de genéricos una clase o interfaz normales, debi-
do a que un genérico con límites puede no diferir de la especificación de una clase o una interfaz.
Una solución que proporcionan algunos lenguajes de programación se denomina tipos latentes o tipos estructurales. Un tér-
mino más coloquiai es el de lipos palo (dllck typing); este ténnino ha llegado a ser muy popular debido a que no acarrea el
bagaje que los otros términos sí acarrean. El ténnino proviene de la frase "si camina como un pato y se compona como un
pato, podemos tratarlo como un pato".
El código genérico, nonnalmente, sólo invoca unos cuantos métodos de un tipo genérico, y los lenguajes con tipos latentes
relajan la restricción (teniendo que producir código más genérico) requiriendo que tan sólo un subconjunto de los métodos
sea implementado, no una clase o interfaz concretas. Debido a esto, los tipos latentes penniten saltar las fronteras de las
jerarquías de clase, invocando métodos que no fonnan parte de una interfaz común. En la práctica, un fragmento de código
podría decir: "No me importa de qué tipo eres, siempre y cuando dispongas de los métodos speak() y silO". Al no reque-
rir un tipo específico, el código puede ser más genérico.
Los tipos latentes son un mecanismo de organización y reutilización del código. Con ellos, podemos escribir fragmentos de
código que pueden reutilizarse más fácilmente que si no se empleara el mecanismo. La organización y reutilización del códi-
go son las herramientas fundamentales de la programación infonnática: escribir el código una sola vez, utilizarlo más de una
vez y mantener el código en un único lugar. Al no vemos obligados a especificar una interfaz exacta con la que el código
pueda operar, los tipos latentes nos penniten escribir menos código y aplicar dicho código más fácilmente y en más lugares.
Dos ejemplos de lenguajes que soportan el mecanismo de tipos latentes son Pytbon (que puede descargarse gratuitamente
de \Vww.Python.org) y C++. 6 Python es un lenguaje con tipos dinámicos (prácticamente todas las comprobaciones de tipos
se realizan en tiempo de ejecución) y C++ es un lenguaje con tipos estáticos (las comprobaciones de tipos se realizan en tipo
de compilación), por lo que los tipos latentes pueden utilizarse tanto con las comprobaciones de tipo estáticas como con las
dinámicas.
Si tomamos la descripción anterior y la expresamos en Python, tendría el aspecto siguiente:
#: generics/DogsAndRobots.py
class Dog:
def speak(self):
print It Arf ! It
def sit (se lf ) :
print nSitting n
de f reproduce (se 1 f ) :
pass
class Robot:
def speak (self) :
print "Click!"
def sit (self) :
print "Clank!"
def oilChange (self) :
pass
def perform(anything):
anything.speak()
anything. si t ()
a = Dog()
b = Robot (1
perform(a)
perform(b)
#,-
python utiliza el sangrado para detenniDar el ámbito (así que no hacen falta símbolos de llaves) y un símbolo de dos pun-
tos para dar comienzo a un nuevo ámbito. Un símbolo '#' ¡.ndica un comentario que se extiende hasta el final de la línea, al
igua l que '//' en Java. Los métodos de una clase especifican explícitamente, como primer argumento, el equivalente de la
referencia this, que en este lenguaje se denomina self por convenio. Las llamadas a constructor no requieren ninguna clase
de palabra clave "new". Y Python permite la ex istencia de funciones nonnales (es decir, funciones que no son miembro de
una clase), como pone de manifiesto la función perform() .
En perform(anylhing), observe que no se indica ningún tipo para for anylhing, y que anylhing es simplemente un identi-
ficado r. Esa variable debe ser capaz de realiza r las operaciones que perform( ) le pida, por lo que hay un a interfaz implíci-
ta. Pero nunca hace falta escribir explícitamente dicha interfaz, ya que está latente. perform( ) no se preocupa de cuál sea
el ti po de su argumento, así que podemos pasarle a esa función cualquier objeto siempre y cuando éste soporte los métodos
,peak() y ,il(). Si pasamos a perform( ) un objeto que no soporte estas operaciones, obtendremos una excepción en tiem-
po de ejecución.
Podemos producir el mismo efecto en e ++:
// : generics/DogsAndRobots.cpp
class Dog {
public:
void speak (1 {)
void sitll {}
void reproduce (1 {)
};
class Robot {
public:
void speak () {}
void sitll {}
void oilChange{)
};
template<class T> void perform{T anything) {
anything.speak{) ;
anything.sit() i
int main ()
Oog di
Robot r;
perform (d) i
perform(r) ;
///,-
466 Piensa en Java
Tanto en Python como en C++, Dog y Robot no tienen nada en común. salvo que ambos disponen de dos métodos con sig.
naturas idénticas. Desde el punto de vista de los tipos. se trata de tipos completamente distintos. Sin embargo. a perform()
no le preocupa el tipo específico de su argumento y el mecanismo de tipos latentes le pennite aceptar ambos tipos de objeto.
C++ verifica que pueda enviar dichos mensajes. El compilador proporcionará un mensaje de error si tralamos de pasar el
tipo incorreclO (estos mensajes de error han sido, históricamente. bastante amenazallles y muy extensos, y son la razón prin·
cipal de que las plantillas C++ tengan una reputación tan mala). Aunque ambos lo hacen en instantes distintos (C++ en tiem·
po de compi lación y Python en tiempo de ejecución). los dos lenguajes garantizan que no se puedan utilizar incorrectamente
los tipos, y esa es la razón por la que decimos que los dos lenguajes sonfuer,emente (ipodas. 7 Los tipos latentes no afectan
al tipado fuerte.
Como el mecanismo de genéricos se añadió a Java en una etapa tardía, no hubo la oportunidad de implementar un mecanis-
mo de tipos latentes. así que Java no tiene soporte para esta funcionalidad. Como resultado, puede parecer al principio que
el mecanismo de genéri cos de Java es "menos genérico" que el de otros lenguajes que sí soporten los tipos latentes. 8 Por
ejemplo, si tratamos de implementar el ejemplo anterior en Java! estaremos obligados a utilizar una clase o una interfaz y a
especificarlas dentro de una expresión de límite:
11 : generics/Performs.java
11 : generics/DogsAndRobots.java
II No hay tipos latentes en Java
import typeinfo.pets.*¡
import static net.mindview.util.Print.*¡
class Communicate {
public static <T extends Performs>
void perform(T performer)
performer.speak{) ;
performer . sit() ¡
7 Dado que se pueden utilizar proyecciones de tipos, que deshabilitan en la práctica el sistema de tipos, algunos autores argumentan que e++ es un len-
guaje débilmente tipado, pero creo que esa opinión es exagerada. Probablemente sea más justo decir que C++ es un lenguaje "fuertemente tipado" con una
pueTla trasera."
8 La implementación de los mecanismos de Java utilizando el borrado de tipos se denomina, en ocasiones, mecanismo de lipos genéricos de segunda clas{'.
15 Genéricos 467
/ * Output:
Woo f!
Si tting
Cl i ck !
Clank!
, /// ,-
Sin embargo. observe que perform( ) no necesita utili zar genéricos para poder funcionar. Podemos simplemente especifi-
car que acepte un objeto Performs :
JI : generics j SimpleDogsAndRobots.java
// Eliminación del genérico, el código sigue funcionando.
c l ass CommunicateSimply {
static void perform ( Performs performer ) {
performer.speak {) ;
performer . sit () i
/ * Qutput:
Woof!
Sitting
Click!
Clank!
, / / / o-
En este caso, los genéricos eran simplemente innecesarios, ya que las clases estaban ya obligadas a implementar la interfaz
Performs.
Reflexión
Una de las técnicas que podemos utili zar es el mecanismo de reflexión. He aquí el método perform() que utili za tipos laten-
tes:
11 : generics / LatentReflection.java
II Utilización del mecanismo de reflexión para generar tipos latentes.
import java.lang.reflect. * ¡
import static net.mindview.util.Print.*¡
II No implementa Performs:
class Mime {
public void walk.AgainstTheWind () {}
public void sit () { print ("Pretending te sit tl ) ¡ }
468 Piensa en Java
// No implementa Performs:
class SmartDog {
public void speak () { print ("Woof! ") ;
public void sit () { print ("Sitting");
public void reproduce () ()
class CornmunicateReflectively {
public static void perform{Object speaker)
Class<?> spkr = speaker.getClass{);
try (
try (
Methad speak = spkr.getMethod("speak U ) i
speak . invoke (speaker) ;
catch {NoSuchMethodException el {
print (speaker + cannot speak");
11
}
try (
Methad sit = spkr.getMethod( " sit lt ) ;
sit.invoke{speakerl i
catch(NoSuchMethodException el {
print (speaker + " cannot si t ") i
catch{Exception el
throw new RuntimeException(speaker.toString(), el;
/ * Output:
Woof!
Sitting
Click!
Clank!
Mime cannot speak
Pretending to sit
*///,-
Aquí, las clases son completamente disjuntas y no tenemos clases base (distintas de Object) ni interfaces en común. Grac ias
al mecanismo de reflexión, CommunicatcReflectively.perform( ) puede establecer dinámicamente si los métodos desea-
dos están disponibles e in vocarlos. Incluso es capaz de gestionar el hecho de que Mime sólo tiene uno de los métodos nece-
sari os, cumpliendo parcialmente con su objeti vo.
Exa minemos un ej emplo donde se analiza este problema. Suponga que desea crear un método a pply( ) que pueda aplicar
cualquier método a todos los objetos de una secuencia. Ésta es una situación en la que las interfaces parecen no encajar. Lo
que que remos es apli car cualquier método a una colección de objetos, y las interfaces introducen demasiadas restricciones
como para poder describir el concepto de "cualquier método". ¿Cómer podemos hacer esto en Java?
Ini cialmente, podemos resolver el problema medi ante el mecanismo de refle xión, que res ulta ser bastante elegante gracias
a los varargs de Java SES:
ji : generics / Apply.java
II {main , ApplyTest}
i mport java.lang.reflect.*i
import java.util.*;
i mport static net.mindview.util.Print.*¡
class Shape {
public void rotate() {pr int(this + 11 rotate ll )¡ }
public void resize (int newSize ) {
print (this + 11 resize 11 + newSize ) ;
class ApplyTest
public static void main (String[] args ) throws Exception
List<Shape> shapes = new ArrayList<Shape>( ) ;
for (int i = O; i < 10 ; i++)
shapes.add(new Shape( )) ;
Apply. apply {shape s, Shape.class . getMethod ( " rotate" ) ;
Apply. apply (shapes,
Shape.class . getMethod("resize", int.class ) , 5);
List<Square> square s = new ArrayList<Square>( ) ;
for(int i = O; i < 10; i++)
squares.add (new Square( ) );
Apply. apply (squares, Shape . class.getMethod ( lIrotate") ) i
470 Piensa en Java
Apply.apply (squares,
Shape . class . getMetho d ( "resize", int. c lass ) , S} i
"Se trata de una sintax is que se utiliza intensivamente en las nuevas API para manipulación de anotaciones, por ejemplo".
Sin embargo. en mi opinión. no todo el mundo se siente igual de cómodo al utilizar esta técnica; algunas personas prefieren
emplear el método de la factoría que fue presentado anteriormente en este capitulo.
Asimismo. aunque la solución de Java resulta bastante elegante. debemos observar que el LISO del mecanismo de reflexión
(aunque se ha mejorado significativamente en las últimas versiones de Java) puede hacer que el programa sea más lento que
las implementaciones no basadas en la reflexión. ya que hay demasiadas tareas que llevar a cabo en tiempo de ejecución.
Esto no deberia impedir que empleáramos esta solución, al menos como primera sol ución al problema (salvo que queramos
caer en el error de la optimización prematura), pero representa ciertamente una diferencia entre las dos técnicas.
Ejercicio 40: (3) Añada un método speak( ) a todas las clases de typeinfo.pets. Modifique Apply.java para invocar al
método speak() para una colección heterogénea de objetos Peto
lINo funciona con tlcualquier cosa que tenga un método add () ti. No hay
l/u na interfaz "Addable tl , por lo que nos vemos limitados a
// utilizar un contenedor Collection. No podemos generalizar
/1 empleando genéricos en este caso.
class Contract {
private static long counter = O;
private final long id = counter++i
public String toString () {
return getClass{) .getName() + " 11 + id;
class FillTest {
public static void main(String [] args ) {
List<Contract> contracts = new ArrayList<Contract>();
Fill.fill(contracts, Contract.class, 3} i
Fill.fill{contracts, TitleTransfer.class, 2);
for(Contract e: contracts)
System.out . println(c) i
SimpleQueue<Contract> contractQueue
new SimpleQueue<Contract>();
II No funciona. fill() no es lo suficientemente genérico:
II Fill.fill{contractQueue, Contract.class, 3);
1* Output:
Contract O
Contract 1
Contract 2
TitleTransfer 3
TitleTransfer 4
*11 1 ,-
Es en estas situaciones donde resulta ventajoso disponer de un mecanismo parametrizado con tipos latentes, porque de esa
fonna no estaremos a merced de las decisiones de diseño que hubiera tomado en el pasado cualquier diseñador concreto de
bibliotecas; gracias a eso no tendremos que reescribir nuestro código cada vez que nos encontremos con una biblioteca que
no hubiera tenido en cuenta nuestra situación concreta (así que el código será verdaderamente "genérico"). En el caso ante~
rior, como los diseñadores de Java no vieron la necesidad (lo cual resulta bastante natural) de agregar una interfaz
"Addable", estamos obligados a movernos dentro de la jerarquía Colleetion, y SimpleQueue no funcionará, aún cuando
disponga de un método addQ. Dado que ahora está restringido a trabajar con Colleetion, el código no es particularmente
"genérico". Con los tipos latentes este problema no se presentaría.
)
// Versión con generador :
public static <T> void fill {Addable<T> addable,
Generator<T> generator, int size ) {
for ( int i = O; i < size; iT+ )
addable.add (generator.next () ) ;
e lass Fil12Test (
publie statie vo id main (String (] args ) (
II Adaptar una colección:
List<Coffee> earrier = new ArrayList<Coffee> () ;
Fi1l2. till (
new AddableCollectionAdapter<Coffee> (earrier ) ,
Coffee.class, 3);
II El método auxiliar captura el tipo :
Fill2.fill(Adapter.collectionAdapter(earrier) ,
Latte . elass, 2);
for(Coffee c: earrier)
print ( e) ;
print("---------------------- " ) ;
II Utilizar una clase adaptada:
AddableSimpleQueue<Coffee> coffeeQueue
new AddableSimpleQueue<Coffee> {) ;
FiI12.fill(eoffeeQueue , Mocha.class, 4);
Fill2 . fill(eoffeeQueue, Latte.class, 1);
for(Coffee c: coffeeQueue)
print (e ) ;
474 Piensa en Java
/ * Output:
Coffee O
Coffee 1
Coffee 2
Latte 3
Latte 4
Mocha 5
Mocha 6
Mocha 7
Mocha 8
Latte 9
*///,-
Fill2 no requiere un objeto Collection a diferencia de FiII. En lugar de ello, sólo necesita algo que implemente Addable, y
Addable ha sido escrita precisamente para FiII, este ejemplo es una manifestación del tipo latente que queríamos que el
compilador construyera por nosotros.
En esta versión, también hemos añadido un método fill( ) sobrecargado que toma un objeto Generator en lugar de un indi-
cador de tipo. El objeto Gener.tor no presenta problemas de seguridad de tipos en tiempo de compilación: el compilador
garantiza que lo que pasemos sea un objeto Gt'nerator vá lido, as í que no puede gene rarse ningun a excepción.
El primer adaptador, AddableCollcctionAdapler, funciona co n el tipo base Colleclion, lo que significa que puede utilizar-
se cualquier implementación de Collection. Esta versión simplemente almacena la referenc ia a Colleetion y la utiliza para
implementar add( ).
Si disponemos de un tipo específico en lugar de disponer de la clase base de una jerarquía, podemos escribir algo menos de
código a la hora de crear el adaptador empleando el mecanismo de herencia, como puede verse en AddableSimpleQueue.
En Fill2Test.main( ), podemos ver cómo funcionan los diversos tipos de adap tadores. En primer lugar, se adapta un tipo
Collcclion con AddableCollcctionAdapler. Una segunda versión de esto utili za el método aux iliar ge nérico, y podemos
ver cómo el método genérico captura el tipo. así que no es necesario escribirl o explícitamente; se trata de un truco bastan-
te conveni ente, que nos permite escribir código más elegante.
A continuación, se utiliza la clase AddableSimpleQucue pre-adaptada. Observe que en ambos casos los adaptadores per-
miten util izar con Fil12.fíIl( ) las clases que previamente no implementaba Add.ble.
La utilización de adaptadores de esta fornla parece compensar la falta de un mecanismo de tipos latentes, por lo que podría
pensarse que podemos escribir código genuinamente genérico. Sin embargo, se trata de un paso de programación adicional
y es necesario que lo comprendan tanto el creador de la biblioteca como el consumidor de la misma; y este concepto puede
no ser entendido fácilmente por los programadores menos expertos. Los verdaderos mecanismos de tipos latentes permiten,
al eliminar este paso adicional, aplicar el código genérico más fácilmente, y ahí radica precisamente su valor.
Ejercicio 41: ( 1) Modifique Fi112.j.va para utilizar las clases typeinfo.pels en lugar de las clases Coffee.
La solución consiste en utilizar el patrón de diseño de EstraTegia, que pennite obtener código más elegante porque aísla
completamente "las cosas que cambian" dentro de un objelo deful1ción lO . Un objeto de función es un objeto que se com-
porta. en cierta manera, como una función: normalmente, existe un método de interés (en los lenguajes que soportan el
mecanismo de sobrecarga de operadores, podemos hacer que la llamada a este método pare=ca una llamada a método nOf-
mal). El valor de los objetos de función es que, a diferencia de un método normal , los podemos pasar de un sitio a olro y
también pueden tener un estado que persista entre sucesivas llamadas. Por supuesto, podemos conseguir algo como esto con
cua lquier método de una clase, pero (al igual que con cualquier patrón de diseño) el objeto de función se distingue princi-
palmente por su intención original. AquÍ, la intención es crear algo que se comporte como un único método que podamos
pasar de un sitio a otro; por tanto, está estrictamente acoplado (yen ocasiones es indistinguible de é l). con el patrón de dise-
ño de Esrrateg;a.
De acuerdo con mi experiencia con distintos patrones de diseño, las fronteras son un tanto difusas en este caso: lo que vamos
a hacer es crear objetos de función que realicen una cierta adaptación, yesos objetos se van a pasar a una serie de métodos
para utilizarlos como estrategias.
Adoptando este enfoque, en este ejemplo se añaden los diversos tipos de métodos genéricos que originalmente queríamos
crear, así como algunos otros. He aquí el resultado:
11: generics/Functional.java
import java.math.*;
import java.util.concurrent . atomic.*¡
import java.util.*;
impo rt static net.mindview.util.Print.*¡
10 En ocasiones, podni. ver que a estos objetos se los denominafimclores. En este texto, utilizaremos el término objeto defllllció" en lugar defimctor, ya
que el ténnino "functor" tiene un significado diferente y muy específico en matemáticas.
476 Piensa en Java
return func;
static class
IntegerSubtracter implements Combiner<Integer> {
public Integer combi ne ( Integer x, Integer y ) {
return x - y;
static class
BigDecimalAdder implements COmbiner<BigDecimal> {
public BigDecimal cOmbine (BigDecimal x, BigDecimal y ) {
return x. add (y ) ;
static class
BiglntegerAdder implements Combiner<Biglnteger> {
public Biglnteger cOmbine (Biglnteger x, Biglnteger y)
return x.add(y ) ;
static class
AtomicLongAdder implements Combiner<AtomicLong> {
public AtomicLong cOmbine (AtomicLong x, AtomicLong y ) {
// No está claro si esto es significativo:
return new AtomicLong (x.addAndGet (y.get( ))) ;
print(result) i
print(forEaeh(li,
new MultiplyinglntegerCollector()) .result()) i
printlfilterllbd,
new GreaterThan<BigDecimal>(new BigDeeimal(3)))} ¡
print (lbi) i
/ * Output:
28
-26
(5, 6, 71
5040
210
11.000000
(3.300000, 4.4000001
[11, 13, 17, 1 9, 23, 29, 31, 37, 41, 43, 47 ]
311
true
265
(0.000001, 0 . 00000 1, 0 . 00000 1, 0 . 0000011
* ///,-
El ejemplo comienza definiendo interfaces para distintos tipos de objetos de función. Estas interfaces se han creado a medi-
da que eran necesarias mientras se desarrollaban los diferentes métodos y se descubría la necesidad de cada una. La clase
Combíner me fue sugerida por un contribuidor anónimo a uno de los artículos que publiqué en mi sitio web. Combiner
abstrae los detalles específicos relativos a tratar de sumar dos objetos y se limita a enunciar que esos objetos están siendo
combinados de alguna manera. Corno resultado, podemos ver que IntegerAdder y IntegerSubtraeter pueden ser tipos de
Combiner.
Una función unario (U naryFunetion) toma un único argumento y produce un resultado, el argumento y el resultado no tie-
nen por qué ser del mismo tipo. Se utiliza un elemento Collector como "parámetro de recopilación" y podemos extraer el
resultado una vez que hayamos acabado. Un predicado UnaryPredicate produce un resultado de tipo booleano Hay otros
tipos de objetos de función que pueden definirse. pero estos son suficientes para entender el concepto.
La clase Functional contiene Wla serie de métodos genéricos que aplican objetos de función a secuencia. El método
reduce() aplica la función contenida en un objeto Combiner a cada elemento de una secuencia con el fin de producir un
único resultado.
forEaeh() toma un objeto Colleetor y aplica su función a cada elemento, ignorando el resultado de cada llamada a función .
Podemos invocar este método simplemente debido a su efecto colateral (lo que no encajaría con un estilo de programación
·'funciona l" pero puede, a pesar de todo, ser útil), o bien el objeto Colleetor puede mantener el estado interno para actuar
como un parámetro de recopilación, como es el caso en este ejemplo.
transform( ) genera una lista invocando una función UnaryFunetion sobre cada objeto de la secuencia y capturando el
resultado.
Finalmente, filter( ) aplica un predicado UnaryPredieate a cada objeto de una secuencia y almacena los objetos que pro-
ducen true en un contenedor List. que luego se devuelve.
Podemos definir funciones genéricas adicionales. La biblioteca estándar de plantillas de C++, por ejemplo, dispone de mul-
titud de ellas. El problema también se ha resuelto con unas bibliotecas de código abierto, como por ejemplo JGA (Generic
A Igorilhms for Java).
En C++, el mecanismo de tipos latentes se encarga de establecer la correspondencia entre las operaciones cuando se invo-
can las funciones, pero en Java necesitamos escribir los objetos de función para adaptar los métodos genéricos a nuestras
necesidades concretas. Por tanto, la siguiente parte de la clase muestra diferentes implementaciones de los objetos de fun-
ción. Observe, por ejemplo, que IntegerAdder y BigDecimalAdder resuelven el mismo problema, (sumar dos objetos),
15 Genéricos 479
in\'ocando las operaciones apropiadas para su tipo concreto. Éste es un ejemplo de combinación de los patrones de diseño
Adaptador y Estrategia.
En main( ). podemos ver que en cada llamada a método se pasa una secuencia junto con el objeto de función apropiado.
Asimismo. \emos que hay expresiones que pueden llegar a ser bastante complejas, como por ejemplo:
f o rEach (filter {li, new GreaterThan (4 )) ,
new MultiplyinglntegerCollector ()) .result ()
Esta expresión genera una lista seleccionando todos los elementos de li que sean mayores que 4, y luego aplica el método
MultiplyinglntegerCoUector() a la lista resultante y extrae e l resultado con resul t(). No va mos a explicar los detalles del
resto del código, aunque el lector no debería tener problemas en comp renderlo sin más que analizarlo.
Ejercicio 42: (5) Cree dos clases separadas, que no tengan nada en común. Cada clase debe almacenar un valor y dis-
poner al menos de métodos que produzcan dicho valor y pennitan reali zar una modificación del mismo.
Modifique FUDctional.java para que realice operaciones funcionales sobre colecciones de dichas dos cIa-
ses (estas operaciones no tienen que ser aritméticas como son las de Fu nctional.java).
Esto no es sólo una molestia, En ocasiones, puede dar lugar a errores de pmgramación difici-
les de de/ec/m: Si una parte de un programa (o varias par/es de un pmgrama) inserta objetos
en un contenedor y lo único que podemos descubrir en una parte separada del programa, por
la generación de una e..tcepción, es que se ha introducido un objeto incorrecto dentm del con-
tenedOl; entonces nos vemos obligados a averiguar en qué punto se ha producido la inserción
incorrecta,
Sin embargo, después de examinar de nuevo la cuestión, comencé a pensar en ella, En primer lugar, ¿con qué frecuencia se
produce este problema? Personalmente, no recuerdo que nunca me haya pasado algo así Y. cuando he preguntado a la gente
que asistía a mis conferencias. tampoco he logrado encontrar a nadie que me dijera que a ellos le había pasado. En otro libro
sobre el tema, se incluía un ejemplo de una lista denominada files que contenía objetos Strin g; en este ejemplo, parecía com-
pletamente natural anadir un objeto F ile (archivo) a files, por lo que babria sido mejor denominar al objeto liIeNa mes (nom-
bres de archivo). Independientemente de lo exhaustivos que sean los mecani smos de comprobación de tipos de Java, sigue
siendo posible escribir programas enrevesados, y el hecho de que un programa mal escrito se pueda co mpilar no quiere decir
que deje de ser un programa mal escrito, Quizá la mayoría de los programadores utilicen contenedores con nombres apro-
480 Piensa en Java
piados como "cats" que proporcionan una advertencia visual al programador que esté intentado añadir un objeto que no sea
del tipo Cato E incluso si llegara a darse el caso de que alguien introduzca el objeto incorrecto, ¿duranre cuánto tiempo per-
manecería oculto ese problema antes de ser descubierto? Parece lógico pensar que, tan pronto como empezáramos a ejecu-
tar pruebas con datos reales, se generaría rápidamen te una exce pción.
Un detenninado autor ha llegado a deci r que dicho problema podría ·'pennanecer oculto durante años", pero yo no he podi-
do encontrar infom1cs que hablen de personas que tengan grandes difi cultades para encontrar crrores del tipo "perro en una
lista de gatos ", ni tampoco he encontrado infonnes donde se diga que ese problema se produce muy a men udo. Mientras
que con las hebras de programac ión, como veremos en el Capítulo 2 1, Concurrencia. resulta bastante senci llo y común que
haya errores que sólo se manifiesten de fomla muy infrecuente, y que sólo nos proporcionan una vaga indicación de qué es
lo que anda mal, en este otro ejem plo de la inserción de objetos erróneos, las cosas no son así. Por tanto, ¿es el problema
de la inserción de objetos erróneos la razón de que todas estas funcionalidades tan significati vas y complejas hayan sido aña-
didas a Java?
En mi opinión. la intención de esa funcionalidad del lenguaje de propósito general denominada "genéricos" (no necesaria-
mente de la implementación concreta que Java hace) es la expresividad, no simplemente la creación de contenedores que
sean seguros en lo que respecta a los tipos de datos. La posibilidad de disponer de contenedores que sean seguros respecto
a los tipos de datos se obtiene corno efecto colateral de la capacidad de crear código que tenga un propósito más general.
Por tanto, au nque el argumento de la inserción de tipos incorrectos en UDa lista se utiliza a menudo para justificar la exis-
tencia de los genéricos, dicho argumento resulta cuestionable. Y, co mo decíamos al principio del capítulo, no creo que el
concepfo de genéricos tenga nada que ver con ese problema. Por el contrario, los genéricos, como su propio nombre indi-
ca, constituyen una fonna de escribir código más "genérico" y que esté menos restringido por los tipos de datos con los que
pueda trabajar, de manera que un mismo fragmento de código pueda aplicarse a una mayor cant idad de tipos de datos. Como
hemos visto en este capítulo, resulta fácil escribir clases "contenedoras" verdaderamente genéricas (es decir, lo que son los
contenedores de Java), pero escribir código que manipule sus tipos genéricos requiere un esfuerzo adicional tanto por parte
del creador de la clase, como por parte del consumidor de la misma, que debe comprender el concepto y la implementación
del patrón de diseño Adaptador. Dicbo esfuerzo adicional reduce la facilidad de uso de esta funcionalidad y puede, por tanto,
hacer que sea menos aplicable en diversos lugares en los que podría sin embargo representar un valor añadido.
Observe también que como los genéricos fueron introducidos de manera bastante tardía en Java en lugar de haber sido
incluidos en el lenguaje desde el principio, algunos de los cont enedores no pueden ser tan robustos como deberían. Por ejem-
plo, fijese en Map, y en panicular en los métodos containsKey(Object key) y get(Object key). Si estas clases hubieran
sido diseñadas con genéri cos previamente existentes, estos métodos hubieran utili zado tipos parametrizados en lugar de
Object, permitiendo as í las co mprobac iones en tiempo de compilación que se supone que los genéricos deben proporcio-
nar. En los mapas de e++ por ejemplo, el tipo de la cla ve se comprueba siempre en tiempo de compilación.
Hay una cosa muy clara: introducir cualquier tipo de mecanismo genérico en una versión posterior del lenguaje después de
que ese lenguaje haya llegado a ser de uso genera l, conduce a situaciones extremadamente liosas, y es imposible cumplir el
objetivo sin un esfuerzo enonne. En C++, las plantillas fueron introducidas en la versión ISO inicia l del lenguaje (aunque
incluso eso fue causa de cierta confusión, porque ya se estaba usando una versión anterior, sin plan tillas, antes de que el pri-
mer estándar de C++ apareciera), por lo que, en la práctica, las plantillas fueron siempre una pane de l lenguaje. En Ja va, los
gené ri cos no se introdujeron hasta casi 10 años después de que el lenguaje empezara a utilizarse, por lo que los problemas
co n la migración hacia los genéricos son considerables y han tenido un impacto signifi cati vo en el diseño del propio meca-
nismo de genéricos. El resultado es que nosotros, los programadores, tenemos que sufrir ahora las consecuencias derivadas
de la falta de vis ión de los diseñadores de Java que crearon la versión 1.0. Cuando Java se diseñó originalmente, los dise-
ñadores tenían conocimiento, por supuesto, acerca de las plantillas C++. E incluso consideraron incluirlas en el lenguaje,
pero por alguna razón decidieron dejarlas fuera (lo que probablemente indica que tenían prisa por terminar el di seño). Como
resultado, tanto el lenguaje corno los programado res que lo emplean tienen que enfrentarse con una serie de problemas deri-
vados de esa omisión. Sólo el tiempo nos dirá cuál es el impacto fin al sobre el lenguaje que tendrá la solución que Java ha
adoptado para el tema de los genéricos.
Algunos lenguaj es, y en especial Nice (véase hllp://nice.sollrceforge.net; este lenguaje genera código intennedio Ja va y fun·
ciona con las bibliotecas Java existentes) y NextGen (véase hllp://j apan.cs.rice.edu/nexlgell) han inco rporado otras solucio-
nes más limpias y menos problemáticas para el tema de los tipos parametrizados. No resulta posible imaginar que uno de
estos lenguajes llegue a ser el sucesor de Java, porque ambos han adoptado el mi smo enfoque exacto que C++ adoptó con
respecto C: usar aquello que estaba disponible y mejorarlo.
15 Genéricos 481
Lecturas adicionales
El documento de carácter introductorio para los genéricos es Generics in fhe Java Programming Language, de Gilad Bracha,
que puede encontrar en http://java.slIll.com/j2sel/ .5/pdflgenerics-1lIIoria/pdf
Java Generics FAQs de Angelika Langer es un recurso muy útil. Puede encontrarlo en \Vw\v./angel:camelot.deIGenericsFAQ
/JavaGenerics FA Q. hlm/.
Puede ver deta lles acerca de los co modines en Adding Wildcards lO (he Java Programming Languoge, de Torgerson, Ernst,
Hansen, von der Ahe, Bracha y Gafter, que podrá encontrar en \Vwwjolfm/issues/issue_2004_ 12/arlicle5.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tlle Thinkil/g ;1/ )01'0 Anflotated SOllllioll Guide. disponible para
la venIa en 1I'11"l\:AlindV,ew.nel.
Matrices
Al fInal del Capítulo 5, Jniciali::.ación y limpieza, vimos cómo definir e inicializar una matriz.
Podriamos tratar de describir de manera simple las matrices diciendo que lo que hacemos es crearlas y rellenarlas. luego
seleccionar elementos de las mismas utilizando índices enteros y diciendo. además, que las matrices no cambian de tama-
ño. La mayoría de las veces, eso es todo 10 que necesitamos saber pero en ocasiones hay que realizar operaciones más sofis-
ticadas con las matrices. y también puede que tengamos que comparar la utilización de una matriz con la de otros
contenedores más llexibles. En este capítulo veremos cómo analizar las matrices con un mayor detalle.
class BerylliumSphere
private static long counter¡
private final long id = counter++;
public String toString () { return "Sphere " + id; }
List<8erylliumSphere> sphereList =
new ArrayList<BerylliumSphere>();
for(int i = O ; i < 5; i++}
sphereList.add(new BerylliumSphere(»;
print(sphereList) ;
print(sphereList.get(4» ;
int [] integers = ( O, 1, 2, 3, 4, 5 );
print(Arrays.toString(integersl) ;
print (integers [4] ) ;
/ * Output:
(Sphere O, Sphere 1, Sphere 2, Sphere 3, Sphere 4, null, null, null, null, null]
Sphere 4
(Sphere S, Sphere 6, Sphere 7, Sphere 8, Sphere 9]
Sphere 9
[O, 1, 2, 3, 4, 5]
4
[O, 1, 2, 3, 4, 5, 97]
4
* ///,-
En ambas maneras de almacenar los objetos se comprueban los tipos de los datos y la única diferencias aparente es que las
matrices utilizan I I para acceder a los elementos, mientras que un contenedor de tipo Lis! utiliza métodos como add( ) y
get( ). La similitud entre las matrices y el contenedor Ar rayList es intencionada, de manera que resulte conceptualmente
fácil conmutar entre ambas soluciones. Pero, como vimos en el Capítulo 11 , Almacenamiento de los objetos, los contenedo-
res tienen una funcionalidad mucho más rica que las matrices.
Con la aparición de los mecanismos de conversión automática, los contenedores son casi tan fáciles de utili za r con primiti-
vas como las matrices. La única ventaja que le queda, en consecuencia, a las matrices es la eficiencia. Sin embargo, cuan-
do lo que estemos tratando de resolver sea un problema más general, las matrices pueden ser demasiado restrictivas, y en
esos casos lo que hacemos es utilizar una clase de contenedor.
ciali zación de la matriz. cuanto ex plícitamente mediante una expresión new. Parte del objeto matri z (de hec ho, el único
cam po o método al que podemos acceder) es el miembro length (longitud) de sólo lectura que nos di ce cuántos elementos
pueden almacenarse en dicho objeto matriz. La si ntaxis ¡ I J' es la úni ca otra fonna que tenemos de acceder al objeto matri z.
El siguiente ejemplo resume las diversas fom18s en que puede inicializarse una matri z, y las maneras en que las referencias
de matriz pueden asignarse a diferentes objetos matriz. El ej emplo también muestra que las matrices de objetos y las matri-
ces de primitivas so n casi idénticas en lo que a su uso se refiere. La úni ca diferencia es que las matrices de objetos almace-
nan referencias, mientras que las matrices de primitivas almacenan directamente valores primitivos.
11 : arrays/ArrayOptions.java
II Inicialización y reasignación de matrices.
import java . util .*;
import static net.mindview.util.Print.*;
I1 Matrices de primitivas:
int[) e; 11 Referencia nula
int[] f = new int[5];
II Las primitivas contenidas en la matriz se
I1 inicializan automáticamente con cero:
print f:(11 + Arrays. toString {f) ) ;
11
e ~ new int [1 { 1, 2 };
print ( "e.1ength = + e.1ength ) ;
/ * Output:
b: [nu11, nu11, nu11, nu11, nu11]
a .1ength .2
b.1ength 5
e .1ength 4
d .length 3
a .1ength 3
f, [O, O, O, 0, 01
f . 1ength 5
9 .1ength 4
h .length 3
e .1ength 3
e .1ength 2
* ///,-
La matriz a es una variable local no inicializada y el compilador nos impide que hagamos nada con esta referencia hasta
que la hayamos inicializado adecuadamente. La matriz b se inicializa para que apunte a una matriz de referencias
BeryUiumSphere, pero en esa matriz nunca se llegan a almacenar objetos BeryIHumSphere directamente. De todos modos,
podemos seguir preguntando cuál es el tamaño de la matriz, ya que b está apuntando a un objeto legítimo. Esto nos revela
una cierta desventaja: no podemos averiguar cuántos elementos hay realmente almacenados en la matriz, ya que length nos
dice sólo cuántos elementos pueden almacenarse; en otras palabras, dicho campo nos dice el tamaño del objeto matriz no el
número de elementos que está almacenando en un momento determinado. Sin embargo, cuando se crea un objeto matriz sus
referencias se inicializan automáticamente con el valor null, por lo que podemos ver si una posición concreta de una matriz
tiene un objeto almacenado, comprobando si esa posición tiene un valor null. De forma similar, las matrices de primitivas
se inicializan automáticamente con cero para los tipos numéricos, con (c har)O para char, y con fabe para boolean.
La matriz e pennite ilustrar la creación del objeto matriz seguida de la asignación de objetos BerylliumSphere a todas las
posiciones de la matriz. La matriz d muestra la sintaxis de "inicialización agregada" que hace que el objeto matri z se cree
(implícitamente con new en el cúmulo de memoria, al igual que la matriz e) e inicialice con objetos BerylliumSphere, todo
ello en una única instrucción.
La siguiente inicialización de matriz puede considerarse como una especie de "inicialización agregada dinámica". La ini-
cialización agregada utilizada por d debe utilizarse en el lugar donde se define d , pero con la segunda sintaxis podemos crear
e inicializar un objeto matriz en cualquier parte. Por ejemplo, suponga que hide() es un método que toma como argumen-
to una matriz de objetos Berylli umSphere. Podríamos invocar ese método escribiendo:
hide (d) ;
pero también podemos crear dinámicamente la matriz que queramos pasar como argumento:
hide(new Bery11iumSphere[] { new Bery11iumSphere (),
new BerylliumSphere (1 });
En muchas situaciones, esta sintaxis proporciona una forma mucho más conveniente de escribir el código.
La expresión:
a = d;
muestra cómo podemos tomar una referencia asociada a un objeto matriz y asignarla a otro objeto matriz, al igual que pode-
mos hacer con otro tipo de referencia a objetos. Ahora, tanto a como d están apuntando al mismo objeto matriz situado en
el cúmulo de memoria.
La segunda parte de ArrayOptions,java muestra que las matrices de primitivas funcionan igual que las matrices de obje-
tos, salvo porque las matrices de primitivas almacenan directamente los valores primitivos.
Ejercicio 1 : (2) Cree un método que tome como argumento una matriz de objetos BerylliumSpbere. Invoque el méto-
do creando el argumento dinámicamente. Demuestre que la inicialización agregada normal de matrices no
funciona en este caso. Descubra las únicas situaciones en las que funciona la inicialización agregada de
matrices y en las que la inicialización agregada dinámica es redundante.
16 Matrices 487
// : arrays / lceCream.java
II Devolución de matrices desde métodos.
import java . util.*;
1* Output:
[Rum Raisin, Mint Chip, Mocha Almond Fudge]
[Chocolate, Strawberry, Mocha Almond Fudge]
[Strawberry, Mint Chip, Mocha Almond Fudgel
[Rum Raisin, Vanilla Fudge Swirl, Mud Pie)
[Vanilla Fudge Swirl , Chocolate, Mocha Almond Fudge]
[Pra line Cream, Strawb erry, Mocha Almond Fudge )
[Mocha Almond Fudge , St rawberry, Mi nt Chip ]
* ///,-
El método navorSet( ) crea una matriz de objetos String denominada results. El tamaño de esta matri z es n, que está deter-
minado por el argumento que le pasemos al método. A continuación, el método selecciona una serie de valores aleatoria-
mente de entre la matri z FLAVORS y los coloca en rcsults, devol viendo después esta matriz. La devolución de una matriz
es exactamente igual que la devolución de cualquier otro objeto: se trata simplemente de una referencia. No es importante
que la matri z haya sido creada dentro de navorSct( ), o en cualquier otro lugar. El depurador de memoria se encargará de
borrar la matri z cuando hayamos tenninado de usarla y esa matri z persistirá durante todo el tiempo que la necesitemos.
488 Piensa en Java
Como nota adicional, observe que cuando flavorSet( ) selecciona valores aleatoriamente, se encarga de comprobar qu e un
valor concreto no haya sido previamente seleccionado. Esto se hace en un bucle do que continúa realizando selecciones alea-
torias hasta que encuentre un valor que-no esté ya en la matri z picked (por supuesto, también podría haberse realizado una
comparación String para ver si el valor aleatorio está ya en la matri z results ). Si tiene éxito, añade la nueva entrada y loca-
li za la sigui ente (i se incrementa).
Puede ver analizando la salida que flavorSet() selecciona los valores en orden aleatorio cada una de las veces.
Ejercicio 2: ( 1) Escriba un método que tome un argumento int y devuelva una matri z de di cho tamaño rellenada con
objetos BerylliumSphere.
Matrices multidimensionales
Podemos crear fácilmente matrices multidimensionales. Para las matrices multidimensionales de primitivas, delimitamos
cada vector de la matriz mediante llaves:
JI: arrays/MultidimensionalPrimitiveArray.java
/1 Creación de matrices multidimensionales.
import java . util .* ¡
1* Output :
[[1 , 2, 31. [4, 5, 6]]
* /1/,-
Cada conjunto anidado de llaves nos desplaza al siguiente nivel de la matriz.
Este ejemplo utiliza el método Arrays.deepToString() de Java SES, que transfonna matrices multidimensionales en obje-
tos String, como podemos ver a la salida,
También podemos asignar una matri z con new. He aquí una matriz tridimensional asignada en una expresión Dew:
/1 : arrays/ThreeDWithNew.java
import java,util ,*¡
1* Output:
[1 [0, 0, 0, Ol. [0, 0, 0, O]l. [[0, 0, 0, O]. [0, 0, 0, 01]]
* ///,-
Podemos ver que los va lores primitivos de la matri z se inicializan automáticamente si no proporcionamos un valor de ini-
cialización explícito. Las matrices de objetos se inicializan con nuU,
Cada vector de las matrices que fonnan la matriz total puede tener cualquier longitud (esto se denomina matriz desigual):
11 : arrays/RaggedArray,java
import java,util, * ¡
System.out.println(Arrays.deepToString(a) ;
} / * Output,
[[), [[O), [o), [O I OI O, O]], [[), [O, O), [O, O)),
[ [O, O, O), [O), [O, O, O, O)), [(O, O, O), (O, O, O),
[O), [)), [[O), [), [O]))
* /// ,-
La primera instrucción new crea una matriz con un primer elemento de longitud aleatoria y el resto indeterminado. La
segunda instrucción new dentro del bucle for rellena los elementos, pero dejando el tercer índice indetenninado hasta que
nos encontramos con la tercera instrucción new.
Podemos tratar matrices de objetos no primitivos de una forma similar. En el siguiente ejemplo podemos ver cómo agrupar
varias expresiones new mediante llaves:
jj: arraysjMultidimensionalObjectArrays.java
import java . util.*¡
j * Output:
[[Sphere O, Sphere 1], [Sphere 2, Sphere 3, Sphere 4, Sphere 5],
[Sphere 6, Sphere 7, Sphere 8, Sphere 9, Sphere la,
Sphere 11, Sphere 12, Sphere 131]
*///,-
Podemos ver que spheres es otra matriz desigual, siendo la longitud de cada li sta de objetos diferente.
El mecanismo de conversión automática también funciona con los inicializad ores de matrices:
jj: arraysjAutoboxingArrays.java
import java.util.*¡
} 1* Output:
[[1, 2, 3, 4, 5, 6, 7, 8, 9, lO], [21, 22, 23, 24, 25, 26,
27, 28, 29, 301, [51, 52, 53, 54, 55, 56, 57, 58, 59, 601,
[7 1, 72, 73, 74, 75, 76, 77, 78, 79, 80]]
' ///0-
He aquí cómo podríamos construir por partes una matriz de objetos no primitivos:
11: arrays/AssemblingMultidimensionalArrays.java
II Creación de matrices multidimensionales.
import java.util.*;
public class AssemblingMultidimensionalArrays
public static void main(String[] args) {
Inceger [1 [1 a;
a == new Integer (3] [] ;
for {int i = O; i < a.length; i++ )
a [i] = new Integer [3] ;
for (int j O; j < a(i] .length; j++)
a [iJ [j J = i * j j II Conversión automática
System.out.println{Arrays.deepToString(a)) ;
/ * Output:
[[O, O, O], [O, 1, 2], [O, 2, 411
'/ // 0-
La expresión i*j sólo tiene por objeto asignar un valor interesante al objeto Integer.
El método Arrays.deepToString( ) funciona tanto con matrices de primitivas como con matrices de objetos:
/1 : arrays/MultiDimWrapperArray.java
II Matrices multidimensionales de objetos "envoltorio".
import java.util.*;
public class MultiDimWrapperArray
public static void main(String[] args)
Integer [] [] al = { /1 Conversión automática
{1,2,3,},
{4,5,6,).
};
Double [1 [1 [1 a2 { // Conversión automática
{ { 1.1, 2.2 }. { 3.3, 4.4 } } ,
{ { 5.5, 6.6 } , { 7.7, 8 . 8 } } ,
{ { 9.9, 1.2 } , { 2.3, 3.4 } }.
};
String [1 [1 a3 : {
{ IIThe ll , IIQuick ll , "S ly", "Fox" },
{ "Jumped", "Over ll } ,
{ tlThe", "Lazy", "Brown ll , IIDog", tland", tlfriend" },
};
System . out. println ( ti al: + Arrays.deepToString(al));
System. out. println (ti a2 : + Arrays.deepToString (a2));
System.out.println("a3: + Arrays.deepToString(a3));
1* Output:
aL [[1, 2, 3], [4, 5, 611
a20 [[[1.1, 2.2], [3.3, 4.41], [[5.5, 6.6], [7.7, 8.81],
[9.9, 1.2], [2.3, 3.4111
a3: [[The, Quick, Sly, Fox] , [Jumped, Over) , [The, Lazy,
Brown, Dog, and, friendll
' /// 0-
16 Matrices 491
De nuevo. en las matrices Integer y Double. el mecanismo de conversión automática de Java SES se encarga de crear por
nosotros los objetos envoltorio.
Ejercicio 3: (4) Escriba un método que cree- e inicialice una matriz bidimensional de valores double. El tamaño de la
matriz estará determinado por los argumentos del método y los valores de inicialización serán un rango
detenninado por sendos valores inicial y final que también serán argumentos del método. Cree un segun-
do método que imprima la matriz generada por el primer método. En main( ) compruebe los métodos
creando e imprimiendo varias matrices de tamaños diferentes.
Ejercicio 4: (2) Repita el ejercicio anterior para una matriz tridimensional.
Ejercicio 5: (1) Demuestre que las matrices multidimensionales de tipos primitivos se inicilizan automáticamente con
null.
Ejercicio 6: (l) Escriba un método que tome dos argumentos ¡ot, indicando las dos dimensiones de una matriz 2-D. El
método debe crear y rellenar una matriz 2-D de objetos Berylliul11Sphere de acuerdo con los argumentos
de dimensión.
Ejercicio 7: (1) Repita e l ejercicio anterior para una matriz 3-D.
Matrices y genéricos
En general, las matrices y los genéricos no se combinan demasiado bien. No se pueden instancia r matrices de tipos parame-
trizados:
Peel<Banana~(] peels = new Peel<Banana~(lOl i jj Ilegal
El mecanismo de borrado automático de tipos elim ina la infonnación del parámetro de tipo, y las matrices deben conocer
el tipo exacto que almacenan, con el fin de poder imponer los mecanismos de seguridad de tipos.
Sin embargo, lo que sí se puede es parametrizar el tipo de la propia matriz:
ji : arraysjParameterizedArrayType.java
class ClassParameter<T~ {
public T[] f (T [] arg) { return arg; )
class MethodParameter {
public static <T~ T(] f(T(] arg) return arg; }
11 : arrays / ArrayOfGenerics.java
II Es posible crear matrices de genéricos.
impo rt java.util. * ¡
Una vez que disponemos de una referencia a List<String> lI. podemos ver que se obtiene una cierta comprobación en tiem-
po de compilación. El problema es que las matrices son covariantes, por lo que una matri z List<String> 1I es también una
matri z ObjectrL y podemos utili zar esto para asignar un objeto ArrayList<Integer> a nuestra matri z, sin que se produzca
ningún error ni en ti empo de compilación ni en ti empo de ejecución.
Sin embargo, si sabemos que no vamos a efectuar nin guna generalización y nuestras necesidades son relativamente simples,
es posi ble crear una matriz de genéri cos, lo que nos proporciona una cierta comprobación de tipos básica en ti empo de com-
pilac ión. No obstante, casi siempre un contenedor genéri co será preferible a una matri z de genéri cos.
En general, nos encontraremos con que los genéricos son efecti vos en los límites de una clase o método. En los interiores,
el mecanismo de borrado de tipos suele hacer inutilizables los genéricos. De este modo, no podemos, por ejemplo, crear una
matriz de un tipo genérico:
11 : arrays / ArrayofGenericType.java
II Las matrices de tipos genéricos no se p ue d en compilar.
II Ilegal,
JI ! public c:U> Uf] makeArray( ) { r e turn ne w U[lO] ¡ }
111 ,-
16 Matrices 493
De nuevo, el mecanismo de borrado de tipos interfiere con nuestros propósitos; en este ejemplo, se intenta crear matrices
de tipos que se han visto sometidos al mecanismo de borrado de tipos y que son, por tanto, de tipo desconocido. Observe
que podemos crear una matriz de tipo Object, y proyectarla, pero si quitamos la anotación @S uppressWarnings obtendre-
mos una adve rtencia "no comprobada" en tiempo de compilación, porque la matriz no almacena rea lmente, ni comprueba
de fanna dinámica el tipo T. En otras palabras, si creamos una matriz Strin g ll. Java impondrá tanto en tiempo de compila-
ción como en tiempo de ejecución que sólo podemos colocar objetos String en dicha matriz. Sin embargo, si creamos una
matriz O bj ectf] , podemos almacenar en ella cualquier cosa menos tipos primitivos.
Ejercicio 8 : (1) Demuestre las afinnaciones del párrafo anterior.
Ejercicio 9: (3) Cree las clases necesarias para el ejemplo Peel<Bana na> y demuestre que el compilador no lo acep-
ta. Corrija el problema utilizando un contenedor ArrayL ist.
Ejercicio 10: (2) Modifique ArrayOfGeneri cs.j ava para emplear contenedores en lugar de matrices. Demuestre que
puede eliminar las advertencias de tiempo de compilación.
ArraysJiIIO
La clase A rrays de la biblioteca estándar de Java tiene un método fiU () bastante trivial: se limita a duplicar un mismo va lor
en cada posición o, en el caso de los objetos, inserta en cada posición copias de la misma referencia. He aquí un ejemplo:
JJ : arraysJFillingArrays.java
JJ Utilización de Arrays.fill()
import java.util.*¡
import static net.mindview.util.Print.*¡
/ * Output:
al [true, true, true, true, true, trueJ
a2 [11, 11, 11, 11, 11, 11J
a3 [x, x, x, x, x, xJ
a4 [17, 17, 17, 17, 17, 17J
a5 [19, 19, 19, 19, 19, 19J
a6 [23, 23, 23, 23, 23, 23J
a7 [29. O, 29 . O, 29.0, 29.0, 29 . 0, 29 . 0J
aS [47.0, 47.0, 47.0, 47.0, 47 . 0, 47 . 0J
a9 [HelIo, HelIo, HelIo, HelIo, HelIo, HelIo]
a9 [HelIo, HelIo, HelIo, World, Wor l d, HelIo]
* /// , -
Podemos rellenar la matriz completa o, como muestran las dos últimas instrucciones, rellenar tan solo un rango de elemen-
tos. Pero como sólo se puede invocar Arrays.fill( ) con un único valor de datos, los resultados no son especialmente útiles.
Generadores de datos
Para crear matrices de datos más interesantes, pero de una manera flexible utilizaremos el concepto de Ge nerador introdu-
cido en el Capítulo 15, Genéricos. Si una herramientas utiliza un objeto Generator, podemos generar cualquier clase de
datos eligiendo el objeto Ge nerator adecuado (se trata de un ejemplo del patrón de di seño basado en estrategia), cada un o
de los diferentes generadores representa una estrategia diferente.]
En esta sección proporcionaremos algunos generadores y, como ya hemos visto en ejemplos anteriores, también podemos
definir fácilmente otros generadores que deseemos.
En primer lugar, he aquí un conjunto básico de generadores de recuento para todos los tipos envoltorio de primitivas y para
las cadenas de caracteres. Las clases generadoras están anidadas dentro de la clase CountingGenerator, de modo que pue-
den utilizar el mismo nombre que los tipos de objeto que estén generando; por ejemplo, un generador que cree obj etos de
tipo Integer podría generarse mediante la ex presión new CountingGcnerator.lnteger( ):
11: net/mindview/util/CountingGenerator.java
II Implementaciones simples de generadores .
package net.mindview . util;
I Si bicn hay quc recalcar que en este tema las fronteras resultan un tanlo borrosas. También podría mos argumentar quc un objeto Generator representa
el patrón de diseño de Comando: sin embargo. en mi opinión, la tarea consiste en rellenar una matTiz y el objeto Generator lleva a cabo parte de dicha
tarea, así que se trata mas de una estrategia que de un comando.
16 Matrices 495
}
///0-
Cada clase implementa su propio significado del términ o " recuento". En el caso de CountingGe nerator.Character, se trata
simplemente de las letras mayúsculas y min úsculas repetidas un a y otra vez. La clase CountingGcnerator.String uti liza
496 Piensa en Java
CountingGenerator.Character para rellenar un a matriz de caracteres, que luego se transfonna en un objeto de tipo String.
El tamaño de la matriz está detenl1inado por el argu mento del constructor. Observe que CountingGenerator.String utili za
un objeto Generator<java.lang.Character> básico en lugar de una referencia específica a CountingGenerator.
Character. Posteriormente, este generado r puede sustituirse para gene rar RandornGenerator.String en
RandornGcnerator.java.
He aquí una herramienta de prueba que utiliza el mecanismo de reflexión con la sintaxi s de generadores an idados, de mane-
ra que pueda utilizarse para probar cualquie r conj unto de generadores que se adapten a esta estmctura:
1/: arrays/GeneratorsTest.java
import net.mindview.util.*¡
1* Output:
Double, 0 . 0 1.0 2 . 0 3 . 0 4.0 5.0 6.0 7 . 0 8.0 9.0
Float, o . O 1. O 2. O 3 . O 4. O 5. O 6 . O 7. O 8 . O 9. O
Long: O 1 2 3 4 5 6 7 8 9
Integer: O 1 2 3 4 5 6 7 8 9
Short: O 1 2 3 4 5 6 7 8 9
String: abcdefg hijklmn opqrstu vwx yzAB CDEFGHI JKLMNOP QRSTUVW XYZabcd efghijk lmnopqr
Character: a b c d e f 9 h i j
Byte , O 1 2 3 4 5 6 7 8 9
Boolean: true false true false true false true false true false
* ///,-
Esto presupone que la clase que estemos probando contiene un conjunto de objetos Generator anidados, cada uno de los
cuales dispone de un constructor predeterminado (sin argumentos). El método de reflexión getClasses( ) genera todas las
clases anidadas. Entonces, el método test() crea una instancia de cada un o de estos generadores e im prime el res ultado pro-
ducido invocando next( ) diez veces.
He aquí un conjunto de objetos Generator que utilizan el generador de números aleatorios. Puesto que el constructor
Random se inicial iza con un valor constante, la salida es repetible cada vez que ejecutemos un programa empleando un o
de estos generadores:
11 : net/mindview/util/RandomGenerator.java
/1 Generadores que producen valores aleatorios.
package net.mindview.util¡
import java . util.*;
Puede ver que RandomGenerator.String hereda de Counti ngG enerator.String y simplemente inserta el nuevo ge nerador
de tipo Character.
Para generar números que no sean demasiado grandes, RandomGenerator.Integer utiliza de fonna predetenninada un
módulo igual a 10.000, pero el constructor sob recargado nos pennite elegir un valor más pequeii.o. La misma técnica se usa
para RandornGenerator.Long. Para los generadores de tipo Float y Double. los valores situados detrás de l punto decimal
se recortan.
Podemos reutili zar GeneratorsTest para probar RandomGenerator:
11 :arrays/RandomGeneratorsTest.java
import net.mindview.util.*;
1* Output:
Doub1e. 0 . 73 0.53 0.16 0.19 0.52 0.27 0 . 26 0.05 0 . 8 0.76
F1oat. 0.53 0.16 0.53 0.4 0.49 0 . 25 0.8 0.11 0.02 0 . 8
Long . 7674 8804 8950 7826 4322 896 8033 2984 2344 5810
Integer: 8303 3 14 1 7138 6012 9966 8689 7185 6992 5746 3976
Short: 3358 20592 284 26791 12834 -8092 13656 29324 -1423 5327
String: bklnaMe sbtWHkj UrUkZPg wsqPzDy CyRFJQA HxxHvHq
XumcXZJ oogoYWM NvqeuTp nXsgqia
Character : x x E A J J m z M s
Byte. -60 -17 55 -14 -5 115 39 -37 79 115
Boolean: false true false false true true true true true true
* /// .-
Podemos modificar el número de valores producidos cambiando el va lor GeneratorsTest.size, que es de tipo public.
La clase CollectionData se definirá en el Capítulo 17, Análisis detallado de los contenedores. Esta clase crea un objeto
ColJection reUeno con elementos producidos por el generador gen. El número de elementos está detenninado por el segun-
do argumento del constmctor. Todos los subtipos de CoUection tienen un método toArray() que rellena la matriz argumen-
to con [os elementos del objeto eollection.
El segundo método emplea el mecanismo de reflexión para crear dinámicamente una matriz del tipo y tal11ai10 apropiados.
Entonces esta matriz se rellena empleando la misma técnica que con el primer método.
Podemos probar Generated utilizando una de las clases CountillgGenerator definidas en la sección anterior:
1/ : arrays/TestGenerated.java
import java.util.-;
import net.rnindview.util.*¡
1* Output:
[9, 8, 7, 6)
[O, 1, 2, 3)
[O, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
* /// ,-
Aún cuando la matriz a está inicializada, dichos va lores se sobreesc riben al pasarla a través de Generated .array(), que sus-
tinlye los va lores (pero deja la matriz original en su lugar). La inicialización de b muestra cómo puede crearse una matriz
rellena partiendo de cero.
Los genéricos no funcionan con va lores primitivos, y queremos utilizar los generadores para rellenar matrices de primiti-
vas. Para resolver este problema, creamos un convertidor que toma cualquier matriz de objetos envoltorio y la convierte en
una matriz de los tipos primitivos asociados. Sin esta herramienta, tendríamos que crear generadores especiales para todas
las primitivas.
11 : net/mindview/util/ConvertTo.java
package net.mindview.util¡
return result;
He aqu í un ejemplo que muestra cómo puede utili zarse ConvertTo con ambas versiones de Generated.array( ):
jj : arraysjPrimitiveConversionDemonstration.java
import java.util .* ;
import net.mindview.util .* ;
/ * Output:
[O, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
[true, false, true, false, true, false, truel
*///,-
Finalmente, he aquí un programa que prueba las herramientas de generación de matrices utilizando clases
RandomGenerator:
JI: arraysjTestArrayGeneration.java
JI Comprobar las herramientas que utilizan generadores
/1 para rellenar matrices .
impert java.util.*;
import net.mindview.util.*¡
import static net.mindview.util.Print.*¡
public class TestArrayGeneration {
public static void main(String[] argsl
int size = 6;
boolean(] al = ConvertTo.primitive{Generated.array(
Boolean.class, new RandomGenerator.Boolean(), size)) i
print ("al = 11 + Arrays. toString (al));
byte[] a2 = ConvertTo.primitive(Generated.array(
Byte.class, new RandomGenerator.Byte(), size)) i
print ( OI a2 = 01 + Arrays. toString (a2) ) i
char[] a3 = ConvertTo.primitive(Generated.array(
Character.class,
new RandomGenerator.Character(), size)} i
print (01 a3 = 01 + Arrays. toString (a3) ) ;
short[] a4 = ConvertTo.primitive(Generated.array(
Short.class, new RandomGenerator.Short(), size));
print ( OI a4 = 01 + Arrays. toString (a4) ) ;
int[] aS = ConvertTo.primitive(Generated.array(
Integer.elass, new RandomGenerator.lnteger(), size)) i
print(OIaS = JI + Arrays.toString(aS))¡
long[] a6 = ConvertTo.primitive(Generated.array(
Long . class, new RandomGenerator.Long(), size)) ¡
print ( OI a6 = 01 + Arrays. toString (a6) } i
float[] a7 = ConvertTo.primitive(Generated.array(
Float.elass, new RandomGenerator.Float(}, size));
print( OI a7 = n + Arrays.toString{a7}) i
double[] aS = ConvertTo .primitive {Generated.array(
Double.class, new RandomGenerator.Double(}, size));
print (" aS = ti + Arrays. toString (aS) ) i
/* Output:
al (true, false, true, false, false, true]
a2 [104, -79,
-76, 126, 33, -641
a3 (Z, n, T, e, Q, r]
a4 [-13408, 22612, 15401, 15161, -28466, -126031
a5 [7704, 7383, 7706, 575, 8410, 63421
a6 [7674, 8804, 8950, 7826, 4322, 8961
a7 [0.01, 0.2, 0.4, 0 .79, 0.27, 0.451
a8 [0.16, 0.87, 0.7, 0.66, 0 .87, 0.591
*///,-
Esto garantiza tamb ién que cada versión de ConvertTo.primitive() funcione correctamente.
Ejercicio 11: (2) Demuestre que el mecanismo de conversión automát ica (autoboxing) no funciona con las matrices.
Ejercicio 12: (1 ) Cree una matriz inicializada de valores double utili zando CountingGenerator. Imprima los resul-
tados.
502 Piensa en Java
/ * Output:
i [47, 47, 47, 47, 47, 47, 47J
j [99, 99, 99, 99, 99, 99, 99, 99, 99, 99J
j [47 , 47, 4 7, 47, 47, 47, 47, 99, 99, 99J
k [47, 47, 47 , 47, 4 7J
i [103, 103, 103, 103, 103, 47, 47J
u [47, 47, 47, 47, 47, 47, 47, 47, 47, 47J
v [99, 99, 99, 99, 99J
u [47, 4 7,47, 47 , 4 7, 99, 99 , 99, 99, 99J
* /// , -
Los argumentos de arraycopy() son la matriz de origen, o el desplazamiento dentro de la matriz de ori gen a partir del cual
hay que empezar a copiar, la matriz de destino, el desplazamiento dentro de la matriz de destino donde debe empezar la
copia y el número de elementos que hay que copiar. Naturalmente, cualquier violación de las fronteras de las matrices gene-
rará una excepción.
El ejemplo muestra que pueden copiarse tanto matrices de primitivas como matrices de objetos. Sin embargo, si copiamos
matrices de objetos, entonces sólo se copian las referencias, no produciéndose ninguna duplicación de los propios objetos.
Esto se denomina copia superjicial (consulte los suplementos en línea del libro para conocer más detalles).
System.arraycopy() no realiza conversiones automáticas para los tipos primitivos: las dos matrices deben tener exactamen-
te el mismo tipo.
Ejercicio 18: (3) Cree y rellene una matriz de objetos BerylliumSphere. Copie esta matriz en otra nueva y demuestre
que se trata de una copia superficial.
Comparación de matrices
Arr.ys proporciona el método equals( ) para comprobar si dos matrices son iguales; dicho método está sobrecargado para
poder trabajar con todos los tipos de primitivas y con Object. Para ser iguales, las matrices deben ten er el mismo número
de elementos y cada elemento tiene que ser equivalente al elemento correspondiente de la otra matri z, utili zándose el méto-
do equals() para cada elemento (para las primitivas, se utiliza el método equals( ) de la correspondiente clase en voltorio,
po r ej emplo, Integer.equals() para in!). Por ejemplo:
11 : arrays / ComparingArrays.java
II Utilización de Arrays . equals( )
import java . util . *;
import static net.mindview . util . Print .* ;
/ * Output:
true
false
true
* ///,-
Originalmente, a 1 y a2 son exactamente iguales, por lo que la salida es "true", pero después se cambia uno de los elemen-
tos, lo que hace que el resultado sea "false". En el último caso, todos los elementos de sI apuntan al mismo objeto, pero 52
tiene cinco objetos diferentes. Sin embargo, la igualdad de matrices está basada en los contenidos (a través de
Object.equals( )), por lo que el resultado es "true".
Ejercicio 19: (2) Cree una clase con un campo int que se inicialice a partir de un argumento de un CQnstruclOr. Cree dos
matrices de estos objetos, utilizando valores de inicialización idénticos para cada matriz, y demuestre que
Arrays.equals() dice que son distintas. Ailada el método equals() a la clase para corregir el problema.
Ejercicio 20 : (4) Ilustre mediante un ejemplo el uso de deepEquals() para matrices multidimensionales.
2 Design Paltems, Erich Gamma el al. (Addison·Wesley, 1995). Consulte Thinking in Pattems (n-íth Java) en IVlvw.MindViev.énel.
16 Matrices 505
if{count++ % 3 == O)
result += "\n H ¡
return result;
/ * Output:
befare sorting:
[[i = 58, j = 55], [ i = 9 3 , j 61], [ i = 6 1 , j = 2 9 ]
[i 68, j O], [i = 22, j 7], [i = 88, j = 28]
[i 51, j 89] , [i 9, j 78], [i = 98, j = 61]
[i 20 I j 58] , [i = 16, j = 40], [i = 11, j = 22]
after sorting:
[[i = 9, j 78], [i = 11, j 22], [i = 16, j = 40]
[i 20, j 58], [i 22, j 7], [i = 51, j = 89]
[i 58, j 55], [i 61, j 29], [i 68, j O]
[i 88, j 28], [i 93, j 61], [i = 98, j = 61]
* /// >
Cuando definimos el método de comparación somos responsables de decidir qué quiere decir comparar uno de nuestros
objetos con otro. Aquí, sólo se utilizan los valores i para la comparación, mientras que los valores j se ignoran .
El método generator( ) produce un objeto que implementa la interfaz Generator creando una clase interna anónima. Ésta
construye objetos Comp1)rpe inicializándolos con valores aleatorios. En main(), se utiliza el generador para rellenar una
matriz de objetos CompType, que se ordena a continuación. Si no hubiéramos implementado Comparable obtendríamos
una excepción ClassCastException en tiempo de ejecución cuando tratáramos de invocar sort(). Esto se debe a que sort()
proyecta su argumento sobre Comparable.
Ahora suponga que alguien nos entrega una clase que no implementa Comparable, o que alguien nos entrega una clase que
sí que implementa Comparable, pero decidimos que no nos gusta la fonna en que funciona y que preferiríamos disponer
de un método de comparación distinto para ese tipo de objeto. Para resolver el problema, creamos una clase separada que
implementa una interfaz llamada Comparator (ya la hemos presentado brevemente en el Capítulo 11 , Almacenamiento de
objetos). Se trata de un ejemplo del patrón de diseño basado en Estrategia. Tiene dos métodos, compare( ) y equals(). Sin
embargo, no tenemos necesidad de implementar equals() salvo por necesidades especiales de rendimiento, ya que cada vez
que se crea una clase, ésta hereda implícitamente de Object, que tiene un método equals(). Por tanto, podemos limitamos
a utilizar el método eq uals() predetenninado de Object sin por ello dejar de satisfacer las imposiciones de la interfaz.
La clase Collections (que examinaremos con más detalle en el siguiente capítulo) contiene un método reverseOrder() que
produce un objeto Comparator para invertir el sistema natural de ordenación. Esto puede aplicarse a CompType:
506 Piensa en Java
1* Output:
before sorting:
[ [i ~ 58, i ~ 551 , [i ~ 93, i 611 , [i ~ 61, i ~ 291
[i 68, i 01 , [i ~ 22, i 71 , [i ~ 88, i ~
281
[i 51, i 891 , [i 9, i 781, [i ~
98, i ~
611
[i 20, i 581 , [i ~
16, i ~
401 , [i ~
11, i ~
221
after sorting:
[ [i ~ 98, i ~
611 , [i ~
93, i ~
611, [i 88, i 281
[i 68, i 01 , [i ~
61, i ~
291 , [i 58, i 551
[i 51, i 891 , [i 22, i 71 , [i 20 , i 581
[i 16, i 40] , [i 11, i 221 , [i ~ 9, i 781
* ///,-
También podemos escribir nuestro propio objeto Comparator. El siguiente ejemplo compara objetos CompType basados
en sus valores j , en lugar de en sus valores i:
11 : arraysjcomparatorTest.java
II Implementación de un comparador para una cl a s e .
import java.util .* ¡
import net . mindv i ew . util .* ;
import s t atic net . mindview . util.Print. * ;
1* Output :
befare sorting :
[[i ~ 58, i ~ 551, [i 93, i 611, [i ~ 61, i ~ 291
[i ~ 68, i ~ 01, [i 22, i 71, [i ~ 88, i ~ 281
16 Matrices 507
after sorting:
[ [i = 68, i = O] , [i = 22, i 7] , [i = 11, i = 22]
[i 88, i 28]. [i 61, i = 29] , li = 16, i = 40]
li 58, i 55] , [i 20, i = 581 , li = 93, i = 61]
li 98, 61] , [i 9, i = 78] , li = 51, i = 89]
* ///0-
Ejercicio 21: (3) Trate de ordenar una matri z de objetos del Ejercicio 18. Implemente Comparable para corregir el pro-
blema. Ahora cree un objeto Comparator para disponer los objetos en orden inverso.
/* Output:
Before sort: (YNzbr, nyGcF, OWZnT, cQrGs, eGZMm, JMRoE,
suEcU, OneOE, dLsmw, HLGEa, hKcxr, EqUCB, bklna, Mesbt,
WHkiU, rUkZP, gwsqP, zDyCy, RFJQA, HxxHv]
After sort: (EgUCS, HLGEa, HxxHv, JMRoE, Mesbt, OWZnT,
OneOE, RFJQA, WHkjU, YNzbr, bklna, cQrGs, dLsmw, eGZMm,
gwsgP, hKcxr, nyGcF, rUkZP, suEcU, zDyCy]
Reverse sort: (zDyCy, suEcU, rUkZP, nyGcF, hKcxr, gwsgP,
eGZMm, dLsmw, cQrGs, bklna, YNzbr, WHkjU, RFJQA, OneOE,
OWZnT, Mesbt, JMRoE, HxxHv, HLGEa, EgUCB]
Case-insensitive sort: [bklna, cQrGs, dLsmw, eGZMm, EgUCB,
gwsqP, hKcxr, HLGEa, HxxHv, JMRoE, Mesbt, nyGcF, OneOE,
OWZnT , RFJQA, rUkZP, suEcU, WHkjU, YNzbr, zDyCy)
* ///,-
Una de las cosas que observamos al analizar la salida en los algoritmos de ordenación para objelos String es que la ordena-
ción es lexicográfica, por lo que se ponen primero ladas las palabras que comienzan por letras mayúsculas y después todas
las palabras que comienzan con minúscula. Si queremos agrupar las palabras con independencia de si las letras son mayús-
culas o minúsculas, se utiliza String,CASE_INSENS IT IVE_ORDER, como se muestra en la última llamada a sort() del
ejemplo anterior.
3 Sorprendentemente, Java 1.0 y J.l no incluían ningÍln soporte para la ordenación de objetos String.
508 Piensa en Java
El algoritmo de ordenac ión que se utiliza en la biblioteca estándar de Java está diseñado para comportarse de manera óp-
tima para el tipo concreto que se esté ordenando: un algoritmo Quicksort para primitivas y un a ordenación estable por com-
binación para objetos. No es necesario preocuparse acerca de las cuestiones de rendimiento a menos que la herramienta de
perfilado nos indique que el proceso de ordenación está actuando como un cuello de botella.
1* Output:
Sorted array' [128, 140, 200, 207, 258, 258, 278, 288, 322, 429, 511, 520, 522, 551, 555,
589,693,704,809,861,861,868,916,961,9981
Location of 322 is 8, a[8] = 322
*///,-
En el buc le while, se generan elementos de búsqueda con valores aleatorios hasta que se encuentra uno de ellos.
Arrays.binarySea rch( ) devuelve un valor mayor o igual que cero si se encuentra el elemento que se está buscando. En
caso contrario, devuelve un valor negativo que representa el lugar en el que debería insertarse el elemento, si se está man-
teniendo la ordenación de la matriz de forma manual. El valor devuelto es:
- (punto de inserción) - 1
El punto de inserción es el índice del primer elemento superi or a la cla ve, o bien a.size(), si todos los elementos de la matri z
son inferiores a la clave especi ficada .
Si una matriz contiene elementos dupli cados, no hay ninguna garantía en lo que respecta a cuál de esos duplicados se encon-
trará. El algoritmo de búsqueda no está diseñado para soportar elementos duplicados, aunque sí que los tolera. Si necesita-
mos un a lista ordenada de elementos no duplicados, hay que emplear TreeSet (para mantener la ordenación) o
LinkedHashSet (pa ra mantener el orden de inserción). Estas clases se encargan de resolver por nosotros todos los detalles,
de manera automática. Sólo en aquellos casos en los que aparezcan cuellos de botella de rendimiento debería sustituirse una
de estas clases por una matriz cuyo mantenimiento se realizará de forma manual.
16 Matrices 509
Si ordenamos una matri z de objetos utilizando un objeto Comparator (las matrices primitivas no penniten realizar ordena-
ciones con un objeto Comparator), hay que incluir ese mismo objeto Comparator cuando se invoque a binarySearch()
(empleando la ve rsión sobrecargada de este método). Por ejemplo, podemos modificar el programa StringSorting.java para
realizar una busqueda:
JI: arrays/AlphabeticSearch.java
JI Búsqueda con un comparador .
import java . util .*;
import net.mindview.util.*¡
/ * Output:
[bklna, cQrGs, cXZJo, dLsmw, eGZMm, EqUC8, gwsqP, hKcxr, HLGEa, HqXum, HxxHv, JMRoE,
JmzMs, Mesbt, MNvqe, nyGcF, ogoYW, OneOE, OWZnT, RFJQA, rUkZP, sgqia, slJrL, suEcU,
uTpnX, vpfFv, WHkjU, xxEAJ, YNzbr, zDyCy]
Index: la
HxxHv
*///>
El objeto Comparator debe pasarse como tercer argumento al método binarySearch( ) sobrecargado. En este ejemplo, el
éxito de la operación de búsqueda está garantizado porque el elemento de búsqueda se selecciona a partir de la propia matri z.
Ejercicio 22 : (2) Demuestre que los resultados de reali zar una búsqueda binaria con binarySearch( ) eo una matriz
desordenada son impredecibles.
Ejercicio 23: (2) Cree una matri z de objetos (nteger, relléoela con valores int aleatorios (utili zando conversión automá-
tica) y dispóngala en orden inverso utili zando Comparator.
Ejercicio 24: (3) Demuestre que se pueden realizar búsquedas en la clase del Ejerci cio 19.
Resumen
En este capítulo, hemos visto que Java proporciona un soporte razonable para las matrices de tamaño fijo y bajo nivel. Este
tipo de matri z pone énfasis en el rendimiento más que en la flexibilidad, de fonna similar al modelo de matrices de C y C++.
En la versión inicial de Java, las matrices de tamaño fijo y bajo ni vel eran absolutamente necesarias, no sólo porque los dise-
ñadores de Java el igieron incluir tipos primitivos (también por razones de rendimiento), sino también porque el soporte para
contenedores en dicha versión era mínimo. Por esa razón, en las primeras versiones de Java siempre era razonable elegir las
matrices como fonna de implementación.
En las versiones subsiguientes de Java, el soporte de contenedores mejoró significativamente y ahora los contenedores ti en-
den a ser preferibles a las matrices desde todos los puntos de vista, salvo el de rendimiento. Incluso en lo que al rendimien-
to respecta, el de los contenedores ha mejorado significativamente. Como hemos indicado en otros puntos del libro, los
problemas de rendimiento nunca suelen presentarse, de todos modos, en los lugares donde nos los imaginamos.
Con la adición del mecanismo de conversión automático de tipos primiti vos y del mecanismo de genéricos, resulta muy sen-
cillo almacenar primitivas en los contenedores, lo que constituye un argumento ad icional para sustituir las matrices de bajo
nivel por contenedores. Puesto que los genéricos penniten producir contenedores seguros en lo que respecta a los tipos de
obj etos, las matrices han dejado también de suponer una ventaja a ese respecto.
Como se ha indicado en este capítulo, y como podrá ver cuando trate de utili zarlos, los genéricos resultan bastante hostiles
en lo que a las matri ces se refiere. A menudo, incluso cuando conseguimos que los genéricos y las matrices funcionen con-
510 Piensa en Java
juntamente de alguna manera (como veremos en el siguiente capítulo). seg uimos teniendo advertencias "no comprobadas"
durante la compilación.
En muchas ocasiones, los disei'ladores del lenguaje Java me han dicho directamente que se deberían utilizar contenedores
en lugar de matrices; esos comentarios surgían a la hora de discutir ejemplos concretos (yo he estado empleando matrices
para ilustrar técnicas específicas y no tenía, por tanto, la opción de utilizar contenedores).
Todas estas cuestiones indican que los contenedores son preferibles, por regla general. a las matrices a la hora de programar
con las versiones recientes de Java. Sólo cuando esté demostrado que el rendimiento constituye un problema (y que utilizar
una matriz pemlitirá resolverlo) deberíamos recurrir a la utilización de matrices.
Puede ser una afinnación demasiado categórica, pero algunos lenguajes no disponen en absoluto de matrices de tamaño fijo
y bajo nivel. Sólo disponen de contenedores de tamaño variable con una funcionalidad significativamente superior a la de
las matrices estilo C/e ++/Java. Python,4 por ejemplo, tiene un tipo list que utiliza la sintaxis básica de matrices, pero dis-
pone de una funcio nalidad muy superior; podemos incluso heredar a partir de ese tipo:
#: arraysjPythonLists.py
aList = [1, 2, 3, 4, 5]
print type (aList) # <type 'list' >
print aList # [1, 2, 3, 4, 5]
print aList[4] # 5 Indexación básica de la lista
aList.append(6) # Ae puede cambiar el tamaño de las listas
aList += [7, 8] # Añadir una lista a una lista
print aList # [1, 2, 3, 4, 5, 6, 7, 8]
aSlice = aList[2:4]
print aSlice # [3, 4]
La sintaxis básica de Python se ha presentado en el capítulo anterior. En este ejemplo, se crea una lista simplemente inser-
tan do una secuencia de objetos separados por comas entre corchetes. El resultado es un objeto cuyo tipo en tiempo de eje-
cución es list (la salida de las instrucciones print se muestra en fonna de comentarios en la misma línea). El resu ltado de
impri mi r un objeto Iist es el mismo que si utilizáramos Arrays.toString( ) en Java.
La creación de una subsecuencia de un obj eto list se lleva a cabo a partir de los "fragmentos", incluyendo el operador ' :'
dentro de la operación de índice. El tipo Iist tiene muchas otras operaciones predefinidas.
MyL ist es una defi ni ción de class ; las clases base se indican entre paréntesis. Dentro de la clase, las instrucciones def espe-
ci fi can métodos y el primer argumento al método es automáti camente equivalente a this en Java, salvo porque en Python es
ex plícito y se utiliza el identifi cador self por conveni o (no se trata de una palabra clave). Observe que el constructor se bere-
da automáticamente.
Aunque todo en Pytbon es realmente un objeto (incluyendo los tipos enteros y de coma flotante), seguimos disponiendo de
una puerta de escape, en el sentido de que se pueden optimizar partes del código cuyo rendimiento sea crítico escribiendo
extensiones en e, e++ o con una herramientas especial llamada Pyrex, que está diseñada para acelerar fácilmente la ej ecu-
ción del códi go. De esta fonna, podemos mantener la pureza de la orientación a objetos sin que ello nos impida realizar
mejoras de rend imiento.
4 Vease www.Pylhon.org.
16 Matrices 511
El lenguaje PHp5 va un paso más allá toda\ ía al disponer de un único tipo de matriz, que actúa tanto como una matriz con
índices de tipo entero cuanto como una matriz asociativa (un mapa).
Resulta interesante preguntarse después de todos estos años de evolución del lenguaje Java, si los diseñadores incluirían las
primitivas y las matrices de bajo nivel en el lenguaje si tuvieran que comenzar de nuevo con la tarea de diseño. Si se deja-
ran estas funcionalidades fuera del lenguaje, sería posible construir un lenguaje orientado a objetos verdaderamente puro (a
pesar de lo que se diga, Java no es un lenguaje orientado a objetos puro, precisamente debido a los remanentes de bajo nivel).
El argumento de la eficiencia siempre parece bastante convincente, pero el paso del tiempo ha ido mostrando una evolución
en el sentido de alejarse de esta idea para utilizar componentes de más nivel, como los contenedores. Y hay que tener en
cuenta, que si se pudieran incluir los contenedores en el cuerpo principal del lenguaje, como sucede en otros lenguajes,
entonces el compilador dispondria de mejores oportunidades para mejorar el código.
Dejando aparte estas especulaciones teóricas, 10 cierto es que las matrices siguen estando presentes y que nos encontrare-
mos con ellas una y otra vez a la hora de leer código. Sin embargo, los contenedores son casi siempre una mejor opción.
Ejercicio 25: (3) Reescriba Python Lis ts.py en Java.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico rile rhinking in Jav(I A/ll/orafed Sollltio1/ Cuide, disponible para
la venta en w\\w.MindJ1ew.1IC'I.
s Véase www.php.net.
Análisis detallado
de los contenedores
En el Capítulo 11, Almacenamiento de objetos, helLos introducido los conceptos y la
func ionalidad básica de la bibl ioteca de contenedores de Java, y aquellas exp licaciones son
suficientes para poder empezar a utilizar contenedores. En este capítulo se analiza dicha
importante biblioteca con más profundidad.
Para poder sacar el máximo contenido de la biblioteca de contenedores necesitamos conocer más detalles de los que se pre-
sentaron en el Capítulo 11 , Almacenamiento de los objetos, pero el material que en este capítulo se presenta depende de
temas más avanzados (como los genéricos), lo cual es la razón de que se pospusiera hasta este momento.
Después de incluir una panorámica completa de los contenedores veremos cómo funcionan los mecanismos de hash y cómo
esc ribir métodos hashCode() y eq uals() que funcionen con los contenedores a los que se les haya aplicado un mecanismo
de hasJ¡. Veremos también por qué hay diferentes vers iones de algunos contenedores y qué criterios usar para elegir una ver-
sión u otra. El capínllo finaliza con una exploración de las utilidades de propósito general y de una serie de clases especiales.
r-----~----, .________ i_ •
• -------. ._L_ • • _l_ .. • _.J __ • : AbstractMap :
'Listlterator ,..-_ql!n-e!·L-' List'
, , "
I
I
Set , ,'Queue ,'
I
-- ---• • _ L __ ______ •
.- - ---- - -
• _____________ ~q --- ·A-e,
)J,
• " ____ 4
: SortedMap
..~..
._----("---
: AbstraetCollection : ~~ .. ~
. --.& ---------~t>.
,,1. __ J_ ----
" : SortedSet :
.
.__ L_____ .,:.:' \ ____ /~ __ .~---- ____ 4
: AbstractList:
.--e,--- - ----
: AbstraetSet : I HasMap I I TreeMap I
f /ldentilYHaShMap I
I LinkedHashMap I Hashtable I
IWeakHashMa pi (heredado)
Colleetions
Arrays
Relleno de contenedores
Aunque el problema de imprimir contenedores está resuelto, el proceso de rellenar contenedores sufre la misma deficiencia
que java.util.Arrays. Al igual que con Arrays, existe una clase de acompañamiento denominada CoUections que contiene
métodos de utilidad estáticos, incluyendo uno que se llama ftll() Y que sirve para rellenar colecciones. Al igual que la ver-
sión de Arrays, este método fill( ) se limita a duplicar una única referencia a objeto por todo el contenedor. Además. sólo
funciona para objetos List, aunque la lista resultante puede pasarse a un constructor o a un método addAI1( ):
11: containers/FillingLists.java
11 Los métodos Collections.fill() y Collections.nCopies () .
import java.util.*¡
class StringAddress
private String Si
public StringAddress (String s) { this. s Si}
public String toString ( ) {
return super. toString ( ) + 11 + S;
/ * Output, ISample)
[StringAddress@82ba41 HelIo, StringAddress@82ba41 HelIo,
StringAddress@82ba41 HelIo, StringAddress@82ba41 HelIo]
[StringAddress@923e30 World!, StringAddress@923e30 World!,
StringAddress@923e30 World!, StringAddress@923e30 World!J
*///,-
17 Análisis detallado de los contenedores 515
Este ejemplo muestra dos formas de rellenar un objeto Collection con referencias a un único objeto. La primera,
Collections.nCopies( ), crea un objeto List que se pasa al constmctor; el cual rellena el objeto ArrayList.
El método toString() en StringAddress llama a Object.toString(), que genera el nombre de la clase seguido por la repre-
sentación hexadecimal sin signo del código hash del objeto (generada por un método hashCode( ».
Podemos ver. analizan-
do la salida, que todas las referencias apuntan al mismo objeto y éste también se cumple después de invocar un segundo
método, Collections.fill( J. El método fill () es todavía menos útil debido al hecho de que sólo puede sustituir elementos que
ya se encuentren denlro del objeto List no pennitiendo añadir nuevos elementos.
Este ejemplo utiliza Generator para insertar en el contenedor tantos objetos como necesitemos. El contenedor resultante
puede entonces pasar al constructor de cualquier tipo Collection, y dicho constructor copiará los datos dentro de la instan-
cia. También puede utilizarse el método addAU() que fonma parte de todos los subtipos de Collection para rellenar un obje-
to Collection existente.
El método genérico de utilidad reduce la cantidad de texto que hay que escribir a la hora de uti lizar la clase.
CollectionData es un ejemplo del patrón de dise.'o A daptador I ; adapta un objeto Generator al constructor de un tipo
Collecti on.
He aquí un ejemplo que inicializa un contenedor LinkedHashSet:
11: containers/CollectionDataTest.java
import java.util. *;
import net.mindview.util. *¡
1 Puede que esto no encaje estrictamente en la defmición de adaptador, tal como se define en el libro de Design Pallerns , pero creo que cumple con el espi-
ritu de este palrón.
516 Piensa en Java
/* Output:
[strange, women, lying, in, ponds, distributing, swords,
is, no, basis, tor, a, system, of, government]
* /// : -
Los elementos están en el mismo orden en que se insertaron, porque un contenedor LinkedH as hSet mantiene una lista enla-
zada que preserva el orden de inserc ión.
Todos los gene radores definidos en el e ap índo 16, Matrices, están ah ora disponibles a través del adaptador CollectionData.
He aquí un ejemplo donde se utili zan dos de ellos:
1/ : containersjCollectionDataGeneration . java
/1 Utilización de los generadores definidos en el Capítulo 1 6, Matrices .
import java . util.*¡
import net.rnindview.util. * ¡
/ * Output:
(YNzbrnyGc, FOWZnTcQr, GseGZMmJM, RoEsuEcUO, neOEdLsmw,
HLGEahKcx, rEqUCBbkI, naMesbtWH, kjUrUkZPg, wsqPzDyCyl
[573,4779,871,4367,6090,7882,2017,8037,3455,299]
*///:-
La longitud de la cadena de caracteres producida por Ra nd ornGe ner ator.Slr ing se controla mediante el argumento del
constructor.
Generadores de mapas
Podemos usar la misma técni ca para un objeto M ap, pero eso requiere una clase Pair (par), ya que es necesario produ cir
una parej a de objetos (una clave y un va lor) en cada llamada al método next( ) de un generador, con el fin de rellenar un
conlenedor Ma p:
1/: net/rnindview/util/Pair.java
package net . mindview.util¡
El adaptador pa ra Map puede ahora utilizar disti ntas combinac iones de generadores, irerables y val ores constantes para
rellenar obj etos de iniciali zación de Map:
// : net / mindview/ util / MapData.java
// Un mapa relleno con datos utilizando un objeto generador.
package net.mindview.util¡
import java.util. * ;
}
/1 Un generador de clave y un único valor:
public MapData (Generator<K> genK, V value, int quantity) {
for {int i "" O; i < quantitYi i++ ) {
put {genK.next () , value ) ;
}
II Un iterable y un generador de valor:
public MapData ( Iterable<K> genK, Generator<V> genV ) {
for ( K key , genK) {
put (key, genV . next ( » i
}
II Un iterab le y un único valor:
public MapData (Iterable<K> genK, V value ) {
for ( K key genK ) {
put (key, value ) ;
} / * Output ,
{l=A, 2=B, 3=C, 4=D, 5=E, 6=F, 7=G, 8=H, 9=I, lO=J, ll=K}
{a=YNz, b=brn, c=yGc, d=FOW, e=ZnT, f=cQr, g =Gse, h=GZM}
{a=Value, b=Value, c=Value, d=Value, e=Value, f =Value}
{1 =mJM, 2=RoE , 3=suE, 4=cUO, 5=neO, 6=EdL, 7=smw, 8=HLG}
{l=Pop, 2=Pop, 3=Pop, 4=Pop, 5=Pop, 6=Pop, 7=Pop, B=Pop}
* /// ,-
17 Análisis detallado de los contenedores 519
Este ejem plo también util iza los generadores del Ca pítulo 16, Matrices.
Podemos crear cualqui er conj unto de datos generados para mapas o colecciones usa ndo estas herra mi entas. y luego inicia-
lizar Ull objeto Ma p o Collee ti on ut ili zando el co nstru ctor o los métodos Ma p.putAII() o Collee tion.add AIIQ.
2 Estos datos se han extraído de Internet. A lo largo del tiempo diversos lectores me han enviado correcciones.
520 Piensa en Java
{IIUGANDA", "Kampala"},
{"DEMOCRATIC REPUBLIC OF THE CONGO (ZAIRE) ",
"Kinshasa") ,
{"ZAMBrA", "Lusaka"}, { "ZIMBABWE", "Harare"},
II Asia
{ "AFGHANISTAN" I "Kabul " } I { " BAHRAIN" , "Manama " } ,
{"BAN'GLADESH " , "Dhaka II}, {"BHUTAN", "Thimphu 11 } I
public
lterator<Map. Entry<String, String» iterator () {
return new lter{);
} /* Output,
{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto - Novo,
BOTSWANA=Gaberone, BULGARIA=Sofia, BURKINA FASO
=Ouagadougou, BURUNDI=Bujumbura, CAMEROON=Yaounde,
CAPE VERDE=Praia, CENTRAL AFRICAN REPUBLIC=Bangui}
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO,
BURUNDI, CAMEROON, CAPE VERDE, CENTRAL AFRICAN REPUBLIC]
{BENIN=Porto - Novo, ANGOLA =Luanda, ALGERIA=Algiers}
{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=porto-Novo}
{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto-Novo}
{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto-Novo}
[BULGARIA, BURKINA FASO, BOTSWANA, BENIN, ANGOLA, ALGERIA]
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO]
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO]
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO]
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO]
Brasilia
*///,-
La matriz bidimensional de cadenas de caracteres DATA es pública, por lo que se la puede emplear en cualquier otro lugar.
FlyweightMap debe implementar el método entrySet(), que requiere tanto una implementación personalizada de Set como
una clase Map.Entry personalizada. He aquí parte de la solución "peso mosca": cada objeto Map.Entry simplemente alma-
cena su índice, en lugar de almacenar la clave y el valor reales. Cuando invocamos getKey() o getYalue(), utiliza el índi-
ce para devolver el elemento de DATA apropiado. El contenedor EntrySet asegura que su tamaño (size) no sea superior al
de DATA .
Podemos ver la otra parte de la solución "peso mosca" implementada en EntrySet.lterator. En lugar de crear un objeto
Map.Entr)' para cada pareja de datos en DATA, sólo existe un objeto Map,Entry por cada iterador. El objeto Entr)' se
utili za como una ventana a los datos: sólo contiene un índice (index) a la matri z estática de cadenas de caracteres. Cada vez
que invoca mos next() para iterator, se incrementa la variable índice de un objeto Entry, de modo que apunte a la siguien-
te pareja de elementos y luego el úni co objeto Entry de ese objeto Iterator es devuelto por next()3
El método select( ) genera un contenedor FlyweightMap que contiene un conjunto EntrySet del tamaño deseado, el cual
se usa en los métodos eapitals( ) y names() sobrecargados que se ilustran en main().
3 Los mapas de ja\'a.util realizan copias masivas utilizando getKcy( ) y gctValue( ), de modo que esta solución funciona. Si un mapa personalizado fuera
a copiar simplemente el objeto l\1ap.Enlry completo entonces esta técnica causaría problemas.
524 Piensa en Java
Para algunas pmebas, el tamaño limitado de Counlries es un problema. Podemos usa r la misma técnica para generar con-
tenedores personali zados inicializados que tengan un conjunto de datos de cualquier tamaño. A con tinu ación se muestra una
clase de tipo List que puede tener cualquier tamafi o y que se preinicializa con datos de tipo Integer:
// : net / mindviewf util / CountinglntegerList.java
// Lista de cualquier longitud con datos de ejemplo.
package net.mindview.util¡
import java.util.*;
1* Output:
[O, 1, 2, 3, 4, 5, 6, 7, 8 , 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
* /// ,-
Para crear una lista de sólo lectura a partir de AbstractList, hay que implementar get( ) y size( ). De nue vo, se utiliza una
solución de tipo "peso mosca": get( ) produce el va lor cuando lo pedimos, por lo que la lista no ti ene, en la práctica, que
estar rellena.
He aquí un mapa que contiene valores de tipo Integer y Strin g uní vocos preinicializados; puede tener cualquier tamaño:
11 : net / mindview/ util / CountingMapData.java
II Mapa de longitud limitada con datos de ejemplo.
package net.mindview.util;
import java.util.*¡
} / * Output,
{O=AO, 1=80, 2=CO, 3=DO, 4=EO, 5=FO, 6 =GO, 7=HO, 8",10, 9 =JO, lO = KO, 11=LO, 12=MO, 13=N O,
1 4 =00, 15=PO, 16=QO, 17 =RO, 18=50, 19 =TO, 20=UO , 21=VO, 22=WO, 23 =XO, 24=YO, 25=ZO,
26 =A1, 27 =81, 28=Cl, 29 =D1, 30=El, 31=Fl, 32=G1, 33 =H1 , 3 4 = I1 , 35=J1, 36 =K1, 37 =L1,
38=M 1 , 39=N1, 40=01, 41=Pl, 42 = Q1, 43=R1, 44=51, 45=Tl , 46 =Ul, 47=Vl, 48= Wl, 49=Xl,
SO= Yl , Sl=Zl, S2 =A2, 53= 8 2, 54=C2, 55 =D2, 56=E2 , 57= F2 , 58 =G2 , 59 =H2}
* ///,-
Aquí, se utili za un contenedor LinkedHashSet en lugar de crear una clase Set personalizada, por lo que la solución de tipo
"peso mosca" no está completamente implementada .
Ejercicio 1: ( 1) Cree una lista (inténtelo tanto con Array List como con LinkedList) y rellénela utili zando Countries.
Ordene la lista e imprímala, luego aplique Colleelions,shufOe() a la lista repetidamente, imprimiéndola
cada vez de modo que pueda ver cómo el método shufOe() aleatoriza la lista de manera diferente cada vez.
Ejercicio 2: (2) Defina un mapa y un conjunto que contengan todos los países que comiencen por 'A' .
Ejercicio 3: ( 1) Utili zando Countries, rellene un conjunto múltiples veces con los mismos datos y verifique que el con-
junto termina conteniendo una única copia de cada instancia. Pruebe a hacer lo mi smo con HashSet,
Linkcd Has hSet y TreeSet.
Ejercicio 4: (2) Cree un inicializador de Collcetion qu e abra un archivo y lo descomponga en palabras usando
Text File, y luego emplee las palabras como ori gen de los datos para el contenedor de Colleetio n resultan-
te. Demuestre que la solución fun ciona.
Ejercicio 5: (3) Modifique Countio gMapData.java para implementar completamente la solución de tipo '·peso
mosca" añadiendo una clase EntrySct personalizada como la de Co untries.j ava .
boolea n add(T) Garantiza que el contenedor almacene el argumento que es el del tipo genérico T .
Devuelve falsc si no añade el argumento (éste es un método "opcional", descrito en
la sección siguiente).
boolean addAII (Collection<? exl end s T» Añade todos los elementos del argumento. Devuelve true si se aii.ade algún ele-
mento. ("Opcional").
boolean contains(T) Devuelve true si el contenedor almacena el arg umento que es de tipo genérico T.
Boolean containsAII(ColJection<?» DcvucJve t.-ue si el contenedor almacena todos los elementos del argumento.
lterator<T> iterator( ) Devuelve un objeto lterator<T> que puede uti lizarse para recorrer los elemel110s
del contenedor.
Boolean remove(Object) Si el argumento está en el contenedor, se elimi na una instancia de dicho elemento.
Devuelve true si se ha producido alguna eliminación ("Opcional").
boolean removeAlI(Collection<?» Elimina todos los elementos contenidos en el argumento. Devuelve tru€' si se ha
producido alguna elimi nación ("Opcional").
Boolean retainAU (CoUection <?» Re tiene únicamente los elementos que estén contenidos en el argumento (una
"intersección", segú n la teoría de conjuntos). Devuelve truc si se ha producido
algún cambio. ("Opcional").
Objectll toArray( ) Devuelve una matriz que contiene todos los ele mentos del contenedor.
<T> TII toArray(TII a) Devuelve una matriz que contiene todos los elementos del contenedor. El tipo en
tiempo de ejecución del resultado es el que corresponde a la matriz argumento a en
luga r de ser simplemente Obj ect.
11 :containers / CollectionMethods.java
II Cosas que se pueden hacer con todas las colecciones.
import java.ut i l.*¡
import net.mindview.util.*¡
import static net.mindview.util.Print.*¡
1* Output:
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO, ten, eleven )
Collections.max(c) = t e n
Col lections.min(c) = ALGERIA
[ALGERIA, ANGOLA, BENI N, BOTSWANA, BULGARIA, BURKINA FASO,
ten, eleven, ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO)
[ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO, ten, eleven, ALGERIA,
ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO)
[BENIN, BOTSWANA, BULGARIA, BURKINA FASO, ten, eleven, ALGERIA, ANGOLA,
BENIN, BOTSWANA, BULGARI A, BURKINA FASO)
[ten, eleven]
[ten, eleven, ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA,
BURKINA FASO)
c.contains(BOTSWANA) = true
c.containsAll(c2 ) = true
[ANGOLA, BENIN)
c2.isEmpty {) = true
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO)
after e.elear () , [)
* /1/,-
Se crean contenedores ArrayList que contienen diferentes conjuntos de datos y se los generaliza a objetos CollectioD, por
lo que qu eda claro que no se está utili zando más que la interfaz ColJection . maine ) utiliza una serie de ejercicios simples
para ilustrar todos los métodos de CoUection .
528 Piensa en Java
Las siguientes secciones del capítulo describen las diversas implementaciones de List, Set y Map y se indica en cada caso
(con un asterisco) cuál deberia ser la opción preferida. Las descripciones de las clases heredadas Vector, Stack y Hashtable
se dejan para el final del capitulo; aunque no deberían utilizarse estas clases, lo más probable es que tengamos oportunidad
de verlas al leer código antiguo.
Operaciones opcionales
Los métodos que realizan diversos tipos de operaciones de adición y eliminación son operaciones opcionales en la interfaz
Collection. Esto significa que la clase implementadora no está obligada a proporcionar defin iciones de es tos métodos que
funcionen.
Se trata de una fanna bastante inusual de definir una interfaz. Como hemos visto, una interfaz es una especie de contrato
dentro del di seño orientado a objetos. Ese contrato dice: " Independientemente de cómo decidas implementar esta interfaz,
te garantizo que puede enviar estos mensajes al objeto"4. Pero el hecho de que exista una operación "opcional" vio la este
principio fundamental ; ya que implica que al invocar algunos métodos 110 se obtendrá un comportamiento con significado.
En lugar de ello, esos métodos generarán excepciones. Podría parecer que estamos renunciando a la seguridad de tipos en
tiempo de compilación.
Pero las cosas no son en realidad así. Si una operación es opcional, el compilador sigue imponiendo la restricción de que
sólo se puedan invocar los métodos especificados en dicha interfaz. Esto no se parece a los lenguajes dinámicos, en los que
se puede invocar cualquier método para cualquier objeto y averiguar en tiempo de ejecución si una llamada concreta fun-
ciona s . Además, la mayoría de los métodos que toman un contenedor Collection como argumento sólo leen de dicha colec-
ción, y todos los métodos de "lectura" de CoHection no son opcionales.
¿Para qué querríamos defrnir métodos como "opcionales"? Al hacerle así, evitamos una explosión de interfaces en el dise-
ño. Otros diseños de bibliotecas de contenedores siempre parecen tenninar en una confusa plétora de interfaces, para des-
cribir cada una de las variantes del tema principal. Ni siquiera resulta posible capturar todos los casos especiales en las
interfaces, porque alguien puede siempre inventar una nueva interfaz. La técnica de "operaciones no soportadas" pennite
conseguir un objetivo importante de la biblioteca de contenedores de Java: los contenedores son simples de aprender y de
utilizar. Las operaciones no soportados son un caso especial que pueden retardarse hasta que sean necesarias. Sin embargo,
para que esta técnica funcione:
1. La excepción UnsupportedOperationException debe ser un suceso raro. En otras palabras, para la mayoria de
las clases, todas las operaciones deben funcionar, y sólo en casos especiales esa operación no estará soportada.
Esto es asi en la biblioteca de contenedores de Java, ya que las clases que se utilizan el 99 por ciento del tiempo
(ArrayList, LinkedList, HashSet y HashMap, así como las otras implementaciones concretas) soportan todas
las operaciones. El diseño proporciona una " puerta trasera" si queremos crear un nuevo contenedor de tipo
Collection sin proporcionar definiciones significativas para todos los métodos de la interfaz Collection , sin que
por ello ese nuevo contenedor deje de encajar dentro de la biblioteca existente.
2. Cuando una operación no esté soportada, puede existir una probabilidad razonable de que aparezca una excep-
ción UnsupportedOperationException en tiempo de implementación, en lugar de después de haber enviado el
producto al cliente. Después de todo, esa excepción indica que hay un error de programación: se ha empleado una
imp lementación incorrectamente.
Merece la pena reseñar que las operaciones no so portadas sólo so n detectables en tiempo de ejecución y representan, por
tanto, una comprobación dinámica de tipos. Si el lector proviene de un lenguaje con tipos estáticos como e++, Java puede
parecer simplemente otro lenguaje con tipos estáticos. Por supuesto que Java tiene comprobación estática de tipos, pero
también tiene una cantidad significativa de comprobación de tipos dinámica, por lo que resulta dificil decir si Java es un
tipo de lenguaje u otro. Una vez que comience a entender esto, verá otros ejemplos de comprobación dinámica de tipos en
Java.
4 Utilizo aquí el témlino intcrfaz tanto para describir la palabra clave interface como el significado más general de "los métodos soportados por lLna clase
o subclasc".
s Aunque esto suene extraiio y posiblemente sea ¡nutil al ser descrito de esta fonna. hemos visto, especialmente en el Capítulo 14, ¡"formación de tipos,
que esta especie de comportamiento dinámico puede ser importanle.
17 Análisis detallado de los contenedores 529
Operaciones no soportadas
Un origen bastante común de la aparición de operaciones no soportadas es cuando disponemos de un contenedor que está
respaldado por una estructura de datos de tamaño fij o. Obtenemos dichos contenedores cuando transfonnamos una matri z
en una lista con el método Arrays.asList( ). También podemos decidir que algún contenedor (incluyendo los mapas) gene-
re la excecpión UnsupportedOperation Exception util izando los métodos "no modificables" de la clase Colleetions. Este
ejemplo ilustra ambos casos:
// : containers/Unsupported . java
/1 Operaciones no soportadas en los contenedores de Java.
import java.util.*;
t ry ( e.elear() ; } eateh(Exception e) (
System.out.println("clear{): " + e);
/ * Output:
--- Modif i able Copy - --
- - - Arrays. asList () - --
reta i nAll (): java . lang. Un supportedOperat i onException
removeAll(): java.lang.UnsupportedOperationException
530 Piensa en Java
Funcionalidad de List
Como hemos visto, el contenedor List básico es bastante simple de usar: la mayor parte del tiempo nos Limitaremos a invo-
car add( ) para insertar los objetos, o a utili zar get( ) para extraerlos de uno en uno o a llamar a iterator() para obtener un
objeto Iterator para la secuencia.
Los métodos del sigu iente ejemplo cubren, cada uno de ellos, un gmpo distinto de actividades: las cosas que todas las lis-
tas pueden hacer (basicTest( )): el desplazamiento por el contenedor con un iterador (iterMotion( )) comparado con la
modificación de valores mediante un iteradar (iterManipulation( »), la comprobación de los efectos de la manipulación de
una lista (testVisual( )); y las operaciones disponibles únicamente para contenedores de tipo LinkedLists:
jj: containers/Lists.java
// Cosas que se pueden hacer con las li stas .
import java . util. * ;
import net.mindview . util .* ;
17 Análisis delallado de los conlenedores 531
new LinkedList<String>(Countries.names ( 25 }) );
testLinkedList( ) ;
Set (interfaz) Cada elemento que se añada al conjunto debe ser diferente; en caso contrario, el objeto Ser no añadirá
el elemento duplicado. Los elementos añadidos a un conjunto deben al menos definir equals( ) con el
fin de establecer la unicidad de los objetos. Set tiene exactamen te la misma interfaz que Collection. La
interfaz Set no garant iza que vaya a mantener sus elementos en ningún orden detenninado.
HashSet* Para los conjuntos en los que el tiempo de búsqueda sea importante. Los elementos debe definir tam-
bién hashCode( ).
TreeSet Un conj unto ordenado respaldado por un árbol. De esta fomla, se puede extraer una secuencia ordena-
da de un conjunto. Los elementos también deben imp lementar la interfaz Comparable.
LinkedHashSet Tiene la velocidad de búsqueda de un contenedor HashSet. pero mantiene internamente el orden en que
se ailaden los elementos (e l orden de inserción) utilizando Wla lista en lazada. Por tanto, cuando itera-
mos a través del conjunto, los resultados aparecen en orden de inserción. Los elementos también deben
definir has hCode( ).
El asterisco en HashSet indica que, en ausencia de otras restricciones, ésta debe ser la opción preferida, porque está opti-
mizada para conseguir la máx ima ve locidad.
La definición de hashCode() se describirá más adelante en el capítu lo. Es necesario crear un método equals() para el alma-
cenamiento tanto de tipo hash como de tipo árbo l, pero el método hashCode( ) sólo es necesario si se va a almacenar la
clase en un contenedor HashSet (lo cual es bastante probable, ya que esa debería ser nuestra primera elección como imple-
mentación del conjunto) o L inkedHasbSet. Sin embargo, con el fin de mantener un buen estilo de programación, convie-
ne sustituir siempre hashCode() cada vez que se sustituya equals( ).
534 Pi ensa en Java
Este ejemplo ilustra los métodos qu e deben defin irse para ut ilizar convenien temente un tipo de datos con un a implementa-
ción concreta de Set:
/1: concainersfTypesForSets.java
1/ Métodos necesarios para almacenar nuestro propio
JI tipo de datos en un conjunto .
tipos de datos.
import java.util.*;
class SetType {
int i i
public SetType (int ni { i = n; }
public boolean equals (Object o) {
return o instanceof SetType && (i == «SetType)o) .i) i
}
public String toString() { return Integer.toString(il; }
return set i
1* Output: (Sample )
[2, 4, 9, 8, 6, 1, 3, 7, 5, O)
(O, 1, 2, 3, 4, 5, 6, 7, 8, 9)
(9, 7, 6, 5,
8, 4, 3, 2, 1, O)
(9,9, 7, 5, 1, 2, 6, 3,
°, 7, 2, 4, 4, 7, 9, 1, 3, 6, 2,
4, 3,
°,
5, O,
(O, 5, 5, 6, 5,
8,
O,
8, 8,
3, 1,
6, 5, 1)
9, 8, 4, 2, 3, 9, 7, 3, 4, 4, O,
7, 1, 9, 6, 2, 1, 8, 2, 8, 6, 7)
(O, 1, 2, 3, 4, 5, 6, 7, 8, 9, O, 1, 2, 3, 4, 5, 6, 7, 8,
9, O, 1, 2, 3, 4, 5, 6, 7, 8, 9)
[O, 1, 2, 3, 4, S, 6, 7, 8, 9, O, 1, 2, 3, 4, 5, 6, 7, 8,
9, O, 1, 2, 3, 4, 5, 6, 7, 8, 91
java.lang.ClassCastException: SetType cannot be cast to java.lang.Comparable
java.lang.ClassCastException: HashType cannot be cast to java.lang.Comparable
*/11,-
Para demostrar qué métodos son necesarios para un contenedor Set concreto y para evitar, al mismo tiempo, la duplicación
de código, hemos creado tres clases. La clase base. SctType, simplemente almacena un valor int y 10 imprime mediante
toString(). Puesto que todas las clases almacenadas en conjuntos deben tener un método equals(), también incluimos dicho
método en la clase base. La igualdad está basada en el valor de la variable int i.
HashType hereda de SetType y añade el método hashCode() necesario para inse rtar un objeto en una implementación has/¡
de un conjunto.
La interfaz Comparable, implementada mediante TreeTypc, es necesaria si va mos a usar un objeto en algú n tipo de con-
tenedor ordenado, como por ejemplo SortedSet (del cual TreeSet es la única implementación). En compareTo(), observe
que no hemos usado la forma "simple y obvia"' return ¡-¡2. Aunque se trata de un error de programación común, sólo fun-
cionaría adecuadamente si i e i2 fueran valores int "sin signo" (si Java tuviera una palabra clave "unsigned", que no es el
caso). La expresión no funciona para los valores int con signo de Java, que no son lo suficien temente grandes como para
representar la diferencia de dos valores int con signo. Si i es un entero positivo de gran tamaño y j es un entero negativo de
gran tamaño, i-j producirá un desbordamiento y devolverá un valor negativo, lo cual es incorrecto.
Nonnalmente, lo que queremos es que el método compareTo() pennita obtener una ordenación natural que sea coherente
con el método equals(). Si equals() devuelve true para una comparación concreta, entonces compareTo() deberia devol-
ver un resultado igual a cero para dicha comparac ión, y si equals( ) devuelve false para una comparación, entonces
compareTo() debería dar un resultado distinto de cero para dicha comparación.
En TypesForScts, tanto fil1( ) como test( ) se definen utilizando genéricos, con el fin de evitar la duplicación de código.
Para verificar el comportamiento de un conjunto, test() invoca fill() sobre el conjunto de prueba set tres veces, tratando de
introducir objetos duplicados. El método fil1() toma un contenedor Set de cualquier tipo y un objeto Class del mismo tipo.
Utiliza el objeto Class para descubrir el constructor que admite un argumento int, e in voca dicho constructor para aüad ir
elementos al conjunto.
Ana lizando la salida, podemos ver que HashSet mantiene los elementos en alguna especie de orden misterioso (que en ten-
deremos claramente más adelante en el capítulo), LinkedHashSet mantiene los elementos en el orden en que fueron inser~
tados y TreeSet mantiene los elementos ordenados (debido a la forma en que se implementa compareTo( ), dicho orden
resulta ser descendente).
Si tratamos de utili zar tipos de datos que no soporten apropiadamente las operaciones necesarias con conjuntos que requie~
ren dichas operaciones, el funcionamiento es incorrecto. Al insertar un objeto SetType o TreeType, que no incluye un méto~
do hashCode() redefinido, en una implementación hash se generan valores duplicados, con lo que se viola la característica
principal de un conjunto. Este error es bastante molesto, porque ni siquiera se produce un error en tiempo de ejecución: sin
536 Piensa en Java
embargo, el método hashCode() predetemünado es legit imo, y por ta nto es un comportamiento legal, au n cuando sea inco-
rrecto. La única fonn8 fiable de garanti zar la corrección de ese programa consiste en incorporar código de pruebas en el sis-
tema final de producción . (consulte el suplemento en http://MindView. net/Books/BetterJava para obtener más infonnación).
Si tratamos de utili zar un tipo de datos que no im plemente Comparable en un cont enedor TreeSet, se obtiene un resultado
más definido: se genera una excepción cuando el contenedor TreeSet trata de utili zar el objeto como si fuera de tipo
Co mparable.
SortedSet
Los contenedores SortedSet garanti zan que sus elementos estén ordenados, lo que pennite proporcionar funcionalidad adi-
cional mediante los siguientes métodos, definidos en la interfaz SortedSet:
Comparator comparator( ): genera el objeto Comparalor utili zado para este cont enedor Set, o null para el
caso de una ordenación natura l.
Object first( ) : devuel ve el elemento más bajo.
Object last() : devuel ve el elemento más alto.
SortedSct subSet(fromElement, toElement) : ge nera UDa vista de este contenedor Set de los elementos com-
prendidos entre from_E lement, incl uido, y toElement, excluido.
SortedSet headSet(toElement) : genera una vista de este contenedor Set con los elementos inferiores a
toElemen!.
SortedSet tailSet(fromElement): genera una vista de este contenedor Set con los elementos su periores o igua-
les a fromElemen!.
He aquí un ejemplo simple:
JI: containers/SortedSetDemo.java
II Lo que se puede hacer con un contenedor TreeSet.
import java.util .*;
import static net.mindview.util.Print.*;
print (low) ;
print Ihigh) ;
print (sortedSet.subSet (low, high));
print(sortedSet.headSet(high )) i
print(sortedSet . tailSet(low)) ;
1* Output:
[eight, five, four, one, seven, six, three, two]
eight
tWQ
17 Análisis detallado de los contenedores 537
one
two
[ane, seven, six, three]
[eight, five, four, ane, seven, six, three]
[one, seven, six, three, two]
* /// ,-
Observe que SortedSet quiere decir "ordenado de acuerdo con la función de comparación del objeto", no "arde.n de inser-
ción". El orden de inserción puede conservarse utili zando LinkedHashSet.
Ejercicio 9: (2) Utilice RandomGenerator.String para rellenar un contenedor TreeSet, pero empleando ordenación
alfabética. imprima el contenedor TreeSet para verificar la ordenación.
Ejercicio 10: (7) Uti lizando un contenedor LinkcdList como implementación subyacente, defina su propio contenedor
SortedSet.
Colas
Dejando aparte las aplicaciones de concurrencia, las dos unicas implementaciones de colas en Java SES son LinkedList y
PriorityQueue, que se diferencian por el comportamiento en lo que respecta a la ordenación más que por el rendimiento.
He aquí un ejemplo básico donde se ilustran la mayo ría de las implementaciones de Queue (no todas ellas funcionan en este
ejemplo), incluyendo las colas basadas en concurrencia. Los elementos se insertan por un extremo y se ex traen por el otro:
1/ : conta i ners/QueueBehavior.java
11 Compara el comportamiento de algunas de las colas
import java .util.concurrent.*;
import java.util.*;
import net . mindview.util.*;
1* Output:
o n e two three four five six seven eight nine ten
eight five four nine one seven six ten three two
one two three four five six seven eight nine ten
one two three four five six seven eight nine ten
one two three four five six seven eight nine ten
eight five four nine one seven six ten three two
*/ // ,-
538 Piensa en Java
Podemos ver que. con la excepción de las colas con prioridad. un contenedor Queue devuelve los elementos exactamente
en el mismo orden en que fueron insertados en la cola.
/ * Output:
Al, Water lawn
A2, Feed dog
BL Feed cat
B7, Feed bird
C3, Mow lawn
C4: Empty trash
* /// ,-
17 Análisis detallado de los contenedores 539
Podemos ver cómo la ordenación de los elementos se realiza automáticamente gracias a la cola con prioridad.
Ejercicio 11 : (2) Cree ulla clase que contenga un objeto Integer que se inicialice con un valor comprendido entre O y
100 utilizando java.utiJ.Random. Implemente Comparable empleando este campo Integer. Rellene una
cola de tipo PriorityQueuc con objetos de dicha clase y extraiga los valores usando poll( ) para demos-
trar que se obtiene el orden deseado.
Colas dobles
Una cola doble es sim ilar a una cola nannal, pero se pueden añadir y eliminar elementos de cualquier extremo. Existen
métodos en LinkedList que soportan las operaciones de doble cola, pero no existe ninguna interfaz explícita para una doble
cola en las bibliotecas estándar de Ja va. Por tanto, LinkedList no puede implementar esta interfaz y no resulta posible efec-
mar una generalización a una interfaz Deque (cola doble), a diferencia de lo que podríamos hacer con Queue en el ejerci-
cio anterior. Sin embargo, podemos crear una clase Deque empleando el mecanismo de composición y exponer,
simplemente, los métodos relevantes de LinkedList:
11: net/mindview/util/Deque.java
II Creaci6n de una cola doble a partir de LinkedList.
package net.mindview.util¡
import java . util.*¡
printnb(di.removeLast {) + 11 ");
1* Output:
[26, 25, 24, 23, 22, 21, 20, 50, 51, 52, 53, 54]
26 25 24 23 22 21 20 50 51 52 53 54
54 53 52 51 50 20 21 22 23 24 25 26
* /// ,-
Resulta poco probable que tengamos que insertar y extra er element os por ambos ex tremos, por lo que Deque no se emplea
tan comúnmente como Queue.
Mapas
Como vimos en el Capítulo 11 , Almacenamiento de objetos, la idea básica de un mapa (también denominada matriz asocia-
tiva) es la de almacenar asociaciones clave-valor (pares), de modo que se pueda buscar un valor utilizando una clave.
La bib lioteca estándar de Java contiene d iferentes implementaciones básicas de mapas: HashMap , TrecMap,
LinkedHashMap, WeakHashMap, ConcurrentHashMap y IdentityHashMap. Todas tienen la misma interfaz básica
Map, pero difieren en cuanto a su comportamiento, incluyendo la eficiencia, el orden en el que se almacenan y se presen-
tan los pares, el tiempo que el mapa conserva los objetos, el funcionamiento de los mapas en los programas multihebra y el
modo de determinar la igualdad de claves. La gran cantidad de implementaciones de la interfaz Map nos indica la impor-
tancia de este tipo de contenedor.
Para comprender mejor los mapas resulta útil ver cómo se construye una matri z asociativa. He aquí una implementación
extremadamente simple:
JJ : containers J AssociativeArray.java
11 Asocia claves con valores.
import static net.mindview.util.Print.*¡
@SuppressWarnings ( "unchecked 11 )
public V get (K key) (
for ( int i = O; i < index; i++ )
i f (key. equals (pairs [i] [ O] ) )
return (V ) pairs[i] [1] i
return null; 11 Clave no encontrada
return result.toString {) ;
17 Análisis detallado de los contenedores 541
print (map ) ;
print (map.get ( "ocean" )) ;
/* Output:
Too many objects!
sky : blue
grass : green
ocean : dancing
tree : tall
earth : brown
sun : warm
dancing
* /// ,-
Los métodos esenciales en una matriz asociativa son put( ) y get( ), pero para facilita r la visualización se ha susti-
tuido toString( ) con el fm de imprimir los pares clave-valor. Para demostrar que funciona , main() carga una matri z
AssociativeArray con pares de cadenas de caracteres e imprime el mapa resultante, ex trayendo a continuación con get( )
uno de los valores.
Para utilizar el método get(), se pasa la clave (key) que queremos buscar y el método devuel ve como resultado el valor aso-
ciado, o bien devuelve null si no se puede encontrar esa clave. El método get( ) utili za la que posiblemente sea la técnica
menos eficiente imaginable con el fin de localizar el valor: comienza por la parte superior de la matri z y usa equals() para
comparar las claves. Pero el objetivo del ejemplo es la simplicidad no la eficiencia.
Por tanto, la versión anterio r es instmctiva, pero no resulta muy eficiente y tiene un tamaño fijo, lo que da como resultado
una implementación poco flexible. Afommadamente, los mapas de java.util no tienen estos problemas y pueden emplear-
se perfectamente en el ejemplo anterior.
Ejercicio 12: (I)Utilice mapas de tipo HashMap, TreeMap y LinkedHashMap en el método main( ) de
AssociativeArray.java.
Ejercicio 13: (4) Utilice AssociativeArray.java para crear un contador de apariciones de palabras, que establezca la
correspondencia entre un valor de tipo Strin g y un va lor de tipo Integer. Empleando la utilidad net.mind-
view.util.TextFile de este libro, abra un archivo de tex to y extraiga las palabras de dicho archivo utilizan-
do los espacios en blanco y los signos de puntuación como delimitadores. Cuente el número de veces que
cada palabra aparece en dicho archivo.
Rendimiento
El rendimiento es una de las cuestiones fundamentales en los mapas, y resulta demasiado lento utilizar una búsqueda li-
neal en get() a la hora de intentar local izar una clave. Es en este aspecto donde HashMap pennite acelerar las operaciones.
En lugar de real izar una lenta búsqueda de la clave, este contenedor utiliza un valo r especial denominado código hash. El
código hash es una fonna de tomar una cierta información contenida en el objeto en cuestión y transfonnarla en un valor
int "relativamente unívoco" que se utilizará para representar dicho objeto. hashCode( ) es un método de la clase raíz
542 Piensa en Java
Object, por lo que todos los objetos Java pueden generar un código has/¡. Un contenedor HashMap toma el código hash
devuelto por hashCodc() y lo utiliza para localizar rápidamente la clave. Esto pemlite mejorar enonncmente el rendi-
miento. 6
He aquí las implementaciones básicas de Map. El asterisco sinlado junto a HashMap indica que, en ausencia de otras res-
tri cciones, ésta debería ser la opción preferida, ya que está optimizada para maximizar la velocidad. Las otras implementa-
ciones enfatizan otras características. por lo que no resultan tan rápidas como HashMap .
HashMap* Implementación basada en una tabla !IOS/¡ (utilice esta clase en lugar de Hashtable).
Proporciona un rendimiento de tiempo constante para la inserción y localización de
pares. El rendimiento puede ajustarse mediante constructores que penniten fijar la
capacidad y el/actor de cwga de la tabla has/¡.
LinkedHashMap Como l.JashMap. pero cuando se realiza una iteración a su través, se extraen los pares
en orden de inserción o en orden LRU (Ieast-recenrl)'-used, menos recientemente utili-
zado). Es ligeramente más lento que HashMap , sa lvo cuando se está realizando una ite-
ración, en cuyo caso es más rápido debido a que se emplea una lista enlazada para man-
tener la ordenación interna.
TreeMap Implementación basada en un árbol rojo-negro. Cuando se exami nen las claves o las
parejas, estarán ordenadas (la ordenación está dctenninada por Comparable o
Comparalor). La ventaja de un conte nedor Treel\1ap es que los resultados se obt ienen
ordenados. TrecMap es el imico tipo de mapa con el método subMap(), que permite
devolver una parte del árbol.
\VeakHashMap Un mapa de claves débiles que pennite eliminar los objetos a los que hace referencia el
mapa; está diseñado para resolver ciertos tipos especiales de problemas. Si no se con-
serva ninguna referencia a una clave concreta fuera del mapa, dicha clave puede ser
depurada de la memoria.
ConcurrentHashMap Un mapa preparado para hebras de programación que no lncluye bloqueo de sincroni-
zación. Hablaremos de este tema en el Capítulo 21, Concurrencia .
IdentityHashMap Un mapa hash que utiliza - en lugar de equals( ) para comparar las claves. Se utiliza
para resolver ciertos tipos especiales de problemas, no para uso general.
El almacenamiento hash es la forma que más comúnmente se utiliza para almacenar elementos en un mapa. Posterionnente
veremos cómo funciona este tipo de almacenamiento.
Los requisitos para las claves utilizadas en un mapa son iguales que para los elementos de un conjunto. Ya hemos visto una
ilustración de estos requisitos e11 'JypesForSets.java. Todas las claves deben disponer de un método equals( j . Si la clave
se utiliza en un mapa hash, también debe disponer de un método hashCode( j apropiado. Si la clave se utiliza en un mapa
de tipo TreeMap, deberá implementar Comparable.
El siguiente ejemplo muestra las operaciones disponibles en la interfaz Map, utilizando el conjunto de datos de prueba
CountingMapData anterionnente defmido:
jj : containersjMaps.java
JI Cosas que se pueden hacer con mapas.
import java.util.concurrent.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
public class Maps
6 Si este tipo de ut ilización sigue sin satisracer sus requisitos de rendimiento, puede acelerar todavía más las búsquedas en tablas escribiendo su propio
contenedor Map y personalizándolo para los tipos particulares de datos que esté utilizando, con el fin de evitar tos retardos asociados con las proyeccio-
nes de tipo hacia y desde Object. Para obtener niveles todavia mejores de rendimiento, los entusiastas de la velocidad pueden consultar el libro de Donald
Knuth, Tite Art o/Computer Programming, Va/lime 3:Sorring and Sean:hing. Segunda edición, con el fin de sustittlir las listas de segmentos con desbor-
damiento por matrices que tienen dos ventajas adicionales: pueden optimizarse de acuerdo con las características de almacenamiento del disco y pcmúten
ahorrar la mayor pane delliempo invenido en crear y depurar los registros individuales.
17 Análisis detallado de los contenedores 543
1* Output:
HashMap
Size = 25, Keys: [15, B, 23, 16, 7, 22, 9, 21, 6, 1, 14,
24, 4, 19, 11, 18, 3, 12, 17, 2, 13, 20, 10, 5, O]
Values: [PO, ID, XO, QO, HO, WO, JO, VO, GO, 80, 00, YO,
EO, TO, LO, SO, DO, MO, RO, CO, NO, UO, KO, FO, AO]
{ls=PO, 8=rO, 23=XO, 16=QO, 7=HO, 22=WO, 9=JO, 21=VO, 6=GO,
1=80, 14=00, 24=YO, 4=EO, 19=TO, 11=LO, 1B=SO, 3=00, 12=MO,
17=RO, 2=CO, 13=NO, 20=UO, 10=KO, s=FO, O=AO}
map. containsKey (11) : true
map.get(l1) , LO
map. containsValue ( " FO"): true
First key in map: 15
Size = 24, Keys: {B, 23, 16, 7, 22, 9, 21, 6, 1, 14, 24, 4,
19, 11, lB, 3, 12, 17, 2, 13, 20, 10, 5, O]
map.isEmpty(): true
map.isEmpty(): true
El método printKeys( ) muestra cómo generar lIna vista de tipo Collection para un mapa. El método keySet( ) genera un
conjunto con las claves del mapa. Debido a las mejoras en el soporte de impresión introducidas en Java SES, podemos impri-
544 Piensa en Java
mir los resultados del método va lues(), que genera una colección con todos los valores de l mapa (observe que las claves
debe ser unívocas, pero que los valores pueden contener duplicados). Puesto que estas colecciones están respaldadas por el
propio mapa, cualquier cambio efectuado en una colección se reflejará en el mapa asociado.
El resto del programa proporciona ejemplos simples de cada operación efectuada con el mapa y pnleba cada tipo básico de
mapa.
Ejercicio 14: (3) Demuestre que java.util.Properties funciona en el programa anterior.
SortedMap
Si uti lizamos un contenedor SortedMap (del cual la única implementación disponible es TreeMap), se garantiza que las
claves estarán ordenadas, lo que pennite proporcionar funciona lidad adicional con los siguientes métodos de la interfaz
SortedMap :
Comparator comparator( ): gene ra el comparador utilizado para este mapa o null si se utiliza la ordenación
natu ral.
T IirstKey(): dev uelve la clave más baja.
T lastKey( ): devuelve la clave más alta.
SortedMap subMap(fromKey, toKey): genera una vista de este mapa, con las claves comprendidas entre
fromKe y, incluido, y toKey, excluido
SortedMap headMap(toKey): genera una vista de este mapa con las claves que sean inferiores a toKey.
SortedMap taiIMap(fromKey): genera una vista de este mapa COI1 las claves que sean iguales o superiores a
fromKey.
He aquí un ejemplo simi lar a SortedSetDemo.java donde se ilustra este comportamiento adiciona l de los mapas TreeM ap :
jI: containersjSortedMapDemo.java
/1 Lo que se puede hacer con TreeMap.
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
print {low} ;
print (high ) ;
print (sort edMap. subMap(low, high);
print (sortedMap.headMap{high)} ;
print (sortedMap.tailMap {low)} ;
} 1* Output,
{O=AO, 1=80, 2 =CO. 3=DO, 4=EO, 5=FO, 6=GO, 7=HO, 8=IO, 9=JO}
°9
17 Análisis detallado de los contenedores 545
3
7
{3=00, 4""EO, 5=FO, 6=GO}
{O=AO, 1=80, 2=CO, 3=00, 4=EO, 5=FO, 6=GO}
{3=00, 4=EO, 5=FO, 6=GO, 7=HO, 8=IO, 9=JO}
" ///,-
Aquí, los pares se almacenan ordenados según la clave. Puesto que existe el concepto de orden en TreeMap, el concepto de
"posición" también tiene sentido, así que se pueden tener subrnapas y también se puede detenninar el primer elemento yel
último.
LinkedHashMap
LinkedHashMap utiliza un almacenamiento hash para conseguir ve locidad, pero también genera las parejas en orden de
inserción cuando se recorre el mapa (System,out.pri ntln( ) itera a través del mapa, por lo que se pueden comprobar los
resultados de ese recorrido). Además, LinkedHashMap puede configurarse mediante el constructor para utilizar un algo-
ritmo LRU (leasl-recenlly-used) basado en el acceso a los elementos, de modo que los elementos a los que se haya accedi-
do menos (y sean, por tanto, candidatos a la eliminación) aparezcan al principio de la lista. Esto pennite crear fácilmente
programas que realizan una limpieza periódica con el fin de ahorrar espacio. He aquí un ejemplo donde se ilustran ambas
características:
j/ : containersjLinkedHashMapDemo.java
// Lo que se puede hacer con LinkedHashMap.
import java.util.*j
import net.mindview.util.*;
import static net.mindview.util.Print.*;
public class LinkedHashMapDemo {
public static void main(String[) args)
LinkedHashMap<Integer,String> linkedMap
new LinkedHashMap< In teger,String>(
new CountingMapData(9));
print(linkedMap) ;
// Orden LRU,
linkedMap =
new LinkedHashMap<Integer, String> (16, O. 7Sf, true);
linkedMap.putAll(new CountingMapData(9));
print(linkedMap) ;
for(int i = O; i < 6; i++) // Provocar accesos:
linkedMap.get(i) ;
print(linkedMap) ;
linkedMap.get(OI;
print(linkedMap) j
} / " Output:
{O=AO, 1=80, 2=CO, 3=DO, 4=EO, 5=FO, 6=GO, 7=HO, 8=IO}
{O=AO, l=BO, 2=CO, 3=00, 4=EO, 5=FO, 6=GO, 7=HO, 8=IO}
{6 =GO, 7=HO, 8=IO, O=AO, 1=80, 2=CO, 3=DO, 4=EO, 5=FO}
{6=GO, 7=HO, 8=IO, l=BO, 2=CO, 3=00, 4=EO, S=FO, O=AO}
" /1/ ,-
Podemos ver, analizando la salida, que las parejas se recorren por orden de inserción, incluso para la ve rsión LRU. Sin
embargo, después de acceder exclusivamente a los primeros seis elementos en la versión LRU, los tres últimos elementos
se desplazan al principio de la lista. Desp ués, cuando se vuelve acceder a " O", dicho elemento se desplaza al fInal de la lista.
Uno de los problemas más comunes es el que se produce cuando creamos nuestras propias clases para utilizarlas como cla-
ves para mapas de tipo HashMap, y nos olvidamos de añadir los elementos necesarios. Por ejemplo, considere un sistema
de predicción meteorológica basado en el estudio del comportamiento de las marmotas donde se hagan corresponder obje-
tos Ground hog (mannota) con objetos Predictio n (predicción meteorológica). La tarea parece sencilla: basta con crear las
dos clases y lIsar Gro undhog como clave y Pred iction como valor:
11: containers/Groundhog.java
II Parece plausible, pero no funciona como clave para HashMap.
11 : containers/Prediction.java
1I Predicción del clima mediante marmotas .
import java.util . *;
11: containers/SpringDetector.java
II ¿Qué tiempo hará?
import java.lang.reflect.*;
import java.util.*;
import static net . mindview.util.Print.*;
1* Output:
17 Análisis detallado de los contenedores 547
JI: containersjSpringDetector2.java
1/ Una clave adecuada.
/ * Output:
map = {Groundhog #2=Early Spring!, Groundhog #4=Six more weeks of Winter!, Groundhog
#9=Six more weeks of Winter!, Groundhog #8=Six more weeks of Winter!, Groundhog #6=Early
Spring!, Groundhog #l=Six more weeks of Winter ! , Groundhog #3=Early Spring!, Groundhog
#7=Early Spring!, Groundhog #5=Early Spring!, Groundhog #O=Six more weeks of Winter!}
Looking up prediction for Groundhog #3
Early Spring!
* /// ,-
Groundhog2.hashCode( ) devuel ve el número de marmota como valor de hash. En este ejemplo, el programador es res-
ponsable de garantizar que no existan dos mannotas con el mismo número identificador. El método hashCode( ) no tiene
por qué devo lver un identificador uní voco (es to es algo que explicaremos con más detalle más adelante en el capítulo), pero
el método equals() debe detenninar de manera estricta si dos objetos son equivalentes. Aquí, equals( ) se basa en el núme-
ro de mannota, por lo qu e ex isten como claves dos objetos Groundhog2 en el mapa HashMap que tengan el mismo nú-
mero de marmota, el método fallará.
Aunque parece que el método equals() se limita a comprobar si el argumento es una instancia de Groundhog2 (usando la
palabra clave instanceof, de la que hemos hablado en el Capítulo 14, Información de lipos), instanceof realiza, en realidad,
una segunda comprobación automática para ver si el objeto es null, ya que instanceof devuelve false si el argumento de la
izquierda es nuJI. Suponiendo que el objeto sea del tipo correcto y distinto de nuU, la comparación se basa en los valores
number de cada objeto. Puede ver, analizando la salida, que ahora el comportamiento es correcto.
A la hora de crear su propia clase para emplearla en un contenedor HashSet, deberá prestar atención a los mismos proble-
mas que cuando se utiliza como clave en un mapa de tipo HashMap.
Funcionamiento de hashCode( )
El ejemplo anterior es sólo un primer paso en la resolución correcta del problema. Demuestra que si no susti nlimos
hashCode() y equals() para nuestra clave, la estructura de datos hash (HasIISe!. HashM ap, LinkedHashSet o
LinkedHashMap) probab lemente no podrá gestionar nuestra clave apropiadamente. Sin embargo. para obtener una buena
sol ución del problema, necesitamos comprender qué es lo que sucede dentro de la estructura de datos hash.
En primer lugar, consideremos cuál es la motivación para utilizar almacenamiento hash: lo que queremos es buscar un obje-
to empleando otro objeto. Pero también podríamos hacer esto con un contenedor TreeMap, o incluso podríamos implemen-
tar nuestro propio conrenedor Ma p. Por contraste con una implementación hash , el siguiente ejemplo implementa un mapa
usando una pareja de contenedores ArrayList. A diferencia de AssociativeArray.java, esto incluye una implementación
completa de la interfaz Map, para poder disponer del método entrySet( ):
11: containers/SlowMap.java
II Un mapa implementado con ArrayList.
import java.util.*¡
import net.mindview.util.*¡
1* Output:
{CAMEROON=Yaounde, CHAD=N'djamena, CONGO=Brazzaville, CAPE
VERDE=Praia, ALGERIA=Algiers, COMOROS =Moroni, CENTRAL AFRICAN
REPUBLIC =Bangui, BOTSWANA=Gaberone, BURUNDI =Bujumbura,
BENIN=Porto-Novo, BULGARI A=Sofia, EGYPT=Cairo,
ANGOLA=Luanda, BURKI NA FASO=Ouagadougou, DJIBOUTI=Dijibouti}
Sofia
[CAMEROON=Yaounde, CHAD =N'djamena, CONGO=Bra z zaville, CAPE
VERDE =Praia, ALGERIA=Algiers, COMOROS=Moroni, CENTRAL
AFRICAN REPUBLIC=Bangui, BOTSWANA=Gaberone, BURUNDI=Bujumbura,
BENIN=Po rto-Novo, BULGARIA=Sofia, EGYPT=Cairo, ANGOLA=Luanda,
BURKINA FASO=Ouagadougou, DJIBOUTI=Dijibouti]
* /// , -
El método put() simplemente coloca las claves y va lores en sendos contenedores ArrayList relac ionados. De acuerdo con
la interfaz Map, ti ene que devolver la cla ve anteri or o nuJl si no había clave anterior.
De acuerdo también con las especificaciones de Map, get() devu elve null si la clave no se encuentra en el mapa SlowMap.
Si la clave ex iste, se utiliza para buscar el índice numérico que indica su posición dent ro de la lista de claves keys, y este
número se emplea como índice para generar el va lor asociado a partir de la lista values. Observe que el tipo de key es Object
en get(), en luga r de ser del tipo parametri zad o K como cabría esperar (y que se utili zaba en AssociativeArray.java). Esto
es a consec uencia de la introducción de los genéri cos dentro del lenguaje Ja va en una etapa tan tardía: si los ge néricos hubie-
ran sido un a de las característi cas ori ginales del lenguaje, get() podría haber especificado el tipo de su parámetro.
El método Map.entrySet() debe generar un conjunto de objetos Map.Entry. Sin embargo, Map.Entry es una interfaz que
describe una estructura dependiente de la implementación, por lo que si queremos hacer nuestro propio tipo de mapa, debe-
rem os también definir un a implementación de Map.Entry:
11 : containers / MapEnt r y.java
II Una definición simple de Map.Entry para implementaciones
II de ejemplo de mapas.
import java.util .* ;
problema consiste en mantener las claves ordenadas y luego utilizar Collections.binarySearch() para realizar la búsqueda
(ana li zaremos el proceso correspondiente en un ejercicio).
El almacenamiento fwsh va un paso más allá presuponiendo que en realidad lo único que queremos hacer es almacenar la
clave en alglÍn IlIgar de forma lal que pueda ser encont rada rápidamente. La estrucUlra más rápida en la que se puede alma.
cenar un grupo de elementos es una matriz. así que eso es lo que se utilizará para representar la infonnación de claves (obser-
ve que decimos "información de claves", y no las c laves mismas). Pero, como una matriz no puede cambiar de 18maii.o,
tenemos un problema: queremos almacenar un número indctenninado de va lores en el mapa, pero si el número de va lores
está fijado por el tamaiio de la matriz. ¿cómo podemos solucionar el problema?
La respuesta es que la matriz no almacena las claves. A partir del objeto clave. detenninaremos un número que servirá como
índice dentro de la matriz. Este número es el código ha."íh generado por el método hashCode( ) (en términos de la jerga
infonnática, este método sería la /unción de hash) definido en Object y, normalmente, susti tuido en la clase que es temos
utilizando.
Para resolver el problema de la matriz de tamaiio fijo, es perfectamente posible que haya más de una clave que genere el
mismo índice. En otras palabras, puede haber colisiones. Debido a esto, no importa lo grande que sea la matriz: el código
hash del objeto clave estará almacenado en alguna parte de la matriz.
De modo que el proceso de buscar el valor comienza calculando el código hash y utilizándolo como índice para acceder a
la matriz. Si pudiéramos garantizar que no habrá colisiones (lo cual es posible si disponemos de un número fijo de valores),
tendríamos unafimción hash pe/fec/a, pero eso es un caso especial.1 En todos los demás casos, las colisiones se gestionan
mediante un mecanismo de encadenamiento externo. La matriz no apunta directamente a un valor, sino a una lista de valo-
res. Estos valores se exploran de forma lineal uti lizando el método eq ua ls( ). Por supuesto, este aspecto de la búsqueda es
mucho más lento, pero si la función hash es buena, sólo habrá unos pocos valores en cada posición. De este modo, en lugar
de buscar en la lista completa, saltamos rápidamente a una posición donde sólo tenemos que comparar unas pocas entradas
para encontrar el valor. Esto es mucho más rápido, que es la razón de que H ashMap sea tan ve loz.
Ahora que conocemos los fundamentos de l almacenamiento hash, podemos implementar un mapa has), simple:
//: containers/SimpleHashMap.java
// Un mapa hash de demostración.
import java.util.*¡
import net.mindview.util.*¡
7 El caso de una función hash perfecta está implementado en las estructuras En umMap y Enum Sct de Java SES, porque las enumeraciones definen un
numero fijo de valores. Consulte el Capitulo 19, Tipos enumerados.
552 Piensa en Java
if ( ! found)
buckets (index] . add (pai r ) ;
return oldValue;
return set;
1* Output :
{CAMEROON=Yaounde, CONGO=Brazzaville, CHAD =N'djamena, COTE
D!IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRICAN
REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone,
BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA
FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul,
KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia,
ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo,
BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia,
GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa}
Asmara
[CAMEROON=Yaounde, CONGO=Brazzaville, CHAD=N!djamena, COTE
D!IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRlCAN
REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone,
BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA
FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul,
KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia,
ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo,
BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia,
GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa]
* /1/,-
Como las "posiciones" de una tabla hash se denominan a menudo segmentos (buckels), la matriz que representa a la tabla
se denomina buckets. Para conseguir una distribución uniforme, el número de segmentos es, nonnalmellte, un número
17 Anal isis detallado de los contenedo res 553
primo. 8 Observe que se trata de una matri z de tipo LinkedList, qu e se encarga automát ica mente de las colisiones. Cada
nuevo elemento se añade simplemente al final de la lista correspondiente a un segmento concreto. Incluso aunque Ja va no
nos permite crea r una matriz de genéricos, si que es posible hacer una referencia a dicha matriz. Aqu í resulta útil hacer una
generalización de dicha matriz, para evi tar las proyecciones adicionales de tipos posteriormente en e l código.
Para e l método put( ), se invoca hashCode( ) utilizando la clave y e l resultado se transforma en un número positivo. Para
insertar el número resultante en la matriz buckets, se emplea el operado r de módulo junto con el tamai'i.o de la matriz. Si
dicha posición es nuJl, quiere decir que no hay ningún elemento cuyo código /¡ash se corresponda con esa posición, por lo
que se crea un nuevo objeto LinkedList para a lmacenar el objeto que acaba de ser asignado a esa posición. Sin embargo,
el proceso nomlal consiste en examinar la lista para ver si hay duplicados y, en caso de que los haya, el va lor antiguo se
almacena en oldValue y e l valor nuevo sustitllye al antiguo. El indicador found nos dice si se ha encontrado una pareja
clave-valor antigua y, en caso contrario. la nueva pareja se añade a l final de la lista.
El método get( ) calcula e l indice para la matriz buckets de la misma fonna que put() (esto es importan te con el fin de
garantiza r que tenninemos en la misma posición). Si existe un objeto LinkedList, se le explora en busca de una correspon-
dencia.
Observe que no pretendemos decir que esta implementación esté op timi zada para obtener el mejor rendimiento posible; sólo
tratamos de ilustrar las operaciones realizadas por un mapa has/¡. Si examinamos el código fuente de java.utiI.HashMap,
podremos ver la implementación realmente optimizada. Asimismo, por simpl icidad, SimpleHashMap usa la misma técni-
ca para entr.ySet( ) que ya se utilizó en Slm'\'Map, la cual es demasiado simplista y no funciona para los mapas de propó-
sito general.
Ejercicio 19: ( 1) Repita el Ejercicio 13 utilizando un contenedor SimplcHashMap.
Ejercicio 20: (3) Modifique SimpleHashMap para que informe de las colisiones y pruebe el sistema añadiendo el
mismo conjunto de datos dos veces, con el fin de que se produzcan colisiones.
Ejercicio 21: (2) Modifique SimpleHashMap para que informe del número de "consultas" necesarias cuando se produ-
cen co lisiones. En otras palabras. info rme del número de ll amadas a next( ) que hay que realizar en los
iteradores que recorren las listas enlazadas.
Ejercicio 22: (4) Im plemente los métodos e1ear() y remoye( ) para SimpleHashMap.
Ejercicio 23: (3) Implemente el resto de la interfaz Map para SimpleHashMap.
Ejercicio 24: (5) Siguiendo el ejemplo de SimpleHashMap.jaya, cree y pruebe un contenedor SimpleHashSet.
Ejercicio 25: (6) En lugar de usar un iterador Listlterator para cada segmento, mod ifique MapEntry para que sea una
única li sta enlazada autocontenida (cada objeto MapEntry debe tener un enlace directo al siguiente obje-
to MaIIEntry). Modifique el resto del código de SimpleHashMap.jaya para que func ione correctamente
esta solución.
Sustitución de hashCodeO
Ahora que comprendemos cómo funciona el almacenamiento hash, ti ene más sentido escribir nuestro propio mé todo
hashCode( ).
En primer lugar, nosotros no controlamos la creación del valor concreto que se usa para obtener el índice de la matriz de
segmentos. Ese valor depende de la capacidad del objeto HashMap concreto y dicha capacidad varia dependiendo de lo
lleno que esté el contenedor y de cuál sea el/aclor de carga (describiremos este término más ade lante). Por tanto, e l valor
generado por nuestro método hasbCode( ) se procesará ulterionnente para crear el índice de la matriz de segmentos (en
SimpleHashMap, el cálculo consiste simplemente en hacer una operación de módulo según el tamaño de la matriz de seg-
mentos).
MEn realidad, un numero primo no es en la prácüca el tamaño ideal para los segmentos ¡/asll, y las implementaciones hash más recientes en Java utilizan
un tamaño igual a las potencias de dos (después de haber realizado pruebas exhaustivas). La división o el cálculo del resto es la operación mas lenta en un
procesador moderno. Con una longitud de la tabla hash igual a una potencia de dos, se puede utilizar una operación de enmascaramiento en lugar de la de
di visión. Puesto que get( ) es. con mucho, la operación mas común, % representa una gran parte de! coste y la solución basada en una potencia de dos eli ~
mina este coste (aunque puede que también afecte a algunos métodos hashCode( ».
554 Piensa en Java
El factor más importante a la hora de crear un método hashCode( ) es que, independientemente de cuándo se invoque
hashCode( ). éste debe producir el mismo valor para un objeto concreto cada vez que sea invocado. Si tuviéramos un obje-
10 que produjera un valor hashCodc() al insertarlo con pu t() en un contenedor HashMap y otro valor distinto al extraer-
lo con get(), no podriamos nunca extraer los objetos, Por tan to, si nuestro método has hCode() depende de datos del objeto
que varíen, es necesario informar al usuario de que al modificar los datos se generará una clave diferente, porque se tendrá
un código hashCode( ) diferente,
Además, normalmel11e 110 conviene generar un valor has hCode( ) que esté basado en infonnación de los objetos de carác-
ter distintivo, en concreto. el valor de this genera un código has hCode() no muy bueno. porque eI1lonces es imposible gene-
rar una nueva clave idéntica a la que se ha usado para insertar con pu t() la pareja original clave-valor. Éste era el problema
que ya detectamos en SpringDetector.java. porque la implementación predeterminada de hashCode( ) Ulili:a precisamen-
te la dirección del objeto. Por tanto, lo que conviene es utilizar infonnación del objeto que le identifique de alguna manera
significativa.
Podemos ver un buen ejemplo en la clase String. Las cadenas de caracteres tienen la característica especial de que si un pro-
grama dispone de varios objetos Strin g que contienen secuencias de caracteres idénticas, entonces todos esos objetos Strin g
se corresponden con la misma zona de memoria. Por tanto, tiene bastante sentido que el código hashCode() producido por
dos instancias diferentes de la cadena de caracteres "helio" deba ser idéntico. Podemos ver esto en el siguiente programa:
jj: containersjStringHashCode.java
1* Output: (Sample)
69609650
69609650
*/// ,-
El método hashCode() para String está clara mente basado en el contenido de la cadena de caracteres,
Por tan to, para que un método hashCode( ) sea efect ivo, debe ser rápido y además significativo; en otras palabras, debe
generar un valor basado en el contenido del objeto. Recuerde que este va lor no tiene por qué ser unívoco (lo que nos inte-
resa es la velocidad no la unicidad) pero enrre has hCode() y equals(), la identidad del objeto dcbe ser completamente espe-
cificada .
Puesto que el código generado por hashCode( ) se procesa adicionalmente antes de generar el índice de la ma triz, el rango
de va lores no es importante, basta con que el método genere un va lor int.
Hay otro factor más a tener cuenta: un buen método hashCode( ) debe producir una distribución homogénea de los valo-
res. Si los valores tienden a estar agrupados, entonces el contenedor HashMap o Has hSet estará más cargado en unas áreas
que en otras, y no se rá tan rápido como podría se rl o si se dispusiera de una func ión hash unifonnemente distribuida.
En el libro EJJeclive Java™ Programming Langllage Guide (Addison-Wesley, 2001), Joshua Bloch nos da una receta bási-
ca para generar un método hashCode() aceptable:
1. Almacene algún va lor constante distinto de cero, como por ejemplo 17, en una variable int denomi nada res ult.
2, Por cada campo significati vo f del objeto (es decir, cada campo que sea tenido en cuenta po r el método equals()),
ca lcule un código Itash e de tipo int correspondiente a ese campo:
print (map) ;
for (CountedString cstring : es) {
print ( IlLooking up It + cstringl;
print(map.get(cstring)) ;
/* Output: (Sample)
{String: hi id: 4 hashCode(): 146450=3, String: hi id: 1
hashCode(), 146447=0, String, hi id, 3 hashCode(), 146449=2,
String: hi id: S hashCode{): 146451=4, String: hi
id, 2 hashCode(), 146448=1)
Looking up String: hi id: 1 hashCode(): 146447
O
Looking up String : hi id: 2 hashCode() , 146448
1
Looking up String: hi id: 3 hashCode() , 146449
2
Looking up String : hi id: 4 hashCode() , 146450
3
Looking up String: hi id: 5 hashCode () , 146451
4
* /// ,-
CountedString incluye un objeto String y un id que representa el número de objetos CountedString que contienen un obje-
to String idéntico. El recuento se realiza en el constnlctof, iterando a través del contenedor ArrayList estático en el que
están almacenadas todas las cadenas de caracteres.
Tanto hashCode() como equals( ) generan resultados basados en ambos campos; si estuvieran basados simplemente en el
objeto String o en el valor id, habria correspondencias duplicadas para valores diferentes
En main(), se crean varios objetos CountedString utilizando el mismo objeto String, con el fin de demostrar que los dupli-
cados crean valores distintos, debido al campo id que se utiliza como recuento. El ejemplo muestra el contenedor HashMap.
para que veamos cómo se almacenan los elementos internamente (no hay ningún orden discernible), y después se busca cada
clave individualmente para demostrar que el mecanismo de búsqueda funciona correctamente.
Como segundo ejemplo, considere la clase Individual que ya usamos como clase base para la biblioteca typeinfo.pet defi-
nida en el Capiru lo 14, Información de lipos. La clase Individual se usaba en dicho ca pirulo, pero habiamos retardado su
definición hasta este capitulo con el fin de que el lector comprendiera la implementación:
jj : typeinfojpetsjIndividual.java
package typeinfo.pets;
El método compareTo( ) tiene una jerarquía de comparaciones, de modo que producirá una secuencia que estará ordenada
primero por el tipo real, y luego por Dame si es que existe y finalmente por orden de creación. He aquí un ejemplo que
demuestra cómo funciona:
jj : containersjlndividualTest.java
import holding.MapOfList¡
import typeinfo.pets.*¡
import java.util.*¡
j * Output:
[Cat Elsie May, Cat Pinkola, Cat Shackleton, Cat Stanford
aka Stinky el Negro, Cymric Molly, Dog Margrett, Mutt Spot,
Pug Louie aka Louis Snorkelste in Dupree, Rat Fizzy,
Rat Freckly, Rat Fuzzy)
*jjj.-
Puesto que todas estas mascotas utilizadas en el ejemplo tienen nombres, se las ordena primero por tipo y luego, dentro de
cada tipo, según su nombre.
Escribir sendos métodos hashCode( ) y equals( ) para una nueva clase puede resultar complicado. Puede encontrar herra-
mientas que le ayudarán a hacerlo en el proyecto "Jakarta Commons" de Apache, en jakal'la. apache. org/commons, en el
subdirectorio «Iang" (este proyecto dispone también de muchas otras bibliotecas potencialmente útiles, y parece ser la res-
puesta de la comunidad Java a la comunidad www.boosl.orgde C++).
Ejercicio 26: (2) Añada un campo char a CountedString que también se inicialice en el constructor, y modifique los
métodos hashCode() y equals( ) para incluir el valor de este campo charo
Ejercicio 27: (3) Modifique el método hashCode() en CountedString.java eliminando la combinación con id, y
demuestre que CountedString sigue funcionando como clave. ¿Cuál es el problema con esta técnica?
Ejercicio 28: (4) Modifique net/mindview/utiUTuple.java para convertirla en una clase de propósito general añadien-
do hashCode( ), equals(), e implementando Comparable para cada tipo de Tuple.
558 Piensa en Java
Los diferentes tipos de Queue en la biblioteca Ja\a se diferencian sólo en la fonna en que accptan y devueh'cn los valores
(\'cremos la importancia de esto en el Capínllo 11. Concurrencia).
La disllnción entre unos contenedores y otros usualmente reside en el almacenamiento que se utiliza C0l110 respaldo: es decir.
en las estructuras de datos que implementan fisicamente la interfaz deseada. Por ejemplo. como Arra~ List y LinkedList
implementan la interfaz List. las operacioncs búsicas de List son iguales independientemente de cuál utilicemos. Sin embar-
go. ArrayList utiliza como respaldo una matriz mientras que LinkedList se implementa en la forma usual de las listas
doblcmente enlazadas. en fonna de objetos individuales cada uno de los cuales contiene tanto los datos como sendas refe-
rencias a los elementos anterior y siguiente de la lista. Debido a esto, si queremos hacer muchas inserciones y eliminacio-
nes en mitad de una lista, la elección apropiada será LinkedList (LinkedList dispone también de funcionalidad adicional
que eSHí definida en AbstractSeque ntiaIList). Si no es el caso, ArrayList suele ser más rápido.
Como ejemp lo adicional, podemos imp lementar un conjunto mediante los contenedores TreeSet. Bas hSet O
LinkedHas hSet.9 Cada uno de estos contenedores tiene diferentes comportamientos; l-Ias hSet es para uso normal y pro-
porciona una gran velocidad en las búsquedas. Lin kedHas hSet mantiene las parejas en orden de inserción y TreeSet está
respaldado por un contenedor TreeMap y está diseiiado para disponer de un conjunto constantemente ordenado. La imple-
mentación sc elige basándose en el comportamiento que se necesite.
En ocasiones. las diferentes implementaciones de un contenedor concreto tendrán una serie de operaciones en común. pero
el rendimiento de esas operaciones será diferente. En este caso, la selección entre unas implementaciones y otras se basa en
la frecuencia con la que se utilice una operación concreta y lo veloz que necesitemos que sea esa operación. Para casos como
estos, una de las maneras de evaluar las diferencias entre las distintas implementaciones de contenedores es mediante una
prueba de rendimiento.
9 O como EnumSeI o Co p~ OnWrilcA rnl~'Sct . que son casos especiales. Aunque somos conscientes dI.! que puede haber muchas otra~ implementaciones
adicionales especializadas de diversas interfaces de contenedores, en esta sección estamos tmtando de examinar 5610 los casos más generales.
10 Krlysztof Sobolewski me ayudó a diseñar los genéricos de este ejemplo.
17 Analisls detallado de los contenedores 559
Para cada con tenedor se realizará una secuenci a de llamadas a test(). cada una con un objeto TestParam diferente, de modo
que TestParam también contiene métodos array( ) estát icos que hacen que resulte fácil crear matrices de objetos
TestParam. La primera versIón de array() toma una lista de argumentos variabl es que contiene va lores size y loops alter-
nantes, mientras que la segunda versión toma el mismo tipo de li sta. aunque con valores almacenados dentro de objetos
String. de esta forma, puede utilizarse para analizar argumentos de la linea de comandos:
/1: concainers/TestParam.java
// Un "objeto de transferencia de datos".
Para utilizar el marco de trabajo, lo que hacemos es pasar el contenedor que hay que probar junto con una lista de objetos
Test a un método Tester.run( ) (se trata de métodos genéricos de utilidad sobrecargados, que reducen la cantidad de texto
que hay que escribir para utilizarlos). Tester.run( ) invoca e l constructor sobrecargado apropiado y luego llama a
timedTest(), que ejecuta para ese contenedor cada una de las pmebas de la lista. timedTest( ) repile cada prueba para cada
uno de los objetos TestParam contenidos en paramList. Puesto que pararnList se inicializa a partir de la matriz estática
defaultParams, podemos cambiar la lista paramList para todas las pruebas resaignando defaultParams, o bien podemos
modificar la lista paramList para una prueba determinada, pasándole para esa prueba una lista paramList personalizada:
1/ : containers/Tester.java
// Aplica objetos Test a listas de diferentes contenedores.
import java.util.*;
System.out.println() ;
}
///,-
Los métodos stringField() y numberField() producen cadenas de fonnateo para imprimir los resultados. La anchura están-
dar de formateo puede variarse modificando el val or estáti co fieldWidth . El método displayHeader() da fonnato e impri-
me la infonnación de cabecera para cada prueba .
Si necesitamos realizar una inicialización especial, sustituimos el método initialize( ). Esto produce un objeto contenedor
ini cializado con el tamaño apropiado; podemos modificar el objeto contenedor existente o crear uno nuevo. Como puede
ver en test() el resultado se captura en una referencia local denominada kontainer, que permite sustituir el miembro alma-
cenado container por un contenedor inicializado completamente diferente.
El valor de retorno de cada método Test.test( ) debe ser el número de operaciones realizado por di cha prueba. 10 que se
utiliza para calcular el número de nanosegundos requerid o por cada operac ión . Hay que ten er en cuenta qu e
System.nanoTime() produce nonnalmente valores con una granularidad mayor que uno (y esta granularidad variará de una
máquina a otra y de un sistema operati vo a otro), lo que produce un cierto error estadísti co en los resultados.
Los resultados pueden variar de una máquina a otra, estas pruebas sólo pretenden comparar el rendimiento de los diferen-
tes contenedores.
II Método de utilidad:
public static void run(List<Integer> list,
564 Piensa en Java
List<Test<List<Integer»> tests) {
new ListTester(list, tests) .timedTest(};
Tester.fieldWidth = 12;
Tester<LinkedList<Integer» qTest =
new Tester<LinkedList<Integer»(
new LinkedList<Integer>(), qTests);
qTest . setHeadline ( "Queue tests");
qTest.timedTest() ;
/ * Output: (Samp1e)
- - - Array as List
size get set
10 130 183
100 130 164
1000 129 165
10000 129 165
--------------------- ArrayList ---------------------
size add get set iteradd insert remove
10 121 139 191 435 3952 446
100 72 141 191 247 3934 296
1000 98 141 194 839 2202 923
10000 122 144 190 6880 14042 7333
- - - - - - -- - - - - - - - - - - - - - LinkedList
size add get set iteradd insert remove
10 182 164 198 658 366 262
100 106 202 230 457 108 201
1000 133 1289 1353 430 136 239
10000 172 13648 13187 435 255 239
- - - - - - - - - - - - - - - - - - - - - - - Vector - - - -- - - - - - - - - - - - - - - - - - -
size add get set iteradd ir.sert remove
10 129 145 187 290 3635 253
100 72 144 190 263 3691 292
1000 99 145 193 846 2162 927
1 0000 108 145 186 6871 14730 7135
17 Análisis detallado de los contenedores 565
Para las operaciones get( ) y set( ) de acceso aleatorio, una lista ft'spaldada por una matriz es ligeramente más rápida que
ArrayList. pero esas mismas operaciones son muchísimo más caras para LinkedList porque este comenedor no está dise-
¡iado para operaciones de acceso aleawrio.
Es necesario evitar la utilización de V{'ctor: sólo se ha incluido en la biblioteca para soporte del código heredado (la única
razón de que funcione en estc programa es porque fue ada piado para ser de lipa List por razones de compatibilidad Con
sucesivas versiones).
La mejor solución consiste probablemente en elegir ArrayList como contenedor predetemlinado y cambiar a LinkedList
si hace falta su funcionalidad adicional o si descubre problemas de rendllniento debido a que se hacen múltiples inserciones
y eliminaciones en mitad de la lista. Si estamos trabajando con un grupo de elementos de tamaño fijo, deberemos emplear
°
una lista respaldada por una matriz (como las que produce Arrays.asList( )). en caso necesario, una verdadera matriz.
CopyOn\VritcArrayList es una implementación especial de List que se uti li za en programación concurrente y de la que
hablaremos en el Capinl10 21 . Concurrencia.
Ejercicio 29: (2) Modifique ListPerforrnance.java para que las listas almacenen objetos String en lugar de Integer.
Util ice el objeto Generator del Capitulo 16. Matrices, para crear va lores de pmeba.
Ejercicio 30: (3) Compare el rendimiento de Collections.sort() en ArrayList y en LinkedList.
Ejercicio 31: (5) Cree un contenedor que encapsule una matriz de objetos String y que sólo permita añadir y extraer
cade nas de caracteres. de modo que no sllljan problemas de proyección de tipos durante la utilización del
contenedor. Si la matriz no es lo suficien temen te grande para la siguiente inserción. el contenedor debe
red imensionar automáticamente la 1113tl;Z. En maine ), compare el rendimiento de este conte nedor con el
de ArrayList<String>.
Ejercicio 32: (2) Repita el ejerc icio ant erior para un comenedor de iut y compare el rendimient o con e l de ArrayList
<Integer>. En la comparación de rendimiento, incluya el proceso de incrementar cada objeto en el conte-
nedor.
Ejercicio 33: (5) Cree una lista FastTraversalLinkedList que utilice internamente un contenedor LinkedList para
conseguir inserciones y eliminacio nes rápidas de elementos y un contenedor ArrayList para realizar reco-
rridos rápidos de los elementos y operaciones get() rápidas. Pmebc la so lución moditic3ndo
ListPerformance.java.
// {RUnByHand}
import static net.mindview.util.Print.*;
el se
usage() ;
while(it.hasNext{})
it.next() i
1* Output: (Samp le )
------------- TreeSet ------------ -
size add contains iterate
10 746 173 89
100 501 264 68
1000 714 410 69
10000 1975 552 69
------------- HashSet - - - - - - - - - - - - -
size add contains iterate
10 308 91 94
100 178 75 73
1000 216 110 72
10000 711 215 100
---- ------ LinkedHashSet - - - - - - - - - -
size add contains iterate
10 350 65 83
100 270 74 55
1000 303 111 54
10000 1615 256 58
*/// ,-
17 Aná lisis deta llado de los contenedores 569
El rendimjento de Has hSet generalmente es superior al de TreeSet, pero especialmente a la hora de añadir elementos y de
buscarlos, que son las dos operaciones más importantes. TreeSet existe porque mantiene sus elementos en orden, de modo
que sólo se suele utili zar cuando hace falta un contenedor Set ordenado. Debido a la estructura interna necesaria para sopor-
tar las ordenaciones y debido a que la iteración suele ser una operación muy común, la iteración suele resultar más rápida
con TreeSel que con Has hSet.
Observe que Linkcd Has hSet es más caro respecto a las inserciones que HashSet; eslO se debe al coste adicional de man-
tener la lista enlazada además del contenedor hash.
Ejercicio 34: ( 1) Modifique SelPerform ance.java para que los conjuntos abnacenen objetos Slring en lugar de obje.
tos Inlege r. Utilice el objeto Gener ator del Capitulo 16, Matrices para crear valores de prueba.
/ * Output: (Sample)
--- - ------ TreeMap --- -- -----
size put get iterate
10 748 168 100
100 506 264 76
1000 771 4 50 78
10000 2962 561 83
--- -- ----- HashMap --- --- ----
size put get iterate
10 281 76 93
100 179 70 73
1000 267 102 72
10000 1305 265 97
--- -- - - LinkedHashMap ----- --
size put g e t iterate
10 354 100 72
100 273 89 50
1000 385 222 56
10000 2787 341 56
IdentityHashMap
size put get iterate
10 290 144 101
100 204 287 132
1000 508 336 77
10000 767 266 56
-------- WeakHashMap - - --- - --
size put get iterate
10 48 4 146 151
100 292 126 11 7
1000 411 136 152
10000 2165 138 555
------ - -- Hasht able -- - - - ----
s i ze put get iterate
10 264 11 3 113
100 181 105 76
1 000 260 201 80
1 00 0 0 1245 13 4 77
* /// ,-
Las inserciones en todas las implementaciones de Map excepto en IdentityHashMap van siendo significativamente más
lentas a medida que el tamaño del mapa se incrementa. Sin embargo, en general, la búsqueda es mucho menos costosa que
la inserción, lo cual resulta muy adecuado, porque lo normal es que busquemos elementos con Illucha más frecuencia de la
que los insertamos.
El rendimiento de Haslttable es aproximadamente igual al de Ha,ltMap. Puesto que Ha,hMap pretende sustitui r a
Hashtable, y utiliza por tanto la misma estmctura subyacente de almacenamiento y el mismo mecanismo de búsqueda (de
lo que hablaremos posterionnente), no resulta demasiado sorprendente que tenga un rendimiento mejor.
17 Analisis detallado de los contenedores 571
TreeMap es ge neral mente mas ICllIo que HashMap . Al igua l que sucede con TreeSet, TreeMap es una foml a de crear una
lista ord enad a. El com ponamiento de un árbo l es tal que sus elementos siempre están en orden, sin que sea necesario orde-
narlos especialmente. Una vez rell enado un contenedor TreeMap . podemos invocar keySet() para obtener una vista de tipo
Set de las c1a\'cs y luego podemos llamar a toArray( ) para generar una matriz de dichas claves. Podemos a continuación
emplear un método estático Arrays.binarySearch() para localizar rápidamente objetos en esa matriz ordenada. Por supues-
to. esto sólo tiene semido si el co mportamicmo de un contenedor HashMap no es aceptable, ya que HashMap está diseña-
do para poder localizar rápidamente las claves. Asimismo, podemos crear rápidamente un contenedor HashMap a partir de
otro de tipo TreeMap mediante una única creación de objeto o una llamada a putAII(). En resumen, cuando es temos utili-
zando un mapa nuestra primera elección deberá ser HashMap, y sólo deberíamos recurrir a TreeMap si lo que necesitamos
es un mapa que esté constantemente ordenado.
LinkedHas h.M ap tiende a se r más lento que l-IashMap para las inserciones. porque mantiene la lista enlazada (con el fin
de preservar el orden de inserción), además de mantener la estructura de datos hash. Sin embargo. debido a esta lista, la ite-
ración es más rápida.
ldentit)'l:JashMap tien e un rendimi ento di sti nto porque utiliza = en lugar de equals( ) para hacer las co mparaciones.
WeakHashMap se describe más adelante en el capitulo.
Ejercicio 35: (1) Modifique MapPerformance.java para incluir pruebas de SlowMap.
Ejercicio 36: (5) Modifique SlowMap de modo que, en lugar de dos contenedores ArrayList, almacene un único
ArrayList de objetos MapEntry. Verifique que la ve rsión modificada funciona correctamente. Utilizando
MapPerformance.java, compruebe la velocidad de su nuevo mapa. Ahora cambie el método put( ) de
modo que realice con sort() una ordenación después de introducir cada pareja y modifique get() para uti-
lizar Collections.binarySearch() con el fin de buscar la clave. Compare el rendimiento de la nu eva ve r-
sión con el de las anterio res.
Ejercicio 37: (2) Modifique SimpleHashMap para utili zar contenedores ArrayList en lugar de LinkedList. Modifique
MapPerrormancc.ja\'3 para comparar el rendimiento de las dos implementaciones.
11 En tln mensaje privado, Joshua Bloch escribió: ..... Creo que hemos cometido un error al incluir detalles de implementación (como el factor de carga y
el tamaño de las tablas hash) en nuestras API . El cliente debería. quizá. decirnos el tamaño máximo esperado de una colección y nosotros deberíamos actuar
572 Piensa en Java
Ejercicio 38 : (3) Examine la clase HashMap de la documentación del JDK. Cree un contenedor HashMap, rellénelo
con elementos y determine el facto r de carga. Pruebe la ve locidad de búsqueda con este mapa, luego trate
de incrementar la ve locidad creando un nuevo contenedor HasbMap con una capacidad inicial mayor y
copiando el mapa antiguo en el nuevo; después, ejecute otra vez la prueba de velocidad de búsqueda co n
el nuevo mapa.
Ejercicio 39: (6) Añada un método privado rehash() a SimpleHashMap que se invoque cuando el factor de carga exce-
da de 0.75. Durante el cambio automático de tamaño, duplique el número de segmentos y luego busque el
primer número primo superior a ese número. co n el fin de determinar el nuevo número de segmentos.
Utilidades
Hay diversa s utilidades autónomas para contenedores, expresadas como métodos estaUcos dentro de la clase
java.util.Collections. Ya hemos visto algunas de ellas, como addAII( ), reverseOrder( ) y binarySearch( ). He aquí las
otras (las utilidades de tipo sy nch ronized y unmodifiable se c ubrirán en las secciones siguientes). En esta tabla, se usan
genéricos allí donde son re levantes:
checkedCollection(Collection<T>, Produce una vista dinámicamente segura con respecto é1 tipos de Collection,
Class<T> type) o de un subtipo específico de Collection. Utilice este método cuando no sea
checkedList(List<r>, Cla ss<T> type) posible emplear la versión con comprobación esuitica. Ya hemos visto estos
checkedMap(Map<K, V>, métodos en el Capitulo 15, Genéricos, en la secc ión "Seguridad dinámica de
Class<K> keyType, Class<V> valucTypc) tipos".
checkedSet(Set<T>, Class<T> I)'pe)
checkedSortcdMap(Sorted Map<K, V>,
C lass<K> keyType, Class<V>
valueType)checkedSortedSet(SorledSet<T>,
C lass<T> type)
max(Co llection, Comparator) Devue lve el elemento máximo o mínimo del objeto Co lleerion utilizando el
min(Co llection, Comparator) objeto Comparator.
indexOrsubList(List SOllree, List (arget) Devuelve el índice de inicio del primer lugar donde target aparece dentro de
so u ree, o 2 1 si no aparece.
lastlndexOfSubLisl(List so urce, List targct) Devuel ve el índice de inicio delú/timo lugar donde target aparece dentro de
source, o 21 si no aparece.
rotate(List, inl distanee) Mueve todos los elementos hacia adelante una distancia distance, extrayén-
dolos por el ex tremo y volviendo a colocarlos al principi o.
II(cOlllimwciólI) a panir de ahi. Los clientes pueden, fácilmente, generar más problemas que beneficios al seleccionar los valores de estos parámetros.
Como ejemplo exagerado, considera el valor eapacitylDcrement de V<,ctor. Nadie debería configurar este valor y no deberíamos haberlo incluido. Si se
le asigna un va lor distinto de cero. el coste asintótico de una secuencia de adiciones pasa de lineal a cuadrático. En Olras palabras, el rendimiento se viene
abajo. Con el paso del tiempo, carla vez tenemos más experiencia con este tipo de cosa. Si examinas Id entityHashMap, verás que DO dispone de ningún
parámetro de optimización de bajo nivel".
17 Análisis detallado de los contenedores 573
so rt (List<T» sort(List<T>, Ordena la lista List<T> utilizando una ordenación natural. La segunda forma
Comparator<? super T> e) permite proporcionar un objeto C ompara tor para la ordenación.
copy(Li st<? super T> des l, Copia elementos desde src a des t.
List<? ext ends T> src)
swap(Li st, inl i, inl j) Intercambios los elementos en las posiciones i y j dentro de List.
Probablemente más rápido que los métodos que pudiéramos escribir no-
sotros.
fill (List<? super T>, T x) Sustituye todos los elementos de la lista por x.
nCo pies(int n, T x) Devuelve una lista inmutable List<T> de tamaiio n cuyas referencias apun~
tan todas a x.
d isj oint(Coll ection, C ol1ecti on) Devuelve tru e si las dos colecciones no tienen elementos en común.
frequ ency(Coll ection, Object x) Devuelve el número de elementos de Coll ection que sean iguales a x.
emptyList( ) Devuelve un objeto List. M ap o Set vacío inmutable. Son objetos genéricos,
emlltyM ap( ) emptySetO por lo que el objeto Co llec tion resultante estará parametrizado con el tipo
deseado.
sin gleton (T x) Produce un objeto Sct<T>, List<T>, o Map< K,V> inmutable que contiene
si ngletonList(T x) una única entrada basada en los argumentos proporcionados.
sin gletonMa p( K kcy, V valu e)
list(E num eration<T> e) Produce Wl objeto ArrayList<T> que contiene los elementos en el orden en
el que son devueltos por el (antiguo) objeto Enumeration (p redecesor de
Iter at or). Para la conversión de código heredado.
enum era ti on(C oll ection <T» Genera un objeto Enum era tion<T> de estilo antiguo para el argumento.
Observe que min() y max( ) fu ncionan con objetos Collectio n no con listas, por lo que no es necesario preocuparse acer-
ca de si la colección ya está ordenada o no (como ya hemos mencionado anteriormente, sí que es necesario ordenar con
sort() una lista o una matriz antes de realizar una búsqueda binaria con bin a rySea rch( ».
He aquí un ejemplo que muestra el uso básico de la mayoría de las utilidades de la tabla anterior:
JJ : containers / Utilities.java
JJ Ejemplo simple de las utilidades para colecciones.
import java.util.*i
import static net.mindview.util.Print.*¡
Collections.disjoint {list,
Collections.singletonList("Four" ») ) i
print ( "max: " + Collections. max (list ) } i
print("min: " + Collections.min(list » i
print(" max w/ comparator: 11 + CollectionS.max (list,
574 Piensa en Java
String.CASE_"NSENS"TIVE_ORDER» ;
print ("min w/ comparator: " + Collections. min (list,
String.CASE_INSENSITIVE_ORDER» ;
List<String~ sublist =
Arrays,asList("Four five six".split{" "»;
print (I! indexOfSubList: lO +
Collections.indexOfSubList(list, sublist»;
print (" lastlndexOfSubList: 11 +
Collections.lastlndexOfSubList(list, sublist»;
Collections.replaceAll(list, "ane", "Yo");
print(UreplaceAll: " + list);
Collections.reverse{list) ;
print(ttreverse: " + list);
Collections.rotate(list, 3);
print ("rotate: " + list);
List<String> source =
Arrays.asList{lIin the matrix",split(U 11»;
Collections.copy(list, source);
print{"copy: " + list);
Collections. swap (list, O, list.size() - 1);
print {"swap: 11 + list )¡
Collections.shuffle(list, new Random (47»;
print("shuffled: 11 + list) ¡
Collections. fill (list, "pOp");
print{lifill: 11 + list) ¡
print{Ufrequency of 'pop': " +
Collections.frequency(list, "pOp"»;
List<String> dups = Collections.nCopies(3, IIsnapU);
print("dups: n + dups) ¡
print (11 'list' disjoint 'dups'?: 11 +
Collections.disjoint(list, dups»;
11 Obtención de un objeto Enumeration al estile antiguo:
Enumeration<String> e = Collections.enumeration(dups);
Vector<String> v = new Vector<String>() ¡
while{e.hasMoreElements(»
v.addElement(e.nextElement(» ;
1/ Conversión de un vector de estilo antiguo
11 en una lista a través de un objeto Enumeration:
ArrayList<String> arrayList =
Collections .list (v. elements () ) ;
print ( " arrayList: + arrayList);
11
/ * Output:
[one, Two, three, Four, five, six, one)
t list t disjoint (Four)?: false
max: three
min: Four
max wl comparator : Two
min wl comparator: five
indexOfSubList: 3
lastlndexOfSubList : 3
replaceAll: [Yo, Two, three, Four, five, six, Yo]
reverse: [Yo, six, five, Four, three, Two, Yo]
rotate: [three, Two, Yo, Yo, six, five, Four]
copy: (in, the, matrix, Yo, six, five, Four)
swap: [Four, the, matrix, Yo, six, five, in]
shuffled: [six, matrix, the, Four, Yo, five, in]
till, (pop, pop, pop, pop, pop, pop, pop)
frequency of 'pop': 7
17 Análisis detallado de los contenedores 575
La salida explica el comportamiento de cada método de utilidad. Observe la diferencia en min( ) y max( ) con el objeto
String.CASE_INSENSITIVE_ORDER Compar.tor debido a la utilización de mayúsculas y minúsculas.
1* Output:
[one, Two, three, Four, five, six, one, ane, Two, three,
Four, five, six, one]
Shuffled: [Four, five, one, one, Two, six, six, three,
three, five, Four, Two, ane, ane]
Trimmed : (Four, five, one, one, Two, six, six, three,
three, five]
Sorted: [Four, Two, five, five, one, one, six, six, three,
three]
Location of six is 7, list.get(7) = six
Case-insensitive sorted: [five, five, Faur, ane, ane, six,
576 Piensa en Java
/ * Output:
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASOI
ALGERIA
[BULGARIA, BURKINA FASO, BOTSWANA, BENIN, ANGOLA, ALGERIAI
{BULGARIA=Sofia, BURKINA FASO=Ouagadougou, BOTSWANA=Gaberone,
BENIN=Porto-Novo, ANGOLA =Luanda, ALGERIA=Algiers}
* ///,-
La in vocación del método "un modifiable" (no modificable) para un tipo concreto no hace que se generen advertencias en
tiempo de compilac ión, pero una vez que la transfonnac ión se ha producido, cualquier llamada a un método que modifique
el contenido de un contenedor concreto generará una excepción UnsupportedOperationE xception.
En cada caso, debe rellenar el contenedor con datos significati vos antes de hacerlo de sólo lec tura. Una vez cargado, lo mej or
es susti tuir la refe rencia existente por la referencia generada por la llamada al método "unmodifiable". De esa fonna, no
corremos el riesgo de tratar de modificar acc identalmente el contenido una vez que hemos dicho que no es modificab le. Por
otro lado, esta herramienta también pennite mantener el contenedor modificable como privado dentro de una clase y devol-
ver una referen cia de sólo lectura a di cho co ntenedor desde una llamada a método. De este modo, podemos cambiar el con-
tenedor dentro de la clase, pero desde cualquier otro lugar sólo podrá leerse.
Lo mejor es pasar inmediatamente el nuevo contenedor a través del apropiado método ··synchronized'o. como se muestra en
el ejemplo. De esa forma. no ex iste ninguna posibilidad de que la versión no sincronizada quede accidentalmente expuesta.
Fallo rápido
Los contenedores de Java tamb ién tienen un mecanismo para evitar que más de un proceso modifique el contenido de un
contenedor. El problema se presenta si estamos en mitad de un proceso de iteración a través de un contenedor y entonces
algún otro proceso interviene e inserta, modifica o elimina un objeto de dicho con tenedor. Puede que ya hayamos pasado
dicho elemento del contenedor, O puede que todavía no hayamos llegado a ese elemento, puede que el tamaño del contene-
dor se reduzca después de que hayamos invocado size( ) ... existen muchas posibilidades de que se produzca un desastre. La
biblioteca de con tenedo res de Java utiliza un mecanismo de fallo rápido que exami na si se ha producido en el con tenedor
algún cambio distinto de aquellos de los que nuestro proceso es personalmente responsable. Si detecta que alguien más está
modificando el contenedor. genera inmediatamente una excepción ConcurrentJ\llodificationException. A eso se refiere el
término de fallo rápido: no trata de detectar un problema más adelante utilizando un algoritmo más complejo.
Resulta basta nt e fácil ver el mecani smo de fallo rápido en acción: lo único que hace falta es crear un iterador y luego aña-
dir algo a la colección a la que el iterador esté apuntando, como el ejemplo siguiente:
jI : containers / FailFast.java
II Ilustración del comportamiento del 11 fallo rápida".
import java.util.*¡
j* Output:
java.util.ConcurrentModificationException
*jjj,-
La excepc ión se produce porque se ha incl uido algo en el contenedor después de que se haya adquirido el iterador para ese
contenedor. La posibilidad de que dos partes del programa puedan modificar el mismo contenedor hace que acabemos en
un estado incierto, por lo que la excepción nos notifica que es necesario modificar el código; en este caso, habrá que adqui-
rir el ¡terador después de haber añadido todos los elementos al co ntenedor.
Los contenedores COllcurrentHashMap, CopyOnWriteArrayList y CopyOnWriteArraySet utilizan técni cas que impi-
den que se genere la excepción ConcurrentModíficationException.
Almacenamiento de referencias
La biblioteca java.lang.ref contiene un conjunto de clases que aumentan la flexibilidad del mecani smo de depuración de
memoria. Estas clases son especialmente útiles cuando tenemos objetos de gran tamaño que puedan hacer que la memoria
se agote. Existen tres clases heredadas de la clase abstracta Reference : SoftReference, WeakReference y
PhantomReference. Cada una de ellas proporciona un nivel distinto de indirección para el depurador de memoria si el obje-
to en cuestión sólo es alcanzable a través de uno de estos objetos Reference.
Si un objeto es alcanoable, quiere decir que en algún lugar del programa puede encontrarse el objeto. Esto puede querer
decir que disponemos de una referencia nonual en la pila que apunta directamente al objeto, pero también podemos tener
una referencia a un objeto que tenga a su vez una referencia al objeto en cuestión; puede incluso haber muchos enlaces inter-
medios. Si un objeto es alcanzable, el depurador de memoria no puede ignorarlo, porque sigue siendo utilizado por el pro·
grama. Si el objeto no es alcanzable, no hay ninguna fonna de que nuestTo programa lo use, así que resulta seguro depurar
dicho objeto de la memoria.
17 Análisis detallado de los contenedores 579
Utilizamos objetos Reference cuando queremos continuar almacenando una referencia al objeto (es decir, queremos poder
alcanzar el objeto), pero también querernos pennitir que el depurador de memoria libere el objeto. De este modo, tenemos
una fanTIa de utilizar el objeto. pero si está a punto de agotarse la memoria, dejamos que ese objeto sea eliminado.
Para hacer esto, utilizamos un objeto Reference como intemlediario (pro.\y) entre nosotros y la referencia normal. Además,
no debe haber referencias nomlales al objeto (es decir. referencias que no estén envueltas dentro de objetos de tipo
Reference). Si el depurador de memoria descubre que un objeto es alcanzable a través de una referencia normal. no podrá
liberar dicho objeto.
Ordenando los distintos tipos de referencias SoftReference, WeakReference y PhantomReference, cada uno de e llos es
"más débil" que el anterior y se corresponde con un nivel distinto de alcanzabilidad. Las referencias blandas (50ft referen-
ces) son para implementar cachés se nsib les a la memoria. Las referencias débiles (weak references) sirven para implemen-
tar "mapas canónicos" (con los que pueden usarse instancias de objetos simultáneamente en múltiples lugares de un
programa, con el fin de ahorrar espacio de almacenamiento) que no impiden que sus claves o valores sean eliminados. Las
referencias fantasma (phanlom relerences) sirven para planificar acciones de limpieza pre-mortem de una manera más fle-
xible de lo que puede conseguirse con el mecanismo de flllalización de Java.
Con SoftReference y \VeakReference, tenemos la opción de situar esas referencias en una cola de referencias de tipo
ReferenceQucue (que se utiliza para acciones de limpieza pre-mortem). pero una referencia PhantornReference tiene obli-
gatoriamente que construirse dentro de una cola ReferenceQueue. He aquí un ejemplo simple:
11: containers/References.java
II Ejemplo de objetos Reference
import java. lang.ref.*i
import java.util.*;
class VeryBig {
private static final int SIZE = 10000;
private long [] la = new long [SIZE] ;
private String ident;
public VeryBig (String id) { ident id; )
public String toString () { return ident; }
protected void finalize() {
System.out.println(!!Finalizing !! + ident);
LinkedList<WeakReference<VeryBig» wa =
new LinkedList<WeakReference<VeryBig»();
580 Piensa en Java
SoftReference<VeryBig> s =
new SoftReference<VeryBig> (new VeryBig ( IISoft ji) ) ;
WeakReference<VeryBig> w =
new WeakReference<VeryBig> (new VeryBig ( "Weak H ) ;
System.gcll;
LinkedLisc<PhantomReference<VeryBig» pa =
new LinkedLisc<PhantomReference<VeryBig»() i
far(int i = o; i <: size¡ i++) {
pa.add(new PhantomReference<VeryBig>(
new VeryBig ( " Phantom " + i), rq});
System . out.println(IIJust created: " + pa.getLast());
checkQueue() ;
WeakHashMap
La biblioteca de contenedores dispone de un tipo especial de mapa para almacenar referencias débiles Wea kH as hMap. Esta
clase está discliada para facilitar la creación de mapas canónicos. En dicho tipo de mapas se ahorra espacio de almacena-
miento creando una instancia de cada va lor concreto. Cuando el programa necesita dicho valor, busca el objeto existente en
el mapa y lo utiliza (en lugar de crear uno partiendo de cero). El mapa puede construir los va lores co mo parte de su proce-
so de inicialización, pero lo más probable es que los va lores se constmyan a medida que so n necesarios.
Puesto que se trata de una téc ni ca de ahorro de espacio de almacenamiento, resulta bastante útil que Wea k Has hMa p per-
mita al depurador de memoria limpiar automáticamente las claves y los valores. No hace fa lta hacer nada especial con las
claves y valores que se incluyan en el contenedor Wea kH as hMap ; dichas claves y valores son envueltos automáticamente
por el mapa en referencias de tipo Weak Refe rence. Lo qu e hace que quede permitida la tarea de limpieza del depurador de
memoria es que la clave ya no esté siendo utili zada, co mo se ilustra en el siguiente ejemplo:
11: containers/CanonicalMapping.java
II Ilustra el contenedor WeakHashMap .
import java.util.*¡
class Element {
private String ident¡
public Element (String id) { ident = id; }
public String toString() { return ident; }
public int hashCode () { return ident .hashCode ();
public boolean equals (Obj ect r) {
return r instanceof Element &&
ident . equals ( ( (Element ) r) . ident) ¡
System.gc() i
Vector y Enumeration
El único tipo de secuencia auto-expansiva en Java 1.0/ 1.1 era Vector, así que dicho contenedor se utilizaba con gran fre-
cuencia. Sus defectos son demasiado numerosos como para describirlos aquí (véase la primera edición de este libro, dispo-
nible en inglés para descarga gratuita en www.MindView.nel). Básicamente, podemos considerar este contenedor como un
tipo ArrayList con nombres de métodos muy largos y complicados. En la biblioteca revisada de contenedores Java, se adap-
tó Vector para que pudiera funcionar como un contenedor de tipo a Collection y de tipo List. Esta solución no es excesi-
vame nte buena, ya que podría inducir a algunas personas a pensar que Vector ha mejorado, cuando en realidad sólo se ha
incluido para poder soportar el código Java más anti guo.
Para la versión Java 1.0/ 1.1 del iterador se decidió inventar un nuevo nombre. "enumeration", en lugar de usar el término
con el que todo el mundo estaba fam iliarizado ("iterator"). La interfaz Enumeration es más simple que la de Iterator, con
582 Piensa en Java
sólo dos métodos y utiliza nombres de método más largos: boolean hasMoreElements( ) devuelve true si esta enumera-
ción contiene más elementos y Object nextElement( ) devuelve el siguiente elemento de esta enumeración si es que exis-
te (en caso contrario. genera una excepción).
Enumeration es sólo una interfaz, no una imp lementación, e incluso las nuevas bibliotecas utilizan todavía en ocasiones la
interfaz En umeration, lo que no resulta muy afortunado, aunque tampoco es que genere grandes problemas. En general,
debemos usar Iterator siempre que podamos en nuestro propio código, pero aún así debemos estar preparados para encon-
tramos con bibliotecas en las que nos veamos forzados a manejar contenedores de tipo Enumeration.
Además, podemos generar un contenedor de tipo Enumeration para cualquier contenedor de tipo Collection utili za ndo el
método Collcctions.enumeration(). corno se puede ver en el siguiente ejemplo:
//: containers/Enumerations.java
// Vector y Enumeration de Java 1.0/1.1.
import java.util.*;
import net.mindview.util.*¡
/ * Output:
ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO, BURUNDI, CAMEROON, CAPE VERDE,
CENTRAL AFRICAN REPUBLIC,
*///,-
Para generar un objeto Enumcration, llamamos al método elements(), después de lo cual podemos usarlo para realizar una
iteración en sentido di recto .
La última linea crea un objeto ArrayList y utiliza enumeration( ) para adaptar un objeto Enumcration a partir del itera-
dar de ArrayList Iterator. As í, si disponernos de código antiguo que necesite manejar un objeto Enumeration, podemos
segu ir usando los nuevos contenedores.
Hashtable
Como hemos visto en la comparativa de rendimiento contenida en este ejemplo, el contenedor básico Hashtable es muy
sim ilar a HashMap , incluso en lo que respecta a nombres de métodos. No hay ninguna razón para utilizar Hashtable en
lugar de HashMap en el código nuevo que escribamos.
Stack
El concepto de pila ya ha sido expl icado anterionnente al hablar de LinkcdList. Lo que resulta más confuso acerca del con-
tenedor Stack de Java 1.0/ 1.1 es que en lugar de utilizar un Vector empleando el mecanismo de composición, Stack here-
da de Vector. Por tant o, tiene todas las características y comportamientos de Vector más alguna funcionalidad propia de
Stack. Resulta dificil saber si los diseñadores decidieron conscientemente que ésta era una forma especialmente útil de hacer
las cosas, o si se trata simp lemente de un error absurdo; en cualquier caso. lo que está claro es que nadie revisó el diseño
antes de lanzarlo a distribución, de modo que este diseño incorrecto continúa haciéndose notar todavía hoy en muchos pro-
gramas (pero no debería utilizarse en ningún programa nuevo).
He aquí una ilu strac ión simple de Stack en la que se inserta cada representación de tipo String de una enumeración enum.
El ejemplo muestra también cómo resulta igual de fáci l de utilizar un elemento LinkedList como pila, o bien la clase Stack
creada en el Capítul o 11 , Almacenamiento de objetos:
17 Análisis detallado de los contenedores 583
1* Output:
stack = [JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE,
JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER]
element 5 = JUNE
popping elements:
The last line NOVEMBER OCTOBER SEPTEMBER AUGUST JULY
JUNE MAY APRIL MARCH FEBRUARY JANUARY lstack = [NOVEMBER,
OCTOBER, SEPTEMBER, AUGUST, JULY, JUNE, MAY, APRIL, MARCH,
FEBRUARY, JANUARY]
NOVEMBER OCTOBER SEPTEMBER AUGUST JULY JUNE MAY APRIL MARCH
FEBRUARY JANUARY stack2 = [NOVEMBER, OCTOBER, SEPTEMBER,
AUGUST, JULY, JUNE, MAY, APRIL, MARCH, FEBRUARY, JANUARY]
NOVEMBER OCTOBER SEPTEMBER AUGUST JULY JUNE MAY APRIL MARCH
FEBRUARY JANUARY
* /// , -
A partir de las constantes enum Month se genera una representac ión de String, que se inserta en el contenedor Stack
mediante push( ), y que luego se extrae de la pila mediante pope ). Para resaltar uno de los puntos que hemos comentado
anterionnente, tambi én se realizan operaciones de tipo, Vector sobre el objeto Stack. Esto es posible porque, debido a la
584 Piensa en Java
herencia, un objeto Stack es un Vector. Por tanto, todas las operaciones que puedan reali zarse sobre un objeto Vector tam-
bién podrán realizarse sobre un objeto Stack, como por ejemplo la operación elementAt().
Como hemos mencionado anteriorm ent e. debemos ut ili zar un contenedor LinkedList cuando queram os un contenedor con
comportamiento de pila; o bien, la clase net.mindview.util.Stack creada a partir de la clase LinkedList.
BitSet
BitSet se utili za si se quiere almacenar de manera eficiente una gran cantidad de infonnación de tipo binario. Sólo resulta
eficiente desde el punto de vista del tamaño; si lo que estamos buscando es eficiencia de acceso, es te contenedor resulta lige-
ramente más lento que emplear una matri z nati va.
Además, el tamaño mínimo de un contenedor BitSet es el de los valores a long: 64 bits. Esto implica que si estamos alma-
cenando algún conjunto de bits men or, como por ejemplo, 8 bits, con BitSet estaremos desperdiciando buena parte del espa-
cio; en este caso, sería simplemente mejor crear una clase, o una matriz, para almacenar los indicadores binarios, en caso
de que el tamaño sea un problema (esto sólo será un problema si estamos creando una gran cantidad de objetos que conten-
gan listas de información binaria; y sólo debería tomarse esta decisión después de reali zar un perfilado del programa O de
reali zar algún otro tipo de métrica. Si tomamos la decisión basándonos simplemente en nuestra propia creencia de que algo
es demasiado grande. temlinaremos creando una complejidad innecesari a y desperdiciando una gran ca ntidad de ti empo).
Un contenedor nomlal se expande a medida que añadimos más elementos y BitSet también actúa de esta misma manera. El
siguiente ejemplo muestra cómo funciona BitSet:
1/ : containers/Bi ts . java
/1 I lustración de Bi tSet .
import java.util. ·;
import static net.mindview.util.Print. · ¡
shor t st = (short)rand.nextInt () ;
BitSet b s = ne w BitSet();
for ( i n t i = 15; i >= O; i-- )
if ((( l « i ) & st ) != O)
bs. set (i ) ;
else
b s . clear(i) ;
print ("s hort value : " + st);
printBitSet (bs ) ;
17 Analisis detallado de los contenedores 585
int it = rand.nextlnt() i
BitSet bi = new BitSet ();
for (int i = 31; i >= O; i-- )
if (({l« i) & it ) != O)
bi. set (i) ;
else
bi.clear{i) ;
print("int value: 11 + it};
printBitSet (bi) i
/ * Output:
byte value: -107
bits, {O, 2, 4, 7}
bit pattern: 1010100100000000000000000000000000000000000000000000000000000000
short value: 1302
bit., {l, 2, 4, 8, lO}
bit pattern: 0110100010100000000000000000000000000000000000000000000000000000
int value: -2014573909
bits, {O, 1, 3, 5, 7, 9, 11, 18, 19, 21, 22, 23, 24, 25, 26, 3l}
bit pattern: 1101010101010000001101111110000100000000000000000000000000000000
set bit 127, {127}
set bit 255, {255}
set bit 1023, { 1023, l024}
* /// ,-
Utilizamos el generador de números aleatorios para crear números aleatorios byte, short e int, transfonnando cada uno en
su correspondiente patrón de bits que se almacena en un contenedor BitSet. Esto no causa ningún problema porque BitSet
tiene 64 bits como mínimo, así que ninguno de estos valores hace que se tenga que incrementar el tamaiio. A continuación,
se crea contenedores BitSet de mayor tamaño. Como podemos ver en el ejemplo, cada contenedor BitSet se expande según
es necesario.
Nonnalmente, resulta más conveniente utilizar un contenedor EnumSet (véase el Capítulo 19, Tipos enumerados) en lugar
de BitSet cuando disponemos de un conjunto fijo de indicadores binarios a los que podemos asignar nombre, porque
EnumSet nos pennite manipular los nombres en lugar de las posiciones numéricas de cada bit, con lo que se reduce la posi-
bilidad de cometer erro res en el programa. EnumSet también nos impide añadir accidentalmente nuevas posiciones bina-
rias, que es algo que podría causar algunos errores graves y dificiles de localizar. Las únicas razones por las que deberíamos
utilizar BitSet en lugar de EnumSet son: que no sepamos hasta el momento de la ejecución cuántos indicadores binarios
vamos a uitlizar, o que resulte poco razonable asignar nombres a los indicadores, o que necesitemos algunas de las opera-
ciones especiales incluidas en BitSet (consulte la documentación del JDK relativa a BitSet y EnumSet).
Resumen
La biblioteca de contenedores es, probablemente, la más importante de cualquier lenguaje orientado a objetos. En la mayo-
ría de los programas se utilizarán contenedores más que cualquier otro componente de la biblioteca. Algunos lenguajes
(Python, por ejemplo) incluyen incluso los componentes contenedores fundamentales (listas, mapas y conjuntos) como parte
del propio lenguaje.
586 Piensa en Java
Como hemos visto en el Capítulo 11. Almacenamienlo de objetos, podemos hacer varias cosas cnonnemente interesantes
utilizando contenedores sin necesidad de mucho esfuerzo de programación. Sin embargo. llegados a un cierto punto. nos
vemos obligados a conocer más detalles acerca de los contenedores para poder utilizarlos adecuadamente: en particular.
debemos conocer los suficientes detalles acerca de las operaciones /IOSI1 como para escribir nuestro propio método
hashCode() (y debemos saber también cuándo es necesario hacer esto), y debemos conocer lo suficiente acerca de las dis-
tintas implementaciones de contenedores como para poder decidir cuál es la apropiada para nuestras necesidades. En este
capítulo hemos cubierto estos conceptos y hemos proporcionado detalles útiles adicionales acerca de la biblioteca de con-
tenedores. Llegados a este punto. el lector debería estar razonablemente bien preparado para utilizar los contenedores de
Java como parte de sus tareas cotidianas de programación.
El diseño de una biblioteca de contenedores resulta complicado (como sucede con la mayoría de los problemas de diseilo
de bibliotecas). En C++, las clases de contenedores cubren todos los aspectos fundamentales empleando muchas clases dife-
rentes. Evidentemente. esta solución era mejor que la que había disponible antes de que aparecieran las clases de contene-
dores de C++ (es decir, nada), pero no era una solución que pudiera traducirse demasiado bien a Ja va. En el otro extremo,
yo he visto una biblioteca de contenedores que está compuesta por una única clase. "container" que actúa al mismo tiempo
como secuencia lineal y como matriz asociativa. La biblioteca de contenedores de Java trala de conseguir un cierto equili-
brio. la funcionalidad completa que esperaríamos obtener de una biblioteca de contenedores madura, pero con una facilidad
de aprendizaje de uso superior a la de las clases contenedoras de C++ y a otras bibliotecas de contenedores similares. El
resultado puede parecer algo extraño en algunos aspectos, pero, a diferencia de algunas de las decisiones tomadas en las pri-
meras bibliotecas Java, esos aspectos extraños no son simples accidentes, sino decisiones de diselio cuidadosamente adop-
tadas y basadas en una serie de compromisos relativos a la complejidad de la solución adoptada.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico rhe Thinking in Jm'tI Alllloftlled SOllllioll Guid/!, disponible para
la venta en lI'Ww.MimJView.nel.
Entrada/salida
La creación de un buen sistema de entrada/salida (E/S) es una de las tareas más dificiles para el
diseñador de lenguajes, cosa que queda de manifiesto sin más que ver la gran cantidad de
técn icas distintas que se han utili zado.
Parece ser que el desafio radica en tratar de cubrir todas las posibilidades. No sólo hay diferentes fuentes y consumidores
de datos de E/S con los que es necesario comunicarse: archivos, la consola, conexiones de red, etc., sino que también hay
que hablar con esos distintos interlocutores de diferentes formas (secuencial, acceso aleatorio, con buffer, binaria, de carac-
teres, por líneas, por palabras. etc.).
Los diseñadores de la biblioteca Java abordaron el problema creando una gran cantidad de clases. De hecho, hay tantas cIa-
ses para el sistema de E/S de Java que a primera vista uno se siente intim idado (irónicamente, el diseño del sistema de E/S
en Java evita, de hecho. que se produzca una auténtica explosión de clases). Asimismo. hubo un significativo cambio de
diseño en la biblioteca de E/S después de la versión Java 1.0, cuando la biblioteca original orientada a bytes fue suplemen-
tada con una serie de clases de E/S orientadas a caracteres y basadas en el conjunto de caracteres Unicode. Las clases Dio
(q ue quiere decir "new l/O", nueva E/S, las cuales fueron introducidas en el IDK 1.4 Y ya son. por tanto, bastante "anti-
guas") fueron añadidas para mejorar el rendimiento y la funcionalidad. Como resultado, hay un gran número de clases que
es necesario aprender antes de comprender los suficientes detalles del sistema de E/S de Java como para poderlo utilizar
apropiadamente. Además, es importante entender la evolución de la biblioteca de E/S, aún cuando nuestra primera reacción
sea decir: ''No me des la lata con cuestiones históricas, ¡limítate a enseñanne cómo utilizar las clases". El problema es que,
sin la perspectiva histórica, podemos llegar rápidamente a sentin10s confundidos por algunas de las clases yana tener claro
cuándo deberían usarse y cuándo no.
En este capítulo proporcionaremos una introducción a las diversas clases de E/S existentes en la biblioteca estándar de Java
y veremos también cómo utilizarlas.
La clase File
Antes de presentar las clases que se encargan propiamente de leer y de escribir flujos de datos, vamos a examinar una uti-
lidad de la biblioteca que nos sirve de ayuda a la bora de tratar con aspectos relativos a los directorios de archivos.
La clase File tiene un nombre que se presta a confusión, podríamos pensar que bace referencia a un archivo, pero en reali-
dad no es asÍ. De hecho, "FilePath" (ruta de archivo) hubiera sido un nombre mucho mejor para esa clase. Puede represe n-
ta r o bien el nombre de un archivo concreto o los nombres de un conjunto de arch ivos en un directorio. Si se trata de un
conjunto de archivos, podemos pedir que se nos devuelva dicho conj unto util izando el método list( ), que devuelve una
matriz de objetos St ri ng. Tiene bastante sentido devolver una matriz en lugar de una de las clases con tenedoras más flexi-
bles, porque el número de elementos es fijo y, si queremos un listado de directorio distinto, basta con crear un objeto File
diferente. Esta sección muestra un ejemplo de utilización de esta clase, incluyendo la interfaz asociada FilenameFilte r .
lista restringida, por ejemplo, todos los archivos que tengan la extensión .java, entonces debemos utilizar un "filtro de direc-
torio", que es una clase que especifica cómo seleccionar los objetos File que queramos visualizar.
He aquí el ejemplo. Observe que el resultado se ha ordenado muy fácilmente (de manera alfabética) mediante el método
java.util.Arrays.sort() y el comparador String.CASE_ INSENSITlVE_ORDER:
JI : io / DirList.java
JI Mostrar un listado de directorio utilizando expresiones regulares.
jj {Args: "D .* \ .java"}
import java . util . regex .* ;
import java . io .*¡
import java.util. * ;
/ * Output :
DirectoryDemo.java
DirList . java
DirList 2 . java
DirList3.java
* jjj ,-
La clase DirFilter implementa la interfaz FilenameFilter. Observe 10 simple que es esta interfaz:
public interface FilenameFilter {
boolean accept ( File dir, String name) i
La única razón de ex istir de DirFilter es proporcionar el método accept() al método list(), de modo que éste pueda "retro-
llamar" a accept() con el fin de determinar qué nombres de archi vos deben incluirse en la lista. Debido a esto, esta estmc-
tura se suele calificar como l'e¡rollamada. Más específicamente. éste es un ejemplo del patrón de diseño de Estrategia,
porque list() implementa la funcionalidad básica y proporciona la Estrategia en la forma de un objeto FilenameFilter, con
el fin de completar el algoritmo necesario para que list() proporcione su servicio. Puesto que list() toma un objeto
FilenameFilter como argumento, qu iere decir que podemos pasar a ese método un objeto de cualquier clase que implemen-
te FilenameFilter con el fin de elegir (incluso en tiempo de ejecución) cómo debe comportarse el método list(). El propó-
sito del patrón de diseño de ESll'alegia es proporcionar una dosis de nexibilidad en el comportamiento del código.
El método accept( ) debe aceptar un objeto File que represente el directorio en el que se encuentre un archi vo concreto y
un objeto String que contenga el nombre de di cho archivo. Recuerde que el método list( ) llama a accept( ) para cada uno
18 Entrada/salida 589
de los nombres de archivo del objeto directorio, para ver cuáles hay que incluir; esto se indica mediante el resultado de tipo
boolean devuelto por accept( l.
accept( ) utiliza un objeto matcher de expresiones regulares con el fin de ver si la expresión regular rcgex se corresponde
con el nombre del archivo. Utilizando accept( l, el método list( l devuelve una matriz.
1* Output:
DirecteryDemo.java
DirList. java
DirList2. java
DirList3. java
* /// ,-
Observe que el argumento de filter( ) debe ser de tipo final. Esto es requerimiento de la clase intema anónima, para que
ésta pueda emplear un objeto que está fuera de su ámbito.
Este diseño representa una mejora porque la clase FilenameFilter está ahora estrechamente acoplada a DirList2 . Sin
embargo, podemos llevar este enfoque un paso más allá y definir la clase interna anónima como un argumento de Ust( l, en
cuyo caso el ejemplo es todavía más sucinto:
11 : io / DirList3.java
II Creación de la clase interna anónima "sobre el terreno".
II {Args: "D.*\.java ll
}
import java.util.regex.*¡
import java.io.*¡
impert java.util.*;
1* Output:
DirectoryDemo.java
DirList. java
DirList2. java
DirList3. java
,///,-
El argumento de main() es ahora de tipo final, puesto que la clase interna anónima utiliza args¡O I directamente.
Este ejemplo nos muestra cómo las clases internas anónimas permiten la creación de clases específicas de un solo uso para
resolver cienos problemas. Una de las ventajas de esta técnica es que mantiene aislado en un único lugar el código que
resuelve un problema concreto. Por otro lado, no siempre resulta tan fáci l de leer y entender dicho código, así que hay
que utilizar juiciosamente esta técnica.
Ejercicio 1: (3) Modifique DirList.java (o una de sus varia ntes) para que el objeto FilenameFilter abra y lea cada
archivo (utilizando la utilidad net.mindview.utiI.TextFile) y acepte el archivo basándose en si alguno de
los argumentos finales de la línea de comandos existe en dicho archivo.
Ejercicio 2: (2) Cree una clase denominada SortedDirList con un constructor que tome un objeto File y construya una
li sta de directorio ordenada a partir de los archivos contenidos en dicho objeto File. Añada a esta clase dos
métodos list() sobrecargados: el primero produce la lista completa y el segundo produce el subconjunto
de la lista que se corresponda con su argumento (que será una expresión regular).
Ejercicio 3: (3) Modifique DirList.java (o una de sus variantes) para que ca lcu le la suma de los tamallOS de los archi-
vos seleccionados.
Utilidades de directorio
Una tarea común en programación consiste en realizar operaciones sobre conjuntos de archivos, bien en el directorio local
o bien recorriendo todo el árbol de directori os. Resulta útil disponer de una herramienta que produzca el conjunto de archi-
vos para nosotros. La siguiente clase de util idad produce una matri z de objetos File en el directorio local utili zando el méto-
do local( ) o un objeto List<File> que representa todo el árbol de directorios a partir del directorio indicado. empleando
walk() (los objetos File son más útiles que los nombres de archivo porque dichos objetos contienen más infonnación). Los
archi vos se eligen basándose en la expresión regular que proporcionemos:
11: net/mindview/util/Directory.java
I I Generar una secuencia de objetos File que se correspondan
II con una expresión regular bien en el directorio local,
II o bien recorriendo un árbol de directorios.
package net.mindview.util¡
import java.util.regex.*¡
import java.io.*;
import java.util.*¡
18 Entrada/salida 591
return resul t;
El método local() utiliza una variante de File.list() denominada listFiles() que genera una matriz de objetos File. Podemos
ver también que utiliza un objeto FilenarneFilter. Si hace falta una lista en lugar de LUla matriz, podemos convertir nosotros
mismos el resultado utilizando Arrays.asList().
Et método walk( ) convierte el nombre del directorio de inicio en un objeto File e invoca recurseDirs( ), que realiza un
reco rrido recursivo del directorio, recopilando más infonnación con cada recursión. Para distinguir los archivos normales
de los directorios, el va lor de retorno es, de hecho, una "tupla" de objetos: un contenedor List que almacena archivos nor-
males y otro que almacena los directorios. Los archivos están definidos aquí como public a propósito, porque el objeto
TreeInfo es simplemente para recopilar los objetos; si estuviéramos simplemente devolviendo una lista, no lo definiríamos
co mo priva te, así qu e el hecho de que estemos devo lviendo un par de objetos no quiere decir que los tengamos que definir
como priva te. Observe que Treelnfo implementa lterable<File>, que genera los archivos, de modo que disponemos de una
" iteración predetenninada" a tra vés de la lista de archi vos, mientras que para especificar directorios tenemos que escribir
".dirs".
El método Treelnfo.toString() utili za una clase de " impresión avanzada", para que la salida sea más fácil de visualizar. Los
métodos predeterminados toString( ) de los contenedores imprimen tod os los elementos de un contenedor en una misma
línea. Para colecciones de gra n tamaño, esto puede hacerse dificil de leer, así que podemos tratar de emplear un formato
alternativo. He aquí la herramjenta que añade avances de línea y sangrados a cada elemento:
JI : netJmindviewJutil/PPrint.java
JI Impresión avanzada de colecciones
package net.mindview.uti1i
import java.util.*¡
publie elass PPrint {
public statie String pformat (Collection<?> e) {
if(e . size() == O) return 11 [] "i
StringBuilder result new StringBuilder (11 [11) i
for {Objeet elem : el {
if(c . size() != 1)
result . append ( " \n 11) i
result.append(elem) i
if(e.size{) != 1}
result.append("\n") i
result. append ("] 11 l ;
return result.toString();
El método pformat( ) produce un objeto String fonnateado a partir de un objeto Collection, y el método pprint() utili za
pformat() para llevar a cabo esa tarea. Observe que los casos especiales en los que no existe ningún elemento o sólo exis-
ta uno se gestionan de manera di stinta. También hay una versión de pprint() para matrices.
La utilidad Directory está incluida en el paquete net.míndvíew.util, para que esté fácilmente disponible. He aquí un ejem-
plo de utilización:
18 Entrada/salida 593
JI: iofDirectoryDemo.java
JI Ejemplo de uso de las utilidades de directorio.
import java.io.·;
import net.mindview.util.*¡
import static net.mindview.util.Print.*;
. \ TestEOF . java
. \TransferTo.java
. \ xfiles \ThawAlien. java
.\FreezeAlien.class
.\GZIPcompress.class
. \ ZipCompress. class
. ///, -
Puede que necesite refrescar sus conocimientos sobre expresiones regulares en el Capítulo 13 , Cadenas de caracteres, para
comprender los argumentos situados en segunda posición en local( ) y walk( ).
Podemos llevar esta idea un paso más allá y crear una herramienta que recorra directorios y procese los archivos contenidos
en ellos de acuerdo con un objeto Strategy (se trata de otro ejemplo del patrón de diseño Estrategia):
// : net/mindview/util/ProcessFiles.java
package net.mindview.util;
import java.io.*;
public class ProcessFiles (
public interface Strategy
void process(File file ) ¡
processDirectoryTree(new File{".!!));
else
far (String arg args) {
File fileArg new File(arg);
if{fileArg.isDirectory() )
processDirectoryTree(fileArg) ;
else (
/1 Permitir que el usuario no incluya la extensión:
if(larg.endsWith("." + exc))
arg += "," + ext;
strategy.process(
new File (arg) .getCanonicalFile ());
catch(IOException el
throw new RuntimeException(e);
public void
processDirectoryTree(File rooe) throws IOException
for(File file: Directory .wa lk(
root .getAbsolutePath () * \ \ ." + ext))
I ".
strategy.process(file.getCanonicalFile()) ;
}
JI Ejemplo de utilización :
public static void main(String [] args) {
new ProcessFiles(new ProcessFiles.Strategy()
public vo id process (Fil e fi l e ) {
System.out.println(file) ;
}, "java!! ) .s tart(args) i
JI: io/MakeDirectories.java
/1 Ejemplo de uso de la clase File para crear
/1 directorios y manipular archivos.
// {Args: MakeDirectoriesTest}
import java.io.*;
int count = O;
boolean del = false¡
iflargs[O) . equalsl"-d"ll
count++;
del = true;
count--¡
while ( ++count < args .length ) {
File f = new File(args(count)¡
if lf .exists (ll {
System.out.println(f + " ex i sts " );
ifldell {
System. Qut . println ("deleting .. . " + f ) ;
f. delete I 1 ;
596 Piensa en Java
el se { II No existe
if(!dell {
f . mkdirs (1 ;
System.out.println(lIcreated 11 + f);
fileData (f) i
Entrada y salida
Las bibliotecas de E/S de los lenguajes de programación utili zan a menudo la abstracción deljlujo de dalas (slream), para
representar cualquier origen o destino de datos como un objeto capaz de producir o recibir elementos de datos. El flujo de
datos oculta los detalles de lo que ocurre con los datos dentro del dispositivo real de E/S.
Las clases de la biblioteca de Java para E/S están divididas en entrada y salida, como se puede ver en la jerarquía de clases
al examinar la documentación del JDK. A tra vés del mecanismo de herencia, todas las clases derivadas de las clases
InputStream o Reader disponen de métodos básicos denominados read( ) para leer un único byte o una matriz de bytes.
De la misma fonna , todas las clases derivadas de las clases OutputStream o Writer disponen de métodos básicos denom i-
nados write() para escribir un único byte o una matriz de bytes. Sin embargo, generalmente no utilizaremos estos métodos;
esos métodos existen para que otras clases puedan emplearlos, pero estas otras clases nos proporcionan una interfaz más
útil. Por tanto, raramente crearemos nuestro objeto flujo de datos utili zando una única clase, sino que en su lugar ap ilare-
mos múltiples objetos para obtener la funcionalidad deseada (se trata del patrón de diseño Decorador, como veremos en
esta sección). El hecho de que creemos más de un objeto para producir un único flujo de datos es la razón principal de que
la biblioteca de E/S de Java sea tan confusa.
Resulta útil clasificar las clases según su funcionalidad. En Java 1.0, los diseñadores de bibliotecas comenzaron decidiendo
que todas las clases que tuvieran algo que ver con la entrada de datos heredarían de InputStream, mientras que todas las
clases que estuvieran asociadas con la salida heredarían de OutputStream .
Corno suele ser habitual en este libro, trataré de proporcionar un a panorámica de las clases, pero asumiendo que el lector va
a emplear la documentación del JDK para conocer el resto de los detalles, como por ejemplo la lista exhaustiva de métodos
de una clase concreta.
18 Entrada/salida 597
Tipos de InputStream
La tarea de I np utStrea m consiste en representar clases que produzcan datos de entrada procedentes de diferentes fuentes.
Estos orígenes de datos pueden ser:
1. Una matriz de bytes.
2. Un objeto St ring.
3. Un archivo.
4. Una "cana lización (pipe)" que funciona como una canalización física: se introducen las cosas por un extremo y
esas cosas salen por el otro.
5. Una secuencia de otros flujos de datos, de modo que se los pueda recopilar en un único flujo.
6. Otros orígenes de datos, como por ejemplo una conexión a lntcmct (este tema se cubre en Thinking in EnleJ]Jrise
Java, disponible en lVlVw.MindView.net).
Cada lino de estos orígenes tiene una subclase asociada de Input8tream. Además, Filter[npu tStream también es un tipo
de Inp utSt rea m, para propo rcionar una clase base para las clases "decoradoras" que asocian atributos o interfaces fáci les a
los flujos de datos de entrada. Este tema se analiza más adelante.
Tabla E/S .l. npos de InputStream.
Stri ngBu ffer I nputStream Conviene un String en un InputStrea m. Un objeto String. La implementación subyacente utiliza
en realidad un objeto StringB uffer.
File) nputStream Para leer información de un archivo. Un objeto Stri ng que representa el nombre del archivo o
un objeto Fil e o FileDeseriptor.
Como origen de datos: conéetelo a un objeto
Filterlnp utStream para proporcionar una interfaz útil.
PipedLnputStream Genera los datos que estén siendo escritos PipedO utp utStrearn
en el objeto PipedOutput-Strearn asocia-
do. Implementa el concepto de "canaliza- Como origen de datos en programación multihebra:
ción " de flujos de dalas. conéctelo a un objeto Filter lnputStream para propor-
cionar una interfaz útil.
Seq ue neeI nputStrearn Convierte dos o más objetos ln putStream Dos objetos InputStrea m o un objeto Enumeration
en un único InputStream. para un con tenedor de objetos In putStream.
Filterln p utStream Clase abstracta que actúa como interfaz Véase la Tabla E/S.3
para las clases decoradoras que proporcio-
nen funcionalidad útil a las otras clases
InputStrea m. Véase la Tabla E/S.3.
Véase la Tabla E/S.3
598 Piensa en Java
Tipos de OutputStream
Esta categoría incluye las clases que deciden a dónde debe ir la salida: a una matriz de bytes (pero no a un objeto String,
aunque presumiblemente podemos crea r uno usando la matriz de bytes), a un archivo o a una ca nali zación.
Además, el objeto FilterOutputStream proporciona un a clase base para las clases " decoradoras" que asocien atribu tos o
interfaces útiles a los flujos de datos de salida. Este tema se analiza más adelan te.
FileOutputStream Para enviar infornlación a un arch ivo. Un objeto String que representa el nombre del archi-
vo o un objeto File o FileDescriptor.
1 No esta claro que ésta haya sido una buena decisión de diseño. especialmente si la comparamos con la simplicidad de las bibliotecas de E/S en otros len-
guajes. Pero es, sin ninglma duda. la justificación de esa decisión.
18 Entrada/salida 599
filterOutputStream derivan de las clases base de la biblioteca de E/S InputStream y OutputStream, lo que es un requi-
sito clave de los decoradores (para que puedan proporcionar la interfaz común para todos los objetos que están siendo deco-
rados).
Cómo utilizarlos
DatalnputStream Se utiliza conjulltamente con InputStream
DataOutputStream, para poder leer pri-
mitivas (int, char , long, etc.) de un flujo
de datos de una manera portable. Contiene una interfaz completa con la que leer
tipos primilivos.
B ufferedl nputStream Utilice ésta para evitar una lectura física InputStream , con un tamailo de blljJer opcio-
cada vez que desee más datos. Lo que nal.
estamos diciendo es: "Utiliza un buffer".
No proporciona una interfaz per se. Só lo aiiade
I',",'"m~,' "'"",",m
la capacidad de hujIer al proceso. Asocie un
objeto interfaz.
--
PushbacklnputStream Dispone de un buffer de retroceso de un InputStrcam
único byte para poder retroceder al último
carácter leído. Generalmente, se utiliza en el análisis sintáctico
realizado por un compilndor. Probablemente no
utilice nunca esta clase.
La intención original de PrintStream era impri mir todos los tipos de datos primitivos y objetos Strin g en una forma legi-
ble. Esto difiere de DataOutputStream , cuyo objeti vo es insertar los elementos de datos en un flujo de datos de tal mane-
ra que DatalnputStream pueda reconstnlirlos de manera portable.
Los dos métodos más importantes de PrintStream son print( ) y println( ), que están sobrecargados para imprimir todos
los diversos tipos de datos. La diferencia entre print( ) y println( ) es que este último añade un avance de linea cuando ha
acabado.
PrintStream puede ser problemático porque act iva todas las excepciones de IOException (hay que comprobar explícita-
mente el estado de error con checkError(), que devuelve true si se ha producido un error). Asimismo, PrintStream no se
internacionaliza adecuadamente y no ges ti ona los saltos de línea de manera independien te de la plataforma. Estos proble-
mas están resueltos en PrintWriter, que se describe más adelante.
BufferedOutputStre.m es un modificador que le di ce al flujo de datos que utilice un buffer, para que no se realicen escri-
turas fisicas cada vez que escribamos en el flujo de datos. Nonna lmente, siempre conviene utili zar este modificador a la
hora de llevar a cabo una salida de datos.
PrintStream Para generar salida fonnateada. Mientras OutputStream, con un valor boolean opcional que
que DataOutputStream se encarga del indica que el bufler debe vaciarse con cada nueva
almacenamiel1to de los datos, PrintStream línea.
gestiona la visualización.
Debe ser el envoltorio "final" para el objeto
OutputStream. Probablemente utilice esta clase
muy a menudo.
BufferedOutputStream Utilice esto para evitar que se realice una OutputStream, con un tamaño de buffer opcional.
escrinlra fisica cada vez que se envíe un
elemento de datos. Estamos diciendo: No proporciona una in terfaz per se. Simplemente
··Utiliza un buffer". Puede utilizar flush( ) añade un buffer al proceso. Asócielo a un objeto
para vaciar el buffer. interfaz.
Lectores y escritores
Java 1.1 realizó significativas modificaciones en la biblioteca fundamental de flujos de E/S. Cuando examine las clases
Re.der (lector) y Writer (escritor), su primer pensamiento (al igual que me pasó a mi ) puede ser que esas clases intenta-
ban sustituir a InputStream ya OutputStream , pero en realidad no es así. Aunque algunos aspectos de la biblioteca origi~
nal de la biblioteca de flujos de datos están obsoletos y ahora se desaconsejan (si los emplea, el compilador generará una
advertencia), las clases InputStream y OutputStream siguen proporcionando una valiosa fu ncionalidad en la forma de
mecanismos de E/S orientados a bytes, mientras que las clases Reader y Writer proporcionan mecanismos de E/S o ri enta~
dos a caracteres y compatibles con Unicode. Además:
1. Java 1.1 añadió nuevas clases a las jerarquías InputStream y OutputStream, así que resulta obvio que la inten-
ción no era sustituir esas jerarquías.
2. Existen ocasiones en las que es necesario utilizar clases de la jerarquía orientada a "bytes" en combinación con
las clases de la jerarquía orientada a "caracteres". Para hacer esto, existen clases "adaptadoras": InputStream-
Reader convierte un objeto InputStream en un objeto Reader, y OutputStreamWriter convierte un objeto
OutputStre.m en un objeto Writer.
18 Entrada/satida 601
La razón más importante para la existencia de las jerarquías Reader y Writer es la intemalización. La antigua jerarquía de
flujos de datos de E/S sólo soportaba flujos de datos con bytes de 8 bits y no gestionaba adecuadamente los caracteres
Unicade de 16 bits. Dado que Unicode se utiliza para la intemalización (y el tipo char nativo de Java es Unicode de 16-
bits), las jerarquías Reader y Writer se añadieron para soportar Unicode en todas las operaciones de E/S. Además, las nue-
vas bibliotecas están diseñadas para que sus operaciones sean más rápidas que las de las bibliotecas antiguas.
JnputStream Reader
adaptador: JnputStreamReader
Output5trcam Writer
adaptador: OutputStream\Vriter
Filelnput5tream FileReader
FileOutput5tream FileWriter
ByteArrayOutputStream CharArrayWriter
PipedOutputStream Piped\Vriter
En general, encontrará que las interfaces para las dos jerarquías son similares, sino idénticas.
filterlnputStream
FilterOutputStrcam
--- ~ FilterReader
f------
Bu ffcred 11l1)utStrea nI BuffercdReadcr (también tiene readLine(»
BufferedOutputStream Buffered\Vriter
PrintStrcam Print\\'riter
PushbackInputStream PushbackReader
Existe una directriz bastante clara: cuando quiera utilizar readLine(). no debe hacerlo con un flujo DatalnputStream (esto
origina un mensaje de advertencia en tiempo de co mpilación donde se infonna de que esa técnica está desaconsejada), sino
que debe utilizar BufferedReader. Por lo demás, DatafnputStream continúa siendo uno de los miembros "aconsejados"
de la bibl ioteca de E/S.
Para fac ilitar la transición a soluciones que empleen Print\Vriter, esta clase ti ene constTIlctores que admiten cualquier obje-
to OutputStream así como objetos Writer. La interfaz de [0n11ateo de PrintWriter es casi idéntica a la de PrintStream.
En Java SES . se han añadido constructores a PrintWriter para simplificar la creación de arcbjvos a la hora de escribir la
salida, como vere mos enseguida.
Un constructo r PrintWriter también dispone de una opción para reali zar un vaciado de buffer automát ico, lo que tiene lugar
después de cada llamada a println() si se activa el correspondi ente indicador en el co nstructor.
Clases no modificadas
Algunas clases no sufrieron modificac iones entre las versiones Java 1.0 y Java 1.1:
File
RandomAccessFile
DataOutputStream, en concreto, se utili za sin modificación, por lo que para almacenar y extraer datos en un fonnato trans-
portable, se utilizan las jerarquías InputStream y OutputStream.
RandomAccessFile
RandomAccessFile se utili za para archivos que contengan registros de tamaño conocido, de modo que podamos desplazar-
nos de un registro a otro usando seek(), y luego leer o modificar los registros. Los registros no ti enen que tener el mismo
tamaño; simplemente tenemos que detenninar el tamaño que tienen y el lugar del archivo donde se encuentran.
18 Entrada/salida 603
En principio, resulta bastante dificil de entender que RandomAccessFile no fonne parte de la jerarquía InputStream o
OutputStrcam. Sin embargo, no tiene ninguna asociación con dichas jerarquías. 5al\'0 el hecho de que implementa las inter-
faces DataInput y DataOutput (que también son implementadas por DataInputStroam y DataOutputStroal11). Ni siquie-
ra utiliza ninguna de las funcionalidades de las clases InputStream o OutputStream existentes: se trata de una clase
completamente separada. que se ha escrito partiendo de cero, con sus propios métodos (en sllll1ayor parte nativos). La razón
puede ser que RandomAccessFile tiene un compol13micnto esencialmente distinto del de los otros tipos de E/S, ya que nos
podemos mover hacia adelante y hacia atrás dentro de un archivo. En c ualqui er caso, se trata de una clase aislada. descen-
diente directa de Object.
Esencia lmente, un objeto RandomAcccssFile funciona como un flujo DatalnputStrealll que se hubiera conectado con un
flujo DataOutputStrcam, junto con los métodos de getFilePointer( ) para averiguar en qué lugar del archivo nos encon-
tramos, seek( ) para desplazarse a un lluevo punto en el archi\'o y length( ) para detenninar el tamalio máximo del archivo.
Además. los constmctores requieren un segundo argumento (idéntico a fopen( ) en e) que indica si estamos simplememe
leyendo de manera aleatoria ("r") o leyendo y escribiendo ("rw"). No existe soporte para archivos de sólo escritura, lo que
podría sugerir que RandomAccessFile podría también haberse diseñado como clase heredada de DataInputStream .
Los métodos de búsqueda sólo están disponibles en RandomAccessFiJe. que solamente puede aplicarse a archivos.
Buffcrcdl np ut Stream pennite marcar con mark() una posición (cuyo valor se almacena en una única \'ariable interna) y
efectuar un reposicionamiento con reset( ) a dicha posición, pero esta funcionalidad es muy limitada y no resulta muy útil.
La mayor parte de la funcionalidad de Ra ndo mAccessFile, si es que no toda ella. ha sido sust ituida en el JDK lA por los
archh'os mapeados en memoria nio, que describiremos más adelante en el capítulo.
2 En el diseño original, se suponía que close( ) debía ser invocado cuando se ejecutara fin alize( ), y tendrá ocasión de ver en algunos textos que fin alizc{)
se define de esta fomla para las clases de E/S. Sin embargo, como hemos comentado anteriomlentc, la funcionalidad fi nalize( ) no ha llegado a funcionar
de la fonna en que los disenadores de Java habían previsto originalmente (en otras palabras, no va a llegar a funcionar nunca), por lo que la técnica segu-
ra consiste en invocar close( ) explícitamente para los archivos.
18 Entrada/salida 605
mos leer cualquier cosa (por ejemplo un archivo) de byte en byte utilizando clases InputStream. pero lo que aquí se utili-
za es una cadena de caracteres:
JI : io/FormattedMemorylnput.java
import java.io.*;
JI: io/BasicFileOutput.java
import java.io.*;
Seguimos disponiendo de un buffer. pero no tenemos que encargamos nosotros del mecanismo de l mis mo.
Lamentab lemente, no existen atajos similares para otras tareas muy comunes, por lo que un programa típico de E/S sigue
18 Entrada/salida 607
requi riendo un a gran cantidad de texto red undante. Sin embargo. la utilidad TextFile que se usa en este li bro, que defin ire-
mos más adelante en este capírulo, permite si mplificar estas tareas comun es.
Ejercicio 12: (3) Mod ifiq ue el Ejercicio 8 para abrir también un archivo de texto de modo que podamos escrib ir tex to
en él. Escriba en el archivo las líneas del contenedor LinkedList, j unto con sus correspond ientes núme-
ros de lí nea (no trate de utili zar las clases " LineNum ber" ').
Ejercicio 13: (3) Modifique BasicFileOutput.java para que ut ilice LineNumberReader con el fin de controlar el
número de líneas. Observe que resulta mucho más sencillo llevar la cuenta medi ante programa.
Ejercicio 14: (2) Comenzando con BasicFileOutput.java, escriba un program a qu e compare la velocidad de escritura
en un archivo al utili zar mecanismos de E/S con buffer y sin bulfa
1* Output:
3.14159
That was pi
1.41413
Square root of 2
*///,-
Si utili zamos DataOutputStream para escribir los datos, Java garantiza que podemos recuperar perfectamente los datos con
DatalnputStream, independientemente de las platafonnas que se empleen para leer y escribir los datos. Esto resulta enor-
memente útil, como puede atestiguar cualquiera que haya dedicado algo de tiempo a resolver problemas relacionados con
el tratamiento específico de los datos en cada plataforma. Dichos problemas desaparecen si disponemos de Java en ambas
plataformas 3
JXML es otra fo nna de resolver el problema de trasmitir datos de una platafonna a otra, y no depende de si se di spone de Java cn todas las platafo rmas.
Hablaremos de XML más adelame en este capítulo.
608 Piensa en Java
Cuando estamos usa ndo DataOutputStream , la única fom1a fiable de escribir una cadena de caracteres para que pueda
recuperase mediante un flujo DatalnputStream consiste en utilizar codificación UTF-8, la cual se consigue en este ejem-
plo mediante writeUTF() y readUTF( ). UTF-8 es un fonnato multibyte, y la longitud de codificación varia de acue rdo
con el conjunto de caracteres que se esté empleando. Si estarnos trabajando con ASC II o caracteres que sean ASC II en su
mayor parte (los cuales sólo ocupan siete bits), Unicode representa un tremendo desperdicio de espacio y/o espacio de
banda, por lo que se emplea UTF-8 para codificar los caracteres ASC II en un único byte, y los caracteres no-ASCII en dos
o tres bytes. Además, la longitud de la cadena de caracteres se almacena en los dos primeros bytes de la cadena UTF-8. Sin
embargo, writeUTF( ) y read UTF() uti lizan una variante de UTF-S especial para Java (que está completamente descrita
en la documentación del JDK cOlTespondiente a estos métodos), por lo que si leemos una cadena escrita con writeUTF( )
utilizando un programa no-Java, deberemos escribir un código especial para poder leer esa cadena apropiadamente.
Con wr iteUTF( ) y re.dUTF( ), podemos mezclar cadenas de caracteres con otros tipos de datos empleando un flujo de
datos DataOutputStream. en la seguridad de que las cadenas de caracteres serán aprop iadamente almace nadas como datos
Unicode y podrán recuperarse fácilmente mediante DatalnputStrcam .
El método writeDouble() almacena el número double en el flujo de datos, y el método complementario readDouble() per-
mite recuperarlo (existen métodos similares para poder leer y escribir los otros tipos de datos). Pero para que cualquiera de
los métodos de lectura funcionen correctamente, es necesario conocer la posición exacta de los elementos de datos dentro
del flujo de dalOs, ya que sería igualmente posible el valor double al mace nado como una simple secuencia de bytes, o como
un valor char, etc. Por tal1to, tenemos que tener UI1 fonnato fijo para los datos en el arch ivo o, alternativamente. deberemos
almacenar en el archivo infonnac ión adiciona l que será necesario analizar para detenninar dónde están ubicados los datos.
Observe que otras técnicas. como la de serialización de los objetos o XML (describiremos ambas más adelante en el capí-
tu lo), pueden ser más senci llas a la hora de almacenar y recuperar estructuras de datos más complejas.
Ejercicio 15: (4) Consulte DataOutputStream y DatalnputStream en la documentación del JDK, Comenzando con
Sto rin gA ndRecoveringData.java, cree un programa que almacene y luego ex traiga todos los diferentes
tipos posibles proporcionados por las clases DataOutputStream y DatalnputStream , Verifique que los
valores se almacenan y extraen adec uadamente.
rf . clase () ;
display () ;
rf = new RandomAccessFile ( file, "rw" ) ;
rf.seek {5 *8 ) ;
rf.writeDouble{47.0001) i
rE. clase () ;
display () ;
/ * Output:
Value o: 0.0
Value 1, 1.414
Value 2, 2.828
Value ), 4.242
Value 4, 5.656
Value 5, 7 . 069999999999999
Value 6 , 8.484
The end of the file
Value o,
0.0
Value 1,
1. 414
Value 2,
2.828
Value 3,
4.242
Value 4,
5.656
Value 5,
47.0001
Value 6,
8.484
The end of the file
* jjj ,-
El método display( ) abre un archivo y muestra siete elementos contenidos en él en forma de valores double. En main( l ,
se crea el archivo y luego se abre y modifica. Puesto que, un valor double siempre tiene ocho bytes de longitud, para des-
plazarlos con seek( l al número situado en la posición cinco basta con multiplicar 5*8 con el fin de obtener el valor de bús-
queda.
Como hemos indicado anterionnente, RandomAccessFile está aislado, de hecho, del resto de la jerarquía de E/S, salvo por
la circunstancia de que implementa las interfaces Datalnput y DataOutput. Esta clase no soporta el mecanismo de deco-
ración, asi que no se la puede combinar con ninguno de los aspectos de las subclases InputStream y OutputStream.
Tenemos que asumir. por tanto, que RandomAccessFile dispondrá de los mecanismos de buffer apropiados, ya que no tene-
mos manera de especi ticar que se emplee.
La única opción de la que disponemos se encuentra en el segundo argumento del constructor: podemos abrir un archivo
RaodomAcccssFile para lectura (U r") O lectura y escritura ("rw").
También merece la pena considerar la utilización de archivos mapeados en memoria oio en lugar de RandomAccessFile.
Ejercicio 16: (2) Consulte RandomAccessFile en la documentación del JDK. Tomando como punto de partida
UsingRandomAccessFile.java, cree un programa que almacene y luego extraiga todos los diferentes tipos
posibles soportados por la clase RandomAccessFile. Verifique que los valores se almacenan y se extraen
adecuadamente.
raciones comunes: no existen funciones básicas de utilidad que se encarguen de realizar el trabajo por nosotros. Todavia
peor: los decoradores hacen que resuhe relati\'amente dificil acordarse de qué hay que hacer para abrir archivos. Por tanto,
resulla bastante conveniente aiiadir clases de utilidad a nuestra biblioteca que se encarguen de realizar estas tareas por no-
sotros. Java SE5 ha afiadido un constructor muy útil a Print\Vriter para poder abrir fácilmente un archivo de texto con el
fin de escribir en él. Sin embargo. existen muchas otras tareas comunes que tendremos que realizar una y otra vez y convie-
ne tratar de eliminar el código redundante asociado con dichas tareas.
He aquí la c lase TextFilc que hemos utilizado en ejemplos anteriores de este libro para simplificar la lectura y escritura en
archivos. Contiene métodos estáticos para leer y escribir archivos de texto en una única cadena de caracteres y también
podemos crear un objeto TextFiJe que almacene las líneas del archivo en un contenedor ArrayList (con 10 que tendremos
toda la funcionalidad de ArrayList a la hora de manipular el contenido del archivo):
// : net / mindview / utiI / TexcFile.java
/1 Funciones estáticas para leer y escribir archivos de texto en forma de
// una única cadena de caracteres y para tratar el archivo como un
// contenedor ArrayList.
package net.mindview.util;
import java.io.*;
import java.util. *;
catch(IOException e) {
throw new RuntimeException(e);
return sb.toString() i
catch(IOException el {
throw new RuntimeException(e);
}
/1 Leer un archivo, dividir según cualquier expresión regular:
pUblic TextFile(String fileName, String splitter)
super (Arrays. asList (read (fileName) . spli t (splitt er ) ) ) ;
1/ El método split() con expresiones regulares suele dejar una
18 Entrada/salida 611
catch(IOException el
throw new RuntimeException(e);
JI Prueba simple:
public static void main{String[) args)
String file = read {"TextFile.java" );
write("test.txt", file);
TextFile text = new TextFile{"test.txt");
text.write{"test2.txt") ;
JI Descomponer en una lista ordenada de palabras distintas:
TreeSet<String> words = new TreeSet<String>{
new TextFile("TextFile.java", " \\W+ ")) i
II Mostrar las palabras en mayúsculas:
System.out.println{words.headSet{"a")) ;
1* Output:
[O, ArrayList, Arrays, Break, BufferedReader,
BufferedWriter, Clean, Display, File, FileReader,
FileWriter, I OException, Normally, Output, PrintWriter,
Read, Regular, RuntimeException, Simple, Static, String,
StringBuilder, System, TextFile, Tools, TreeSet , W, Write]
* ///,-
read() añade cada línea a un objeto StringBuilder, seguida de un avance de línea, ya que los caracteres de avance de línea
se eliminan durante la lectura. A conlinuación, devuelve un objeto String que contiene el archivo completo. write() abre el
archivo y escribe en él el texto contenido en String.
Observe que todos los elementos de código en los que se abre un archivo protegen la llamada a c1ose() dentro de una cláu-
sula finally para garantizar que el archivo se cierre correctamente.
El constructor utiliza el método read() para transfonnar el archivo en un objeto String, y luego emplea String.split() para
dividir el resultado en líneas. según estén dispuestos los avances de línea (si utiliza esta clase muy a menudo, trate de rees-
cribir este constructor para mejorar el rendimiento). Como no existe ningún método para combinar las lineas es necesario
utilizar el método write() no estálico para escribir las líneas de fonna manual.
Puesto que esta clase pretende simplificar el proceso de lectura y escritura de archivos, todas las excepciones IOException
se convierten en excepciones RuntimeException, para que el usuario no tenga que emplear bloques try-catch. Sin embar-
go, en los programas reales puede que sea necesario crear otra versión que pase las excepciones IOException al Hamante.
En main(), se realiza una prueba sencilla para verificar que el método funciona.
Aunque esta utilidad no requiere de una cantidad excesiva de código, si que pennite ahorrar una gran cantidad de tiempo de
programación, como veremos posterionnente en algunos de los ejemplos de este capítulo.
612 Piensa en Java
Otra fonna de resolver el problema de leer archivos de texto consiste en utilizar la clase java.utilScanner introduc ida en
Ja va SES. Sin embargo. esa clase sólo sirve para leer archivos, no para escribirlos, y di cha herramienta (que 110 se encuen-
tra en java.io) está diseñada principalmente para crear ana li zadores de lenguajes de programación o "pequeños lenguajes" .
Ejercicio 17: (4) Utilizando TextFile y un contenedor Map<Charaeter,lnteger>, cree un programa que cuente el
número de apariciones de los diferentes caracteres dentro de un archivo (en otras palabras, si la letra 'a'
aparece 12 veces en el arc hivo, el valor Integer asociado con el va lor C hara cter que contenga 'a' en el
mapa será' 12').
Ejercicio 18: (1) Modifique TextFile.java para que pase las excepciones IOException al lIamante .
11: net/mindview/util/BinaryFile.java
I I Utilidad para leer archivos en forma binaria.
package net.mindview.util;
import java.lo.*;
E/S estándar
El ténnino E/S estándar hace referencia al concepto Unix de un único flujo de ¡nfonnación que es utilizado por un progra-
ma (esta idea se reproduce en cierta manera en Windows y muchos otros sistemas operativos). Toda la entrada de un pro-
grama puede provenir de la entrada estill7dar, toda su salida puede ir a la salida estándar y todos sus mensajes de error
pueden enviarse a la salida de error esttÍndm: El valor de la E/S estándar es que los programas se pueden encadenar fácil-
mente entre sí, y la salida de un programa puede ser la entrada estánda r de otro programa. Se trata de una herramienta de
gran potencia.
18 Entrada/salida 613
La razón de la especificación de excepciones es que readLine() puede generar una excepción IO Exception . Observe que
System.in debería normalmente emplearse con un buffer, al igual que la mayoría de los flujos de datos.
Ejercicio 21 : (1) Escriba un programa que tome datos de la entrada estándar y pase a mayúscu las todos los caracteres,
y que luego inserte los resultados en la salida estándar. Redirija los cOlllcnidos de un archivo hacia este
programa (el proceso de redirección variará dependiendo de su sistema operativo).
J* Output:
HelIo, world
* //j ,-
Es importante uti lizar la versión de dos argumentos del constructor de PrintWriter y asignar al segundo argumento el valor
tru e, para pennitir el vaciado automático de buffer; en caso contrario, podríamos no llegar a ver la sa lida.
setIn(JnputStream)
setO ut(Pri ntStream)
setErr(printStream)
La redirección de la salida resulta especialmente útil si comenzamos de repente a generar una gran cantidad de salida en la
pantalla y ésta empieza a desplazarse más rápido de lo que la podamos leer. 4 El redireccionamiento de la entrada resulta
muy úti I para un programa de línea de comandos en el que queramos probar repetidamente una secuencia concreta de entra-
da de datos de usuario. He aquí un ejemplo simple que muestra el uso de estos métodos:
/1: io/Redirecting . java
// Ilustra la redirección de la E/S estándar.
import java.io.*;
Control de procesos
A menudo es necesario ejecutar otros programas del sistema operativo desde dentro de Java y controlar la entrada y la sali-
da de tales programas. La biblioteca Java proporciona clases para reali zar tales operaciones.
Una tarea común consiste en ejecutar un programa y enviar la salida resultante a la consola. En esta sección vamos a pre-
sentar una utilidad que permite simplificar dicha tarea.
Con esta utilidad pueden producirse dos tipos de errores: los errores nonnales que generan excepciones (para los que nos
limitaremos a regenerar una excepción de tiempo de ejecución) y los errores debidos a la ejecución del propio proceso.
lnfonnaremos de dichos errores mediante una excepción diferente:
11 : net/mindview/util/OSExecuteException . java
package net.mindview . util¡
4 En el Capitulo 22, Interfaces gráficas de l/sI/ario, se muestra una solución todavía mas cómoda para este problema: un programa GUI con un area de
texto desplazable.
18 Entradaisalida 615
catch(Exception el {
// Corrección para Windows 2000, que genera una
// excepción para la línea de comandos predeterminada:
if(!comrnand.startsWith("CMD /C"))
command ("CMD /C " + cornmand) i
el se
throw new RuntirneException(e) i
if (err)
throw new OSExecuteException ("Errors executing ti +
cornmand) ;
}
// /,-
Para capturar el flujo de salida estándar del programa a medida que éste se ejecuta, hay que invocar getInputStream( ). La
razón es qu e un objeto InputStream es algo de lo cual podemos leer.
Los resultados del programa llegan línea a línea, as í que los leemos mediante readLine( ). Aquí nos limitamos a imprimir
las líneas, pero también podríamos capturarlas y devolverlas desde cornmand() .
Los errores de programa se envían al flujo estándar de error y se capturan in vocando getErrorStream( ). Si ex isten erro-
res, se imprimen y se genera una excepción OSExecuteException, de modo que el program a llamante pueda gesti onar el
problema.
He aquí un ejemplo que muestra cómo utili zar OSExccute:
//: io/OSExecuteDemo.java
// Ilustra el redireccionamiento de la E/S estándar.
616 Piensa en Java
import net.mindview.util.*¡
/* Output:
Compiled fram "OSExecuteDemo.java"
public class OSExecuteDemo extends java.lang.Object{
public OSExecuteDemo () ;
public static void main ( java.lang.String[) ) ;
}
* /// ,-
Este ejemplo utiliza el descompilador javap (incluido en el JDK) para descompilar el programa.
Ejercicio 22 : (5) Modifique OSExecute.java para que, en lugar de imprimir el flujo estándar de salida, devuelva los
resultados de la ejecución del programa como una lista de cadenas de caracteres. Jlustre con un ejemplo
el empleo de la nueva versión de esta utilidad.
/ * Output:
Sorne text Sorne more
*///;-
Para cualquiera de las clases de flujos de datos mostradas aquí, getChannel( J generará un objelO F'ileChannel. Los cana-
les son bastante básicos. Podemos entregarlos un objeto ByteBuffer para lectura o escritura, y podemos bloquear regiones
del archivo con el fin de obtener acceso exclusivo (hablaremos más sobre esto posterionnente).
Una forma de insertar bytes en un objelO ByteBuffcr consiste en introducirlos directamente utilizando uno de los métodos
put, con el fm de insertar uno o más bytes, o valores de tipos primitivos. Sin embargo. como puede verse en el ejemplo,
también podemos envolver una matriz de tipo byte en un objeto ByteBuffer utilizando el método wrap( J. Cuando hace-
mos esto, no se copia la matriz subyacente, sino que se utili za como almacenamiento para el objeto ByteBuffer generado.
En este caso, decimos que el objeto ByteBuffer está "respaldado" por la matriz.
El archivo data.txt se vue lve a abrir utilizando un objclO RandomAccessF'i1e. Observe que debemos desplazar el objelO
FileChannel por el archivo; en el ejemplo, se le desplaza hasta al final para poder añadir nueva información mediante escri·
ruras adicionales.
Para acceso de sólo lectura. es necesa rio asignar explícitamente un objeto ByteBuffer utili zando el método estático alloca-
te( J. El objetivo de nio consiste en transferir rápidamente grandes cantidades de datos, por lo que el tamaño del objeto
ByteBuffer tiene su importancia: de hecho, el valor de 1K utilizado aquí resulta, probablemente, más pequeño de lo que
normalmente conviene utilizar (tendrá que experimentar con cada aplicación para encontrar el tamaño adecuado).
Resulta posib le obtener una velocidad aún mayor usando allocateDirect( J en lugar de allocate( J, con el fin de generar un
buffer "directo" que pueda estar acoplado de forma aún más estrecha con el sistema operativo. Sin embargo, el gasto adi-
cional de procesamiento de dicho tipo de asignación es mayor, y la implementación varía de un sistema operativo a otro, así
que, de nuevo, será necesario experimentar con cada aplicación para determinar si un buffer directo permite obtener una
ventaja de velocidad.
Después de in vocar read( J para deci rl e al objeto F'ileChannel que almacene bytes en el objeto ByteBuffer, es necesario
invocar flip() en el buffer con el fin de que éste se prepare para la extracción de los bytes que contiene (como puede ver, el
mecanismo parece un poco mdimentario, pero recuerde que es un mecanismo de muy bajo nivel y que está pensado para
obtener la máxima velocidad). Y si fuéramos a utilizar el buffer para operaciones read( ) adicionales, tendríamos también
que invocar clear() para preparar el b,![(er para cada read(). Podemos ilustrar esto mediante un sencillo programa de copia
de archivos:
JJ : ioJChannelCopy.java
II Copia en un archivo utilizando canales y buffers
II {Args: ChannelCopy . java test.txt}
import java.nio.*i
618 Piensa en Java
import java.nio.channels. *i
import java. lo.·;
FileChannel
in = new FilelnputStream(args{O]) .getChannel(),
out = new FileOutputStream(args[l]) .getChannel();
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
while(in.read(buffer) != -1) {
buffer.flip(); 1/ Preparación para la escritura
out.write(buffer) ;
buffer. clear () ; / / Preparación para la lectura
Como puede ver, se abre un objeto FileC ha nn el para lectura y otro para escritura. Se asigna un objeto Byte Bu rre r , y cuan-
do FileC ha nn el.read( ) devuelve - ( (una reminiscenc ia, sin lugar a duda, de Unix y C), querrá decir que hemos alcanza-
do el fina l del flujo de datos de entrada. Después de cada read(), que inserta datos en el buffer, tlip() prepara el buffer para
poder extraer la infornlación con una llamada a wri te(). Después de la ejecución writ e( ), la infonnación continuará estan-
do en el buffer, y clear() permitirá reinicializar todos los punteros internos para que el buffer quede listo para aceptar datos
durante otras llamadas a read ( ).
Sin embargo. el programa anterior no es la forma ideal de gestionar este tipo de operación. Dos métodos especiales
t r a nsrerTo( ) y tra nsrerFrom( ) permiten conectar directamente un canal con otro:
11: io/TransferTo.java
II Utilización de transferTo() entre canales
II {Args: TransferTo.java TransferTo.txt}
import java.nio.channels.*¡
import java.io.*;
public c lass TransferTo
public static void main(String[) args) throws Exception {
if(args.length != 2) {
System. out .println ("arguments: sourcefile destfile") ¡
System.exit(l) ;
FileChannel
in = new FilelnputStream(args [O]) . getChannel (),
out = new FileOutputStream (a rgs[l] ) .getChannel{)¡
in.transferTo(O, in.size(), out);
II O bien,
II out.transferFrom(in, O, in.size())¡
No vamos a tener que hacer este tipo de cosas muy a menudo en nuestras tareas de programación. pero resulta convenien-
te conocerlas.
Conversión de datos
Si volvemos a examinar GetChannel.j ava , observaremos que para imprimir la información del archivo, estamos extrayen-
do los datos de byte en byte y proyectando cada byte sobre un valor charo Esto parece un tanto primitivo: si examinamos
la clase java.nio.C harBufrer, veremos que dispone de un método toString() que dice: "Quiero que me devuel vas una cade-
18 Entrada/salida 619
na de caracteres que contenga los caracteres de este buffer". Puesto que un objeto ByteBuffer puede manipularse como un
buffer de tipo C harBuffer mediante el método asC harBuffer ( ), ¿por qué no usar dicho método? Como puede ver anali-
zando la primera línea de la salida del siguiente ejempl o, dicha solución no funciona :
JI: io/SufferToText . java
JI Conversión de texto para objetos ByteBuffer
import java . nio. * ;
import java . nio.channe l s .* ;
import java.nio.charset .* ¡
import java.io. * ;
fe. clase () ;
fe = new FilelnputStream("data2.txt") .getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE) i
fc.read{buff) ;
buff. flip () ;
II No funciona :
System.out.println(bu ff. asCharBuffer(» ;
II Decodificar usando el conjunto de caracteres
II predeterminado de este sistema:
buff.rewind() i
String eneoding = System. getProperty ("file. encoding") ;
System.out.println(tlDecoded using " T encoding + ": "
+ Charset. forName (encoding) .decode(buff» i
II O bien, podemos codificar con algo que permita imprimir:
fe = new FileOutputStream( tldata2. txt") . getChannel () i
fc.write(ByteBuffer.wrap(
"Sorne text" .getBytes("UTF-16BE " »);
fe. elose () ;
II Ahora intentamos leer de nuevo:
fc = new FilelnputStream("data2.txt") .getChannel();
buff . clear () ;
fC.read{buff) ;
buff.flip{) ;
System.out . println(buf f . asCharBuffer(» ;
II Utilizar un objeto CharBuffer a través del cual escribir:
fc = new FileOutpu tStream ("data2 . txt " ) . getChannel () ;
buff = ByteBuffer . allocate(24); II Más que suficiente
buff.asCharBuffer{) .put{"Some text");
fc.write(buff) i
fe. elose () ;
II Leer y visualizar :
fe = new FilelnputStrearn ("data2 . txt") . getChannel () i
buff . clear () ;
fC.read{buff) ;
buff.flip{) ;
Systern.out.println(bu ff .asCharBuffer(» ;
1* Output :
????
Decoded using Cp1252 : Sorne tex t
Sorne text
Sorne text
* ///,-
620 Piensa en Java
El buffer contiene bytes sin fonnato, y para transfonnarlos en caracteres debemos codificarlos a medida que los introduci-
mos (para que tengan significado cuando se los extraiga) o decodificar/os a medida que salen del bllffer. Esto se puede con-
seguir utilizando la clase java.nio.charset.Charset, que proporciona herramientas para la codificación en muchos tipos
distintos de conjuntos de caracteres:
JJ : ioJAvailableCharSets.java
JJ Muestra los conjuntos de caracteres y sus alias
import java.nio.charset.*¡
import java.util.*¡
import static net . mindview util.Print.*¡
print() ;
/ * Output:
Big5 : csBig5
Big5-HKSCS: big5-hkscs, big5hk, big5-hkscs:unicode3 . 0,
big5hkscs, Big5_HKSCS
EUC-JP: eucjis, x-eucjp, csEUCPkdFmtjapanese, eucjp,
Extended_UNIX_Code_Packed_Format_for_Japanese, x-euc-jp,
euc_jp
EUC-KR: ksc5601, 5601, ksc5601_1987, ksc_5601, ksc5601-
1987, euc_kr, ks_c_5601-1987, euckr, csEUCKR
GB1B030, gblB030-2000
GB2312: gb2312-1980, gb2312, EUC_CN, gb2312-80, eue-cn, euccn, x-EUC-CN
GBK: windows-936, CP936
* /// ,-
Por tanto, volviendo a BufferToText.java, si rebobinamos el bllffer con rewiod() (para retroceder al principio de los datos)
ya continuación utili zamos el conjunto de caracteres predeterminado de esa plataforma para decodificar los datos con deco-
de( ), el objeto de bllffer resultante eharBurrer podrá imprimirse sin problemas en la consola. Para averiguar el conjunto
de caracteres predeterminado, use System.getPropcrty("file.encoding"), que genera la cadena de caracteres que da nom-
bre al conjunto de caracteres. Pasando este nombre a Charset.forName() se genera el objeto Charset (conjunto de caracw
teres) que puede utili zarse para decodificar la cadena.
Otra alternativa consiste en codificar (con encode( » utilizando un conjunto de caracteres que pemlita obtener algo que
pueda imprimirse en el momento de leer el archivo, como podemos ver en la tercera parte del archivo BufferToTcxt.java.
Aquí, se utiliza UTF-16BE para escribir el texto en el archivo, y en el momento de leerlo, todo lo que hay que hacer es con-
vertirlo a un objeto CharBuffer, el cual generará el texto esperado.
Por último, podemos ver lo que sucede si escribimos en el objeto ByteBuffer a través de un objeto CharBuffer (analizare-
mos este tema con más detalle posterionnente). Observe que asignamos 24 bytes para el objeto ByteBuffer. Puesto que cada
18 Entrada/salida 621
valor char requiere dos bytes, esto es suficiente para 12 caracteres, pero "Some text" sólo tiene 9. Los restantes bytes. con
va lor cero, continuarán apareciendo en la representación del objeto CharBuffe r generada por su método toStrillg(), como
puede verse en la salida.
Ejercicio 23 : (6) Cree y pruebe un método de utilidad para imprimir el contenido de un objeto C harB uffer basta la posi-
ción en que los caracteres dejen de ser imprimibles.
Extracción de primitivas
Aunque un objeto ByteBuffe r sólo almacena bytes, contiene métodos para generar cada uno de los diferentes tipos de valo-
res primit ivos a parti r de los bytes que contiene. Este ejemplo ilustra la inserción y la extracción de varios valores em-
pleando dichos métodos:
/1 : io/GetData.java
/1 Obtención de diferentes representaciones
II a partir de un objeto ByteBuffer
import java.nio.*¡
import static net.mindview.util.Print.*;
/ * Output:
i = 1025
H o w d y
12390
99471142
99471142
9.9471144E7
9 . 9471142E7
* /// ,-
Después de asignar un objeto ByteBuffer, se comprueban sus valores para ver si el proceso de asignación del buffer pone
a cero automáticamente el contenido; como podemos ver. así sucede. Se comprueban los 1.024 valores (hasta el límite de
buffer obtenido con Iimit( )), y veremos que todos ellos son cero.
La fonna más fácil de insertar valores primitivos en un objeto ByteBuffer es obtener la "vista" apropiada de dicho buffer
utilizando asCharBuffer( ), asShortBuffer( ), etc. , y luego empleando el método put( ) correspondiente a dicha lista.
Podemos ver en el ejemplo que éste es el proceso que se ha utilizado para cada uno de los tipos de datos primitivos. El único
de estos casos que resulta un poco más extraño es el método put( ) para el objeto ShortBuffcr, que requiere una proyec-
ción de datos (observe que la proyección trunca y modifica la proyección resultante). Todos los demás buffers utilizados
como vistas no requieren que se efectúe ninguna proyección de datos en sus métodos put( ).
/ * Output:
99
11
42
47
1811
143
18 Entrada/salida 623
811
1016
* ///,-
Se utili za primero el método sobrecargado put( ) para almacenar una matriz de valores ¡nt. Las siguientes ll amadas a los
métodos ge!() y pu!() acceden directamente a una posición in! dentro del objeto By!eBuffer subyacente. Observe que estos
accesos mediante la posición absoluta están tamb ién disponibles para los tipos primitivos si manipulamos directamente el
objeto By!eBuffer.
Una vez rellenado el objeto ByteBuffer con objetos in! o algún otro tipo primitivo a través de un bllffer de vis ta, podemos
escribir el objeto By!eBuffer directamente en un canal. También podemos, con igual fac ilidad, leer de un canal y usar un
buffer de vista para convertir todo a un tipo concreto de primitiva. He aquí un ejemplo que interpreta la secuencia de bytes
como va lores short, int, 11oat, long y double generando diferentes buffers de vista para el mismo objeto Byte BufTer:
JI: io /Vi ewBuffers.java
import java.nio. *;
import static net.mindview.util.Print.*;
while (db.hasRemaining ()
printnb(db position{)+ " -> n + db.get() + ") i
/ * Output:
Byte Buffer O ->0,1->0, 2 - > 0 , 3 - > O, 4 - > O, S - > O, 6 - > O, 7 - > 97,
Char Buffer O -> , 1 -> 2 -> , 3 -> a,
Float Buffer °
- > 0.0, 1 -> 1.36E-43,
Int Buffer °
-> 0, 1 -> 97,
Long Buffer O -> 97,
Short Buffer O - > 0, 1 -> 0, 2 -> 0, 3 - > 97,
Double Buffer °
-> 4.8E - 322,
* /// ,-
El objeto ByteBuffer se genera "envo lviendo" una matri z de ocho bytes que a continuación se visualiza a tra vés de buffers
de vista apropiados para todos los di ferentes tipos primiti vos. Podemos ver en el siguiente diagrama las distintas formas en
qu e los datos aparecen cuando se los lee desde los diferentes tipos de buffers:
o I O O I O O I O O
J 97 byte
a char
O O O 97 shOI1
O 97 int
97 long
4.8E-322 doubl e
Terminaciones
Las diferentes máquinas pueden utilizar di fe rentes esquemas de ordenación de los bytes a la hora de almacenar los datos.
Las máq uinas con "tem1 inac ión alta" (big endian) colocan el byte más signifi cati vo en la dirección de memoria más baja,
mientras que las máqu inas con "terminac ión baja" (liule endian) colocan el byte más significati vo en la dirección de memo-
ri a más alta.
A la hora de almacenar una magnitud con un tamaño superior a un byte, como por ejemplo int, float, etc., puede ser nece-
sario tener en cuenta la ordenación de los bytes. Un objeto ByteBuffer almacella los datos en fom1ato de tem1inación alta
y los datos enviados a través de una red siempre utilizan tenninación alta. Podemos cambiar el tipo de tenninac ión de un
obj eto ByteBuffer utili za ndo order() y pasando a di cho método el argumento ByteOrder.BIG_ENDIAN o
ByteOrder.LITTLE_ENDIAN .
Considere un objeto ByteBuffer que contenga los siguientes dos bytes:
b1 b2
18 Entrada/salida 625
Si leemos los datos como un valor sho r! (ByteBuffer.asShortBuffer( )), obtendremos el número 97 (00000000 01100001 l,
pero si cambiamos a tenninación baja, obtendremos el número 24832 (O1I 0000 1 00000000).
He aquí un ejemplo que muestra cómo se modifica la ordenación de los bytes en los caracteres dependiendo de la term ina-
ción elegida:
/ , Output :
[0, 97, 0 , 98, 0 , 99, 0, 1 0O, 0, 101,
°,
102]
[ O, 97,
°, 98, 0, 99, 0 , 1 0O , 0 , 101,
°,
102]
[97 , 0 , 98 ,
, /// , -
°, 99 ,
°, 100 , 0 , 101 , 0, 102, O]
Al objeto ByteBuffer se le asigna suficiente espacio para almacenar todos los bytes de una matri z de caracteres, de modo
que podemos in vocar el método array( ) para visua lizar los bytes subyacentes. El método array( l es "opcional" y sólo se
puede invocar sobre un buffer que esté respa ldado por una matri z; en caso contrario, se generará una excepción
Unsu pportcdOpcration Exception .
Al visualizar los bytes subyacentes, podemos ver que la ordenación predetenninada coincide con la impuesta por el s i ste~
ma de tenninación alta, mientras que el sistema de terminación baja invierte los bytes.
t
FilelnpulStream
t
Socket
ceí 1)
l
Utilidades
Canales
FileOutputStream DatagramSocket
RandomAccessFile ServerSocket
t getChannelO
r1 FileChannel
write(BvteBuffer)
ByteBuffer I
read(ByteBuffer)
;- -- -- - -~ - ------- -- ;,
:, I MappedByteBuffer I ,,
: Apare?8 en el espacio de :
: direCCiones del proceso :
~--- - --------------_.
arraYO/get(byteO)
I byteO
~
wrap(byteO)
arrayO/get(ftoatO) asFloatBufferO
1ftoatO 1- wrap(ftoatO) .1 FloatBuffer 1
array(Vget(intO) aslntBuffer()
1 intO 1" wrap(intO) .1 In!Buffer 1
arrayO/get(longO) asLongBufferO
1 10ngO 1_ .1 LongBuffer
1
wrap(longO)
arraYO/get(shortO) asShortBuffer()
1shortO 1- • 1ShortBuffer 1
wrap(shortO)
Charset
newDecoder()
decode(ByteBuffer)
De un flujo de bytes codificado
clear( ) Borra el buffer, establece la posición en cero y asigna al límite el valor de la capacidad.
Invocamos este método para sobreescribi r un buffer existente.
Oip( ) Asigna al /imite el valor de posición y asigna a posición el va lor cero. Este método se uti-
liza para preparar el buffer para una lectura después de haber escrito datos en él.
hasRemai nin g( ) Devuelve true si existe algún elemento entre posición y límite.
Los métodos que insertan y extraen datos del buffer actualizan estos índices para reflejar los cambios.
Este ejemplo utili za un algoritmo muy simple (intercambio de los caracteres adyacentes) para cifrar y descifrar los caracte-
res contenidos en un objeto CharBuffer:
/1 : io j UsingBuffers. j ava
import java.nio.*¡
import static net.mindview.util.Print.*¡
/ * Ou tput:
UsingBuffers
s UniBgf uefs r
UsingBu ffers
* /// ,-
Aunque podríamos generar un buffer de tipo CharBuffer directamente invocando wrap() con una matriz de tipo char,
asignamos en su lugar un objeto ByteBuffer subyacente, generándose el objeto e h.rBuffer como una vista del ByteBuffer.
Este método enfatiza el hecho de que nuestro objetivo es siempre manipulado como un objeto ByteBuffer, ya que es éste
objeto el que interactúa con un canal.
He aquí el aspecto del buffer a la entrada del método the symmetricScramble( ):
628 Piensa en Java
La posición apunta al primer elemento del buffer, y la capacidad y el límite apu ntan al último elel11ento.
En sym metricScramble( ), el bucle while realiza una serie de iteraciones hasta que posición es equivalente a límite. La
posición del buffer va ría cada vez que se invoca una funciona get( ) o put( ) relativa. También se pueden invocar métodos
get( ) y put() absolutos, que incluyen un argumento de índice que especifica la ubicación en la que la operac ión get( ) o
put( ) tienen lugar. Estos métodos no modifican el valor de la posición del bl/ffer.
Cuando el control entra en el bucle whi le, el va lor de marca se fija utilizando una llamada a mark( ). El estado del buffer
es entonces:
Las dos llamadas a get() relativas guardan el va lor de los dos primeros caracteres en las variables el y c2 . Después de estas
dos llamadas, el buffer tendrá el siguiente aspecto:
Para realizar el intercambio, necesitamos escribi r e2 en posición = O Y el en posición = l. Podemos em plear el metodo abso -
luto de inserción para co nseguir esto, o asignar a posición el va lor de marca, que es precisamente lo que hace el método
reset( ):
Durante la siguiente iteración del bucle, se asigna a marca el valor actual de posición:
18 Entrada/sal ida 629
El proceso continúa hasta que se ha recorrido todo el buffer. Al final del bucle while, posición apuntará al final del buffer.
Si imprimimos el buffer, sólo se imprimirán los caracteres entre posición y ¡imite. Por tanto, si querernos mostrar el Conte-
nido completo del buffer, deberemos fijar la posición al princ ipio del buffer uti lizando rewind( ). He aquí el estado del
buffer después de la llamada a rewínd( ) (el valor de marca pasa a ser indefinido):
Cuando se invoca de nuevo la función symmetricScramble( ), el buffer CharBuffer pasa por el mi smo proceso y se res-
taura a su estado original.
Rendimiento
Aunque el rendimiento del "antiguo" mecanismo de flujos de datos de E/S se ha mejorado al implementarlo con oio, el acce-
so a archivos mapeados tiende a ser muchisimo más rápido. Este programa hace una comparativa simple de rendimiento:
11 , io/MappedIO.java
import java.nía. ·;
import java.nio.channels.*¡
import java.io. *¡
),
new Tester (ItMapped Write") {
public void test() throws IOException
FileChannel fe =
new RandomAceessFile(lItemp.tmplt, "rw")
.getChannel() ;
IntBuffer ib = fc.map(
FileChannel.MapMode.READ_WRITE, O, fC.size(»
.aslntBuffer() ;
for(int i = O; i < numOflnts; i++)
ib.put(i ) ;
fe.close() ;
),
new Tester("Stream Read lt ) {
public void test() throws IOExeeption {
DatalnputStream dis = new DatalnputStream(
new BufferedlnputStream (
new FilelnputStream (lttemp. tmp") » ;
for(int i = O; i < numOflntsi i++)
18 Entrada/salida 631
dis.readlnt() ;
dis.close() i
)
),
new Tester{"Mapped Read") {
public void test() throws IOException (
FileChannel fe = new FilelnputStream(
new File (lItemp .tmpll ) ) .getChannel();
IntBuffer ib = f c.map(
FileChannel . MapMode.READ_ONLY, O, fC.size{»
. asIntBuffer () ;
while (ib.hasRemaining () )
ib.get();
fe. close () ;
)
).
new Tester ( "Stream Read/Write") {
public vold test() throws IOException
RandomAccessFile raf = new RandomAccessFile(
new File("temp.tmp ll), "rw");
raf .writelnt (1);
for (int i = O; i < numOfUbufflnts; i++) {
raf. seek 1raf .length () - 4 );
raf.writelnt(raf,readlnt(» ;
raf.close() ;
)
),
new Tester ( "Mapped Read/Write") {
public void test() throws IOException
FileChannel fe = new RandomAccessFile(
new File("temp.tmp"), "rw ") .getChannel();
IntBuf fer ib = fc.map (
FileChannel.MapMode.READ_WRITE, 0, fC.size(»
.asIntBuffer() ;
ib.put(O) ;
for(int i = 1; i < numOfUbuffInts; i++)
ib.putlib.getli - 1));
fe. close () ;
)
);
public static void main(String [] args) {
for(Tester test : tests)
test.runTest() ;
Aunque podría parecer que una escritura mapeada debería utilizar un flujo FileOutputStream , todas las operaciones de sali-
da en el mecanismo de mapeo de archivos deben utilizar un objeto RandomAccessFile, al igual que se hace con las opera-
ciones de lectura/escritura en el programa anterior.
Observe que los métodos tcst() incluyen el tiempo necesario para inicializar los distintos objetos de E/S, de modo que aun-
que la configuración de los archivos mapeados puede requerir un gasto de procesamiento considerable, la ganancia global
de ve locidad. por comparación con la E/S basada en flujos de datos resulta significati va.
Ejercicio 25: (6) Experimente cambiando las instrucciones ByteBuffer.aUocate( ) de los ejemplos de este capitulo por
8yteBuffcr.allocateDirect(). Demuestre las diferencias de rendimi ento que existen, pero observe también
si el tiempo de arranque de los programas se modifica de manera perceptible.
Ejercicio 26: (3) Modifique strings/JGrep.java para utili zar archivos mapeados en memoria al estilo nio de Java.
Bloqueo de archivos
El bloqueo de archi vos pennite sincroni zar el acceso a un archivo utilizado como recurso compartido. Sin embargo, las dos
hebras que compiten por el mismo archivo pueden estar en diferentes máquinas virtuales Java, o bien una de ellas puede ser
una hebra de programación Java y la otra puede ser alguna hebra nativa del sistema operati vo. Los bloqueos de archivo son
visibles para otros procesos del sistema operativo, porque el mecani smo de bloqueo de archivos de Java se mapea directa~
mente sobre la funcionalidad de bloqueo nati va del sistema operativo.
He aqui un ejemplo simple de bloqueo de archivos.
11 : io/FileLocking.java
import java.nio.channels. * ;
import java .util.concurrent.*¡
import java.io. *;
fos.close() i
1* Output:
Locked File
Released Lock
* ///,-
Podemos obtener un bloqueo (FileLock) sobre el archi vo completo invocando tryLock() o lock() sobre un objeto
FileChannel. (SocketChannel, DatagramChannel y ServerSocketChannel no necesitan bloqueos, ya que son inherente-
mente entidades de un único proceso, generalmente un socket de red no se comparte entre dos procesos). tryLock( ) es no
bloqueante: este método trata de establecer el bloqueo, pero si no puede (porque algún otro proceso ya ha establecido el
mismo bloqueo y éste no es de tipo compartido), simplemente se limita a vo lver del método tenninando así la llamada.
lock() se bloquea hasta que adquiere el bloqueo indicado, o hasta que se interrumpe la hebra que ha in vocado lock( ),
o hasta que se cierra el canal para el cual se ha invocado el método lock( ). Un bloqueo se libera utilizando FileLock.
release( ).
También es posible bloquear una parte del archivo con:
tryLock{long posición, long tamaño, boolean compartido)
o
lock(long posición, long tamaño, boolean compartido)
18 Entrada/salida 633
que bloquea la región (ta ma ño - posición). El terce r argumento especifica si este bloqueo es compartido.
Aunque los métodos de bloqueo que no utilizan argumentos pueden adaptarse a los cambios en el tamaño de un archivo, los
bloqueos con un tamaño fijo no se modifican cuando cambia el tamaño del archivo. Si se establece un bloqueo para una
región comprendida entre posición y posición + tamaño y el archivo se inc rementa más allá de posición + tama ño, enton-
ces la sección situada después de posic ión + ta ma ño no estará bloqueada. Los métodos de bloqueo que no utili zan argu-
mentos bloquean el archivo com pleto, incluso si és te aumenta de tamalio.
El soporte para los bloqueos exclusivos o compartidos debe ser proporcionado por el sistema operativo subyacente. Si el
sistema operativo no so porta los bloqueos compartidos y se solicita uno de estos bloqueos, en su lugar se emplea un blo-
queo exclusivo. El tipo de bloqueo (compartido o exclusivo) puede consultarse utilizando FileLock.isS hared().
fl.release () ;
System.out.println ( "Released: "+start+" to "+ end ) i
catch (I OException e l {
thro w new Run t imeException (e ) ;
La clase de hebra LockAndModify confi gura la región del buffer y crea con slice() un fragmento para modifi carl o. En
run(), se establece el bloqueo sobre el ca nal del archi vo (no se puede establecer un bloqueo sobre el buffer, sólo sobre el
canal). La llamada a lock() es mu y similar a establecer un bl oqueo de un objeto en una hebra de programación: después de
la llamada dispondremos de una sección críti ca con acceso exclusivo a di cha parte del archi vo.5
Los bloqueos se liberan automáti camente cuando tennjna la ejec ución de la máquina JVM o cuando se cierra el canal para
el que se haya n establecido los bloq ueos, pero también se puede in vocar ex plícitamente release() sobre el obj eto FileLock
como se muestra en el ejemplo.
Compresión
La biblioteca de E/S de Java contiene clases que permiten manejar fluj os de lectura y escri tura en fom13to comprimido. Estas
clases se envuelven en otras clases de E/S para proporcion ar la funcionalidad de compresión.
Estas clases no deri van de las clases Reader y \Vriter, sino que forman parte de las jerarqu ías InputStream y
OutputStream. Esto se debe a que la bibli oteca de comprensión trabaja con bytes, no con caracteres. Sin embargo, a veces
podemos vernos forzados a mezclar los dos tipos de fluj os (recuerde que puede utili zar InputStreamReader y
OutputStreamWriter para proporcionar un mecanismo senci llo de conve rsión entTe un tipo y otro).
Checkedlnl1utStream GetCheckSum() proporciona la suma de comprobac ión para cualq uier objeto InputSlream
(no si mplemente descompresión).
CheckedOutputStream GetChcckSum() proporciona la suma de comprobac ión para cualqui er objeto OutputStream
(no si mplemente compres ión).
ZipOutputStream UIl fl ujo DeflaterOutputStream que comprime datos en el fonnato de archivos Zip.
ZiplnputStream Un flujo InflaterlnputStream que descomprime los datos que hayan sido al macenados en el
formato de archi vos Zip.
GZIPlnputStrcam Un flujo lnflaterlnputStream que descomprime los datos que hayan sido alm acenados en el
fomlato de archi vos GZIP.
Aunque existen muchos algoritmos de compresión, Zip y GZrP son posiblemente los más comúnmente utili zados. De este
modo, podemos manipular fácilmente los datos comprimidos con muchas de las herramientas disponibles para la lectura y
escritura de estos fonnatos de archi vo.
5 Puede encontrar más detalles acerca de las hebras de programación en el Capitulo 21, Concurrencia.
18 Entrada/salida 635
ji : io j ZipCompress.java
1/ Utiliza compresión Zip para comprimir cualquier
1/ número de archivos que se indique en la línea de comandos.
II {Args, ZipCompress.java}
import java.util.zip.*¡
import java.io.*;
import java . util.*¡
import statie net.mindview.util.Print . *¡
out. clase () ;
II ¡La suma de comprobación sólo es válida
II después de cerrar el archivo!
print ( "Checksum: " + csum . getChecksum{ ) .getValue (» i
II Ahora extraer los archivos:
print ( uReading file" ) ;
FilelnputStream fi = new Filelnput Stream ( " test. zip" ) i
Chec kedlnputStream csumi =
new CheckedlnputStream (fi, new Adler32 (» i
Ziplnpu tStre a m in2 = new ZiplnputStream (cs umi ) ¡
BufferedlnputStream bis = new BufferedlnputStream (in2 ) i
ZipEntry ze;
while « ze = in2 .getNextEntry () ! = null ) {
print ( "Reading file + ze ) ;
11
int x¡
while « x = bis . read (» ! = -1 )
System.out.write (x ) ;
i f (args.length = = 1)
print ( "Checksum: 11 + csumi.getChecksum () .getValue ()} ;
bis.close () ¡
II Forma alte r nativa de abrir y leer archivos Zip:
ZipFile zf = new ZipFile ( "test.zip " ) ;
Enumeration e = zf . entries () ¡
while (e. hasMoreElements (» {
ZipEnt r y ze2 = (Z i pEntry ) e . nextElement () ;
18 Entrada/sal ida 637
/ * if (args.length == 1) */
Las opciones son simplemente un conjunto de letras (no hace falta ningún guión ni ningún otro sí mbolo indicador). Los
usuarios de Unix/ Linux se percatarán de la similitud que existe con las opciones de taro Las opciones dispon ibles son:
638 Piensa en Java
r Comunica al programa: "Voy a proporcionarte el nombre del archivo", Si no se usa esta opción, jar
presupone que su enl rada procede de la entrada estándar, 0, si está creando un archivo, que su sali-
da irá a la sal ida estándar.
m Espec ifi ca que el primer argumento va a ser el nombre del archivo de manifiesto creado por el
usuario.
,- Genera una sa lida más pro lija que describe lo que jar está haciendo.
O Sólo almacena los archivos, sin com primirlos (utilice esta opción para crear un archivo JAR que
pueda incluir en su ruta de clases).
Si se incluye un subdirectorio dentro de los archivos que hay que insertar en el archivo JAR, dicho subdirectorio se añade
automáticamente incluyendo todos sus subdirectorios, etc. La infonnación de ruta también se preserva.
He aqui algunas fonna .ipicas de invocar jaroEl siguiente comando crea un archivo JAR denominado myJa r File.jar que
contiene todos los archivos de clases del directorio actual, junto con un archivo de manifiesto generado automáticamente:
jar cf rnyJarFile.jar *. class
El siguiente comando es como el del ejemplo anterior, pero añade un archivo de manifiesto creado por el usuario que se
denomina rnyManifestFile.mf:
jar cmf myJarFile. j ar myManifestFile.mf *.class
Este otro comando genera una tabla de contenidos de los archivos en myJarFHe.jar:
jar tf myJarFile.jar
En el siguiente comando se añade la opción de salida "verbosa", para proporcionar infonnación más detallada acerca de los
archivos myJarFile.jar:
jar tvf myJarFil e .jar
Suponiendo que audio, classes e image sean subdirectorios, el siguiente comando combina todos los subdirectori os dentro
del archivo myApp.jar. También se incluye la opción "verbosa" para obtener infonnación adicional sobre un proceso mien-
tras que está trabajando el programa jar:
jar cvf rnyApp. j ar audio classes image
Si se crea un archivo JAR utilizando la opción O (cero), dicho archivo puede incluirse en la variable CLASSPATH:
CLASSPATH=" libl. j ar i lib2 . jar i "
Con esto, Java podrá explorar libl.jar y Iib2.jar en busca de archivos de clase.
La herramienta j ar no es de propósito tan general como una utilidad Z ip. Por ejemplo, no podemos añadir archi vos a un
archivo JAR ni actualizar los archivos existentes; los archivos JAR sólo pueden crearse partiendo de cero. Asin1ismo, tam-
poco se pueden desplazar archivos a un archi vo JAR, borrando los originales a medida que se los desplaza. Sin embargo,
un archivo JAR creado en una plataforma podrá ser leído transparente mente por la herramienta jar en cualquier otra plata-
fonna (evitándose así uno de los problemas que en ocasiones afecta a las uti lidades Zip).
Como veremos en el Capítulo 22, ¡me/faces gráficas de usuario, los archivos JAR se utili zan para empaquetar componen-
tes Java Beans.
18 Entrada/salida 639
Serialización de objetos
Cuando se crea un objeto, éste existe durante todo el tiempo que se le necesite, pero siempre deja de existir en cuanto el pro-
grama termina. Aunque esto tiene bastante sentido a primera vista, existen situac iones en las que sería enonnemente úti l que
un programa pudiera existi r y almacenar su información incluso cuando el programa no se estuviera ejecutando. Si esto fuera
as í, la siguiente vez que iniciáramos el programa, el objeto ya se encontraría allí y contendría la misma infonnación que
tuviera la vez anterior que se ejecutó e l programa. Por supuesto, podemos conseguir un efecto similar escribiendo la infor-
mación en un arch ivo o en una base de datos, pero si tratamos de mantener el espíritu de que todo sea un objeto, resultaría
bastante conveniente poder declarar un objeto como "persistente", y que el sistema se encargara de reso lver todo s los deta-
lles por nosotros.
El mecanismo de serialización de objetos de Ja va nos pennite tomar cualquier objeto que implemente la interfaz
Serializable y transfonnarlo en una secuencia de bytes que pueda restaurarse poste rionnente de modo completo, para rege-
nerar e l objeto original. Esto es así incluso si estamos trabajando a través de una red, lo que significa que el mecanismo de
serialización trata de compensar automáticamente las diferencias existentes en los di stintos sistemas operativos. En otras
palabras, podemos crear un objeto en una máquina Windows, serializarlo y enviarl o a través de la red a un máquina Unix,
donde podrá ser correctamente reconstruido. No es necesario preocuparse acerca de las representaciones de los datos en las
distin tas máqu inas, de la ordenación de bytes, ni de cualquier otro detalle.
En sí misma, la seri alización de objetos resu lta interesante porque nos permite implementar lo que se denomina persisten-
cia ligera. El concepto de persistencia quiere dec ir que el ti empo de vida de un objeto no está detem1inado por si un pro-
grama se esté ej ecutando; el objeto continúa existiendo entre sucesivas invocaciones del programa. Tomando W1 objeto
serializable, escribiéndolo en disco para posterionnente restaurar dicho objeto cuando se vuelva a invocar el programa,
podemos obtener el efecto de persistencia. La razón por la que a esa persistencia se le denomina " ligera" es que no pode-
mos limitarnos simplemente a definir un objeto utili zando algún tipo de palabra clave " persistent" y dejar que el sistema
se ocupe de los detalles (aunque quizá pueda hacerse esto en el futuro) . En lugar de ello. podemos se rial izar y des-seriali-
zar explícitamente los objetos en nuestro programa. Si necesitamos lm mecanismo de persistencia más se ri o, considere la
uti lización de alguna herramienta como (hllp :l/hibernate.sourceforge.net). Para obtener más detalles, consulte Thinking in
Enterprise Java , que se puede descargar en la dirección H'W\V. MindVieH'.nel.
La serialización de objetos se añadió al lenguaje para soportar dos características principales. El mecanismo RMI (Rernote
Me/llod Invoco/ion, invocación remota de métodos) de Java pennite que los objetos que residen en otras máquinas se com-
porten como si estuvieran en nuestra propia máquina. Cuando se envían mensajes a los objetos remotos, la serialización de
objetos es necesaria para transportar los argumentos y los va lores de retomo. El mecanismo de RM] se analiza en Thinking
in Enterprise Java.
La serialización de objetos también es necesaria para el sistema de com ponentes Java Beans, descrito en e l Capítulo 22,
¡me/faces gráficas de uSI.lQr;o. Cuando se utiliza un componente Bean, su infom1ación de estado suele configurarse, gene-
ralmen te, en tiempo de diseño. Esta infonnación de estado debe almacenarse, para poder recuperarse posteriormente cuan-
do se inicie el programa; el mecanismo de serialización de objetos se encarga de esta tarea.
La serialización de un objeto es una tarea bastante simple, siempre y cuando el objeto implemente la interfaz Serializable
(ésta es una interfaz marcadora que no incluye ningún método). Cuando se añadió la serial ización al lenguaje, se modifica-
ron muchas clases de la biblioteca estándar para hacerlas serial.izables. incluyendo todos los envoltori os de los tipos primi-
tivos, todas las clases contenedoras y muchas otras. Incluso los objetos Class pueden serializarse.
Para serial iza r un objeto, creamos algún tipo de objeto OutputStream y luego lo envolvemos dentro de un objeto
ObjectOutputStream. Con esto, lo único que necesitamos es invocar writeObjcct( ), y el objeto se serializará y se envia-
rá al flujo de sal ida OutputStream (la serialización de objetos está orientada a bytes, por 10 que uti liza las jerarquías
InputStream y OutputStrea m ). Para inverti r el proceso, envolve mos un objeto InputStream dentro de un objeto
Objectlnput5tream e invocamos readObject() . Lo que este método nos devuelve es, como de costumbre, una referencia
a un objeto generalizado de tipo Object, con lo que es necesario realizar una especialización para que todo funcione correc-
tamente.
Un aspecto particulannente inteli gente del mecanismo de serialización de objetos es que éste no sólo guarda una imagen de
nuestro objeto, sino que también sigue todas las referencias contenidas en nuestro objeto y guarda esos objetos, siguiendo a
su vez todas las referencias de cada uno de esos objetos, etc. Esto se denomina en ocasiones la "red de objetos" a la que un
único objeto puede estar conectado, e incluye matrices de referencias a objetos, además de objetos miembros. Si tuviéramos
640 Piensa en Java
que mantener nu estro propio esquema de seriali zación de objetos, mant ener el código para poder segu ir todos nuestros vín-
culos sería enanncmente dificil. Sin embargo, e l mecanismo de se riali zac ión de objetos de Java parece poder reali za r esta
tarea de manera muy precisa, utili za ndo sin ninguna duda algún algoritmo optimizado qu e recorre la red de objetos. El
siguiente ejemplo pennite probar e l mecanismo de seriali zación utili zando una cadena de objetos vinculados, cada uno de
los cuales tiene un enlace al siguiente seg mento de la cadena, así C0l110 una matri z de referencias a objetos de una clase di s-
tinta, Data :
1/: io/Worm.java
/1 Ilustra el mecanismo de serialización de objetos.
import java.io.·;
import java.util. * ;
import static net . mindview.util.Print.*;
public Worm () {
print("Default constructor"};
/ , Output:
Worm constructor: 6
Worm constructor: S
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worrn constructor: 1
w = ,a(853) ,b(1l9) ,c(802) ,d(788) ,e (199) ,f(881)
Worm storage
w2 = ,a(853) ,b(1l9) ,c(802) ,d (788) ,e(199) ,f(881)
Worm storage
w3 = ,a(853) ,b(1l9) , c(802) ,d (788) ,e(199) ,f(881 )
* ///,-
Para que las cosas sean interesantes, la matri z de objetos Data contenida en la cadena Worm se inicial iza con números alea-
torios (de esta forma, eliminamos las sospechas de que el compilador esté conservando algún tipo de meta-infon11ación).
Cada segmento de Worm se etiqueta con un va lor char que se ge nera automáticamente como parte del proceso de genera-
ción rec ursiva de la li sta enlazada de objetos Worm . Cuando se crea un Worm, se le dice al constructor la longitud que que-
remos qu e tenga. Para construir la referencia next, invoca al constructor de Worm con una longitud inferior en una unidad.
etc. La referencia "ext fina l se deja con el va lor null. lo que indica el final de la cadena Worm .
El objetivo de esto es construir una estmctura razonablemente compleja que no pueda serial izarse fáci lmente. Sin embargo,
el ac to de seri alizar es bastante simp le. Una vez que se crea el objeto ObjectOutputStream a partir de algún otro fiujo de
datos, el método writeObject() pennite se ri alizar el objeto. Observe que también se ha incluido una llamada a
writeObject( ) para un objeto String. Se pueden también escribir todos los tipos de datos primilivos ut ilizando los mismos
métodos que DataOutputStream (comparten la misma interfaz).
Existen dos secciones de código separadas que tienen un aspecto similar. La primera escribe y lee un archivo, mientras que
la segunda, para tener un ejem plo más variado, escribe y lee una matri z ByteArray. Podemos leer y escribir un objeto, uti-
lizando el mecanismo de seri alización, en cualquie r fluj o DatalnputStream o DataOutputStream, incluyendo (como
puede verse en Thinking in Entelprise Java) una red .
Podemos ve r, examinando la sa lida, que el objeto des-se riali zado contiene tod os los enlaces que estaban en el objeto ori-
ginal.
Observe que no se invoca ningún constmctor, ni siquiera el constructor predetenninado, en el proceso de des-seriali zación
de un objeto de tipo Scrializable. El objeto completo se restaura recuperando los dat os desde el fiujo de entrada Input-
Stream .
Ejercicio 27: ( 1) Cree una clase Scrializable que contenga una referen cia a un objeto de una segunda clase Serializable.
Cree una instancia de esa clase, serialícela en disco, restáurela a continuación y verifique que el proceso
ha funcionado correctamente.
642 Piensa en Java
Localización de la clase
Podríamos preguntamos qué es lo que hace falta para poder recuperar un objeto a partir de su estado serial izado. Por ejem-
plo. suponga que serializamos un objeto y lo enviamos como un archivo o lo mandamos a través de una red hacia otra
máquina. ¿Podría un programa en la atTa máquina reconstnlir el objeto utilizando únicamente los contenidos del archivo?
La mejor fomla de responder a esta cuestión es (como siempre) reali zando un experimento. El siguiente archivo está con-
tenido en el subdirectorio de este capítulo:
JI : io/Alien.java
/1 Una clase serializable.
import java.io.*¡
public class Alien implements Serializable {} /// :-
El archivo que crea y serial iza un objeto Alie n está incluido en el mismo directorio:
/1 : io/FreezeAlien.java
II Crear un archivo de salida serializable.
import java.io.*;
1* Output:
class Alien
*111 ,-
La simple operación consistente en abrir el archivo y leer el objeto mystery requiere disponer del objeto C lass correspon-
diente a Alieo ; la máquina JVM no puede localizar Alicn.c1ass (a menos que se encuentre en la ruta de clases, lo que no
deberia ser el caso en este ejemplo). Por ello, obtenemos una excepción C lassNot Fo und Exception. La máquina JVM debe
ser capaz de encontrar el archivo .class asociado.
Control en la serialización
Como podemos ver, el mecanismo predetenninado de serialización es bastante sencillo de usar. Pero ¿qué sucede si tene-
mos necesidades especiales? Quizá, haya problemas especiales de seguridad y no queramos serial izar cie rtas partes del obje-
18 Entrada/salida 643
to, o quizá no tiene se mido que uno de los subobjetos sea serial izado si de todos modos hay que crear de nuevo ese subob-
jeto cuando recuperemos el objeto completo.
Podemos controlar el proceso de serial ización implementando la interfaz Externalizable en lugar de la interfaz
Serializablc. La interfaz Externalizable amplia la interfaz Scrializable y añade dos métodos, wrileExlernal() y
readExternal( ). que se invocan automáticamente para el objeto durante la seri alización y la des-serialización, para poder
realizar esas operaciones especiales que necesitamos.
El siguiente ejemplo muestra implementac iones simples de los métodos de la interfa z Exlernalizablc. Observe que Blip J
Y Blip2 so n casi idénticos salvo por una sutil diferencia (trate de desc ubrirla examinando el códi go):
11 , io / Blips.java
// Uso simple de Externalizable, junto con un problema.
impo rt java.io.*¡
import static net.mindview.util.Print.*¡
/ * Output:
Constructing objects:
Blip3 (Stri ng X, int al
A String 47
Saving object:
Blip3.writeExternal
Recovering b3:
Blip3 Constructor
Blip3.readExternal
A String 47
*/1/,-
Los campos s e i sólo se incializan en el segundo co nstructor, pero no en el constructOr predetenninado. EstO quiere decir
que si no iniciali zamos s e i en readExternal(), s será null e i será cero (ya que el espacio de almacenamie nt o del objeto
se pone a cero en el primer paso de la creación del objeto). Si desactivamos mediante comentarios las dos líneas de código
a continuación de las frases: "Hay que hacer esto:" y ejecutamos el programa, podremos ver que al recuperar el objeto s es
null e i es cero.
Si estamos heredando de un objeto Externalizable, lo que haremos normalmente será invocar las versiones de la clase base
de writeExtern.l( ) y readExternal( ), para almacenar y recuperar apropiadamente los componentes de la clase base.
Para hacer que las cosas funcionen correctamente, no sólo hay que escribi r los datos importantes de los datOs del objeto
durante el método writeExternal( ) (no hay ningún comportamiento predetenninado que escriba ninguno de los objetos
miembro de un objeto Externalizable), sino que también hay que recuperar esos datos en el método readExternal() . Esto
puede resultar confuso a primera vista, porque la rea!jzación de las tareas de construcción predetenninadas para un objeto
Externalizable podrían hacer parecer que se está produciendo automáticamente algún tipo de operación de almacenamien-
to y recuperación, pero en realidad no es así.
Ejercicio 28: (2) En Blips.java, copie el archivo y renómbrelo como BlipCheek.java. Renombre también la clase Blip2
como BlipCheek (haciéndola pública y eliminando el ámbito público de la clase Blips en el proceso).
Elimine las marcas de comentario I/! del arc hi vo y ejecute el programa, incluyendo las líneas problemáti-
cas. A continuación, desactive con un comentario el constructor predetenninado de BlipCheck. Ejecute el
programa y explique por qué funciona. Observe que, después de la compilación, es necesario ejecutar el
programa con "j.v. Blips" porque el método main() sigue estando en la clase Blips.
Ejercicio 29: (2) En Blip3.java, desactive co n co mentarios las dos lineas situadas después de las frases: " Hay que hacer
esto:" y ejecute el programa. Ex plique el resultado y las razones de que éste difiera de lo que sucede cuan-
do las dos líneas fonnan parte del programa.
646 Piensa en Java
1* Output: (Sample)
logon a = logon info:
username: Hulk
date: Sat Nov 19 15:03:26 MST 2005
password: myLittlePony
18 Entrada/salida 647
Podemos ver que los campos date y username son normales (no de tipo transient), por lo que se los serial iza automática-
mente. Sin embargo, el campo password es de tipo transient, así que no se almacena en disco; asimismo, el mecanismo de
seriali zación no hace nada por intentar recuperarlo. Cuando se recupera el objeto, el campo password contiene el valor "ull.
Observe que, mientras toString() está constmyendo un objeto String utilizando el operador sobrecargado '+', las referen-
cias null se convierten automáticamente en la cadena "null".
También puede ver que el campo date se almacena en disco y se recupera desde allí, no siendo generado de nuevo.
Puesto que los objetos Extcrnalizablc no almacenan ninguno de sus campos de manera predetenninada, la palabra clave
transient es para ser usada ún icamente por los objetos Serializable.
Desde un punto de vista de diseño, las cosas pueden ser bastante complicadas si recurrimos a esta solución. En primer lugar,
podemos pensar que como estos métodos no fonnan parte de una clase base ni de la interfaz Serializable, deberían ser defi-
nidos en sus propias interfaces. Pero obselVe que esos métodos están definidos como private, lo que significa que sólo los
pueden invocar otros miembros de esta clase. Sin embargo, en realidad no se invocan desde otros miembros de esta clase,
sino que son los métodos writeObject() y readObject() de los objetos ObjectOutputStream y ObjectlnputStream los
que se encargan de invocar a los métodos writeObject() y readObject() de nuestTO objeto (observe cómo estoy contenién-
dome para no entrar en una larga discusión acerca de lo inapropiado que resulta utiliza r aquí los mismos nombres de méto-
dos; por decirlo en pocas palabras: resulta enormemente confuso). Puede estar preguntándose cómo es posible que los
objetos ObjectOutputStream y ObjectlnputStream tengan acceso privado a métodos de nuestra clase. Lo única respues-
ta en la que podemos pensar es que esto forma parte de la magia de la serialización 6
Cualquier cosa que definamos en una interfaz es automáticamente de tipo public, por lo que si writeObject() y
readObject() deben ser privados, eso quiere decir que no pueden formar parte de una interfaz. Puesto que querernos ajus-
tamos a las signaturas exactamente, el efecto es el mismo que si estu viéramos implementando una interfaz.
Cabe imaginar que, cuando invocamos ObjectOutputStream.writeObject(), el objeto de tipo Serializable que pasamos a
ese método es interrogado (utilizando, sin duda, el mecanismo de refl exión) para ver si implementa su propio método
writeObject( ). En caso afirmativo, se omite el proceso normal de seriali zación y se invoca el método writeObject( ) per-
sonalizado. La misma situación se produce para el método readObject().
Existe otra consideración adicional que debemos tener en cuenta. Dentro de nuestro método writeObject(), podemos deci-
dir llevar a cabo la acción writeObject() predeterminada invocando defaultWriteObject(). De la misma forma , dentro de
6 La sección ·· Interfaces e infannación de tipos" al final del Capitulo t4 ./lIformación de lipos, muestra cómo es posible acceder a métodos privados desde
fuera de la clase.
648 Pien sa en Java
readObject( ) podemos in vocar defaultReadObject(). He aquí un ejemplo simple en el que se ilustra cómo puede contro-
larse el almacenamiento y la recuperación de un objeto Serializable:
/ / : io/SerialCtl. java
JI Control de la serialización añadiendo nuestros propios
/ / métodos writeObject () y readObject () .
import java.io. * ¡
/* Output:
Befare:
Not Transient: Testl
Transient: Test2
After:
Not Transient: Testl
Transient: Test2
*///,-
En este ejemplo, uno de los campos Strillg es de tipo nonnal y el otro está definido como transient, para demostrar que el
campo que no es de tipo transient es guardado por el método defaultWriteObject() mientras que el campo transient se
guarda y restaura ex plícitamente. Los campos se inicializan dentro del constructor en lugar de en el punto de defini ción, para
demostrar que no están siendo inicializados por ningún mecanismo de tipo auto mático durante la des-serialización.
Si util izamos un mecanismo predetenninado para escribir las partes no transitori as (no marcadas como transient) del obje-
to, debemos invocar defaultWriteObject() como primera operación en writeObject(), y defaultReadObject() como pri-
mera operación en readObject(). Se trata de sendas llamadas a métodos que resultan un tanto extrañas. Podría parecer, por
ejemplo, que estamos invocando defaultWriteObject() para 1111 objeto ObjectOulputSlTeam sin pasarle ningún argumen-
to, a pesar de lo cua l, ese método es capaz de averiguar la referencia a nu estro objeto y cómo escribir todas las partes no
transitorias. Verdaderamente asombroso.
18 Entrada/salida 649
El almacenamiento y recuperación de los objetos transient utiliza un código más familiar. Y, sin embargo, examine atenta-
mente lo que sucede: en maín( j, se crea un objeto Seríalet! y luego se seriali za en un flujo ObjeelOulputStream (obser-
ve en este caso que se utiliza un buffer en lugar de un archivo; para el objeto ObjcctOutputStream no hay ninguna
diferencia). La serialización ti ene lugar en la línea:
o .writeObject (se);
El método wríteObject( j debe examinar se para ve r si dispone de su propio método wríteObjeet( j (no comprobando la
interfaz, ya que no existe ninguna, ni el tipo de la clase, sino buscando realmente el método mediante el mecanismo de refle-
xión). Si el objeto dispone de ese método, lo utilizará. Para readObject( ) se utiliza una técnica similar. Quizá ésta fuera la
única forma práctica con la que se podía resolver el problema, pero hay que reconocer que resulta un tanto extraña.
Versionado
Es posible que queramos modificar la verslOn de una clase serializable (por ejemplo, los objetos de la clase original
podrían estar almacenados en una base de datos). Este tipo de mecanismo está soportado en el lenguaje, aunque lo más pro-
bable es que no tengamos que recurrir a él más que en casos especiales; el mecanismo requiere un análisis más en profun-
didad que no vamos a realizar aquí. Los documentos del IDK descargables en la dirección hllp:/ljava.su17.com analizan este
terna de forma bastante exhaustiva.
También podrá observar en la documentación del JDK muchos comentarios que comienzan con la advertencia:
los objetos serializados de l/na determinada clase no serán compatibles con las fllturas versiones de
Swing y que el soporTe actual de seriali=ación resulta apropiado para el almacenamiento a corto plazo
O para la invocación RMl entre aplicaciones ..
Esto se debe a que el mecanismo de versionado es demasiado sencillo como para funcionar de manera fiable en todas las
situaciones. especialmente con JavaBeans. Los diseliadores del lenguaje están trabajando para corregir el diseño, y a eso es
a lo que hace referencia la advertencia.
Utilización de la persistencia
Resulta bastante atractiva la posibilidad de utilizar la tecnología de serialización para almacenar parte del estado del progra-
ma, de modo que se pueda posteriormente restaurar con sencillez el programa y devolverlo a su estado actual. Pero, antes
de poder hacer esto, es necesario que respondamos algunas cuestiones. ¿Qué sucede si serializamos dos objetos que tienen
una referencia a un tercer objeto? Cuando se restauren esos dos objetos a partir de su estado serializado, ¿obtenemos una
única instancia de un tercer objeto? ¿Qué sucede si serial izarnos los dos objetos en archivos separados y los des-serializa-
mos en diferentes partes del programa?
He aquí un ejemplo que ilustra el problema:
jj: iojMyWorld.java
import java . io .* ;
import java.util.*¡
import static net.mindview . util.Print .* ¡
/ * Output: (Samplel
animals : [Bosco t he dog[Animal@addbfl] House@42 e B16
I
Por supuesto, lo que cabria esperar es que los objetos des-serial izados tuvieran direcciones diferentes de las de sus origina-
les. Pero observe que en animals1 y animals2 aparecen las mismas direcciones, incluyendo las referencias al objeto House
que ambos comparten. Por otro lado. cuando se recupera animals3, el sistema no tiene fon113 de saber que los objetos de
este a tTo flujo de datos son alias de los objetos del primer flujo de da lOS, así que construye una red de objetos completamen-
te distinta.
Mientras estamos seria lizando todo en un único flujo de datos, recuperaremos la mi sma red de objetos que hayamos escri-
to, sin que se pueda producir ninguna duplicación accidental de los objetos. Por supuesto, podemos modificar el estado
de los objetos en el tiempo que tran scurre entre la escritura de l primer objeto y del último, pero eso es nuestra responsabi-
lidad; los objetos se escribirán en el estado en que se encuentren (y con cualesquiera conex iones que tengan con otros obje-
tos) en e l momento de serial izarlos.
Lo más seguro. si queremos guarda r el estado de un sistema, es hacer la serialización en fonna de operación ·'atómica·'. Si
seria l izamos algunos objetos, realizamos otras tareas y luego serial izamos más objetos, etc .. no estaremos guardando e l esta-
do del sistema de una fonna segura. En lugar de ello, incluya todos los objetos que fonnan parte del estado de su sistema
en un único contenedor y escriba simplemente dicho contenedor como parte de una única operación. Entonces podrá res-
taurarlo tambien con una única llamada a metodo.
El sigu iente ejemplo es un sistema imaginario de diseño asistido por computadora (CAD, compllfer-aicled design) que ilus-
tra la tecnica descri ta. Además. el ejemplo plantea la cuestión de los campos estáticos; si examina la documentación del
JDK, podrá ver que Class es Serializable, así que debe ser sencillo almacenar los campos de tipo static serial izando sim-
plemente el objeto C lass . En cualqu ier caso. parece una so lución ll ena de sent ido común.
1* Output:
18 Entrada/sa lida 653
La clase Shape implementa Serializable, por lo que cualquier cosa que herede de Shapc será también automáticamente de
tipo Serializable. Cada objeto Shape contiene datos y cada clase derivada de Sbape contiene un campo slalic que determi-
na el color de todos esos tipos de objetos Shape (si insertáramos un campo estático en la clase base sólo lendríamos un
campo, ya que los campos estáticos no se duplican en las clases derivadas). Los métodos de la clase base pueden ser susti-
tuidos para configurar el color de los diferentes tipos (los métodos estáticos no se acoplan dinámicamente, así que son méto-
dos normales). El método randomFaclory() crea un objeto Shape diferente cada vez que se lo invoca, utili zando valores
aleatorios como datos para el objeto Shape.
Circle y Square son extensiones sencillas de Shape; la única diferencia es que Circle inicializa color en el plinto de defi-
nición) mientras que Square lo inicializa en el constmctor. Dejaremos el análisis de Une para más adelante.
En main( ), se utili za un contenedor ArrayList para almacenar los objetos Class y el otro para almacenar las fonnas geo-
métricas.
La recuperación de los objetos es bastantc sc ncilla:
11 : io/RecoverCADState.java
II Restauración del estado del supuesto sistema CAD.
II (RunFirst' StoreCADState)
import java.io.*;
import java . util. *;
1* Output:
[class Circlecolor [1] xPos [58] yPos [55] dim [93]
class Squarecolor[O] x Pos[61] yPos(61] dim[29]
class Linecolor[3] xPos[68j yPos[O] dim[22]
class Circlecolor[l] xPos[7] yPos[88] dim[28j
class Squarecolor[O] xPos[51] yPos(89] dim{9]
class Linecolor[3] xPos[78] yPos[98] dim[61]
class Circlecolor[1] xPos[20] yPos[58] dim[16]
class Squarecolor[O] xPos[40j yPos[11] dim[22]
class Linecolor[3] xPos[4] yPos[83) dim[6]
class Circlecolor[l] xPos[75] yPos[10] dim[42]
654 Piensa en Java
Puede ver que los valores de xPos, yPos y dim son almacenados y recuperados satisfactoriamente, pero existe algún pro-
blema con la recuperación de la infonnación de tipo static. Todos los valores son "3" al entrar, pero al salir son distintos.
Los objetos Cirelc ti enen un valor de 1 (RED, que es la definición) y los objetos Squarc ti enen un valor de O(recuerde que
se inicializaba n en el constructor). ¡Es como si los datos de tipo static no se hubieran seriali zado en absoluto! En efecto, así
es: aún cuando la clase Class es Serializable, no hace lo que cabria esperar. Por tanto, si queremos se riali zar valores de tipo
sta tic. debemos hacerlo nosotros mismos.
Para esto es para lo que se utilizan los métodos seriatizeSt.tieState( ) y deserializeStatieState( ) de tipo statie en Line.
Como podemos ver, se los invoca ex plícitamente como parte del proceso de almacenamiento y de recuperación (observe
que es necesario mantener el orden de escritura y lectura en el archivo de serialización). Por tanto, para hacer que estos pro-
gramas fun cionen correctamente es necesario:
l. Añadir sendos métodos serializeStatieState() y deserializeStatieState( ) a las clases.
2. Eliminar el contenedor ArrayList shapeTypes y todo el código relacionado con él.
3. Añadir llamadas a los nuevos métodos estáticos de seriali zación y des-se riali zac ión en las clases que representan
a las distintas fonnas geo métri cas.
Otra cuestión en la que hay que pensar es la de la seguridad, ya que el mecanismo de seriali zació n también guarda los datos
de tipo priva te. Si tenemos problemas de seguridad, dich os campos deben marcarse como transient. Pero entonces, será
necesario diseñar alguna fornla segura de almacenar dicha infonnación, para que cuando efectuemos una restauración, poda-
mos reinicializar dichas variabl es de tipo private.
Ejercicio 30: ( 1) Corrija el programa CADState.java como se ha descrito en los párrafos anteriores.
XML
Una importante limitación de la seriali zac ión de objetos es que es una solución válida sólo para Java: sólo los programas
Java pueden des-serial iza r sus objetos. Una solución más interoperable consiste en convertir los datos a formato XML, lo
que pennite que sean consumidos por una amplia variedad de platafonl1as y de lenguajes.
Debido a su popularidad, ex iste un número enormemente grande y confuso de opciones para programar con XML, inclu-
yendo las bibliotecas javax.xml.* distribuidas con el JDK. Aquí, he dec idido utili zar la biblioteca XOM de código abierto
de ElIione Rusty Harold (puede descargar los archivos y la documentación en wWH',xom.nu) porque parece ser la fonna más
simple y directa de generar y modificar código XML utili za ndo Java. Además, XOM pone un gran énfasis en garantizar la
corrección del código XM L.
Como ejemplo, suponga que tenemos objetos Person que contienen campos para representar el nombre y el apell ido, los
cuales queremos seríali zar mediante código XML. La siguiente clase Person tiene un método getXML() que utili za XOM
para convert ir los datos Person en un objeto EJement XML y un constmctor que toma un objeto Element y extrae los datos
Person apropiados (o bserve que los ejemplos XML están en su propio subdirectorio):
ji : xml / Person.java
JI Utilizar la biblio teca XOM para escribir y leer XML
JI {Requires: nu.xom.Node¡ You must install
/1 the XOM library fr a m http: // www.xo m.nu }
impo rt nU.xom.*¡
import java . io.*;
import java . util.*;
/ * Output:
[Dr. Bunsen Honeydew, Gonzo The Great, Phillip J. Fry]
<?xml version="l.O " encoding="ISO-8859-1"?>
<people>
<person>
<first>Dr. Bunsen</first>
<last>Honeydew</last>
</ person>
<person>
<first>Gonzo </first>
<last>The Great</last>
</ person>
<person>
<first>Phillip J.</first>
<last>Fry</last>
</person>
</people>
* /// ,-
Los métodos XOM son bastante auto-explicativos y puede encontrarlos en la documentación de XOM.
656 Piensa en Java
XOM también contiene una clase Serializer que, como podemos ver, se utili za en el método format( ) para transformar el
código XML a un [onnato más legible. Con invocar simplemente toXML() todo el sistema funciona , asi que Serializer es
una herramienta bastante útil.
Des-seri alizar los objetos Person a partir de un archi vo XML también resulta sencillo:
11 : xml / People. j ava
II {Requ ires: nu.xom . Node¡ You must install
II the XOM library from http. llwww.xom . nu }
II {RunFirst: Person}
import nu. x om. * ¡
import java.util. * ¡
1* Output :
[Dr . Bunsen Honeydew, Gonzo The Great, Phillip J. Fry]
* /11 ,-
El constructor People abre y lee un archivo usando el método Builder.build() de XOM, y el método getChildElements()
genera una lista Elements (no es un objeto List estándar de Java, sino un objeto que sólo tiene un método size() y un méto-
do get( ), Harold no quería obligar a los programadores a utili zar Java SES, pero seguía queriendo disponer de un contene-
dor que fuera seguro en lo que respecta a tipos). Cada objeto Element de esta lista representa un objeto Person, por lo que
se lo entrega al segundo constructor de Persono Observe que esto requiere que conozcamos por adelantado la estructura
exacta del archi vo XM L, pero ésta suele ser la nonna en este tipo de problemas. Si la estructura no se ajusta a lo que espe-
ramos, XOM generará una excepción . También podríamos escribir código más complejo que analizará el documento XML
en lugar de hacer suposiciones acerca del mismo, para aquellos casos en los que tengamos una información menos concre-
ta acerca de la estructura XML entrante.
Para que estos ejemplos puedan compilarse, es necesario incluir los archi vos JAR de la distribución XOM dentro de nues-
tra ruta de clases.
Esto sólo es un a breve introducción a la programación XML con Java y a la biblioteca XOM ; para obtener más informa-
ción, consulte WWlV.xo m.n u.
Ejercicio 31: (2) Añada una infonmación de dirección postal a Person.java y People.java .
Ejercicio 32: (4) Utilizando un contenedor Map<String,lnteger> y la utilidad net.mindview.utiI.TextFile, escriba un
programa que cuente el número de apariciones de las distintas palabras en un archi vo (utilice "\\W+"
como segundo argumento para el constmctor TextFile). Almacene los resultados como un archi vo XML.
Preferencias
La API Preferences está mucho más próxima a lo que son los mecanismos de persistencia que a los de serialización de obje-
tos, porque se encarga de almacenar y recuperar automáticamente infonnación. Sin embargo, su uso está restringido a con-
juntos de datos limitados de pequeño tamaño: sólo se pueden almacenar primitivas de objetos String, y la longitud de cada
objeto String no puede ser superior a 8K (no es un tamaño pequeño, pero tampoco nos permite construir ninguna aplica-
ción seria). Como su propio nombre sugiere, la API Preferences está diseñada para almacenar y extraer preferencias de usua-
rio y opciones de configuración de los programas.
18 Entrada/salid a 657
Las preferencias son conjuntos de clave-valor (como los contenedores Ma p) que se almacenan en una jerarquía de nodos.
Aunque la jerarquía de Dodos puede utilizarse para crear estmcturas complicadas. lo nannal es crear un único nodo con el
mismo nombre que nuestra clase y almacenar allí la infonn3ción. He aquí un ejemplo simple:
JI : io/PreferencesDemo.java
import java.util.prefs.*;
import static net.mindview.util.Print.*¡
/ * Output: (Sampl e)
Location: Oz
Footwear: Ruby Slippers
Companions: 4
Are there witches?: true
UsageCount: 53
How many companions does Dorothy have? 4
* /// ,-
Aqui, se utiliza use r No deForPackage( ), pero también podríamos elegir system NodeFor Package( ); la elección es hasta
cierto puma arbitraria; pero la idea es que "user" es para preferencias de los usuarios individuales, mientras que "system"
es para las opciones generales de configurac ión de una instalación. Puesto que main() es de tipo statie. se utiliza
Preferenecs Oemo.class para utili zar el nodo, pero dentro de un método no estático, probablemente utili za ríamos
getClass(). No es necesario utiliza r la clase actua l como identificador del nodo, aunque esa es la práctica habitual.
Una vez creado el nodo, estará di sponible para cargar o leer los datos. Este ejemplo carga el nodo con varios tipos de ele-
mentos y luego obtiene las claves con keys( ). Las claves se devuelven como un objeto Str ingl l. lo que puede resultar un
tanto sorprendente si estamos acostumbrados a utilizar el método keys( ) de la biblioteca de colecciones. Observe el segun-
do argumento de get( ): se trata de l valor predeternlinado que se genera si no existe ninguna entrada para dicho valor de
clave. Mientras estamos real izando una iteración a través de un conjunto de claves, siempre sabemos si existe una entrada,
así que resulta seguro utilizar nuH como va lor predetemlinado, pero lo nonnal es que estemos extrayendo una clave nomi-
nada, como en:
prefs.getlnt("Companions", O)) i
En el caso nonnal, lo que conviene es proporcionar un valor predetenninado razonable. De hecho, podemos ver una estruc-
tura bastante típica en las líneas:
De esta fonna , la prime ra vez que ejecutemos el programa, Us ageCount tendrá el valor cero, pero en las subsiguientes invo-
caciones será distinto de cero.
658 Piensa e n Java
Al ejecutar PreferencesDemo.java, podemos ver que, en efecto, UsageCount se increment a cada vez que se ejecu ta el pro-
grama. pero ¿dónde se almacenan los datos? No aparece ningún archi vo local después de ejecutar el programa por primera
vez. La API Preferences utiliza los recursos apropiados del sistema para llevar a cabo su tarea y estos recursos va riarán
dependiendo del sistema operativo. En Windows, se utili za el Reg istro (puesto que éste es ya de por sí una jerarquía de nodos
con parejas clave-valor). Pero lo importante es que la infom13ción se almacena de alguna manera mágica y transparente, de
fanna que no tenemos que preocupamos de c6mo funciona el meca nismo en un sistema o en otro.
Habría mucho más que comentar acerca de la API Preferences. por lo que puede consultar la documentación del IDK, que
resulta bastante co mprensible para obtener más detalles.
Ejercicio 33: (2) Escriba un programa que muestre el va lor actual de un directorio denominado "directorio base" y que
pida que introduzca mos un nuevo va lor. Utilice la API Preferences para almacenar el va lor.
Resumen
La biblioteca de flujos de datos de E/S de Java satisface los requisitos básicos. Podemos efectuar lecturas y escrituras a tra-
vés de la consola, un archivo. un bloque de memori a o incluso a través de Internet. Mediante el mecanismo de herencia
podemos crear nuevos tipos de objetos de entrada y de salida. E incluso podemos añadir un mecanismo simple de amp lia-
bilidad a los tipos de objetos que un flujo de datos puede aceptar, redefiniendo el método toString() que se invoca automá·
ticamente cada vez que pasarnos un objeto a un método que esté esperando un argumento de tipo String (la limitada
"conversión de tipos automática" de Java).
Existen diversas cuesti ones que la documentaci ón y el di seno de la biblioteca de flujos de E/S dejan sin resolve r. Por ejem-
plo, hubiera resultado muy conveniente que pudiéramos especificar que se generara una excepción cada vez que tratáramos
de sobreescribir un archivo a la hora de abrirlo para llevar a cabo una salida de datos. al gunos sistemas de programación
permiten especificar que queremos ab rir un archivo de salida, pero sólo si éste no existía anterionnente. En Java, parece que
debemos utili zar un objeto File para deternlinar si existe un archivo, porque si lo abrimos como FileOutputStream o
FileWriter, siempre será sobreescrito.
La biblioteca de fluj os de E/S tiene sus ventajas y sus inconvenientes; se encarga de reali zar una parte de la tarea y es una
biblioteca portable. Pero si no estamos familiarizados con el patrón de diseño Decorador, el di seno de la biblioteca no resul-
ta intuitivo, por 10 que existe una cierta curva de aprendizaje y también requiere más esfuerzo a la hora de explicar el fun-
cionamiento de la biblioteca a los que estén aprendiendo el lenguaje. Asimismo, se trata de una biblioteca incompleta; por
ejemplo, no tendríamos por qué tener necesidad de escribir utilidades como TextFile (la nueva utilidad Print\Vriter de Java
SES representa un paso en la solución cOfTecta, pero sólo se trata de una solución parcial). Se han efectuado grandes mejo-
ras en Java SE5, añadiendo por ejemplo los mecanismos de fom18teo de salida que siempre han estado soportados en prác-
ticamente todos los demás lenguajes.
Una vez que comprendamos el patrón de di seño Decorador y que comencemos a utili zar la biblioteca en aquellas situacio-
nes donde haga falta la flexibilidad que ésta proporciona, comenzaremos a sacar provecho de su di se ño, siendo esa ventaja
sufi ciente para compensar las líneas de código ad iciona les que se req ui eren para incorporar esa funcionalidad.
Puede encontrar las solucionc:> a los ejercicios selecc ionados en el documento electrón ico n,e Thinking ;11 Ja\'a Allllolllled So/míon G/lide, disponible para
la venta en wwu:¡\!indVj"w.nel.
Tipos enumerados
La palabra clave enum nos permite crear un nuevo tipo con un conjunto restringido de valores
nominados, y tratar dichos valores como componentes normales del programa. Esta
característica resulta ser enormemente útil. l
Las enumeraciones se han introducido brevemente al final del Capítulo 5, Inicialización y limpieza. Sin embargo, ahora que
comprendemos los ternas más avanzados de Java, podemos realizar un análisis más detallado de la fu ncionalidad de la enu-
meración incluida en Java SES. Como veremos, las enumeraciones nos pem1iten realizar cosas enomlemente interesantes,
aunque este capítulo también nos permitirá comprender mejor otras características del lenguaje que ya hemos presentado
antes, como los genéricos y el mecanismo de refl exión . También hablaremos de unos cuantos patrones de di seño adicio-
nales.
/* Output:
GROUND ordinal: O
-1 false false
class Shrubbery
GROUND
CRAWLING ordinal: 1
O true true
class Shrubbery
CRAWLING
HANGING ordinal: 2
1 false false
class Shrubbery
HANGING
HANGING
CRAWLING
GROUND
*//1,-
El método ordinal() genera un va lor int que indica el orden de declaración de cada instancia enum, comenzando en cero.
Siempre podemos comparar con seguridad instancias enurn utilizando =, y los métodos equals() y hashCodc() se crean
de manera automática y transparente. La clase Enum es de tipo Comparable, por lo que existe un método compareTo(),
que también es de tipo Serializable.
Si invocamos gctDeclaringClass() para una instancia enum, podemos averiguar cuál es la clase enum que se utiliza como
envoltorio.
El método name( l devuelve el nombre tal como está declarado, y esto es lo que devuel ve también el método loSlring( l.
El método va lueOf( l es un miembro estático de Enum y devuelve la instancia enum correspondiente al nombre (en fonna
de cadena de caracteres) que se le pase; si no se localiza ninguna correspondencia. se genera una excepción.
JI : enumeratedfBurrito.java
package enumerated;
import static enumerated.Spiciness.*¡
/* Output:
19 Tipos enumerados 661
Burrito is NOT
Burrito is MEDIUM
Burrito is HOT
* /1/,-
la importación es tática trae todos los identificadores de instancias enum al espacio de nombres local, así que no es nece-
sario cua lificarlos. ¿Se trata de una buena idea o sería mejor ser explícito y cualificar todas las instancias enum? La res-
puesta dependerá, probablemente, de nuestro código. El compi lador no nos pennitirá en ningún caso utilizar el tipo
incorreclO, por lo que 10 único que nos debe preocupar es si el código resultará confuso para el lector. En muchas situacio-
nes, probablemente resulte adecuado eliminar las cualificaciones, pero es algo que habrá que evaluar caso por caso.
Observe que no es posible utilizar esta técnica si la enumeración está definida en el mismo archivo O en el paquete prede-
tem'¡nado (al parecer, se produjeron algunas di scusiones dentro de Sun sob re si debía pennitir hacer esto).
/ * Output:
WEST: Miss Gulch, aka the Wicked Witch of the West
NORTH: Glinda, the Good Witch of the North
EAST: Wicked Witch of the East, wearer of the Ruby
Slippers, crushed by Dorothy's house
SOUTH: Good by inference, but missing
* ///,-
Observe que si vamos a definir métodos, tenemos que finalizar la secuencia de instancias enum con un carácter de punto y
coma. Asimismo, Java nos obliga a definir las instancias en primer lugar dentro de la enumerac ión. Si tratamos de definir-
las después de algunos de los métodos o campos, obtendremos un error en tiempo de compilación.
El constmctor y los métodos tienen la misma fo nna que las de las clases nonnales, porque se trata de lino clase normal,
sólo que con algunas restricciones. Así que podemos hacer prácticamente todo lo que queramos con las enumeraciones (si
bien lo más nonnal es que empleemos enumeraciones simples).
662 Piensa en Java
Aunque el constructor se ha definido en nuestro ejemplo como privado, no tiene demasiada importancia el tipo de acceso
que usemos: el constructor sólo puede utili zarse para crear las instancias enum que se declaren dentro de la definición de
la enumeración; el compilador no nos permitirá uti lizarlo para crear una nueva instancia una vez que esté completada la defi-
nición de la enumeración.
1* Output:
Scout
Cargo
Transport
Cruiser
Battleship
Mothership
*/ // ,-
El método toString( j obtiene el nombre de la instancia SpaceShip invocando name( j, y modificando el resultado de modo
que sólo la primera letra esté en mayúscula.
/ * Output:
The traffic light is RED
The traffic light is GREEN
The traffic light is YELLOW
The traffic light is RED
The traffic light is GREEN
The traffic light is YELLOW
The traffic light is RED
* ///,-
El compilador no se queja de que no haya ninguna instTucción defaul t dentro de la estructura switch, pero eso no se debe
a que detecte que hay instrucciones case para cada instancia Signal. Si desacti vamos una de las instrucciones case median-
te un comentario, el compilador seguirá sin quejarse. Esto quiere decir que es necesario tener cuidado y comprobar explíci-
tamente que todos los casos están cubiertos. Por otro lado, si estamos ejecutando la instrucción returo dentro de las
instrucciones case, el compilador si se queja,.á si no incluimos una opción default , incluso aunque hayamos cubierto todos
los valores de la enumeración.
Ejercicio 1 : (2) Utilice la importación estática para modificar Tra flicLight.java de modo que no haya que cualificar
las instancias enum.
El misterio de values( )
Como hemos indicado anterionnente, el compi lador se encarga de crear automáticamente todas las clases enum y esas cIa-
ses heredan de la clase E nu m. Sin embargo, si examinamos Enum, veremos que no hay ningún método values( ). a pesar
de que nosotros sí que lo hemos estado utili zando. ¿Existe algún otro método oculto? Podemos escribir un pequeño progra-
ma basado en el mecanismo de reflexión para averiguarlo:
11: enumerated/Reflection.java
/1 Análisis de enumeraciones utilizando el mecanismo de reflexión.
import java.lang.reflect .* ¡
import java . util.*¡
import net.mindview.util. * ¡
import static net.mindview . util.Print. *¡
1* Output:
----- Analyzing class Explore
Interfaces:
Base: class java . lang . Enurn
Me thods:
(compareTo, equals, ge t Class, getDeclaringClass, hashCode,
name, notify, notifyAll, ordinal, toString, valueOf, values, waitl
----- Analyzing class java . lang.Enum
Interfaces:
java.lang.Cornparable<E>
interface java . io.Seriali zable
Base: class java . lang.Object
Methods:
(compareTo, equals, getC l ass, getDeclaringClass, h a shCode,
name, notify, noti f yAll, ordinal, toString, valueOf, waitl
Explore.containsAII(Enurn)? true
Explore.removeAII(Enum ) ; [values]
Campiled frarn "Reflection. java"
final class Explore extends java.lang.Enurn{
public static final Explore HERE;
public static final Explore THERE;
public static final Explore[] values{) i
publ i c s t atic Explore valueOf (java.lang . String);
static {};
}
*/ // ,-
Así que la respuesta es que values( ) es un método estático añadido por el compilador. Debemos ver que también se aí'iade
a Explore el método valueOfO dentro del proceso de creación de la enumeración. Esto resulta un tanto confuso, porque
también existe un método valueOf() que forma parte de la clase Enum, pero dicho método tiene dos argumentos y el méto-
do aí'iadido sólo dispone de uno. Sin embargo, la utili zación del método Set sólo comprueba los nombres de los métodos y
no las signaturas, por lo que después de invocar Explore.removeAll(Enum), lo único que queda es Ivalue'l.
A la salida, podemos ver que Explore ha sido definido como final por el compilador, por lo que no podemos heredar de una
enwneración. También existe una cláusula de inicialización estática, la cual podemos redefInir como veremos más tarde.
Gracias al mecanismo de borrado de tipos (descrito en el Capitulo1 5, Genéricos) , el descompilador no dispone de inforrua-
ción completa acerca de EDum. por lo que muestra la clase base de Explore como una clase Enum simple, en lugar de
Enum<Explore>.
19 Tipos enumerados 665
Puesto que values( ) es un método estático insertado dentro de la definición de enum por el compilador, si generaliza mos
un tipo enum a Enum, el método values( ) no estará disponible. Observe, sin embargo, que existe un método
getE nurnCoDstants() en Class. por lo que incluso values() no fonna parle de la interfaz Enum, podernos seguir obtenien-
do las instancias enum a tra vés del objeto Class:
ji: enumerated/UpcastEnum . java
// No hay método value s () si generalizamos l a enumeración
1* Output:
HITHER
YON
, /// , -
Como getEnurnConstants() es un método C lass, podemos invocarlo para una clase que no tenga enumeraciones:
11 : e numerated/NonEnum . java
1* Output:
java.lang.NulIPointerException
,///>
Sin embargo, el métod o devuel ve null, así que se generará una excepción si tratamos de utili zar el resultado.
Implementa, no hereda
Ya hemos dicho que todas las enumeraciones amplían java.lang.Enurn. Puesto que Java no soporta la herencia múltiple,
esto quiere decir que no se puede crear una enumeración mediante herencia :
enum NotPossible extends Pet { . .. liNo f unciona
Sin embargo, sí es posible crear una enumeración que implemente una o más interfaces:
11 : enumerat e d/cartoon s /Enumlmpl e mentation . j a va
II Una enumeración pue de implementar una inter f az
packag e enumerated.cartoons;
import java.util. *;
import net . mindvie w. util. * ;
enum CartoonCharacter
imp lemen ts Gene r ator<Cart oonCharacter>
SLAPPY, S PANKY , PUNCHY, SILLY, SOUNCY, NUTTY, SOS;
666 Piensa en Java
/* Output:
B08, PUNCHY, B08, SPANKY, NUTTY, PUNCHY, SLAPPY, NUTTY,
NUTTY, SLAPPY,
* ///,-
El resultado es algo extraño, porque para llamar a un método es necesario tener una instancia de la enumeración para la
cual invocarlo. Sin embargo, cualquier método que admite un objeto Generator podrá ahora aceptar una instancia
CartoonCharacter; por ejemplo, printNext( J.
Ejercicio 2: (2) En lugar de implementar una interfaz, defina ncx t() como un método estático. ¿Cuáles son las venta-
jas y desventajas de esta sol ución?
Selección aleatoria
Muchos de los ejemplos de este capítulo requieren efectuar una selección aleatoria entre varias instancias cn um, como
vimos en Ca rtoo nC haractcr.next() . Es posible generalizar esta tarea utilizando genéricos e incluir el resultado en la biblio-
teca común:
11 : net/mindviewjutil/Enums.java
package net . mindview.ucil;
import java.util.*;
La extraña sintaxis <T cx tends E num <T» describe T como una instancia enum . Pasando Class<T>, hacemos que esté
disponible el objeto clase, pudiéndose así generar la matri z de instancia enum . El método random ( J sobrecargado sólo
necesita conocer que se le está pasando un objeto T I!' porque no necesita realizar operaciones de la clase Enum; sólo nece-
sita seleccionar aleatoriamente un elemento de una matriz. El tipo de retomo es el tipo exacto de la enumeración.
He aquí una prueba simple del método ra "dom( J:
/1 : enumerated /RandomTest.java
import net.mindview.util.*;
/ * Oueput:
STANDING FLYING RUNNING STANDING RUNNING STANDING LYING
DODGING SITTING RUNNING HOPPING HOPPING HOPPING RUNNING
STANDING LYING FALLING RUNNING FLYING LYING
* // / , -
Aunque Enum es una clase no demasiado compleja, ya veremos en este capítulo que pennite ahorrarse muchas duplicacio-
nes. Las dupl icaciones tienden a generar erro res, así que eliminar esas dupl icaciones es un objetivo bastante importante.
Puesto que el úni co mecanismo de subtipos disponible para una enumeración está basado en la implementación de interfa-
ce, cada enumeración anidada implementa la interfaz envoltorio Food. Ahora sí que podemos decir que "todo es un tipo de
Food", como podemos ver aquí :
11 : enumerated/menu / TypeOfFood.java
pac kage enumerated.menu¡
import static enumerated.menu . Food . *¡
System.out.println ( It- -- It l i
/ * Output :
SPRING ROLLS
VINDALOO
FRUIT
DECAF COFFEE
SOUP
VINDALOO
FRUIT
TEA
SALAD
BURRI TO
FRUIT
19 Tipos enumerados 669
TEA
SALAD
BURRITO
CREME CARAMEL
LATTE
SOUP
BURRITO
TIRAMISU
ESPRESSO
En este caso, la ventaja de crear una enumeración de enumeraciones es que con ello podemos iterar a través de cada objeto
Co urse. Posteriormente, en e l ejemplo VendingMachine.java, veremos otra técni ca de categorización qu e está basada en
un conjunto diferente de restricciones.
Otra solución más compacta al problema de la catego ri zación consiste en anidar enumerac iones dentro de otras enumera-
ciones, co mo en el ejemplo siguiente:
1/: enumerated/SecurityCategory.java
JI Una subcategorización más sucinta.
import net.mindview.util.*¡
enum SecurityCategory (
STOCK(Security.Stock.class), BOND(Security.Bond.class) i
Security[] values¡
SecurityCategory(Class<? extends Security> kind)
values = kind.getEnumConstants();
interface Security {
enum Stock implements Security { SHORT, LONG, MARGIN
enum Bond implements Security { MUNICIPAL, JUNK }
/* Output:
BOND, MUNICIPAL
BOND, MUNICIPAL
STOCK, MARGIN
STOCK , MARGIN
BOND, JUNK
STOCK, SHORT
STOCK , LONG
STOCK, LONG
BOND, MUNICIPAL
BOND, JUNK
* /// ,-
La interfaz Security es necesaria para recopi lar todas las enumeraciones dentro de un tipo común. Entonces, se rea liza la
categorizac ión dentro de SecurityCategory.
670 Piensa en Java
11: enumerated/menu/Mea12.java
package enumerated.menu;
import net.mindview.util. * ;
System.out.println(II---II) ;
1* Output:
[BATHROOM]
[STAIRl, STAIR2, BATHROOM, KITCHEN]
[LOBBY, OFFICEl, OFFICE2, OFFICE3, OFFICE4, BATHROOM, UTILITY]
[LOBBY, BATHROOM, UTILITY]
[STAIRl, STAIR2, OFFICEl, OFFICE2, OFFICE3, OFFICE4, KITCHEN]
*///,-
672 Piensa en Java
Se utili za un a cláusula de importación estática para simplificar el uso de las constantes enum . Los nombres de los métodos
son bastantes auto-explicativos, y puede encontrar detalles completos de los mismos en la documentación del JDK. Cuando
examine esta documenta ción, podrá ver un detalle interesa nte: el método of() ha sido sobrecargado tanto con varargs como
con métodos individuales que admiten entre dos y cinco argumentos explícitos. Esto es un indicio de la preocupac ión por
el rendimjcnt o que imperaba a la hora de diseñar la clase EnumSet, porque un único método of( ) usando varargs podría
haber resuelto el problema, pero es ligeramente menos eficiente que si se di spone de argumentos ex plícitos. Así, si invoca-
mos of() con entre dos y cinco argumentos se obtienen las llamadas a método explícitas (ligeramente más ráp idas), pero si
lo in voca mos con un argumento o con más cinco, se obtiene la versión varargs de of( ). Observe que, si lo invocamos con
un argumento, el compilador no construye la matriz varargs, así que no exis te ningún gasto de procesamiento adicional si
se invoca dicha vers ión con un úni co argumento.
Los conjuntos EnumSet se construyen a partir de va lores long, cada va lor long tiene 64 bits y cada instancia enum requi e-
re un bit para indicar la presencia o ausencia. Esto sign ifica que podemos tener un conjunto EnumSet para una enumera-
ción de hasta 64 elementos si n utili zar más que un único va lor long. ¿Qué sucede si tenemos más de 64 elementos en la
enumeración?
11: enumerated/BigEnumSet.java
import java.util .* ¡
/ * Output :
[AO, Al, A2, A3, M, AS, A6, A7, AB, A9, A1O, All, A12,
Al3, Al4, A1S, A16, A17, A1B, Al9, A20, A21, A22, A23, A24,
A25, A26, A27, A2a, A29, A30, A31, A32, A33, A34, A3S, A36,
A37, A3B, A39, MO, Ml, A42, M3, M4, MS, M6, M7, A4a,
M9, ASO, ASl, AS2, AS3, A54, ASS, AS6, AS?, A5B, A59, A60,
A6l, A62, A63, A64, A65, A66, A67, A6B, A69, A70, A71, A72,
A73, A74, A7SJ
* ///,-
La clase EnumSet, como podemos ver, no tiene ningún problema con las enumeraciones que tengan más de 64 elementos,
por lo que cabe presumir que se añade otro va lor long adicional cada vez que es necesario.
Ejercicio 7: (3) Localice el códi go fuente correspondi ente a EnumSet y explique cómo funciona.
Utilización de EnumMap
Un mapa EnurnMap es un tipo de mapa especializado que requiere que sus claves formen parte de la misma enumeración .
Debido a las restricciones impuestas en las enumeraciones. un mapa EnumMap puede impleme ntarse internament e co mo
una matri z. Por tanto, son extremadamente rápidos, así que podemos utili zar libremente los mapas EnumMap para búsque-
das basadas en enumeraciones.
Únicamente podemos in vocar el método put() para aquellas claves que fonnen palie de nu estra enum eraci ón, pero por lo
demás la utilización es similar a la de los mapas ordinarios.
He aquí un ejemplo que ilustra el uso del patrón de di seí'i.o basado en comandos. Este patrón comi enza con una interfaz que
contiene (nonnalmente) un único método y crea múltiples implementaciones de dicho método, cada una con un comporta-
miento distinto. Basta con instalar los objetos COl11mand y el programa se encargará de llamarlos cuando sea necesario:
19 Tipos enumerados 673
JI: enumerated/EnurnMaps.java
JI Fundamentos de los mapas EnumMap.
package enumerated¡
import java.util.*;
import static enumerated AlarmPoints.*¡
import static net.mindview.util.Print.*¡
/ * Output:
BATHROOM: Bathroom alert!
KITCHEN: Kitchen fire!
java.lang.NullPointerException
* ///,-
Al igual que con EnumSet. el orden de los elementos en EnurnMap está determinado por su orden de definición dentro de
la enumeración cnum.
La última parte de main() muestra que siempre hay una entrada de clave para cada una de las instancias de la enumeración,
pero el valor será null a menos que hayamos invocado put() para dicha clave.
Una ventaja de un mapa EnumMap con respecto a los métodos especificos de constallle (que se describen a continuación)
es que el mapa EnumMap pem1ite modificar los objetos que representan los va lores, mientras que, co mo ve remos, los
métodos específicos de constante se fijan en tiempo de compilación.
Como veremos posteriormente en el capítulo, los mapas EnumMaps pueden utilizarse para tareas de despacho múltiple en
aquellas situaciones en las que se disponga de múltiples tipos de enumeraciones que interaccionen entre sí.
String getInfo () {
return
DateFormat.getDatelnstance () . format (new Date ()) ;
),
CLASSPATH
String getlnfo ( )
return System. getenv ("CLASSPATH" ) ;
),
VERSION
String getlnfo ()
return System.getProperty ( "java.version" ) ;
)
);
abstraet String getlnfo () i
public static void main (String [] args ) {
for (ConstantSpecificMethod csm : va!ues ())
System.out.println (csm.getlnfo () ) ;
enum LikeClasses {
WINKEN { void behavior ( ) { print ("Behaviorl"); ) ),
BLINKEN { void behavior () { print ("Behavior2" ) ; ) ),
NOD { void behavior () { print ("Behavior3"); ) );
abstract void behavior( ) ;
En fl(), podemos ver que el compilador no permite utili za r una instancia enum corno un tipo de clase, lo que tiene bastan-
te sentido si co nsideramos que el código generado por el compilador: cada elemento enurn es una instancia de tipo sta tic
final de LikeClasscs.
19 Tipos enumerados 675
Asimismo, como 5011 estáticas, las instancias enum de las enumeraciones internas no se comportan como clases internas
normales, no podemos acceder a los campos o métodos no estáticos de la clase externa.
Veamos un ejemplo más interesante, en el que se intenta representar un sistema de lavado de coches. A cada cliente se le da
un menú de opciones para su lavado y cada opción lleva a cabo una acción diferente. Podemos asociar cada opción con un
método especifico de constante y emplear un conjunto EnumSet para almacenar las selecciones del cliente:
ji: enumeratedjCarWash.java
import java.util.*¡
import static net.mindview util.Print.*¡
1* Output:
[BASIC, RINSE]
The basic wash
Rinsing
676 Piensa en Java
1* Output:
NUT: default behavior
BOLT: default behavior
WASHER: Overridden method
*///,-
Aunque las enumeraciones impiden utilizar ciertos tipos de estructuras sintácticas, en general lo que deberá hacer es expe-
rimentar con ellas como si tratara de clases normales.
Dentro de Maj), vemos el método randornMail( ), que crea ej emplos aleatori os de envios postales de prueba. El método
generator( ) produce un obj eto Iterable que utili za randomMail( ) para generar una serie de objetos que representan en-
víos postales, generán dose un objeto cada vez que se invoca next( ) a través del iterador. Esta estructura penn ite crear de
manera sencilla un bucl e foreach invocando Mail.generator( ):
JI: enumeratedjPostOffice.java
// Modelado de una oficina de correos .
import java.util.*;
import net.mindview.util. * ;
import static net.mindview.util.Print. * ;
class Mail {
JI Los valores NO disminuyen la probabilidad de la selección aleatoria:
enum GeneralDelivery {YES,NOl,N02,N03,N04,NOS}
enum Scannability {UNSCANNABLE,YESl,YES2,YES3,YES4}
enum Readability {ILLEGIBLE,YES1,YES2,YES3,YES4}
enum Address {INCORRECT,OK1,OK2,OK3,OK4,OKS,OK6}
enum ReturnAddress {MISSING,OK1,OK2,OK3,OK4,OKS}
GeneralDelivery generalDelivery¡
Scannability scannability;
Readability readability;
Address address ¡
ReturnAddress returnAddress¡
static long counter = O¡
long id = counter++¡
public String toString{) return "Mail 11 + id¡ }
public String details () {
return toString() +
General Delivery: 11 + generalDelivery +
Address Scanability: " + scannability +
Address Readability: " + readability +
Address Address: " + address +
Return address : 11 + returnAddress ¡
}
II Generar objeto Mail de prueba:
public static Mail randomMail () {
Mail m = new Mail{)¡
m.generalDelivery= Enums.random{GeneralDelivery.class) ¡
m.scannability = Enums.random(Scannability.class);
m.readability = Enums.random{Readability.class) ¡
m.address = Enums.random(Address.class);
m.returnAddress = Enums.random(ReturnAddress . class);
return m¡
},
MACHINE _ SCAN {
boolean handle (Mail m) {
switch (m. scannability)
case UNSCANNABLE: return false;
default:
switch(m.address)
case INCORRECT: return false¡
default:
print("Delivering "+ ro +11automatically") ¡
return true;
}
},
VISUAL_INSPECTION {
boolean handle (Mail m) {
switch(m.readability) {
case ILLEGIBLE: return false:
default :
switch(m.address)
case INCORRECT: return false¡
default:
print("Delivering 11 + m + 11 normally"):
return true;
},
RETURN_TO_SENDER (
boolean handle(Mail m)
switch (m. returnAddress)
case MISSING: return false;
default:
print ("Returning + m + " to sender");
return true;
}
};
abstraet boolean handle(Mail m};
/ * Output:
Mail O, General Delivery : N02, Address Scanability:
UNSCANNABLE, Address Readability: YES3, Address Address: OK1,
Return address: OK1
Delivering Mail O normally
*.*.*
Mail 1, General Delivery : N05, Address Scanability: VES),
Address Readability: ILLEGIBLE, Address Address: OK5,
Return address: OK1
Delivering Mail 1 automatically
. *• • •
Mail 2, General Delivery: VES, Address Scanability: YES3,
Address Readability: YES1, Address Address: OK1,
Return address: OK5
.*...
Using general delivery for Mail 2
La Cadena de responsabilidad se expresa en la enumeración enum MailHandler, y el orden de las definiciones enurn deter-
mina el orden en el que se intentarán aplicar las diferentes estrategias para cada envío postal. Se intenta aplicar cada una de
las estrategias por turnos hasta que una de ellas tiene éxito, o todas ellas fallan, en cuyo caso tendremos un envío postal que
no podrá ser entregado.
Ejercicio 8: (6) Modifique PostOffiee.java para incluir la capacidad de reenviar correo.
Ejercicio 9: (5) Modifique la clase PostOffiee para que utilice un mapa EnurnMap.
Proyecto: 2 Los lenguajes especializados como Prolog utilizan el encadenamiento inverso para resolver problemas
como éste. Utilizando PostOffice.java como base, haga una investigación acerca de dichos lenguajes y
desarrolle un programa que permita añadir fácilmente nuevas "reglas" al sistema.
} .
STOP { II Esta debe ser la última instancia.
public int amount () { 1I No permitir
throw new RuntimeException ( "SHUT_ DOWN. amount () " ) ;
}
};
int value; II En centavos
Input ( int value ) { this. value value; }
Input () {}
int amount ( ) { return value; } ; II En centavos
static Random rand ~ new Random (47 ) ;
public static Input randomSelection ()
II No incluir STOP:
return values () [rand.nextInt (values () .length - 1 ) ];
2 Los proyectos son sugerencias que pueden utilizarse, por ejemplo, como proyectos de fin de curso. Las soluciones a los proyeclOs no se incluyen en la
Gllía de solllciolle:)'.
19 Tipos enumerados 681
Observe que dos de las entradas input tienen una cantidad asociada, así que definimos el método amount() para represen-
tar la cantidad dentro de la interfaz. Sin embargo, resulta inapropiado para los otros dos tipos de Input, así que se generará
una excepción si invocamos ese método. Aunque se trata de un diseno un poco extraño (definir un método en una interfaz
y luego generar una excepción si se lo invoca para ciertas implementaciones), nos vemos obligados a utilizarlo debido a las
restricciones de la enumeraciones.
El objeto VendingMachine (máquina expendedora) reaccionará a estas entradas categorizándolas primero mediante la enu-
meración Category, para poder conmutar mediante switch entre las diferentes categorías. Este ejemplo muestra cómo las
enumeraciones consiguen que el código sea más claro y más fácil de gestionar:
11: enumerated/VendingMachine.java
II {Args: VendingMachineInput.txt}
package enumerated¡
import java.util.*;
import net.mindview.util.*;
import static enumerated.Input.*;
import static net . mindview . util.Print.*;
enum Category
MONEY(NICKEL, DIME, QUARTER, DOLLAR),
ITEM_SELECTION(TOOTHPASTE, CHIPS, SODA, SOAP) ,
QUIT_TRANSACTION(ABORT_TRANSACTION) ,
SHUT_DOWN(STOP) ;
private Input[] values¡
Category (Input. .. types) { values = types; }
private static EnumMap<Input/Categ ory~ categories
new EnumMap<Input,Category~{Input.class} ¡
static {
for(Category c : Category.class.getEnumConstants())
for(Input type : c.values}
categories.put(type, cl;
).
ADDING_MONEY {
void next (Input input) {
switch{Category.categorize(input))
682 Piensa en Java
case MONEY:
amount += input.amount();
break;
case ITEM SELECTION:
selection = input;
if(amount < selection.amount())
print ("Insufficient money for " + selection);
el se state = DISPENSING;
break;
case QUIT_TRANSACTION:
state = GIVING_CHANGE;
break;
case SHUT DOWN:
state = TERMINAL;
default:
},
DISPENSING(StateDuration.TRANSIENT)
void next () {
print ("here is your " + selection);
amount -= selection.amount{) i
state = GIVING_CHANGE;
}
},
GIVING CHANGE{StateDuration.TRANSIENT)
void next() {
if{amount :> O) {
print ("Your change: " + amount);
amount = Oi
},
TERMINAL { void output 11 { print ("Halted"); } };
private boolean isTransient = false¡
State() {}
State (StateDuration trans) { isTransient = true;
void next (Input input) {
throw new RuntimeException ("Only call " +
"next(Input input) for non-transient states"};
void next ()
throw new RuntimeException ("Only cal! next () for n +
"StateDuration.TRANSIENT states") i
/* Output:
25
50
75
here i5 your CHIPS
O
100
200
here i5 your TOOTHPASTE
O
25
35
Your change: 35
O
25
35
Insufficient money for SODA
35
60
70
75
Insufficient money for SODA
75
Your change: 75
O
Halted
*///,-
Puesto que la selección entre las distintas instancias enum se suele realizar con una instrucción a switch (observe el esfuer-
zo adicional que ha hecho el lenguaje para que se pueda aplicar fácilmente una instrucción nvitch a las enumeraciones), una
de las cuestiones más comunes que podemos preguntamos a la hora de organizar múltiples enumeraciones es: "¿Qué es lo
que quiero utilizar para la instrucción switch?" Aquí, lo más fácil es proceder en sentido inverso a partir del objeto
VendingI\1achine observando que en cada estado Sta te, necesitamos conmutar con switch según las categorías básicas de
acciones de entrada: si se ha insertado dinero, si se ha seleccionado un elemento, si se ha abortado la transacción y si se ha
apagado la máquina. Sin embargo, dentro de estas categorías tenemos diferentes tipos de monedas que pueden insertarse y
diferentes alimentos que se pueden seleccionar. La enumeración Category agrupa los diferentes tipos de objetos Input de
modo que el método categorize( ) puede producir el elemento apropiado Category dentro de una instrucción switch. Este
método utiliza un mapa EnumMap para realizar las búsquedas de manera eficiente y segura.
684 Piensa en Java
Si estudiamos la clase VendingMachine, podemos ver cómo cada estado es diferente y responde de fanna distinta a las
entradas. Observe también los dos estados transitorios. En run() la máquina espera una entrada [nput y no deja de pasar a
través de los estados hasta que deja de estar en un estado transitorio.
El sistema VendingMachine puede probarse de dos formas, utilizando dos objetos Generator diferentes. El objeto
RandomlnputGenerator simplemente genera de forma continua una serie de entradas, exceptuando SHUT_DOWN que
hace que se pare la máquina. Ejecutando este procedimiento durante un tiempo lo suficientemente largo, podemos compro-
bar que todo está en orden y verifica r que la máquina no entrará en un estado incorrecto. El objeto FUeInputGenerator
toma un archivo que describe las entradas en fanna textual , las convierte en instancias enum y crea objetos Input. He aquí
un archivo de texto utilizado para producir la salida mostrada en el ejemplo:
jj :! enumerated jVendingMachinelnput.txt
QUARTER; QUARTER; QUARTER; CHIPS;
DOLLAR; DOLLAR; TOOTHPASTE;
QUARTER; DIME; ABORT_TRANSACTION;
QUARTER; DIME; SODA ;
QUARTER; DIME; NICKEL; SODA;
ABORT_TRANSACTION;
STOP;
/// ,-
Una limitación de este diseño es que los campos de VendingMachine a los que acceden las instancias de la enumeración
enurn deben ser estáticos, lo que significa que sólo podemos tener una única instancia VendingMachine. Esto no tiene por
qué ser un problema si pensamos en tilla implementación real (Java embebido), ya que lo nonnal es que sólo tenga mos una
aplicación por cada máquina.
Ejercicio 10: (7) Modifique la clase VendingMachine (únicamente) utilizando EnumMap de modo que un programa
pueda disponer de múltiples instancias de VendingMachine.
Ejercicio 11: (7) En una máquina expendedora real, conviene poder añadir y modificar fáci lmente el tipo de elementos
expedidos, porque los límites impuestos a Input por una enumeración resultan poco prácticos (recuerde
que las enumeraciones son para un conjunto restringido de tipos). Modifique VendingMachine.java para
que los elementos expedidos estén representados por una clase en lugar de ser parte de Input e inicialice
un contenedor ArrayList de estos objetos a partir de un archivo de texto (utilizando net.mindview.util.
TextFile).
Proyecto: 3 Diseñe la maquina expendedora utilizando funcionalidades de intemacionalización, de tal modo que una
máquina pueda fácilmente se r adoptada en todos los países.
Despacho múltiple
Cuando estamos tratando con múltiples tipos que interactúan entre sí, los programas pueden llegar a ser especialmente com-
plejos. Por ejemplo, consideremos un sistema que analice sintácticamente y luego ejecute expresiones matemáticas. Nos
interesaría poder decir Number.plus(Number), Number.multiply(Number), etc., donde Number es la clase base de una
familia de objetos numéricos. Pero cuando decimos a.plus(b), y no conocemos el tipo de exacto de a o b, ¿cómo podemos
hacer que interactúen apropiadamente?
La respuesta comienza con algo en lo que probablemente no haya pensado basta el momento: Java únicamente realiza lo
que se denomina despacho simple. En otras palabras, si estamos revisando una operación sob re más de un objeto cuyos tipos
sean desconocidos, Java sólo puede invocar el mecanismo de acoplamiento dinámico para uno de esos tipos. Esto no resuel-
ve el problema descrito aquÍ, por lo que tenninamos detectando unos tipos manualmente e implementado, en la práctica,
nuestro propio comportamiento de acoplamiento dinámico.
La solución se denomina despacho lIIúltiple (en este caso, sólo hay dos despachos, por lo que el mocanismo se denomina
despacho doble.). El polimorfismo sólo puede tener lugar desde llamadas a métodos, por lo que si queremos realizar un des-
pacho doble, deben existir dos llamadas a métodos: la primera para detenninar el primer tipo desconocido y la segunda para
) Los proyectos son sugerencias que pueden utilizarse, por ejemplo, como proyectos de fin de curso. Las soluciones a los proyectos no se incluyen en la
GlIía de soluciones.
19 Tipos enumerados 685
determinar el segundo tipo desconocido. Con el despacho múltiple, debemos tener una llamada virtual para cada uno de los
tipos: estamos trabajando con dos j erarquías de tipos diferentes que están interactuando, necesitaremos una llamada virtual
en cada jerarquía. Generalmente, lo que hacemos es establecer una configuración tal que una única llamada a método pro-
duzca una llamada a método vi rtual , pernlitiendo detenninar así más de un tipo a lo largo del proceso. Para obtener este
efecto, necesitamos trabajar con más de un método. Hará falta una llamada a método para cada operación de despacho. Los
métodos del siguiente ejemplo (que implementa el juego "piedra, papel, tijera") se denominan compete() y eval() y ambos
son miembros de un mismo tipo. Estos métodos producen uno de tres posibles resultados: 4
jj : enumeratedjOutcame.java
package enumerated¡
public enum Out come ( WIN, LOSE, DRAW ) ///,-
interface Item {
Outcome compete(Item it);
Outcome eval (Paper p) ¡
Outcome eval(Scissors s) ¡
Outcome eval(Rack r)¡
" Este ejemplo ha estado utilizándose desde hace muchos años tanto en C++- como en Java (en Thinking ¡n Parterns) en www.MindVie"Wel. antes de apa 4
/* Output:
Rack VS. Rack: ORAW
Paper VS . Rack: WIN
Paper VS. Rack: WIN
Paper VS. Rack: WIN
Scissors VE. Paper: WIN
Scissors VS. Scissors: DRAW
Scissors VS. paper: WIN
Rack vs. Paper: LOSE
Paper VS. Paper: DRAW
Rack VE. Paper: LOSE
Paper VS. Scissors: LOSE
Paper VS. Scissors: LOSE
Rack VS. Scissors: WIN
Rack vs. Paper: LOSE
Paper VS. Rack: WIN
Scissors vs. paper: WIN
Paper vs. Scissors: LOSE
Paper vs. Scissors: LOSE
Paper vs. Scissors: LOSE
Paper vs. Scissors: LOSE
* ///,-
lIem es la interfaz para los tipos con los que se va a realizar el despacho múltiple. RoS hamBol.match( ) toma dos objetos
h em e inicia el proceso de doble despacho llamando a la [unción hem.compete(). El mecanismo virtual detennina el tipo
de a , por lo que se activa dentro de la función co mpete() correspondiente al tipo concreto de a. La función compele() rea-
liza el segundo despacho llamando a eval( ) con el tipo restante. Pasándose a si mismo (Ihis) como un argumento a cval( )
se genera una llamada a la función eval() sobrecargada, preservándose así la infonnación de tipo correspondiente al primer
despacho. Al completarse el segundo despacho, conocemos el tipo exacto de ambos objetos h e m.
Preparar el mecanismo de despacho múltiple requiere de un montón de ceremonia, pero recuerde que la ventaja que se obtie-
ne es la elegancia sintáctica que se consigue al realizar la Harnada: en lugar de escribir un código muy complicado para deter-
minar el tipo de uno o más objetos durante una llamada, simplemente decirnos: "¡Vosotros dos, no me importa de qué tipo
sois, pero interactuar apropiadamente entre vosotros!", Sin embargo, asegúrese de que este tipo de elegancia sea importan-
te para usted antes de embarcarse en programas que impliquen un despacho múltiple.
/* Output:
ROCK V$. RQCK: DRAW
SCISSORS vs. ROCK: LOSE
SCISSORS vs. ROCK, LOSE
SCISSORS vs. ROCK, LOSE
PAPER vs. SCISSORS, LOSE
PAPER vs. PAPER: DRAW
PAPER vs. SCISSORS: LOSE
ROCK V$. SCISSORS: WIN
SCISSORS vs. SCISSORS, DRAW
ROCK vs. SCISSORS: WIN
SCISSORS vs. PAPER, WIN
SCISSORS vs. PAPER: WIN
ROCK vs. PAPER: LOSE
ROCK vs. SCISSORS, WIN
SCISSORS vs. ROCK, LOSE
PAPER vs. SCISSORS: LOSE
SCISSORS vs. PAPER, WIN
SCISSORS vs. PAPER: WIN
SCISSORS vs. PAPER: WIN
SCISSORS vs. PAPER, WIN
*///,-
Una vez que se han detenninado ambos tipos en compete(), la única acción consiste en devolver el resultado con Outcome.
Sin embargo. también podríamos invocar otro método. incluso (por ejemplo, a través de un objeto Comando que hubiera
sido asignado en el constructor.
RoShamBo2.java es más pequeño y más sencillo que el ejemplo original por lo que también resulta más fácil de controlar.
Observe que estamos todavía utilizando dos despachos para determinar el tipo de ambos objetos. En RoShamBo1.java,
ambos despachos se realizaban mediante llamadas virtuales a métodos pero aquí, sólo se utiliza una llamada virtual a méto-
do en el primer despacho. El segundo despacho emplea una instrucción switch, pero esta solución es perfectamente válida
porque la enumeración limita las opciones disponibles en la instrucción switch.
El código relativo a la enumeración ha sido separado para que pueda utilizarse en otros ejemplos. En primer lugar, la inter-
faz Competitor define un tipo que compite con otro Competitor:
688 Piensa en Java
JI: enumeratedjCompetitor.java
JI Conmutación de una enumeración basándose en otra.
package enumerated;
),
SCISSORS
public Out come compete (RoShamBo3 it) {
switch{it} (
default:
case PAPER: return WI N¡
case SCISSORS: return DRAW¡
case ROCK: return LOSE;
)
).
ROCK
public Outcome compete (RoS hamBo3 it) {
switch (it)
default :
case PAPER: return LOSE;
case SCISSORS: return WIN¡
case ROCK: return DRAW¡
)
);
public abstraet Outcome compete(RoShamBo3 it);
public static void main(String[] args) {
RoShamBo.play(RoShamBo3.class, 20);
).
SCISSORS
public Out come compete(RoShamBo4 opponent)
return compete {PAPER, opponent) i
),
PAPER
public Out come compete(RoShamBo4 opponent)
return compete {ROCK, opponent) i
)
);
Out come compete{RoShamBo4 loser, RoShamBo4 opponent)
return ({opponent == this) ? Outcome.DRAW
{(opponent == loser) ? Outcome.WIN
Outcome.LOSE)) i
Aquí, el segundo despacho es realizado por la versión de dos argumentos de compete( ), que realiza una secuencia de com-
paraciones y es, por tanto, similar a la acción de una instrucción switch. Este ejemplo es más pequeño, pero resulta un poco
confuso. Para un sistema de mayor envergadura, esta confusión puede ser una desventaja.
El mapa EnurnMap se inicializa mediante una cláusula static; podemos ver la estructura con forma de tabla de llamadas a
initRow( ). Observe el método compete( ), donde puede verse que ambos despachos tienen lugar en una única instrucción.
La tabla table tiene exactamente el mismo orden que las llamadas a initRow() en el ejemplo anterior.
El tamaño pequeño de este código resulta muy atractivo si lo comparamos con los ejemplos anteriores. en parte porque pare-
ce mucho más fácil de entender y de modificar pero también porque parece una solución mucho más directa. Sin embargo,
no es una solución tan "segura" como los ejemplos anteriores, porque utiliza una matriz. Con Wla matriz de mayor tamaño,
podríamos obtener el tamaño inapropiado y, si las pruebas no cubrieran todas las posibilidades, algún error podría terminar
por deslizarse.
Todas estas soluciones representan diferentes tipos de tablas, pero merece la pena explorar la fomla de expresar cada una
de las tablas para localizar la que mejor se ajuste a nuestras necesidades. Observe que, aunque la solución anterior es la más
compacta, también resulta bastante rígida, porque sólo permite producir una salida constante a partir de unas entradas cons-
tantes. Sin embargo, no hay nada que nos impida tener una tabla que genera un objeto de función. Para ciertos tipos de pro-
blemas, el concepto de "código conducido por tablas" puede ser muy potente.
Resumen
Aún cuando los tipos enumerados no son terriblemente complejos por si mismos, hemos pospuesto este capíllllo hasta este
momento debido a que queríamos analizar lo que se puede bacer con las enumeraciones al combinarlas con características
tales corno el polimorfismo, los genéricos y el mecanismo de reflexión.
Aunque son significativamente más sofisticadas que las enumeraciones de e o e++, las enumeraciones Java siguen siendo
una característica menor, algo sin lo que el lenguaje ha sobrevivido (aunque a costa de una gran complejidad) durante
muchos años. A pesar de elJo, este capítulo muestra el valor que una característica "menor" puede tener. En ocasiones nos
proporciona el mecanismo adecuado para resolver un problema de manera elegante y clara y, corno hemos dicho en di ver-
sas ocasiones a lo largo del libro, la elegancia es importante y la claridad puede marcar la diferencia entre una solución ade-
cuada y otra que fracasa porque las demás personas son incapaces de entenderla,
Hablando de claridad, una fuente muy lamentable de confusión proviene de la mala decisión que se tomó en Java LO, con-
sistente en emplear el témlino "enumeration" en lugar del ténnino más común y ampliamente aceptado de " iterator" para
referirse a un objeto que selecciona cada elemento de una secuencia (como hemos mencionado al hablar de las colecciones).
Algunos lenguajes hacen incluso referencias a los tipos de datos enumerados utilizando la palabra '"enumerators". Este error
se ha rectificado desde entonces en Java, pero la interfaz Enumeration no pudo, por supuesto, eliminarse de manera direc-
ta, por lo que sigue estando presente en el código antiguo (¡ya veces en el código nuevo!), en la biblioteca y en la docu-
mentación.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico TITe TITinking il! Java AflIlO/ated Solllrioll GlIide, di sponible para
la venta en \\·w\\'.lIIindViell'.l/el.
Anotaciones
Las anotaciones (también conocidas como melada/os) proporcionan una manera formalizada de
añadir información a nuestro código que nos permita utilizar fácilmente dichos datos en algún
momento posterior. 1
Las anotaciones están en parte motivadas por la tendencia general existente hacia combinar metadatos con los archivos de
código fuente en lugar de mantenerlos en documentos externos. También son una respuesta a las presiones provenientes de
otros lenguajes como C# en el sentido de añadir más características al lenguaje.
Las anotaciones son uno de los cambios fundamentales del lenguaje introducidos en Java SES. Proporcionan infonnación
que hace falta para describir completamente el programa, pero que no puede expresarse en Java. De este modo, las anota-
ciones nos permiten almacenar infaonación adicional en un fonnata que es probado y verificado por el compilador. Pueden
utilizarse anotaciones para generar archivos descriptores o incluso nuevas definiciones de clases y para ayudar a facilitar la
tarea de escribir plantillas de código. Utilizando anotaciones podemos mantener estos metadatos en el código fuente Java y
conseguir con ello un código de aspecto más limpio, un mecanismo de comprobación de tipos de compilación y la API ano-
taciones como ayuda para construir herramientas de procesamiento de las anotaciones. Aunque hay unos cuantos tipos de
rnetadatos predefinidos en Java SE5, en general, el tipo de anotaciones que se añadan y lo que hagamos con ellas son res-
ponsabilidad completamente nuestra.
La sintaxis de las anotaciones es razonablemente simple y consiste principalmente en la adición del símbolo @ al lengua-
je. Java SE5 contiene tres anotaciones predefinidas de propósito general, que están definidas en java.lang:
• @Override, para indicar que la definición de un método pretende sustituir otro método de la clase base. Esta ano-
tación genera un error de compilación si se escribe mal accidentalmente el nombre del método o si se proporcio-
na una signatura incorrecta. 2
• @Deprecated, para que se genere una advertencia del compilador si se utiliza este elemento.
• @SuppressWarnings, para desactivar las advertencias de compilador inapropiadas. Esta anotación está penniti-
da, pero no soportada como en las versiones primeras de Java SE5 (la anotación era ignorada).
Otros cuatro tipos adicionales de anotación soportan la creación de nuevas anotaciones, aprenderemos acerca de estos tipos
en este capítulo.
Cada vez que creemos clases descriptoras o interfaces que impliquen una tarea repetitiva, normalmente utilizaremos anota-
ciones para automatizar y simplificar el proceso. Buena parte del trabajo adicional en Enterprise JavaBeans (Effi), por
ejemplo, se elimina mediante el uso de anotaciones en Effi3.0.
Las anotaciones penniten sustituir sistemas existentes como XDoclet, que es una herramienta independiente para docIet
(consulte el suplemento contenido en http://MindView.net/Books/BetterJava) diseñado específicamente para crear doclets
1 Jeremy Meyer tuvo la gentileza de acudir a Crested Bulte y pasar allí dos semanas trabajando conmigo en este capítulo. Su ayuda ha sido extremadamen-
te valiosa.
2 EslO está, sin ninguna duda, inspirado en atTa característica similar disponible en C#. La característica de C# es una palabra clave y no una anotación, y
está impuesta por el compilador. En otras palabras, si se sobreescribe un método en C#, es necesario utilizar la palabra clave override. mientras que en
Java la anotación @Override es opcional.
694 Piensa en Java
con estilo de anotación. Por contraste. las anotaciones son verdaderas estructuras del lenguaj e y están, por tanto, estructura-
das, comprobándose sus tipos en tiempo de compilación. Al mantener toda la información en el propio código fuente y no
en comentarios, el código resulta más limpio y fácil de mantener. Utilizando y ampliando la API y las herramientas de ano-
taciones, o empleando bibliotecas ex temas de manipulación del código intennedio como veremos en este capítulo, podemos
realizar potentes tareas de inspección y manipulación del código fuente y del código intennedio.
Sintaxis básica
En el ejemplo sigu iente. el método testExecute( ) está anotado con @Test. Esto no hace nada por sí mismo pero el compi-
lador comprobará que existe una definición de la anotación @Test en la ruta de construcción del programa. Como ve remos
posterioffilellte en el capítulo, podemos crear una herramienta que ejecute este método por nosotros a través del mecanismo
de reflexión.
11 : annotations/Testable.java
package annotations¡
import net.mindview.atunit.*¡
///>
Los métodos anotados no difieren de los demás métodos. La anotación @Test de este ejemplo puede utilizarse en combi-
nación con cualquiera de los modificadores como public, static o void. Sintácticamente, las anotaciones se utilizan de fonna
similar a los modificadores.
Definición de anotaciones
He aquí la definición de la anotación anterior. Podemos ver que las definiciones de anotaciones se parecen bastante a las
definiciones de interfaz. De hecho, se compilan en archivos de clase, al igual que cualquier otra interfaz Java:
11: net/mindview/atunit/Test.java
1/ El marcador @Tes t.
package net.mindview.atunit¡
import java.lang.annotation.*¡
@Target(ElementType .METHOD )
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {} ///,-
Aparte del símbolo @, la definición de @Test se parece bastante a la de Wla interfaz vaCÍa. La definición de una anotación
también requiere las lIle/a-ano/aciones@Targety @Retention. @Targetdefinedónde se puede aplicar esta anotación (un
mélodo o un campo)@ Retentiondefinesi las anolaciones eSlarán disponibles en el código fuenle (SOURCE), en los archi-
vos de clase (CLASS), o en liempo de ejecución (RUNTlME).
Las anotaciones contendrán usualmente elementos para especificar valores para las anotaciones. Un programa o una herra-
mienta pueden utilizar estos parámetros para procesar las anotaciones. Los elementos se asemejan a los métodos de una
interfaz, excepto porque no se pueden declarar valores predetenninados.
Una anotación sin ningún elemento, como la anotación @Test anterior, se denomina anotación marcadora.
He aquí una anotación simple que controla los casos de uso en un proyecto. Los programadores anotan cada método o con-
junto de métodos que satisfacen los requisitos de un caso de uso concreto. El jefe de proyecto puede hacerse una idea del
progreso del proyecto contando los casos de uso implemen tados y los desarrolladores encargados de mantener el proyec~
to pueden encontrar fácilmente los casos de uso si necesitan actualizar O depurar las regla s de negocio utilizadas en el sis-
tema.
20 Anotaciones 695
/1: annotations/UseCase.java
import java.lang.annotation.*¡
@Target{ElementType.METHOD)
@Retention{RetentionPolicy .RUNTIME )
public @interface UseCase {
public int id();
public String description() default "no description"¡
!/ 1,-
Observe que id y description se asemejan a declaraciones de métodos. Puesto que el tipo de id es comprobado por el com-
pilador, se trata de una forma fiable de enlazar una base de datos de control con el documento de casos de uso y el código
fuente. El elemento description tiene un va lor predetenninado que es seleccionado por e l procesador de anotaciones si no
se especifica ningún va lor en el momento de anotar un método.
He aquí una clase con tres métodos anotados como casos de uso del programa:
// : annotations/PasswordUtils.java
import java.util.*;
@UseCase(id = 48}
public String encryptPassword(String passwordl {
return new StringBuilder (password) . reverse () . toString () ;
Los valores de los elementos de anotación se expresan como pares nombre-valor encerrados entre paréntesis después de la
declaración @:UseCase. A la anotación para encryptPassword() no se le pasa un valor en el ejemplo para el elemento des-
cription, por lo que cuando se procese la clase con un procesador de anotac iones aparecerá el va lor predeterminado defmi-
do en @i nterface UseCase.
Podemos imaginarnos fácilmente cómo podría emplearse un sistema C0l110 éste para "esbozar" un programa y luego relle-
nar la funciona lidad a medida que vamos completando el diseno.
Meta-anotaciones
Actualmente sólo hay tres anotaciones estándar (descritas anterionnente) y cuatro meta-anotaciones definidas en el lengua-
je Java. Las meta-anotaciones se utilizan para anotar anotaciones:
«<Target Dónde puede aplicarse esta anotación. Los posibles argumentos de EJementType son:
CONSTRUCTOR: declaración de constmctor
rIELD : declaración de campo (incluye constantes enum)
LOCAL_VARIABLE : declaración de variable local
METHOD: declaración de método
PACKAGE: declaración de paquete
PARAMETER: declaración de parámetro
TYPE: clase, interfaz (incluyendo tipo de anotación), o declaración enum
~--------~------
696 Piensa en Java
@Retention Durante cuánto tiempo se mantiene la información de anotación. Los posibles argumen-
tos de RetentionPolicy son:
SOURCE: el compilador descarta las anotaciones.
CLASS: las anotaciones están disponibles en los archivos de clases introducidos por el
compilador, pero pueden ser descartados por la máquina virtual.
RUNTll\1E: las anotaciones son retenidas por la máquina virtual en tiempo de ejecución,
por lo que se pueden leer mediante el mecanismo de reflexión.
La mayor parte del tiempo lo que haremos es definir nuestras propias anotaciones y escribir nuestros procesadores para tra-
tar con ellas.
11: annotations/usecaseTracker.java
import java .lang.reflect.*¡
import java.util.*;
1* Output:
Found Use Case:47 Passwords must contain at least one numeric
Found Use Case:48 no description
Found Use Case:49 New passwords can't equal previou sly used ones
Warning: Missing use case-50
* /// ,-
20 Anotaciones 697
Este ejemplo utiliza tanto el método de reflexión getDec1aredMethods() como el método geIAnnolalion(), que proviene
de la interfaz AnnolaledElement (clases como Class, Melhod y Field implementan esta interfaz). Este método devuelve
el objeto anotación del tipo especificado, que en este caso es "UseCase". Si no hay anotaciones de ese tipo concreto en el
método anotado, se devuelve un valor nulL Los valores de los elementos se extraen invocando ¡d( ) Y description( ).
Recuerde que no hemos especificado ninguna descripción en la anotación para el método encryptPassword(), por lo que
el procesador anterior localiza el valor predeterminado " no description" al invocar el método description() para esa ano-
tación concreta.
Elementos de anotación
El marcador @UseCase definido en UseCase.java contiene el elemento id de tipo int y el elemento description de tipo
String. He aquí una lista de los tipos pennitidos para los elementos de anotación:
• Todas las primitivas (int, 110al, boolean etc.)
• Slring
• Class
• enum
• Annotation
• Matrices de cualquiera de los tipos anteriores.
El compilador generará un error si se intenta emplear cualquier otro tipo. Observe que no está pennitido utilizar ninguna de
las clases envoltorio de los tipos primitivos, pero gracias a la característica de conversión automática, esto no es una verda-
dera limitación. También podemos tener elementos que sean ellos mismos anotaciones. Como veremos un poco más ade-
lante, las anotaciones anidadas pueden resultar muy útiles.
@Target{ElementType .METHOD }
@Retenti o n (RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
public int id() default -1;
public String description () default !I!I i
///>
Esta estructura sintáctica es bastante típica en las definiciones de anotaciones.
la misma fomla para cada componente bean. Los servicios web. las bibliotecas de marcadores personalizados y las herra-
mientas de mapeo objeto/relacional como Toplink y Hibf"matc a menudo requieren descriptores XML que son externos al
código. Después de definir WUI. clase Java, el programador debe lIevtlT a cabo la tediosa tarea de volver a especificar infor-
maciones tales como el nomb!'e, el paquete, etc., es declT, ¡nfonnaciones que ya existen en la clase original. Cada vez que
utilizamos un archivo descriptor externo. tenllin3mos con dos fucntes de infonnación separadas de una clase, lo que nor-
malmente conduce a que aparezcan problemas de sincronización del código. Esto requiere también que los programadores
que trabajen en el proyecto sepan cómo editar e l descriptor además de cómo escribir programas Java.
Suponga que queremos proporcionar una funcionalidad básica de mapeo objeto/re lacional para automatizar la creación de
una tab la de base de datos con el fin de almacenar un componente JavaBean. Podríamos utilizar un archivo descriptor XML
para especificar el nombre de la clase, cada de sus miembros y la infonnación acerca de su mapeo sobre la base de datos.
Sin embargo. utilizando anotaciones, podemos mantene r toda la infonnación en el archivo fuente del componente JavaBean.
Para hacer esto, necesitamos anotaciones para definir el nombre de la tabla de base de dalos asociada con el componente
bean, sus columnas y los tipos SQL que hay que hacer corresponder con las propiedades del componente bean.
He aquí una anotación para un componente beall que le dice al procesador de anotaciones que tiene que crear una tabla de
base de datos:
//: annotations/database/DBTable.java
package annotations.database¡
import java. lang. annotation. * ¡
@Target(ElementType.FIELD)
@Retention(RetentionPolicy .RUNTIME )
public @interface Constraints {
boolean primaryKey() default false¡
boolean allowNull{) default true;
boolean uniqueCl default false;
!/ 1,-
1/: annotations/database/SQLString.java
package annotations.database¡
import java.lang.annotation.*;
@Target(ElementType . FIELDl
@Retention(Retent ionPolicy.RUNTIME l
public @i nterface SQLString {
int value() default O;
String name() default 1111;
Constraints constraints() default @Constraints¡
20 Anotaciones 699
} 111,-
jI: annotations/databasejSQLlnteger.java
package annotations.database¡
import java. lang.annotation. *¡
@Target(ElementType,FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLlnteger {
String name() default I I I I ¡
Constraints constraints(} default @Constraints¡
1/ 1,-
La anotación @Constraints pennite al procesador extraer los metadatos relativos a la tabla de la base de datos. Esto repre-
senta un pequeño subconjunto de las restricciones que generalmente ofrecen las bases de datos, pero nos permite hacemos
una idea general. Los elementos primaryKey( ), allowNull() y unique() tienen asignados valores predeterminados ade-
cuados. de modo que en la mayoría de los casos un usuario de la anotación no tendrá que escribir demasiado texto.
Las otras dos anotaciones @interface definen tipos SQL. De nuevo, para que este sistema sea más util, necesitamos defi-
nir una anotación para cada tipo SQL adicional. En nuestro ejemplo, dos tipos serán suficientes.
Cada uno de estos tipos tiene un elemento name() y un elemento constraints( ). Este último hace uso de la característica
de anotaciones anidadas, para incluir la información acerca de las restricciones de base de datos aplicable al tipo de colum-
na. Observe que el va lor predetenninado para el elemento contraints() es @Constraints. Puesto que no hay valores de ele-
mentos especificados entre paréntesis después de este tipo de anotación, el valor predetemünado de cODstraints( ) es en la
práctica una anotación @Constraints con su propio conjunto de valores predetenninado. Para definir una anotación
@Constraints anidada donde la característica de unicidad esté definida como true de manera predeterminada, podemos
definir su elemento de la fonna siguiente:
JJ: annotationsJdatabaseJUniqueness.java
JJ Ejemplo de anotaciones anidadas
package annotations.database¡
vamente. Estas anotaciones son interesantes por dos razones: en primer lugar, utilizan el valor predetenninado en la anota-
ción @Constraints anidada. y en segundo lugar emplean una característica especia l de abreviatura. Si definimos un ele-
mento de una anotación con el nombre va lue. entonces no será necesario utilizar la sintaxis basada en parejas de
nombre-va lor siempre y cuando sea el único tipo de elemento especificado; podemos limitamos a especificar el valor entre
paréntesis. Esto puede ap licarse a cualquiera de los tipos de elementos legales. Por supuesto, con esto estamos obligados a
llamar a nuestro elemento "value", pero en el caso anterior, nos pem1ite emplear una especificación de anotación semánti-
camente significati va y muy fácil de leer:
@SQLString(30)
El procesador utiliza este valor para establecer el tamaño de la columna SQL que cree.
Aunque la sintaxis relativa a los valores predetenninados es bastante limpia, puede volverse muy rápidamente muy comple-
ja. Observe la anotación correspondiente al campo handle. Tiene una anotación @SQLString, pero también necesita ser
una clave principal de la base de datos, por lo que es necesario activar el tipo de elemento en primaryKey en la anotación
@Constrain t anidada. Aquí es donde el ejemplo comienza a ser confuso. Ahora estamos forzados a utilizar la fonna, bas-
tante larga, basada en una pareja de nombre-valor para esta anotación anidada, volviendo a especificar el nombre de ele-
mento y el nombre de la interfaz @interface. Pero, como el elemento de nombre especial value ya no es el único valor de
elemento que se está especificando, no podemos emplear la fonna abreviada. Como puede ver, el resultado no es precisa-
mente elegante.
Soluciones alternativas
Existen otras fonnas de crear anotaciones para llevar a cabo esta tarea. Podríamos, por ejemplo, tener una única clase de
anotación denominada @TableColumn con un elemento enum que definiera valores como STRlNG, lNTEGER, FLOAT,
etc. Es to elimina la necesidad de definir una anotación @interface para cada tipo SQL, pero hace que sea imposible cuali-
ficar los tipos con elementos adicionales corno size (tamaño) o precision (precisión), lo cual resulta probablemente más útil.
También podríamos utilizar un elemento de tipo String para describir el tipo SQL correcto, como por ejemplo, "VAR-
CHAR(30)" o "lNTEGER". Esto nos permite cualificar los tipos, pero nos fuerza a fijar en el código la correspondencia
entre el tipo Java y el tipo SQL, lo cual no es una buena práctica de diseño. No conviene tener que recompilar las clases si
cambiamos las bases de datos, sería más elegante limitarnos a decirle a nuestro procesador de anotaciones que estamos usan-
do una ¡'versión" diferente de SQL, y dejar que el procesador lo tenga en cuenta a la hora de procesar las anotaciones.
Una tercera solución factible consiste en utilizar conjuntamente dos tipos de anotación: @Constraints y el tipo SQL rele-
vante (por ejemplo, @SQLInteger), para anotar el campo deseado. Esto resulta ligeramente complicado, pero el compila-
dor nos pennite utilizar tantas anotaciones diferentes como queramos sobre un mismo objetivo de anotación. Observe que,
al utilizar múltiples anotaciones, no podemos emplear la misma anotación dos veces.
/* Output:
Table Creation SQL for annocations . database.Member is
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.database . Member is
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30) ,
LASTNAME VARCHAR(50));
Table Creation SQL for annotations.database.Member is :
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30) ,
LASTNAME VARCHAR(50) ,
AGE INT);
Table Creation SQL for annotations.database.Member is :
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30) ,
LASTNAME VARCHAR (50) ,
AGE INT,
HANDLE VARCHAR(30) PRlMARY KEY);
* /1/,-
El método main( ) recorre sucesivamente cada uno de los nombres de clase en la línea de comandos. Cada clase se carga
utili zando forName() y se la comprueba para ver si incluye la anotación @DBTable utilizando getAnnotation(DBTable
.class). Si se incluye esa anotación, se locali za el nombre de la tabla y se almacena, entonces se cargan y se comprueban
todos los campos de la clase con getDeclaredAnnotations( ). Este método devuelve una matriz con todas las anotaciones
definidas para un método concreto. Se utiliza el operador instanceof para detenninar que estas anotaciones son de tipo
@SQLlnteger y @SQLString, y en cada caso se crea entonces el fragmento de cadena de caracteres relevante, con el nom-
bre de la columna de la tabla. Observe que, como 110 existe posibilidad de herencia en las interfaces de anotación, la utili-
zación de getDeclaredAnnotations() es la única fanna con la que podemos aproximamos al comportam iento polimórfico.
La anotación @Constraint anidada se pasa al método getConstraints( ), que construye un objeto de tipo String que con-
tiene las restricciones SQL.
Merece la pena mencionar que la técnica mostrada anterionnente es una fonna un tanto ingenua de definir un mapeo
objeto/relacional. Disponer de una anotación de tipo @DBTable, que toma el nombre de la tabla como parámetro, nos obli-
ga a recompilar el código Java cada vez que queramos cambiar el nombre de la tabla, lo que puede resultar no muy conve-
niente. Hay disponibles muchos sistemas para mapear objetos sobre bases de datos relacionales, y cada vez un número
mayor de ellos está haciendo uso de las anotaciones.
Ejercicio 1: (2) Implemente más tipos SQL en el ejemplo de la base de datos.
Proyecto: 3 Modifique el ejemplo de la base de datos para que se conecte con una base de datos rea l e interacme con
ella utili zando mBC.
Proyecto : Modifique el ejemplo de la base de datos para que cree archivos compatibles con XML en lugar de escri-
bi r código SQL.
J Los proyectos son sugerencias que pueden utilizarse, por ejempl o. como proyectos de fin de curso. Las soluciones a los proyectos no se incluyen en la
Guia de .fOll/clones.
20 Anotaciones 703
@Target(ElementType .TYPE )
@Retention(RetentionPolicy.SOURCE )
public @i nterface Extractlnterface
pUblic String value();
} 111,-
El valor de RelenlionPolicy es SO URCE porque no tiene ningún sentido mantener esta anotación en el archivo de clase
después de haber extraído de ésta la interfaz. La siguiente clase proporciona un método público, que podría fonnar parte de
una interfaz:
11 : annotations/Multiplier.java
II Procesamiento de anotaciones basado en APT.
package annotations;
4 Sin embargo, utilizando la opción no estándar-XclassesAsOecls, se puede trabajar con anOlacioncs que estén contenidas en clases compiladas.
s La palabra mirror significu espejo, así que se trata de un juego de palabras de los diseñadores Java para hacer referencia en realidad al mecanismo de
reflexión.
704 Piensa en Java
/* Output:
11*16 = 176
*///0-
La clase MultipLier (q ue sólo funci ona con enteros positivos) tiene un método multiply() que in voca numerosas veces el
método privado add( ) para llevar a cabo la multiplicación. El método add( ) no es público, así que no fo rma parte de la
interfaz. A la anotación se le as igna el valor de lMultiplier, que es el nombre de la interfaz que bay que crear.
Ahora necesitamos un procesador para realizar la ex tracción:
1/: annotations/lnterfaceExtractorProcessor.java
JI Procesamiento de anotaciones basado en APT.
// {Exeeo apt -faetory
JI annotations.lnterfaceExtractorProcessorFactory
JI Multiplier.java -5 . . /annotations}
package annotationsi
import com . sun.mirror.apt.*¡
import com.sun.mirror.declaration.*¡
import java.io.*;
import java.util. * ;
writer.println(") ;");
writer.println {u}") ;
writer.close() i
catch(IOException ioe)
throw new RuntimeException(ioe);
}
/// ,-
El lugar donde se realiza todo el trabajo es en el método process(). La clase MethodDeclaration y su método
getModifiers() se usan para identificar los métodos públicos (pero ignorando los estáticos) de la clase que se esté proce-
sando. Si se encuentra algún método público, se almacena en un contenedor ArrayList y se em plea para crear los métodos
de una nueva definición de interfaz en un archivo.java.
Observe que al constructor se le pasa un objeto AnnotationProcessorEnvironment. Podemos consultar este objeto para
detenninar todos los tipos (definiciones de clase) que la herramienta apt está procesando, y podemos usa:-Io para obtener un
objeto Messager y un objeto Filer. El objeto Messager nos permite emitir mensajes dirigidos al usuario, por ejemplo cual-
quier error que pueda haberse producido en el procesamiento, junto con el lugar del código fuente donde se haya produci-
do. El objeto Filer es un tipo objeto PrintWriter a través del cual se crean nuevos archivos. La principal razón de emplear
un objeto Filer, en lugar de un objeto PriotWriter simple es que pennite a apt llevar la cuenta de los nuevos archivos que
creemos, para así poder comprobar si contienen anotaciones y compilarlas, en caso necesario.
También podemos ver que el método createSourceFile() abre un flujo de salida ordinario con el nombre correcto para nues-
tra interfaz o clase Java. No existe ningún tipo de soporte para la creación de estmcturas sintácticas del lenguaje Java, así
que bay que generar el código fuente Java utilizando los métodos print() y println(), un tanto primitivos. Esto quiere decir
que tenemos que asegurarnos de que los corchetes estén bien emparejados y de que el código sea sintácticamente correcto.
La herramienta apt invoca al método process(), porque la herramienta necesita una factoría para proporcionar el procesa-
dor adecuado:
11: annotations/lnterfaceExtractorProcessorFactory.java
II Procesamiento de anotaciones basado en APT.
package annotations;
import com.sun.mirror.apt.*¡
import com.sun.mirror.declaration.*¡
import java.util.*¡
public class InterfaceExtractorProcessorFactory
implements AnnotationProcessorFactory {
public AnnotationProcessor getProcessorFor(
Set<AnnotationTypeDeclaration> atds,
AnnotationProcessorEnvironment env) {
return new InterfaceExtractorProcessor(env);
impore com.sun.mirror.apt.~i
import com.sun.mirror.declaration.*¡
import com.sun.mirror.ucil.*¡
impore java.util.*;
impore static com.sun.mirror.ucil.DeclarationVisitors.*¡
if(d.getAnnotation(SQLString.class) != null) {
SQLString sString = d.getAnnotation(
SQLString.class) ;
JI Utilizar el nombre del campo si no se especifica un nombre .
if(sString.name() .length() < 1)
columnName d.getSimpleName() ,toUpperCase(}¡
else
columnName sString.name() i
sql += "\n + columnName + " VARCHAR(" +
11
1* Output:
annotations.AtUnitExamplel
. methodOneTest
6 Originalmente, pensé en discnar una ··versiÓn avanzada de JUniC· basada en el diseno mamado aquí. Sin embargo, pareec que JUnit 4 lambién incluye
muchas de las ideas que aqui ~e presentan, así que me resulta más scncillo utilizar la herramienta disponible.
m2 This is methodTwo
m3
failureTest (failed)
anotherDisappointment (failed)
( S tests)
}
/ * Output:
annotations.AtUnitExternalTest
methodOne
_methodTwo This i s methodTwo
OK (2 tests)
*/1/,-
Este ejemplo también ilustra el valor de los esquemas de denominación flexibles Ca diferencia del requisito de JUnit que
exige que todas nuestras pruebas comiencen por la palabra "test"). Aquí, los métodos @Test que están probando directa-
mente otro método reciben el nombre de dicho método, pero comenzando por un guión bajo (no estoy sugiriendo que este
estilo resulte ideal, sino simplemente mostrando una posibilidad).
También podemos utilizar el mecanismo de composición para crear pruebas no embebidas:
// : annotations/AtUn itComp os i tion . java
// Creación de pr uebas no e mbebi da s.
package annotations ¡
import net.mindvie w. atunit. * ¡
import net . mindview . util .* ;
20 Anotaciones 711
/* Output:
annotations . AtUnitComposition
methodOne
methodTwo This i5 methodTwo
OK (2 tests)
* /// , -
Para cada prueba se crea un nuevo miembro testObject, ya que se crea para cada prueba un objeto AtUnitComposition .
No existen métodos especiales "assert" como en JUnit, pero la segunda forma del método @Test pennite devol ver void (o
boolean, si seguimos queriendo devolver true o false en este caso). Para comprobar que la prueba ha tenido éxito, pode-
mos utilizar instrucciones assert de Java. Las aserciones de Java normalmente tienen que ser habilitadas con el indicador
-ea en la linea de comandos java, pero @Unit se encarga automáticamente de habilitarlas. Para indicar el fallo, podemos
incluso emplear una excepción. Uno de los objetivos de diseño de @Unit es imponer la menor cantidad posible de sintaxis
adicional, y las excepciones e instrucciones assert de Java son lo único necesario para informar acerca de las errores. Una
aserción fallida o una excepción generada dentro del método de prueba se tratan como una prueba fallida, pero @Unit no
se detiene en este caso, sino que continúa hasta haber ejecutado todas las pruebas.
11 : a nnotations/ AtUnit Example2 . java
11 Se pueden ut i lizar aserciones y excepciones en las prue bas.
package annotations;
import java.io. * ;
import net.mindview.atunit .* ;
import net.mindview.util.*;
/ * Output:
annotations.AtUnitExample2
· assertExample
· assertFailureExample java.lang.AssertionError: What a surprise!
Ifailedl
· exceptionExample java.io.FileNotFoundException: nofile.txt (The system cannot find
the file specified)
(failedl
· assertAndReturn This is methodTwo
(4 tests)
He aquí un ejemplo utilizando pruebas no embebidas con aserciones, en el que se realizan algunas pruebas simples de
java.util.HashSet:
JI: annotations/HashSetTest.java
package annotations¡
import java.util .*;
import net.mindview.atunit.*¡
import net.mindview.util.*¡
/* Output:
annotations.HashSetTest
initialization
remove
contains
OK (3 tests )
* jjj ,-
Ejercicio 5: (l) Modifique el ejemplo anterior para utilizar la solución basada en la herencia.
Ejercicio 6: (1 1 Pruebe LinkedList utilizando la técnica mostrada eo HashSetTest.java.
Ejercicio 7: (1) Modifique el ejercicio anterior para utilizar la solución basada en la herencia.
Para cada pmeba unitaria, @Un it crea un objeto de la clase que se está probando utilizando un constructor predetenninado.
Se invoca la prueba para dicho objeto, y a continuación se descarta el objeto para evitar que se deslicen efectos secundarios
en otras pruebas unitarias. Esta solución utiliza el constructor predeterminado para crear los objetos. Si no disponemos de
un constructor predetenrunado o necesitamos un mecanismo más sofisticado de construcción de los objetos, bay que crear
un métodos estático para generar el objeto y asociar la anotación @TestObjectCreate, como en el ejemplo siguiente:
11 : annotations/AtUnitExample3.java
package annotations¡
import net.mindview.atunit.*¡
import net . mindview.util.*¡
1* Output:
annotations.AtUnitExample3
initialization
methodOneTest
m2 This is methodTwo
OK (3 tests )
* ///,-
El método @TestObjectCreate debe ser estático y debe devolver un objeto del tipo que estemos probando; el programa
@U nit se encargará de comprobar que esto es asÍ.
En ocasiones, necesitamos campos adicionales para realizar las pruebas unitarias. Podemos utilizar la anotación
@TestProperty para marcar aquellos campos que sólo se utilicen en las pruebas unitarias (con el fin de poderlos eliminar
antes de entregar el producto al cliente). He aqui un ejemplo que lee valores de un objeto Striog que se descompone utili-
zando el método Stri ng.split( l. Esta entrada se emplea para generar objetos de prueba:
//: annotations/AtUnitExample4.java
package annotationsi
import java.util.*¡
import net.mindview.atunit.*¡
714 Piensa en Java
/ * Output:
starting
annotations.AtUnitExample4
scramblel I All '
lAl
20 Anotaciones 715
. scramble2 'brontosauruses'
tsaeborornussu
words 'are'
OK (3 tests)
* ///,-
También podemos usa r @TestProperty para marcar métodos que pueden ser utilizados durante las pruebas, pero que no
sean pruebas en sí mismos.
Observe que este programa depende del orden de ejecución de las pruebas, lo cual no es, en general, una buena práctica.
Si nuestro proceso de creación de objetos de pruebas realiza una inicialización que requiera una posterior limpieza. pode-
mos añadir opcionalmente un método estático @TestObjectCleanup para realizar la limpieza cuando hayamos tern1inado
de usar el objeto de pmeba. En este ejemplo, @TestObjectCreatc abre un archivo para crear cada objeto de pmeba, así que
es necesario CCITar el archivo antes de descartar el objeto de prueba:
/1 : annotations/AtUnitExampleS.java
package annotations¡
import java.io.*;
import net.mindview.atunit.*;
import net.mindview.util.*;
/ * Output:
716 Piensa en Java
annotations.AtUnitExampleS
· testl
Running cleanup
· test2
Running cleanup
· test3
Running cleanup
OK (3 tests)
*///,-
Podemos ver, analizando la sal ida, que después de cada prueba se ejecuta automáticamente el método de limpieza.
Implementación de @Unit
En primer lugar, necesitamos definir todos los tipos de anotación. Se trata de marcadores simples que no tienen ningún
campo. El marcador @Test ya se ha definido al principio del capítulo y aquí están el resto de las anotaciones:
//: net/mindview/atunit/TestObjectCreate.java
// El marcador @Unit @TestObjectCreate.
package net.mindview.atunit;
import java.lang.annotation.*;
@Target(ElementType .METHOD )
@Retention(RetencionPolicy.RUNTIME)
public @interface TestObjectCreate {} ///,-
//: net/mindview/atunit/TestObjectCleanup.java
/1 El marcador @Unit @TestObjectCl eanup.
package net.mindview.atunit¡
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestObjectCleanup {} ///,-
//: net/mindview/atunit/TestProperty.java
718 Piensa en Java
JI Se pueden marcar como propiedades tanto los campos como los métodos:
@Ta rget({ElementType . FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestProperty {} 1//:-
Todas las pruebas ti enen un tipo retención igual a RUNTlME, porque el sistema @Unit debe descubrir las pruebas en el
código co mpilado.
Para imp lementar el sistema que ejecuta las pruebas, utilizamos el mecanismo de reflexión para ex traer las anotaciones. El
programa utili za esta información para decidir cómo constmir los objetos de prueba y para ejecutar las pruebas sobre ellos.
Grac ias a las anotaciones el sistema es so rprendentemente pequeño y sencillo:
JI : net /m indview / atunit / AtUnit.java
/1 Un sistema de pruebas unitarias basadas en anotaciones.
// {RunByHand}
package net.mindview.atunit;
impert java.lang.reflect. * ;
import java.io.*;
import java.util.*;
impert net.mindv iew.util .* ;
impert static net . mindview.util.Print. *;
creator = checkForCreatorMethod{rn);
if(cleanup == null)
cleanup = checkForCleanupMethod(rn) ¡
if(testMethods.size() > O) {
if(creator == null)
try (
if(lModifier.isPublic(testClass
. getDeclaredConstructor () . getModifiers () ) )
print (ji Error: JI + testClass +
" default constructor rnust be public") ¡
Systern. exit (1) ;
catch (NoSucbMethodExceptíon el {
II Constructor determinado sintetizado; OK
print(testClass.getName()) ¡
for(Method m testMethods ) {
printnb ( " !I + m. getName () + ti n) ¡
try (
Object testObject = createTestObject{creator);
boolean success = false;
try (
if (m . getReturnType () . equals (boolean. class l )
success = (Boolean)m .invoke {testObjec t );
else (
m.invoke(testObjectl ¡
success = true; II Si no falla ninguna aserción
catch(InvocationTargetException el
jI La excepción en sí está dentro de e:
print (e .getCause () ) ;
if(cleanup != null}
cleanup. ínvoke (testObject, testObject);
catch(Exception e) {
throw new RuntimeException{e) ;
8 No está claro por qué el constructor prcdetemlinado de la clase que estemos probando debe ser público, pero si no lo es, la llamada a lIewl nstance() se
cuelga (sin generar una excepción).
20 Anotaciones 721
Uno de los problemas que AtUnit.java debe resolver cuando descubre archivos de clase es que el nombre de clase real cua-
lificado (incl uyendo el paquete) no resulta evidente a partir del nombre de archivo de clase. Para descubrir esta información,
debe analizarse el archivo de clase, lo cual no es trivial, aunque tampoco imposible. 9 Por tanto, lo primero que sucede cuan-
do se locali za un archivo .class es que se abre y sus datos binarios son leidos y entregados a ClassNameFinder.thisClass( ).
Aquí, nos estamos introduciendo en el campo de la "ingeniería de código intennedio", porque lo que estamos haciendo es
anali zar el contenido de un archivo de clase:
ji: net/mindview/atunit/ClassNameFinder.java
package net.mindview.atunit¡
import java.io.*;
import java.util. *;
import net.mindview.util.*¡
import static net.mindview.util.Print.*¡
9 Jcrcmy Meyer y yo nos pasamos la mayor parte de una jornada tTIltando de descubrir la solución.
722 Piensa en Java
case 12 , II NAME_AND_TYPE
data.readlnt(); // descartar 4 bytes;
break;
default:
throw new RuncimeException (UBad tag n + tag);
JI Ilustración:
public static vOld main(String[) args) throws Exception {
i f (args . length > O) (
for{String arg : args)
print (thisCl ass (BinaryFil e.read(new File (arg ))));
else
JI Recorrer todo el árbol:
tor (File klass : Directory. wa lk (" . ", u. * \ \ . class") )
print(thisClass(BinaryFile.read(klass))) ;
}
111,-
Aunque no podemos analizar aquí todos los detalles, cada archivo de clase se ajusta a un fonnato concreto y hemos tratado
en el ejemplo de utilizar nombres de campo significativos para los fragmentos de datos extraídos del flujo de datos
ByteArraylnputStream; también podemos ver el tamaño de cada fragmento examinando la longitud de la lectura realiza-
da en el flujo de entrada. Por ejemplo, los primeros 32 bits de cualquier archivo de clase son siempre el "número mágico"
oxcafebabe, JO y los dos siguientes valores short son la infonnación de versión. La sección de constantes contiene las cons-
tantes del programa y es, por tanto, de tamaño variable; el siguiente valor short nos dice cuál es el tamaño para poder asig-
nar una matriz del tamaño apropiado. Cada entrada de la sección de constantes puede ser un valor de tamaño fijo o variable,
así que tenemos que examinar el marcador con el que comienza cada uno para ver qué hay que hacer con él, ésa es la razón
de la instmcción switch. Aquí, no estamos tratando de analizar con precisión todos los datos del archivo de clase, sino sim-
plemente recorrer ésta y extraer los fragmentos de interés. así que, como puede ver en el ejemplo, se descarta una gran can-
tidad de datos. La infom,ación acerca de las clases está almacenada en las tablas classNameTable y offsetTable. Después
de leer la sección de constantes, podemos encontrar la infomlación this_class que es un índice para la tabla offsetTablc, que
produce un índice para la tabla classNameTable, en la que podemos leer el nombre de la clase.
Volviendo a AtUnít.java, el método process( l abara dispone del nombre de la clase y puede tratar de detenninar si contie-
ne ".', lo que quiere decir que está dentro de un paquete. Las clases no incluidas en un paquete se ignoran. Si una clase se
encuentra en un paquete se utiliza el cargador de clases estándar para cargar la clase con Class.forNamc( l. Ahora podemos
analizar la clase en busca de anotaciones @Unit.
Sólo necesitamos buscar tres cosas: métodos @Test, que están almacenados en una lista TestMethods, y si existen méto-
dos @TestObjectCrcate y @TestObjectCleanup. Estos métodos se descubren mediante las llamadas a método asociadas
que se pueden ver en el código, que buscan las correspondientes anotaciones.
Si se encuentra algún método @Test, se imprime el nombre de la clase para que podamos ver lo que está sucediendo, a con-
tinuación de lo cual se ejecuta cada prueba. Esto implica imprimir el nombre de un método, luego invocar
crcateTestObjcct( l , el cual utilizara el método @TestObjectCreate si existe o utilizará el constructor predeterminado si
no existe. Una vez creado el objeto de prueba, se in voca el método de prueba para dicho objeto. Si la pmeba devuelve un
10 Hay varias leyendas relat ivas al sign ificado de este número mágico, pero como Java fue creado por auténticos frikies, podemos suponer, razonablemen-
te, que tiene algo que ver con fantasías adolescentes acerca de una mujer en una cafeteria.
20 Anotaciones 723
valor boolean, se captura el resultado. Si no. presuponemos que la prueba ha tenido éxito a menos que se genere una excep-
ción (que es lo que sucedería en caso de que se produzca una aserción fallida o cualquier OlTO tipo de excepción). Si se gene-
ra una excepción, se imprime la infom13ción de excepción para mostrar la causa. Si se produce cualquier fallo, se incrementa
el contador de fallos y se añade el nombre de la clase y el método a fa iledTests para poder incluirlos en el infonme que se
genera al final de la ejecución.
Ejercicio 11 : (5) Aliada una anotación @TestNote a @Unit, para que la nota asociada se visualice simplemente duran-
te las pmebas.
new ProcessFiles(
new AtUnitRemover () , "class" ) .start (args } ;
11 Gracias al Dr. Shigeru Chiba por crear eSIa biblimeca, y por toda la ayuda que me prestó a la hora de desarrollar AlUnit Removcr-.j ava .
724 Piensa en Java
BinaryFile.read(cFile)) i
if (! cName. contains (n . 11) )
return; /1 Ignorar clases no empaquetadas
ClassPool cPool = ClassPool.getDefault();
CtClass ctClass = cPool.get(cName);
for{CtMethod methad : ctClass.getOeclaredMethods())
Methodlnfo mi = mechod.getMethodlnfo() i
AnnotationsAttribute attr = (AnnotationsAttribute)
mi.getAttribute(AnnotationsAttribute.visibleTag) i
if(attr == null) continue;
for(Annotation ann : attr.getAnnotations())
if(ann.getTypeName()
. startsWi th (ltnet. mindview. atuni t") )
print (ctClass .getName () + 11 Methad:
+ mi.getName() + " " + ann);
if (remove) {
ctClass.removeMethod(method) i
modified = true;
)
/1 Los campos no se eliminan en esta versión (véase el texto).
if (modifiedl
ctClass.toBytecode(new DataOutputStream(
new FileOutputStream(cFile)));
ctClass.detach() ;
catch (Exception el {
throw new RuntimeException(e} i
)
/// ,-
ClassPoo) es una especie de resumen de todas las clases del sistema que estemos modificando. Garantiza la coherencia de
todas las clases modificadas. Podemos extraer cada clase CtClass de ClassPool, de fonna similar a como el cargador de
clases y Class.forName() cargan las clases en la máquina NM.
CtClass contiene el código intermedio de un objeto de clase y nos permite generar infomlación acerca de la clase y mani-
pular el código de la misma. Aquí, invocamos getDeclaredMethods( ) (al igual que el mecanismo de reflexión de Java) y
obtenemos un objeto Methodlnfo a partir de cada método CtMethod . Con esto, podemos examinar las anotaciones. Si
algún método tiene una anotación en el paquete net.mindview.atunit, se elimina dicho método.
Si la clase ha sido modificada, se sobreescribe el archivo de clase original con la nueva clase.
En el momento de escribir estas líneas, se acababa de añad ir la funcionalidad de "eliminación" de Javassist 12 , y descubri-
mos que eliminar los campos @TestProperty resulta más complejo que eliminar los métodos. Dado que pueden existir ope-
raciones de inicialización estática que hagan referencia a esos campos, no podemos limitarnos a borrarlos. Por tanto, la
versión anterior del código sólo elimina los métodos @U nit. Sin embargo, consulte el sitio web de Javassist para ver si exis-
ten actualizaciones: es posible que en el futuro se añada la funcionalidad de eliminación de campos. Mientras tanto, obser-
ve que el método de prueba externo mostrado en AtUnitExternaJTest.java permite eliminar todas las pruebas simplemente
borrando todos los archivos de clase creados por el código de prueba.
Resumen
Resulta muy de agradecer que se hayan añadido las anotaciones a Java. Constituyen una forma estructurada (y con compro-
bación de tipos) de añadir metadatos al código sin hacer que éste se complique innecesariamente y resulte ilegible. Las
12 El Dr. Shigeru Chiba fue tan amable de añadir el método CtClass.remo\'cMethod( ) a solicitud nuestra.
20 Anotaciones 725
anotaciones pueden ayudarnos a eliminar la tediosa tarea de escribir descriptores de implantación y otros archivos genera-
dos. El hecho de que el marcador Javadoc @deprecatcd haya sido sustiruido por la anotación @Deprecated es simplemen_
te una indicación de hasta qué punto las anotaciones son mucho más convenientes que 105 comenrarios para describir la
información de las clases.
Java SES sólo incluye un pequeño número de anotaciones. Esto quiere decir que, si 110 utiliza una biblioteca de otro fabri-
cante, necesitará crear sus propias anotaciones, junto con la lógica asociada. Con la herramienta apt, podemos compilar los
archivos recién generados en un único paso, facilitándose así el proceso de constmcción de aplicaciones. pero actualmente
la API miTror tan sólo incluye la funcionalidad básica para ayudarnos a identificar los elementos de las definiciones de cla-
ses Java. Como hemos visto, podemos utilizar Javassist para las tareas de ingeniería de código intennedio, o bien podemos
escribir a mano nuestras propias herramientas de manipulación de código intennedio.
La situación mejorará sin ninguna duda en el futuro, y los fabricantes de interfaces API y sistemas comenzarán a proporcio-
nar anotaciones como parte de sus herramientas. Como puede imaginarse al analizar el sistema @U nit, resulta bastante pre-
visib le que las anotaciones provoquen cambios significativos en la forma de programar en Java.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico The Thinking in JlI\'ll AllllOla/ed SO/lIIiol/ CI/ide, disponible para
la venia en W'lI"'w.A1indViell.nel.
Concurrencia
Hasta este momento, hemos estado hablando de programación secuencial. Todo lo que sucede
en un programa sucede paso a paso.
Una gran cantidad de problemas de programación pueden resolverse utilizaodo programación secuencial. Sin embargo, para
algunos problemas, resulta conveniente e incluso esencial ejecutar varias partes del programa en paralelo, de modo que
dichas partes parezcan estarse ejecutante concurrentemente o, si hay disponibles varios procesadores, se ejecuten realmen-
te de manera simultánea.
La programación paralela puede mejorar enormemente la velocidad de ejecución de los programas, proporcionar un mode-
lo roás sencillo para el diseño de ciertos tipos de programas, o ambas cosas a la vez. Sin embargo, llegar a familiarizarse
con la teoría y las técnicas de la programación concurrente es algo situado a un nivel superior que las técnicas de progra-
mación que hemos aprendido hasta ahora en el libro, y representa un tema de nivel intermedio o avanzado. Este capítulo tan
sólo puede proporcionar una introducción al tema, y después de estudiarlo será mucho el camino que le quede para llegar a
ser un buen programador concurrente.
Como veremos, el problema real de la concurrencia es el que se presenta cuando una serie de tareas que se están ejecutan-
do en paralelo comienzan a interferir entre sí. Esto puede suceder de una maoera tan sutil y ocasional que probablemente
resulte bastante apropiado decir que la concurrencia es "teóricamente determinista pero prácticamente no determinista". En
otras palabras, podemos demostrar que resulta posible escribir programas concurrentes que, con el suficiente cuidado y con
las necesarias inspecciones de código, funcionen correctamente. Sin embargo, en la práctica, resulta mucho más fácil escri-
bir programas conCWTentes que únicamente "parezcan" funcionar correctamente pero que, dadas las condiciones adecuadas,
fallarán. Es posible que estas condiciones nunca lleguen a darse o que se den de una manera tao infrecuente que jamás apa-
rezcan los fallos durante las pruebas. De hecho, puede que no seamos capaces de escribir código de pruebas que permita
generar las condiciones de fallo de nuestros programas concurrentes. Los fallos resultantes sólo ocurrirán ocasionahnente,
y como resultado aparecerán en forma de quejas de los clientes. Éste es uno de los argumentos principales de estudiar el
tema de la concurrencia: si lo ignoramos, lo más probable es que los problemas terminen por asaltamos.
La concurrencia parece estar, por tanto, rodeada de peligros, y si eso le hace sentirse un tanto atemorizado, mejor que mejor.
Aunque Java SE5 ha realizado mejoras significativas en lo que respecta a la concurrencia, siguen sin existir sistemas de pro-
tección como la verificación en tiempo de compilación o las excepciones comprobadas, para ¡nfonnamos de cuándo hemos
cometido un error. Con la concurrencia, toda la responsabilidad recae sobre nosotros, y sólo si somos suspicaces yagresi-
vos podremos escribir código multihebra en Java que sea 10 suficientemente fiable.
Hay algunas personas que sugieren que la concurrencia es un tema demasiado avanzado como para incluirlo en un libro de
introducción al lenguaje. Su argumento es que la concurrencia es un tema autónomo que puede tratarse independientemen-
te y que los pocos casos en los que la concurrencia aparece durante las tareas cotidianas de programación (como por ejem-
plo, con las interfaces gráficas de usuario) pueden tratarse sin necesidad de recurrir a estructuras especiales del lenguaje.
¿Por qué introducir un tema tao complejo si podemos evitarlo?
iOjalá fuera así! Lamentablemente, la decisión de si nuestros programas Java utilizarán hebras no está en nuestras manos.
El sólo hecho de que nosotros no creemos ninguna hebra no quiere decir que vayamos a ser capaces de evitar escribir códi-
go basado en hebras. Por ejemplo, los sistemas web constituyen una de las aplicaciones Java más comunes y la clase bási-
ca de la biblioteca web, servlet, es inherentemente multihebra; esto resulta esencial porque los servidores web contienen a
menudo múltiples procesadores y la concurrencia es una forma ideal de emplear esos procesadores. Aunque un servlet puede
728 Piensa en Java
parecer muy simple, es necesario que entendamos los problemas de la concurrencia con el fin de utilizar los servlets apro-
piadamente. Lo mismo podríamos decir de la programación de las interfaces gráficas de usuario, como veremos en el
Capítulo 22, Interfaces gráficas de usuario. Aunque las bibliotecas Swing y SWT disponen de mecanismos para la segnri-
dad de las hebras resulta dificil utilizar dichos mecanismos adecuadamente sin entender el tema de la concurrencia.
Java es un lenguaje multihebra y los problemas de concurrencia están presentes, con independencia de que seamos cons-
cientes de su existencia. Como resultado, existen muchos programas Java que funcionan simplemente por accidente o que
funcionan la mayor parte de las veces y que fallan misteriosamente de vez en cuando debido a problemas de concurrencia
na localizados. En ocasiones, estos fallos son benignos, pero otras veces pueden representar la pérclida de datos de gran
valor, y si no somos al menos conscientes de los problemas concurrencia, podemos tenninar asumiendo que el problema se
encuentra en algún otro lugar en vez de en nuestro software. Este tipo de problemas también pueden verse expuestos o
amplificados si se transfiere un programa a un sistema multiprocesador. Básicamente, conocer el tema de la concurrencia
nos permite ser conscientes de que existe una posibilidad de que programas aparentemente correctos puedan exhibir un com-
portamiento incorrecto.
La programación concurrente es como desembarcar en un nuevo mundo y aprender un nuevo lenguaje, o al menos IDl nuevo
conjunto de conceptos del lenguaje. Comprender la programación concurrente tiene el mismo nivel de clificultad que com-
prender la programación orientada a objetos. Si nos aplicamos, podemos llegar a entender el mecanismo básico, pero gene-
ralmente es neesario un estudio profundo y un cierto nivel de práctica para llegar a dominar realmente la materia. El objetivo
de este capítulo es proporcionar una panorámica de los fundamentos básicos de la concurrencia, para que se puedan enten-
der los conceptos y se puedan escribir programas multihebra de una complejidad razonable, pero tenga en cuenta que resul-
ta fácil confiarse demasiado. En cuanto comience a desarrollar soluciones de una cierta complejidad, necesitará estudiar
libros específicamente dedicados a esta materia.
Lo que hace que la concurrencia pueda mejorar el rendimiento en estos casos es el bloqueo. Si una tarea del programa no
puede continuar con su procesamiento debido a alguna condición que no está bajo control del programa (normalmente ope-
raciones de E/S), decimos que la tarea o la hebra se bloquea. Sin la concurrencia, todo el programa tendrá que detenerse
ante esa condición externa; sin embargo, si se ha escrito el programa utilizando concurrencia, las otras tareas del programa
pueden continuar ejecutándose cuando una tarea se bloquee, con lo que el programa continuará avanzado. Desde el punto
de vista del rendimiento, no tiene sentido utilizar la concurrencia en una máquina con un único procesador, a menos que
alguna de las tareas pueda llegar a bloquearse.
Un ejemplo muy común de mejora de rendimiento en los sistemas monoprocesador es la programación dirigida por suce-
sos. De hecho, una de las razones más imperiosas para utilizar la concurrencia es la de construir una interfaz de usuario con
una buena capacidad de respuesta. Pensemos en un programa que tenga que realizar algún tipo de operación de larga dura-
ción y que termine, por tanto, ignorando la entrada del usuario, sin dar a éste ninguna respuesta. Si disponemos de un botón
para salir del programa, no queremos tener que consultar si ese botón se ha apretado en cada fragmento de código que escri-
bamos. Si lo hiciéramos, el código tendría un aspecto terrible, y además no existiría ninguna garantía de que un programa-
dor no se olvidara de realizar esa comprobación. Sin la concurrencia, la única forma de tener tma interfaz gráfica de usuario
con una buena respuesta es que todas las tareas comprueben periódicamente la entrada de usuario. Sin embargo, al crear una
hebra de ejecución separada para responder a las entradas de usuario, incluso aunque esta hebra estará bloqueada la mayor
parte del tiempo, el programa permitirá garantizar un cierto nivel de respuesta.
El programa necesita continuar realizando sus operaciones, y al mismo tiempo necesita también devolver el control a la
interfaz de usuario para poder responder a éste. Pero un método convencional no puede continuar llevando a cabo sus ope-
raciones y al mismo tiempo devolver el control al resto del programa. De hecho, parece que esto fuera imposible, como si
estuviéramos exigiendo a la CPU que estuviera en dos sitios a la vez; pero, ésta es, la ilusión que la concurrencia permite
(en el caso de los sistemas multiprocesador se trata de algo más que una ilusión).
Una forma muy sencilla de implementar la concurrencia es en el nivel del sistema operativo, utilizando procesos. Un pro-
ceso es un programa auto-contenido que se ejecuta en su propio espacio de direcciones. Un sistema operativo multitarea
puede ejecutar más de un proceso (programa) simultáneamente, conmutando periódicamente la CPU de un proceso a otro,
al mismo tiempo que parece que cada proceso estuviera ejecutándose por separado. Los procesos resultan muy atractivos,
porque el sistema operativo se encarga normalmente de aislar un proceso de otro de modo que no puedan interferir entre sí,
lo que hace que la programación basada en procesos sea relativamente sencilla. Por contraste, los sistemas concurrentes,
como el que se utiliza en Java, comparten recursos tales como la memoria y la E/S, por lo que la dificultad fundamental a
la hora de escribir programas multihebra es la de coordinar el uso de estos recursos entre distintas tareas dirigidas por hebras,
de modo que no haya más de una tarea en cada momento que pueda acceder a un determinado recurso.
He aquí un ejemplo simple donde se utilizan procesos del sistema operativo: mientras yo escribía este libro, solía hacer múl-
tiples copias de seguridad redundantes del estado actual del libro. Hacía una copia en un directorio local, otra en un dispo-
sitivo USB, otra en un disco Zip y otra en un sitio FTP remoto. Para automatizar este proceso, escribí un pequeño programa,
(en Python, pero los conceptos son los mismos) que comprime el libro en un archivo, incluyendo un número de versión en
el nombre y luego realizaba las copias. Inicialmente, realizaba todas las copias secuencialmente, esperando a que cada una
se completara antes de dar comienzo a la siguiente. Pero entonces me di cuenta de que cada operación de copia req1?-ería una
cantidad de tiempo distinta, dependiendo de la velocidad de E/S del medio. Puesto que estaba utilizando un sistema opera-
tivo multitarea, podía iniciar cada operación de copia como UD proceso separado y dejarlas ejecutarse en paralelo, 10 que
aceleraba la ejecución completa del programa. Mientras que uno de los procesos estaba bloqueado otro podía continuar con
su tarea.
Éste es un ejemplo ideal de concurrencia. Cada tarea se ejecuta como un proceso en su propio espacio de direcciones, así
que no existe la posibilidad de interferencias entre las distintas tareas. Lo más imponante es que no hay ninguna necesidad
de que las tareas se comuniquen entre sÍ, porque todas ellas son completamente independientes. El sistema operativo se
encarga de todos los detalles necesarios para garanti zar que todos los archivos se copien apropiadamente. Como resultado,
no existe ningún riesgo y lo que obtenemos es un programa más rápido sin ningún coste adicional.
Algunos autores llevan incluso a defender que los procesos son la única solución razonable para la concurrencia,l pero
lamentablemente existen, por regla general, limitaciones relativas al número de procesos y al gasto administrativo adicional
asociado con cada uno que impiden que esa solución basada en procesos pueda aplicarse a todo el conjunto de problemas
de concurrencia.
1 Eric Raymond, por ejemplo, hace un encendida defensa de esta idea en rile Arf o/ UNIX Programming (Addison-Wesley, 2004).
730 Piensa en Java
Algunos lenguajes de programación están diseñados para aislar las tareas concurrentes unas de otras. Estos programas se
denominan, generalmente, lenguajes func ionales, y en ellos cada llamada a función no produce ningún efecto secundario
(no pudiendo así interferir con otras funciones) y se la pueda ejecutar como una tarea independiente. Erlang es uno de dichos
lenguajes e incluye mecanismos seguros para que una tarea se comunique con otra. Si nos encontramos con que una parte
de nuestro programa tiene que hacer un uso intensivo de la concurrencia y tropezamos con demasiados problemas a la hora
de desarrollar esa parte, podemos considerar como posible solución el escribir esa parte del programa en un lenguaje con-
currente dedicado, como Erlang.
Java adoptó la solución más tradicional que consiste en añadir el soporte para hebras por encima de un lenguaje secuencial. 2
En lugar de iniciar procesos externos en un sistema operativo multitarea, el mecanismo de hebras crea las distintas tareas
dentro de un único proceso, representado por el programa que se está ejecutando. Una de las ventajas que esta solución pro-
porciona es la transparencia con respecto al sistema operativo, que era uno de los principales objetivos de diseñ.o en Java.
Por ejemplo, las versiones pre-OSX del sistema operativo Macintosh (que era objetivo relativamente importante par. l.s
primeras versiones de Java) no soportaba la multitarea. Si no se hubiera añ.dido el mecanismo multihebra a Java, los pro-
gramas Java concurrentes no habrían podido portarse a Macintosh ni a otras platafonnas similares, incurriendo así en el
requisito de que los programas deberían "escribirse una vez y ejecutarse en todas partes". 3
2 Se podría argumentar que tratar de agregar la concurrencia a lUl lenguaje secuencial es un enfoque condenado al fracaso, pero que cada cual saque sus
propias conclusiones.
3 Este requisito nunca ha llegado a satisfacerse del todo y Sun ya no pone tanto énfasis en él. Irónicamente, una de las razones de que este requisito no lle-
gara a satisfacerse puede ser, precisamente, los problemas relativos al sistema de hebras, y puede que se hayan solventado en Java SES.
21 Concurrencia 731
lo largo de una red. En este caso, todos los procesos se ejecutan de forma completamente independiente unos de otros y no
existe ni siquiera la posibilidad de compartir recursos. Sin embargo, seguimos teniendo que sincronizar la transparencia de
información entre los distintos procesos, de modo que el sistema de mensajería, entendido como un todo, no pierda infor-
mación ni introduzca infonnación en los instantes incorrectos. Incluso si no pretende utilizar la concurrencia a menudo en
el futuro imnediato, resulta muy útil entender los conceptos implicados para poder comprender las arquitecturas de mensa-
jeria, que se están convirtiendo en la forma predominante de crear sistemas distribuidos.
La concurrencia tiene sus costes asociados, incluyendo la complejidad inherente a este tipo soluciones, pero estos costes son
más que compensados por las mejoras en el diseño del programa, por el equilibrado de recursos y por la mayor comodidad
de los usuarios. En general, las hebras nos permiten crear un diseño con un acoplamiento más débil; si no fuera por ellas,
determinadas partes de nuestro código se verían obligadas a prestar una atención explícita a determinadas actividades de
cuya gestión se encargan normalmente las hebras.
4 Esto es cierto cuando el sistema utiliza un mecanismo de franjas temporales (por ejemplo, Windows). Solaris utiliza un modelo de concurrencia basado
en \Ula cola FIFO; a menos que se despierte \Ula hebra de mayor prioridad, la hebra actual continuará ejecutándose hasta que se bloquee o termine. Eso
significa que otras hebras con la misma prioridad no podrán ejecutarse hasta que la hebra actual ceda el control de procesador.
732 Piensa en Java
El identificador id distingue entre múltiples instancias de la tarea. Es de tipo final porque no se espera que cambie una vez
que ha sido inicializado.
El método run( ) de una tarea suele tener algún tipo de bucle que continúa ejecutándose hasta que la tarea deja de ser nece-
saria, por lo que es preciso establecer la condición de salida de este bucle (una opción consiste simplemente en ejecutar una
instrucción return desde run( )). A menudo, run() se implementa en la forma de un bucle infmito, lo que quiere decir que
si no aparece un factor que haga que run() termine, este método continuará ejecutándose para siempre (posteriormente en
el capítulo veremos cómo terminar las tareas de manera segura).
La llamada al método estático Thread.yield( ) dentro de run( ) es una sugerencia para el planificador de hebras (la parte
del mecanismo de hebras de Java que conmuta el procesador de una hebra a la siguiente). Dicha sugerencia dice: "Acabo
de finalizar las partes importantes de mi ciclo y este sería un buen momento para conmutar a otra tarea durante un rato". Es
completamente opcional, pero utilizamos dicho método aquí porque tiende a producir una salida más interesante en estos
tipos de ejemplo: tenemos más probabilidades de ver cómo se cambia de unas tareas a otras.
En el siguiente ejemplo, el método run( ) de la tarea no está dirigido por una hebra separada, sino que simplemente se le
invoca desde main() (en realidad, sí que estamos usando una hebra: la que siempre se asigua a maine )):
JI: concu rrency f MainThread .java
j * OUtpu t ,
#0(9), #0(8), #0(7), #0(6) , #0 (5) , #0 (4) , #0(3 ) , #0(2 ), #0(1), #O(Lifto f fl),
*j jj,-
Cuando derivamos una clase de Runnable, debe tener un método run( ), pero esto no tiene nada de especial: no produce
ninguna capacidad innata de gestión de hebras. Para conseguir disponer del mecanismo de hebras tenemos que asociar explí-
citamente una tarea a una hebra.
La clase Thread
La forma tradicional de transfonnar un objeto Runnable en una tarea funcional consiste en entregárselo a un constructor
Thread (hebra). Este ejemplo muestra cómo asignar una hebra a un objeto LiftOff utilizando un objeto Thread:
ji : concurrencyj BasicThreads.java
/1 El uso más básico de la clase Thread .
/ * Ou t p u t: ( 90% match)
Wait i ng for Lift Off
#0 (9), #0(8), #0(7), # 0(6) , #0 (5) , #0 (4 ) , #0(3 ), #0(2) , #0(1), #O(Lif to ffl),
*jjj,-
21 Concurrencia 733
Un constructor Thread s610 necesita un objeto Runnable. Al invocar el método start( ) de un objeto Thread se realizará
la inicialización necesaria para la hebra y a continuación se invocará el método run( ) del objeto Runnable para iniciar la
tarea dentro de la nueva hebra.
Aún cuando start( ) parezca estar baciendo una llamada a un método de larga duración, podemos ver a la salida (el mensa-
je "Waiting for LiftOff' aparece antes de completarse la cuenta atrás) que start() vuelve rápidamente. En la práctica, hemos
hecho una llamada al método LiftOff.run(), y dicho método no ha finalizado todavía, pero como LiftOff.run() está sien-
do ejecutado por una hebra distinta, podemos continuar realizando otras operaciones en la hebra main() (esta capacidad no
está restringida a la hebra main(): cualquier hebra puede iniciar otra hebra). Así, el programa está ejecutando dos métodos
a la vez: main( ) y LiftOff.run( ). El método run( ) es el código que se ejecuta "simultáneamente" con las otras hebras del
programa.
Podemos añadir fácilmente más hebras para controlar más tareas. A continuación podemos ver cómo todas las tareas se eje-
cutan de manera concertada: 5
1/: concurrencyjMoreBasicThreads . java
f/ Adición de más hebras.
public c l as s Mo reBasicThreads
public s ta t ic vo i d main (St ring [] args )
for (i nt i "" O; i < Si i ++ }
new Thread(new LiftOff ()} .start(}¡
System.out.println(IIWaiting for LiftOff!1 ) i
/* Output, (Sample )
Waiting for LiftOff
#0 ( 9 ), #1 (9) , #2(9 ) . #3 ( 9), #4 (9 ) . #0(8), #1(8 ) . #2 ( 8),
#3(8), # 4 (8 ) , #0 (7 ), #1 (7), #2 ( 7), #3(7 ), #4(7 ) , # 0( 6),
#1 ( 6 ) , #2(6 ) , #3 ( 6) , #4 ( 6), #0(5), #1 (5), #2(5), #3 ( 5 ) ,
#4(5 ) , #0(4 ) , # 1( 4), #2(4 ) , # 3(4). #4(4 ) , #0(3 ), #1 (3 ) ,
#2(3) , #3 ( 3), #4 (3), #0(2), #1(2 ) , #2(2), #3 (2), # 4(2) ,
#0 (1), #1 (1), #2(1), #3 (1), #4(1), #0 (Lifto ff ! ),
#l(Liftoff!), # 2IL iftoff!), #3(Liftoff!), #4( L ift off !),
*///,-
La salida muestra que la ejecución de las diferentes tareas está entremezclada a medida que se conmuta de una tarea a otra.
El planificador de hebras controla esta conmutación de forma automática. Si tenemos múltiples procesadores en la máqui-
na, el procesador de hebras distribuirá de manera transparente las hebras entre los distintos procesadores.6
La salida de este programa será diferente en cada ejecución, porque el mecanismo de planificación de hebras no es deter-
minístico. De hecho, podemos ver enormes diferencias en la salida de este programa en una versión del JDK y en la siguien-
te. Por ejemplo, una versión anterior del JDK no efectuaba demasiado a menudo la conmutación de hebras, por lo que la
hebra 1 podía recorrer todas las pasadas del bucle hasta terminar, luego la hebra 2 completaría todas las pasadas de su bucle,
etc. En la práctica, esto equivalía a llamar a una rutina que realizara todos los bucles de manera consecutiva, salvo porque
el iniciar todas esas hebras resulta bastante más costoso. Las versiones anteriores del JDK parecen exhibir un mejor
comportamiento de asignación de franjas temporales, con lo que cada hebra parece recibir un servicio más regular.
Generalmente, estos tipos de cambios de comportamiento en el JDK no son mencionados por Sun, así que no podemos basar
nuestros planes en ninguna previsión coherente relativa al comportamiento del mecanismo de hebras. La mejor solución
consiste en ser lo más conservador posible a la hora de escribir código basado en hebras.
Cuando main() crea los objetos Thread, no está capturando las referencias de ninguno de ellos. Con un objeto normal, esto
haría que el objeto fuera candidato para la depuración de memoria, pero eso no sucede así con los objetos Thread. Cada
objeto Thread "se registra" a sí mismo, por lo que existe de hecho una referencia a ese objeto en algún lugar, y el depura-
dor de memoria no puede borrar el objeto hasta que la tarea salga de su método run( ) y termine. Podemos ver, analizando
, En este caso, una única hebra (main()) está creando todas las hebras LiftOff. Sin embargo, si tenemos múltiples hebras creando hebras LUtorr es posi-
ble que más de una hebra LIftOff tenga el mismo valor id . Posteriormente en el capítulo veremos cuál es la razón.
la salida, que todas las tareas se ejecutan efectivamente hasta su conclusión, por lo que una hebra crea una hebra de ejecu-
ción separada que persiste después de que se complete la llamada a start( ).
EjercIcio 1: (2) Implemente una clase Runnable. Dentro de run( ), imprima un mensaje y luego invoque yield( ).
Repita este proceso tres veces, y luego vuelva desde run( ). Ponga un mensaje de inicio en el constructor
y un mensaje de tennLnación cuando la tarea tennine. Cree varias de estas tareas y contrólelas utilizando
hebras.
Ejercicio 2: (2) Siguiendo la fonua de generies/Fibonaeci.java, cree una tarea que genere una secuencia de n núme-
ros de Fibonacci, donde n sea un parámetro proporcionado al constructor de la tarea. Cree varias de estas
tareas y contrólelas mediante hebras.
Utilización de Executor
Los Ejecutores java.util.eolleurrent de Java SE5 simplifican la programación concurrente, encargándose de gestionar los
objeto Thread por nosotros. Los ejecutores proporcionan un nivel de indirección entre un cliente y la ejecución de tIDa tarea,
en lugar de ejecutar una tarea directamente, hay un objeto intermedio que se encarga de ejecutar la tarea. Los ejecutores per-
miten gestionar la ejecución de tareas asíncronas sin tener que gestionar de manera explícita el ciclo de vida de las hebras.
Los ejecutores son el método preferido de inicio de tareas en Java SE5/6.
Podemos utilizar un ejecutor (Exeeutor) en lugar de crear explícitamente objetos Tbread en MoreBasicThreads.java. Un
objeto LiftOff sabe cómo ejecutar una tarea específica; al igual que el patrón de diseño Comando, expone un único méto-
do para ser ejecutado. Un objeto ExecutorService (un objeto Exeeulor con un ciclo de vida de servicio, por ejemplo, apa-
gar) sabe cómo construir el contexto apropiado para ejecutar objetos Runnable. En el siguiente ejemplo, el objeto
CaebedTbreadPooi crea una hebra por cada tarea. Observe que se crea un objeto ExeeutorServlcc utilizando un método
Exeeutors estático que detennina el tipo de objeto Exeeutor que tiene que ser:
//: concurrencyfCa chedThreadPool.java
import java.util .concurrent.*¡
1* Outpu t, ( Sample)
#0 (9), #0 (8) , #1 (9) , #2 (9 ) , #3 (9) , # 4 (9 ) , #0 (7), #1 (8) ,
#2 (8), #3 (8) , #4 (8) , #0 (6) , #1 (7) , #2 (7), #3 (7), #4 (7) ,
#0 (5) , #1 (6) , # 2 (6), #3 (6) , #4 (6), #0 (4) , #1 (5), #2 (5) ,
#3 (5 ) , #4 (5), #0 (3), #1 ( 4 ) , #2 (4 ) , #3 ( 4 ) , #4(4 ), #0 (2 ),
#1 (3) , #2 ( 3), #3 (3) , #4 (3 ), #0 (1) , # 1 (2) , #2 ( 2), #3 ( 2).
#4 (2) , #0 ( Liftoff 1) , #1( 1 ) , #2 ( 1) , #3 ( 1 ) , #4 (1 ) ,
#1 (Liftoff 1) , #2 (L iftoffl) , #3 (Liftoffl) , #4 (Liftof fl) ,
*11 1,-
A menudo, puede utilizarse un único Exeeulor para crear y gestionar todas las tareas del sistema.
La llamada a sbutdown( ) impide que se envíen nuevas tareas a ese objeto Exeeutor. La hebra actual (en este caso, la que
controla main(» continuará ejecutando todas las tareas enviadas antes de shuldown(). El programa finalizará en cuanto
fmalice todas las tareas del objeto Exceutor.
Podemos sustituir fácilmente el objeto CaebedThreadPool del ejemplo anterior por un tipo diferente de Exeeutor. Un obje-
to FixedTbreadPool utiliza un conjunto limitado de hebras para ejecutar las tareas indicadas:
1/: concurrency/FixedTh re adPool .java
import java.util.concurrent.*¡
} /* Output, (Samp1e)
#0 (9) , #0 (8) , #1 (9) , #2 (9) , #3 (9) , #4 (9) , #0 (7), #1 (8),
#2 (8) , #3 (8) , #4 (8) f #0 (6) , #1 (7) , #2 (7) , # 3 (7), #4 (7),
#0 (5) , #1 (6), #2 (6) , #3 (6) , #4 (6), #0 (4 ) , #1 (5), #2 (5) ,
#3 (5) , #4 (5), #0 (3 ) , #1 (4), #2 (4), #3 (4 ) , #4 ( 4), #0 (2),
#1 ( 3), #2(3 ) , #3 (3 ) , #4 (3), #0 (1 ) , #1 (2 ) , #2 (2 ) , #3 (2 ) ,
#4 (2 ), #0 ILif t off ! ) , #1 ( 1 ) , #2 ( 1), #3 ( 1) , #4 (1) ,
#1 ILifto ff! ) , #2 ( Lif toff!) , #3 (Liftoff! ) , #4 (Liftoff! ) ,
* /// ,-
Con FixedTbreadPool, realizamos la costosa asignación de hebras una única vez, por adelantado, con lo que limitamos el
número de hebras. Esto pennite ahorrar tiempo, porque no tenemos que pagar constantemente el coste asociado a la crea-
ción de una hebra para cada tarea individual. Asimismo, en un sistema dirigido por sucesos, las rutinas de tratamiento de
sucesos que requieren hebras pueden ser servidas con la rapidez que queramos, extrayendo simplemente hebras de ese con-
junto preasignado. Con esta solución, no podemos agotar los recursos disponibles porque el objeto FixcdThreadPool utili-
za un número limitado de objetos Thread.
Observe que en cualquiera de los dos tipos de conjuntos de hebras que hemos examinado, las hebras existentes se reutilizan
de manera automática siempre que sea posible.
Aunque en este libro utilizaremos conjuntos de hebras de tipo CachedThreadPool, también puede considerar utilizar
FixedThreadPool en el código de producción. CachedTbre.dPool creará generalmente tantas hebras como necesite duran-
te la ejecución de un programa y luego dejará de crear nuevas hebras a medida que vaya pudiendo reciclar las antignas, por
lo que resulta razonable elegir en primer lugar este tipo de objeto Executor. Sólo si esta técnica causa problemas necesita-
remos cambiar a un conjunto de hebras de tipo FixedTbrcadPool.
Un ejecutor SingleThreadExecntor es como FlxedThreadPool pero con un tamaño de mÍa única hebra7 Esto resulta útil
para cualquier cosa que queramos ejecutar de manera continua en otra hebra (una tarea de larga duración), como por ejem-
plo una tarea que se dedique a escnchar a las conexiones socket entrantes. También es útil para tareas de corta duración que
queramos ejecutar dentro de una hebra, por ejemplo, un registro de sucesos local o remoto, o también para una hebra que
se emplee para despachar sucesos.
Si se envía más de una tarea a un ejecutor SingleThreadExecutor, las tareas se pondrán en cola y cada una de ellas se eje-
cutará hasta completarse antes de qne se inicie la signiente tarea, utilizando todas ellas la misma hebra. En el siguiente ejem-
plo, podemos ver cómo cada tarea se completa en el orden en que fue enviada antes de que dé comienzo la siguiente. Así,
un ejecutor SingleTbreadExecutor serializa las tareas que se le envían y mantiene su propia cola (oculta) de tareas pen-
dientes.
/ 1: concurrency/SingleThreadExecutor.java
i mport java.util.concurrent.*;
1* Output:
7 También ofrece una importante garantía de concurrencia que los otros tipos de ejecutores no ofrecen: no se pueden invocar conClUTcntemente dos tareas.
Esto hace que cambien los requisitos de bloqueo de las tareas (hablaremos sobre el bloqueo más adelante en el capítulo).
736 Piensa en Java
exec.shutdown() i
1* Output:
result of TaskWi thResul t o
result of TaskWithResult 1
result of TaskWi thResul t 2
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
result of TaskWithResult 6
result of TaskWithResult 7
resu lt of TaskWithResu lt 8
resu l t of TaskWithResu l t 9
*111 ,-
El método submit( ) produce un objeto Future, parametrizado para el tipo particular de resultado devuelto por el objeto
Callable. Podemos consultar el objeto Future con isDone( ) para ver si se ha completado. Cuando la tarea se ha completa-
do y dispone de un resultado, podemos invocar get( ) para extraer éste. También podemos simplemente invocar get( ) sin
comprobar isDone( ), en cuyo caso get( ) se bloqueará hasta que el resultado esté listo. Podemos asimismo invocar get( )
con una temporización, o invocar isDone( ) para ver si la tarea se ha completado, antes de tratar de llamar a get( ) para
extraer el resultado.
El método sobrecargado Executors.callable( ) toma un objeto Runnable y produce un objeto Callable. ExecutorService
tiene ~lgunos métodos de "invocación" que ejecutan colecciones de objetos CaUable.
Ejercicio 5: (2) Modifique el Ejercicio 2 de modo que la tarea sea un objeto Callable que sume los valores de todos
los número de Fibonacci. Cree varias tareas y muestre los resultados.
catch(InterruptedException el {
System.err.println(IlInterrupted rl ) ;
1* Output :
738 Piensa en Java
Prioridad
La prioridad de una hebra le indica al planificador la importancia de esa hebra. Aunque el orden en que el procesador eje-
cuta una serie de hebras es indeterminado, el planificador tenderá a ejecutar primero la hebra en espera que tenga la mayor
prioridad. Sin embargo, esto no significa que las hebras con una menor prioridad no se ejecuten (asi que es imposible que
se produzca un interbloqueo debido a las prioridades). Simplemente, las hebras de menor prioridad tienden a ejecutarse
menos a menudo.
La inmensa mayoría de las veces, todas las hebras deberían ejecutarse con la prioridad predeterminada. Tratar de manipu-
lar las pIioridades de las hebras suele ser un error.
He aquí UD ejemplo que ilustra los niveles de prioridad. Podemos leer la prioridad de una hebra existente con getPriority( )
y cambiarla en cualquier momento con setPriority( ).
11 : concurrency/ SimplePriorities.j a va
II Muestra el u so de las prioridades de l a s hebras.
import j a va .ut i l . concur rent .*;
publi c vo id r un() {
Thread.cur r entThr ea d( ) .set Prior i ty {priority) i
21 Concurrencia 739
while (true) {
/1 Una operación costosa, interrumpible:
for(int i =; 1; i < 100000; i++} {
d +0 (Math.PI + Math.E) / (double)i;
if(i % 1000 00 O)
Thread.yield() ;
System.out.println(this) i
if{--countDown ==; O) return;
toString() se sustituye para utilizar Thread.toString(), que imprime el nombre de la hebra, el nivel de prioridad y el "grupo
de hebras" al que la hebra pertenece. Podemos establecer el nombre de la hebras nosotros mismos a través del constructor;
aquí se generan automáticamente como pool-l-thread-l, pool-l-thread-2, etc. El método toString( ) sustituido también
muestra el valor de cuenta atrás de la tarea. Observe que podemos obtener, dentro de una tarea, una referencia al objeto
Thread que está dirigiendo la tarea, invocando Thread.currentThread( ).
Podemos ver que el nivel de prioridad de la última hebra es el más alto, y que el resto de las hebras tienen el nivel más bajo.
Observe que la prioridad se fija al comienzo de run( ); fijar la prioridad en el constructor no sería adecuado, ya que el obje-
to Executor no ha comenzado la tarea en dicho instante.
Dentro de run( ), se realizan 100.000 repeticiones de un cálculo en coma flotante bastante costoso, que implica la suma y
división de valores douhle. La variable d es de tipo volatile para tratar de garantizar que no se realicen optimizaciones del
compilador. Sin este cálculo, no podríamos ver el efecto de establecer los niveles de prioridad (inténtelo: desactive median-
te un comentario el bucle for que contiene los cálculos de tipo double). Con el cálculo, podemos ver que la hebra con
MAX_PRIORITY recibe una preferencia mayor por parte del planificador de hebras (al menos, éste era el comportamien-
to en la máquina Windows XP). Aún cuando imprimir en la consola también representa un comportamiento relativamente
costoso, no es posible percibir los niveles de prioridad de esa forma, porque la impresión en la consola no puede verse inte-
rrumpida (en caso contrario, la visualización en la consola mostraría mensajes entremezclados al emplear hebras), mientras
que los cálculos matemáticos sí que se pueden interrumpir. Los cálculos duran lo suficiente como para que el mecanismo
de planificación intervenga, conmute dos tareas y preste atención a las prioridades, de modo que las hebras de alta priori-
dad obtienen preferencia. Sin embargo, para garantizar que se produzca un cambio de contexto, se invocan regulannente
instrucciones yield( ).
Aunque el JDK tiene 10 niveles de prioridad, este número no se corresponde excesivamente bien con los mecanismos uti-
lizados por muchos sistemas operativos. Por ejemplo, Windows tiene 7 niveles de prioridad que no son fijos, por 10 que esa
740 Piensa en Java
correspondencia es indeterminada en el caso de este sistema operativo. El sistema Solaris de Sun tiene 23 \ niveles. El único
enfoque portable consiste en limitarse a utilizar MAX]RIORITY, NORM]RIORITY y MIN]RIORITY a la hora de
ajustar los niveles de prioridad.
Hebras demonio
Una hebra "demonio" pretende proporcionar un servicio general en segundo plano mientras el programa se ejecuta, pero sin
formar parte de la esencia del programa. Por tanto, cuando todas las hebras no demonio se completan, el programa se ter-
mina, terminando también durante el proceso todas las hebras demonio. A la inversa, si existe alguna hebra no demonio que
todavia se esté ejecutando, el prograroa no puede tenninar. Existe, por ejemplo, una hebra no demonio que ejecuta el méto-
do mai n().
/ * Output, (Sample )
Al l daemons started
Thread [Thread - O, S ,main] Si mp leDa e mons@S30daa
Thread[Thread - l,5, main] SimpleDaemons@a62fc3
ThreadtThre ad - 2,S,main] Simpl e Daemons@89 aege
Thread [Thread-3, S,main] SimpleDaemons@1270b73
Thread [Thr ead - 4,S , ma i n ] S i mp leDaemo n s@60a e bO
21 Concurrencia 741
*///, -
Debemos definir la hebra como demonio invocando sctDacmon( ) antes de iniciarla.
No hay nada que impida al programa terminar una vez que main() finaliza su trabajo, ya que lo único que queda ejecután-
dose son hebras demonio. Para poder ver el resultado de iniciar todas las hebras demonio, la hebra main( ) se pone breve-
mente a dormir. Sin esto, sólo veríamos parte de los resultados de la creación de las hebras demonio (pruebe a realizar
llamadas a sleep() de diversas duraciones para ver este comportamiento).
SimpleDaemons.java crea objetos Tbread explícitos para poder activar el indicador que los define como hebras demonio.
Se pueden personalizar los atributos (demonio, prioridad, nombre) de las hebras creadas por objetos Executor escribiendo
una factoría ThreadFactory personalizada:
/1 : net/mi ndview/ util jDaemonThreadFactory.java
package net.mindview.util;
impo rt java. util.concurrent.*i
La única diferencia con respecto a un objeto ThreadFactory normal es que éste asigna el valor true al indicador que
identifica las hebras demonio. Ahora podemos pasar un nuevo objeto DaemonThreadFactory como argumento a
Executors.newCachedThreadPool( ):
11 : concurrency / DaemonFromFactory.java
II Ut ilizac i6n de una fac t oría de heb ras para crear demonios.
impor t java.util.concurrent.*;
i mpor t ne t . mindview.util.*;
impo rt stat ic net.mindview.util.Print.*¡
Cada uno de los métodos de creación estáticos ExecutorService se sobrecarga para tomar un objeto T hreadFactory que se
utilizará para crear nuevas hebras.
Podemos llevar este enfoque un paso más allá y crear una utilidad DaemonThreadPoolExecutor:
JI: net/mindview /util/ DaemonThreadPoolExecutor . java
package net.mindview.util¡
import j ava . uti l.concurrent.*¡
publi c c l as s Daemons {
publ i c static void main(String [) args) throws Exception {
Thread d = new Thread {n ew Daemon ( ) ) ¡
d. s etDaemon ( true) ;
d . star t () ¡
pri ntnb(lId .isDaemon() = 11 + d.isDaemon() + ti , II};
II Permi tir que l as hebras demonio finalicen
II sus procesos de arranque:
TimeUnit.SECONDS.sl eep(l ) ;
21 Concurrencia 743
1* Output: (Sample)
d.isDaemon() = true, DaemonSpawn o started, DaemonSpawn 1
started, DaemonSpawn 2 started, DaemonSpawn 3 started,
DaemonSpawn 4 started, DaemonSpawn 5 started, DaemonSpawn 6
started, DaemonSpawn 7 started, DaemonSpawn 8 started,
DaemonSpawn 9 started, trOJ ,isDaemon() = true,
t [1] . isDaemon () true, t [2] . isDaemon() true,
t[3] .isDaemon() true, t [4] .isDaemon() true,
t[5] . isDaemon() true, t [6] . isDaemon () true,
t [7J .isDaemon() true, t [8] . isDaemon () true,
t[9] . isDaemon() true,
*///,-
La hebra Daemon se configura en modo demonio. A continuación, esa hebra inicia una serie de otras hebras (que no se defi-
nen explícitamente como demonios), para demostrar de todos modos que esas hebras serán de tipo demonio. A continua-
ción, Daemon entra en un bucle infinito que llama a yield( ) para ceder el control a los otros procesos.
Es necesario tener en cuenta que las hebras demonio terminarán sus métodos run() sin ejecutar cláusulas finaUy:
ji: concurrency/DaemonsDontRunFinally.java
ji Las hebras demonio no ejecutan la cláusula finally
import java.util.concurrent.*i
import static net.mindview.util.Print.*¡
j* Output:
Starting ADaemon
*///,-
Cuando ejecutamos este programa, vemos que la cláusula finaUy no se ejecuta, pero si desactivamos mediante comentarios
la llamada a setDaemon( ), la cláusula fmally sí que se ejecutará.
Este comportamiento es correcto, aún cuando resulte algo inesperado, teniendo en cuenta las explicaciones dadas antes para
fmaUy. Los demonios se terminan "abruptamente" cuando termina la última de las hebras no demonio. Por tanto, en cuan-
to salimos de main( ), la máquina NM termina todos los demonios inmediatamente sin ninguna de las formalidades que
cabria esperar. Dado que no se pueden fmalizar los demonios de una manera limpia, las hebras demonio no suelen ser muy
convenientes. Generalmente, resulta mejor emplear objetos Executor no demonio, ya que todas las tareas controladas por
un objeto Executor pueden terminarse de una sola vez. Como veremos posterionnente en el capítulo, esa terminación tiene
lugar, en este caso, de una manera ordenada.
Ejercicio 7: (2) Experimente con diferentes tiempos de dormir en Daemons.java, para ver lo que sucede.
744 Piensa en Java
Ejercicio 8: ( 1) Modifique MoreBasicThreads.java para que todas las hebras sean de tipo demonio y el programa ter-
mine en cuanto maine ) sea capaz de terminar.
Ejercicio 9: (3) Modifique SimplePriorities.java para que una fac toría personalizada TbreadFactory establezca las
prioridades de todas las hebras.
Variaciones de código
Eu los ejemplos que hemos visto hasta ahora, todas las clases de tareas implementan la interfaz Runnable. En algrmos casos
muy simples, podemos utilizar la técnica alternativa de heredar directamente de Thread, como en este ejemplo:
/1 : concurrency/SimpleThread . java
// Herencia directa de la clase Thread .
1* Output:
#1(5), #1(4), #1(3), #1(2), #1(1 ), #2 ( 5), #2(4 ) , #2(3),
# 2(2), # 2(1) , #3(5), #3(4), #3(3), #3(2), # 3 (1), #4(5),
#4(4), #4(3), #4(2), #4(1), #5(5), # 5(4), #5(3), #5(2),
#5 (1),
*/// :-
Proporcionarnos a los objetos Thread nombres específicos invocando el constructor Thread apropiado. Este nombre se
extrae en toString() utilizando getName().
Otra estructura de código que puede que se encuentre alguna vez es la del objeto Runnable auto-gestionado:
11 : concurrencyl Se lfManaged. j ava
II Un objet o Runnab le que con tiene su propia hebra directora.
public class SelfManaged implements Runnable
private int countDown = Si
private Thread t = new Thread(this );
public SelfManaged () { t. start (); }
pub l ic String toString () {
return Thread . currentThread () .getName() +
11 (I! + countDown + ") f ";
System.out.print(th is) ;
if (- -countDown ~= O)
ret urn ;
1* Output,
Thread- O(5) , Thread-O (4), Thread- Q(3) , Thread - O(2) , Thread-O (1 ) ,
Thread - l (S) , Thread-l (4), Thread-l (3) , Thread- l(2) , Thread-l (l ) ,
Thread-2 (S) , Thread-2 (4) I Th read-2 (3), Threa d-2 (2) , Thread-2 (1) ,
Thread- 3(S) , Thread-3 (4) , Th read - 3(3) , Thread - 3 (2), Thread-3 (1 ) ,
Thread -4 (5) , Thread -4 (4) , Thread- 4 (3), Thread- 4 (2 ) , Thread- 4(1) ,
* /11 ,-
Esta técnica no difiere especialmente de la de heredar de Tbread, salvo porque la sintaxis es ligeramente más abstrusa. Sin
embargo, implementar una interfaz nos permite heredar de una clase distinta, cosa que no se puede hacer si heredamos desde
Thread.
Observe que start( ) se invoca dentro del constructor. Este ejemplo es bastante simple y resulta, por tanto, probablemente
seguro, pero hay que tener en cuenta que iniciar hebras dentro de un constructor puede resultar bastante problemático, por-
que otra tarea podría empezar a ejecutarse antes de que el constructor se haya completado, lo que quiere decir que la tarea
pudiera ser capaz de acceder al objeto en un estado inestable. Ésta es otra razón adicional de preferir objetos Executor a la
creación explícita de objetos Thread.
En ocasiones, resulta conveniente ocultar el código de gestión de hebras dentro de la clase utilizando una clase interna, como
se muestra a continuación:
JI: concur rency/ThreadVariations.java
jI Creación de h ebras con c lases internas.
import java.util.concurrent.*¡
i mport static net . mi ndv iew .uti l .Print .*¡
catch( InterruptedException e ) {
print (11 i n terrupted" ) ;
catch(InterruptedException el
print (I! sleep () interrupted 11) ;
catch(InterruptedException e)
print (11 sleep () interrupted 11) ;
private Thread t i
public InnerRunnable2(String name)
t = new Thread(new Runnable()
public void run() {
try {
while (true)
print(this) j
if(--countDown == O) return;
TimeUnit.MILLISECONDS.s!eep(lOl j
catch(InterruptedException el
print("sleep() interrupted U ) i
catch(InterruptedException e)
print (I! sleep () interrupted ") ¡
InnerThreadl crea una clase interna nominada que amplía Thread y crea una instancia de esta clase interna dentro del
constructor. Esto tiene sentido si la clase interna dispone de capacidades especiales (nuevos métodos) a los que necesitamos
acceder desde otros métodos. Sin embargo, la mayor parte de las veces la razón para crear una hebra es únicamente utilizar
las capacidades de la clase Thread, por lo que no es necesario crear una clase interna nominada. InnerThread2 muestra
cuál es la alternativa: dentro del constructor se crea illla subclase interna anónima de Thread y se la generaliza a una refe-
rencia t a Thread. Si otros métodos de la clase necesitan acceder a t, pueden hacerlo a través de la interfaz Thread, y no
necesitan conocer el tipo exacto del objeto.
La tercera y cuarta clases del ejemplo repiten las dos primeras clases, pero en lugar de utilizar la interfaz Runnable em-
plean la clase Thread.
La clase ThreadMethod muestra la creación de una hebra dentro de un método. Si invocamos el método una vez que este-
mos listos para ejecutar la hebra, el método termina antes de que la hebra dé comienzo. Si la hebra sólo está realizando una
operación auxiliar en lugar de alguna otra cosa más fundamental para la clase, probablemente este enfoque resulte más útil
y apropiado que iniciar una hebra dentro del constructor de la clase.
Ejercicio 10: (4) Modifique el Ejercicio 5 de acuerdo con el ejemplo de la clase ThreadMethod, de modo que
runTask( ) tome un argumento que especifique la cantidad de números de Fibonacci que hay que sumar,
y que cada vez que invoquemos runTask( ) devuelva el objeto Future producido por la llamada a
submit( ).
Terminología
Como muestra la sección anterior, existen diversas alternativas a la hora de implementar programas concurrentes en Java, y
dichas alternativas pueden resultar confusas. A menudo, el problema procede de la terminología empleada a la hora de des-
cribir la tecnología de programas concurrentes, especialmente en aquellos casos que hay implicadas hebras.
A estas alturas, deberla ya entender que existe una distinción entre la tarea que se está ejecutando y la hebra que la dirige;
esta distinción resulta especialmente clara en las bibliotecas Java, porque realmente no tenemos ningún control sobre la clase
Thread (y esta separación es todavía más clara con los ejecutores, que se encargan de crear y gestionar las hebras por noso-
tros). Lo que hacemos es crear una tarea y asociar una hebra con cada tarea, de modo que la hebra se encargue de dirigirla.
En Java, la clase Thread no hace nada por sí misma. Se limita a dirigir la tarea que se le indique. A pesar de ello, sobre los
mecanismos de gestión de hebras, suele utilizar expresiones como "la hebra realiza esta acción o esta otra". La nnpresión
que se obtiene al leer esto es que la hebra es la tarea, y cuando yo tropecé con las hebras de Java, esta impresión era tan
fuerte que para mí existía una relación de tipo "es-un" muy clara, y de 10 cual yo deducía que era necesario heredar una tarea
de un objeto Thread. Añadamos a esto la inadecuada elección del nombre de la interfaz Runnable (ejecutable), que debe-
ría haberse denominado, mucho más apropiadamente "Task" (tarea).
El problema es que los niveles de abstracción están mezclados. Conceptualmente, queremos crear una tarea que se ejecute
independientemente de otras tareas, por 10 que deberíamos poder defmir una tarea y decir "ejecutar" sin preocupamos de
los detalles. Pero físicamente las hebras pueden resultar muy costosas de crear, por 10 que es necesario conservarlas y ges-
tionarlas adecuadamente. Por tanto, tiene sentido, desde el punto de vista de la implementación, separar las tareas de las
hebras. Además, el mecanismo de hebras en Java está basado en la solución de pthreads de bajo nivel proveniente de e, que
es una solución en la que es necesario swnergirse y en la que es preciso entender todos los detalles de lo que está ocurrien-
do. Parte de esta naturaleza de bajo nivel ha terminado deslizándose en la implementación Java, por 10 que para permane-
cer en un nivel mayor de abstracción, es necesario ser disciplinado a la hora de escribir el código (trataremos de mostrar esa
disciplina a 10 largo del capítulo).
Para clarificar estas explicaciones, trataré de utilizar el término "tarea" cuando me refiera al trabajo que hay que realizar y
"hebra" únicamente a la hora de referirme al mecanismo específico que se ocupa de dirigir la tarea. Por tanto, si estamos
analizando un sistema en un nivel conceptual podemos limitamos a emplear el término "tarea" sin necesidad de mencionar
en absoluto el mecanismo encargado de dirigir esa tarea.
También podemos invocar join( ) con un argumento de fin de temporización (en milisegundos o en milisegundos y nanose-
gundos), de modo que si la hebra objetivo no finaliza en dicho período de tiempo, la llamada a joio( ) vuelve de todos
modos.
La llamada a join( ) puede abortarse invocando ioterrupt( ) sobre la hebra invocante, para lo que hace falta una cláusula
try-catch.
Todas estas operaciones se ilustran en el siguiente ejemplo:
ji: concurrency/Joining.java
II Ejemplo de join().
import static net.mindview . util.Print.*¡
1* Output ,
750 Piensa en Java
class UnresponsiveUI
private volatile double d = 1¡
public UnresponsiveUI() throws Exception
while(d> O)
d = d + (Math.PI + Math.E) I d;
System.in.read(); II Nunca llega aquí
UnresponsiveUI realiza un cálculo dentro de un bucle while infmito, por lo que nunca, obviamente, alcanza la línea de
entrada de datos de la consola (hemos engañado al compilador para que piense que la línea de entrada es alcanzable utili-
zando el while condicional). Si desactivamos mediante un comentario la línea que crea una interfaz UnresponsiveUI, ten-
dremos que matar el proceso para poder salir.
Para hacer que el programa tenga una adecuada capacidad de respuesta, hay que colocar el cálculo dentro de un método
run( ) para permitir que se lo pueda desalojar del procesador, y cuando pulsemos la tecla Intro, veremos que el cálculo ha
estado ejecutándose en segundo plano mientras se esperaba a que se produjera la entrada del usuario.
Grupos de hebras
Un grupo de hebras mantiene una colección de hebras. La ventaja de los grupos de hebras puede resumirse citando las pala-
bras de Joshua Bloch, 8 el arquitecto de software que, mientras estaba en Sun, corrigió y mejoró enormemente la biblioteca
de colecciones de Java en el JDK 1.2 (entre otras contribuciones):
"Los grupos de hebras podrían definirse como un experimento que no tuvo éxito, así que se puede sim-
plemente ignorar su existencia ".
Si el lector ha invertido tiempo y esfuerzo tratando de comprender el valor de los grupos de hebras (como es mi caso), podría
preguntarse por qué no se ha producido ningún anuncio más oficial de Sun acerca de este tema: la misma cuestión podría
plantearse acerca de varios otros cambios que Java ha sufrido a lo largo de los años. El economista Joseph Stiglitz laurea-
do con el Premio Nobel tiene una filosofia de la vida que pod...ría aplicarse aquí. 9 Se denomina La teoría del compromiso
delegado:
"El coste de continuar con los errores es soportado por otros, mientras que el coste de admitirlos es
soportado por nosotros. "
Captura de excepciones
Debido a la naturaleza de las hebras no podemos capturar una excepción que haya escapado de una hebra. Una vez que una
excepción sale del método run( ) de una tarea, no se propagará hacia la consola a menos que adoptemos medidas especia-
les para capturar esas excepciones "vagabundas". Antes de Java SES, se utilizaban los grupos de hebras para capturar estas
excepciones, pero con Java SES podemos resolver el problema mediante objetos Executor, por lo que deja de ser necesa-
rio saber nada acerca de los grupos de hebras (salvo para entender el código heredado, véase Thinking in Java, 2" Edición,
descargable de www.MindView.net. para conocer más detalles sobre los grupos de hebras).
He aquí una tarea que siempre genera una excepción que se propaga fuera de su método run( ) y un método maine ) que
muestra lo que sucede al ejecutarlo:
jj: concurrencyjExceptionThread.java
II {ThrowsException}
import java.util.concurrent.*;
9 Y en varias otras ocasiones relacionadas con la utilización de Java. Bueno, en realidad, ¿por qué detener esto? En el pasado he prestado labores de con-
sultoría en bastantes proyectos en los que esta filosofía era aplicable.
752 Piensa en Java
Esto produce el mismo resultado que el ejemplo anterior: una excepción no capturada.
Para resolver el problema, cambiemos la forma en que el objeto Executor produce las hebras. Tbread.UncaugbtExcep-
tionHaodler es una nueva interfaz en Java SES; que nos pernlite asociar IDla rutina de tratamiento de excepciones a cada
objeto Tbread. Tbread.UncaughtExceptionHandler.nncaugbtException() se invoca automáticamente cuando esa hebra
está a punto de morir debido a una excpeción no capturada. Para usarla, crearnos un nuevo tipo de factoría ThreadFactory
que asocia un nuevo objeto Tbread.UncaugbtExceptionHandler a cada nuevo objeto Tbread que crea Pasamos dicha
fac toría al método Executors que crea un nuevo objeto ExecutorService:
11 : con currency/Cap tureUnca ugh t Exception.java
import java.util . concurrent .*¡
t .setUncaughtExceptionHandl er {
new MyUncaugh tExceptionHandler {» i
System.ou t.println (
fleh = n + t . getUncaughtExceptionHandle r ( }) ;
re turn t i
/* Output:
caught j ava .l ang .RuntimeException
*///,-
Esta rutina de tratamiento sólo se invoca si no existe una rutina de tratamiento de excepciones no capturadas para la hebra
concreta. El sistema comprueba si la hebra dispone de una rutina y, en caso contrario, mira a ver si el grupo de hebras espe-
cializa su método uncaughtException(); en caso contrario, invoca la rutina predeterminada defaultUncaughtException-
Handler .
Compartición de recursos
Podemos pensar en un programa que tenga una sola hebra como si fuera una entidad solitaria que se mueve en nuestro espa-
cio de problema y que hace una sola cosa cada vez. Puesto que s6lo hay una entidad, no tenemos que pensar en el proble-
ma de que dos entidades intenten utilizar el mismo recurso al mismo tiempo: problemas que son similares al caso de dos
personas que intentaran aparcar en el mismo sitio, que estuvieran tratando de pasar por una misma puerta al tiempo o que
estuvieran incluso intentando hablar simultáneamente.
Con la concurrencia, no tienen por qué existir entidades solitarias, sino que tenemos la posibilidad de que haya dos o más
tareas interfi riendo entre sí. Si no evitamos las colisiones, podemos encontramos con que las dos tareas traten de acceder a
la misma cuenta corriente al mismo tiempo, imprimir en la misma impresora, ajustar la misma válvula, etc.
754 Piensa en Java
public vo i d r un()
while(!generator.isCanceled() )
int val = generator.next()¡
if(val % 2 != O) {
System.out .println (va l + " no t even! " ) ;
g en erato r. c a nce l ( ) ; II Cancel a todos los ob jetos EvenCh ecker
Observe que en este ejemplo la clase que puede cancelarse no es de tipo Runnable. En su lugar, todas las tareas
EvenCbecker que dependen del objeto lntGenerator lo comprueban para ver si ha sido cancelado, Como podemos ver en
run(). De esta forma, las tareas que comparten un recurso común (el objeto IntGenerator) observan dicho recurso para
ver la señal de terminación. Esto elimina las denominadas condiciones de carrera, en las que dos o más tareas compiten
para responder a una condición y, por tanto, colisionan o producen de alguna otra manera resultados incoherentes. Es nece-
sario pensar con cuidado todas las posibles formas en que un sistema concurrente puede fallar y protegerse frente a ellas.
Por ejemplo, una tarea no puede depender de otra tarea porque el orden de terminación de las tareas no está garantizado.
Aquí, haciendo que las tareas dependan de un objeto que no es una tarea, eliminamos esa potencial condición de carrera.
El método testO prepara y realiza una prueba de cualquier tipo de IntGenerator, iniciando una serie de objetos
EvenChecker que usan el mismo objeto IntGenerator. Si IntGenerator provoca un fallo, test() informará de él y volve-
rá. En caso contrario, es necesario pulsar Control-C para terminar el método.
Las tareas EvenChecker están constantemente leyendo y probando los valores de su objeto IntGenerator asociado.
Observe que si generator.isCanceled( ) es true, run( ) vuelve, lo que dice al objeto Executor en EvenChecker.test( ) que
la tarea está completa. Cualquier tarea EvenCbecker puede invocar cancele ) sobre su objeto IntGcnerator asociado, lo
que hará que todas las tareas EvenChecker que estén usando IntGencrator terminen de manera grácil. En secciones pos-
teriores, veremos que Java ofrece mecanismos más generales que estos para la terminación de las hebras.
El primer objeto IntGenerator que vamos a examinar dispone de un método next( ) que produce un. serie de valores pares:
/ * Output, (S amp l e )
Press Con tro l - C to exi t
894 7 6 9 93 not even !
89476993 not e v e n!
*///, -
Resulta posible que Wla tarea invoque next( ) después de que otra tarea haya realizado el primer incremento de
currentEvenValue pero no el segundo <en el lugar del código que tiene el comentario " iPunto de peligro!"). Esto hace que
el valor quede en un estado "incorrecto" . Para probar que esto puede suceder, EvenChecker.test() crea un grupo de obje-
tos EvenChecker para leer continuamente la salida de un objeto EvenGenerator y ver que cada valor es par. Si no lo es,
se informa del error y el programa termina.
Este programa terminará por fallar, porque las tareas EvenChecker tienen que acceder a la información de EvenGenerator
mientras que esa infonnació n está en un estado "incorrecto". Sin embargo, puede que el problema no se detecte hasta que
el objeto EvenGenerator haya completado muchos ciclos, dependiendo de las particularidades de nuestro sistema operati-
vo y de otros detalles de implementación. Si queremos ver mucho más rápido cómo falla el programa, podemos incluir una
llamada a yield() entre el primer y el segundo incremento. Esto es parte del problema con los programas multihebra: pue-
den parecer correctos a pesar que existe un error, siempre y cuando la probabilidad de fallo sea baj a.
Es importante comprobar que la propia operación de incremento requiere múltiples pasos y que la tarea puede ser suspen-
dida por el mecanismo de gestión de hebras en mitad de una operación de incremento; en otras palabras, el incremento no
es una operación atómica en Java. Así que, incluso no sería seguro realizar una operación de incremento sin proteger la
correspondiente tarea.
756 Piensa en Java
Una tarea puede adquirir el bloqueo de un objeto múltiples veces. Esto sucede si un método invoca un segundo método sobre
el mismo objeto, que a su vez invoque otro método sobre el mismo objeto, etc. La máquina NM lleva la cuenta del núme-
ro de veces que el objeto ha sido bloqueado. Si el objeto está desbloqueado, ese valor de recuento será igual a cero. Cuando
una tarea adquiere un bloqueo por primera vez, el valor de recuento pasa a ser uno. Cada vez que la misma tarea adquiere
otro bloqueo sobre el mismo objeto, se incrementa el valor de recuento. Naturalmente, esa adquisición múltiple de bloqueos
sólo está permitida para la tarea que haya adquirido el bloqueo en primer lugar. Cada vez que la tarea abandona un método
synchronized, el valor de recuento se decrementa, hasta que llegne a valer cero, lo que hace que se libere el bloqueo com-
pletamente y que pueda ser usado por otras tareas.
También existe un único bloqueo por clase (como parte del objeto Cia •• de dicha clase), de modo que los métodos synchro-
nized estáticos pueden impedir que otros métodos similares accedan simultáneamente a los datos estáticos de la clase.
¿Cuándo debemos efectuar una sincronización? Le recomendamos que aplique la Regla de Brian de la sincronización: 10
Si estamos escribiendo una variable que pueda ser leída a continuación por otra hebra, o leyendo
una variable que pueda haber sido escrita en último lugar por otra hebra, es necesario utilizar la sin-
cronización; además, tanto el lector como el escritor deben sincronizarse usando el mismo bloqueo
monitor.
Si tenemos más de un método en nuestra clase que trate con los datos críticos, será necesario sincronizar todos los métodos
relevantes. Si sólo sincronizamos uno de los métodos, entonces los otros podrán ignorar el bloqueo del objeto y podrán ser
invocados con impunidad. Éste es un punto muy importante: todo método que acceda a un recurso compartido crítico debe-
rá estar sincronizado, porque sino el programa no funcionará.
Sincronización de EvenGenerator
Añadiendo synchronized a EvenGenerator.java, podemos impedir los accesos no deseados de las hebras:
public c lass
SynchronizedEvenGenerator extends IntGenerator
private int current EvenValue = O;
public synchronized i nt next ( ) {
++currentEvenVa l ue¡
Thread.yield () ; II Provoca e l fal l o más rápidamente
++current EvenValue¡
return currentEvenValue;
lO En bonor de Brian Goetz, autor de Java Concurrency hl Practice, por Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes y Doug
Lea (Addison-Wesley, 2006).
758 Piensa en Java
Ejercicio 11: (3) eree una clase que contenga dos campos de datos y un método que manipule dichos campos en un pro-
ceso multipaso, y que durante la ejecución de dicho método, dichos campos pasen por "estados incorrec-
tos" (de acuerdo con una cierta definición que usted mismo puede establecer), Añada métodos para leer
los campos y cree múltiples hebras para invocar los distintos métodos y mostrar que los datos están visi-
bles en su "estado incorrecto". Corrija el problema empleando la palabra clave synchronized.
1* Output:
tryLock () , t r ue
t ryLock (2, Ti meUnit.SECONDS ) : true
a cquired
t ryLock () , fals e
tryLock(2 , Ti me Oni t . SECONDS) : fals e
* /// , -
Un bloqueo ReentrantLock nos permite intentar adquirir el bloqueo sin éxito por 10 que si alguien más ha adquirido el blo-
queo, podemos decidir renunciar a adquirirlo por el momento y hacer alguna otra cosa mientras en lugar de esperar a que
se libere, como podemos ver en el método ulllimed( ). En tlmed( ), se realiza un intento de adquirir el bloqueo que puede
fallar después de 2 segundos (observe el uso de la clase TimeVolt de Java SES para especificar las unidades), En main(),
se crea un objeto Thread separado como una clase anónima y éste adquiere el bloqueo para que los métodos untimed( ) y
timed( ) tengan algo con lo que trabajar,
El objeto Lock explícito también nos proporciona un control de granularidad masiva sobre el bloqueo y el desbloqueo de
lo que peanite el bloqueo synchronized integrado. Esto resulta útil para implementar estructuras de sincronización especia-
lizadas, tales como por ejemplo la de bloqueo mano a mano (también conocida como acoplamiento de bloqueos), utilizada
760 Piensa en Java
para recorrer los nodos de una lista enlazada, el código que recorre la lista debe capturar el bloqueo del nodo siguiente antes
de liberar el bloqueo del nodo actual.
Atomicidad y volatilidad
Una idea incorrecta pero que se repite a menudo en las explicaciones sobre el mecanismo de hebras de Java es "las opera-
ciones atómicas no necesitan sincronizarse". Una operación atómica es aquella que no puede ser interrumpida por el plani-
ficador de hebras: si la operación se inicia, entonces continuará ejecutándose hasta completarse, sin que pueda producirse
un cambio de contexto durante el proceso. Confiar en la atomicidad resulta peligroso: sólo debemos tratar de emplear la ato-
micidad en lugar de la sincronización si somos auténticos expertos en concurrencia o si disponemos de ayuda de uno de tales
expertos. Si considera que es lo suficientemente hábil como para jugar con fuego, haga esta prueba:
La Prueba de Goetz ll : si eres capaz de escribir una máquina NM de altas prestaciones para un único
procesador moderno, entonces podrás comenzar a pensar si puedes ahorrarte la sincronización. 12
Resulta útil saber acerca de la atomicidad, y saber también que ésta se utilizó, junto con otras técnicas avanzadas, para
implementar algunos de los componentes más inteligentes de la biblioteca java.util.concurrent. Pero no caiga en la tenta-
ción de depender usted mismo de la atomicidad; tenga siempre presente la Regla de Brian de la sincronización, presentada
anteriormente.
La atomicidad se aplica a las "operaciones simples" sobre los tipos primitivos, excepto para valores long y double. Leer y
escribir variables primitivas de long y double es, de manera garantizada, una operación de acceso hacia y desde la memo-
ria absolutamente indivisible (atómica). Sin embargo. la máquina NM está autorizada a realizar lecturas y escrituras de
valores de 64 bits (variables long y double) como dos operaciones de 32 bits separadas, haciendo surgir la posibilidad de
que se produzca un cambio de contexto en mitad de una lectura o escritura, con lo que diferentes tareas podrían ver resul-
tados incorrectos (esto se denomina en ocasiones desgajamiento de palabra. porque existe la posibilidad de ver el valor des-
pués de que sólo se haya cambiado una parte del mismo). Sin embargo, sí que tenemos atomicidad (para asignaciones y
devoluciones simples) si utilizamos la palabra clave volatile al definir una variable long o double (observe que volatile no
funcionaba adecuadamente antes de Java SES). Las diferentes máquinas NM son libres de proporcionar garantias más fuer-
tes, pero tenga en cuenta que no se debe confiar en las características que sean específicas de determinadas plataformas.
Por tanto, las operaciones atómicas no son interrumpibles por el mecanismo de gestión de hebras. Los programadores exper-
tos pueden aprovechar esto para escribir código libre de bloqueos, que no necesita sincronizarse. Pero incluso esto es una
simplificación excesiva. En ocasiones, aún cuando parezca que una operación atómica debería ser segura, puede que no lo
sea. Los lectores de este libro no podrán, normalmente, pasar la Prueba de Goetz antes mencionada, por lo que no deberian
pensar en sustituir la sincronización por operaciones atómicas. Tratar de eliminar la sincronización es realmente un signo de
optimización prematura, que generará una gran cantidad de problemas, probablemente, sin ganar nada a cambio, o muy
poco.
En los sistema multiprocesador (que ahora están apareciendo en la forma de procesadores multinúcleo: múltiples procesa-
dores en un mismo chip), la visibilidad más que la atomicidad suele ser mucho más importante que en los sistemas de un
solo procesador. Los cambios realizados por una tarea, incluso si son atómicos en el sentido de que no son interrumpibles,
puede que no sean visibles para otras tareas (los cambios deben estar almacenados temporalmente en una caché del proce-
sador local, por ejemplo), de modo que diferentes tareas tendrán una visión distinta del estado de la aplicación. El mecanis-
mo de sincronización, por el contrario, fuerza a que los cambios realizados por una tarea en un sistema multiprocesador sean
visibles para toda la aplicación. Sin la sincronización no resulta posible determinar cuándo serán visibles los cambios.
La palabra clave volatile también asegura la visibilidad para toda la aplicación. Si declaramos que un campo es de tipo
volatile, esto quiere decir que, tan pronto como se realice una escritura en dicho campo, todas las lecturas podrán ver el cam-
bio. Esto es cierto incluso si se utilizan cachés locales: los campos volátiles se escriben inmediatamente en la memoria prin-
cipal y las lecturas se realizan también en la memoria principal.
11 Llamada así por el antes mencionado Brian Goetz, un experto en concurrencia que me ha ayudado a elaborar este capítulo, que está parcialmente basa-
do en algunos comentarios que me hizo.
12 Un corolario de esta prueba es: "Si alguien sugiere que la gestión de hebras es sencilla, asegúrate de que esa persona no tenga bajo sus responsabilida-
des el tomar decisiones importantes acerca del proyecto. Si esa persona está en disposición de tomar esas decisiones, tienes un problema".
21 Concurrencia 761
Es importante entender que la atomicidad y la volatibilidad son conceptos distintos. Una operación atómica en un campo no
volátil no será necesariamente volcada en la memoria principal, por lo que otra tarea que lea dicho campo no tendría por
qué, necesariamente, ver el nuevo valor. Si hay múltiples tareas accediendo a un campo, dicho campo debe ser de tipo volá-
til; en caso contrario, sólo debería accederse al campo utilizando la sincronización. La sincronización también provoca el
volcado en memoria principal, por lo que si un campo está completamente protegido por bloques o métodos sincronizados,
no es necesario defmido como de tipo volátil.
Cualquier escritura que una tarea realice será visible para dicha tarea, por lo que no es necesario un campo volátil si ese
campo sólo es consultado dentro de una tarea.
La palabra volatile no funciona cuando el valor de un campo depende de su valor anterior (por ejemplo, el incrementar un
contador), ni tampoco funciona con aquellos campos cuyos valores están restringidos por los valores de otros campos, como
por ejemplo, los límites inferior (Iower) y superior (upper) de una clase Range (rango) que deba obedecer la restricción
lower <= uppor.
Nonnalmente, sólo resulta seguro volatile en lugar de sYllchronized si la clase sólo tiene un campo modificable. De nuevo,
nuestra primera opción debería ser emplear la palabra clave syncbronized; éste es el enfoque más seguro e intentar hacer
cualquier otra cosa resulta arriesgado.
¿Qué cosas pueden llegar a ser operaciones atómicas? La asignación y la devolución del valor de un campo serán normal-
mente atómicas. Sin embargo, en e++ incluso las siguientes instrucciones podrían ser atómicas:
i++¡ JI Podría ser atómica en c++
i += 2 ¡ /1 Podría ser atómica en c+ +
Pero en C++, esto depende del compilador y del procesador. No podemos escribir código inter-platafonna en C++ que
dependa de la atomicidad, porque C++ no tiene un modelo de memoria coherente, a diferencia de Java (en Java SE5)13
En Java) las operaciones anteriores son, sin ninguna duda, no atómicas, como se puede ver analizando las instrucciones JVM
generadas por los siguientes métodos:
1/ : con currency/Atomici ty . j ava
11 {Exec , javap - e Atomicity }
public c l ass Atomi city
int i¡
vo i d El () { i ++; }
void f 2 () { i +" 3;
1* Output , (Sample )
void fl () ;
Code,
°,
1,
aload O
dup
2, getfield #2; IICampo i,r
5, iconst 1
6, iadd
7, putfield #2; II Campo i,r
10 , re t urn
voi d f2 () ;
Cede:
°,
1,
a load O
dup
2, getfield #2; II Campo i,r
5, iconst 3
6, i add
7, putfield #2; IICampo i,r
10, return
*111, -
13 Esto se está tratando de remediar en el siguiente estándar e++ que se va a publicar.
762 Piensa en Java
Cada instrucción produce una operación "get" y una operación "puf', con instrucciones entremedias. Por tanto, entre el ins-
tante de obtener el valor y el instante de almacenar el valor corregido, otra tarea podría modificar el campo, así que las ope-
raciones no son atómicas.
Si aplicamos ciegamente la idea de le atomicidad, podemos ver que getValue( ) eu el siguieute programa se ajusta a la des-
cripción:
JJ: concu rrencyJAtomicityTest .java
import java. ut i l . concurrent.*;
p ubl ic c l ass AtomicityTest i mp l ements Runnable {
private in t i = O;
publ ic int getValue () { return i i }
private synchronized v oi d evenlncrement() i++¡ i++¡
public void run () {
wh i l e (true )
evenInc rement {) ¡
/* Output, (Samp1e)
1 9158 376 7
* /// , -
Sin embargo, el programa encontrará valores no pares y terminará. Aunque return i es ciertamente una operación atómica,
la falta de sincronización permite que el valor se lea mientras que el objeto se encuentra en un estado intennedio inestable.
Además, puesto que i también es de tipo volátil, habrá problemas de visibilidad. Tanto getValue( ) como evenlncrement( )
deben ser sincronizados. S610 los expertos en concurrencia deberían intentar realizar optimizaciones en situaciones como
ésta. De nuevo, recuerde siempre la Regla de Brian de la sincronización.
Como segundo ejemplo vamos a considerar algo todavía más simple: una clase que produce números de serie. 14 Cada vez
que se llama a uextSeriaINumber( ), debe devolver un valor unívoco alllamante:
JJ : concurrencyJSer ial NumberGene rat or.java
public class SerialNumber Generator {
pri vate static volatile i nt serial Number = O;
public sta tic int nextSerialNumber () {
re turn seri a lNumber ++¡ /J No es seguro con l as h ebras
}
/ / / o-
SerialNumberGenerator es una clase de lo más simple que podríamos imaginar, y para cualquiera que proceda de C+t o
que tenga alguna otra experiencia previa similar de bajo nivel, cabría esperar que la de incremento fuera una operación ató-
mica, porque un incremento en C++ a menudo puede implementarse como una instrucción de microprocesador (aunque no
en una fomla inter-platafonna~ fiable). Sin embargo, como hemos indicado antes, una operación incremento en Java no es
atómica e implica tanto una lectura como una escritura, por lo que existen problemas con las hebras incluso en operaciones
tan simples como ésta. Como veremos, el problema aquí uo es la volatilidad, el problema real es que nextSeriaINumber()
accede a un valor compartido y mutable sin sincronización.
14 Ejemplo inspirado en el libro EjJective JavaTMProgram ming Language Guide de Joshua Bloch (Addison·Wesley, 2001), p. 190.
21 Concurrencia 763
El campo serialNumber es volátil porque es posible que cada hebra tenga una pila local y mantenga en ella copias de algu-
nas variables. Si defInimos una variable como volátil, esto le dice al compilador que no realice ninguna optimización que
pudiera eliminar las lecturas y escrituras que mantienen al campo en sincronización exacta con los datos locales almacena-
dos en las hebras. En la práctica, las lecturas y escrituras se realizan directamente en memoria y no se almacenan valores en
caché. La palabra clave volatile también restringe la posibilidad de que el compilador realice reordenaciones de los accesos
durante la optimización. Sin embargo, volatile no afecta al hecho de que la de incremento no es una operación atómica.
Básicamente, debemos defmir un campo como volatile si hay varias tareas que pueden acceder a la vez a dicho campo y al
menos uno de esos accesos es una escritura. Por ejemplo, un campo que se utilice como indicador para detener una tarea
debe ser declarado como de tipo volatile; en caso contrario, dicho indicador podría estar almacenado en un registro de caché,
y cuando se hicieran cambios en el indicador desde fuera de la tarea, el valor de la caché no sería modificado y la tarea nunca
sabría que debe detenerse.
Para probar SerialNumberGenerator, necesitamos un conjunto que no se quede sin memoria, por si acaso se tarda un largo
tiempo en detectar el problema. El conjunto CircularSet mostrado aquí reutiliza la memoria usada para almacenar valores
enteros, en la suposición de que para cuando volvamos al principio del conjunto, la posibilidad de una colisión con los valo-
res sobreescritos será mínima. Los métodos add() y contains() están sincronizados para impedir las colisiones entre hebras:
11: concurrency/SerialNumberChecker.java
II Determinadas operaciones que pueden parecer seguras no lo son
II cuando hay hebras presentes.
II {Args, 4}
import java.util.concurrent.*;
int se rial
Ser ialNumberGenerato r .next8erialNumber() ;
if (serials . contains (s erial» {
System . out.println(uDuplicate: n + serial) i
System.exit(O) ;
public s ta tic void main(S tri ng( ] args } throws Exc ept i on {
for (int i = O; i < SIZE; i++ )
exec.execute(new Seria lChecker(» i
/1 Detenerse después de n segundos si hay un argumento:
if(args.length> 01 {
TimeUni t .SECONDS .sleep( new Integer(args [O]» ;
Syst em. out .println (liNo dup licates detected ll ) i
System .exit (O) ;
/ * OUtput, (Sampl el
Dupl ic ate : 84 686 56
*/// , -
SerialNumberChecker contiene un conjunto estático CircularSet que almacena todos los números de serie que se han pro-
ducido y una clase SerialChecker anidada que garantiza que los números de serie sean univocos. Creando múltiples tareas
para contender con el acceso a los núm.eros de serie, descubriremos que las tareas llegan a obtener un número de serie dupli·
cado, si dejamos que el programa se ejecute el tiempo suficiente. Para resolver el problema, añada la palabra clave synchro-
nized a nextSerialNumber( ).
Las operaciones atómicas que se supone que son seguras son las de lectura y asignación de primitivas. Sin embargo, como
hemos visto en AtomicityTest.java, sigue siendo posible utilizar una operación atómica que acceda a nuestro objeto, mien·
tras que éste se encuentre en un estado intennedio inestable. Realizar suposiciones acerca de este tema resulta muy peligro-
so. Lo mejor que puede hacerse es aplicar la Regla de Brian de la sincronización.
Ejercicio 12: (3) Corrija AtomicityTest.java utilizando la palabra clave synchronized. ¿Puede demostrar que ahora es
correcto?
EjercIcio 13: (1) Corrija SeriaINumberChecker.java utilizando la palabra clave synchronized. ¿Puede demostrar que
ahora es correcto?
Clases atómicas
Java SE5 introduce clases de variables atómicas especiales como Atomiclnteger, AtomicLong, AtomicRefel'ence, etc.,
que proporcionan una operación condicional de actualización atómica de la fonna:
boolean compareAndSe t(expectedValue, updat eVa l ue ) ;
Este ejemplo de método compararia y actualizaria en una operación atómica una detenninada variable, proporcionándose
como argumentos el valor esperado y el valor de actualización. Este tipo de método se utiliza para realizar una optimiza-
ción avanzada, aprovechando la atomicidad del nivel de la máquina disponible en algunos procesadores modernos, por lo
que generalmente no tenemos por qué preocupamos de utilizarlos. En ocasiones, pueden resultar útiles para los programas
nonnales, pero, de nuevo, sólo cuando se esté intentando realizar una optimización. Por ejemplo, podemos escribir de nuevo
AtomicityTest.java para utilizar AtomicInteger:
jj : concurrencyjAtomi c l ntegerTest.java
i mpor t j a va.util .concur rent .*;
import java.util.concurrent.atomic.*¡
impo rt java. u til.*¡
pubI ic cI as s AtomiclntegerTest implements RunnabIe {
21 Concurrencia 765
Aqui hemos eliminado la palabra clave synchronized utilizando en su lugar Atomiclntcgcr. Puesto que el programa no
falla, hemos añadido un objeto Timer para abortar automáticamente la ejecución después de 5 segundos.
He aquí MutexEvenGenerator.java reescrito para utilizar Atomiclnleger:
JI : concurrency/AtomicEvenGenerator.java
/ 1 Las c lases atómi cas son útiles e n ocasiones en los programas normales .
II {RunByHand}
import java.ut il .concurrent.atomic . *¡
public cIass AtomicEv enGene ra tor extends IntGenerat or
privat e Atomiclnteger currentEvenValu e
new Atomiclnteger{Q) ;
p u blic int n ext () (
ret urn currentEvenValue . addAndGet(2);
Secciones críticas
En ocasiones, lo único que queremos evitar es que múltiples hebras accedan a parte del código dentro de un método, en lugar
de al método completo. La sección de código que queramos aislar de esta manera se denomina sección critica y se crea uti-
766 Piensa en Java
lizando la palabra clave synchronized. Aquí, syncbronized se utiliza para especificar el objeto cuyo bloqueo se está em-
pleando para sincronizar el código incluido en la sincronización:
s ynchr onized (syncOb j ect ) {
JI En cada momento , s610 una tarea puede
// a cceder a este código
Esto se denomina también bloqueo sincronizado; 10 que quiere decir que antes de poder entrar en esa sección de código, hay
que adquirir el bloqueo para syncObject. Si alguna otra tarea ha adquirido ya este bloqueo, entonces no podrá entrarse en
la sección crítica hasta que dicho bloqueo se libere.
El siguiente ejemplo compara ambas técnicas de sincronización, demostrando que el tiempo disponible para que otras ta-
reas accedan a urí objeto se incrementa significativamente empleando un bloque synchronized en lugar de sincronizar el
método completo. Además, muestra cÓmo se puede utilizar una clase no protegida en entornos multibebra si se la controla
y protege mediante otra clase:
1/ : concurrency( Cr i t icalSect ion.j ava
/1 Sincronización de bloque en lugar de métodos completos. También
// il ustra la protecci ón de una clase n o protegida mediante
JI o t ra clase protegida.
pac kage concu rrenc Yi
i mpo rt java.util.concurrent .*;
import java .ut i l. concurrent,at omic,*¡
import java.uti l.*¡
II Inva riante arbit rari o ambas vari ables deben ser iguales:
publ ic void checkState{)
if(x ! = y)
throw new PairVal uesNotEqualExc ept i on ( ) ;
s t ore (temp ) ;
public vo i d run ()
while (true ) {
pm.checkCounter.incrementAndGet() ;
pm.getPair() .checkState() j
768 Piensa en Java
System. out. println (npm1: 11 .,. pro1 .,. n\npm2 : n .,. pm2);
System.exit(O) i
PairManipulator se crea para probar los dos diferentes tipos de objetos PairManager, invocando increment() en una
tarea mientras se ejecuta la comprobación PairChecker desde otra tarea. Para ver con qué frecuencia es capaz de ejecutar
la proeba, PairChecker incrementa checkCounter cada vez que tiene éxito. En main(), se crean dos objetos Pair-
Manipulator y se les permite ejecutarse durante un tiempo, después de lo cual se muestran los resultados de cada objeto
PairManipulator.
Aunque probablemente pueda percibir una gran variación a la salida entre una ejecución del programa y la siguiente, en
general verá que PairManagerl.increment() no permite a PairChecker unos accesos tan frecuentes como a Pair-
Manager2.increment( ), que tiene el bloque synchronized y goza, por taoto, de un mayor tiempo con los bloqueos libera-
dos. Ésta es, normalmente, la razón para utilizar un bloque syncbronized en lugar de la sincronización del método completo:
para permitir que las otras tareas puedan acceder más frecuentemente (siempre y cuando sea seguro hacerlo).
También se pueden usar objetos Lock explícitos para crear secciones críticas:
JI: concurrency/Explici t CriticalSection .java
JI USO de objetos Lock expl ícitos para crear secc i ones críticas.
package concurrencYi
import java.ut il.concur rent.l ocks.*;
// S i nc ronizar e l método completo:
c l ass Explic itPairManagerl extends Pai rManager
p r ivate Lock l ock = new Reent ran tLock () ¡
public synchron ized void increment ( ) {
lock .lock () ;
try (
p.incrementx() ¡
p.incrementY() ¡
store {ge t Pair ()) i
Einally (
lock.un l ock () ;
store (temp ) ¡
/* Output, (Sampl e)
pm1: pai r : x: 15, y : 15 checkCounter 1740 35
pm2: pair: x : 16, y: 16 checkCounter 26 08588
*///,-
770 Piensa en Java
Este programa reutiliza la mayor parte de CriticalSection.java y crea nuevos tipos de PairManager que utilizan objetos
Lock explícitos. ExplícitPairManager2 muestra la creación de una sección critica mediante un objeto Lock; la llamada a
store( ) se encuentra fuera de la sección crítica.
public void 9 () {
synchronized (syncObject )
for(int i = O; i < S; i++ )
print (119 () 11) i
Thread. yield 1) ;
) /* Output, ISample)
gl)
fl)
g il
fl)
g ()
f ()
gl )
fl)
g()
f()
*///,-
21 Concurrencia 771
DuaISync.f() se sincroniza con Ihis (sincronizando el método completo), y g() tiene un bloque synchronized que se sin-
croniza con syncObject. Así, las dos sincronizaciones son independientes. Esto se iluslra en main() creando un objeto
Thread que invoca f( ). La hebra en main( ) se utiliza para llamar a g( ). Podemos ver, analizando la salida, que ambos
métodos se ejecutan al mismo tiempo, así que ninguno de ellos está bloqueado por la sincronización del otro.
Ejercicio 15: (1) Cree una clase con tres métodos que contengan secciones críticas, y que todas se sincronicen con el
mismo objeto. Cree múltiples tareas para demostrar que sólo uno de estos métodos puede ejecutarse cada
vez. Ahora, modifique los métodos de modo que cada uno de ellos se sincronice con un objeto distinto y
muestre que los tres métodos pueden ejecutarse simultáneamente.
Ejercicio 16: (1) Modifique el Ejercicio 15 para usar objetos Lock explícitos.
*///, -
Los objetos ThreadLocal normalmente se almacenan como campos estáticos. Cuando creamos el objeto TbreadLocal, sólo
podemos acceder al contenido del objeto con los métodos get() y set( ). E l método get( ) devuelve una copia del objeto aso-
ciado con dicha hebra, mientras que set( ) inserta su argumento en el objeto almacenado para dicha hebra, devolviendo el
objeto anterior que estuviera almacenado. Los métodos increment() y get() ilustran este mecanismo en TbreadLocal-
VariableHolder. Observe que incremcnt( ) y get( ) no están sincronizados, porque ThreadLocal garantiza que no se pro-
duzca ninguna condición de carrera.
Cuando ejecute este programa, podrá ver que a cada hebra individual se le asigna su propio espacio de ahnacenamiento, ya
que cada una de ellas mantiene su propio valor de recuento, aún cuando sólo existe un objeto ThreadLocalVariableHolder.
Terminación de tareas
En algunos de los ejemplos anteriores, los métodos cancel( ) e isCanceled( ) se colocan en una clase que es visible para
todas las tareas. Las tareas comprueban isCanceled( ) para determinar cuándo deben fmalizar. Esta solución resulta bastan-
te razonable. Sin embargo. en algunas situaciones, la tarea debe terminarse de manera más abrupta. En esta sección, vere-
mos qué problemas existen con respecto a dicha finalización.
En primer lugar, veamos un ejemplo que no sólo ilustra el problema de la terminación, sino que también es un ejemplo adi-
cional de la compartición de recursos.
El jardín ornamental
En esta simulación, el comité director del jardín quiere saber cuántas personas entran el jardín cada día a través de las dis-
tintas puertas. Cada puerta tiene un tomo o algún otro tipo de contador, y después de incrementar el contador del tomo, se
incrementa una cuenta compartida que representa el número total de personas que hay en el jardín.
11: concurrencylOrnamentalGarden.java
i mport java.util. concurrent.*;
import java .ut i l. *¡
i mpo r t static net.mindview.ut i l .Print.*¡
clas s Count {
private int c ount = Di
pr ivate Random rand = new Random (47);
II Elimi nar la palabra clave synchro n ized para ver cómo falla el recuento:
public synchronized int increment () {
int temp = count ¡
if( rand.nextBool ean( )) II Seguir el control la mitad d e l tiemp o
Thread.yield() ¡
21 Concurrencia 773
print("Stopping + this);
/ * Output: (Sample)
Entrance O, 1 To ta l: 1
Entrance 2, 1 To tal: 3
Entrance 1, 1 To tal: 2
Entrance 4, 1 Total : 5
Entrance 3, 1 Tota l : 4
Bn tra nce 2, 2 Total: 6
Entrance 4, 2 To t a l : 7
Entrance O, 2 To ta l: 8
A medida que se ejecuta este programa, podemos ver cómo se muestran el recuento total y el recuento correspondiente a
cada entrada a medida que las personas pasan por los tomos. Si eliminamos la declaración synchronized de Count.incre-
ment( ), observaremos que el número total de personas no es el esperado. El número de personas contabilizadas por cada
tomo será distinto del valor almacenado en couot. Mientras utilicemos el mutex para sincronizar el acceso a Count, las
cosas sucederán correctamente. Recuerde que Count.increment( ) exagera la probabilidad de fallos, utilizando temp y
yield( ). En los problemas reales de los programas multihebra, la posibilidad de fallo puede ser estadísticamente pequeña,
así que podemos caer fácilmente en la trampa de pensar que las cosas están funcionando correctamente. Al igual que en el
ejemplo anterior, resulta posible que existan problemas ocultos que no se nos hayan ocurrido, por 10 que debe tratar de ser
lo más diligente posible a la hora de revisar el código concurrente.
Ejercicio 17: (2) Cree un contador de radiaciones que pueda tener cualquier número de sensores remotos.
Formas de bloquearse
Una tarea puede llegar a estar bloqueada por las signientes razones:
• Hemos mandado la tarea a dormir invocando sleep(milliseconds), en cuyo caso no podrá ejecutarse durante el
tiempo especificado.
• Hemos suspendido la ejecución de la hebra con wait(). No volverá a ser ejecutable de nuevo hasta que la hebra
reciba el mensaje notify( ) o notifyAll( ) (o los mensajes equivalentes signal( ) o signalAll() para las herramien-
tas de la biblioteca de Java SE5 java.util.concurrent). Examinaremos este caso en una sección posterior.
• La tarea está esperando a que se complete una operación de E/S.
• La tarea está tratando de invocar un método synchronized sobre otro objeto y el bloqueo no está disponible por-
que ya ha sido adquirido por otra tarea.
En los programas antignos, puede que también vea que se usan los métodos suspende ) y resume( ) para bloquear y des-
bloquear las hebras, pero estos métodos son desaconsejados en los programas Java modernos (porque son proclives a los
776 Piensa en Java
interbloqueos), así que no los examinaremos en el libro. También se desaconseja el método stop( ), porque no libera los blo-
queos que la hebra haya adquirido, y si los objetos están en un estado incoherente ("dañados"), otras tareas podrían consul-
tarlos y modificarlos en dicho estado. Los problemas resultantes pueden ser muy sutiles y dificiles de detectar.
El problema que ahora debemos examinar es el siguiente: en ocasiones, queremos terminar una tarea que se encuentra en el
estado bloqueado. Si no podemos esperar a que la hebra alcance un punto en el código en el que ella misma pueda compro-
bar un valor de estado y decidir tenninar por cuenta propia, tenernos que forzar a que la tarea salga de su estado bloqueado.
Interrupción
Como puede imaginarse, resulta mucho más complicado salir en mitad de un método Runnable.run( ) que esperar a que
ese método alcance un pWlto donde se compruebe un indicador de "cancelación", o algún otro lugar en el que el programa-
dor esté listo para abandonar el método. Cuando salimos abruptamente de una tarea bloqueada, puede que tengamos que
efectuar tareas de limpieza de los recursos. Debido a esto, salir en mitad del método run( ) de una tarea se parece más a
generar una excepción que a ninguna otra cosa, por lo que en las hebras Java se utilizan las excepciones para este tipo de
interrupción 16 (esto significa que estamos caminando sobre el filo que separa el uso adecuado e inadecuado de las excep-
ciones, porque quiere decir que a menudo se las emplea para control del flujo del ejecución). Para volver a un estado acep-
table conocido cuando se termina una tarea de esta forma, es necesario analizar con cuidado los caminos de ejecución del
código y escribir la cláusula catch para limpiar adecuadamente las cosas.
Para poder tenninar una tarea bloqueada, la clase Thread contiene el método interrupt( ). Este método activa el indicador
de estado interrumpido para la hebra. Una hebra que tenga activado el indicador de estado interrumpido generará una inte-
rrupción InterruptedException si ya está bloqueada o si se trata de realizar una operación de bloqueo. El indicador de esta-
do interrumpido será reinicializado cuando se genere la excepción o si la tarea llama a Thread.interrupted( ). Como puede
ver, Thread.interrupted() proporciona una segunda forma de abandonar el bucle run( ), sin generar una excepción.
Para invocar interrupt( ), es necesario disponer de un objeto Thread. Puede que el lector haya observado que la nueva
biblioteca concurrent parece evitar la manipulación directa de objetos Thread y trata en su lugar de realizar todas las ta-
reas a través de objetos Executors. Si llamamos a shutdownNow( ) sobre un objeto Executor, se generará una llamada
interrupt( ) dirigida a cada una de las hebras que el ejecutor haya iniciado. Esto tiene bastante sentido, porque norrnahnen-
te querremos terminar de una sola vez todas las tareas para Wl ejecutor concreto, cuando hayamos finalizado parte de un
programa o el programa completo. Sin embargo, hay veces en las que puede que sólo queramos interrumpir una única tarea.
Si estamos usando ejecutores, podemos obtener información sobre el contexto de una tarea en el momento de iniciarla lla-
mando a submit() en lugar de a execute(). submit() devuelve un genérico Future<?>, con un parámetro no especificado
(porque nunca se va a llamar get( ) para ese parámetro); el motivo de guardar este tipo de objeto Future es que se puede
invocar cancel( ) sobre el objeto y usarlo para interrumpir una tarea completa. Si pasamos el valor true a cancel( ), esta
hebra tendrá permiso para invocar interrupt( ) sobre dicha hebra, con el fin de detenerla; por tanto cancel( ) es una forma
de interrumpir hebras individuales iniciadas mediante un ejecutor.
He aquí un ejemplo que muestra los fundamentos básicos de utilización de interrupt( ) empleando ejecutores:
11: concurrency/Interrupting.java
II Interrupción de una hebra bloqueada.
import java.util.concurrent.*¡
import java.io.*¡
import static net.mindview.util.Print.*;
16 Sin embargo, las excepciones nunca se generan asíncronamente. Por tanto, no hay ningún riesgo de que algo se interrumpa en mitad de la ejecución de
una instrucción o de una llamada a método. Y, siempre que utilicemos la estructura try~finally al utilizar objetos mutex (en lugar de la palabra clave
synchronized), esos mutex serán liberados automáticamente si se genera una excepción.
21 Concurrencia 777
publi c SynchronizedBlocked()
new Thread () {
public void run ()
f(); JI Bl oqueo adquirido por esta hebra
)
) . start () ;
17 A lgunas versiones del JDK también proporcionaban soporte para la instrucción InterruptedIOException. Sin embargo, este soporte s610 estaba par-
cialmente implementado, y únicamente en algunas plataformas. Si se genera esta excepción hace que los objetos de EIS sean inutilizables. Resulta poco
probable que las futuras versiones sigan proporcionando soporte para esta excepción.
21 Concurrencia 779
1* Output: (Sample)
Waiting tor read() in NIOBlocked@7a84e4
Waiting tor read() in NIOBlocked@15c7850
ClosedBylnterruptException
Exiting NIOBlocked.run() NIOBlocked@15c7850
AsynchronousCloseException
Exiting NIOBlocked.run() NIOBlocked@7a84e4
*///,-
Como se muestra, también podemos cerrar el canal subyacente para liberar el bloqueo, aunque esto sólo debería ser nece-
sario en raras ocasiones. Observe que utilizando execute() para iniciar ambas tareas e invocar e.shutdownNow() permite
tenninar fácilmente cualquier cosa. La captura del objeto Future en el ejemplo anterior sólo era necesaria para enviar la
interrupción a una hebra y no a la otra. 18
Ejercicio 18: (2) Cree una clase que no sea de tipo tarea con un método que llame a sleep() durante un intervalo de larga
duración. Cree una tarea que llame al método contenido en la clase que haya definido. En main( ), inicie
la tarea y luego llame a interrupt( ) para terminarla. Asegúrese que la tarea termina de manera segura.
Ejercicio 19: (4) Modifique OrnamentalGarden.java para que utilice interrupt().
Ejercicio 20: (1) Modifique CachedThreadPool.java para que todas las tareas reciban una interrupción (con inte-
rrnpt()) antes de completarse.
public vo i d run () (
multiLock ,fl(lO) j
}
}.start ();
1* Output :
n () cal ling f2 () with count 9
f2 () cal ling n () wi th count 8
n() call ing f2 O with count 7
f2 () call i ng no wi th count 6
n I) cal ling f 2 1) wi th count 5
f2 O calling n () with count 4
n () cal ling f2 O with count 3
f21) call i ng no with count 2
n() call ing f2 () with count 1
f2 () c a lling fll) wi th c o unt o
*111 : -
En main( ), se crea no objeto Thread para invocar O(); luego O( ) y f2() se llaman el uno al otro hasta que el valor de
couut pasa a ser cero. Puesto que la tarea ya ha adquirido el bloqueo del objeto multiLock dentro de la primera llamada a
f1( ), esa misma tarea lo estará volviendo a adquirir en la llamada a n( ), y así sucesivamente. Esto tiene sentido, porque
una tarea debe ser capaz de invocar otros métodos sincronizados contenidos dentro del mismo objeto, ya que dicha tarea ya
posee el bloqueo.
Corno hemos observado anteriormente al hablar de la E/S ininterrumpible, cada vez que noa tarea pueda bloquearse de tal
forma que no pueda ser interrumpida, existirá la posibilidad de que el programa se quede bloqueado. Una de las caracterís-
ticas aíladidas a las bibliotecas de concurrencia de Java SE5 es la posibilidad de que las tareas bloqueadas en bloqueos de
tipo ReentrantLock sean interrumpidas, a diferencia de las tareas bloqueadas en métodos sincronizados o secciones críticas:
/1: concurrency/Interrupting2.java
II Interrupc ión de una tarea bloqueado con un bloqueo Reentrant Lock.
import java . util . concurrent. * ;
i mport java . u til.concurrent . locks . *;
import static net.mindvi ew.uti l. Pri n t.*;
class BlockedMutex {
pri vate Lock lock = new Reentra ntLock();
public BlockedMutex() (
II Adquirir l o d i rectamente, para demostrar la i nterrupción
II de la tarea bloqueada en un bloqueo Reen t ran t Lock:
lock.lock() i
public void f 1)
t ry (
II Es to no es tará n unca disponible para una segunda tarea
l ock. l ockI nterruptibly( ) ; II Llamada especial
print ("lock acquired in f () ") ;
catch(InterruptedExcept ion el {
print(IIInterrupted from lock acquisition in f() 11) i
/* Output:
Waiting for f() in BlockedMutex
I ssuing t .interrupt ()
I n terrupted f rem lock acquisition in f ( )
Broken out of blocked call
*111,-
La clase BlockedMutex tiene un constructor que adquiere el propio bloqueo del objeto y nunca lo libera. Por esa razón, si
tratamos de invocar f() desde una segunda tarea (diferente de la que haya creado el objeto BlockedMutex), siempre nos
quedaremos bloqueados, porque el objeto Mutex no puede ser adquirido. En Blocked2, el método run( ) se detendrá en la
llamada a blocked.f( ). Cuando ejecutamos el programa, vemos que, a diferencia de una llamada de E/S, interrupt( ) per-
mite salir de una llamada que esté bloqueada por un mutex. 19
19 Observe que, aunque resulta poco probable, la llamada a tinte ....upt() podría llegar a suceder antes que La llamada a blocked.f( ).
21 Concurrencia 783
1* Output: (Sample)
NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
Finished time-consuming operation
Cleaning up 2
Cleaning up 1
NeedsCleanup 1
Sleeping
Cleaning up 1
Exiting via InterruptedException
*///,-
784 Piensa en Java
La clase NeedsCte.nup enfatiza la necesidad de efectuar una limpieza apropiada de los recursos si se abandona el bucle
mediante una excepción. Observe que todos los recursos de NeedsCte.nup creados en Btocked3.run() deben ir seguidos
inmediatamente de cláusulas try-fmally para garantizar que siempre se invoque el método eleanup().
Debe proporcionar al programa un argumento de línea de comandos que es el retardo en milisegundos antes de que se invo-
que interrupt(). Utilizando diferentes retardos, podemos salir de Blocked3.run( ) en diferentes puntos del bucle: en la Ha-
mada sleep( ) bloqueante y en el cálculo matemático no bloqueante. Como vemos, si se invoca interrupt( ) después del
comentario "punt02" (durante la operación no bloqueante), se completa primero el bucle, después se destruyen todos los
objetos locales y finalmente se sale del bucle por la parte superior gracias a la instrucción while. Sin embargo, si se invoca
interrupt( ) entre "punto 1" Y "punt02" (después de la instrucción while pero antes o durante la operación bloqueante
»,
sleep( la tarea sale a través de la excepción InterruptedException, la primera vez que se intente una operación bloquean-
te. En ese caso, sólo se limpian los objetos NeedsCleanup que hayan sido creados hasta el punto en que se genera la excep-
ción, y tenemos la oportunidad de crear cualquier otra tarea de limpieza dentro de la cláusula catch.
Una clase diseñada para responder a una interrupción deberá establecer una política para garantizar que permanezca en un
estado coherente. Esto significa, generalmente, que la creación de todos los objetos que requieran limpieza deberá ir segui-
da por cláusulas try-finally de modo que esa limpieza tenga lugar independientemente de CÓmo se salga del bucle run( ).
El código de este tipo puede funcionar bien, aunque hay que señalar que, debido a la falta de llamadas automáticas a des-
tructores en Java, depende de que el programador de clientes describa las cláusulas try-finaUy apropiadas.
Es importante comprender que sleep( ) no libera el bloqueo del objeto cuando se lo iovoca, pero tampoco 10 hace yield( ).
Por otro lado, cuando una tarea entra en una llamada a wait( ) dentro de un método, se suspende la ejecución de esa hebra
y se libera el bloqueo sobre ese objeto. Puesto que wait( ) libera el bloqueo, quiere decir que ese bloqueo podrá ser adqui-
rido por otra tarea, por lo que durante mla espera con wait() podrán iovocarse otros métodos sincronizados en el (abara des-
bloqueado) objeto. Esto resulta esencial, porque esos otros métodos son normalmente los que provocan el cambio que hacen
que sea interesante que se vuelva a despertar la tarea suspendida. Así, cuando llamamos a. wait( ), estamos diciendo: "he
hecho todo lo que puedo por ahora, así que voy a esperar aquí, pero quiero pennitir que otras operaciones sincronizadas pue-
dan tener lugar, si es que pueden".
Existen dos formas de wait(). Una versión toma un argumento en milisegundos que tiene el mismo significado que sleep():
"efectúa una pausa durante este período de tiempo". Pero, a diferencia de sleep( ), con wait(pausa):
1. El bloqueo del objeto se libera durante la ejecución de wait().
2. También se puede salir de la llamada a wait( ) debido a la recepción de notify( ) o notify All( ), además de per-
mitir que la temporización fmalice.
La segunda forma de wait( ) más común no toma ningún argmnento. Este tipo de wait( ) continuará indefinidamente hasta
que la hebra reciba un mensaje notify() o notlfyAll().
Una aspecto bastante distintivo de wait(), Dotify() y notifyAll() es que estos métodos forman parte de la clase base Object
y no de Thread. Aunque esto parece algo extraño a primera vista (tener algo exclusivo del mecanismo de hebras como parte
de la clase base universal), resulta esencial porque estos métodos manipulan el bloqueo que también forma parte de todos
los objetos. Como resultado, podemos incluir una llamada a wait( ) dentro de cualquier método sincronizado, independien-
temente de si dicha clase amplía a Thread o implementa Runnable. De hecho, el único lugar en que se puede llamar a
wait(), notify() o notifyAll() es dentro de un método o bloque synchronized, (sleep() puede invocarse dentro de méto-
dos no sincronizados ya que no manipula el bloqueo). Si iovocamos cualquiera de estos métodos dentro de un método que
no sea de tipo synchronized, el programa se podrá compilar, pero al ejecutarlo se obtendrá una excepción IIIegalMonitor-
StateException con el poco intuitivo mensaje de "current thread not owner" Oa hebra actual no es la propietaria). Este men-
saje qniere decir que la tarea qne está invocando wait( ), notify( ) o notifyAll( ) debe "poseer" (adquirir) el bloqueo del
objeto antes de poder invocar ninguno de sus métodos.
Podemos pedir a otro objeto que realice una operación que manipula su propio bloqueo. Para hacer esto, tenemos primero
que capturar el bloqueo de ese objeto. Por ejemplo, si queremos enviar notifyAll() a un objeto x, deberemos hacerlo den-
tro de UD bloque sincronizado que adquiere el bloqueo para x:
synchronized(x) {
x .notifyAll () ;
Examinemos un ejemplo simple. WaxOMatic.java tiene dos procesos: uno para aplicar cera a un objeto coche (represen-
tado por un objeto Car) y otro para pulirlo. La tarea de pulido no puede llevar a cabo su trabajo hasta que haya finalizado
la tarea de aplicación de la cera y la tarea de aplicación de cera hasta que la tarea de pulido haya finalizado, antes de poner
otra capa de cera. Tanto WaxOn como WaxOffutilizan el objeto Car, que emplea wait() y notifyAlI() para suspender y
reiniciar las tareas mientras éstas están esperando a que una condición cambie:
ji : concurrency/ waxomatic / WaxOMa t ic.java
JI Cooperación básica entre t a r eas.
package concurrency .waxomatic;
import java.util.concurrent. *;
import static net.mindview. util.Pr int .*;
class Car (
private boolean waxOn = fa l se;
public s ynchronized void waxed( )
waxOn = true: /1 Listo para pulido
notifyAll () ;
notifyAll () ;
catch(InterruptedException el {
print(IIExiting via interr upt ll l ;
20 En algunas plataformas, existe una tercera forma de salir de una llamada a wait( ): el denominado despertar espúreo. Un despertar espúreo significa,
esencialmente, que una hebra puede abandonar el bloqueo prematuramente (mientras está esperando de acuerdo con un semáforo o con una variable de
condición) sin que ello venga desencadenado por un mensaje a notify() o notifyAII() (o sus equivalentes para los nuevos objetos Condition). La hebra
simplemente se despierta aparentemente por sí misma. Estos despertares espúreos existen porque la implementación de hebras POS IX, o sus equivalentes,
no es siempre tan sencilla como debería ser en algunas platafonnas. Pennitir estos despertares espúreos hace que la tarea de construir una biblioteca como
pthreads sea más fácil en esas platafonnas.
788 Piensa en Java
primera tarea después de que haya pasado lli1 cierto número de segundos, de modo que la primera tarea
pueda mostrar un mensaje. Pruebe las dos clases utilizando lli1 objeto Executor.
Ejercicio 22: (4) Cree un ejemplo de espera activa. Una tarea debe donnir durante un cierto tiempo y luego asignar el
valor true a un indicador. La segunda tarea deberá comprobar dicho indicador dentro de un bucle while
(ésta es la espera activa) y cuando el indicador sea true, deberá asignarle de nuevo el valor false e infor-
mar del cambio a través de la consola. Observe cuánto tiempo desperdicia el programa dentro de la espe-
ra activa, y cree una segunda versión del programa que emplee wait( ) en lugar de esa espera activa.
Señales perdidas
Cuando se coordinan dos hebras utilizando notlfy( )/wait() o notifyAIJ( )/wait(), resulta posible perder una señal. Suponga
que Ti es una hebra que notifica a TI, y que las dos hebras se implementan utilizando el siguiente enfoque (incorrecto):
Tl,
synchronized(sharedMonitor ) {
<condi ción de confi guraci 6n para T2>
sharedMoni tor .no tify() ;
T2,
while(someCondition)
11 Punto 1
synchroniz ed(sha redMonito r)
sharedMonitor .wait() ;
La <condición de configuración pora T2 > es una acción destinada a impedir que T2 invoque wait( ), si es que no lo ha
hecho ya.
Suponga que T2 evalúa someCondilion y encuentra que esa condición es verdadera. En Punto 1, el planificador de hebras
podría conmutar a Tl. TI ejecutaría su configuración, y entonces invocaría notify( ). Cuando T2 continúe ejecutándose, es
demasiado tarde para que T2 se dé cuenta de que la condición se ha modificado mientras tanto, por lo que entrará ciega-
mente en wait(). El mensaje notify() se perderá y T2 esperará indefinidamente a recibir una señal que ya había sido envia-
da, lo que producirá un interbloqueo.
La solución consiste en imped¡f la condición de carrera que afecta a la variable someCollditioD. He aquí la técnica correc-
ta para T2:
synchronized(sharedMonitor)
while(someCondition)
sharedMonitor.wait() ;
Ahora, si se ejecuta primero TI, cuando el control vuelve a T2 éste podrá ver que la condición se ha modificado, y no entra-
rá en wait(). A la inversa, si se ejecuta primero TI, entrará en wait( ) y será posteriormente despertado por TI . De este
modo, no puede perderse ninguna señal.
notifyO y notifyAIIO
Puesto que técnicamente podría darse el caso de que hubiera más de una tarea esperando con wait( ) con un único objeto
Car, resulta más seguro invocar notifyAIJ() que simplemente notify(). Sin embargo, la estructura del programa anterior es
tal que sólo habrá una tarea esperando con wait(), por lo que podemos perfectamente usar notify() en lugar de notifyAll().
Utilizar notify() en lugar de notifyAll( ) es una optimización. Sólo se despertará con notify( ) a una de las tareas de las
muchas posibles que estén esperando con bloqueo, así que si tratamos de utilizar notify( ) debernos estar seguros de que se
despertará la tarea correcta. Además, todas la tareas deberán estar esperando por la misma condición si queremos utilizar
ootify(), porque si hubiera tareas que estuvieran esperando por condiciones diferentes, no sabríamos si se despertará la tarea
correcta. Si empleamos notify( ), sólo una tarea deberá aprovecharse cuando se modifique la condición. Finalmente, estas
21 Concurrencia 789
restricciones deben cumplirse para todas las subclases posibles. Si no se puede cumplir alguna de estas reglas, es necesario
emplear notifyAll() en lugar de notlfy().
Una de las afirmaciones confusas que a menudo se hacen a la hora de explicar el mecanismo de herramientas de Java es que
notifyAll() despierta "a todas las tareas en espera". ¿Quiere esto decir que cualquier tarea que se encuentre esperando con
wait( ) en cualquier lugar del programa, será despertada por cualquier llamada a notifyAU()? En el siguiente ejemplo, el
código asociado con Task2 demuestra que esto no es así; de hecho, cuando se invoque notifyAll() para un cierto bloqueo
sólo se despertarán las tareas que estén esperando por ese bloqueo concreto:
ji: concurrency/NotifyVsNotifyAll.java
import java.util.concurrent.*¡
import j ava. u ti l.*¡
class Blocker {
synchronized void waitingCall() {
try {
while ( !Thread.interrupted ()) {
wait {) ;
System.out.print(Thread.currentThread() + n ") i
catch{InterruptedException el
JI OK salir de esta fo rma
}
}, 400, 400}¡ II Ejecutar cada 4 segundos
790 Piensa en Java
TimeUnit.MILLISECONDS.sleep(500) ;
System.out.println(u\nShutting down ll ) ;
exec.shutdownNow(); JI Interrumpir todas las tareas
/* Output, (Samplel
notify() Thread [pool-l-thread-l,5,mainJ
notifyAll() Thread[pool-l-thread-l,5,main] Thread [pool-l-
thread-5,5,mainJ Thread [pool-l-thread-4, S,mainJ
Thread [pool-l-thread-3, S,main] Thread [pool-l-thread-2, S,main]
notify() Thread[pool-l-thread-l,5,mainJ
notifyAll() Thread[pool-l-thread-l,5,main] Thread [pool-l-
thread-2,5,mainJ Thread [pool-l-thread-3, 5,mainJ
Thread [pool-l-thread-4,5,mainJ Thread [pool-l-thread-5, 5,mainJ
notify() Thread[pool-l-thread-l,5,main]
notifyAll() Thread[pool-l-thread-l,5,mainJ Thread [pool-l-
thread-5,5,main] Thread [pool-l-thread-4, 5,main]
Thread [pool-l-thread-3, 5,mainJ Thread [pool-l-thread-2, 5,mainJ
notify() Thread[pool-l-thread-l,5,mainJ
notifyAll() Thread[pool-l-thread-l,5,mainJ Thread [pool-l-
thread-2,5,mainJ Thread [pool-l-thread-3, 5,main]
Thread [pool-l-thread-4, 5,main] Thread [pool-l-thread-5, 5,mainJ
notify() Thread[pool-l-thread-l,5,mainJ
notifyAll() Thread[pool-l-thread-l,5,main] Thread [pool-l-
thread-5,5,mainJ Thread [pool-l-thread-4, 5,mainJ
Thread [pool-l-thread-3, 5,main] Thread [pool-l-thread-2, 5,mainJ
notify() Thread[pool-l-thread-l,5,mainJ
notifyAll() Thread[pool-l-thread-l,5,main] Thread [pool-l-
thread-2,5,main] Thread [pool-l-thread-3, 5,mainJ
Thread [pool-l-thread-4,5,mainJ Thread [pool-l-thread-5, 5,main)
Timer canceled
Task2.blocker.prodAll() Thread[pool-l-thread-6,5,mainJ
Shutting down
*///,-
Task y Task2 tienen, cada uno de ellos, su propio objeto Blocker, de modo que cada objeto Task se bloquea sobre
Task.blocker, y cada objeto Task2 se bloquea sobre Task2.blocker. En main( ), se configura un objeto java.util.Timer
para ejecutar su método run( ) cada 4/10 segundos y ese método rnn( ) llama alternativamente a los métodos notify( ) y
notifyAII() sobre Task.blocker, a través de los métodos "prod".
Analizando la salida, podemos ver que, aunque existe un objeto Task2 que no está bloqueado sobre Task2.blocker, ningu-
na de las llamadas notify() o notifyAII() sobre Task.blocker hace que el objeto Task2 se despierte. De forma similar, al
final de main( ), se invoca cancel( ) para timer y, aun cuando se cancela el temporizador, las primeras cinco tareas siguen
ejecutándose y siguen bloqueadas en sus llamadas a Task.blocker.waitingCall( ). La salida correspondiente a la llamada
Task2.blocker.prodAlI( ) no incluye ninguna de las tareas que está esperando por el bloqueo de Task.blocker.
Esto también tiene sentido si examinamos prod( ) y prodAll( ) en Blocker. Estos métodos son sincronizados, lo que quie-
re decir que tienen su propio bloqueo, de manera que cuando invocan notify() o notifyAll(), resulta lógico que sólo estén
invocando dichos métodos para ese bloqueo. y que sólo despierten a las tareas que estén esperando por ese bloqueo con-
creto.
Blocker.waitingCall() es lo suficientemente simple como para que podamos escribir for(;;) en lugar de
while(!Thread.interrupted( )), y conseguir el mismo efecto en este caso, porque en este ejemplo no hay diferencia entre
abandonar el bucle con una excepción y abandonarlo comprobando el indicador interrupted( ); en ambos casos se ejecuta
el mismo código. Sin embargo, por cuestión de estilo, este ejemplo comprueba interrnpted( ), porque existen dos formas
21 Concurrencia 791
distintas de salir del bucle. Si decidimos posteriormente añadir más código al bucle, correríamos el riesgo de introducir un
error si no se cubren ambas fannas de salir del bucle.
Ejercicio 23: (7) Demuestre que WaxOMatic.java funciona adecuadamente cuando se emplea notify( ) en lugar de
notifyAll( ).
Productores y consumidores
Considere un restaurante que tiene un cocinero y un camarero. El camarero tiene que esperar a que el cocinero prepare un
plato. Cuando el cocinero tiene un plato preparado, se lo notifica al camarero, que toma el plato y lo entrega al cliente y
vuelve a quedar en espera. Éste es un ejemplo de cooperación entre tareas: el cocinero representa al productor y el camare-
ro representa al consumidor. Ambas tareas deben negociar a medida que se producen y consumen los platos y el sistema
tiene que ser capaz de terminar de una manera ordenada. He aquí este ejemplo modelado en código Java:
ji: concurrency/Restaurant.java
// La técnica productor-consumidor para cooperación entre tareas.
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
class Meal {
private final int orderNum¡
public Meal(int orderNum) { this.orderNum orderNum¡ }
public String toString () { return uMeal 11 + orderNum¡ }
catch(InterruptedException e) {
print ("Wai tPerson interrupted");
if (++count == 10) {
print ("Out of food, closing") ¡
restaurant.exec.shutdownNow() ;
TimeUnit.MILLISECONDS.sleep(lOO) ;
catch(InterruptedException e)
print("Chef interrupted!1} ¡
/* Output:
Order upl Waitperson got Meal 1
Order up! Waitperson got Meal 2
Order up! Waitperson got Meal 3
Order up! Waitperson got Meal 4
Order up! Waitperson got Meal 5
Order up! Waitperson got Meal 6
Order up! Waitperson got Meal 7
Order up! Waitperson got Meal 8
Order up! Waitperson got Meal 9
Out of food, closing
WaitPerson interrupted
Order up! Chef interrupted
*///,-
El objeto Restaurant es el punto focal tanto para el camarero (WaitPerson) como para el cocinero (Chef). Ambos deben
saber para qué objeto Restaurant están trabajando, porque ambos deben colocar o tomar los platos en el "mostrador" del
mismo restamante, restanrant.meal. En rnn( ), el objeto WaitPerson entra en modo wait( ), deteniéndose esta tarea hasta
que ésta es despertada mediante un mensaje notifyAll() procedente del objeto Chef. Puesto que esto es un programa muy
simple, sabemos que sólo habrá una tarea esperando por el bloqueo correspondiente a WaitPerson: la propia tarea
WaitPerson. Por esta razón, seria teóricamente posible invocar notify() en lugar de notifyAll(). Sin embargo, en situacio-
nes más complejas, puede que haya múltiples tareas esperando por el bloqueo concreto de un objeto, así que no sabremos
qué tarea debe ser despertada. Por tanto, resulta más seguro invocar notifyAll(), que despierta a todas las tareas que estén
esperando por ese bloqueo. Cada tarea deberá entonces decidir si la notificación es relevante.
Una vez que el objeto Chef entrega un plato (un objeto Meal) y notifica al objeto WaitPerson, el Chef espera hasta que
WaitPerson toma el plato y lo notifica al Chef, que entonces puede producir el siguiente objeto Meal.
Observe que la llamada a wait( ) está encerrada dentro de una instrucción while( ) que comprueba esa misma condición por
la que se está esperando. Esto parece un poco extraño a primera vista: si estamos esperando un pedido, una vez que desper-
tamos, ese pedido tendrá que estar disponible, ¿verdad? Como hemos indicado anteriormente, el problema es que en una
21 Concurrencia 793
aplicación concurrente, alguna otra tarea podría interferir y hacer el pedido mientras que el objeto WaitPerson se está
despertando. El único enfoque seguro consiste en utilizar siempre la siguiente sintaxis para una llamada a wait() (emplean-
do, por supuesto, la adecuada sincronización y preparando el programa para que no exista la posibilidad de que se pierdan
señales):
while(noSeCumpleCondición)
wait () ;
Esto garantiza que la condición se habrá cumplido antes de salir del bucle de espera y que si se nos ha notificado algo que
no afecta a la condición (como puede ocurrir con notifyAll( »,
o la condición cambia antes de que salgamos del todo del
bucle de espera, estará garantizado que volveremos a entrar en espera.
Observe que la llamada a notifyAU( ) tiene que capturar priroero el bloqueo sobre waitPerson. La llamada wait( ) en
WaitPerson.run( ) libera automáticamente el bloqueo, así que esto resulta posible. Dado que el bloqueo deberá haber sido
adquirido para poder invocar notifyAU(), estará garantizado que no puedan interferir dos tareas que estén tratando de invo-
car notifyAU( ) sobre un mismo objeto.
Ambos métodos mn() están diseñados para poder efectuar una terminación ordenada encerrando el método run( ) comple-
to dentro del bloque try. La cláusula eateh se cierra justo antes de la llave de cierre del método run(), por lo que si la tarea
recibe una excepción InterruptedException, terminará inmediatamente después de capturar la excepción.
En Chef, observe que después de invocar shutdownNow(), podríamos simplemente volver (con retum) de run(), yeso
es lo que haremos normalmente. Sin embargo, resulta un poco más interesante hacerlo de la forma en que se lleva a cabo
en el ejemplo. Recuerde que shutdownNow( ) enVÍa una notificación ioterrupt( ) a todas las tareas que hayan iniciado el
objeto ExecutorService. Pero en el caso de Chef, la tarea no se termina inmediatamente después de recibir la notificación
interrupt( ), porque la interrupción sólo genera InterruptedException cuando la tarea intenta iniciar lma operación blo-
queante (interrumpible). Por tanto, primero veremos que se muestra el mensaje "Order up!" y luego se genera Interrupted-
Exception cuando el objeto Chef trata de invocar sleep(). Si eliminamos la llamada a sleep(), la tarea alcanzará la parte
superior del bucle run( ) y saldrá de la comprobación Thread.interrupted( ), sin generar una excepción.
El ejemplo anterior sólo tiene un lugar cuando una tarea pueda almacenar un objeto de modo que otra tarea pueda utilizar
ese objeto posteriormente. Sin embargo, en una implementación típica productor-consumidor, se utilizaría una cola de tipo
FIFO para almacenar los objetos que estén siendo producidos y consumidos. Aprenderemos más acerca de dichas colas pos-
teriormente en el capítulo.
Ejercicio 24: (1) Resuelva un problema de un único productor y un único consumidor utilizando wa!t() y DotifyAlI().
El productor no debe desbordar el buffer del receptor, lo que podria ocurrir si el productor fuera más rápi-
do que el consumidor. Si el consumidor es más rápido que el productor, entonces no deberá leer los mis-
mos datos más de una vez. No realice ninguna suposición acerca de las velocidades relativas del productor
y el consumidor.
Ejercicio 25: (1) En la clase Chef de Restaul'ant.java, vuelva del método run() después de invocar shutdownNow()
y observe la diferencia de comportamiento.
Ejercicio 26: (8) Añada una clase BusBoy (ayudante) a Restaurant.java. Después de entregar un plato, WaitPerson
debe notificar a BusBoy que tiene que efectuar la limpieza.
c Iass Car
p r í vate Lock lock = new ReentrantLock () i
pri vate Condit ion condition = l ock.newCondition();
prí vate boolean waxOn = fa l se¡
public void waxed()
l ock .lock () ;
t ry (
waxOn = true; 1/ Listo par a pu lir
condi t i on . signalAll () j
fina lly (
l ock . unlock()¡
publ ic vo id buffed( )
lock .lock () ;
t ry (
waxOn = fal se; ji Listo para otra capa de ce ra
condition.signalAll{) ;
finally (
loc k . un l ock() ;
car.waitForBuffing( ) ;
catch{InterruptedException e ) {
print (" Exi ting vía interrupt II) i
catch(InterruptedException e ) {
print (11 Exi t ing via interrupt n) i
Productores-consumidores y colas
Los métodos wait( ) y notifyAll( ) resuelven el problema de la cooperación entre tareas a bastante bajo nivel, efectuando
una negociación para cada iteración. En muchos casos, podemos movernos un nivel de abstracción hacia arriba y resolver
los problemas de la cooperación entre tareas utilizando una cola sincronizada, que sólo pennite que una tarea inserte o eli-
mine un elemento cada vez. Este tipo de funcionalidad la proporciona la interfaz java.util.concurrent.BlockingQueue, que
tiene varias implementaciones estándar. Normalmente utilizaremos LinkedBlockingQueue, que es una cola no limitada; la
cola ArrayBlockingQueue tiene un tamaño fijo, por lo que sólo se puede introducir en ella un cierto número de elementos
antes de que se bloquee.
Estas colas también suspenden una tarea consumidora si dicha tarea trata de extraer un objeto de la cola estando ésta vacía;
la tarea se reanudará cuando haya más elementos disponibles. Las colas bloqueantes como éstas pueden resolver un gran
número de problemas de una forma mucho más simple y más fiable que wait() y notifyAll( ).
He aqlÚ una prueba simple que serializa la ejecución de objetos LiftOff. El consumidor es LiftOffRunner, que extrae cada
objeto LiftOrf de la cola bloqueante BlockingQueue y lo ejecuta directamente <es decir, utiliza su propia hebra invocando
explícitamente run() en lugar de iniciar una nueva hebra para cada tarea).
JI : concurrencyjTestBlockingQueues.java
/ / {Ru nByHand}
import java.util.concurrent.*¡
i mport java.io.*;
import static net.mindview.util.Print.*¡
catch( InterruptedException el
print ( "Waking fram take (l n) i
stat ic void
test(Str i ng msg, BlockingQueue<LiftOff> queue) {
print (msg) ;
LiftOffRunner runner = new LiftOffRunner(queue)¡
Thread t = new Thread{runne r) i
t.startO;
for(int i = O; i < S; i++ )
r unner.add {new LiftOff {S ) ¡
getkey(uPress 'Enter ' ( " + msg + ,,) n);
t.inter r upt() ;
prin t ("F i nished " + msg + " test");
Las tareas son introducidas en la cola BlockingQueue por maln( ) y son extraídas de BlockingQueue por el objeto
LiftOflRunner. Observe que LiftOffRunner puede ignorar los problemas de sincronización porque éstos son resueltos por
la cola BlockingQueue.
Ejercicio 28: (3) Modifique TestBlockingQueues.java añadiendo una nueva tarea que introduzca objetos LiftOff en la
col. BlockingQueue, en lugar de hacerlo en main( ).
c I ass Toast {
p u b l ic e nurn Status { DRY, BOTTERED, JAMMED }
private Statu s status Status.DRY;
private final int id;
publ ic Toast(int i dn ) { id = idn; }
publ i c void butte r( ) { status = Status . BUTTERBD;
public void jamO { status = Status.JAMMED; }
public Status getStatus () { return status; }
public int getldO { return id; }
publi c String toString () {
return UToast " + id + n. !I + status¡
}
798 Piensa en Java
catch(InterruptedException e)
print (nToaster int errupted 11) i
c atch(Int erruptedException el {
print("Butterer interrupted ll ) i
Toas t t : butteredQueue.take( } i
t. jam !) ;
print!t) ;
f i nishedQueue . pu t( t l ;
print(IlJammer o ff ll ) i
JI Consumir la tostada:
class Ea ter implements Runnable
private ToastQueue finishedQue ue;
private i nt c ount er = O;
public Eater(ToastQueue fi nished )
fi nishedQueue = fini shed¡
Toas! es un excelente ejemplo de la ventaja de emplear valores enum. Observe que no hay ninguna sincronización explíci-
ta (utilizando objetos Lock o la palabra clave synchronized), porque la sincronización es gestionada de manera implícita
por la colas (que se sincronizan internamente) y por el diseño del sistema: cada objeto Toas! sólo es manipulado por una
única tarea cada vez. Puesto que las colas son bloqueantes, los procesos se suspenden y se reanudan automáticamente.
800 Piensa en Java
Podemos ver que BlockingQueue puede simplificar el problema enormemente. El acoplamiento entre las clases que exis-
tiría con instrucciones wait( ) y notifyAll( ) explícitas se elimina, porque cada clase se comunica sólo con sus colas
BlockingQueue.
Ejercicio 29: (8) Modifique ToastOMatic.java para fabrícar bocadillos de mantequilla de cacabuete y mermelada, uti-
lizando dos líneas de fabricación separadas (una para el pan con mantequilla, otra para el pan con merme-
lada y luego mezclando las dos líneas).
catch(IOException e) {
print (e + " Sender write exception") i
eatch(InterruptedException e) {
print (e + " Sender sleep interrupted");
catch(IOException e)
print(e + rr Receiver read exceptionrr);
21 Concurrencia 801
Interbloqueo
A estas alturas ya sabemos que un objeto puede tener métodos sincronizados u otras formas de bloqueo que impidan a las
tareas acceder a dicho objeto hasta que se libere el mutex. También hemos visto que las tareas pueden bloquearse. Por tanto,
es posible que una tarea se quede esperando por otra tarea, que a su vez espere por otra tarea, etc., hasta que la cadena se
cierre de nuevo con una tarea que esté esperando por la primera. En esta situación, tendríamos un ciclo continuo de tareas
esperando unas por otras y ninguna de ellas podría continuar con su procesamiento. Esta situación se denomina interblo-
queo (dead/ock).21
Si tratamos de ejecutar un programa y se produce directamente un interbloqueo, podemos intentar localizar inmediatamen-
te el error. El problema real se produce cuando nuestro programa parece estar funcionamiento correctamente pero tiene la
posibilidad oculta de interbloquearse. En este caso, puede que no tengamos ninguna indicación de que ese interbloqueo es
posible, así que el error estará latente en el programa hasta que se presente de manera inesperada cuando un cliente 10 eje-
cute (en una forma que casi siempre será muy dificil de reproducir). Por tanto, la prevención del interbloqueo mediante un
diseño cuidadoso del programa es una parte critica del desarrollo de sistemas concurrentes.
El problema de la cena de los filósofos, inventado por Edsger Dijkstra, es una ilustración clásica del interbloqueo. La des-
cripción básica especifica cinco filósofos (aunque el ejemplo mostrado aqlÚ pennitiría cualquier número de ellos). Estos
2] También podemos tener lo que se denomina bloqueo activo (liveloc:k) cuando hay dos tareas que son capaces de cambiar su estado (no están bloquea-
das), pero nunca consiguen realizar ningilO progreso útil.
802 Piensa en Java
filósofos pasan parte de su tiempo pensando y parte de su tiempo comiendo. Mientras están pensando, no necesitan ningún
recurso compartido, pero todos ellos comen usando un número limitado de utensilios. En la descripción original del proble-
ma, los utensilios eran tenedores, y se requieren dos tenedores para tomar espagueti de una fuente situada en el centro de la
mesa, aunque parece que tiene más sentido decir que esos utensilios sean palillos. Claramente, cada filósofo necesitará dos
palillos para poder comer.
Introducimos una dificultad en el problema: como filósofos, tienen muy poco dinero, así que sólo pueden permitirse com-
prar cinco palillos (o más generalmente, el mismo número de palillos que de filósofos). Esos palillos están espaciados alre-
dedor de la mesa entre los filósofos. Cuando un filósofo quiere comer, debe tomar el palillo situado a su izquierda y el
situado a su derecha. Si alguno de los filósofos sentado a su lado está usando uno de los palillos que necesita, nuestro filó-
sofo deberá esperar hasta que los palillos necesarios estén disponibles.
ji : concurrency/Chopstick. j ava
JI Palillos para la cena de los filósofos.
id =o ident¡
ponderFactor = ponder;
public void run() {
try (
while(!Thread.interrupted())
print{this + I! 11 + "thinking") i
pause() i
JI El filósofo tiene hambre
print{this + 11 + !1grabbing right") i
right.take() ;
print(this + + "grabbing 1eft") i
left. take () ;
print (this + + Ueating H) i
pause () i
right.drop() i
left . drop () ;
catch(InterruptedException el {
print (this + JI n + lIexiting via interrupt ll ) i
exec.shutdownNow() ;
Podrá observar que si los objetos Philosopher pasan demasiado poco tiempo pensando, todos ellos competirán por los obje_
tos Chopstick cuando traten de comer, de modo que el interbloqueo se producirá mucho más rápidamente.
El primer argumento de la línea de comandos ajusta el factor ponder, que afecta al intervalo de tiempo que cada objeto
Philosopber dedica a pensar. Si tenemos muchos objetos Philosopber o éstos pasan una gran cantidad de tiempo pensan-
do, puede que nunca lleguemos a ver un problema de interbloqueo, aunque éste seguirá siendo posible. Un argumento de la
línea de comandos igual a cero tiende a hacer que el programa se interbloquee muy rápidamente.
Observe que los objetos Chopstick no necesitan identificadores internos; se los identifica por su posición dentro de la matriz
stick,. A cada constructor Philosopber se le proporciona una referencia a sendos objetos Cbopstick izquierdo y derecho.
Cada objeto Philosopber excepto el último se inicializa situando dicho objeto Pbilosopher entre el siguiente par de obje-
tos Chopstick. Al último objeto Philosopher se le asigna el objeto Chopstick situado en la posición cero como palillo dere-
cho, con lo que se completa la mesa redonda. Esto se debe a que el último filósofo está situado justo alIado del primero y
ambos comparten ese palillo número cero. Ahora, será posible que todos los objetos Pbilosopher traten de comer, quedan-
do todos ellos a la espera de que el objeto Pbilosopher situado a continuación de ellos libere su objeto Chopstick. Esto hará
que el programa se interbloquee.
Si nuestros filósofos invierten más tiempo pensando que comiendo, tendrán una posibilidad mucho menor de requerir los
recursos compartidos (los palillos), con lo que podríamos quedarnos convencidos de que el programa no sufre interbloqueos
(utilizando un valor de ponder distinto de cero o un gran número de objetos Philosopber), aunque en realidad no es así.
Es te ejemplo es interesante precisamente porque ilustra que un programa puede parecer estar ejecutándose correctamente y
sin embargo ser capaz de interbloquearse.
Para corregir el problema, es necesario entender que el interbloqueo puede producirse si se cumplen simultáneamente cua-
tro condiciones:
1. Exclusión mutua. Al menos uno de los recursos utilizados por las tareas no debe ser compartible. En este caso,
un objeto Chopstick sólo puede ser utilizado por un objeto Philosopher cada vez.
2. Al menos una tarea debe poseer un recurso y estar esperando a adquirir otro recurso que actualmente es propie-
dad de otra tarea. Es decir, para que el interbloqueo se produzca, un objeto Philosopher deberá poseer un objeto
Chopstick y estar esperando a adquirir otro.
3. Los recursos no pueden ser desalojados de una tarea. La liberación de recursos por parte de tareas sólo puede pro-
ducirse como un suceso normal. En otras palabras, nuestros filósofos son muy educados y no arrebatan los pali-
llos a los otros filósofos.
4. Puede producJrse una espera circular, según la cual una tarea estará esperando por un recurso que posee otra tarea,
que a su vez estará esperando por un recurso que posee otra tarea, y así sucesivamente, hasta que una de las ta-
reas esté esperando por un recurso poseído por la primera tarea, lo que hace que se produzca una cadena circular
de bloqueo. En DeadlockingDiningPbilosopbers.java, esta espera circular se produce porque cada fi lósofo trata
de obtener primero el palillo derecho y luego el izquierdo.
Como todas estas condiciones deben producirse para provocar un interbloqueo, basta con que impidamos que una de ellas
se produzca para conseguir que no haya interbloqueos. En este programa, la forma más fácil de impedir el interbloqueo con-
siste en impedir que se dé la cuarta condición. Esta condición sucede porque cada filósofo trata de tomar los correspondien-
tes palillos en un orden concreto, primero el derecho y luego el izquierdo. Debido a ello, resulta posible encontrarse en una
situación en la que cada uno de los filósofos haya tomado su palillo derecho y esté esperando a poder conseguir el izquier-
do, provocando la aparición de la condición de espera circular. Sin embargo, si inicializamos el último filósofo para que trate
de obtener primero el palillo izquierdo y luego el derecho, ese filósofo nunca impedirá que el filósofo situado inmediata-
mente a su derecha tome los dos palillos que necesita. En este caso, se impide la espera circular. Ésta sólo es una de las posi-
bles soluciones del problema; también podríamos evitar el problema impidiendo que se cumpla alguna de las otras
condiciones (consulte algún otro libro para conocer más detalles sobre la gestión avanzada de hebras):
11 : concurrenc y/FixedDiningPh i l osophers. java
II La cena de l os filóso fos sin i nt erbloqueo.
// {Args, 5 5 timeout}
import java.util.concurrent.*¡
exec.shutdownNow() i
CountDownLatch
Esta clase se usa para sincronizar una o más tareas forzándolas a esperar a que se complete un conjlll1l0 de operaciones que
estén siendo realizadas por otras tareas.
Al objeto Co untDownLatch le proporcionamos un valor de recuento inicial y cualquier tarea que invoque await() sobre
dicho objeto se bloqueará hasta que el recuento alcance el valor cero. Otras tareas pueden invocar countDown( ) sobre el
806 Piensa en Java
objeto para reducir el valor de recuento, presumiblemente cuando esas tareas finalicen su trabajo. CountDownLatch está
diseñado para util izarlo una sola vez, el valor de recuento DO puede reinicializarse. Si necesitamos una versión donde pueda
reinicializar el va lor de recuento, podemos emplear en su lugar CyclicBarrier.
Las tareas que invocan countDown() no se bloquean cuando hacen esa llamada. Sólo la llamada a await( ) se bloquea hasta
que el recuento alcanza el valor cero.
Un uso normal consiste en dividir un problema en n tareas independientemente resolubles y crear un objeto CountDown_
Latch con un valor n. Cuando cada una de las tareas finaliza invoca countDown() para el objeto encargado del recuento.
Las tareas que están esperando a que el problema se resuelva pueden invocar a awail() sobre el objeto encargado del recuen-
lo para esperar a que el problema esté completamente resuelto. He aquí un esqueleto de ejemplo que ilustra esta técnica:
JI : concu rren cyjCountDownLatchDemo . j ava
i mport java.util . concurrent.* ¡
import java.util.*;
import static net.mindvi ew.util.Print.*¡
CyclicBarrier
La clase CyclicBarrier (barrera circular) se utiliza en aquellas situaciones en las que queremos crear un grupo de tareas para
realizar un cierto trabajo en paralelo, y luego esperar a que todas hayan finalizado, antes de continuar con el signiente paso
.(algo parecido a join( ), podríamos decir). Esta clase alinea todas las tareas paralelas en la barrera circular, de modo que
podamos continuar hacia adelante al unísono. Se trata de una solución muy similar a CountDo"nLatch, excepto porque
ConntDownLatch representa un suceso que sólo se produce una vez, mientras que CyclicBarríer puede reutilizarse
muchas veces.
Las simulaciones me han fascinado desde que empecé a utilizar computadoras y la concurrencia es un factor clave a la hora
de realizar simulaciones. El primer programa que recuerdo haber escrito22 era una simulación: un juego de carreras de caba-
llos escrito en BASIC y denominado (debido a las limitaciones de los nombres de archivos) HOSRAC.BAS. He aqulla ver-
sión orientada a objetos y con hebras de dicho programa, utilizando una clase CyclicBarrier:
11: concurrency/HorseRace.java
1I Utilización de Cycl icBarrier.
22 Cuando estaba en la universidad; en la laboratorio disponíamos de un teletipo SR-33 con un nodo de acoplamiento de acústico de 110 baudios median-
te el que se accedía a una computadora HP-lOOO.
808 Piensa en Java
impor t java.util.concurrent.*¡
import java.util.*¡
i mport static net . mi ndview.util.Print.*¡
barrier.await() i
catch(InterruptedException el {
/1 Una forma legí tima de sal ir
catch(BrokenBarrierException el
/1 Ésta queremos probarla
throw new RuntimeException(e);
try
TimeUn i t.MILLISECONDS .sleep(pause ) i
21 Concurrencia 809
catch(InterruptedException el {
print (Ubarrier-action sleep interruptedl!);
}
}) ;
for(int i = Di i < nHorses¡ i++l
Harse harse = new Horse(barrier);
horses.add(horse) i
exec.execute(horse)¡
DelayQueue
Se trata de una cola BlockingQueue de objetos sin limitación de tamaño que implementa la interfaz Delayed. Un objeto só-
lo puede ser extraído de la cola una vez que su retardo haya trauscurrido. La cola está ordenada, de modo que el objeto situa-
do al principio es el que tiene el retardo que ha transcurrido hace más tiempo. Si no ha transcurrido ninguno de los retardos,
no habrá ningún elemento de cabecera y el método poll( ) devolverá null (debido a esto, no pueden insertarse elementos
null en la cola).
He aquí un ejemplo donde los objetos Delayed son tareas y el consumidor DelayedTaskConsumer extrae la tarea más
"urgente" (aquella cuyo retardo haya caducado hace más tiempo) de la cola y la ejecuta. Observe que DelayQueue es, por
tanto, una variante de una cola con prioridad.
jj: concurrencyjDelayQueueDemo.java
import java.util.concurrent.*¡
import java.util.*¡
import static java.util.concurrent.TimeUnit.*¡
import static net.mindview.util.Print.*¡
class DelayedTask implements Runnable, Delayed
private static int counter = O;
private final int id = counter++¡
810 Piensa en Java
print () ;
print (this + 11 Calling shutdownNow () II} ;
exec.shutdownNow() ¡
/* Output:
[128 ] Task 11 [200 ] Task 7 [429 ] Task 5 [520 ] Task 18
[555 ] Task 1 [961 ] Task 4 [998 ] Task 16 [1207] Task 9
[1693] Task 2 [1809] Task 14 [1861] Task 3 [2278] Task 15
[3288] Task 10 [3551] Task 12 [4258] Task O [4258] Task 19
[4522] Task 8 [4589] Task 13 [4861] Task 17 [4868] Task 6
(0,4258) (1,555) (2,1693) (3,1861) (4,961) (5,429) (6,4868)
(7,200) (8,4522) (9,1207) (10,3288) (11,128) (12,3551)
(13,4589) (14,1809) (15,2278) (16,998) (17,4861) (18,520)
(19,4258) (20,5000)
[5000J Task 20 Calling shutdownNow()
Finished DelayedTaskConsumer
*///,-
DelayedTask contiene nna lista List<DelayedTask> denominada sequenee que preserva el orden en que fueron creadas las
tareas, para poder comprobar que efectivamente se está realizando lUla ordenación.
La interfaz Delayed tiene un método, getDelay( ), que dice cuánto queda para que caduque el retardo o cuánto tiempo hace
que ha caducado. Este método nos fuerza a utilizar la clase TImeUnit porque es el argumento de tipo. Esta clase resulta ser
bastante útil, porque podemos convertir fácilmente las unidades sin realizar ningún cálculo. Por ejemplo, el valor de delta
se almacena en milisegundos, pero el método System.nanoTIme( ) de Java SE5 devuelve el tiempo en nanosegundos.
Podemos convertir el valor de delta indicando en qué unidades está y en qué nnidades lo queremos como en el ejemplo
siguiente:
NANOSECONDS.convert(delta, MILLISECONDS) i
En getDelay( ), pasamos las nnidades deseadas como argrnnento unit, y las usamos para convertir el intervalo de tiempo
transcurrido desde el instante de disparo a las nnidades solicitadas por elllamante, sin siquiera tener por qué conocer qué
nnidades son esas (éste es un ej emplo simple del patrón de diseño Estrategia, según el cual parte del algoritmo se pasa como
argumento).
Para la ordenación, la interfaz Delayed también hereda la interfaz Comparable, así que habrá que implementar
eompareTo( ) para que realice uua comparación razonable. toString( ) y summary( ) se encargan del formateo de la sali-
da y la clase anidada EndSentinel proporciona nna forma de terminar todo, colocándola como último elemento de la cola.
Observe que, como DelayedTaskConsumer es ella misma una tarea, dispone de su propio objeto Thread que puede usar
para ejecutar cada tarea que se extraiga de la cola. Puesto que las tareas se están ejecutando según el orden de prioridad de
la cola, no hay necesidad en este ejemplo de iniciar hebras separadas para ejecutar las tareas DelayedTask.
Podemos ver, analizando la salida, que el orden en que se crean las tareas no tiene ningún efecto sobre el orden de ejecu-
ción, en lugar de ello, las tareas se ejecutan según el orden de sus retardos, como cabría esperar.
PriorityBlockingQueue
Se trata, básicamente, de una cola con prioridad que tiene operaciones de extracción bloqueantes. He aquí un ejemplo en el
que los objetos de la cola con prioridad son tareas que salen de la cola según el orden de prioridad. A cada tarea con priori-
dad PrioritizedTask se le proporciona nn número de prioridad para fijar el orden:
812 Piensa en Java
11 : concurrency/PriorityBlockingQue ueDemo.java
import java . u ti l . concurrent. * ¡
i mport java . util .*¡
import static net.mindv iew.u t il.Print.* ¡
print (this) ;
print () ;
print (t his + " Calling s hu tdownNow() " ) ;
exec.shu t downNow () ;
print(IIFinished PrioritizedTaskConsumer" ) ¡
Al igual que en el ejemplo anterior, la secuencia de creación de los objetos PrioritizedTask se almacena en la lista sequen_
ce, para compararla con el orden real de ejecución. El método run( ) durante un corto período de tiempo aleatorio imprime
la información del objeto, mientras que EndSentinel proporciona la misma funcionalidad que antes al garantizar que es el
último objeto de la cola.
Los objetos PrioritizedTaskProducer y PrioritizedTaskConsumer se interconectan a través de una cola Priority_
BlockingQueue. Puesto que la naturaleza bloqueante de la cola proporciona todos los mecanismos necesarios de sincroni-
zación, observe que no hace falta ninguna sincronización explicita: no tenemos que preocupamos por si la cola tiene algún
elemento en el momento de leer de ella, porque ésta simplemente bloqueará al lector cuando no tenga ningún elemento.
public void
repeat(Runnabl e even t , long initialDelay , l ong periad) {
schedu ler.scheduleAtFixedRate {
event¡ initialDelay, period, TimeUnit.MILLISECONDS);
humidi t y '" h u m;
}
prívate Calendar lastTime '" Calendar.getlnsta nc e() j
Esta versión reorganiza el código y añade una nueva característica: recopilar las lecturas de temperatura y humedad en el
invernadero. Cada objeto DataPoint (punto de datos) almacena y visualiza un único dato, mientras que CollectData es la
tarea planificada que genera los elementos simulados y los añade al contenedor List<DataPoint> de Greenhouse cada vez
que se la ejecuta.
Observe el uso tanto de vol.tiIe como de synchronized en los lugares apropiados, para impedir que las tareas interfieran
unas con otras. Todos los métodos de la lista que almacena los objetos DataPoint se sincronizan empleando la utilidad
synchronizedList( ) de java.util.Collections en el momento de crear la lista.
Ejercicio 33: (7) Modifique GreenhouseScheduler.java para que utilice un objeto DelayQueue en lugar de
ScheduledExecutor.
Semaphore
Un bloqueo normal (de concurrent.locks o el bloqueo integrado synchronized) sólo permite a una única tarea acceder a un
recurso cada vez. Un semáforo contador pennite que n tareas accedan al recurso simultáneamente. También podemos pen-
sar en un semáforo como algo que entrega "permisos" para usar un recurso, aunque en realidad no se utiliza ningún objeto
penniso.
Como ejemplo, consideremos el concepto de conjunto compartido de objetos, que gestiona un .número limitado de objetos
permitiendo que esos objetos se extraigan del conjunto para utilizarlos y se devuelvan una vez que el usuari o haya termina-
do con ellos. Esta funcionalidad puede encapsularse en una clase genérica:
ji: concurrency/Pool .java
JI Utilización de un semáforo dentro de conjunto compart ido,
JI para res tr ingir el número d e t areas q ue pueden u sar un recurso .
í mp o rt java . u ti l . concurre n t.*i
import java.util.*¡
En esta fonna simplificada, el constructor utiliza newInstance() para cargar de objetos el conjunto compartido. Si necesi-
tarnos un nuevo objeto, invocamos checkOut( ), y cuando hemos fUlalizado con un objeto, lo devolvemos con cbeckIn( ).
La matriz booleana cbeckedOut lleva la cuenta de los objetos que han sido extraídos y está gestionada por los métodos
getItem() y releaseltem( ). Estos, a su vez, están protegidos por el semáforo available, de modo que, en checkOut( ), avai-
lable bloquea el progreso de la llamada si no hay pennisos de semáforo disponibles (lo que quiere decir que DO hay más
objetos en el conjunto compartido). En cbeckIn( ), si el objeto qu e se está devolviendo es válido, se devuelve un penniso
al semáforo.
Para construir un ejemplo, podemos usar Fat, un tipo de objeto que resulta costoso de crear, porque su constructor tarda bas-
tante tiempo en ejecutarse:
JI : concurren cy/ Fat. java
JI Objetos que son cos t osos de crear.
public vo id run( ) {
try {
T item = pool.checkOut(}¡
p rint(this + " checked out 11 + item);
TimeUnit.SECONDS.sleep (l) i
prin t (this + lI checking in 11 + item);
pool. checkln (item) ;
catch (Inte rruptedExcept ion el
JI Forma aceptable de terminar
}
}) ;
TimeUn it .SECONDS.sleep(2) ;
b l ocked.cancel(true); // Salir de la llamada bloqueada
print (IICheck ing in ob j ects i n 11 + l ist);
for( Fat f , l ist )
pool.checkl n (f);
for(Fat f , list )
pool.ch eckln( f ) ¡ // Segunda devolución ignorada
exec . s hutdown();
Este ejemplo depende de que el cliente de Pool sea riguroso y devuelva voluntariamente los elementos, lo cual es la solu-
ción más simple, siempre que funcione. Si no podemos confiar siempre en esto, Thinldng in Patterns (en www.
MindView.llet) contiene análisis adicionales de fonnas que pueden emplearse para gestionar los objetos extraídos de conjun-
tos de objetos compartidos.
Exchanger
Un intercambiador (Exchanger) es una barrera que intercambia objetos entre dos tareas. Cuando las tareas entran en la
barrera, cada una de ellas tiene un objeto, y cuando salen tienen el objeto que anteriormente era propiedad de la otra tarea.
Los intercambiadores se utilizan típicamente cuando una tarea está creando objetos que son caros de producir y otra tarea
está consumiendo dichos objetos; de esta forma, pueden crearse más objetos al mismo tiempo que están siendo consumidos.
Para probar la clase Exchanger, vamos a crear tareas productoras y consumidoras que, mediante genéricos y objetos
Generator, funcionarán con cualquier tipo de objeto, y luego las aplicaremos a la clase Fa!. Los objetos Exchanger-
Producer y ExchangerConsumer utilizan una lista List<T> como el objeto que hay que intercambiar; cada uno contiene
un objeto Exchanger para este contenedor List<T>. Cuando se invoca el método Exchanger.exchange( ), éste se bloquea
hasta que la otra tarea invoca su método exchange( ), y cuando ambos métodos exchange( ) se han completado, los conte-
nedores List<T> habrán sido intercambiados:
/1 : concurrencyjExchangerDemo.java
impo rt java.util.concurrent.*¡
import java.util .*¡
import net.mindview.util.*¡
catch( I nterruptedExcept i o n el
II OK terminar de esta forma .
public vo id run() {
try {
21 Concurrencia 821
while ( !Thread.interrupted (» {
holder = exchanger.exchange(holder) i
for(T x : holder) (
value = Xi /1 Extraer valor
holder.remove(x) ; JI OK para CopyOnWriteArrayList
ca t c h(InterruptedExc eption el
// OK termi nar de esta f orma .
exec.execute{
new ExchangerConsumer<Fat>( xc,consumerList » ;
TimeUnit.SECONDS.sleep(delay) i
exec.shutdownNow{) ;
/ * Output : (Sample )
Final value: Fat id: 299 99
*///:-
En main(), se crea un OOleo objeto Exchanger para que lo empleen ambas tareas, y dos contenedores CopyOn-
WriteArrayList para intercambiar. Esta variante concreta de Lis! permite que se invoque .el método remove( ) mientras
que se está recorriendo la lista sin que se genere una excepción CODcurrentModificationException. ExchangerProducer
rellena una lista y luego intercambia la lista llena por la vacía que ExchangerConsumer le entrega. Debido a la existencia
del objeto Exchangor, el llenado de una lista y el consumo de la otra pueden tener lugar simultáneamente.
Ejercicio 34: (1) Modifique ExchangerDemo.java para utilizar su propia clase en lugar de Fa!.
Simulación
Uno de los usos más interesantes y atractivos de la concurrencia es el de crear simulaciones. Utilizando la concurrencia,
cada componente de una simulación puede ser su propia tarea. y esto hace que la simulación resulte mucho más fácil de pro-
gramar. Muchos juegos infOlIDáticos y animaciones por computadora en las películas son simulaciones, y HorseRace.java
y GreenhouseScheduler.java, mostrados anteriormente, también podrían considerarse simulaciones.
Simulación de un cajero
Esta simulación clásica puede representar cualquier situación en la que aparecen objetos aleatoriamente, y estos objetos
requieren un intervalo de tiempo aleatorio para ser servido por lU1 número limitado de servidores. Es posible construir la
simulación para detenninar el número ideal de servidores.
822 Piensa en Java
En este ejemplo, cada cliente del banco requiere una cierta cantidad de tiempo de servicio, que es el número de UIÚdades de
tiempo que el cajero debe invertir en el cliente para satisfacer sus necesidades. La cantidad de tiempo de servicio será dife-
rente para cada cliente y se detenninará aleatoriamente. Además, no sabemos cuántos clientes llegarán en cada intervalo,
por lo que esto también se detenninará aleatoriamente.
1/: concurrency/BankTe llerSimulation . j ava
ji Utilización de colas y me cani smos multihebra.
II {Args, S}
import java.uti l.concurrent.*¡
import java.ut il.*;
catch(InterruptedException el {
System, out .println ("CUstomerGenerator in terrupted I!) i
catchllnterr uptedException e) {
System.out.println{this + lIinterrupted"};
public String toStri ng() { ret urn I'Te lle r " + id + 11 " ;
public Stri ng shortString () { return liT" + id ; }
II Usado por la cola de prioridad :
public synchronized i nt compareTo(Teller other )
return customersServed < other.customersServed ? -1 :
(customersSe rved == other.customersServed ? O : 1)¡
catch(InterruptedException e) {
System.out.println(this + lIinterruptedn) i
System.out.println(this + "terminating ll ) ;
new CustomerLine(MAX_LINE_SIZE) i
exec.execute (new CustomerGenerator (customers» i
JI El director añadirá o quitará cajeros según sea necesario:
exec. execute(new TellerManager(
exec, customers, ADJUSTMENT_PERIOD»;
if (args. length > O) /1 Optional argument
TimeUnit . SECONDS.sleep( new Integer (args[O }» i
else (
System.out.println ( npress 'Enter' to quit");
System.in .read(} i
exec.shut downNow() i
} /* Ou t put: (Sample)
[429 ] [2 00] [2 07] { T O T1 }
[861] [258] [140] [322] { TO T1 }
[575] [342] [8 04] [826] [8 96 ] [ 984 ] { TO T 1 T2 }
[984] [8 10 ] [141] [ 12 ] [68 9] [992] [976 ] [368] [ 395 ] [354] { TO T1 T2 T3 }
Tel le r 2 int errupted
Te ller 2 term i n ating
Teller 1 interrupted
Teller 1 terminating
TellerManager interrupt ed
TellerManager terminating
Telle r 3 inte r r upted
Tel l er 3 terminating
Teller O interrupted
Teller O terminating
CustomerGenerator interrupte d
CustomerGenerato r terminating
*/// :-
Los objetos Customer son muy simples, conteniendo únicamente un campo final int. Puesto que estos objetos nunca cam~
bian, son objetos de sólo lectura y no requieren sincronización ni el uso de volatile. Además, cada tarea Teller (cajero) sólo
elimina un objeto Customer cada vez de la cola de entrada, y trabaja sobre un objeto Customer hasta que se haya comple-
tado, por lo que en cualquier caso a cada objeto Customer sólo accederá una tarea en cada momento.
CustomerLine representa una única fila en la que los clientes esperan antes de ser servidos por un objeto Teller. Se trata
simplemente de una cola ArrayBlockiogQueue que tiene un método toString( ) que imprime los resultados de la forma
deseada.
Con cada objeto CustomerLine se asocia un obj eto CustomerGenerator, que introduce clientes en la cola a intervalos
aleatorios.
Cada objeto Teller extrae clientes de ]a cola CustomerLine y los procesa de uno en uno, llevando la cuenta del número de
clientes a los que ha servido durante ese intervalo concreto. Se le puede decir a] cajero que haga alguna otra cosa con
doSomethingElse( ) cuando no haya suficientes clientes o que atienda a la fila con serveCustomerLine( ) cuando haya
muchos clientes. Para seleccionar el siguiente cajero que hay que poner a atender a los clientes, el método compareTo( )
examina el número de clientes seIVidos, de modo que una cola PriorityQueue puede seleccionar automáticamente al caje-
ro que haya realizado un menor trabajo hasta el momento.
El objeto TellerManager'es el centro de actividad. Lleva el control de todos los cajeros y de lo que está sucediendo con los
clientes. Uno de los aspectos interesantes de esta simulación es que se intenta descubrir el número óptimo de cajeros para un
flujo de clientes determinado. Podemos ver esto en el método adjustTeUerNumber(), que es un sistema de contra] para agre-
gar y eliminar cajeros de una manera estable. Todos los sistemas de control tienen problemas de estabilidad; si reaccionan
demasiado rápido a un cambio, son inestables y si reaccionan demasiado lento, el sistema se desplaza a uno de sus extremos.
Ejercicio 35: (8) Modifique BankTellerSimulation.java para que represente clientes web que estén enviando solicitu-
des a un número fijo de servidores. El objetivo es detenninar la carga que el grupo de servidores puede
gestionar.
826 Piensa en Java
Simulación de un restaurante
Esta simulación retoma el ejemplo simple Restaurant.java mostrado anteriormente en el capítulo, añadiendo más compo_
nentes de simulación, como los pedidos (Order) y los platos (Plate), y reutiliza las clases menu del Capítulo 19, TIpos enu-
merados.
También introduce la cola SynchronousQueue de Java SE5, que es una cola bloqueante que no tienen capacidad interna,
por lo que cada operación put() (inserción) debe esperar a que se produzca una operación take() (extracción), y vicever_
sa. Es como si estuviéramos entregando un objeto a alguien (no hay ninguna mesa sobre el que ponerlo, por lo que sólo fun-
ciona si la otra persona está tendiendo una mano hacia nosotros y lista para recibir el objeto). En este ejemplo,
SynchronousQueue representa el espacio situado delante del comensal, para hacer hincapié en la idea de que sólo se puede
servir un plato cada vez.
El resto de las clases de la funcionalidad de este ejemplo se ajustan a la estructura de Restaurant.java o pretenden ser una
plasmación bastante directa de las operaciones de un restaurante real:
JI : concurrency/ res taurant 2 j RestaurantWithQueues.jav a
II {Ar gs, s}
package con currency.res t auran t 2;
import enumerated . menu.*¡
impor t java.util.concurrent.*¡
i mport java.util.*;
import s tatic net.mindview.util. Print.*;
catch(InterruptedExcept i on el
pri nt (thi s + 11 inte rrupted" ) i
publi c vo id run() {
try {
whi le (!Thr ead. interrupted() )
21 Concurrencia 829
catch(InterruptedException el {
print ("Res taurant i n terrupted " ) ;
exec.shut downNow () ;
/* Output, (Samp l e)
WaitPerson O rece ived SPRI NG_ROLLS de livering te Cus teme r 1
Cus t omer 1 eat ing SPRING_ ROLLS
WaitPerson 3 received SPRING_ ROLLS de l ivering to Customer O
Custome r O eat i ng SPRING_ROLLS
WaitPersen O received BURRITO de live ring te Customer 1
Customer 1 eat ing BURRI TO
Wait Person 3 rece ived SPRING_ROLLS de liver ing te Cus tome r 2
Customer 2 eating SPRING_ROLLS
WaitPer son 1 received SOUP delivering to Customer 3
Custome r 3 eat i ng SOUP
WaitPerson 3 received VINDALOO de livering to Cus tomer O
Cust omer O eating VINDALOO
WaitPerson O received FRUIT del i vering to Customer 1
*/// ,-
Un aspecto muy importante de este ejemplo es la gestión de la complejidad utilizando colas para la comunicación entre ta-
reas. Esta técnica simplifica enonnemente el proceso de la programaci6n concurrente, al invertir el control: las tareas no
interfieren directamente entre sí. En su lugar, las tareas se intercambian objetos a través de colas. La tarea receptora ges tio-
na el objeto, tratándola como un mensaje, en lugar de recibir directamente mensajes. Si seguimos esta técnica siempre que
podamos, tendremos una mayor posibiJidad de construir sistemas concurrentes robustos.
Ejercicio 36: (lO) Modifique RestaurantWithQueues.java para que haya un objeto OrderTicket (nota de pedido) por
cada mesa. Cambie order por orderTicket, y añada la clase Table (mesa), con múltiples clientes (Custo-
mer) por mesa.
Distribución de trabajo
He aquí un ejemplo de simulación simple donde se aúnan muchos de los conceptos vistos en el capítulo. Considere una hipo-
tética línea de montaje robotizada para automóviles. Cada objeto automóvil (Ca r) será construido en varias etapas, comen-
zando por la fabricación del chasis y siguiendo por el montaje del motor, de la transmisión y de las medas.
830 Piensa en Java
cl ass Car
priva te fi nal int id;
private boolean
engi ne ~ false, driveTrain = fal se , wheels fa lse ¡
publi c Car (int idn) {id = idn; }
IJ Objeto Car yac i o:
public Car () {id = -1; }
publi c synchronized int getId () { re turn id¡ }
public synchron ized vo id addEngine() { engine true¡}
publ ic synchronized void addDriveTra i n () {
d riveTrain = tru e;
catch(InterruptedExc ep tion e } {
print (11 I nte rrupt ed : Chass isBuilder") ¡
catch (InterruptedException el {
print("Exiting Assemble r v i a interrupt" ) ;
ca t c h (BrokenBarrierException e) {
// Queremos que nos informen de esta excepción
throw new RuntimeException(e);
catch ( InterruptedExcept i o n e l {
print (UExiting Reporter via i nterrup t ll
);
pr i nt (UReporter off n) ¡
wh ile(!Thread.interr upted(»
perEormService() ;
assembler . barri er () . awa i t (); II Sincronizar
II Hemos fina l izado con este trabajo ...
powerDown()¡
catch(Interr uptedException e) {
print (n Exi t i ng 11 + th i s + 11 via interrupt n ) j
catch(BrokenBarr ierException e) {
I1 Queremos que nos informen de esta excepción
throw new Runtime Exception(e ) ¡
class RobotPool (
II Impide sileciosamente que e x istan entrada idén ticas :
private Set<Robot > pool = new HashSe t <Robot >() ;
public synchroni zed void add(Robot r) {
pool.add(r) i
not if yAll () ;
enfoque es la siguiente: en determinados lugares, podemos encontrar una serie de vallas dispuestas
en lugares donde existen desniveles abruptos, y junto a ellas podemos ver señales que dicen: "No se
apoye en las vallas ". Por supuesto, el auténtico propósito de esta regla no es impedirnos apoyarnos
en las vallas, sino impedirnos que nos caigamos por el acantilado. Pero "No se apoye en las vallas"
es una regla mucho más fácil de seguir que "no se caiga por el acantilado ".
Ejercicio 37: (2) Modifique CarBuilder.java para añadir otra etapa al proceso de construcción de automóviles, en la
que añadiremos el sistema de escape, los asientos y los accesorios. Al igual que con la segunda etapa,
suponga que estos procesos pueden ser realizados simultáneamente por robots.
Ejercicio 38: (3) Utilizando la técnica empleada en CarBuilder.java, modele el ejemplo de construcción de casas que
hemos comentado en este capítulo.
incr,increment() i
return System.nanoTime() - start;
23 Brian Goetz me ayudó mucho, explicándome estos problemas. Consulte su artículo en -www128.ibm.com/developerworks/library(j-jtp12214paracono-
cer más detalles acerca de las medidas de rendimiento.
836 Piensa en Java
Executors .newFixedThreadPool(N*2 ) ;
private s t a ti c Cyc licBarr i er barrier
new CyclicBarrier (N*2 + 1 ) ;
protected vol ati l e i n t i ndex : Di
protec t ed vol ati l e long value = O;
protected l ong duration = O;
protected String id = !l error ";
protected fina l static i nt SIZE = 100000;
protected static int [] preLoaded = new i nt [SIZE];
static {
// Cargar la matriz con números aleatorios:
Ran dom rand = new Random (4?);
for (in t i = O; i < SI ZE¡ i++ }
preLoaded[il = rand.next Int( } ¡
public vo id timedTest( ) {
long start = System. nanoTime() ¡
for {int i = O; i < Ni i++ ) {
exec.execu te {new Modif i er (}) i
exec.execu te (new Reader(») i
}
try (
barrier.awai t() i
catch(Exception e)
thr ow new RuntimeExcept ion(e) ¡
publi c static vo ld
report(Accumu lator accl, Accumula tor acc 2) {
p r int f ( I! %- 22s: %.2f\nl1 , accl.i d + ,, / " + acc2.id ,
(doubleJaccl.duration/(double)acc2.duration) ¡
21 Concurrencia 837
/* Output, (Sample)
Warmup
BaseLine 34237033
Cyc l es 5 00 00
BaseLine 20966632
synchronized 24326555
Lock 536 69950
Atomic 3 0552487
synchronized/Base Line 1.16
Lock/BaseLine 2 .56
Atomic/BaseLine 1.46
synchronizedJLock 0.45
synchronizedJAtomic 0.79
LockjAtomic 1.76
Cycles 100000
BaseLine 415 1 28 18
synchronized 43843003
Lock 87430386
Atomic 51 892350
synchronizedj BaseLine 1.06
LockjBaseLine 2.11
Atomic j BaseLi ne 1. 25
synch ronized/ Lock 0.50
synchronized/ Atomic 0. 84
LockjAtomic 1. 68
=============================
21 Concurrencia 839
Cycl es 200000
BaseLine 80176670
synchronized 5455046661
Lock 177686829
At omic 101789194
synchronized/BaseLine 68.04
Lock/BaseLine 2.22
Atomic/ BaseL ine 1. 27
synchronized/Lock 30.7 0
synchronized/Atomic 5 3 .5 9
Lock/Atomic 1. 75
= ===== ======= ===== ===========
Cyc l es 400000
BaseLine 1 60383513
synchronized 780052493
Lock 362 1 87652
Atomic 202030984
synchroni zed/BaseLine 4 . 86
Lock/BaseLine 2.26
Atomic/BaseLine 1. 26
synchronized/Lock 2.15
synchronized/Atomic 3.8 6
Lock/Atomic 1. 79
== ======== ==== === === ========
Cycles 800000
BaseLine 322064955
synchronized 336155014
Lock 704615531
Atomic 39323 1542
synchronized/BaseLine 1.04
Lock/BaseLine 2. 1 9
Atomic/BaseLine 1.22
synchronized/Lock 0 .47
synchroni zed/Atomic 0.85
Lock/Atomic 1. 79
== ========= ==== === ========= =
Cycles 1600 000
BaseLine 65000 4120
synchronized 52235762925
Lock 1419602771
Atomic 796950171
synchronized/Bas eLine 80.36
Lock/BaseLine 2.18
Atomic/BaseLine 1. 23
synchronized/Lock 36.80
synchronized /Atomic 65.54
Lock/Atomi c 1. 78
Cycles 3200000
BaseLine 1285664519
synchron ized 96336767661
Lock 2846988654
Atomi c 1590545726
synchronized/BaseLine 74 . 93
Lock/BaseLine 2.21
Atomic/BaseLine 1. 24
synchronized/Lock 33.84
synchronized/Atomic 60.57
Lock/At omi c 1. 79
*///,-
840 Piensa en Java
Este programa utiliza el patrón de diseño basado en plantillas24 para poner todo el código común en la clase base y aislar
todo el código variante en las implementaciones de accumulate( ) y read( ) en las clases derivadas. En cada una de las cla-
ses derivadas SynchronizedTest, LockTest y AtomicTest, podemos ver cómo accumulate( ) y read( ) expresan diferentes
formas de implementar la exclusión mutua.
En este programa, las tareas se ejecutan mediante un conjunto compartido FixedThreadPool en un intento de realizar toda
la creación de hebras al principio, impidiendo así que se pague un coste adicional durante las pruebas. Simplemente para
asegurarse, la prueba inicial se duplica y el primer resultado se descarta, porque incluye el coste de la creación inicial de
hebras.
Es necesaria una barrera CyclicBarrier porque queremos aseguramos de que todas las tareas se hayan completado antes de
declarar completa cada prueba.
Se utiliza una cláusula static para precargar la matriz de números aleatorios, antes de que den comienzo las pruebas. De esta
forma, si existe cualquier coste asociado con la generación de los números aleatorios, este coste no se reflejará durante la
prueba.
Cada vez que se invoca accumulate(), nos movemos a la siguiente posición de la matriz preLoaded (volviendo al princi-
pio cuando se alcanza el final de la matriz) y se añade otro número generado aleatoriamente a value. Las múltiples tareas
Modifier y Reader hacen que aparezca el fenómeno de la contienda para el objeto Accumulator.
Observe que, en AtomicTest, la situación es demasiado compleja como para tratar de utilizar objetos Atomic, básicamen-
te, si hay más de un objeto Atomic implicado, probablemente nos tengamos que dar por vencidos y utilizar mutex más con-
vencionales (la documentación del JDK indica específicamente que la utilización de objetos Atomic sólo funciona cuando
las actualizaciones criticas de un objeto están limitadas a una única variable). Sin embargo, hemos dejado la prueba dentro
del ejemplo para poder seguir teniendo una idea de la mejora de prestaciones que se pueden tener con los objetos Atomic.
En main( ), se ejecuta la prueba repetidamente y tenemos una opción de pedir que se ejecuten más de cinco repeticiones,
que es el valor predetenninado. Para cada repetición, se dobla el número de ciclos de prueba, de manera que podemos ver
cómo se comportan los diferentes mutex a medida que el tiempo de ejecución crece. Analizando la salida, vemos que los
resultados son bastante sorprendentes. En las primeras cuatro iteraciones, la palabra clave synchronized parece ser más efi-
ciente que la utilización de Lock o Atomic. Pero repentinamente, se cruza W1 umbral y synchronized parece ser bastante
ineficiente, mientras que Lock y Atomic parecen mantener aproximadamente las prestaciones relativas a la prueba inicial
BaseLine, llegando a ser así mucho más eficientes que synchronized.
Recuerde que este programa sólo nos proporciona una indicación de las diferencias entre las diferentes técnicas de mutex,
y que la salida del ejemplo anterior sólo indica esas diferencias en mi máquina concreta y en mis circunstancias concretas.
Como podrá ver si experimenta con el programa, encontrará significativos cambios de comportamiento cuando se utiliza un
número diferente de hebras y cuando se ejecuta el programa durante periodos de tiempo más largos. Algunas optimizacio-
nes de tiempo de ejecución no se invocan hasta que un programa ha estado ejecutándose durante varios minutos, y en el caso
de los programas servidores, algunas horas.
Dicho esto, resulta bastante claro que la utilización de Lock suele ser bastante más eficiente que la de synchronized, y tam-
bién resulta que el coste de synchronized varía ampliamente, mientras que el de Lock es relativamente estable.
¿Quiere esto decir que nunca deberíamos utilizar la palabra clave synchronized? Hay que considerar dos factores: en pri-
mer lugar, en SynchronizationComparisons.java, el cuerpo de los métodos protegidos por mutex es muy pequeño. En
general, ésta es una buena práctica: proteja sólo con mutex las secciones que sean imprescindibles. Sin embargo, en la prác-
tica, las secciones protegidas con mutex pueden tener un tamaño mayor que en el ejemplo anterior, por 10 que el porcenta-
je de tiempo que se invertirá dentro del cuerpo de los métodos, será probablemente significativamente mayor que el coste
de entrar y salir del mutex, lo que podría anular cualquier beneficio derivado de los intentos de acelerar el mutex. Por
supuesto, la única fonna de saberlo es (y sólo en el momento en que estemos realizando las actividades de rendimiento, no
antes) probar las distintas técnicas y ver el impacto que tienen.
En segundo lugar, está claro, al leer el código contenido en este capítulo, que la palabra clave synchronized permite un códi-
go mucho más legible que la sintaxis 10ck-try/finaUy-unlock que Lock requiere, y esa es la razón por la que en este capí-
tulo hemos utilizado principalmente la palabra clave synchronized. Como hemos dicho en otros lugares del libro, resulta
mucho más nonnalleer código que escribirlo (al programar resulta más importante comunicarse con otros seres humanos
que comunicarse con la computadora), por lo que la legibilidad del código es critica. Como resultado, tiene bastante senti-
do comenzar utilizando la palabra clave synchronized y cambiar únicamente a objetos Lock si la optimización del rendi-
miento lo requiere.
Finalmente, resulta bastante atractivo poder utilizar las clases Atomic en nuestros programas concurrentes, pero tenga en
cuenta, como hemos visto en SynchronizationComparisons.java, que los objetos Atonde sólo son útiles en casos muy
simples, generalmente cuando sólo hay un objeto Atomic que esté siendo modificado y cuando dicho objeto sea indepen-
diente de todos los demás objetos. Resulta más seguro comenzar con otras técnicas más tradicionales de mutex y sólo tra-
tar de cambiar a Atomic posteriormente, si así lo exige la optimización del rendimiento.
Problemas de rendimiento
Mientras que estemos principahnente leyendo de un contenedor libre de bloqueos, éste será mucho más rápido que otro
basado en synchronizcd, porque se elimina el coste de adquirir y liberar los bloqueos. Esto sigue siendo cierto en cuanto se
realiza un pequeño número de escrituras en un contenedor libre de bloqueos, aunque resultaría interesante poder hacerse una
idea de qué quiere decir eso de "un número pequeño". En esta sección trataremos de hacemos una idea aproximada de las
diferencias de rendimiento de estos contenedores bajo distintas condiciones.
Comenzaremos con un sistema genérico para la realización de pruebas sobre cualquier tipo de contenedor, incluyendo los
mapas. El parámetro genérico C representa el tipo de contenedor:
11: concurrency/Tes ter.java
II Sistema para probar el rendimiento de lo s contenedores concurrentes .
import java.util.concurrent.*¡
i mport n et.mindview.util.*¡
842 Piensa en Java
void runTest ()
endLatch = new CountDownLatch(nReaders + nWriters ) ¡
testContai ner = con t ainerlnitializer () ;
start ReadersAndWriters( } ;
try {
endLat ch . await(} ;
catch( InterruptedExcept i on ex)
System .out.println(llendLatch interrupted"} ¡
endLatch.countDown() ;
21 Concurrencia 843
void putResul ts () {
readRes ult += resul t;
readTi.me += durati on¡
void putResults (1 {
writeTime+ =durat ion;
844 Piensa en Java
1* Output : ISample)
Type Read time Wr ite time
Synched ArrayList 1 0r Ow 232158294700 O
Synched ArrayLis t 9r lw 1 98947618203 24918613399
readTime + writeTime 223866231602
Synched ArrayLis t 5r 5w 117367305062 132176613508
readTime + writeTime 249543918570
CopyOnWri t eArrayList 10 r Ow 758386889 O
CopyQnWriteArrayList 9r 1w 741305671 136145237
readTime + writeTime 877450908
CopyOnWriteArrayLi s t 5r 5w 21 27630 75 6796 7464 300
readTime + writeTime 68180227375
* jjj ,-
En ListTest, las clases Reader y Writer realizan las acciones específicas para un contenedor List<Integer>. En
Reader.putResults( ), la duración (duration) se almacena, al igual que el resultado (result), para impedir que los campos
sean optimizados por el compilador. startReadersAndWriters() se define a continuación para crear y ejecutar los objetos
lectores y escritores específicos.
21 Concurrencia 845
Una vez creada la lista ListTest, hay que heredar de ella para sustituir containerInitializer() con el fin de crear e iniciali-
zar los contenedores específicos de prueba.
En maine ), podemos ver variantes de las pruebas, con diferentes números de lectores y escritores. Podemos cambiar las
variables de prueba usando argumentos de la línea de comandos gracias a la llamada a Tester.initMain(args).
El comportamiento predeterminado consiste en ejecutar cada prueba 10 veces, esto ayuda a estabilizar la salida, que puede
variar debido a actividades propias de la máquina NM, como la optimización y la depuración de memoria 25 La salida de
ejemplo que podemos ver ha sido editada para mostrar únicamente la última iteración de cada prueba. Analizando la salida,
podemos ver que un contenedor ArrayList sincronizado tiene aproximadamente el mismo rendimiento independientemen-
te del número de lectores y escritores: los lectores contienden con otros lectores para la obtención de dos bloqueos, al igual
que hacen los escritores. Sin embargo, el contenedor CopyOn WriteArrayList es mucho más rápido cuando no hay escri-
tores y sigue siendo significativamente más rápido cuando hay cinco escritores. Parece que podemos utilizar de manera bas-
tante libre CopyOn WriteArrayList; el impacto de escribir en la lista no parece superar al impacto de sincronizar la lista
completa. Por supuesto, es necesario probar las dos técnicas en cada aplicación específica para cerciorarse de cuál es la
mejor.
De nuevo, observe que este programa no constituye una verdadera prueba de rendimiento en lo que respecta a los números
absolutos, y los resultados que obtenga en su máquina serán diferentes casi con toda seguridad. El objetivo del programa es
simplemente hacerse una idea del comportamiento relativo de los dos tipos de contenedor.
Puesto que CopyOn WriteArraySet utiliza CopyOn WriteAl'l'ayList, su comportamiento será similar y no es necesario
que hagamos aquí una prueba separada.
void putResults() {
readResult += result¡
readT i me += duration¡
25 Para ver una introducción a la realización de pruebas comparativas de rendimiento bajo la influencia de la compilación dinámica de Java, consulte www-
128. i bm. com/developerworksllibralylj-jtp 12214.
846 Piensa en Java
void putResults(}
writeTime += duration¡
void startReadersAndWriters()
for(int i = O; i < nReaders¡ i++}
exec.execute(new Reader());
for(int i = o; i < nWriters; i++}
exec,execute(new Writer()) i
1* Output: (Sample)
Type Read time Write time
Synched HashMap 10r Ow 306052025049 O
Synched HashMap 9r lw 428319156207 47697347568
readTime + writeTime = 476016503775
Synched HashMap Sr 5w 243956877760 244012003202
readTime + writeTime = 487968880962
21 Concurrencia 847
ConcurrentHashMap l Or Ow 23352654318 o
Concurren tHashMap 9r lw 18833089400 1541853224
readTime + writeTime 20374942624
ConcurrentHashMap Sr 5w 120 37625732 11850489099
readTime + writeTime 23888114831
* /// , .
El impacto de añadir escritores a un mapa ConcurrentHashMap es todavia menos evidente que para CopyOn Write-
ArrayList, pero eso es porque ConcurrentHashMap emplea uua técnica distinta que minimiza claramente el impacto de
las escrituras.
Bloqueo optimista
Aunque los objetos Atomic realizan operaciones atómicas como decrementAndGet( ), algunas clases Atomic también nos
permiten realizar lo que se denomina "bloqueo optimista". Esto quiere decir que no se utiliza realmente un mutex cuando
se está realizando un cálculo, sino que, después de que el cálculo ba acabado y estamos listos para actualizar el objeto
Atomic, se usa un método denominado compareAndSet( ). Lo que se hace es entregar a este método el antiguo valor y el
nuevo valor, y si el antiguo valor no concuerda con el que está almacenado en el objeto Atomic, la operación falla: esto sig-
nifica que alguna otra tarea ha modificado el objeto mientras tanto. Recuerde que normalmente 10 que haríamos es utilizar
un mutex (synchronized o Lock) para impedir que más de una tarea pudiera modificar un objeto al mismo tiempo, pero lo
que estamos haciendo aquí es ser "optimistas" dejando los datos desbloqueados y esperando que ninguna otra tarea llegue
mientras tanto y nos modifique. De nuevo, todo esto se hace en aras del rendimiento: utilizando Atomic en lugar de
synchronizcd o Lock, podemos .aumentar las prestaciones.
¿Qué ocurre si falla la operación compareAndSet()? Aquí es donde el asunto se complica, y sólo podemos aplicar esta téc-
nica a aquellos problemas que puedan adaptarse a una serie de requisitos. Si fallara compareAndSet( ), tenemos que deci-
dir qué hay que hacer; este aspecto es muy importante, porque si no hay nada que podamos hacer para recuperamos de este
hecho, entonces no se puede emplear esta técnica y es preciso utilizar en su lugar mutex convencionales. Quizás podamos
reintentar la operación y no pase nada si ésta tiene éxito a la segunda. O quizá sea perfectamente adecuado ignorar el fallo:
en algunas simulaciones, si se pierde un punto de datos, esto no tiene ninguna importancia dentro del esquema general de
las cosas (por supuesto, debemos entender nuestro modelo lo suficientemente bien como para saber si esto es cierto).
Considere una simulación ficticia, compuesta de 100.000 "genes" de longitud 30; quizá pudiera tratarse del principio de
alguna especie de algoritmo genético. Suponga que para cada "evolución" del algoritmo genético se realizan algunos cálcu-
los muy costosos, por lo que decidimos emplear una máquina multiprocesador con el fm de distribuir las tareas y mejorar
las prestaciones. Además, utilizamos objetos Atomic en lugar de objetos Lock para evitar el coste asociado de los mutex
(naturalmente, sólo habremos llegado a esta solución después de escribir el código de la forma más simple posible, utilizan-
do la palabra clave synchronized; una vez que tenemos el programa ejecutándose, descubrimos que es demasiado lento y
empezamos a usar técnicas de optimización). Debido a la naturaleza de nuestro modelo, si se produce una colisión durante
un cálculo, la tarea que descubra la colisión puede limitarse a ignorarla, sin actualizar el valor. He aqu! el aspecto que ten-
dría la solución:
JI : con currency/FastS imulati on.java
import java . util.concurrent .*¡
import java.ut il.concurrent .atomic.*¡
import j ava.ut il.*;
i mpert static net. mindv iew. u til.Print . *¡
ReadWriteLock
Los bloqueos ReadWrlteLock optimizan aquellas situaciones en las que escribimos en una estructura de datos de manera
relativamente infrecuente, pero tenemos múltiples tareas leyendo a menudo de la misma. ReadWriteLock permite tener
múltiples lectores simultáneamente siempre y cuando ninguno de ellos esté intentando realizar una escritura. Si alguien
adquiere el bloqueo de escritura, no se permite ninguna lectura hasta que el bloqueo de escritura se libere.
Es bastante incierto si RcadWriteLock permite mejorar la velocidad del programa, ya que esto depende de cuestiones como
la frecue ncia con que se leen los datos, comparada con la frecuencia con que se modifican; la duración de las operaciones
de lectura y escritura (el bloqueo es más complejo, por lo que con operaciones de corta duración no se percibiría ninguna
ventaja); la frecuencia con que se producen contiendas entre las hebras; y el hecho de si estamos trabajando con una máqui-
na multiprocesador o no. En último término, la única forma de saber si podemos obtener alguna ventaja con Read-
WriteLock consiste en comprobarlo.
He aquí un ejemplo que muestra únicamente el uso más básico de los bloqueos ReadWriteLock:
21 Concurrencia 849
JI: concurrency/ReaderWriterList.java
i mpor t j ava . util.concurrent.*¡
i mport j ava . util.concurrent.locks.*¡
import java .util.*;
import stati c net.mindview.util . Print.*¡
c l ass ReaderWriterListTest {
ExecutorService exec = Executors.newCachedThreadPool {) ;
private final static i n t SIZE : 1 00 ;
pri vate static Random r and = new Random( 47 l ;
privat e ReaderWriterList< Int eger> list =
ne w Rea de rWriterList<Int eger> (SIZE, O);
private class Writer imp l emen ts Runnable {
public void run (1 (
try (
for( i n t i = O; i < 20; i ++) { // Prueba de 2 segundos
list .set(i, rand.nex t ln t();
TimeUnit.MILLISECONDS . sleep{l OO) ;
exec.shutdownNow () ;
Objetos activos
Después de estudiar este capítulo, habrá observado que el mecanismo de gestión de hebras en Java parece bastante comple-
jo y dificil de usar correctamente. Además, puede parecer que es un mecanismo que reduce la productividad: aunque las
tareas funcionan en paralelo, es necesario hacer un gran esfuerzo para implementar técnicas que impidan que dichas tareas
interfieran entre sí.
Si alguna vez ha escrito programas en lenguaje ensamblador, la escritura de programas multihebra produce una sensación
similar: todos los detalles importan, nosotros somos los responsables de todo y no existe ninguna red de seguridad en forma
de comprobaciones realizadas por el compilador.
¿Podria ser que existiera un problema con el propio modelo de hebras? Después de todo, este modelo proviene, con pocas
modificaciones del mundo de la programación procedimental. Quizá exista un modelo diferente de concurrencia que enca-
je mejor con la programación orientada a objetos.
21 Concurrencia 851
Una técnica alternativa es la basada en los denominados objetos activos o actores. 26 La razón de que ruchos objetos se deno-
minen "activos" es que cada objeto mantiene su propia hebra funcional y su propia cola de mensajes, y todas las solicitudes
a cada objeto se ponen en cola para procesarlas de una en lUla. Por tanto, con los objetos activos, serializamos los mensajes
en lugar de los métodos , lo que significa que no necesitamos protegemos frente a aquellos problemas que smgen cuando se
interrumpe una tarea en mitad de su bucle.
Cuando enviamos un mensaje a un objeto activo, dicho mensaje se transforma en una tarea que se inserta en la cola del obje-
to, para ejecutarla en algún instante posterior. La clase Future de Java SES resulta útil para implementar este esquema. He
aquí un ejemplo simple que dispone de dos métodos que ponen en cola las llamadas a método:
JI : c onc urrencyjAc tiveObjectDemo.java
/I Sólo se pueden pasar constantes, imrnutables, "obj etos
II desconectados" , u otros objetos ac tivos como argumento s
II a métodos así n cronos.
import j ava. uti l .conc ur ren t . * ¡
i mport j ava .uti l .*¡
import stac i c net.mindview . util.P rint .*¡
26 Gracias a ABen Holub por dedicarme el tiempo necesario para explicarme este tema.
852 Piensa en Java
new copyOnWriteArrayList<Future<?»();
for(float f ~ O.Of; f < 1.0f; f +~ 0.2f)
results.add(dl.calculateFloat(f, f));
for(int i "" o; i < s; i++)
results.add(dl.calculatelnt(i, i));
print (HAll asynch calls made ll ) ;
while(results.size() > O) {
for(Future<?> f : results)
if(f.isDone()) {
try {
print (f .get ());
catch(Exception e) {
throw new RuntimeException(e) ;
results.remove(f) ;
dI . shutdown () ;
Para impedir un acoplamiento inadvertido entre hebras, los argumentos que se pasen a una llamada a método de objeto acti-
vo deben ser bien de sólo lectura, bien objetos activos, o bien objetos desconectados (esto es terminología del autor), que
son objetos que no tienen ninguna conexión con ninguna otra tarea (resulta difícil imponer esta regla, porque no hay ningu-
na estructura para soportarla.
Con los objetos activos:
1. Cada objeto tiene su propia hebra funcional.
2. Cada objeto mantiene un control total sobre sus propios campos (lo que es algo más riguroso que las clases nor-
males, que no sólo disponen de la opción de proteger sus campos).
3. Toda la comunicación entre objetos activos toma la forma de mensajes intercambiados entre esos objetos.
4. Todos los mensajes entre objetos activos se ponen en cola.
Los resultados son muy atractivos, puesto que un mensaje de un objeto activo a otro sólo puede ser bloqueado por el retar-
do a la hora de ponerlo en cola, y puesto que dicho retardo es siempre muy corto y no depende de ningún otro objeto. el
envío de un mensaje no es bloqueable en la práctica (lo peor que puede pasar es un corto retardo). Puesto que un sistema
basado en objetos activos sólo se comunica a través de mensajes, no puede suceder que dos sucesos se bloqueen mientras
contienden por invocar un objeto de otro método. y esto significa que puede llegar a producirse un interbloqueo, lo cual ya
es un gran paso adelante. Puesto que la hebra funcional dentro de un objeto activo sólo ejecuta un mensaje cada vez, no hay
contienda por los recursos y no tenemos que preocupamos por sincronizar los métodos. La sincronización se sigue produ-
ciendo, pero tiene lugar en el nivel de mensajes, poniendo en cola las llamadas a métodos de modo que todas se procesen
de una en una.
Lamentablemente. sin un soporte directo del compilador, la técnica de codificación mostrada anteriormente resulta dema-
siado engorrosa. Sin embrago, hay que resaltar que se está progresando mucho en el campo de los objetos activos y actores
y, lo que es más interesante, en el campo de la denominada programación basada en agentes. Los agentes son, en la prác-
tica, objetos activos, pero los sistemas de agentes también soportan mecanismos de transparencia entre redes y máquinas.
No me sorprendería nada que la programación basada en agentes llegara a convertirse en el sucesor de la programación
orientada a objetos, porque combina los objetos con una solución de concurrencia relativamente sencilla.
Puede encontrar más información sobre los objetos activos, los actores y los agentes buscando en la Web. En particular,
algnnas de las ideas subyacentes a los objetos activos provienen de la Teoria de Procesos Secuenciales Comunicantes (CSP,
Cammunicating Sequential Pracesses) de C.A.R. Hoare.
Ejercicio 41: (6) Añada una rutina de tratamiento de mensajes a ActiveObjectDemo.java que no tenga ningún valor de
retomo e invóquela desde maine ).
Ejercicio 42: (7) Modifique WaxOMatic.java para implementar objetos activos.
Proyecto: 27 Utilice anotaciones y Javassist para crear una anotación de clase @Active que transforme la clase indica-
da en un objeto activo.
Resumen
El objetivo de este capítulo era proporcionar las bases de la programación concurrente con hebras en Java, de modo que el
lector entendiera que:
1. Pueden ejecutarse múltiples tareas independientes.
2. Hay que considerar todos los posibles problemas que pueden producirse cuando estas tareas terminan.
3. Las tareas pueden interferir entre sí por el uso de recursos compartidos. El mutex (bloqueo) es la herramienta
básica utilizada para impedir estas colisiones.
4. Las tareas pueden interbloquearse si no se diseñan cuidadosamente.
27 Los proyectos son sugerencias que pueden utilizarse, por ejemplo, como proyectos de fin de curso. Las soluciones a los proyectos no se incluyen en la
Guia de soluciones.
854 Piensa en Java
Resulta vital aprender cuándo hay que utilizar la concurrencia y cuándo hay que evitarla. Las principales razones para
emplearla son:
• Gestionar una serie de tareas que al entremezclarlas utilizarán la computadora de manera más eficiente (incluyen_
do la capacidad de distribuir transparentemente las tareas entre múltiples procesadores).
• Permitir una mejor organización del código.
• Aumentar la comodidad para el programador.
El ejemplo clásico de equilibrado de recursos consiste en utilizar el procesador durante la espera de E/S. La mejora en la
organización de código suele manifestarse de forma especialmente clara en las simulaciones. El ejemplo clásico de mejora
de la comodidad para los programadores es el de monitorizar un botón de "parada" durante las descargas de larga duración
a través de la red.
Una ventaja adicional de las hebras es que proporcionan cambios de contexto de ejecución "ligeros" (del orden de 100 ins-
trucciones) en lugar de cambios de contexto de proceso "pesados" (miles de instrucciones). Puesto que todas las hebras de
un proceso dado comparten el mismo espacio de memoria, un cambio de contexto ligero sólo modifica el punto de ejecu-
ción del programa y las variables locales. Un cambio de proceso (el cambio de coutexto pesado) debe intercambiar el espa-
cio de memoria completo.
Las principales desventajas de los mecanismos multihebra son:
1. Se produce una ralentización mientras las hebras están esperando a utilizar los recursos compartidos.
2. Se requiere un gasto adicional de procesador para gestionar las hebras.
3. Las decisiones de diseño inadecuadas conducen a un incremento de la complejidad sin que se obtenga a cambio
ninguna ventaja.
4. Se abre la puerta a patologías tales como la inanición de hebras, las condiciones de carrera, los interbloqueos y
los bloqueos activos (múltiples hebras están realizando tareas individuales que el sistema no es capaz de termi-
nar).
5. Pueden aparecer incoherencias entre las distintas plataformas. Por ejemplo, al desarrollar algunos de los ejemplos
de este libro, descubrí condiciones de carrera que aparecían rápidamente en algunas computadoras, pero que sin
embargo no aparecían en otras. Si desarrolla un programa en estas últimas, podría encontrarse con desagradables
sorpresas a la hora de distribuir el programa.
Una de las mayores dificultades con las hebras se produce cuando hay más de una tarea compartiendo un recurso (como por
ejemplo, la memoria de un objeto) y es necesario asegurarse de que no haya múltiples tareas intentando leer y modificar el
recurso al mismo tiempo. Esto requiere un uso juicioso de los mecanismos disponibles de bloqueo (por ejemplo, la palabra
clave synchronized). Estos mecanismos son herramientas esenciales, pero es necesario comprenderlos en profundidad, por-
que podrian introducirse inadvertidamente en situaciones de posible interbloqueo.
Además, la aplicación de hebras es todo un arte. Java está diseñado para permitimos crear tantos objetos como necesitemos
para resolver nuestro problema, al menos en teoría (la creación de millones de objetos para un análisis de elementos fmitos
en Bioingeniería, por ejemplo, podría no resultar práctica en Java sin utilizar el patrón de diseño Peso mosca). Sin embar-
go, parece que existe un límite superior al número de hebras que pueden crearse, porque llegados a un cierto número las
hebras ralentizan el sistema. Este punto crítico puede ser dificil de detectar y dependerá a menudo del sistema operativo y
de la máquina NM; puede ser menor de cien o encontrarse en el rango de los miles. Puesto que muy a menudo sólo crea-
remos un puñado de hebras para resolver un problema, este límite no suele ser una restricción, pero en problemas de dise-
ño más generales sí que esa restricción puede obligamos a agregar un esquema de concurrencia cooperativo.
Independientemente de lo simple que pueda parecer la programación multihebra empleando una biblioteca o un lenguaje
concretos, considere ese tipo de programación como una tecnología realmente avanzada. Siempre hay algo que puede esta-
llamos en la cara cuando menos 10 esperemos. La razón de que la cena de los filósofos resulte interesante es que se puede
ajustar de manera que el interbloqueo aparezca muy raramente, dándonos la impresión de que todo está bien diseñado.
En general, utilice las hebras con cuidado y con mesura. Si sus programas multihebra son muy grandes y complejos, valo-
re utilizar un lenguaje como Erlang. Éste es uno de los varios lenguajes funcionales especializados en el tema de hebras.
Puede que sea posible emplear uno de esos lenguajes para aquellas partes del programa que necesiten soporte multibebra,
21 Concurrencia 855
si es que está escribiendo muchos de estos programas y su nivel de complicación es lo suficientemente alto como para jus-
tificar esta solución.
Lecturas adicionales
Lamentablemente, hay mucha información errónea acerca de la concurrencia; esto constituye una constatación de 10 confu-
so que puede llegar a ser este tema, y lo fácil que resulta pensar que se comprenden adecuadamente los problemas (y sé de
lo que hablo porque ya he pensado muchas veces anterionnente que había comprendido perfect.1mente el tema de las hebras
y no tengo ninguna duda de que volveré a tener esa misma errónea sensación en el futuro). Siempre es necesario abord~
con una cierta mirada crítica cada nuevo documento que encontremos acerca del tema de concurrencia, para intentar enten-
der hasta qué punto la persona que ha escrito el documento comprende este tema adecuadamente. He aquí algunos libros
que podemos considerar como fiables:
Java Concurrency in Practice, por Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes y Doug Lea
(Addison-Wesley, 2006). Básicamente, es el "quién es quién" dentro del mundo de la programación multihebra en Java.
Concurrent Programming in Java, Second Edition, por Doug Lea (Addison-Wesley, 2000). Aunque este libro se basa sig-
nificativamente en Java SES, buena parte del trabajo de Doug se utilizó para crear las nuevas bibliotecas java.utll.concu-
rrent, así que este libro resulta esencial para comprender a fondo los problemas de concurrencia. Va más allá de la
concurrencia en Java y analiza el estado actual de la técnica en lo que respecta a lenguajes y tecnologías. Aunque algunas
de las partes pueden resultar un tanto obtusas, merece la pena leerlo varias veces (dejando preferiblemente unos meses entre
una lectura y otra, para poder interiorizar la infonnación). Doug es una de las pocas personas del mundo que comprende
realmente la concurrencia, así que merece la pena abordar la lectura de esta obra.
The Java Language Specificatioll, Third Editioll (Capítulo 17), por Gosling, Joy, Steele y Bracha (Addison-Wesley, 2005).
La especificación técnica, está disponible como documento electrónico en http://java.sun.com/docs/books/jls.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico The Thinking in Java Annotated So/urjon Guide, disponible para
la venta en www.MindView.net.
Interfaces gráficas
de usuario
Una directriz de diseño fundamental es: "Hacer fáciles las cosas simples y
hacer posibles las cosas dificiles".l
El objetivo de diseño original de la biblioteca de interfaces gráficas de usuario (GUI, graphical user interface) de Java 1.0
era permitir al programador construir una interfaz GUI que tuviera un buen aspecto en todas las plataformas. Ese objetivo
no se consiguió. En su lugar, la herramientaAWT (Abstract Windowing Too/kit, herramienta abstracta de ventanas) de Java
producía una GUI con un aspecto igualmente mediocre en todos los sistemas. Además, era bastante restrictiva; sólo se uti-
lizaban cuatro tipos de fuentes y no se podía acceder a ninguno de los elementos GUI sofisticados que existen en el siste-
ma operativo. El modelo de programación AWT de Java era asimismo bastante obstuso y no estaba orientado a objetos. Un
estudiante de uno de mis seminarios (que había estado en Sun durante la creación de Java) nos explicó por qué: la herra-
mienta AWT original había sido concebida, diseñada e implementada en un solo mes. Se trata, ciertamente, de una produc-
tividad maravillosa, y también de una lección instructiva sobre por qué es importante el diseílo.
La situación mejoró con el modelo de sucesos AWT de Java 1.1, que adopta un enfoque orientado a objetos, mucho más
claro, junto con la adición de JavaBeans, un modelo de programación con componentes que está orientado a facilitar la crea-
ción de entornos de programación visuales. Java 2 (JDK 1.2) completó la transformación desde el modelo AWT antiguo de
Java 1.0 sustituyendo esencialmente casi todo con las clases JFC (Java Foundation Classes), cuya parte para diseño de inter-
faces GUI se denomina "Swing". Se trata de un rico conjunto de componentes JavaBeans fáciles de usar y de entender que
pueden arrastrarse y colocarse (además de programarse de forma manual) para crear una GUI razonable. La regla de la
"tercera revisión" aplicable al sector del software (un producto no es bueno hasta la tercera revisión) parece tener vigencia
también en el campo de los lenguajes de programación.
Este capítulo presenta la moderna biblioteca Swing de Java y se basa en la razonable suposición de que Swing es la biblio-
teca GUI que Sun tiene prevista como destino final para Java 2 Si por alguna razón necesita utilizar el modelo AWT
"antiguo" (porque tiene que soportar código heredado o debido a limitaciones del explorador web), puede encontrar una
introducción a dicho modelo en la primera edición de este libro, descargable en www.MindView.net. Observe que algunos
componentes AWT continúan estando presentes en Java y en algunas situaciones es preciso utilizarlos.
Tenga en cuenta que este capítulo no constituye un glosario completo ni de todos los componentes Swing ni de todos los
métodos de las clases descritas. Lo que pretendemos es proporcionar una introducción sencilla. La biblioteca Swing es enor-
me y el objetivo de este capítulo sólo es familiarizar al lector con los aspectos esenciales y hacer que se sienta cómodo con
los conceptos. Si necesita ir más allá de lo que aquí se cuenta, probablemente Swing le permita resolver el problema que
tenga entre manos, aunque tendrá que investigar un poco acerca del tema.
Presuponemos aquí que el lector ha descargado e instalado la documentación del JDK desde http://java.sun.com y que tiene
la posibilidad de examinar las clases javax.swing contenidas en la documentación para conocer los detalles concretos y los
métodos de la biblioteca Swing. También puede buscar información en la Web, aunque el mejor lugar para comenzar es el
propio tutorial de Swing elaborado por Sun disponible en http://java.slln.com/docslbooks/tllloria//uiswing.
I Una variante de esta regla es la que se denomina "el principio de menor estupefacción" que esencialmente dice: "No sorprendas al usuario",
2 Observe que IBM ha creado una nueva biblioteca GUl de código abierto pata su editor Eclipse (www.Eclipse.org) , que podemos evaluar como alterna-
tiva de Swing. Presentaremos esta alternativa posteriormente en el capitulo.
8S8 Piensa en Java
Existen numerosos (y voluminosos) libros dedicados específicamente a Swing, y puede consultar alguno de ellos si necesi-
ta analizar los temas con mayor profundidad o si quiere modificar el comportamiento predctemlinado de Swing.
A medida que estudiemos Swing, verá que:
1. Swing es un modelo de programación enormemente mejorado, si se compara con muchos otros lenguajes y entor_
nos de desarrollo (no queremos decir que sea perfecto, pero sí que representa un gran paso adelante). JavaBeans
(del que hablaremos hacia el final del capítulo) es el marco de trabajo para dicha biblioteca.
2. Los "constructores de interfaces GUI" (entornos de programación visual) fannan una parte fundamental de cual-
quier entorno completo de desarrollo en Java. JavaBeans y Swing permiten al construc tor de interfaces GUl escri-
bir código automáticamente a medida que colocamos los componentes sobre fonnularios empleando herramientas
gráficas. Esto pennite acelerar el desarrollo enonnemente durante la construcción de interfaces GUI, y pennite
una mayor dosis de experimentación y, por tanto, la posibilidad de probar más diseños y, presumiblemente, ter-
minar adoptando mejores diseños.
3. Puesto que Swing es razonablemente sencillo, aún cuando uti licemos un constructor de interfaces GUI en lugar
de realizar la codificación a mano, el código resultante debería seguir siendo comprensible. Esto resuelve uno de
los mayores problemas que los constructores de interfaces GUl presentaban en el pasado, que era que generaban
fáci lmente código ciertamente ilegible.
Swing contiene todos los componentes que cabría esperar ver en una interfaz de usuario moderna: desde botones con imá-
genes a árboles y tablas. Se trata de una biblioteca enom1emente grande, pero ha sido diseñada de tal fonna que su comple-
jidad resulta apropiada para la tarea que tengamos entre manos; si esa tarea es simple no es necesario escrib ir código, pero
a medida que tratamos de hacer cosas más co mpl ejas, la com plejidad del código se incrementa proporcionalmente.
Uno de los aspectos más atractivos de Swing es lo que podríamos denominar "ortogona lidad de uso". Este ténnino quiere
decir que, una vez que entendemos las ideas generales acerca de la biblioteca, usualmente podemos aplicarl as en todas par-
tes. Debido principalmente a los convenios estándar de denominación, mientras escribíamos ejemplos de este capitulo com-
probé que nonnalmente se podía adivinar con precisión qué es lo que cada método hacía basándose en su nombre.
C ienamente, se trata de todo un hito en lo que respecta al diseño adecuado de bibliotecas de programación. Además, pode-
mos generalmente complementar unos componentes con otros y las cosas funcionarán correctamente.
La navegación mediante el teclado es automática; podemos ejecutar una ap licación Swing utilizando el ratÓn y esto no
requiere ningún esfuerzo de programación adicional. El soporte de desplazamiento de pantalla es muy sencillo; basta con
insertar el componente en un panel JScrollPane en el momento de añadirlo al fonnulario. Características tales como las
sugerenci as de pantalla requieren, nonnalmente, una única línea de código.
En aras de la portabilidad, Swing está escrito entera mente en Java.
Swing soporta también una funcionalidad bastante radical denominada '"'aspecto y esti lo seleccionables", que quiere decir
que la apari encia de la interfaz de usuario puede modificarse dinámicamente para ajustarl a a las expectativas de los usua-
rios que está n trabajando bajo diferentes platafomlas y sistemas operativos. Resulta incluso posible (aunque algo complica-
do) inventar nuestros propios aspecto y estilo de interfaz. En la Web pueden encontrarse algunos ejemplos de modelos
básicos de interfaz. 3
A pesar de todos sus aspectos positivos, Swing no es para todos los programadores. ni tampoco ha podido resolver todos los
problemas de interfaz de usuario que los diseñadores querían. Al fina l del capítulo, examinaremos dos soluciones alternati-
vas a Swing: el modelo SWT patrocinado por IBM, desarrollado para el editor Eclipse que está disponible de manera gra-
tuita como biblioteca OUI autónoma de código abieno y la herramienta F1ex de Macromedia para el desarrollo de intcrfaces
Flash del lado del cliente para aplicaciones web.
Applets
Cuando apa reció Java, una pane de la publicidad que recibió el lenguaje se debia al concepto de applet, un programa que
puede distribuirse a través de In ternet para ejecutarlo dentro de un ex plorador web (dentro de un entorno de seguridad).
) Mi ejemplo favorito es el modelo "Servilleta" de Kcn Amold que hace que las ventanas parezcan dibujadas en una servi lleta. Véase Imp://napkilllajsollr-
ceforge.net.
22 Interfaces gráficas de usuario 859
La gente pensó que el applet Java era el paso siguiente en la evolución de Internet, y muchos de los libros originales sobre
Java presuponían que la razón por la que uno podía estar interesado en el lenguaje era para escribir applets.
Por diversas razones, esta revolución nunca llegó a suceder. Buena parte del problema es que la mayoría de las máquinas
no incluye el software Java necesario para ejecutar applets, y descargar e instalar un paquete de 10 MB para poder ejecutar
algo que hemos encontrado casualmente en la Web no es algo que la mayoría de los usuarios estén dispuestos a hacer.
Muchos usuarios se muestran incluso atemorizados ante esa idea. Los applets Java como sistema de distribución de aplica-
ciones del lado del cliente nunca consiguieron una masa crítica, y ailllque ocasionalmente podemos seguir encontrándonos
con applets, estos componentes se han visto relegados, en general, al rincón de la historia de la informática.
Esto no significa que los applets no sean una tecnología interesante y valiosa. Si nos encontramos en una situación en la que
podamos garantizar que los usuarios tienen instalado un entorno JRE (como por ejemplo dentro de un entorno corporativo),
entonces los applets (o JNLP/Java Web Start, que se describe posterionnente en el capítulo) pueden ser la [onna perfecta de
distribuir programas cliente y de actualizar automáticamente la máquina de todos los usuarios sin el coste y el esfuerzo habi-
tualmente necesarios para distribuir e instalar un nuevo software.
Puede encontrar una introducción a la tecnología de applets en los suplementos (en inglés) en línea de este libro en
wl1lW.MindView.net.
Fundamentos de Swing
La mayoría de las aplicaciones Swing se construyen dentro de un marco JFrame, que crea la ventana en el sistema opera-
tivo que estemos empleando. El título de la ventana se puede fijar utílizando el constructor JFrame de la [onna siguiente:
jj: guijHelloSwing.java
import javax.swing.*;
setDefanltCloseOperation( ) le dice a JFrame lo que debe hacer cuando el usuario ejecute una operación de cierre. La
constante EXIT _ ON_CLOSE le dice que salga del programa. Sin esta llamada, el comportamiento predetenninado consis-
te en no hacer nada, por lo que la aplicación no se cerraría.
setSize( ) establece el tamaño de la ventana en píxeles.
Observe la última línea:
frame.setVisible(true) ;
Sin ella, no podríamos ver nada en la pantalla.
Podemos hacer las cosas algo más interesantes añadiendo una JLabel al marco JFrame:
jj: guijHelloLabel.java
import javax.swing.*;
import java.util.concurrent.*i
TimeUnit.SECONDS.sleep {l );
label. setText ("Hey! This is Different! 11) ;
}
/// , -
Después de un segundo, el texto de JLabel cambia. Aunque esto resulta entretenido y seguro, teniendo en cuenta la trivia-
lidad del programa, no resulta en realidad una buena idea que la hebra main( 1 escriba directamente en los componentes
GUI. Swing dispone de su propia hebra dedicada a recibir sucesos de la interfaz de usuario y a actualizar la pantalla. Si
comenzamos a manipular la pantalla con otras hebras, podemos encontramos con las colisiones e interbloqueos descritos en
el Capítulo 21, Concurrencia.
En lugar de ello, otras hebras (como aquí main( II deben enviar las tareas para que sean ejecutadas por la hebra de despa-
cho de sucesos de Swing 4 Para hacer esto, entregamos una tarea a SwingUtilities.invokeLater( l, que la coloca en la cola
de sucesos para que la ejecute (en algún momento) la hebra de despacho de sucesos. Si hacemos esto con el ejemplo ante-
rior, lo que obtendríamos es:
// : gui j Submi tLabe l ManipulationTask .j ava
import javax.swing .*¡
import java.util.concurrent.*¡
5 Esta práctica fue añadida en Java SES, por lo que podrá encontrar multitud de programas más antiguos que no se ajustan a ella. Eso no quiere decir que
los autores de esos programas fueran ignorantes. Las prácticas sugeridas parecen estar en constante evolución.
22 Interfaces gráficas de usuario 861
Observe que la llamada a sleep( ) no se encuentra dentro del constructor. Si la ponemos abí, el texto de la etiqueta JLabel
original nunca aparecerá, debido a que el constructor no se completa hasta que la llamada a sleep( ) finaliza y se inserta la
nueva etiqueta. Además, si la llamada a sleep( ) se encuentra dentro del constructor, o dentro de cualquier operación de la
interfaz de usuario, quiere decir que estaremos suspendiendo la hebra de despacho de sucesos durante la ejecución de
sleep(), lo que generalmente no es una buena idea.
Ejercicio 1: (l) Modifique HelloSwing.j ava para demostrar que la aplicación no se cerrará sin la llamada a
setDefaultCloseOper atioo( ).
Ejercicio 2: (2) Modifique HeDoLabel.java para demostrar que la adición de etiquetas es dinámica, añadiendo un
número aleatorio de etiquetas.
Un entorno de visualización
Podemos combinar las ideas anteriores y reducir la redundancia del código creando un entorno de visualización para em-
plearlo en los ejemplos Swing del resto del capítulo:
/1 : net / mindview /util/SwingConsole .java
// Herramient a para ejecutar ejemplos de Swing desde
// l a consola, tanto app l ets como marcos JFrame.
package net.mi ndview . util¡
import javax. s wing.*;
Dado que el lector podría querer utilizar esta herramienta en otras situaciones, la hemos incluido en la biblioteca oet.mind-
vi.w.utí!. Para utilizarla, la aplicación debe estar dentro de un marco J Frame (como lo están todos los ejemplos del libro).
El método estático run ( ) establece el título de la ventana asignándole el nombre simple de la clase del marco JFrame.
Ejercicio 3: (3) Modifique SubmitSwiogProgram.java para utilizar SwingConsole.
862 Piensa en Java
Definición de un botón
Defmir un botón es bastante simple: basta con invocar el constructor JButton con la etiqueta que queramos incluir en el
botón. Veremos posteriormente que se pueden hacer otras cosas más atractivas, como por ejemplo incluir imágenes gráficas
en los botones.
Usualmente, lo que haremos será crear un campo para el botón dentro de nuestra clase, con el fin de poder hacer referencia
al mismo posteriormente.
JButton es un componente (con su propia ventana de pequeño tamaño) que se volverá a pintar automáticamente como parte
de cada actualización. Esto quiere decir que no hace falta pintar explícitamente los botones ni ningún otro tipo de control;
basta con incluirlos en el formulario y dejar que ellos mismos se encarguen de pintarse. Normalmente, para insertar un botón
en un formulario, lo haremos dentro del constructor:
11: gui/Buttonl.java
II Inclusión de botones en una aplicación Swing.
import javax.swing.*¡
import java.awt.*¡
import static net.mindview.util.SwingConsole.*¡
Captura de un suceso
Si compilamos y ejecutamos el programa anterior, no sucede nada cuando apretamos los botones. Aquí es donde debemos
intervenir y escribir algo de código para determinar qué es lo que tiene que suceder. La base fundamental de la programa-
ción dirigida por sucesos, que. están muy relacionados con el comportamiento de ooa interfaz GUI, consiste en conectar los
sucesos con el código que debe responder a esos sucesos.
La manera para llevar esto a cabo en Swing consiste en separar limpiamente la interfaz (los componentes gráficos) de la
implementación (el código que queramos ejecutar cuando un suceso afecte a un cierto componente). Cada componente
Swing puede informar de todos los sucesos que le ocurran, pudiendo informar de cada tipo de suceso individualmente. Por
tanto, si no estamos interesados en, por ejemplo, si se ha desplazado el ratón por encima del botón, no registraremos nues-
tro interés en dicho suceso. Se trata de una forma muy sencilla y elegante de gestionar la programación dirigida por suce-
sos, y una vez que se entiendan los conceptos básicos, pueden emplearse fácilmente componentes Swing que no se hayan
visto anteriormente; de hecho, este modelo puede abarcar cualquier cosa que se pueda clasificar como un componente
JavaBean (de los que hablaremos más adelante en el capítulo).
22 Interfaces gráficas de usuario 863
Al empezar, vamos a centrarnos solamente en el suceso de interés principal para los componentes que estemos utilizando.
En el caso de un botón JButton, este "suceso de interés" es que se pulse el botón. Para registrar nuestro interés en la pul-
sación de un botón, invocamos el método addActionListener() de JButton. Este método espera un argumento que es un
objeto que implemente la interfaz ActionListener. Dicha interfaz contiene un único método denominado
actionPerformed(). Por tanto, para asociar código con un botón JButton, implemente la interfaz ActionListener en una
clase y registre un objeto de dicha clase ante JButton mediante addActionListener( ). Entonces, se invocará el método
actionPerformed() cada vez que se presione el botón (normalmente esto se denomina retrollamada) .
Pero, ¿cuál debe ser el resultado de presionar ese botón? Nos gustaría que algo cambiara en la pantalla, así que introducire-
mos un nuevo componente Swing: el campo de texto JTextField. Este campo es un lugar en el que el usuario final puede
escribir texto o, en este caso, en el que el programa puede insertar texto. Aunque hay varias fonnas de crear un campo
JTextFleld, la más simple consiste en informar al constructor sobre cuál es la anchura que queremos que tenga ese campo.
Uoa vez colocado el campo JTextFleld en el formulario, podemos modificar su contenido utilizando el método setText( )
(existen muchos otros métodos eo JTextField, y puede encontrar la información correspondieote eo la documentación del
JDK disponible eo http://java.sun.com) . He aquí UD ejemplo:
Para crear un campo JTextField y colocarlo en el formulario hay que realizar los mismos pasos que para JButton o para
cualquier otro componente Swing. La difereocia en el programa anterior radica en la creación de la cIase ButtonListener
que implementa la interfaz ActionListener antes mencionada. El argumento de actionPerformed( ) es de tipo
ActionEvent, que contiene toda la información acerca del suceso y del lugar en que se ha producido. En este caso, queria-
mas describir el botón que ha sido presionado; getSource( ) devuelve el objeto en el que se ha originado el suceso y hemos
supuesto (utilizando una proyección de tipos) que el objeto es de tipo JButton. El método getText() devuelve el texto aso-
ciado al botón, el cual se coloca en el campo JTextField para demostrar que verdaderamente se ha invocado el código en
el momento de pulsar el botón.
En el constructor, se utiliza addActionListener( ) para registrar el objeto ButtonListener aute ambos botones.
864 Piensa en Java
A menudo resulta más cómodo programar la clase ActionListener como una clase interna anónima, especialmente, dado
que lo nonnal es utilizar una única instancia de cada una de estas clases. Podemos modificar Button2.java para utilizar UDa
clase interna anónima de la forma siguiente:
// : gui /Bu t ton2b.java
/1 Utilización de clases internas a nó nimas .
impo rt javax.swing.*¡
import java. awt.*¡
impert java.awt .ev ent.*;
imper t s tat i c net . mi ndvi e w. u ti l .SwingCensole.*¡
Esta técnica de emplear una clase interna anónima será la que utilizaremos con preferencia (siempre que sea posible) en los
ejemplos del libro.
Ejercicio 5: (4) Cree una aplicación utilizando la clase SwingConsole. Incluya un campo de texto y tres botones.
Cuando pulse cada botón, haga que aparezca un texto distinto en el campo de texto.
Áreas de texto
Un área JTextArea se parece a un campo JTextField salvo porque puede tener múltiples líneas y tiene una mayor funcio-
nalidad. Un método particularmente útil es append( ); con él podemos ir colocando fácilmente la salida de datos del com-
ponente JTextArea. Corno podemos desplazar la pantalla hacia atrás, se trata de una mejora con respecto a los programas
de línea de comandos que imprimen en la salida estándar. Por ejemplo, el siguiente programa rellena un área JTextArea con
la salida del generador geography que hemos presentado en el Capítulo 17, Análisis detallado de los contenedores:
// : gui/ TextArea.java
11 Uti li zación del c o ntrol JTextArea.
import javax.swing.*i
impo rt java.awt.*¡
i mport java.awt.event.*¡
impert java. util.* ;
i mport net.mindview . ut i l. *¡
import static net.mindview.util.SwingConsole . * ¡
private J Button
b ::: new JButton ("Add Da ta 11) f
En el constructor, el mapa se rellena con todos los países y sus capitales. Observe que, para ambos botones, se crea el obje-
to ActionListener y se añade sin definir una variable intermedia, dado que no necesitamos volver a referimos a dicho obje-
to durante el programa. El botón "Add Data" (añadir datos) formatea y afiade todos los datos, y el botón "Clear Data" (borrar
datos) utiliza setText() para eliminar todo el texto de JTextArea.
Al añadir el control JTextArea al marco JFrame, lo envolvemos en un panel JScroUPane para controlar el desplazamien-
to de pantalla cuando se inserta demasiado texto en el control. Es lo único que tenemos que hacer para obtener capacidades
completas de desplazamiento de pantalla. Habiendo intentado averiguar cómo hacer algo equivalente en algunos otros entor-
nos de programación Gill, he de confesar que me impresionan bastante la simplicidad y el adecuado diseño de componen-
tes tales como JScroUPane.
Ejercicio 6: (7) Transforme stringslTestRegnlarExpression.java en un programa Swing interactivo que permita
insertar una cadena de caracteres de entrada en un área JTextArea y una expresión regular en un campo
JTextField. Los resultados deben mostrarse en un segundo control JTextArea.
Ejercicio 7: (5) Cree una aplicación usando SwingConsole, y añada todos los componentes Swing que dispongan de
un método addActionListener() (búsquelos en la documentación del JDK disponible en http://java.
sun.com. Consejo: busque addActionListener( ) utilizando el índice). Capture sus sucesos y muestre un
mensaje apropiado para cada uno dentro de un campo de texto.
Ejercicio 8: (6) Casi todos los componentes Swing derivan de Component, que dispone de un método setCursor( ).
Busque este método en la documentación del JDK. Cree una aplicación y cambie el cursor por uno de los
cursores definidos en la clase Cursor.
Control de la disposición
La forma de colocar los componentes en un formulario en Java difiere, probablemente, de cualquier otro sistema Gill que
haya utilizado. En primer lugar, todo se fija en el código, no hay ningún "recurso" que controle la colocación de los com-
ponentes. En segundo lugar, la fonna en la que se colocan los componentes en un formulario está controlada no por la pos~~.
866 Piensa en Java
ción absoluta, sino por un "gestor de diseño o de disposición" (layout manager), que decide c6mo se disponen los compo-
nentes, basándose en el orden con el que los agreguemos mediante el método add( ). El tamaño, la forma y la colocación
de los componentes difieren enormemente de un gestor de diseño a otro. Además, los gestores de diseño se adaptan a las
dimensiones del applet o de la ventana de aplicaci6n, por 10 que si se cambian las dimensiones de la ventana, el tamaño, la
forma y la colocación de los componentes pueden cambiar como resultado.
JApplet, JFrame, JWindow, JDialog, JPanel, etc., pueden contener y visualizar objetos Component. En Container, exis-
te un método denominado setLayout( ) que permite elegir un gestor de diseño diferente. En esta sección, vamos a analizar
los diversos gestores de diseño, colocando botones en ellos (ya que ésa es la cosa más simple que podemos hacer). En estos
ejemplos no vamos a capturar los sucesos de botón, ya que sólo queremos mostrar cómo se disponen los botones.
BorderLayout
A menos que indiquemos otra cosa, un marco JFrame utilizará un BorderLayout como su esquema de disposición prede-
terminado. En ausencia de instrucciones adicionales, este gestor toma todo aquello que agreguemos con add( ) y lo coloca
en el centro, estirando el objeto hasta alcanzar los bordes.
BorderLayout se base en la existencia de cuatro regiones de borde y un área central. Cuando añadimos algo a un panel que
esté utilizando BorderLayout, podemos emplear el método sobrecargado add( ) que toma un valor constante como primer
argumento. Este valor puede ser uno de los siguientes:
BorderLayout.NORTH Arriba
BorderLayout.SOUTH Abajo
BorderLayout.EAST Derecha
BorderLayout.WEST Izquierda
FlowLayout
Este gestor hace simplemente " fluir" los componentes en el formulario de izquierda a derecha, hasta que se llena la parte
superior; a continuación, se desplaza una fila hacia abajo y continúa con el fluj o de los componentes.
He aquí un ejemplo donde se selecciona FlowLayout como gestor de disposición y luego se colocan botones en el fonnu-
lario. Observará que, con FlowLayout, los componentes se muestran con su tamaño "natural", Un contro1 JButton, por
ejemplo, tendrá el tamaño de su cadena de caracteres asociada.
JI : gui jFlowLayou tl.java
JI Ej empl o de FlowLayou t.
i mpor t j avax.sw i ng .* ;
i mpo r t j ava .awt.*i
impo r t static net .min dview.ut il.SwingConsole. *¡
Todos los componentes se compactarán para tener el tamaño más pequeño posible cuando se emplea un gestor FlowLayout,
por lo que el comportamiento que se obtiene puede resultar algo sorprendente. Por ejemplo, como una etiqueta JLabel ten-
drá el tamaño de su cadena de caracteres asociada, si tratamos·de justificar a la derecha su texto, la visualización no se modi-
ficará.
Observe que si cambiamos el tamaño de la ventana, el gestor de disposición hará que los componentes vuelvan a fluir en
consecuencia.
GridLayout
GridLayout pennite construir una tabla de componentes, y a medida que los añadimos, esos componentes se colocan de
izquierda a derecha y de arriba a abajo dentro de la cuadrícula. En el constructor, especificamos el número de filas y colum-
nas necesarias y dichas filas y columnas se disponen en iguales proporciones.
JJ : gui/Grid Layoutl.java
JI Ejempl o de GridLayout.
i mport javax . sw i ng.*;
impor t j ava.awt.*;
impor t stat i c net.mindview.util.SwingConsole.* ¡
En este caso, hay 21 casillas pero sólo 20 botoues. La última casilla se deja vacía porque GridLayout no efectúa ningón
proceso de "equilibrado".
868 Piensa en Java
GridBagLayout
GridBagLayout proporciona una gran cantidad de control a la hora de decidir cómo disponer exactamente las regiones de
la ventana y cómo éstas deben reformatearse cuando el tamaño de la ventana cambie. Sin embargo, también es el gestor de
disposición más complicado, y resulta bastante dificil de comprender. Está pensado principalmente para la generación auto-
mática de código por parte de un constructor de interfaces GUI (los constructores de interfaces GUI pueden usar
GridBagLayout en lugar de nn sistema de posicionamiento absoluto). Si el diseño es tan complicado qne piensa que puede
necesitar GridBagLayout, es mejor que emplee una herramienta de construcción de interfaces GUI para generar ese dise-
ño. Si, por alguna razón quiere conocer todos los detalles acerca de este gestor de disposición, debe consultar para empezar,
alguno de los libros dedicados específicamente al tema de Swing.
Como alternativa, puede evaluar la utilización de TableLayout, que no forma parte de la biblioteca Swing pero puede des-
cargarse de http://java.sun.com. Este componente está apilado sobre GridBagLayout y oculta la mayor parte de su comple-
j idad, así que permite simplificar enormemente el diseño.
Posicionamiento absoluto
También resulta posible establecer la posición absoluta de los componentes gráficos:
1, Asigne el valor null al gestor de disposición del objeto Container: setLayout(null) .
2_ Invoque setBounds( ) o reshape( ) (dependiendo de la versión del lenguaje) para cada componente, pasando
como parámetro un rectángulo de contorno en coordenadas de píxel. Puede hacer esto en el constructor o en
paint( ), dependi endo del efecto que quiera consegnir.
Algunos constructores de interfaces GUI utilizan esta técnica de manera intensiva, pero normalmente ésta no es la mejor
forma de generar código.
BoxLayout
Debido a que los programadores tenían muchas dificultades a la hora de comprender y utilizar GridBagLayout, Swing tam-
bién incluye BoxLayout, que proporciona muchos de los beneficios de GridBagLayout pero sin la complejidad asociada.
A menudo podemos utilizar este gestor de disposición cuando necesitemos colocar manualmente los componentes (de
nuevo, si el diseño se hace demasiado complej o, utilice una herramienta de construcción de interfaces GUI que se encargue
de generar automáticamente la disposición de componentes). BoxLayout permite controlar la colocac ión de los componen-
tes en sentido vertical u horizontal, así como el espaciado entre los componentes. Podrá encontrar algooos ejemplos básicos
de BoxLayout en los suplementos en linea (en inglés) del libro, disponibles en www.MindView.net.
con ese suceso. Por tanto, el origen de un suceso y el lugar donde ese suceso se trata pueden estar separados. Puesto que
nonnahnente emplearemos los componentes Swing tal como son, pero necesitaremos escribir código personalizado que se
invoque cuando los componentes reciban un suceso, éste es un ejemplo excelente de la separación que existe entre interfaz
e implementación.
Cada escucha de sucesos es un objeto de una clase que implementa un tipo concreto de interfaz de escucha. Por tanto, como
programador, todo lo que tenemos que hacer es crear un objeto escucha y registrarlo ante el componente que está dispa-
rando el suceso. Este registro se realiza invocando el método addXXXListener( ) en el componente encargado de dispa-
rar el suceso, donde "XXX" representa el tipo de suceso para el que estamos a la escucha. Podemos determinar fácilmente
los tipos de sucesos que pueden tratarse fijándonos en los nombres de los métodos "addListener" y si intentamos detectar
los sucesos incorrectos, descubriremos un error en tiempo de compilación. Veremos más adelante en el capítulo que
JavaBeans también utiliza los nombres de los métodos "addListener" para determinar los sucesos que un componente Bean
puede tratar.
Por tanto, toda la lógica de sucesos estará contenida dentro de la clase escucha. Cuando creamos una clase escucha, la única
restricción es que ésta tiene que implementar la interfaz adecuada. Podemos crear una clase escucha global, pero ésta es una
situación en la que las clases internas tienden a ser muy útiles, no sólo porque proporcionan un agrupamiento lógico de las
clases escucha dentro de la interfaz del usuario o de las clases de la lógica del negocio a las que están prestando servicio,
sino también, porque un objeto de una clase interna mantiene una referencia a su objeto padre, lo que proporciona una forma
muy adecuada para realizar invocaciones a través de las fronteras de clase y del subsistema.
Todos los ejemplos que hemos presentado hasta ahora en este capítulo han estado empleando el modelo de sucesos de
Swing, pero en el resto de esta sección vamos a proporcionar el resto de los detalles que describen dicho modelo.
MouseEvent (tanto para los ches como para Component y sus derivados*
el movimiento del ratón)
MouseListener
addMouseListener( )
removeMouseListener( )
MouseEvent6 (tanto para los clics como para Component y sus derivados*
el movimiento del ratón)
MouseMotionListener
addMouseMotionListener( )
removeMouseMotionListener( )
Puede ver que cada tipo de componente soporta sólo ciertos tipos de sucesos. Resulta bastante tedioso buscar todos los suce-
sos soportados por cada componente. Una solución más sencilla consiste en modificar el programa ShowMethods.java del
Capítulo 14, Información de tipos, para que muestre todos los escuchas de sucesos soportados por cualquier componente
Swing que introduzcamos.
En el Capítulo 14, Información de tipos, se presentó el mecanismo de reflexión y dicha funcioualidad se empleó para bus-
car los métodos de una clase concreta, bien la lista completa de todos o un subconjunto de los nombres que se correspon-
dan con una palabra clave que proporcionemos. Uno de los aspectos más atractivos del mecanismo de reflexión es que
permite mostrar automáticamente todos los métodos de una clase, sin tener que recorrer la jerarquía de herencia y tener que
examinar las cIases base de cada nivel. Así, proporciona una valiosa herramienta de ahorro de tiempo de programación,
puesto que los nombres de la mayoría de los métodos Java son suficientemente descriptivos, podemos buscar los nombres
de método que contengan una palabra en concreto. Cuando haya encontrado 10 que crea que está buscando, consulte la docu-
mentación del JDK.
He aquí la versión GUI más útil de ShowMethods.java, especializada para buscar los métodos "addListener" de los com-
ponentes Swing:
6 No hay ningún suceso MouseMotionEvent, aún cuando parezca que debería haberlo. Los clics y el movimiento de ratón están combinados en el suce~
so MouseEvent, por lo que esta segunda aparición de MouseEvent en la tabla no es un error.
22 Interfaces gráficas de usuarío 871
JI : gU i /s howAddLis t eners.java
JI Mue s t ra los métodos "addXXXLis ten er " de cualquier clase Swing .
i mport javax. swi ng .*;
i mport j ava . awt. *¡
i mport j a va.awt.event .*i
i mport j ava.lang .ref lect.*i
i mport java.ut il.regex.*i
import stati c net.mindview.util.SwingConsol e.*¡
Introducimos el nombre de la clase Swing que queramos buscar en el campo JtextField Dame. Los resultados se extraen
utilizando expresiones regulares y se muestran en un control JTextArea.
Observará que no hay botones ni otros componentes para indicar que dé comienzo la búsqueda. Eso se debe a que el campo
JTextField está monitorizado por un escucha ActionListener. Cada vez que realizamos un cambio y pulsamos Intro, la lista
se actualiza inmediatamente. Si el campo de texto no está vacío, se utiliza en Class.forName() para tratar de buscar la clase.
Si el nombre es incorrecto, Class.forName() fallará, lo que quiere decir que generará una excepción. Esta excepción se cap-
tura yen el control JTextArea se muestra el mensaje "No match" (no hay correspondencia). Pero si escribimos un nombre
correcto (teniendo en cuenta las mayúsuculas y las minúsculas), Class.forName() tendrá éxito y getMethods() devolverá
una matriz de objetos Method.
Aquí se emplean dos expresiones regulares. La primera, addListener, busca la cadena "add" seguida por cualquier combi-
nación de caracteres alfabéticos y seguida de "Listener" y de la lista de argumentos entre paréntesis. Observe que esta expre-
sión regular está encerrada entre paréntesis carácter de escape, lo que significa que será accesible como "grupo" de la
expresión regular, cuando se detecte una correspondencia. Dentro de Na meL.ActionPerformed( ) se crea un objeto
Matcher pasando cada objeto Method al método Pattern.matcher( ). Cuando se invoca lind() para el objeto Matcher,
devuelve true s610 si se detecta una correspondencia, y en este caso podemos seleccionar el primer grupo de corresponden-
cia entre paréntesis invocando group(l). Esta cadena de caracteres sigue conteniendo cualificadores, así que para quitarlos
se emplea el objeto Patlern de tipo qualilier, al igual que se hacía en ShowMethods.java.
Al final del constructor, se coloca un valor inicial en name y se ejecuta el suceso de acción para realizar una prueba con
datos iniciales.
Este programa es una fonna cómoda de investigar las capacidades de un componente Swing. Una vez que conocemos los
sucesos soportados por un componente concreto, no necesitamos buscar información adicional para ver reaccionar a dicho
suceso. Simplemente:
1. Tomamos del nombre de la clase de suceso y eliminamos la palabra "Event". Añadimos la palabra "Listener" a
lo que nos haya quedado. Ésta será la interfaz escucha que habrá que implementar en la clase interna.
2. Implementamos la interfaz anterior y escribimos los métodos para los sucesos que queramos capturar. Por ejem-
plo, podríamos estar interesados en detectar movimientos del ratón, así que escribiríamos código para el método
mouseMoved( ) de la interfaz MouseMotionListener (hay que implementar, por supuesto, los otros métodos,
pero a menudo existe un atajo para esta tarea, como veremos más adelante).
3. Creamos un objeto de la clase escucha del paso 2. Lo registramos ante el componente de interés utilizando para
ello el método producido al agregar como prefijo "add" al nombre del escucha. Por ejemplo,
addMouseMotionListener( ).
He aquí algunas de las interfaces escucha:
ActionListener actionPerformed(ActionEvent)
AdjustmentListener adjustmentValueChanged(AdjustmentEvent)
ComponentListener componentHidden(ComponentEvent)
ComponentAdapter componentShown(ComponentEvent)
componentMoved(ComponentEvent)
componentResized(ComponentEvent)
ContainerListener componentAdded(ContainerEvent)
ContalnerAdapter componentRemoved(ContainerEvent)
FocusListener focusGained(FocusEvent)
FocusAdapter focusLost(FocusEvent)
KeyListener keyPressed(KeyEvent)
KeyAdapter keyReleased(KeyEvent)
22 Interfaces gráficas de usuario 873
MouseListener mouseClicked(MouseEvent)
MouseAdapter mouseEntered(MouseEvent)
mouseExited(MouseEvent)
mousePressed(MouseEvent)
mouseReleased(MouseEvent)
MouseMotionListener mouseDragged(MouseEvent)
windowOpened(\VindowEvent)
WindowAdapter windowClosing(WinctowEvent)
windowClosed(WindowEvent)
windowActivated(WindowEvent)
windowDeactivated(WindowEvent)
windowlconified(WindowEvent)
No se trata de un listado exhaustivo, en parte porque el modelo de sucesos pennite crear nuestros propios tipos de sucesos jun-
to a sus escuchas asociados. Por tanto, probablemente se encuentre con bibliotecas en las que el programador habrá inventado
sus propios sucesos y los conocimientos obtenidos en este capítulo le permitirán figurarse cómo hay que usar esos sucesos.
Esto funciona, pero el programador puede volverse loco tratando de ver por qué, ya que todo se compilará y ejecutará correc-
tamente, salvo porque el método no será invocado cuando se haga un clic de ratón. ¿Puede ver el problema? El problema
se encuentra en el nombre del método: MouseClicked( l en lugar de mouseClicked (l. Un simple error en el uso de mayús-
culas y minúsculas ha dado como resultado la adición de un método completamente nuevo. Sin embargo, este método nuevo
no es el que se invoca cuando se hace clic sobre el ratón, así que no se obtienen los resultados deseados. A pesar de la inco-
874 Piensa en Java
modidad, una interfaz garantiza que los métodos se implementen adecuadamente, porque el compilador avisará, en caso de
cometer algún error con el uso .de mayúsculas y de que falte por implementar un método.
Una mejor alternativa para garantizar que estamos sustituyendo efectivamente un método consiste en emplear la anotación
@Override predefmida en el código anterior.
Ejercicio 9: (5) Partiendo de ShowAddListeners.java, cree un programa con la funcionalidad completa de typeinfo.
ShowMethods.java.
7 En Java 1.O/ I.1 no se podía heredar el objeto botón ni ninguna clase útil. Éste era uno de los numerosos fallos de diseño fundamentales.
22 Interfaces gráficas de usuario 875
public TrackEvent () {
set Layout( n ew GridLayout(event. l ength T 1, 2» i
f or (Stri ng evt : even t ) {
JText Field t = new JTextFiel d() i
t . s e t Editable(false ) ;
a dd {new JLabe l {evt, JLabel.RIGHT l);
add (t) ;
h.put (ev t, tl;
add(bl) ;
add (b2 ) ;
En el constructor de MyBu!ton, el color del balón se fij a mediante una llamada a SetBackgrouud( ). Todos los escuchas
se instalan con simpl es llamadas a métodos.
876 Piensa en Java
La clase TrackEvent contiene un HashMap para almacenar las cadenas de caracteres que representan el tipo de suceso, y
campos JtextField donde se almacena la información acerca de dicho suceso. Por supuesto, podíamos haber creado esta
información estáticamente en lugar de incluirla en un contenedor HashMap, pero estoy convencido de que el lector estará
de acuerdo en que el mapa es mucho más fácil de utilizar y modificar. En particular, si necesitamos agregar o eliminar un
nuevo tipo de suceso en TrackEvent, simplemente basta con añadir o borrar una cadena de caracteres en la matriz event;
todo lo demás se hace automáticamente.
Cuando se invoca reporte ), le proporcionamos al método el nombre del suceso y la cadena de parámetro del suceso. Ese
método utiliza el mapa HashMap h de la clase externa para buscar el campo JTextField asociado con ese nombre de suce-
so y luego coloca la cadena de parámetro dentro de dicho campo.
Resulta bastante entretenido este programa, porque con él podemos ver qué sucesos se está n esperando dentro del progra-
ma.
Ejercicio 10: (6) Cree una aplicación utilizando SwingConsole, con un control JButton y otro control JTextField.
Escriba y asocie los escuchas apropiados, de mod o que si botón tiene el foco, los caracteres escritos apa-
rezcan en el campo JTextField .
Ejercicio 11: (4) Herede un nuevo tipo de botón de JButton. Cada vez que pulse este botón, debe cambiar su color a
otro color elegido aleatoriamente. Consulte ColorBoxesojava (más adelante en el capítulo) para ver cómo
generar un valor aleatorio de color.
Ejercicio 12: (4) Monitorice un nuevo tipo de suceso en TrackEventojava añadiendo el nuevo código de tratamiento de
sucesos. Deberá descubrir por sí mismo el tipo de suceso que quiera monitorizar.
Botones
Swing incluye distintos tipos de botones. Todos los botones, casillas de verificación, botones de opción e incluso elemen-
tos de menú heredan de AbstractButlon (que en realidad, ya que están incluidos los elementos de menú, debería, proba-
blemente, haberse llamado "AbstractSelector" o algún otro nombre igualmente genérico). Más adelante veremos la
utilización de los elementos de menú, pero el sigui ente ejemplo ilustra los distintos tipos de botones disponibles:
11: gu ilButtons. j ava
II Diversos botones Swing.
import javax.swing.*¡
import javax. s wing.border.*¡
import j avax. sw ing.plaf. bas ic.* ¡
import java.awt.*¡
22 Interfaces gráficas de usuario 877
El ejemplo comienza con el botón BasicArrowButton de javax.swing.plaf.basic, y luego continúa con los diversos tipos
específicos de botones. Cuando se ejecuta el ejemplo, vemos que el botón conmutador mantiene su última posición, pulsa-
do o no pulsado. Pero las casillas de veriftcación y los botones de opción se comportan de forma idéntica, pennitiendo sim-
plemente hacer clic para activarlo O desactivarlo (heredan de JToggleButton).
Grupos de botones
Si queremos que una serie de botones de opción se comporte de forma "or exclusiva", debemos añadirlos a un "grupo de
botones". Sin embargo, como el siguiente ejemplo demuestra, cualquier botón de tipo A bstractButton puede añadirse a un
grupo ButtonGroup .
Para evitar repetir una gran cantidad de código, este ejemplo utiliza el mecanismo de reflexión para generar los grupos de
diferentes tipos de botones. Esto se ve en makeBPanel( J, que crea un grupo de botones en un control JPanel. El segundo
argumento de make8Panel( J es una matriz de objetos String. Para cada objeto String, se añade al control JPanel un botón
de la clase representada por el primer argumento:
//: g ui /ButtonGroup s . java
ji Uso de l mecanismo de re fle xión para c rear grupos
// d e d ifere ntes t ipos de botones Abst ra c tBut ton .
i mport javax.swing.*¡
impor t javax. s wing. bo rder .*¡
import java.awt .*i
import java . l ang.re fl ect.*¡
impo rt sta ti c n et . mindvi ew. util.SwingConsol e.*;
bg.addlabl;
jp.addlabl;
return jp¡
public ButtonGroups()
setLayout(new FlowLayout()) ¡
add(makeBPanel(JButton.class, ids)) i
add(makeBPanel(JToggleButton.class, ids)) ¡
add(makeBPanel(JCheckBox.class, ids)) i
add(makeBPanel(JRadioButton.class, ids)) ¡
Iconos
Podemos utilizar un objeto Icon dentro de un control JLabel o de cualquier control que herede de AbstractButton (inclu-
yendo JButton, JCheckBox, JRadioButton y los diferentes tipos de JMenuItem). Utilizar Icon con JLabel resulta bas-
tante sencillo (veremos un ejemplo posteriormente). El siguiente ejemplo explora todas las formas adicionales en las que
podemos emplear iconos con los botones y con sus descendientes.
Podemos utilizar cualquier archivo GIF que deseemos, pero los empleados en este ejemplo forman parte de la distribución
de código del libro, disponible en www.MindView.net. Para abrir un archivo y tomar la imagen, cree simplemente un con-
l:rol ImageIcon y pasarle el nombre del archivo. A partir de ahí, podrá emplear el icono resultante en su programa.
JJ: guijFaces.java
j j Comportamiento de los iconos en controles JButton.
import javax.swing.*¡
import java.awt.*¡
22 Inte rfaces gráficas de us uario 879
};
jb = n ew JBu t ton (II JBu t ton " , faces[3 ] )¡
setLayou t (new Fl owLayou t(});
j b . addAct i onListene r (new Act i onListene r ()
public voi d actionPerf ormed(ActionEven t e)
if (mad ) {
jb . setlcon( f ac e s [3 ]) ¡
ma d ::: f a l se¡
e l se {
j b . se t l con (faces [ O] ) ¡
mad :: true ¡
}
}J ;
add (jb2) ;
Sugerencias
En el ejemplo anterior hemos añadido una "sugerencia" a un botón. Casi todas las clases que utilizaremos para crear nues-
tras interfaces de usuario derivan de JComponent, que contiene un método denominado setTooITipTcxt(String), que sirve
para definir una sugerencia (tool tip). Por tanto, para casi todos los componentes que coloquemos en el fonnulario, lo único
que hace falta es decir (para un objeto jc de cualquier clase derivada de JComponent):
jc . setToolTipTe xt (UMi sugerencia 11) i
Cuando el ratón pelmanezca sobre dicho objeto JComponent durante un período predeterminado de tiempo, aparecerá un
pequeño recuadro al lado del puntero del ratón en el que se mostrará la sugerencia.
Campos de texto
Este ejemplo muestra lo que se puede hacer con los controles JTextField:
/1 : gui/TextFields. j ava
JI Campos de texto y sucesos Java.
import j avax.swing .*¡
i mpo r t javax.swing.event.*¡
impor t jav ax . swing .text .*i
import j ava.awt.*¡
impo r t j ava.awt .event.*¡
import static net.mindview.util. SwingConsol e.*;
publ ic vo i d
i nsertS t ri ng (i n t offse t , String st r, At t r ibu teSet attSet )
t hr ows BadLocationExcep tion {
if (upperCase ) str = str. toUpperCase( ) ;
super .insertString(of f se t , str, at tS et) ¡
}
/// , -
El control t3 de tipo JTextField se incluye para disponer de un lugar en el que infonnar cada vez que se dispare el escncha
de acción para el control tI de tipo JTextField tI. Podrá comprobar que el escucha de acción de un control JTextField sólo
se dispara cuando se pulsa la tecla Intro.
El control tl de tipo JTextField tiene asociados varios escuchas. El escucha TI es un objeto DocumentListener que res-
ponde a cualquier cambio que se produzca en el "documento" (el contenido del control JTextField, en este caso). Ese escu-
cha copia automáticamente todo el texto de tl en t2. Además, el documento de t1 ha sido definido como una clase derivada
de PlainDocument, denominada UpperCaseDocument, que fuerza a todos los caracteres a aparecer en mayúsculas. Esta
clase detecta automáticamente los caracteres de retroceso y se encarga de realizar el borrado, ajustando la posición del cur-
sor de texto y gestionando toda la operación de la manera que cabría esperar.
Ejercicio 13: (3) Modifique TextFields.java para que los caracteres en t2 retengan su condición original de mayúscu-
las o minúsculas con la que fueron escritos, en lugar de transformar automáticamente todo a mayúsculas.
Bordes
JComponent contiene un método denominado setBorder( ), que permite añadir diversos bordes interesantes a cualquier
componente visible. El siguiente ejemplo ilustra varios de los diferentes bordes disponibles utilizando un método denomi-
nado showBorder() que crea un control JPanel y agrega en cada caso el borde correspondiente. Asimismo, el ejemplo uti-
liza el mecanismo RTTI para averiguar el nombre del borde que se esté usando (quitando la información de ruta) y luego
inserta dicho nombre en una etiqueta JLabel situada en la zona central del panel:
882 Piensa en Java
pUblic Borders()
setLayout(new GridLayou t (2, 4»;
add (showBorder (new Tí t ledBorder ( "Title l1 ) » ¡
add(s howBorder(new EtchedBorder ());
add(showBorder(new LineBorder(Color. BLUE»);
add (showBorder (
new MatteBorder (5,5, 30,30, Color.GREENl»;
add(showBorder(
new BevelBorder(BevelBorder . RAISED» ) ¡
a dd (showBorder (
new SoftBevelBorder(Bevel Border.LOWERED»);
add(showBorder(new CompoundBorder(
new EtchedBorder(),
new LíneBorder(Color . RED»» ¡
También podemos crear nuestros propios bordes y colocarlos en botones, etiquetas, etc., es decir, en cualquier cosa deriva-
da de JComponent.
Un mini-editor
El control JTextPalle proporciona un gran soporte de edición de texto, sin demasiado esfuerzo. El siguiente ejemplo hace
un uso muy simple de este componente, ignorando gran parte de su funcionalidad:
jj: guij TextPane.java
jj El cont rol JText Pane es un pequeño editor .
i mport javax . swing.*¡
i mport java.awt.*¡
import java . awt.event.*¡
import net.mindview.util.*;
import static net.mindview.u t il.SwingConsole.*¡
El botón añade texto generado aleatoriamente. La intención del control JTextPane es pennitir editar el texto en pantalla,
por lo que verá que no existe un método append(). En este ejemplo (que admito que constituye un uso muy pobre de las
capacidades de JTextPane), el texto tiene que capturarse, modificarse y volverse a colocar en el panel utili zando setText().
Los elementos se añaden al control J Frame utilizando su gestor predeterminado BorderLayout. El control JTextPane se
añade (dentro de un panel JScrollPane) sin especificar una región, por lo que el gestor rellena con él, el centro del panel,
hasta los bordes del mismo. El botón JButton se añade a la región SOUTH, por lo que el componente encajará en dicha
regi6n, en este caso, el botón se mostrará en la parte inferior de la pantalla.
Observe la funcionalidad integrada de JTextPane, como por ejemplo, el salto automático de línea. Este control tiene otras
muchas funcionalidades que puede examinar en la documentación del JDK.
Ejercicio 14: (2) Modifique TextPane.java para usar Wl control JTextArea en lugar de JTextPane.
Casillas de verificación
Una casilla de verificación proporciona una forma de efectuar una única elección binaria, Está fonnada por un pequeño
recuadro y una etiqueta. El recuadro almacena normalmente una pequeña "x." minúscula (o alguna otra indicación de que la
casiUa está activada) o está vacía, dependiendo de si el elemento ha sido seleccionado o no,
Nonnalmente, creamos un control JCheckBox utilizando un constructor que tome la etiqueta como argumento. Podemos
establecer y consultar el estado y también establecer y consultar la etiqueta, si es que queremos leerla o modificarla después
de haber creado el control JCheckBox.
Cada vez que se activa o desactiva un control JCheckBox se produce un suceso que se puede capturar de la misma fornla
que para un botón: utilizando un objeto ActionListener. El siguiente ejemplo emplea un control JTextArea para enumerar
todas las casillas de verificación que hayan sido activadas:
jj: guijCheckBoxes .java
jj Utilización de controles JCheckB ox.
import javax.swing.*¡
import java.awt.*;
i mp or t java.awt.event.*¡
import sta tic net. mindview. u t i l ,swingConsole.*¡
El método trace() envía el nombre de la casilla JCheckBox seleccionada, junto con su estado actual, al control JTextArea
utilizando el método append( ), de manera que podremos ver una lista acumulada de las casillas de verificación que hayan
sido seleccionadas, junto con su estado.
Ejercicio 15: (5) Añada una casilla de verificación a la aplicación creada en el Ejercicio 5, capture el suceso e inserte
diferentes textos en el campo de texto.
Botones de opción
El concepto de botones de opción (o de radio) en la programación de interfaces GUI proviene de las radios de automóvil
anteriores a la aparición de los circuitos electrónicos que estaban equipadas con botones mecánicos. Cuando se pulsaba uno
de ellos, todos los demás saltaban hacia arriba, Así, este tipo de controles nos permite imponer que se efectúe una única elec-
ción entre varias disponibles.
Para definir un grupo asociado de botones JRadioButton, los añadimos a un grupo ButtonGroup (en un formulario puede
haber varios grupos ButtonGroup). Uno de los botones puede opcionalmente configurarse con el valor troe (utilizando el
segundo argumento del constructor). Si tratamos de asignar el valor true a más de un botón de opción, sólo el último de los
botones configurados adoptará el valor true.
He aquí un ejemplo simple del uso de los botones de opción, donde se ilustra la captura de sucesos utilizando
ActíonListener:
ji : g ui/RadioButtons . java
1/ Utilización de controle s JRadioBut ton.
i mport javax.swing. *¡
import java. awt .*¡
import j a va. awt .event .*i
import s t atic net . mi ndview . ut i l.SwingConsole . *¡
El control JTextField muestra el "índice seleccionado", que es el número de secuencia del elemento actualmente seleccio-
nado, junto con el texto que ese elemento tiene en el recuadro combinado.
Cuadros de lista
Los cuadros de lista difieren significativamente de los cuadros JComboBox, y no sólo por su distinta apariencia v isual.
Mientras que 1m recuadro JComboBox se despliega cuando los activamos, un contro JList ocupa un número fijo de línea
dentro de una pantalla durante todo el tiempo, y no se modifica. Si queremos ver los elementos de una lista, basta con invo-
car getSelectedValues( ), que produce una matriz de objetos String de los elementos que hayan sido seleccionados.
Un control JList permite efecfuar selecciones múltiples; si hacemos control-elic sobre más de un elemento (manteniendo
pulsada la tecla Control mientras realizamos clics adicionales con el ratón), el elemento original continuará estando resalta-
do y podemos seleccionar tantos elementos como queramos. Si seleccionamos un elemento, y luego pulsamos Mayús-clic
sobre otro elemento, todos los elementos pertenecientes al rango comprendido entre esos dos elementos quedarán seleccio-
nados. Para eliminar un elemento de un grupo, podemos hacer Control-clic sobre él.
jj , guijList.java
import javax.swi ng .*¡
impor t j avax.swing .border.*¡
import javax.swing.event.*¡
import j ava.awt .*¡
i mport java.awt.event.*;
i mp ort static net .mindview.uti1. Swing Conso1e .*¡
}
};
private Lis tSelection Listener 11 =
new ListSe l e ct i onL istene r () {
pub li c void valueChanged{ListSelectionEvent el {
i f(e.getVa l ue IsAdju s t i ng()) re turn¡
t . setText ( " ") i
f or(Object i t em ls t.ge tS electedVa l ues())
t .append(item + lI \n " ) i
}
};
private int count = O;
p u b li c List () (
t. setEditable(f a ls e ) ;
setLayout(new FlowLayout()) i
/1 Crear bo rdes para los componentes:
Bo r der brd = Borde rFactory.c r eateMatteBorder(
1 , 1, 2, 2 Color. BLACK) i
1
lst.setBorder(br d ) ;
t .setBor der(brd) ;
11 Añadir l os primeros cuatro elementos a l a lis t a
fo r (int i = Oi i < 4; i ++)
lI tems.addElement (flav ors[count+ +J ) ;
add( t) ;
add(lst) ;
add(b ) ;
11 Regi s tr ar escuchas de sucesos
l st.addListSe l ect i o nListener{ l l) i
b . addActionListener (b l) ;
jj : guijTabbedPanel .java
jj Ejemp l o de panel con fichas.
import javax.swing.*¡
import javax.swi ng.event.*i
impor t j ava . awt.*¡
import static net.mindview.util.SwingConsole.*¡
Recuadros de mensaje
Los entornos de ventanas suelen contener un conjunto estándar de recuadros de mensaje que pennite presentar rápidamen-
te información al usuario o capturar informaci6n del usuario. En Swing, estos recuadros de mensajes están contenidos en el
control JOptionPane. Disponemos de muchas posibilidades distintas (algunas bastante sofisticadas), y las que usaremos
más comúnmente, con toda probabilidad, son el cuadro de diálogo de mensajes y el cuadro de diálogo de confitmación, que
se invocan con los métodos estáticos JOptionPane.showMessageDialog( ) y JOptionPane.showConfirmDialog( ). El
siguiente ejemplo muestra un subconjunto de los recuadros de mensajes disponibles con JOptionPane:
jj : gui jMessageBoxes .java
jj Ejemp lo de JOptionPane.
i mport javax.sw ing.*¡
import java.awt.*i
import java.awt.event.* ¡
i mport stat i c ne t .mindview.ut i l.SwingConsole.*¡
);
private JTextF i eld t xt = new JTextFie l d (15 ) ¡
private Ac t i onLis t ener al ~ new Acti onLi s tene r{)
publi c vo i d actionPerformed (ActionEvent el {
Stri ng id = ((JButton)e . getSourc e ( ) .getText()¡
i f (id.equa l s ( "Alert" ) )
JOptionPane. showMes sageDialog (null,
uThere r s a bug en you! "Hey!!I
11 I I
JOp t ionPane.ERROR_MESSAGE ) ;
e l se if{id .equal s {"Yes/Nol!))
JOp t i onPan e. showConfirrnDialog (null ,
"er no", II c h oos e yes",
JOp t ionPane . YES_NO_OPTION) ;
els e if (i d.equals ( nCol o r ")) {
Objec t[] aptians = { 11 Red " , "Green n } i
i nt sel = JOptionPane.showOptionDial og (
n u ll, uChoose a Co l or ! I ! , "Warn ing ll ,
JOpt i onPane . DEFAULT_O PTION,
JOptionpane .WARNING_MESSAGE, nu ll ,
aptians, options [O] ) i
if(sel ! = JOp tionPane . CLOSED_OPTION )
tx t . setText ( "Color Sel ecte d: " + options [se l] ) i
else if( id. equals ( IIInput")) {
String va l = JOpt ionPane.showl nputDialog (
uHow many fi ngers do you s ee ? ");
txt .setText (val ) i
el se if (id . equals ( "3 Va l s ll } ) {
Object(J sel ect i ons = {" Fi rs t " , "Second", IIThird ll }¡
Object va l = JOpt i onPane.showl nputDialog (
nul l, IIChoose one ll , "Input H ,
J Option Pane . IN FORMATI ON_MESSAGE,
nu ll, selections , select ions [O]) ;
i f (val != nul l )
tx t . s etText (val.toS t ri ng (» ¡
)
);
publ i c Me s sageBoxes() {
setLayout(ne w FlowLayout(»);
for(i n t i = O; i < b. length¡ i++) {
b[i ] . addActionListener (al ) i
add(b[iJ) ;
a dd (tx t) ;
Ejercicio 18: (4) Modifique MessageBoxes.java para que tenga un escucha ActionListener para cada botón (en lugar
de buscar la correspondencia por el texto del balÓn).
Menús
Cada componente capaz de almacenar un menú, incluyendo JApplet, JFrame, JDialog y sus descendendientes, dispone de
Wl método setJMenuBar() que acepta un objeto JMenuBar que representa una barra de menú (sólo puede haber un com-
ponente JMenuBar en cada componente concreto). Lo que hacemos es añadir menús JMenu a la barra de menús
JMenuBar y elementos de menú JmenuItem a los menús JMenu. Cada objeto JMenultem puede tener asociado un escu-
cha ActionListener que se seleccione cuando se seleccione ese elemento de menú.
Con Java y Swing es necesario agrupar manualmente todos los menús en el código fuente. He aquí Wl ejemplo simple de
menú:
JI : guijSimpleMenus. java
import javax. swing .*¡
i mpo rt jav a.awt.*¡
i mport java.awt.event .* i
i mpor t stat i c net.mindview.uti l. Swi ngConsole.*¡
El uso del operador módulo en "i%3" distribuye los elementos de menú entre los tres controles JMenu. Cada objeto
Jl\1enultem puede tener asociado un escucha ActionListener; aquí, se utiliza el mismo escucha ActionListener en todas
partes, pero lo nonnal es que necesitemos un escucha distinto para cada elemento Jl\'Ienultem.
22 Interfaces gráficas de usuario 891
JMenuItem hereda de AbstractButtou, así que tiene algunos comportamientos similares a los de los botones. Por sí
mismo, proporciona un elemento que puede situarse en un menú desplegable. Existen también tres tipos heredados de
JMenuItem: JMenu, para almacenar otros elementos JMenuItem (de modo que pueda haber menús en cascada);
JCheckBox~1enuItem, que produce una casil1a de verificación para indicar si dicho elemento de menú está seleccionado
y JRadioButtonMenuItem, que contiene un balón de opción.
Corno ejemplo más sofisticado, he aquí de nuevo el ejemplo de los sabores de helados para crear menús. Este ejemplo tam-
bién ilustra la definición de menús en cascada, de atajos de teclado, de elementos JCheckBoxMenultem, y también mues-
tra la fanna de cambiar dinámicamente los menús.
ji: gu i/Menus .java
// Submenú s, eleme ntos de menú con casi l las de verif icaci ón, menús
/1 i n tercambiables (ataj os de teclado) y comandos de acci ón .
impo r t javax .swing .*i
i mport java . awt .* ¡
i mport j ava.awt . even t.* ¡
impor t static net. mindv iew.util.SwingConsole.*¡
}
II Alternativamente, podemos crear una clase diferente
II para cada elemento Menultem. Entonces, no tenemos
II por qué tratar de figurarnos cuál es:
class FooL implements ActionListener {
public void actionPerformed(ActionEvent e)
t.setText(IIFoo selected ll ) ¡
public Menus ()
ML mI = new ML();
CMIL cmil = new CMIL() i
safety [O] . setActionCommand ("Guard ll ) i
safety[O] .setMnemonic(KeyEvent.VK_G) ¡
safety[OJ . addltemListener(cmil) i
safety [lJ . setActionCommand ("Hide ll ) ;
safety[lJ .setMnemonic(KeyEvent.VK_H) j
22 Interfaces gráficas de usuario 893
mb l.addl f) ;
mbl. add (m) ;
setJMenu Bar (mb l) ;
t . setEditabl e (f alse) i
add (t, BorderLayout. CENTER);
JI Preparar un sis t ema para inter cambiar menús:
b.addAc t ionL i s tener( new BL ()) ¡
b . se t Mnemonic( KeyEvent . VK_ S } j
add (b, BorderLayout.NORTH) i
for (JMenul t em oth : other)
fooB ar .add (o th) ;
f ooBar . s etMnemonic (KeyEven t . VK_ B) ;
mb 2.add(f ooBar) ;
En este programa, hemos colocado los elementos de menú en matrices y luego hemos recorrido cada matriz invocando
add() para cada elemento JMenuItem. Esto hace que la adición o eliminación de lUl elemento de menú sea algo menos
tedioso.
Este programa crea dos controles JmenuBar para demostrar que las barras de menú pueden intercambiarse de manera acti-
va mientras el programa se está ejecutando. Podemos ver cómo los patrones JM.e nuBar están compuestos de elementos
JMenu, y que cada control JMenu está formado por elementos JMenuItem, JCheckBoxMenuItem, o incluso por otros
elementos JMenu (lo que permite obtener submenús). Cuando se define un control JMenuBar, se puede instalar en el pro-
grama actual mediante el método setJMenuBar() . Observe que, cuando se pulsa el botón se comprueba qué menú está ins-
talado actualmente invocando getJMenuBar( ) y luego se sustituye por el otro menú.
A la hora de comprobar la correspondencia con la cadena de caracteres "Open", observe que la ortografia y la utilización de
mayúsculas y minúsculas son críticas, pero que Java no proporciona ningwIa señal de error si no se detecta ninguna corres-
pondencia con "Open". Este tipo de comparación entre cadenas de caracteres constituye una fuente de errores de programa-
ción.
La activación y desactivación de los elementos de menú se gestiona automáticamente. El código que gestiona los elemen-
tos JCheckBoxMenuItem muestra dos fonnas distintas de detenninar qué es lo que se ha activado: comparación de cade-
894 Piensa en Java
na de caracteres (la técnica menos segura, aunque también se suele utilizar) y la detección del objeto de destino del suceso.
Tal como se muestra, podemos utilizar el método getState( ) para determinar el estado. También podemos cambiar el esta-
do de un objeto JCheckBoxMenuItem con setState().
Los sucesos relacionados con los menús son algo incoherentes y pueden conducir a confusión: los elementos JMenultem
utilizan escuchas ActionListener, mientras que los elementos JCheckBoxMenuItem utlizan escuchas ItemListener. Los
objetos JMenu también pueden soportar los escuchas ActionListener, aunque eso no suele resultar útil. En general, aso-
ciaremos escuchas a cada elemento JMenuItem, JCheckBoxMenuItem o JRadioButtonMenuItem, pero el ejemplo
muestra escuchas de tipo ItemListener y ActionListener asociados a los diversos componentes de menú.
Swing soporta el uso de "atajos de teclado", así que podemos seleccionar cualquier cosa derivada de AbstractButton (boto-
nes, elementos de menú, etc.) utilizando el teclado en lugar del ratón. Estos atajos funcionan de fonna muy simple; para
JMenuItem, podemos utilizar el constructor sobrecargado que admite como segundo argumento el identificador de la tecla.
Sin embargo, la mayoría de las clases AbstractButton no tiene constructores como éste, por lo que la fonna más general
de resolver el problema consiste en utilizar el método setMnemonic( ). En el ejemplo anterior se añaden atajos al botón y
a algunos elementos de menú; al hacerlo, aparecen automáticamente indicadores de los componentes que infonnan del atajo
de teclado.
También podemos ver cómo se utiliza el método setActionCommand( ). Este método parece algo extraño, porque en cada
caso el "comando de acción", defmido por el método coincide exactamente con la etiqueta del menú. ¿Por qué no utilizar
snnplemente la etiqueta en lugar de esta cadena de caracteres alternativa? El problema estriba en la internacionalización. Si
rehiciéramos este programa para otro idioma lo que querríamos es cambiar simplemente la etiqueta del menú, sin tener que
cambiar el código (porque sin duda ese proceso de modificación podría introducir nuevos errores). Utilizando
setActionCommand( ), el "comando de acción" puede ser inmutable, mientras que la etiqueta de menú se puede modifi-
car. Todo el código funciona con el "comando de acción", así no se ve afectado por los cambios que efectuemos en las eti-
quetas de menú. Observe que en este programa, no se examinan todos los componentes de menú para detenninados
comandos de acción, de modo que no hemos configurado aquellos componentes de menú donde ese examen no se realiza.
El grueso del trabajo tiene lugar dentro de los escuchas. BL se encarga de realizar el intercambio de los objetos JMenuBar.
En ML, se adopta la solución de "adivinar quién ha llamado" obteniendo el origen del suceso ActionEvent y proyectándo-
lo sobre JMenuItem, después de lo cual se extrae el comando de acción para pasarlo a través de una cascada de instruccio-
nes if.
El escucha FL es lo bastante simple, aún cuando se encarga de gestionar todos los diferentes sabores del menú de sabores.
Esta técnica resulta útil si la lógica que estamos utilizando es suficientemente simple, pero en general, conviene emplear la
solución por FooL, BarL y BazL, en la que cada escucha se asocia a un único componente de menú, lo que hace innece-
sario utilizar una lógica adicional de detección y nos pennite saber exactamente quién ha invocado el escucha. Incluso con
la profusión de cIases que de esta manera se genera, el código tiende a ser más pequeño, y el proceso está más libre de erro-
res.
Como puede ver, el código de especificación de menús puede llegar rápidamente a ser muy largo y confuso. Éste es un caso
en el que la solución apropiada consistiría en emplear una herramienta de construcción de interfaces GUI. Si se tiene una
buena herramienta, ésta se encargará también del mantenimiento de los menús. .
Ejercicio 19: (3) Modifique Menus.java para utilizar botones de opción en lugar de casillas de vetificación en los
menús.
Ejercicio 20: (6) Cree un programa que descomponga un archivo texto en sus palabras componentes. Disttibuya dichas
palabras como etiquetas en una serie de menús y submenús.
Menús emergentes
La fonna más directa de implementar un menú emergente JPopupMenu consiste en crear una clase interna que amplíe
MouseAdapter, y luego agregar un objeto de dicha clase interna a cada componente al que queramos añadir ese compor-
tamiento emergente:
jj: guijpopup.java
jj Creación de menús emergentes con Swin g.
import javax.swing.*¡
22 Interfaces gráficas de us uario 895
import j ava.awt . *i
import j ava.awt . eVent.*i
import static net . mindview.util.SwingConsole.*;
private vo id maybeShowPopup(MouseEvent e) {
if(e.isPopupTrigger{))
popup.show(e .getComponent()} e.getX() I e . getY() ) i
En el ejemplo se añade el mismo escucha ActionListener a cada elemento JMenuItem. El escucha extrae el texto de la eti-
queta del menú y lo inserta en el campo JTextField.
Dibujo
En un buen entorno GUI, las tareas de dibujo deberían resultar razonablemente sencillas, y así sucede en la biblioteca Swing.
El problema con cualquier ejemplo de dibujo es que los cálculos que determinan dónde hay que colocar cada elemento sue-
len ser bastante más complicados que las llamadas a las rutinas de dibujo, y estos cálculos están a menudo mezclados con
las llamadas a esas rutinas, lo que puede hacer que parezca que la interfaz es más complicada de lo que realmente es.
896 Piensa en Java
En aras de la simplicidad, considere el problema de representar una serie de datos en la pantalla; aquí, los datos estarán pro-
porcionados por el método predefinido Math.sin( ), que genera una función matemática seno. Para bacer las cosas algo más
interesantes y para demostrar lo fácil que es utilizar los componentes Swing, colocaremos un deslizador en la parte inferior
del fonnulario, para controlar dinámicamente el número de ciclos de la onda senoidal que se van mostrando, Además, si
cambiamos el tamatlo de la ventana, veremos que la onda seno se reajusta automáticamente en la nueva ventana,
Aunque cualquier control JComponent puede ser pintado y ser utili zado como el lienzo, si queremos disponer de una super-
ficie de dibujo sencillo, lo que haremos nonnalmente será heredar de JPanel. El único método que hay que sustituir es
paintComponent( ), que se invoca cada vez que hay que repintar dicho componente (normalmente no hace falta preocu-
parse por esto, porque Swing toma la decisión). Cuando se invoca el método, Swing le pasa un objeto de tipo Graphic.,
pudiendo nosotros pasar dicho objeto para dibujar o pintar en la superficie.
En el siguiente ejemplo, toda la inteligencia relativa al proceso de dibujo se encuentra dentro de la clase SineDraw; la clase
SineWave simplemente configura el programa y el control deslizador. Dentro de SineDraw, el método .etCycles() propor-
ciona un mecanismo para que otro obj eto (en este caso, el control destizador) controle el número de ciclos.
jj : guij sineWav e.java
jj Dibujo con Swing, utilizando un contro l JSlide r .
i mport javax. s wing .* ;
i mpor t javax,swing.event.*¡
import j ava.awt. * ¡
import stati c ne t . mi ndv i ew. u t il .SwingConsole,*¡
repa int () i
22 Interfaces gráficas de usuario 897
Todos los campos y matrices se utilizan en el cálculo de los puntos que definen la onda senoidal; cycles indica el número
de ondas senoidales completas que deseamos, points contiene el número total de puntos que se dibujarán, sines contiene los
valores de la función seno y pts contiene las coordenadas de los puntos que se dibujarán sobre el panel JPanel. El método
setCycles( ) crea las matrices de acuerdo con el número de puntos necesarios y rellena la matriz sines con una serie de valo-
res. Invocando repaint( J, setCycles( J fuerza a que se llame a paintComponent( J, con lo que se producirá el resto de los
cálculos y el proceso de redibujo.
Lo primero que hay que hacer cuando se sustituye paintComponent( J es invocar la versión de la cIase base del método.
Después, somos libres de hacer lo que queramos; normalmente, esto significa emplear los métodos gráficos que podemos
encontrar en la documentación correspondiente a java.awt.Graphics (en la documentación del JDK disponible en
http://java.sun.comJ, para dibujar y pintar pixeles en el control JPanel. Podemos ver que casi todo el código está dedicado
a la realización de los cálculos; las únicas dos llamadas a método que se encargan de manipular de hecho la pantalla son
setColor( J y drawLine( J. Probablemente se encuentre, al hacer sus propios programas que muestren datos gráficos, con
una experiencia similar: invertirá la mayor parte del tiempo tratando de determinar qué es lo que quiere dibujar, pero el pro-
pio proceso de dibujo será bastante simple.
Cuando creé este programa, la mayor parte del tiempo lo invertí en intentar que se visualizara la onda sinusoida1. Una vez
resuelto eso, pensé que sería bastante atractivo poder cambiar dinámicamente el número de ciclos. Mis experiencias de pro-
gramación intentando hacer cosas como ésta en otros lenguajes hacía que fuera un poco renuente a realizar esto, pero resul-
tó ser la parte más fácil del proyecto. Creé un control JSlider (los argumentos el valor más a la izquierda del deslizador, el
valor más a la derecha y el valor de inicio, respectivamente, pero también hay otros constructores) y lo inserté en el marco
JFrame. Después examiné la documentación del JDK y observé que el único escucha era addChangeListener, que se dis-
paraba cada vez que se cambiaba el deslizador lo suficiente como para generar un nuevo valor. El único método para este
escucha era el denominado stateChanged( J, que proporcionaba un objeto ChangeEvent con el que se podia determinar el
origen del cambio y averiguar el nuevo valor. Invocar el método setCycles( ) del objeto sines permitía encontrar el nuevo
valor y redibujar el panel JPanel.
En general, podrá encontrar que la mayor parte de los problemas basados en Swing pueden resolverse siguiendo un proce-
so similar, y además verá que es un proceso bastante simple, incluso si no ha utilizado un componente concreto anterior-
mente.
Si el problema es más complejo existen otras alternativas más sofisticadas para el tema de dibujo, incluyendo componentes
JavaBeans de otras fuentes y la APl 2D de Java. Estas soluciones caen fuera del alcance de este libro, pero puede informar-
se acerca de ellas si el código que está empleando para dibujar resulta demasiado complejo.
Ejercicio 21: (5) Modifique SineWave.java para transformar SineDraw en un componente JavaBean añadiendo méto-
dos "getter" y "setter".
Ejercicio 22: (7) Cree una aplicación utilizando SwingConsole. La aplicación debe tener tres deslizadores, que permi-
tan ajustar los valores rojo, verde y azul en java.awt.Color. El resto del fonnulario debe ser un control
JPanel que muestre el color determinado por los tres deslizadores. Incluya también campos de texto no
editables que muestren los valores RGB actuales.
898 Piensa en Java
Ejercicio 23: (8) Utilizando SineWave.java como punto de partida, cree un programa que muestre un cuadrado rotato-
rio en la pantalla. Un deslizador debe controlar la velocidad de rotación y un segundo deslizador contro-
lará el tamaño del recuadro.
Ejercicio 24: (7) ¿Recuerda ese juguete de dibujo que tenía dos mandos, uno para controlar el movimiento vertical del
punto de dibujo y otro para controlar el movimiento horizontal? Cree una variante de este jugoete, utili-
zando SineWave.java como punto de partida. En lugar de mandos, utilice deslizadores. Añada un botón
que permita borrar todo el dibujo.
Ejercicio 25: (8) Comenzando con SineWave.java, cree un programa (una aplicación utilizando la clase Swing-
Console) que dibuje una onda senoidal animada que parezca deslizarse a lo largo de la pantalla, como si
fuera un osciloscopio, controlando la animación con un temporizador java.utiI.Timer. La velocidad de la
animación debe poder regolarse mediante un control javax.swing.JSlider.
Ejercicio 26: (5) Modifique el ejercicio anterior para que se creen múltiples paneles con ondas sinoidales dentro de la
aplicación. El número de paneles con ondas sinoidales debe controlarse mediante parámetros de la línea
de comandos.
Ejercicio 27: (5) Modifique el Ejercicio 25 para utilizar la clase javax.swing.Timer con el fin de dirigir la animación.
Observe la diferencia entre esta clase y java.util.Timer.
Ejercicio 28: (7) Cree una clase que represente un dado (sólo una clase sin interfaz GUl). Cree cinco dados y láncelos
repetidamente. Dibuje la curva que muestre la suma de los puntos obtenidos en cada tirada y muestre la
curva evolucionando dinámicamente a medida que se hacen más tiradas.
Cuadros de diálogo
Un cuadro de diálogo es una ventana que emerge a partir de otra ventana. Su propósito es resolver algún tema específico,
sin atestar la ventana original con los correspondientes detalles. Los cuadros de diálogo normalmente se emplean en entor-
nos de programación basados en ventanas.
Para crear un cuadro de diálogo, hay que heredar de JDialog, que es simplemente otro tipo de objeto Wiodow, como
JFrallle. Un control JDialog tiene un gestor de disposición (que de manera predeterminada es BorderLayout), y tenemos
que añadir escuchas de sucesos al cuadro para poder tratar los sucesos. He aquí un ejemplo muy simple:
JJ : gui JDialogs.java
II Creación y uso de cuadros de diálogo.
i mport javax.swing.*¡
import java.awt.*;
impert java.awt.event.*¡
impert static net.mindview.util.SwingConsole.*¡
Una vez creado el control JDialog, hay que invocar setVisible(true) para mostrarlo y activarlo. Cuando se cierra el cuadro
de diálogo, es necesario liberar los recursos empleados por esa ventana, invocando dispose( ).
El siguiente ejemplo es algo más complejo, el cuadro de diálogo está fonnado por una cuadricula (utilizando GridLayout)
de un tipo especial de botón que se define aquí mediante la clase ToeButton. Este botón dibuja un marco alrededor suyo y,
dependiendo de su estado, un espacio en blanco, una "x," o una "o" en la parte central. Inicialmente, el botón está en blan-
co y luego, dependiendo de a quién le corresponda el tumo cambia a una "x" o a una "o", Sin embargo, también alternará
entre "x" y "o" cuando se baga clic en el botón, para proporcionar una interesante variante del juego de tres en raya. Además,
el cuadro de diálogo puede configurarse para tener cualquier número de filas ocultas, modificando los valores en la venta
de aplicación principal.
ji: gui/TicTacToe.java
// Cuadros de diálogo y creación de sus propios componentes.
import javax.swing.*¡
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*;
íf(state == State.OQ)
g.drawOval(xl, yl, xl + wide/ 2, yl + h igh/2l ;
eIs e
state
(st at e State. XX ? S tat e .OO : State .XX ) ;
repa i nt () i
public Ti cTacToe () {
Jpanel p = n e w JPanel()¡
p. set Layout (new Gr idLayout (2 ,2) ) i
p. add (new JLabel (IfRows t i , JLabe l. CENTER) } i
p. add( rows) ;
p.add(new JLabel(uColumns u , JLabel.CENTER)};
p.add(cols) ;
add( p, BorderLayout .NORTH) i
JButton b = new JBu t ton {ligo" } j
b. addActionListener (new BL ()} j
add(b, BorderLayout.SOUTH} ;
Puesto que los valores estáticos sólo se pueden encontrar en el nivel interno de la clase, las ciases internas no pueden tener
datos estáticos ni clases anidadas.
El método paintComponent() dibuja el cuadrado alrededor del panel y la indicación "x" u "o". Este proceso está lleno de
tediosos cálculos, pero resulta bastante sencillo.
Los clics de ratón se capturan mediante el escucha MouseListener, que primero comprueba si hay algo escrito en el panel.
Si no, se consulta la ventana padre para averiguar a quién le corresponde el turno, lo que establece el estado del control
ToeButton. Gracias al mecanismo de la clase intema, el control ToeButton puede acceder a los datos del padre y pasar el
turno. Si el botón ya está mostrando una indicación "x" u "o", entonces se invierte esa indicación. Podemos ver, dentro de
los cálculos la"comodidad que proporciona el uso de la instrucción ir-else ternaria, descrita en el Capítulo 3, Operadores.
Después de cada cambio de estado, se redibuja el control ToeButton.
22 Interfaces gráficas de usuario 901
El constructor para ToeDialog es bastante simple: añade a un gestor GridLayout tantos botones como solicitemos y luego
cambia el tamaño para que cada botón tenga 50 pixeles de lado.
TIcTacToe configura la aplicación completa creando los campos JTextField (para introducirlos en las filas y columnas de
la cuadricula de botones) y el botón "inicio" con su escucha ActionListener. Cuando se pulsa el botón, es necesario extraer
los datos de JtextField y, como están en formato Strlng, transformarlos a formato int utilizando el constructor de Integer
que toma un argumento de tipo String.
if(rVal == JFileChooser.CANCEL_OPTION) {
fileName.setText(nyou pressed cancel") i
dir.setText("") ¡
Observe que hay muchas variantes que podemos aplicar a JFileChooser, incluyendo filtros para seleccionar los nombres de
archivo pennitidos.
Para un cuadro de diálogo de apertura de archivos ("open file"), invocamos showOpenDialog( ), y para un cuadro de diá-
logo para guardar el archivo ("save file"), invocamos showSaveDialog( ). Estos comandos no vuelven hasta que se cierra
el cuadro de diálogo. El objeto JFileChooser sigue existiendo, así que podemos leer datos desde el mismo. Los métodos
getSelectedFile( ) y getCurrentDirectory( ) son dos fonuas que penuiten preguntar por el resultado de la operación. Si
devuelven null, quiere decir que el usuario ha cancelado el cuadro de diálogo.
Ejercicio 29: (3) En la documentación del JDK correspondiente a javax.swing, busque el control JColorChooser.
Escriba un programa con un botón que haga aparecer en fonna de cuadro de diálogo este selector de color.
)) ;
se t Layout(new FlowLayou t ()} ¡
add{b) ;
Es necesario iniciar el texto con "<html>", después de lo cual se pueden emplear marcadores HTML. Observe que no esta-
mos obligados a incluir los marcadores normales de cierre.
ActionListener añade una nueva etiqueta JLabel al formulario, que también contiene texto HTML. Sin embargo, esta eti-
queta no se añade durante la construcción, así que es necesario invocar el método vaJidate( ) del contenedor para forzar una
nueva disposición de los componentes (y con ello la visualización de la nueva etiqueta).
También podemos utilizar texto HTML para J TabbedPane, JMenuItem, JToolTIp, JRadioButton, y J CheckBox.
Ejercicio 30: (3) Escriba un programa que ilustre el uso de texto HTML en todos los elementos mencionados en el párra-
fo anterior.
}
}) ;
La clave para asociar los componentes deslizador y b'4Ta de progreso estriba en compartir sus modelos, en la línea:
pb.setModel(sb.getModel()) ;
Por supuesto, también podríamos controlar los dos utilizando un escucha, pero usar el modelo es mucho más simple en algu-
nas situaciones sencillas. El control P rogressMonitor no tiene un modelo, por lo que es obligatorio utilizar el método basa-
do en escucha. Observe que el control ProgressMonitor·sólo se mueve hacia adelante y que se cierra automáticamente una
vez que alcanza el final.
La barra de progreso JProgressBar es bastante sencilla, pero el control J Slider tiene bastantes opciones, como por ejem-
plo las que fijan la orientación y las marcas principales y secundarias del deslizador. Observe lo fácil que es añadir un borde
con título.
Ejercicio 31: (8) Cree un "indicador asintótico de progreso" que vaya cada vez más lento a medida que se aproxime al
final. Añada un comportamiento errático aleatorio, de manera que el indicador parezca estarse acelerando
periódicamente.
Ejercicio 32: (6) Modifique Progress.java para que, en lugar de compartir los modelos, utilice un escucha para conec-
tar el deslizador y la barra de progreso.
No hace falta incluir nada en la cláusula cateh porque el gestor de la interfaz de usuario UIMa nager tomará como opción
predetenninada el aspecto y estilo interplataforma si los intentos de seleccionar cualquiera de las otras alternativas fallan.
Sin embargo, durante la depuración, la excepción puede resultar muy útil, así que podemos tratar de ver al menos algunos
resultados mediante.la cláusula eateh.
8 Cabría discutir si las capacidades de visualización de Swing bacen justicia al sistema operativo.
22 Interfaces gráficas de usuario 905
He aquí un programa que acepta un argumento de la línea de comandos para seleccionar un detenninado aspecto y estilo
que muestra el aspecto de los distintos componentes con la opción elegida:
//: gui/LookAndFeel.java
1/ Selección del aspecto y el estilo de la aplicación.
// {Args, motif}
import javax.swing.*¡
import java.awt.*¡
import static net.rnindview.util.SwingConsole.*¡
catch(Exception e) {
e.printStackTrace() j
el se if(args[O] .equals("systern ll ) )
try {
UIManager.setLookAndFeel(UIManager.
getSystemLookAndFeelClassName()) ;
catch (Exception e) {
e.printStackTrace() ;
} el se usageError() j
Puede ver que una opción consiste en crear explícitamente una cadena de caracteres para definir el aspecto y el estilo, como
se ve con MotifLookAndFeel. Sin embargo, esa opción y la opción predeterminada "metal" son las únicas que pueden
emplearse legalmente en todas las plataformas, aunque hay otras cadenas de caracteres que definen el aspecto y estilo para
Windows y Macintosh, dichas cadenas sólo pueden usarse en sus respectivas plataformas (puede obtener estas cadenas invo-
cando getSystemLookAndFeelClassNamc() mientras se encuentre en la plataforma deseada).
También es posible crear un paquete personalizado de aspecto y estilo, por ejemplo si estamos diseñando un sistema para
una compañía que quiera disponer de una apariencia distintiva en sus aplicaciones. Se trata de una tarea compleja y que
queda fuera del alcance de este libro (de hecho, verá que queda fuera del alcance de muchos libros dedicados a Swing).
He aquí una variante FileChooserTest.java utilizando los servicios JNLP para abrir el selector de archivos, de modo que la
clase pueda implantarse como una aplicación JNLP en un archivo JAR no firmado.
1/: gui/ j nlp/JnlpFi l e Chooser.java
// Apertura de archivos e n una máquina local con JNL P.
11 {Requires: j avax.jnlp.FileOpenServics¡
11 Hay que ten er jav aws.j a r en l a ruta de clases }
1/ Para crear e l archivo j nlpfilec hooser. j ar, haga esto:
11 cd ..
11 cd ..
// j ar cvf guijjnlpjjnlpfilechooser .jar gu i /jnlp/*.class
package gui.jnlpi
i mport javax. j nlp .* i
import javax.swing.*¡
import java.awt . *;
i mport java.awt .eve n t . * i
import java.io .* ¡
public class JnlpFileChooser extends JFrame {
prívate JTextF i e l d f i l eName = new JTextF i eld()¡
pri vate JButton
open = new JButt on ( nopen" ),
save = new JBu tton (nS ave n ) ¡
pri vate JEd i t orPane ep = new JEdi t orPane();
private JSc r o llPane jsp = n e w JScro ll Pane()¡
private FileCon t ents fil eContents¡
pub lic JnlpFileChooser () {
Jpanel p = new J Panel()¡
open . addActionListen er{new OpenL()¡
p.add(open ) ;
save.addActionListen er(new SaveL(» j
p . add(save) ;
jsp.getViewport( ) .add (epl;
add (j sp, BorderLayout.CENTER);
add (p, BorderLayou t . SOUTH) ¡
f i l eName.setEdit ab l e(false) ;
p = new JPane l () ;
p.setLayout(new Gr idLayout(2, l » ¡
p.add(fileName ) ¡
add(p, BorderLayout.NORTH);
ep . setContentType (ntext n ) ;
save. setEn abled (falsel ¡
i f (fs ! = nul l )
try (
fi l eContent s = fs.openFi l eDialog{".",
n ew St r i ng[] {ntx t ll , n*n}) ,;
if{fi l e Contents == nul l l
r e turn;
file Name.setText(f ileContents.getName(» ;
ep.read(f i leContents.getlnputSt r eam(), nu l l l ;
908 Piensa en Java
Observe que las clases FileOpenService y FileCloseService se importan del paquete javax.jnlp y que en ninguna parte del
código se hace referencia directa al cuadro de diálogo JFileCbooser. Los dos servicios usados aquí deben solicitarse em-
pleando el método ServiceManager.lookup(), y s6lo se podrá acceder a los recursos del sistema cliente a través de los obje-
tos devueltos por este método. En este caso, los archivos de l sistema de archivos del cliente están siendo escritos y leídos
mediante la interfaz FileContent, proporcionada por lNLP. Cualquier intento de acceder a los recursos directamente utili-
zando, por ejemplo, un objeto File o un objeto FileReader haría que se generara una excepción SecurityException de la
misma manera que si se intentaran emplear desde un applet no firmado. Si desea utilizar estas clases y no quedarse restrin-
gido a las interfaces de servicios de JNLP, tendrá que firmar el archivo JAR.
El comando comentado jar en JnlpFileCbooser.java generará el archivo JAR necesario. He aqui un archivo de arranque
apropiado para el ejemplo anterior.
j j ,l gui j jnlp j f i l echooser. j n l p
<?xml v ersion="1.0 n encoding=" UTF-8"? >
<jnl p s pec = "l.O+'!
codebase::"fi l e : C: /AAA -T IJ4/code/gui/jnlp"
href :: I! f ilechooser . j nlp >
11
Utilice el marcador sccurity cuando la aplicación esté implantada en un archivo JAR firmado. En el ejemplo anterior no era
necesario porque es posible acceder a todos Jos recursos locales a través de los servicios JNLP.
Hay disponibles unos pocos más marcadores, cuyos detalles puede consultarlos en la especificación di sponible en
http://java.sun. comlproductsljavawebstartldownioad-spec.h Imi.
Para ejecutar el programa, es necesaria una página de descarga que contenga un vínculo de hipertexto al archivo .jnlp. He
aquí un ejemplo (sin la primera y última línea):
ji :! gui/jnlp/ f i lechooser . h trnl
<htrn!>
Follow the i nstru c tions in JnlpFil e Chooser.j ava t e
build jnlpfilechooser.jar, t h e n:
<a href=" fi le c h oos er.jnlpll>cl ick here</a>
910 Piensa en Java
Una vez que haya descargado la aplicación, podrá configurarla utilizando la consola de administración. Si está empleando
Java Web Start sobre Windows, entonces se le solicitará un acceso directo a su aplicación la segunda vez que lo use. Este
comportamiento es configurable.
Aquí sólo se cubren dos de los servicios JNLP, aunque existen siete servicios en la versión actual. Cada uno de ellos está
diseñado para realjzar una tarea específica, como imprimir~ o cortar y pegar en el portapapeles, Puede encontrar más infor-
mación en http://java.sun.com.
Concurrencia y Swing
Al programar con Swing se emplean hebras. Hemos visto esto al principio del capítulo al estudiar que todo debería ser envia-
do a la hebra de despacho de sucesos de Swing a través de SwingUtilities.invokeLater( j. Sin embargo, el hecho de que no
se haya creado explícitamente 1m objeto Thread sigaifica que los problemas del mecanismo de hebras puede aparecer por
sorpresa. Debemos recordar siempre que existe una hebra de despacho de sucesos en Swing, la cual está siempre allí, ges-
tionando túdos los sucesos Swing extrayéndolos uno a uno de la cola de sucesos y ejecutándolos. Recordar que la hebra de
despacho de suoesos está presente le ayudará a garantizar que la aplicación no se vea sometida a interbloqueos o condicio-
neS de carrera.
En esta sección se analizan las cuestiones relativas al mecanismo multihebra que pueden surgir a la hora de trabajar con
Swing.
1
1) ¡
setLayout(new FlowLayou t {));
addlbl) ¡
addlb2)¡
Cuando se pulsa bi, la hebra de despacho de sucesos se ve de repente ocupada en realizar la tarea de larga duración. Podrá
ver que el botón ni siquiera vuelve a salir hacia afuera, porque la hebra de despacho de sucesos que se encarga nonnalmen-
te de repintar en la pantalla está ocupada. Y no podemos hacer ninguna otra cosa, como pulsar b2, porque el programa no
responderá hasta que se haya completado la tarea de bl y la hebra de despacho de sucesos vuelva a estar disponible. El códi-
go de b2 es un intento incorrecto del problema, interrumpiendo la hebra de despacho de sucesos.
Por supu esto, la respuesta consiste en ejecutar los procesos de larga duración en hebras separadas. Aquí, se emplea el obje-
to ejecutor con la hebra Executor, que pone automáticamente las tareas pendientes en cola y las ejecuta de una en una.
1/: gu i /InterruptableLongRunningTask. java
ji Tareas de larga duración dentro de hebras.
import javax.swing.*¡
import java.awt.*¡
impo rt java.awt.event.*¡
import j ava.util .concurrent.*;
import stat i c net.mindview.util.SwingCon so le.*¡
System.out.println(this + !I complet ed ll ) ¡
Esta versión es mejor, pero cuando pulsamos b2, se llama a shutdownNow( ) sobre el objeto ExeclltorService, con lo que
se desactiva. Si intentamos añadir más tareas, se genera una excepción. Por tanto, pulsar b2 hace que el progTanla deje de
funcionar correctamente. Lo que nos gustaría es poder lenninar la tarea actual (y cancelar las lareas pendientes) sin dete-
ner nada. El mecanismo de Callable/Futllre de Java SES descrito en el Capítulo 21 , Concurrencia, es justo lo que necesi-
tamos. Definiremos una nueva clase denominada TaskManager, que contienen tuplas que almacenan el objeto Callable
que representa la tarea y el objeto Future devuelto por el objeto C.ll.ble. La razón de que sea necesaria la tupla es que nos
pennite mantener el control de la tarea original, con lo cual podemos tener información adicional que no esté disponible en
el objeto Future. He aqui cómo se haria:
1/: net /minctview/util/Taskltem.j ava
1/ Un objeto Future y el objeto Callable que l o produce.
package net.mindview.uti l ¡
import java .ut il .concurrent.*;
En la biblioteca java.lltil.collcurrent, la tarea no está disponible a través del objeto Future de manera predeterminada, por-
que la tarea no tendría por qué seguir necesariamenle existiendo cuando oblengamos el resultado del objeto Future. Aquí,
obligamos a que la tarea siga existiendo por el procedimiento de almacenarla.
TaskManager ha sido incluida en net.mindview.util para que esté disponible como utilidad de propósito general:
1/: net/mindview/uti l /Ta skManager . java
1/ Ges t i ón y ejecución de una cola de tareas .
package net.mindview.util¡
import java .util .concurrent.*¡
i mport java.util.*¡
try {
results.add(item.future.get()) i
catch (Exception el {
throw new RuntimeException(e);
items.remove() i
return resul ts i
return results i
TaskManager es un contenedor ArrayList de elementos TaskItem. También contiene un ejecutor monohebra Executor,
de modo que cuando invocamos add() con un objeto Callable, se ejecuta el objeto Callable y se almacena el objeto Future
resultante junto con la tarea original. De esta forma, si necesitamos hacer algo con la tarea, disponemos de una referencia a
la misma. Como ejemplo simple, en purge( ) se utiliza el método toString( ) de la tarea.
Ahora podemos utilizar este mecanismo para gestionar las tareas de larga duración de nuestro ejemplo:
JI: gui/lnterruptableLongRunningCallable.java
JI Utilización de de objetos Callable para tareas de larga duración.
import javax.swing.*¡
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.*¡
import net.mindview.util.*;
import static net.mindview .util.SwingConsole.*;
public cIass
InterruptableLongRunningCallable extends JFrame {
private JButton
bl new JButton(UStart Long Running Task!l),
b2 "" new JButton("End Long Running Taskl!),
b3 "" new JButton(ITGet results lT ) i
private TaskManager<String,CallableTask> manager
new TaskManager<String,CallableTask>() i
public InterruptableLongRunningCallable() {
bl.addActionListener(new ActionListener()
914 Piensa en Java
}
}) ;
b2.addActionListener(new Ac tionListener()
publi c void actionPerf ormed(ActionEvent el
for (String result : manager .purge(l)
System.ou t.print ln(r esult) ;
}
}) ;
b3.addActionListener (new Act ionLis tene r{)
public void act ion Performed(ActionEvent el
II Ll amada de e jemplo a un método de una tarea :
fo r( Ta skltem<String, CallableTask> tt :
man ager )
tt.task.id(); JI No se requi er e proyección
for(St ring result : man ager.ge tRe su l ts()
System.ou t .pri n tln(resul t) ;
}
}) ;
setLayout(n ew FlowLayout{) i
add (bl ) ;
add(b2) ;
add(b3 ) ;
}
) ;
}
});
setLayout(new FlowLayout(})¡
add(b1);
add(b2) ;
add(b3) ;
}
///:-
El constructor MonitoredCallable toma un objeto ProgressMonitor como argumento, y su método eall( ) actualiza ese
objeto cada medio segundo. Observe que un objeto MonitoredCalloble es una tarea separada y no debe, por tanto, intentar
controlar la interfaz de usuario directamente, asi que se utiliza SwingUtilities.invokeLater() para enviar los cambios en la
información de progreso al monitor. El tutoríal de Swing elaborado por Sun (disponible en hup://java.sulI.com) muestra
una técnica alternativa, consistente un utilizar el temporizado Tlmer de Swing, el cual comprueba el estado de la tarea y
actualiza el mortitor.
Si se pulsa el botón de "cancelación" para el monitor, monltor.isCanceled( ) devolverá true. Aquí, la tarea se limita a invo-
car interrupt( ) sobre su propia hebra, lo que ahora hace que entre en la cláusula eateh donde se termina el monitor con el
método elose( ).
El resto del código es, en la práctica, igual que antes salvo por la creación del objeto ProgressMonitor como parte del cons-
tructor MonitoredLongRunningCallable.
Ejercicio 33: (6) Modifique InterrnptableLongRunningCallable.java para que se ejecuten todas las tareas en parale-
lo en lugar de secuencialmente.
Hebras visuales
El siguiente ejemplo defme una clase JPanel de tipo Runnable que dibuja diferentes colores en el panel. Esta aplicación
está preparada para tomar valores de la línea de comandos con el fin de determinar el tamaílo de la cuadrícula de colores y
cuánto tiempo hay que dormir (con sleep(» entre los sucesivos cambios de color. Jugando con estos valores, podemos des-
cubrir algunas características interesantes y posiblemente inexplicables en la implementación del mecartismo multihebra en
nuestra plataforma:
JI ! guijColorBoxes .java
JI Demostración visual del mecanismo multihebra.
// {Args: 12 5 0}
i mport javax.swing.*;
import java.awt .*i
import j ava .util.concurrent.*¡
impo rt j ava . util.*¡
import static net.mindview. u ti l .SwingConsole.*¡
catch(InterruptedException el
JI Forma aceptable de salir
ColorBoxes configura un objeto GridLayout de tal manera que tenga una serie de celdas grid en cada dimensión. A con-
tinuación añade el número apropiado de objetos CBox para rellenar la cuadrícula, pasando el valor pause a cada uno de esos
objetos. En maiu( l podemos ver que pause y grid tienen valores predeterminados que pueden modificarse proporcionan-
do los correspondientes argumentos a través de la línea de comandos.
Todo el trabajo se realiza en CBox. Esta clase hereda de JPanel e implementa la interfaz Ruunable, de tal forma que cada
panel JPanel también puede ser una tarea independiente. Estas tareas están dirigidas por un conjunto de hebras
ExecutorService.
El color de la celda actual es cColor. Los colores se crean utilizando un constructor Color que admite números de 24 bits,
que en este caso se crean aleatoriamente.
paintComponeut( l es bastante simple; sólo asigna un color a cColor y rellena el JPanel completo con dicho color.
En run( l, podemos ver el bucle infinito que asigna un nuevo color aleatorio a cColor y luego invoca repaint( l para mostrar-
lo. A continuación, la hebra pasa a dormir (con sleep( II durante el intervalo de tiempo especificado en la línea de comandos.
La llamada a repaint( l en run( l merece una cierta atención. A primera vista, pudiera parecer que estamos creando una gran
cantidad de hebras, cada una de las cuales está forzando a que se produzca una operación de dibujo. Pudiera parecer que
esto viola el principio de que s6lo debemos enviar hebras a la cola de sucesos. Sin embargo, estas hebras no están en reali-
dad modificando el recurso compartido. Cuando llaman a repaint( l , eso no fuerza un redibujo en dicho instante, sino que
simplemente fija un "indicador de modificación" para especificar que la siguiente vez que la hebra de despacho de sucesos
esté lista para dibujar las cosas, ese área es un candidato para el redibujo. Por tanto, el programa no provoca ningún proble-
ma de gestión de hebras con Swing.
Cuando la hebra de despacho de sucesos realiza verdaderamente un redibujo con paint( l, llama primero a
paintComponent( l, luego a paintBorder( l y paintChlldren( l. Si necesitáramos sustituir paint( l en un componente deri-
vado, tenemos que acordamos de llamar a la versión de la clase base de paint( ), de modo que se sigan realizando las accio-
nes apropiadas.
918 Piensa en Java
Precisamente porque este diseño es flexible y el mecanismo de hebras está asociado con cada elemento JPanel, podemos
experimentar creando tantas hebras como queramos (en realidad, hay una restricción impuesta por el número de hebras que
la máquina lVM sea capaz de gestionar).
Este programa también constituye una interesante prueba comparativa de rendimiento, ya que puede mostrar enormes dife-
rencias entre prestaciones y comportamiento entre una implementación multihebra de la NM y otra, así como entre unas
plataformas y otras.
Ejercicio 34: (4) Modifiqne ColorBoxes.java de modo que comience distribuyendo puntos ("estrellas") por el lienzo,
y luego cambiando aleatoriamente el color de esas "estrellas".
c lass Spo t s {}
En primer lugar. podemos ver que se trata simplemente de una clase. Normalmente, todos los campos serán privados y sólo
se podrá acceder a ellos a través de sus métodos y propiedades. Siguiendo el convenio de denominación, las propiedades
son jumps, color, spots y jumper (observe el cambio de mayúsculas a minúsculas en la primera letra del nombre de la pro-
piedad). Aunque el nombre del identificador interno es el mismo que el nombre de la propiedad en los tres primeros casos,
en jumper podemos ver que el nombre de la propiedad no nos obliga a utilizar ningún identificador concreto para las varia-
bles internas (ni tampoco nos obliga, de hecho, ni siquiera a tener ninguna variable interna para dicha propiedad).
Los sucesos gestionados por este componente Bean son ActionEvent y KeyEvent, basados en la denominación de los méto-
dos "add" "remove" para el escucha asociado. Finalmente, podemos ver que el método normal croak( ) sigue siendo parte
de la Bean simplemente porque se trata de un método público, no porque se adapte a ningún esquema de denominación.
de interés. Sin embargo, la utilización de Introspector para mostrar infonnación acerca de una Bean constituye un ejerci-
cio educativo excelente. He aquí una herramienta que hace precisamente esto:
1/: guijBeanDumper.java
JI Obtención de la información acerca de una Bean.
import javax.swing.*¡
import java.awt.*i
import java.awt.event.*i
import java.beans.*¡
import java.lang.reflect.*;
import static net.mindview.util.SwingConsole.*¡
for(PropertyDescriptor d: bi.getPropertyDescriptors()) {
Class<?> p = d.getPropertyType();
if(p == null) continue;
print (JI Property type: \n u +p. getName () +
JI Property name: \n + d. getName () ) ;
11
print(UPublic methods:");
for(MethodDescriptor m : bi.getMethodDescriptors())
print(m.getMethod() .toString()) i
print('!======================") i
print ("Event support:") ¡
for(EventSetDescriptor e: bi.getEventSetDescriptors()) {
print (lIListener type: \n n +
e.getListenerType() .getName());
for(Method 1m : e.getListenerMethods())
print(UListener method:\n !1 + lm.getName()) i
for(MethodDescriptor lmd :
e.getListenerMethodDescriptors()
print("Method descriptor:\n n + lmd.getMethod());
Method addListener= e.getAddListenerMethod() ¡
print(JlAdd Listener Method:\n u + addListener) ¡
Method removeListener = e.getRemoveListenerMethod()¡
print (JlRemove Listener Method: \n U+ removeListener) ¡
print("====================,!) ¡
dump (c) ;
public BeanDumper() {
Jpanel p = new Jpanel()¡
p.set Layou t(new FlowLayou t(»;
p.add (n ew JLabel ( ~'Qualified bean name:" »;
p .add(query) ;
add (Border Layout . NORTH, p);
add(new JSc rol lPane (res ults»;
Dumper dmpr = new Dumper( ) ;
query. addActionL i s tener (dmpr) ;
query.setText(lIfrogbean . Frogl! } ;
JI Forz ar la evaluac i ón
dmpr,action?erformed(new ActionEvent(dmpr , O, 1111 ) i
Property type:
bool ean
Property name:
j u mper
Read method:
publi c boolean isJumpe r()
Wr i te me thod:
publ ic void setJump er (boolean)
Property type:
i nt
Property name:
jumps
Read method:
p ub lic int getJump s ()
Wr i te method:
publ ic void setJumps(int)
Property type:
frogbean.Spots
Prope rty name:
s p ots
Read method :
publ ic frogbean . Spots g e t Spots()
Wr i t e method:
publ ic vo i d setSpots(frogbean.Spotsl
Publi c methods:
publ ic void setSpots(frogbean . Sp ots)
public void set Col or(Col or )
publ ic vo i d setJumps(int l
public boolean isJumper()
pub l ic frogbean . Spots getSpots()
public void c roak (l
publ ic void addActionL i stener(ActionListener)
public void addKeyListener(KeyListener)
publ i c Color getColor()
public void setJumper(bool ean )
publi c i nt getJumps()
pub l ic void removeAc t ionLi stener(ActionLis tener)
public void removeKey Listener(KeyListener)
Event support:
Lis tener type:
KeyListener
Liste n er met hod :
keyPressed
List ene r method:
keyRel eased
Lis t ene r method:
keyTyped
Method descriptor:
publ ic abstract void keyPressed(KeyEvent )
Method descrip tor :
publ i c abs tra ct void keyReleased (KeyEvent )
Method descr i ptor:
public abstract v oid keyTyped(KeyEvent)
Add Listener Method:
pub l ic void addKeyListener(KeyListener}
924 Piensa en Java
Listener t ype:
ActionListener
Listener method:
actionPe rformed
Method d escriptor:
public abstract void action Performed(ActionEvent)
Add Lis tener Method:
public void addActionListener (Ac t ionListener )
Remove Lis t ener Method:
p u blic void removeActionListener(ActionListener)
Esta salida nos revela la mayor parte de la información que Introspector ve a medida que genera un objeto BeanInfo a par-
tir de la Bean. Podemos ver que el tipo de la propiedad y su nombre son independientes. Observe el uso de minúscula en el
nombre de la propiedad (la única vez que esto no sucede cuando el nombre de la propiedad comienza con más de dos letras
mayúsculas seguidas). Y recuerde que los Dombres de métodos que podemos ver aquí (como por ejemplo los de lectura y
escritura) SOD producidos por UD objeto Method que puede emplearse para invocar el método asociado sobre el objeto.
La lista de los métodos públicos incluye los métodos que no están asociados con una propiedad o un suceso, como por ejem-
plo croak( ), asi como los que sí están asociados. Se trata de todos los métodos que se pueden invocar mediante programa
para una Bean, y el entorno IDE puede facilitamos la tarea de programación enumerando todos esos métodos mientras rea-
lizarnos llamadas a métodos.
Por último, podemos ver que los sucesos se analizan completamente, extrayendo la información acerca del escucha, de sus
métodos y de los métodos para agregar y eliminar escuchas. Básicamente, una vez que disponemos del objeto Beanlnfo,
podemos determinar toda la información importante acerca de la Bean. También podemos invocar los métodos para dicha
Bean, a pesar de no disponer de ninguna otra información, salvo el propio objeto (de nuevo, ésta es una funcionalidad ofre-
cida por el mecanismo de reflexión).
p u blic class
BangBean extends JPane l implements Serial izable {
private i nt xm, ym¡
private int cSize = 20; /1 Tamaño del c i rculo
private String text = lIBangl ll ¡
private int fontSize = 48;
private Color teolor = Co l or.RED;
private ActionListener actionListene r ¡
22 Interfaces gráficas de usuario 925
public BangBean() (
a d dMouseLi sten er (new ML () ;
a ddMo u seMo tionListen er (new MML();
}
publi c int getCircl e Si z e () { return cS ize;
public vaid setCircleSize(int newSize) {
cSize = newSize¡
Lo primero que podemos observar es que BangBean implementa la interfaz Serializable. Esto quiere decir que el entorno
lDE puede extraer toda la información de BangBean utilizando serialización después de que el diseñador haya ajustado los
valores de las propiedades. Cuando se crea la Bean como parte de la aplicación que se está ejecutando, estas propiedades
extraídas se restauran de modo que podamos obtener exactamente 10 que deseamos.
Si examinarnos la signatura de addActionListener(), vemos que puede generar una excepción TooMany-
ListenersException. Esto indica que se trata de un escucha de unidifilSión, lo que quiere decir que envía una notificación a
un único escucha en el momento de producirse el suceso. Normalmente, lo que usamos son sucesos de multidifusión, de
modo que muchos escuchas puedan ser notificados por un suceso. Sin embargo, eso nos haría tropezar con cuestiones de
programación multihehra, por lo que volveremos sobre el tema en la siguiente sección, "Sincronización en JavaBeans".
Mientras tanto, un suceso de unidifusión nos permite resolver momentáneamente el problema.
Cuando hacemos clic con el ratón, el texto aparece en la parte central del componente BangBean, y si el campo
actionListener es distinto de null, se invoca su métodos actionPerforrned( ) creando un nuevo objeto ActionEvent en el
proceso. Cada vez que se mueve el ratón se capturan sus nuevas coordenadas y se vuelve a dibujar el lienzo (borrando el
texto que hubiera en el lienzo, como se verá al ejecutar el programa).
He aquí la clase BangBeanTest para probar la Bean:
public BangBeanTest()
BangBean bb = new BangBean( ) ¡
try (
bb.addActionListener(new BBL()) ¡
catch{TooManyListenersException e)
t xt. setText {"Too many li steners!l } ;
add (bb ) ;
add(Border Layout .SOUTH, txt} i
Cuando se emplea la Bean en un entorno IDE, esta clase no se usará, pero es útil proporcionar un método de prueba rápido
para cada Bean que diseñemos. BangBeanTest coloca un componente BangBean dentro del marco JFrame, asociando un
escucha ActionListener simple con el componente BangBean con el fm de imprimir un recuento de sucesos en el campo
22 Interfaces gráficas de usuario 927
JTextField cada vez que se produzca un suceso ActionEvent. Por supuesto, normalmente el IDE crearía la mayor parte del
código que utilice la Bean.
Cuando ejecute el componente BangBean a través de BeanDumper o lo incluya dentro de un entorno de desarrollo prepa-
rado para componentes Bean, observará que hay muchas más propiedades y acciones de las que el código precedente per-
mite incluir. Eso se debe a que 'BangBean hereda de JPanel, y JPanel también es un componente Bean, así que se mostrarán
también sus propiedades y sucesos.
Ejercicio 35: (6) Localice y descargue uno o más de los entornos gratuitos para el desarrollo de interfaces GUI dispo-
nibles en Iuternet, o utilice algún producto comercial que posea. Descubra qué es lo que hace falta para
añadir un BangBean a ese entorno y añádalo.
Sincronización en JavaBeans
Siempre que creemos una Bean, deberemos asumir que ésta será ejecutada dentro de un entorno multihebra. Esto quiere
decir que:
1. Siempre que sea posible, todos los métodos públicos de una Bean deben ser sincronizados. Por supuesto, esto
implica que tendremos que pagar el coste de la sincronización en tiempo de ejecución (que se ha reducido signi-
ficativamente en las versiones recientes del JDK). Si esto es un problema, podemos dejar sin sincronizar los méto-
dos que no vayan a provocar problemas en las secciones críticas, pero recuerde que dichos métodos no resultan
siempre obvios. Los métodos que podrían caer dentro de esta categoría tienden a ser pequeños (tal como
getCirc\eSize() en el ejemplo signiente) y/o "atómicos"; es decir, la llamada al método se ejecuta utilizando una
cantidad de código tan pequeña que el objeto no puede modificarse durante la ejecución (pero recuerde del
Capítulo 21, Concurrencia, que aquello que pensemos que es atómico puede en realidad no serlo). Dejar sin sin-
cronizar dichos métodos puede no tener un efecto significativo sobre la velocidad de ejecución del programa. Lo
mejor es que todos los métodos públicos de la Bean sean sincronizados y eliminar la palabra clave synchronized
de un método únicamente cuando estemos completamente seguros de que eso va a aumentar la velocidad y pode-
mos eliminar la palabra con segmidad.
2. Cuando se dispara un suceso multidifusión para notificar a un conjunto de escuchas interesados en dicho suceso,
debemos asumir que se pueden añadir o eliminar escuchas mientras estemos recorriendo la lista.
El primer punto es bastante sencillo de entender, pero el segundo requiere algo más de reflexión. En BangBean.java evita-
mos los problemas de concurrencia ignorando la palabra clave synchronized y haciendo que el suceso fuera de unidifusión.
He aquí una versión modificada que trabaja en un entorno multihebra y utiliza el mecanismo de multidifusión para los suce-
sos:
IJ: gui/BangBean2.java
IJ Deería escribir sus componentes Beans de esta forma para
IJ poder ejecutarlos en un entorno multihebra.
import javax.swing.*¡
import java.awt.*¡
import java.awt.event.*¡
import java.io.*¡
import java.util.*¡
import static net.mindview.util.SwingConsole.*¡
addMouseMotionListener(new MM());
}
}) ;
JFrame frame = new JFrame()¡
trame. add (bb2 ) ;
run(frame, 300, 300) i
}
/// ,-
Aftadir la palabra clave synchronized a los métodos es un cambio sencillo. Sin embargo, observe en addActionListener( )
y removeActionListener( ) que los escuchas ActionListener se añaden y eliminan ahora en un contenedor ArrayList, por
lo que podemos tener tantos como queramos.
Podemos ver que el método notifyListeners( ) no está sincronizado. Este método puede ser invocado desde más de una
hebra a la vez. También resulta posible invocar addActiollListener( ) o removeActionListener( ) en mitad de una llama-
da a notifyListeners( ), lo que constituye un problema, porque este último método recorre el contenedor actionListeners
de tipo ArrayList. Para alivi ar el problema, el contenedor ArrayList se clona dentro de una cláusula synchronized, y lo
que se hace es recorrer el clan (consulte los suplementos (en inglés) en línea de este libro para conocer más detalles sobre
la clonación). De esta forma, podemos manipular el contenedor ArrayList original sin que ello suponga ningún impacto
sobre notifyListeners().
El método paintComponent() tampoco está sincronizado. Decidir si sincronizar los métodos sustituidos no resulta tan claro
como cuando estamos añadiendo nuestros propios métodos. En este ejemplo, resulta que paintComponent() parece fun-
cionar correctamente independientemente de si se sincroniza o no. Sin embargo, las cuestiones que hay que tener en cuen-
ta son las siguientes:
1. ¿Modifica el método del estado de las variables "críticas" dentro del objeto? Para descubrir si las varíables son
"críticas", hay que determinar si esas variables serán leídas o escritas por otras hebras del programa (en este caso,
la lectura o escritura se hace casi siempre a través de métodos sincronizados. por lo que nos podemos limitar a
examinar esos métodos). En el caso de paintComponent( ), no se realiza ninguna modificación.
2. ¿Depende el método del estado de estas variable "críticas"? Si un método sincronizado modifica una variable que
nuestro método utilice, entonces conviene hacer que nuestro método también esté sincronizado. Basándonos en
esto, podemos observar que cSize es modificado por métodos sincronizados y, por tanto, paintComponent( )
debería estar sincronizado. Sin embargo, en este caso, podemos preguntamos: "¿Qué es lo peor que puede suce-
der si se cambia eSize durante la ejecución de paintComponent( )?" Si la respuesta a esta pregunta es que no
puede suceder nada catastrófico, y que no sucede más que un efecto transitorio, debemos decidir dejar sin sincro-
nizar paintCornponent( ) para evitar el coste adicional asociado a la llamada al método sincronizado.
930 Piensa en Java
3. Una tercera clave consiste en analizar si la versión de la clase base de paintComponent( ) está sincronizada, lo
que no es así. Este elemento no tiene mucho peso, pero sí que nos proporciona una pista. En este caso, por ejem-
plo, un campo que sí que se cambia mediante métodos sincronizados (eSize) se ha mezclado en la fórmula de
paintComponent( ) y podria haber modificado nuestras conclusiones. Sin embargo, observe que el carácter de
sincronizado no se hereda, es decir, si un método está sincronizado en la clase base, no se sincroniza automática-
mente en la versión sustituida de la clase derivada.
4. paint() y paintComponent( ) son métodos que deben ser lo más rápidos posible. Cualquier cosa que permita
reducir el coste de procesamiento de estos métodos resultará altamente recomendable, por lo que si cree que nece-
sita sincronizar estos métodos, eso será un indicador de que el diseño no es demasiado bueno.
El código de prueba en main( ) ha sido modificado con respecto al que se muestra en BangBeanTest para ilustrar las capa-
cidades de multidifusión de BangBean2 añadiendo escuchas adicionales.
Name: bangbeanjBangBean.class
Java-Bean: True
La primera línea indica la versión del esquema de manifiesto, que será la 1.0 en tanto que no se produzca una modificación
de Sun en sentido contrario. La segunda linea (las líneas vacías se ignoran) proporciona el nombre del archivo
BangBean.class, y la tercera dice: "'Esto es una Bean". Sin la tercera línea, la herramienta de construcción de programas no
podría reconocer esa clase como una Bean.
La única parte complicada es que tenemos que aseguramos de incluir la ruta adecuada en el campo "Name:". Si volvemos
a examinar BangBean.java, veremos que se encuentra en el paquete bangbean (y por tanto en un subdirectorio denomina-
do bangbean que está fuera de la ruta de clases), y el nombre del archivo de manifiesto deberá incluir esta información de
paquete. Además, es necesario colocar el archivo de manifiesto en el directorio situado encima de la raíz de la ruta del paque-
te, lo que en este caso significa colocar el archivo en el directorio situado encima del subdirectorio "bangbean". Entonces,
deberemos invocar jar desde el mismo directorio en el que se encuentre el archivo de manifiesto, de la forma siguiente:
jar cfm BangBean.jar BangBean.mf bangbean
Esto presupone que queremos que el archivo JAR resultante se denomine BangBean.jar, y que hemos colocado el archivo
de manifiesto en un archivo denominado BangBean.mf.
Podríamos preguntarnos, "¿Qué sucede con las demás clases que se generaron en el momento de compilar
BangBean.iava?" Todas las clases han terminado incluidas dentro del subdirectorio bangbean, y como puede ver, el últi-
mo argumento de la línea de comandos iar anterior es un subdirectorio bangbean. Cuando se da a iar el nombre de un sub-
directorio, la herramienta empaqueta dicho subdirectorio completo dentro del archivo JAR (incluyendo, en este caso, el
archivo de código fuente original BangBean.java; no podemos incluir el código fuente con nuestros propios componentes
Beans). Además, si invertimos el proceso y desempaquetamos un archivo JAR recién creado, descubriremos que el archivo
de manifiesto no se encuentra en su interior, sino que jar ha creado su propio archivo de manifiesto (basado parcialmente
en el nuestro) denominado MANIFEST.MFy colocado dentro del subdirectorio META-INF ("meta-información"). Si abre
este archivo de manifiesto. verá también que iar ha añadido información de firma digital para cada archivo, de la forma:
Digest-Algorithms: SRA MD5
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udsxjOO=
MDS-Digest: 04NcSlhE3Smnzlp2hj6qeg==
En general, no tenemos que preocuparnos por nada de esto, y si realizamos modificaciones, basta con cambiar nuestro archi-
vo de manifiesto original y volver a invocar jar para crear un nuevo archivo JAR para nuestra Bean. También podemos aña-
dir otros componentes Bean al archivo JAR simplemente añadiendo la información correspondiente al manifiesto.
22 Interfaces graficas de usuario 931
Un aspecto que hay que resaltar es que nonnalmente conviene colocar cada Bean en su propio subdirectorio, ya que en el
momento de crear tul archivo l A R le entregamos a la uti lidad jar el nombre de un subdirectorio y esta utilidad se encarga
de colocar todo lo que ese subdirectorio contenga dentro del archivo JAR. Podrá observar que tanto Frog como BangBean
se encuentran en sus propios subdirectorios.
Una vez que hemos incluido adecuadamente nuestra Bean dentro de un archivo JAR, podemos integrarla dentro de un entor-
no !DE de desarrollo con componentes Bean. La forma de hacerlo varía de una herramienta a otra, pero Sun proporciona
una herramienta de prueba gratuita para JavaBeans, denominada "Bean Builder" (puede descargarla en http://java.
sun.com/beans). Podemos inserlar una Bean dentro de Bean Builder simplemente copiando el archivo JAR en el subdirec-
torio correcto.
Ejercicio 36: (4) Añada Frog.class al archivo de manifiesto de esta sección y ejecute ¡ar para crear un archivo JAR que
contenga tanto Frog como BangBean. Ahora, descargue e instale la herramienta Bean Builder de Sun, o
utilice su propia herramienta de constrncción de programas con Beans y añada el archivo JAR al entorno,
para poder probar los dos componentes Bean.
Ejercicio 37: (5) Cree su propia JavaBean denominada Valve que contenga dos propiedades: un valor de tipo boolean
denominado "on" y un valor ¡nt denominado "leve}", Cree un archivo de manifiesto, utilice jar para empa-
quetar la Bean, y luego cargue Bean Builder o alguna herramienta de construcción de programas basada
en Bean para poder probar el componente.
Alternativas a·Swing
Aunque la biblioteca Swing es la GUI recomendada por Sun, no es en modo alguno la única forma de crear interfaces grá-
ficas de usuario. Dos alternativas importantes son Macromedia Flash, que usa el sistema de programación Flex, para inter-
faces GUI del lado del cliente a través de la Web, y la biblioteca de código abierto SWT (Standard Widget Too/kit) de Eclipse
para aplicaciones de escritorio.
¿Por qué merecería la pena considerar otras alternativas? Para los clientes web. podemos sostener con cierta rotundidad que
los app/els han fallado. Considerando el tiempo que ha transcurrido desde que aparecieron (desde el principio de Java) y
todas las promesas y expectativas que levantaron, y sigue siendo una sorpresa encontrarse con una aplicación web basada
en app/els, ni siquiera Sun usa app/ets en todas partes. He aquí un ejemplo:
http://java.sun. com/deve/oper/onlineTraining/new2java/jovamap/intro.htm/
Un mapa interactivo de las características de Java en el sitio de Sun parecería un candidato bastante apropiado para cons-
truir un opp/el Java, a pesar de lo cual han construido este papel interactivo en Flash. Esto parece ser un reconocimiento
tácito de que las app/ets no han sido precisamente un éxito. Además, el reproductor Flash Player está instalado en más del
98 por ciento de las platafonnas informáticas, por lo que cabe considerarlo como un estándar de facto. Como veremos, el
sistema Flex proporciona un entorno de programación del lado del cliente muy potente, ciertamente más potente que
JavaScript y con un aspecto y estilo que resultan a menudo preferibles a los de un applet. Si queremos emplear opplets,
debemos seguir convenciendo al cliente de que descargue el entorno JRE, mientras que Flash Player es de pequeño tamaño
y se descarga muy rápidamente.
Para aplicaciones de escritorio, un problema con Swing es que los usuarios se dan cuenta de que están utilizando un tipo de
aplicación distinto, porque el aspecto y estilo de las aplicaciones Swing es diferente del que tiene el escritorio normal. Los
usuarios no están, generalmente, interesados en nuevos aspectos y estilos de aplicaciones, lo que quieren es realizar su tra-
bajo y prefieren que el aspecto de una aplicación se asemeje al de las restantes aplicaciones. SWT crea aplicaciones que se
asemejan a las aplicaciones nativas, y como la biblioteca utiliza componentes nativos siempre que es posible, las aplicacio-
nes tienden a ejecutarse más rápidamente que las aplicaciones equivalentes Swing.
Helio, Flex
Considere el siguiente fragmento de código MXML, que define una interfaz de usuario (observe que la primera y la última
líneas no aparecerán dentro del código que descargue como parte del código fuente de este libro):
10 Sean Neville ha creado una pru.1e fundamental del material de esta sección.
22 Interfaces gráficas de usuario 933
JJ>
</ mx:Scrip t >
<mx:Text lnput i d = lI i nput" wid th=1I2 00 11
c ha nge::::"upda t e Output () 11 />
<mx:Label id::::"ou t put " text =" He l lo!!I / >
</mx:Application >
111 ,-
Un control TextInput acepta la entrada de usuario y un control Label muestra los datos a medida que se los escribe. Observe
que el atríbuto id de cada control está accesible dentro del scripl en forma de nombre de variable, por lo que el scripl puede
hacer referencias a instancias de los marcadores MXML. En el campo Textlnpllt, podemos ver que el atríbuto change está
conectado a la función updateOutput( ) de modo que se invoca la función cada vez que se produce cualquier tipo de
cambio.
Compilación de MXML
La forma más fácil de comenzar a trabajar con Flex es con la versión gratuita de prueba, que se puede descargar en
\vww.macromedia.comlsoftware/flexllrial. tt El producto está empaquetado en una serie de ediciones, desde versiones de
prueba gratuitas hasta versiones de servidor empresarial, y Macromedia ofrece herramientas adicionales para el desarrollo
de aplicaciones Flex. El empaquetado exacto de las distintas versiones está sujeto a cambios, por lo que deberá comprobar
el sitio de Macromedia para conocer los detalles específicos. Observe también que puede que necesite modificar el archivo
jvm.eonfig situado en el directorio bin de la instalación de Flex.
11 Observe que es preciso descargar Flex y no FlexEuilder. Esta última es una herramienta de diseño mE.
934 Piensa en Java
Para compilar el código MXML y generar código intermedio Flash, tenemos dos opciones:
1. Podemos insertar el archivo MXML en una aplicación web Java, junto con páginas JSP y HTML en un archivo
WAR, y hacer que las solicitudes del archivo .mxml se compilen en tiempo de ejecución cada vez que un explo-
rador solicite la URL del documento MXML.
2. Podemos compilar el archivo MXML utilizando el compilador de la linea de comandos Flex, mxmlc.
La primera opción, la de la compilación en tiempo de ejecución basada en la Web, requiere un contenedor de servlet (como
Apache Tomcat) además de Flex. El archivo WAR del contenedor servlet de actualizarse con la información de configura-
ción de Flex, como por ejemplo los mapeos de servlet que se añaden al descriptor web.xml, y debe incluir los archivos JAR
de Flex, todos estos pasos se gestionan automáticamente cuando se instala Flex. Después de configurado el archivo WAR,
podemos insertar los archivos MXML en la aplicación Web y solicitar la URL del documento mediante cualquier explora-
dor. Flex compilará la aplicación cuando se reciba la primera solicitud, de forma similar a la que sucede en el modelo JSP,
yen lo sucesivo suministrará el código SWF compilado y almacenado en caché dentro de un envoltorio HTML.
La segunda opción no requiere un servidor. Cuando se invoca el compilador mxmlc de Flex en la línea de comandos, se
generan archivos SWF. Podemos implantar estos archivos de la forma que deseemos. El ejecutable mxmlc está ubicado en
el directorio bln de la instalación Flex, y si lo invocamos sin ningún argumento nos proporcionará una lista de las opciones
válidas de línea de comandos. Normalmente, lo que haremos será especificar la ubicación de la biblioteca de componentes
de cliente Flex como valor de la opción de la línea de comandos tlexlib, pero en algunos ejemplos muy simples como los
dos que hemos visto hasta ahora, el compilador Flex presupondrá la ubicación de la biblioteca de componentes. Por tanto,
podemos compilar los dos primeros ejemplos de la forma siguiente:
mxmlc.exe helloflexl.mxml
mxmlc . exe helloflex2.mxml
Esto genera un archivo heUotlex2.swf que se puede ejecutar en Flash, o que se puede insertar junto con código HTML en
cualquier servidor HTTP (una vez que Flash haya sido cargado en el explorador web, a menudo basta con hacer doble clic
sobre el archivo SWF para que éste se inicie en el explorador).
Para hellotlex2.swf, veremos la siguiente interfaz de usuario ejecutándose en Flash PI ayer:
En aplicaciones más complejas, podemos separar el código MXML y ActionScript haciendo referencia a funciones en archi-
vos ActionScript externos. Desde MXML, se utiliza la siguiente sintaxis para el control Script:
<mx:Script source =!!MyExternalScript.as!1 />
Este código permite a los controles MXML hacer referencia a funciones ubicadas en un archivo denominado MyExternal-
Script.as como si se encontraran en el propio archivo MXML.
MXML y ActionScript
MXML es una especie de abreviatura declarativa para las clases ActionScript. Cada vez que vemos un marcador MXML,
existe una clase ActionScript del mismo nombre. Cuando el compilador Flex analiza el código MXML, primero transforma
el código XML en código ActionScript y carga las clases ActionScript referenciadas, despues de lo cual compila y monta el
código ActionScript para crear un archivo SWF.
Podemos escribir una aplicación Flex completa utilizando exclusivamente ActionScript, sin usar nada de MXML. Por tanto,
MXML es simplemente un lenguaje de utilidad. Los componentes de interfaz de usuario como los contenedores y contro-
les, se declaran típicamente utilizando MXML, mientras que la lógica asociada, como por ejemplo las rutinas de tratamien-
to de sucesos y el resto de la lógica de cliente se gestionan mediante ActionScript y Java.
Podemos crear nuestros propios controles MXML y hacer referencia a ellos utilizando MXML, escribiendo clases
ActionScript. También podemos combinar contenedores y controles MXML existentes en un nuevo docwnento MXML al
que luego podamos hacer referencia mediante un marcador en otro documento :MXML. El sitio web de Macromeclia con-
tiene más información acerca de cómo hacer esto.
22 Interfaces gráficas de usuario 935
Contenedores y controles
El núcleo visual de la biblioteca de componentes Flex es un conjunto de contenedores que gestionan la disposición de los
elementos y una matriz de controles que se inserta en esos contenedores. Entre los contenedores se incluyen paneles, recua-
dros verticales y horizontales, mosaicos, acordeones, recuadros divididos, cuadrículas y otros. Los controles son elementos
de la interfaz de usuario, como por ejemplo, botones, áreas de texto, deslizadores, calendarios, cuadrículas de datos, etc.
En el resto de esta sección vamos a mostrar una aplicación Flex que muestra y ordena una lista de archivos de audio. Esta
aplicación ilustra el uso de contenedores y de controles y muestra cómo conectar Java desde Flash.
Comenzamos el archivo MXML colocando un control DataGrid (uno de los controles más sofisticados de Flex) dentro de
un contenedor de tipo Panel:
//: 1 gui/flex/songs.mxml
<?xml version",;"l.O" encoding="utf-8"?>
<mx:Application
xmlns:mx= .. http://www.macromedia.com/2003/mxml u
backgroundColor",;"#B9CAD2" pageTitle=!1Flex Song Manager"
initialize=lIgetSongs() u>
<tnX:Script source=!lsongScript.as ll />
<mx:Style source="songStyles.css u/>
<mx:Panel id= UsongListPanel "
titleStyleDeclaration=uheaderText U
title=IIFlex MP3 Library">
<mx:HBox verticalAlign",;Ubottom">
<tnx:DataGrid id=!lsongGrid l1
cellPress="selectSong(event) 11 rowCount=uB!l>
<mx:columns>
<mx:Array>
<mx:DataGridColumn colunmName=lIname"
headerText="Song Name ll width="120 11 />
<mx:DataGridColunm colurnnName=lIartist"
headerText=IIArtist" width="lBon />
<tnx:DataGridColunm colurnnName=ualbum"
headerText="Album ir width:="160" />
</mx:Array>
</mx:colutnns>
</mx:DataGrid>
<mx:VBox>
<mx:HBox height="loon >
<mx:lmage id="albumlmage U source=UI!
height=uBOII width=1I1001l
mouseOverEffect=lIresizeBig"
ltIouseOutEffect=lIresizeSmall ll />
<tnX:TextArea id="songlnfo ll
styleName=lIboldText ll height=uIOO%1I width="120u
vScrollPolicy=lIoff ll borderStyle=lInonel! />
</mx:HBox>
<tnX:MediaPlayback id=lIsongPlayer U
contentPath=urr
mediaType=IIMP3 11
height,,=u70 ii
width=1I230 11
controllerPolicy=."on H
autoPlay= ir false li
visible=lIfalse ll />
</mx:VBOX>
</mx:HBox>
<n1X: ControlBar horizontalAlign= 11 right rl >
<mx:Button id=urefreshSongsButtOíl"
936 Piensa en Java
Efectos y estilos
El reproductor Flash Player muestra los gráficos utilizando tecnología vectorial, así que puede realizar transfonmaciones
altamente expresivas en tiempo de ejecución. Los efectos Flex proporcionan una pequeña muestra de este tipo de animacio-
nes. Los efectos son transformaciones que pueden aplicarse a los controles y contenedores utilizando sintaxis MXML.
El marcador Effec! mostrado en el código MXML produce dos resultados: el primer marcador anidado hace crecer dinámi-
camente una imagen cuando se desplaza el ratón sobre él, mientras que el segundo contrae dinámicamente dicha imagen
cuando el ratón se aleja. Estos efectos se aplican a los sucesos de ratón disponibles en el control Image para albumlmage.
Flex también proporciona efectos para animaciones comunes como transiciones, cortinillas y canales alfa modulados.
Además de los efectos predefinidos, Flex soporta la API de dibujo de Flash para la definición de animaciones verdadera-
mente innovadoras. Una exploración detallada de este tema implicaría muchos conceptos de diseño gráfico y animación, y
esto queda fuera del alcance de esta sección.
La utilización de estilos estándar es posible gracias al soporte que Flex proporciona para la especificación CSS (Cascading
Style Sheets, hojas de estilo en cascada). Si asociamos un archivo CSS a un archivo MXML, los controles FJex se adapta-
rán a esos estilos. Para este ejemplo, songStyles.css contiene la siguiente declaración CSS:
//:! gUi / flex / songStyles . css
.headerText {
22 Interfaces gráficas de usuario 937
.boldText
font-family: Arial, 11 sansl! i
font-size: 11;
font-weight: bold¡
Este archivo se importa y se emplea en la aplicación de la biblioteca de canciones a través del marcador Style en el archi-
vo MXML. Después de importada la hoja de estilo, sus declaraciones pueden aplicarse a los controles Flex en el archivo
MXML. Como ejemplo, el control TextArea utiliza la declaración boldText de la hoja de estilo con songInfo id.
Sucesos
Una interfaz de usuario es una máquina de estados; realiza diversas acciones a medida que se producen cambios de estado.
En Flex, estos cambios se gestionan mediante sucesos. La biblioteca de clases Flex contiene una amplia variedad de con-
troles con numerosos sucesos que cubren todos los aspectos de movimiento del ratón y de la utilización del teclado.
El atributo click de un botón Bulton, por ejemplo, representa uno de los sucesos disponibles en dicho control. El valor asig-
nado a click puede ser una función o un pequeño scripl integrado. En el archivo MXML, por ejemplo, el control ControlBar
incluye el botón refreshSongsButton para refrescar la lista de canciones. Puede ver, analizando el marcador, que cuando se
produce el suceso click se invoca songService.getSongs( l. En este ejemplo, el suceso c1ick del control Bulton hace refe-
rencia al objeto RemoteObject que se corresponde con el método Java.
Debido a la configuración de MXML, esto hará que se invoque getSongs( l en la clase SongService:
JI: gui/flexjSongService.java
package gui.flex;
import java.util.*¡
function selectSong(event) {
var song = songGrid.getltemAt(event.iteml ndex)¡
showSonglnfo{song) ¡
22 Interfaces gráficas de usuario 939
function onSongs(songs)
songGr i d.da t aProvider songs;
} 1// , -
Para gestionar la selección de celdas de la cuadrícula de datos DataGrid, añadimos el atributo de sucesos cellPress a la
declaración DataGrid en el archivo MXML:
ce ll Press="selec t Song (event ) 11
Cuando el usuario hace clic sobre una canción en la cuadrícula de dalos DataGrid, se invoca selectSong( ) en el código
ActionScript anterior.
les de los datos, facilitando el uso del patrón Modelo-Visto-Controlador (MVC). En aplicaciones más sofisticadas y de
mayor envergadura, la complejidad inicial provocada por la inserción de un modelo constituye una desventaja si la compa-
ramos con el valor que nos proporciona una aplicación MVC limpiamente desacoplada.
Además de acceder a objetos Java, Flex también puede acceder a servicios web basados en SOAP y a servicios HTTPuti-
lizando los controles WebService y HttpService, respectivamente. El acceso a todos los servicios está sujeto a restriccio-
nes de autorización por razones de seguridad.
Ejercicio 38: (3) Construya el "ejemplo simple de sintaxis de acoplamiento de datos" mostrado anterionnente.
Ejercicio 39: (4) La descarga de código para este libro no incluye los archivos MP3 o JPG mostrados en
SongService.java. Localice algunos archivos MP3 y JPG, modifique SongScrvice.java para incluir los
correspondientes nombres de archivo, descargue la versión de prueba de Flex y construya la aplicación.
Instalación de SWT
Las aplicaciones SWT requieren que se descargue e instale la biblioteca SWT desarrollada en el proyecto Eclipse. Vaya a
www. eclipse.org/down/oads/y seleccioneunode los sitios espejo. Siga los v ínculos hasta localizar la versión Eclipse actua l
y localice un archivo comprimido con un nombre con "sw!" e incluya el nombre de su plataforma (por ejemplo, "win32").
Dentro de este archivo encontrará swt.jar. La fonna más fácil de instalar el archivo swt.jar consiste en colocarlo en el direc-
torio jre/lib/ext (de esta fonna, no tendrá que hacer ninguna modificación en su ruta de clases). Cuando descomprima la
biblioteca SWT, puede que encuentre archivos adicionales que necesitará instalar en los lugares apropiados para su plata-
forma. Por ejemplo, la distribución Win32 incluye archivos DLL que tienen que incluirse en algún lugar de la ruta
java.library.path (ésta coincide usualmente con la variable de entorno PATH, pero puede ejecutar object/ShowProperties
.java para descubrir el valor real de java.llbrary.path). Una vez que haya hecho esto, deberia poder compilar y ejecutar
transparentemente la aplicación SWT como si fuera cualquier otro programa Java.
La documentación de SWT se encuentra en un archivo de descarga separado.
Una técnica altemativa consiste en limitarse a instalar el editor Eclipse, que incluye tanto SWT como la documentación de
SWT que se puede visualizar a través del sistema de ayuda de Eclipse.
Helio, SWT
Comencemos con la aplicación más simple posible del estilo de la conocida aplicación "helio world":
jj , swtjHe ll oSWT.j ava
II {Requires: org. ecl ipse.s wt. widgets.Di splay; You rnust
II instal l the SWT library fram http: // www.eclipse.org }
import org .eclipse. sw t.w idge ts.*;
public class HelloSWT {
12 Chris Grindstaff resultó de mucha ayuda a la hora de lTaducir los ejemplos a SWT y de proporcionar infonnación acerca de SWT.
942 Piensa en Java
Si descarga el código fuente de este libro, descubrirá que la directiva de comentario "Requires" tennina siendo incluida en
el archivo build.xml de Ant como un pre-requisito para construir el subdirectorio swt; todos los archivos que importen
org.eclipse.swt requieren que se instale la biblioteca SWT de www.eclipse.org.
La clase Display gestiona la conexión entre SWT y el sistema operativo subyacente; fonua parte de un Puente entre el sis-
tema operativo y SWT. La clase Shell es la ventana principal de nivel superior. dentro de la que se construyen todos los
demás componentes. Cuando se invoca setText( ), el argumento se convierte en la etiqueta que aparecerá en la barra de títu-
lo de la ventana.
Para mostrar la ventana y luego la aplicación, debe invocar open( ) sobre el objeto Shell.
Mientras que Swing oculta a nuestros ojos el bucle de tratamiento de sucesos, SWT nos obliga a escribirlo explícitamente.
En la parte superior del bucle, miramos si la shell ha sido eliroinada; observe que esto nos proporciona la opción de inser-
tar código para llevar a cabo actividades de limpieza. Pero esto quiere decir que la hebra main( ) es la hebra de la interfaz
de usuario. En Swing, se crea de manera transparente una segunda hebra de despacho de sucesos, pero en SWT, es la hebra
main( ) la que se encarga de gestionar la interfaz de usuario. Puesto que de manera predeterminada sólo existe una hebra y
no dos, esto hace que sea algo menos probable que terminemos sobrecargando la interfaz de usuario con hebras.
Observe que no tenemos que preocuparnos de enviar tareas a la hebra de interfaz de usuario, a diferencia de 10 que sucedía
en Swing. SWT no sólo se encarga de esto por nosotros, sino que genera una excepción si tratamos de manipular un widget
con la hebra errónea. Sin embargo, si necesitamos crear hebras para realizar operaciones de larga duración, seguimos nece-
sitando enviar los cambios de la misma fonna que se hace en Swing. Para esto, SWT proporciona tres métodos que pueden
invocarse sobre el objeto Display: asyncExec(Runnable), syncExec(Runnable) y timerExec(int, Runnable).
La actividad de la hebra main( ) en este punto consiste en llamar a readAndDispatch( ) para el objeto Display (esto signi-
fica que sólo puede haber un objeto Display por cada aplicación). El método readAndDispatch() devuelve true si hay dos
sucesos en la cola de sucesos esperando ser procesados. En dicho caso, hay que volver a invocar un -método inmediatamen-
te. Sin embargo, si no hay nada pendiente, invocamos el método sleep( ) del objeto Display para esperar durante un perio-
do breve de tiempo antes de volver a consultar la cola de sucesos.
Una vez completado el programa, es necesario eliminar explícitamente el objeto Display con dispose( ). SWT requiere a
menudo que eliminemos explícitamente los recursos no utilizados, porque se trata usualmente de recursos del sistema ope-
rativo subyacente, que podrían de otro modo agotarse.
Para demostrar que el objeto Shell es la ventana principal, he aquí un programa que construye una serie de objetos Shell:
11: swt/ShellsAreMainWindows.java
import org.eclipse.swt.widgets.*i
while(lshellsDisposed())
22 Interfaces gráficas de usuario 943
if { !disp l ay .readAndDispatch())
display.s l eep() ;
display .dispose( ) i
Al ejecutarlo, se obtienen diez ventanas principales. De la forma en que se ha escrito el programa, si se cierra una ventana,
se cerrarán todas.
SWT también emplea gestores de disposición, que son distintos a los de Swing, pero que están basados en la misma idea.
He aquí un ejemplo ligeramente más complejo que toma el texto de System.getProperties( ) y lo añade a la ,he/!:
JI : s wt f Display Properties.java
import o rg.eclipse.swt.*i
import org.eclipse.swt.widgets.*¡
impor t org.eclipse .swt.layout.*;
import java.io.*;
En SWT, todos los widgets deben tener un objeto padre de tipo general Composite, y hay que proporcionar este padre como
el primer argumento en el constructor de widget. Podemos ver esto en el constructor Text, donde sheU es el primer argu-
mento. Casi todos los constructores también toman un argumento indicador que permite proporcionar varias directivas de
estilo, dependiendo de lo que el widget concreto acepte. Las diversas directivas de estilo se combinan bit a bit mediante la
operación OR, como puede verse en el ejemplo.
A la hora de configurar el objeto Text(), he añadido indicadores de estilo para que el texto efectúe saltos de línea automá-
ticos, y para que se añada automáticamente una barra de desplazamiento vertical en caso necesario. Cuando trabaje con este
sistema, descubrirá que SWT depende bastante de los constructores; existen muchos atributos de un widget que son difici-
les o imposibles de cambiar, excepto a través del constructor. Compruebe siempre la documentación del constructor del wid-
gel para ver qué indicadores acepta. Observe que algunos constructores requieren un argumento indicador, aun cuando la
docunlentación no especifique ningún indicador "aceptado". Esto permite una futura expansión sin necesidad de modificar
la interfaz.
Shell a partir del objeto Display, creamos un bucle readAndDispatch( l, etc. Por supuesto, en algunos casos especiales,
puede que no hagamos esto, pero estas tareas son lo suficientemente comunes como para que merezca la pena intentar eli-
minar el código duplicado, como hicimos con net.mindview.util.SwingCollsole.
Tendremos que obligar a cada aplicación a adaptarse a una interfaz:
jj: swt jut il jSWTApplication.java
package swt.util¡
import org. eclipse .swt.widgets . *¡
///,-
SWTConsole nos permite centrarnos en los aspectos más interesantes de una aplicación, en lugar de tener que escribir el
código repetitivo.
Ejercicio 40: (4) Modifique DisplayProperties.java para utilizar SWTConsole.
Ejercicio 41: (4) Modifique DisplayEnvironment.java para no utilizar SWTConsole.
Menús
Para ilustrar los fundamentos de los menús, el siguiente ejemplo lee su propio código fuente y lo descompone en palabras,
rellenando luego los menús con estas palab ras:
JI : swc/Menus.j ava
/1 Ejemplo de menús.
i mport swt.ut il.*i
impor t org.eclip se.s wt.*i
import org .eclipse . swt.widgets .*¡
i mport j ava.util.*i
impo rt net . mindview.uti l. *¡
int i = O;
while(it. hasNext())
addltem(bar, i t, mltem[i ] ) ;
i ~ (i + 1 ) % mltem.length ¡
if(browser 1= nu ll) {
b rows er. setUrl ( "http://www.mindview.net ll ) i
tab .setControl (browser) ¡
Aquí, createContents( ) establece la disposición de los elementos y luego invoca los métodos que se encargan de crear las
distintas fichas. El texto de cada ficha se establece con setText() (también podemos crear botones y gráficos en una ficha),
y cada una de las fichas establece también el texto de sugerencia. Al fmal de cada método, puede ver una llamada a
setControl( ), que coloca el control que el método ha creado dentro del espacio correspondiente a cada ficha concreta.
labelTab( ) ilustra un etiqueta simple de texto. directoryDialogTab( ) contiene ID' botón que abre un objeto estándar
DirectoryDialog para que el usuario pueda seleccionar un directorio. El resultado se asigna como texto del botón.
buttonTab( ) muestra los diferentes botones básicos. sliderTab( ) repite el ejemplo Swing presentado anteriormente en el
capítulo, que consistía en asociar un deslizador con una barra de progreso.
22 Interfaces gráficas de usuario 949
scribbleTab() es un divertido ejemplo de gráficos. Se genera un programa de dibujo utilizando sólo unas pocas lineas de
código.
Por último, browserTab() muestra la potencia del componente Browser de SWT, que es un explorador web completamen-
te funciona1 empaquetado en un único componente.
Gráficos
He aquí el programa SineWave.java de Swing traducido a SWT:
/1 : swt/SineWave.java
// Traducción a SWT del programa Swing SineWave.java.
impo rt swt.util . *¡
import org. e clipse.swt .*i
import org.e c lipse .swt.widget s .*¡
import org . eclipse .swt.event s.*;
i mport org.eclipse .swt.layout .*i
)
) ;
setCyc l es(S) j
redraw() ;
950 Piensa en Java
Concurrencia en SWT
Aunque AWT/Swing es monohebra, resulta posible violar fácilmente esa caracteristica monohebra de manera que se obten-
ga un programa no determinista. Básicamente, debemos evitar tener múltiples hebras escribiendo en la pantalla, porque las
unas escribirán sobre lo que hayan escrito las otras de manera sorprendente.
SWT no permite que esto suceda, puesto que genera una excepción si tratamos de escribir en la pantalla empleando más de
una hebra. Eso evitará que un programador inexperto cometa accidentalmente este error e introduzca errores dificiles de
localizar dentro de un programa.
He aquí la traducción a SWT del programa Swing ColorBoxes.java:
1/: swt/ColorBoxes.java
1/ Traducción a SWT del programa Swing ColorBoxes.java.
import swt.util.*¡
import org.eclipse.swt.*i
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.events.*¡
import org. eclipse. swt. graphics. * i
import org.eclipse.swt.layout.*;
import java.util.concurrent.*¡
import java.util.*;
import net.mindview.util.*;
class CBox extends Canvas implements Runnable {
class CBoxPaintListener implements PaintListener
publicvoid paintControl(PaintEvent e) {
Color color = new COlor(e.display, cColor) i
22 Interfaces gráficas de usuario 951
e.gc.setBackground(color) i
Point size = getSize ();
e.gc. fillRectangle (O , O, size.x, size.y);
color.dispose{) ;
prívate i n t pause;
private RGB cColor = newColo r () ;
public CBox{Composíte pare nt, int pause) {
super {parent, SWT.NONE) i
this.pause = pause;
addPaintListener(new cBoxpaintListener{») ¡
)
public vo id run( ) {
try {
whi l e{!Thread. inte rrupted (»
cColor = newColor () ;
getDisplay() .asyncExec(new Runnable()
pUblic void run() {
try { redraw (); ) catch (SWTException e) ()
II SWTException e,s OK cuando el padre es
II terminado desde debajo de nosotros.
)
J) ;
TimeUnit.MILLISECONDS.sleep(pause) ;
¿SWT O Swing?
Resulta dificil hacerse una idea general a partir de una introducción tan corta, pero el lector debería al menos comenzar a
percibir que SWT puede ser, en muchas situaciones, una fonna más sencilla de escribir programas que Swing. Sin embar-
go, la programación de GUI en SWT puede seguir siendo compleja, por lo que los motivos para utilizar SWT deberían ser,
en primer lugar, proporcionar al usuario una experiencia más transparente a la hora de usar la aplicación (porque el aspec-
to y estilo de la aplicación se asemejarán a los de otras aplicaciones en dicha plataforma) y segundo, si la mayor velocidad
proporcionada por SWT resulta importante. En caso contrario, Swing puede ser una elección perfectamente apropiada.
Ejercicio 43: (6) Seleccione alguno de los ejemplos Swing que no haya sido traducido en esta sección y lradúzcalo a
SWT. (Nota: esto puede constituir un buen ejercicio para que los estudiantes realicen en casa, ya que las
soluciones no se han incluido en la guía de soluciones).
Resumen
Las bibliotecas GUI de Java han experimentado cambios bastante drásticos a lo largo del tiempo de existencia del lengua-
je. La biblioteca AWT de Java 1.0 fue muy criticada por su pobre diseílo, y aunque permitía crear programas portables, la
GUI resultante era "igualmente mediocre en todas las plataformas". También era bastante limitadora, abstrusa e incómoda
de emplear comparada con las herramientas de desarrollo nativas disponibles en diversas plataformas.
Cuando Java 1.1 introdujo el nuevo modelo de sucesos y los componentes JavaBeans, el escenario completo ya estaba pre-
parado: allOr. era posible crear componentes GUl que se podían arrastrar y colocar fácilmente dentro de un entorno IDE
visual. Además, el di seño del modelo de sucesos y de JavaBeans muestra claramente que los objetivos eran la facilidad de
programación y la utilización de código fácilmente mantenible (algo que no era evidente en la bibliotecaAWT de la versión
1.0). Pero la transición no se concretó hasta que aparecieron las clases JFC/Swing. Con los componentes Swing, la progra-
mación de interfaces GUI multiplataforma puede resultar sencilla.
La verdadera revolución radica en el uso de entornos IDE. Si desea adquirir un entorno IDE comercial para mejorar la pro-
gramación con un lenguaje propietario, está obligado a cruzar los dedos y a esperar que el fabricante le proporcione lo que
usted espera. Pero Java es un entorno abierto, por lo que no sólo permite que existan entornos IDE competidores, sino que
fomenta esa existencia. Y para que estas herramientas puedan tomarse en serio, están obligadas a soportar JavaBeans. Esto
significa que el campo de juegos está nivelado para todos los contendientes; si aparece un entorno !DE mejor, no eslamos
obligados a continuar empleando el anterior. Podemos seleccionar el nuevo e incrementar con ello nuestra productividad.
Este tipo de campo de juego competitivo para entornos !DE de creación de interfaces GUI no había sido experimentado
antes, y el mercado resultante permitirá generar resultados muy positivos en lo que respecta a la productividad de los pro-
gramadores.
22 Interfaces gráficas de usuario 953
Este capítulo ha pretendido únicamente proporcionar una introducción a la potencia de la programación de interfaces GUr,
y hacer que el lector comenzara a familiarizarse con material de programación de interfaces para ver lo simple que resulta
emplear las correspondientes bibliotecas. Lo que hemos visto en el capítulo bastará, probablemente, para cubrir una parte
de nuestras necesidades de diseño de interfaces de usuario. Sin embargo, tanto Swing como SWT y FlashIFlex tienen
muchas otras características y funcionalidades adicionales, ya que se trata de herramientas de diseño de interfaces de usua-
rio completas. Con ellas, probablemente encuentre una forma de realizar cualquier cosa que sea capaz de imaginar.
Recursos
Las presentaciones en línea de Ben Galbraith disponibles en www.galbraiths.org/presentations hacen un repaso muyade-
cuado tanto de Swing como de SWT.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Jne Thinking in Java Annotated Solution Guide, disponible para
la venta en www.MindView.net.
Suplementos
Este libro tiene varios suplementos, incluyendo los elementos, seminarios y servicios
disponibles en el sitio web MindView.
Este apéndice describe estos suplementos, para que pueda decidir si le pueden ser útiles.
Observe que aunque los seminarios suelen ser públicos, tambi~n pueden impartirse ,como seminarios privados en sus pro,...
piasüficinas.
Suplementos descargables
El código correspondiente a este libro está disponible para su descarga en www.MindView.net. Esto inclnye los archivos de
construcciónAnt y otros archivos .de soporte necesarios para construir y ejecutar correctamente todos los ejemplos del libro.
Además, algunas partes del libro sólo se ofrecen en formato electrónico. Los temas que estas partes abarcan:
• Clonación de objetos
• Paso y devolución de objetos
• Análisis y diseño
• Partes de otros capítulos procedentes de la 3a edición de Thinking in Java, que no eran lo suficientemente rele-
vantes como para incluirlos en la versión impresa de la cuarta edición .dellibro.
• XML
• Servicios Web
• Pruebas automáticas
A Suplementos 957
Puede encontrar infonnación sobre el estado actual (en inglés) de Thinking in Enterprise Java en www.MindView.net.
Software
El JDK disponible ert http://java.slln.com. Incluso si decide emplear un entorno de desarrollo de otro fabricante, siempre es
convertiente tener el JDK a mano, por si acaso se tropieza con algún posible error del compílador. El JDK es la piedra de
toque del diseño Java, y si existe un error en él, hay grandes posibilidades de que dicho error esté bieu documentado.
La documentación del JDK disponible en http://javacSun.com, en fonnato HTML. No he podido encontrar todavía un libro
de referencia sobre las bibliotecas estándar de Java que no estuviera desactualizado o en el que no fa ltara infonuación.
Aunque la documentación del JDK de Sun tiene algunos pequeños errOres y es excesivamente concisa en algunos puntos, a
menos incluye todas las clases y métodos. En ocasiones, algunas personas no se sienten cómodas utilizando inicialmente un
recurso en línea en lugar de un libro en papel, pero merece la pena superar eSa incomodidad inicial y consultar los docu-
mentos HTML, al menos para obtener una panorámica general. Si le sigue sin gustar el método de las referencias ert línea,
adquiera los libros impresos.
Libros
Core Java™ 2, 7'h Edit;on, Volúmenes I & JI, de Horstmann & Comell (prentice Hall, 2005). Voluminoso, completo y es
donde debe buscar en primer lugar cuando esté buscando respuestas. Recomiendo la lectura de este libro una vez que se
haya completado Thinking in JOVd y se necesite comenzar a profundizar en los distintos temas.
The Java™ C/ass Libraries: An Annotated Refermee, por Patrick Chan and Rosatula Lee (Addison-Wesley, 1.997J
Aunque está desactualizado, este Iíbro es lo que la referencia JDK deberfa haber sido; incluye las suficientes descripciones
960 Piensa en Java
como para hacerlo utilizable. Uno de los revisores técnicos de Thinking in Java me dijo: "Si tuviera un único libro sobre
Java, seria éste (además del tuyo, por supuesto)." Yo no estoy tan entusiasmo con este libro, como dicho revisor. Es volu-
minoso, resulta caro y la calidad de los ejemplos no me parece adecuada. Sin embargo, es tul buen lugar en el que consul-
tar cuando estemos bloqueados con un problema y aborda los temas con una mayor profundidad que la mayoría de los demás
libros. Sin embargo, Core Java 2 tiene un tratamiento más actualizado de muchos de los componentes de la biblioteca.
Java Network Programmillg, 2" Edición, por Elliolte Rusty Harold (O'Reilly, 2000). Personahnente, no empecé a compren-
der los aspectos de las redes en Java (ni, en realidad, los aspectos de comunicación por red, en general) hasta que encontré
este libro. También me parece que su sitio web, Café au Lait, es muy estimulante, infonnativo y actualizado en lo que res-
pecta a los desarrollos Java, siendo muy independiente respecto a los distintos fabricantes de software. Las actualizaciones
regulares hacen que el sitio web esté lleno de noticias acerca de la evolución de Java. Consulte www.cafeaulait.org.
Design Pallerns, por Gamma, Helm, Johnson y Vlissides (Addison-Wesley, 1995). El libro originaJ que dio comienzo al
movimiento de patrones de diseílo dentro del campo de la programación y que hemos mencionado en numerosos lugares a
lo largo del libro.
Refactoring to Pallems, por Joshua Kerievsky (Addison-Wesley, 2005). Combina el tema de la reingeniería con el de los
patrones de diseño. Lo más valioso de este libro es que muestra cómo hacer evolucionar un diseño introduciendo nuevos
patrones a medida que son necesarios.
The Art of UNIX Programming, por Eric Raymond (Addison-Wesley, 2004). Aunque Java es un lenguaje interplataforma,
la prevalencia de Java en el mundo de los servidores ha hecho que el conocimiento de Unix/Linux sea importante. El libro
de Eric constituye una excelente introducción a la historia y filosofía de este sistema operativo, y resulta fascinante de leer
aunque sólo se quieran comprender algunos aspectos de los orígenes de la informática.
Análisis y diseño
Extreme Programming Explained, 2"d Edition, por Kent Beck con Cynthia Andres. (Addison-Wesley, 2005). Siempre he
pensado que debería haber un proceso de desarrollo de programas muy distinto y mucho mejor que el que se emplea actual-
mente, y creo que XP se acerca bastante a ese ideal. El único libro que me ha causado un impacto similar es Peopleware
(del que hablaré más adelante), que habla principalmente acerca del entorno y de cómo tratar con la cultura corporativa.
Ex/reme Programming Explained habla acerca de la programación y proporciona una nueva visión sobre los principales
aspectos de esta ciencia. Los autores llegan incluso a decir que las imágenes están bien siempre que no invirtamos demasia-
do tiempo en ellas y estemos dispuestos a prescindir de ellas en caso necesario (observará que el libro no tiene la marca de
aprobación "UML" en la cubierta). En mi opinión podría decidirse si trabajar para una empresa basándose exclusivamente
en si utilizan XP. Es un libro pequeño, de capítulos cortos, que se leen sin esfuerzo y que resuJta excitante. Uno puede ima-
ginarse a sí mismo en ese tipo de atmósfera y le asaltan visiones de un nuevo mundo que se abre a sus pies.
UML Distilled, 2" Edición, por Martin Fowler (Addison-Wesley, 2000). Cuando uno tropieza por primera vez con UML,
resulta aterrador, dada la gran cantidad de diagramas y de detalles que existen. De acuerdo con Fowler, la mayor parte de
estos detalles son innecesarios, por lo que él se centra en los aspectos esenciales. Para la mayoría de los proyectos, basta con
conocer unas pocas herramientas de realización de diagramas, y el objetivo de Fowler es conseguir un buen diseño, en lugar
de preocuparse acerca de todos los .aditamentos que hacen falta para conseguirlo. De hecho, la mayor parte de los lectores
no necesitarán la prímera mitad del libro. Es un libro muy atractivo, de pequeílo tamaño y mi recomendación es que lo
adquiera si desea comprender UML.
Domain-Driven Design, por Eric Evans (Addison-Wesley, 2004). Este libro se centra en el modelo de dominios como prin-
cipal herramienta del proceso de diseílo. En mi opinión, se trata de un desplazamiento interesante del foco de atención que
ayuda a los diseñadores a adoptar el nivel de abstracción adecuado.
The Unified Software Development Process, por Ivar Jacobsen, Grady Booch y James Rumbaugh (Addison-Wesley, 1999).
Cuando aborde la lectura de este libro, estaba preparado para que no me gustara. Parecia tener todos los ingredientes de un
aburrido libro universitarío. Sin embargo, me vi gratamente sorprendido (aunque hay algunas partes que incluyen explica-
ciones que dan la sensación de que los autores no tienen los conceptos claros). El conjunto del libro no sólo es muy claro,
sino también muy agradable de leer. Lo mejor de todo es que el proceso descrito tiene bastante sentido práctico. No se trata
de Extreme Programming (y no tiene la claridad que esta otra técnica presenta en lo que respecta a las pruebas), pero tam-
bién forma parte del arsenal del mundo UML; incluso aquellos que no desean utilizar XP suelen estar de acuerdo en que
"UML es un buen lenguaje de modelado" (independientemente del nivel experiencia real que tengan los que emiten estas
2 Todo es un objeto 961
opiniones). Este libro constituye una verdadera referencia de UML y es lo que yo recomendaría, para conocer más detalles
después de leer UML Distilled de Fowler.
Antes de elegir ningún método concreto, resulta útil ganar cierta perspectiva, aprendiéndola de aquellos que no tratan de
vendernos ninguna en concreto. Es fácil adoptar un método sin llegar a entender realmente qué es lo que queremos obtener
de él o qué es lo que nos puede proporcionar. Que otras personas usen este método, parece una razón suficiente. Sin embar-
go, los seres humanos tienen una tendencia psicológica muy curiosa. Si quieren creer que algo resolverá sus problemas, tra-
tarán de usarlo (esto es experimentación, lo cual es algo bueno). Pero si no resuelve sus problemas, puede que redoble sus
esfuerzos y comience a anunciar a grandes voces esa cosa tan increíble que han descubierto (esto es negación de la reali-
dad, que no es tan bueno). El mecanismo que subyace a este comportamiento es que si podemos conseguir que otras perso-
nas se suban al mismo barco, al menos no estaremos solos, incluso aunque el barco no vaya a ninguna parte (o se esté
hundiendo).
Con esto no quiero sugerir que todas las metodologías no vayan a ninguna parte, sino sólo que debemos utilizar las herra-
mientas mentales que nos permitan pennanecer en modo de experimentación ("No está funcionando, probemos alguna otra
cosa"), sin entrar en el modo de negación ("No, no se trata realmente de un problema. Como todo es maravilloso, no nece-
sitamos cambiarlo"). En mi opinión, los siguientes libros que hay que leer antes de elegir un método, le proporcionarán esas
herramientas fundamentales.
Software Crea/ivity, por Robert L. Glass (Prentice Hall, 1995). Éste es el mejor libro que yo he visto en donde se analiza la
p erspectiva de todo el tema de las metodologías. Es una colección de ensayos cortos y artículos que Glass ha escrito y en
ocasiones adquirido (P.J. Plauger es uno de los contribuidores), en donde se refleja sus muchos años de reflexión y estudio
sobre el tema. Son bastante entretenidos y su longitud es estrictamente la adecuada para transmitir toda la información nece-
saria; el autor no divaga ni aburre. Tampoco se dedica a vender humo: hay cientos de referencias a otros artículos y estu-
dios. Todos los programadores y gestores deberían leer este libro antes de entrar en el tema de las metodologías.
Software Runaways: MOllltlllen/al Software Disas/ers, por Robert L. Glass (Prentice Hall, 1998). Lo mejor acerca de este
libro es que pone de manifiesto todas esas cosas de las que a nadie le gusta hablar: la cantidad de proyectos que no sólo
fallan, sino que lo hacen de manera espectacular. La mayoría de la gente tiende a pensar: "Eso no me puede pasar a mí" (o
"Eso no me puede pasar de nuevo"), yeso hace que juguemos con desventaja. Recordando que las cosas siempre pueden ir
mal, estaremos en una posición mucho mejor para hacer que vayan bien.
Peopleware, 2' Edición, por Tom DeMarco y Timothy Lister (Dorset House, 1999). Tiene que leer este libro. No sólo es
divertido, sino que conmueve todos los cimientos de nuestro esquema mental y destruye nuestras suposiciones previas.
Aunque DeMarco y Lister provienen del campo de desarrollo de software, este libro trata de proyectos y equipos de traba-
jo en general. El libro se centra en las personas y en sus necesidades, más que en la tecnología y en las necesidades de la
tecnología. Hablan acerca de crear un entorno en el que las personas estén contentas y sean productivas, en lugar de deci-
dir las reglas que tienen que seguir para ser componentes adecuados de una máquina. Esta última aptitud, en mi opinión, es
la que más contribuye a que los programadores se sonrian y se burlen cuando se adopta el método XYZ, después de lo cual
continúan haciendo en silencio lo que siempre han estado haciendo.
Secre/s of Consulting: A Guide /0 Giving & Gelling Advice Sltccessfully, por Gerald M. Weinberg (Dorset House, 1985).
Este libro maravilloso es uno de mis favoritos. Resulta perfecto cuando alguien quiere dedicarse a la consultoría, o si ha
contratado los servicios de algún consultor y desea mejorar las cosas. Está compuesto de cortos capítulos, llenos de histo-
rias y anécdotas que nos enseñan cómo llegar hasta el fondo del asunto con un esfuerzo mínimo. Lea también More Secrets
01 Consulting, publicado en 2002, o cualquier otro de los libros de Weinberg.
Complexity, por M. Mitchell Waldrop (Simon & Schuster, 1992). Este libro es la crónica de la reunión celebrada en Santa
Fe, Nuevo México, por un grupo de científicos de diferen tes disciplinas para analizar problemas reales que sus disciplinas
individuales no pueden resolver .(el mercado bursátil en Economía, la formación inicial de la visa en Biología, por qué las
personas hacen lo que hacen en Sociología, etc.). Combinando la Física, la Economía, la Química, las Matemáticas, la
Infonnática, la Sociología, y otras ciencias, se está desarrollando un enfoque multidisciplinar para tratar de abordar estos
problemas. Pero lo más importante es que está emergiendo una forma diferente de pensar acerca de estos problemas ultra-
complejos: una fonna que se aparta del detenninismo matemático y de la ilusión de que se puede escribir una ecuación que
prediga todos los comportamientos, adoptando en su lugar una aptitud que trata primero de observar y de buscar un patrón,
y luego de emular ese patrón utilizando cualquier medio posible (en el libro se narra, por ejemplo, la aparición de los algo-
ritmos genéticos). Este tipo de pensamiento, en mi opinión, resulta útil para ver formas de gestionar proyectos de software
cada vez más complejos.
"962 Piensa en Java
python
Leaming PytllOn, 2" Edición, por Mark Lutz y David Ascher (O'Reilly, 2003). Una buena introducción a mi lenguaje favo-
rito ·que constituye un excelente complemento a Java. El libro incluye una introducción a Jython, que permite combinar Java
y Python en un único programa (el intérprete Jython compila los programas para generar código intermedio Java puro, por
lo que no necesitamos añadir nada especial para conseguir esa combinación). Esta unión de lenguajes parece tener .grandes
posibilidades.
especialización ' 155, 18,362 terminación y reanudación' 281 valores fmal en b1anco 158 589
especificación de la excepción' 286, 309 Throwable . 287 Y eficiencia· 161
especificación explícita del tipo de argu- tratamiento de . 277 Y private . 159
mento' 247, 405 try, bloque' 280, 297 Y static . 156
especificadores de acceso' 5, 121, 128 Y concurrencia ' 759 fmalize( ) . 97, 305
espera activa, concurrencia' 784 Y constructores' 302 fmally' 148, 150, 295
estándares de codificación xxxi Y herencia' 30 1, 307 error ' 300
estático (static) Y la consola· 312 no ejecutar con hebras demonio ' 743
bloque' 108 Exchanger' 820 palabra clave' 295
comprobación de tipos' 392 exclusión mutua (mutex), concurrencia Y constructores . 303
import y enUID . 660 756 yretum· 299
inicialización de datos . 106 Executor, concurrencia· 734 FixedThreadPooI . 734
inicializador . 371 ExecutorService . 734 Flex 932
synchroruzed . 757 explorador de clases· 134 flip(), nio . 617
Y comprobación de tipos dinámicos 528 expresiones regulares· 331 floa!: valor literal (F) . 53
y fmal' 156 extends'13I , 142, 185 FlowLayout . 867
estilo de codificación' 39 e interfaces ' 202 fluj o de dato de E/S' 596
de creación de clases' l33 palabra clave· 142 flujo de datos canalizado' 609
estrategia, patrón de disefio basado en- Y @interfaces . 700 for, palabra clave' 73
196,203,474, 494,504,588,593, extensión con ceros' 55 foreach ' 74, 78, 114, lIS, 127,232,243,
676,811 extensión de signo ' 55 262,267,345,401 , 402, 446,659,
estructural, tipo' 464, 472 Externalizable . 643 677
es-un, relación' 9, 153 una alternativa a . 647 e Iterable, 269
etiqueta (label) . 78 Extreme Prograrnming (XP) . 960 Y método basado en adaptadores' 270
EventSetDescriptors . 922 fonna (shape): ejemplo' 7, 169
excepciones: F YRTTI · 35 1
capturar una excepción' 286 factor de carga de HashMap o HashSet fonnat( ) . 324
comprobadas' 286, 309 571 formato:
condiciónexcepcional . 278 Factorías registradas, variante del patrón anchura' 326
constructores' 303 de diseño método de factoría' 371 de cadenas de caracteres · 324
conversión de comprobadas a no com- fallo rápido, contenedores de . 578 especificadores de . 326
probadas' 313 false' 51 precisión' 326
creación de nuestras propias· 281 FeatureDescriptor . 93 1 Fonnatler . 325
detección de lIDa excepción· 279 Fibonacci . 401 forName( ) . 354, 872
directrices de uso· 315 Field, para reflexión' 375 FORTRAN, lenguaj e de programación·
encadenamiento de . 291, 313 FIFO (first-in, first out) . 263 54
Error, clase· 294 File, clase 587 Fowler, Martin' 121 ,960
especificación de . 286, 310 FileChannel . 6 16 función hash perfecta· 551
Exception, clase· 294 FileDescriptor . 597 Función, objetos de . 474
F ileNotFoundException . 304 FilelnputReader . 603 Future . 737
fillInStackTrace( ) . 289 FileInputStream . 597
finally . 295 G
FileLock . 632
generar' 278 FilenameFilter . 587 generador ' 399, 406, 494, 515, 666, 681
genéricos· 457 FileNotFounrlException . 304 de propósito general . 407
informe de excepciones mediante log- FileOutputStream . 598 para rellenar un objeto Collection . 406
ger . 284 FileReader' 304, 601 generalización' 11 , 154, 165 Y RTTI .
jerarquías de clases y . 307 FileWriter' 60 1, 605 352 Y clases internas· 216
localización de . 307 fillInStackTrace( ) . 289 generar una excepción · 278
no comprobadas . 294 FilterInputStream . 597 Generator 412, 446, 471,494, 505 Véase
NullPointerException . 294 FiltetÜutputStream . 598 Generator
perdida· 300 FilterReader . 602 genérico curiosamente recurrente· 451
printStackTrace( ) . 289 FilterWriter . 602 @Unitcon' 716
problemas de diseño' 304 final, 192,396 genéricos:
regeneración de una excepción' 289 argumentos' 158, borrado' 415, 447
región protegida' 279 clases' 161 clases internas anónimas' 412
registro' 283 con referencias a objeto' 156 comodines· 434
restricciones· 30 1 datos' 156 comodines de supertipo . 438
RuntimeException . 294 métodos' 159, 168, 182 comodines no limitados ' 440
rutina de tratamiento de . 278, 280 palabra clave ' 156 contravarianza . 438
índice 969
respuesta a un suceso Swing . 862 en el tratamiento de excepciones' 280 Unified Modeling Language (UML) . 4,
sistema dirigido pOI sucesos' 231 ternario, operador' 58 960
Y escuchas· 869 this, palabra clave' 93 unmodifiableList(), Collections . 530
sugerencias' 880 ThreadFactory pen;onalizada . 741 UnsupportedOperationException . 530
swna' 46 tbrow, palabra clave' 280 upcasting. Véase generalizacíón
super: . Throwable, clase base para Exception 287 userNodeForPackage(), API preferences .
palabra clave' 143 tiene-un, relación' 6, 153 657
Y clases internas' 236 TimeUnit . 738, 811 Utilidades de java.uti1.Collec,tions . 572
superclase· 143 tipos:
limites· 361 base· 7 v
supertipo. comodines de . 438 comprobación de . 309, 392 values() para enumeraciones· 659, 663
suplantación en la POO . 2 derivado ' 7 Varga, Ervin . 7, 780
suspende ) e interbloqueos · 775 enumerados 117 variable:
sustitución 9: estructurales· 464, 472 defmición . 73
de métodos private . 173 genéricos y contenedores seguros res- inicialización de variables de métodos'
Y clases internas' 236 pecto al tipo· 242 102
Y sobrecarga' 151 hallar el tipo exacto de referencia base' listas de argumentos variables' 113
sustitución: 352 local · 29
herencia yex.tension . 184 inferencia del argumento de . 403 Vector· 566, 581
principio de . 9 latentes· 464,472 vector de cambio' 232
pura ·9, 185 marcador de, en genéricos' 424 Venners, Bill . 98
SWF, formato de código intermedio Flash matrices y comprobación de . 483 versionado, serialización . 649
·932 parametrizados . 393 Visitante, patrón de diseño' 706
Swing·857 pato· 464, 472 Visual BASIC, Microsoft· 918
componentes' 876 primitivos' 25 volatile . 754, 760, 763
HTML en los componentes' 902 seguridad de tipos en Java ' 60
modelo de sucesos' 869 seguridad dinámica de . 456 w
Y concurrencia' 910 tipo de datos equivalente a clase' 3 wait() . 784
switch: tipos de datos primitivos y uso con ope- Waldrop, M. Mitchell ·961
palabra clave· 81 radares' 62 WeakHashMap . 542, 580
Y enuro · 662 toArray() . 571 WeakReference . 578
synchronized: TooManyListenersException . 926 Web Start, Java· 906
contenedores' 577 toString() . 140 West, BorderLayout . 866
decidír qué métodos sincronizar' 929 directrices para usar StringBuilder' 319 while . 72
estático' 757 transferFrom() . 618 windowC losing( ) . 899
Regla de Brian de la sincronización 757 transferTo()·618 write( ) . 596
sección critica y bloque· 765 transieot, palabra clave' .646 nio·618
wait( ) y notifYAlI( ) . 784 TreeMap . 542, 544, 571 writeBytes( ) 607
>