Preview only show first 10 pages with watermark. For full document please download

Piensa En Java 4 Edicion

PIENSA EN JAVA. PIENSA EN JAVA Cuarta Edición BRUCEECKEL President, MindView, ¡ne. Traducción Vuelapluma · PEARSON , Prentice . , Hall Madrid e México e Santa Fe de Bogotá e Buenos Aires e Caracas e Lima Montevideo e San Juan e San José e Santiago e Sao Paulo e White Plains e / Datos de ...

   EMBED

  • Rating

  • Date

    September 2016
  • Size

    210.6MB
  • Views

    10,368
  • Categories


Share

Transcript

PIENSA EN JAVA PIENSA EN JAVA Cuarta Edición BRUCEECKEL President, MindView, ¡ne. Traducción Vuelapluma · PEARSON , Prentice . , Hall Madrid e México e Santa Fe de Bogotá e Buenos Aires e Caracas e Lima Montevideo e San Juan e San José e Santiago e Sao Paulo e White Plains e / Datos de catalogación bibliográfica PIENSA EN JAVA Bruce EckeJ PEARSON EDUCACIÓN S.A. , Madrid, 2007 ISBN: 978-84-8966-034-2 Materia: Informática, 004 Fonnato: 215 x 270 mm. Páginas: 1004 Todos los derechos reservados. Queda prohibida, salvo excepción prevista en la Ley, cualquier forma de reproducción, distribución, comunicación pública y transfonnación de esta obra sin contar con autorización de los ti tulares de propiedad intelectual. La infracción de los derechos mencionados puede ser constitutiva de delito contra la propiedad intelectual (arls. 270 y sgls. Código Penal). 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 Deposito Legal: M-4.753-2007 PRENTICE HALL es un sello editorial autori zado de PEARSON EDUCACIÓN S.A. 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 Diseño de Cubierta: Equipo de diseño de Pearson Ed ucación S.A. Impreso por: Gráficas Rógar, S. A. IMPRESO EN ESPAÑA - PRINTED IN SPAIN Este libro ha sido impreso con papel y tintas ecológicos Dedicatoria A Dawn Resumen del contenido Prefacio ...................... . ..•... . ....•. . .... . ........ . . . .... xix Introducción .... . .............. . ... . ...... . ..... . . ....... . . ... . . . xxv 1 Introducción a los objetos . .. . . .. . ....... . ......... . . .... . . .. .. . ..... 1 2 Todo es un objeto ..... . ... . ..... . ...... . ................... . ...... 23 3 Operadores ................ . ...... . .......... . ............ . ...... 43 4 Control de ejecución ......... . . ... . ........ . ... . .. . . . .... . ......... 71 5 Inicialización y limpieza ..... . .......... .. ...................... . ... 85 6 Control de acceso .... . ....... . ............... . ................ . .. 121 7 Reutilización de clases ......... . ............ . ......... . . . ...... . . . 139 8 Polimorfismo .. .... . ... . . .. . .. .. .... . . .. ....... ... ...•....... . .. 165 9 Interfaces ... . ......................................... . . ....... 189 10 Clases internas .............. . ......... . . . ...... . ......... . . . .... 211 11 Almacenamiento de objetos .... . ..... .. ........ .. . . .. ............. 241 12 Tratamiento de errores mediante excepciones .. . ............ . .. . ..... 277 13 Cadenas de caracteres ........ . ...... . .. . . . . . ..... . ............... 317 14 Información de tipos . ........ . . . .. . ..... .. ......... . ............. . 351 15 Genéricos . . .... . .................. . . . ..................... . ..... 393 16 Matrices . ................... .. . . . . .... .. ...... . . . . . . .. .. . ....... 483 17 Análisis detallado de los contenedores .... .. ........................ 513 18 E/S ..... .. . . ............. . ........ . .. . ............. . . .... .. .. . .. 587 19 Tipos enumerados .... . .... .. . . . . . .. . . . .. ......... . ...... .. ...... 659 20 Anotaciones . ....... . ....... . .... . .. .. .... ... .. .. ......... .. . . ... 693 21 Concurrencia ......... . ..... . . . . .. . .... . ............... .. . .. ..... 727 22 Interfaces gráficas de usuario ... . ...... . .. . . . ........ . . ...... . .. . .. 857 A Suplementos . ........... . . . .................... . ......... . ...... 955 B Recursos ......... . . . .. . .. . ......... . . . ...... . ........ . .. .. ..... 959 índice .. .. . . ........ . ............ . .... . .......... . ....... .. ..... 963 Contenido Prefacio ... .. ..... . .. . ........ .. ... .. . . xix Java SES Y SE6 ........ . . .. .. . .... ,. xx 16 Java e Internet. ¿Qué es la Web? . 16 . . . .... .. ..... . xx Programación del lado del clicntc ............. 18 La cuarta edición .. .. .. . . .. . .. ..... ... .... xx Programación del lado del servidor . . . . . . . . .. .. 21 Java SE6. ... . xxi Cambios Sobre el diseño de la cubierta .......... . .... xxii Agradecimientos. . . . . . . . . . . .......... XXll Introducción . ..... .. .. .... . ........... . xxv Prerrequisitos ... Aprendiendo Java ..... .. . .... xxv . , •• . •. . . . XXVI Objetivos ................ . . . . . . . XXVI Enseñar con este libro ........... . ........ Documentación del JDK en HTML . XXVll . . XXVI1 Ejercicios ............................. XXVll Fundamentos para Java . .. ........... .... . XXVIl Código fuente ........ . ..... . . . .. . ..... Estándares de codificación. XXVIII Resumen........................ . ... 22 2 Todo es un objeto ....... . ...... . ........ 23 Los objetos se manipulan mediante referencias .. 23 Es necesario crear todos los objetos. . . 24 Los lugares de almacenamiento ... ..... ... . . . 24 Caso especial: tipos primitivos. . . . . . . . . . . . . ..25 Matrices en Java. . . . . . . . ... . .. .. . .. 26 Nunca es necesario destruir un objeto ......... 27 Ámbito. .. .. . . . . . . . . . .. Ámbito de los objetos .. . .. . .... . .. 27 ............. . .. 27 Creación de nuevos tipos de datos: class ..... '.. 28 Campos y métodos . . . .... . ... . ... ... 28 . .. xxix Métodos, argumentos y valores de retomo ...... 29 Errores .. ............................... xxx La lista de argumentos ....................... 30 1 Introducción a los objetos . ... ...... . ... . . . 1 El progreso de la abstracción ............ •. ... 1 Construcción de un programa Java .... 3 1 Visibilidad de los nombres. . .... 31 Todo objeto tiene una interfaz ..... . .. ........ 3 Utilización de otros componentes. Un objeto proporciona servicios ...... . ........ 4 La palabra clave static . ... . .. . 31 ......... . . .. 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 . .. . ...... 6 Comentarios y documentación embebida ....... 35 Relaciones es-un y es-como-un ....... ... . . .... . 8 Documentación mcdiantc comcntarios .......... 36 Herencia. . . . . . . . . . . . . . . . . . . . . Objetos intercambiables con polimorfismo ...... 9 Sintaxis. La jerarquía de raíz única. . HTML embebido . ... . ... . .. . . .... ...... 11 Contenedores ... .......................... 12 Tipos paramctrizados (genéricos) . . ... 13 . . 36 .... 37 Algunos marcadores de ejemplo ... . ... . . . Ejemplo de documentación. .37 . . 39 .. . . . . ... 39 Creación y vida de los objetos ... . ... ........ 13 Estilo de codificación. Tratamiento de excepciones: manejo de errores .. 15 Resumen .............. . ..... ... .. ... .. .. 40 Programación concurrente .................. 15 Ejercicios ...... .. .. . . . ......• .. .......... 40 x Piensa en Java 3 Operadores ... ... .... ... . .. .... . . . .... . 43 Instrucciones simples de impresión .....•.. ... 43 Utilización de los operadores Java . 44 ..... .. .. ... . . 89 Sobrecarga con primitivas. Sobrecarga de los valores de retomo. .. .. 92 Constructores predetenninados . . .. 92 Precedencia ..... .......... . ...... . .. . .... 44 La palabra clave tbis ....... ........ .... ... 93 Asignación ..................... ... .. ... . 44 Invocación de constructores desde otros constructores .... . . ... ... . .. .. . ... .. ... . 95 Creación dc alias en las llamadas a métodos ...... 46 Operadores matemáticos. . . . . .......... 46 Operadores unarios más y menos .............. 48 Autoincremento y autodecremento . . . 48 Operadores relacionales . . . . . . . . . . . . . . . 49 Comprobac ión de la equivalencia de obj etos ..... 49 Operadores lógicos ........................ 51 Cortocircuitos . . . . . . . . . . . . . . . . . . . . . . Literales. . . . . . . . . ... . 52 . ........ . . . ....•...... 53 Notación exponencial .. . . . ........ . . ........ 54 Operadores bit a bit. ............. . •...•. ... 55 Operadores de desplazamiento. . 55 Operador temario if-clsc . 58 Operadores + y += para String ............... 59 Errores comunes a la hora de utilizar operadores. 60 Operadores de proyección ... 60 Truncamiento y redondeo ..... 61 Promoción ..... . . .... . . . . . . . . . . . ... . . .. . . . 62 Java no tiene operador "sizeof' . . . . . ... 62 Compedio de operadores . ....... .. .... . .... 62 Resumen .... ......... ....• . . . . . ... . ... .. 70 4 Control de ejecución .. . .................. 71 troe y false ........... . ..... . . ..... •.. ... 7 1 if-else. . .. ..... . . • . • . . ... . . . . .•. •..... 71 El significado de sta tic . . ... ............... . . 96 Limpieza: finalización y depuración de memoria .............. . ...... 97 ¿Para qué se utiliza finalizeO? .. . 97 Es necesario efectuar las tareas de limpieza ... 98 La condición de terminación . . .... . ... . ... .. .. 98 ... 100 Cómo funciona un depurador de memoria Inicialización de miembros .. .. 102 Especificación de la ini cial ización ... 103 Tnicialización mediante constmctores .... 104 Orden de inicialización ..... 105 Inicialización de datos estáticos . .. . 106 Ini cial ización static explícita ................ 108 Iniciali zación de instancias no estáticas Inicialización de matrices ..... . ..... 109 .... 110 . 113 Listas variables de argumentos Tipos enwnerados ..... . ..... ..... . . . . .. .. 11 7 Resnmen ............ .... . . . • . ... .. ..... 119 6 Control de acceso . .. . . . .. . . .... . . .. . .. . 121 package: la unidad de biblioteca ....... . ... . 122 Organización del código . ............. . . 123 Creación de nombres de paquete unívocos ...... 124 Una biblioteca personalizada de herramientas .. 126 .. .... .. . .. .. ........... 72 Uti lización de importaciones para modificar el comportamiento . . ... . ... .. .. . .... . . 128 do-while ... . ... ...... .. . . . . . . . . . ... . . ... . 73 Un consejo sobre los nombres de paquete .. ... . . 128 Iteración for ... .. .. .. ...... ...... .. 73 El operador coma Sintaxis[oreach ...... . ......... . ... 74 . ..... ...... 74 returo ...... ...... . • .. ... .... .. . . . . . .... 76 break y continue . .. .............. . .... ... 77 Especificadores de acceso Java .... 128 .. . . 129 Acceso de paquete public: acceso de interfaz .. .. .. .. . .......... 129 private: ino lo toque! protected: acceso de herencia . . . . . . •. . 130 .. 131 La despreciada instrucción "goto" .... •.. ..... 78 Interfaz e implementación ................. 133 switch Acceso de clase ... ..... ....... .............. 81 Resumen ... ....... ........ . . • ... . ....... 83 Res umen . .......... .. ........... 134 ..... 136 5 Inicialización y limpieza . . .. . ... .. ... . . . . . 85 7 Reutilización de clases .............. .. .. 139 Inicialización garantizada con el constructor .... 85 Sintaxis de la composición ...... ... .... .... 139 Sobrecarga de métodos ..................... 87 Sintaxis de la herencia ..........•... •.. ... 142 Cómo se distingue entre métodos sobrecargados .. 88 Inicialización de la clase base .. ......... . . .. . 143 Contenido xi Delegación ....... . ....... . .... ... . . 145 Ampliación de la interfaz mediante herencia .. 201 Combinación de la composición y de la herencia .... . ............ 147 Co lisiones de nombres al combinar interfaces ... 202 Cómo garantizar una limpieza apropiada Ocultac ión de nombres .. 148 ........... . .. . 151 Adaptaci ón a una interfaz .. ....... 203 Campos en las interfaces .205 Inicializac ión de campos en las interfaces. .205 Cómo elegir entre la composición y la herencia 152 Anidamiento de interfaces. . . . . . protected ....... . .............. . .... 153 Interfaces y factorías ............. . . . ...... 208 Upcasting (generalización) ..... . . ..... ..... 154 Resumen ........ ........ .. . . ....... .... 210 ¿Por qué "upcasling'? . . .. . . 155 Nueva comparación entre la composición y la herencia. .. . . .. . . . La palabra clave final . . . 155 ............. 156 10 Clases internas .. .. . . .. ......... . . .. ... 211 Creación de clases internas . . .. ... 156 Utilización de .this y .new . Métodos final . . .. ... 159 Clases interna y generalización .. 161 Una advertencia sobre final .. .... ..... 213 .... . ..... 214 Clases internas en los métodos y ámbi tos . 16 1 Inicialización y carga de clases .... Inicialización con herenc ia Resumen. .... ... . ..... 2 11 El enlace con la clase externa .. Datos final .. ....... .... . . Clases final . 206 ... 162 ........ ....... . . 162 . . 163 8 Polimorfismo . ... . . . . . . .. .. . . .... . .. .. 165 Nuevas consideraciones sobre la generalización 165 Por qué olvidar el tipo de un objeto . ..... 166 El secreto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 Un nuevo aná lisi s del método factoría Clase anidadas .............. . Clases dentro de interfaces .. . Acceso al exterior desde una clase múltiplemente anidada .. . ¿Para qué se usan las clases internas? Cierres y retro llamada .. Clases internas y marcos de control . . ..... 222 .224 .225 .... 226 .227 .229 . ... 230 Acoplamiento de las llamadas a métodos ....... 168 Cómo heredar de cIases internas ............ 236 Especi ficación del comportamiento correcto 168 ¿Pueden sustituirse las clases internas? ....... 236 Amp1iabi1idad ....................... . . 171 Clases internas locales .................... 238 Error: "sustitución" de métodos privare ElTor: campos y métodos static Constructores y polimorfismo Orden de las llamadas a los constructores. . . 173 Identificadores de una clase interna .. 239 174 Resumen ......................... . ..... 239 ... 175 11 Almacenamiento de objetos .. ... ... . ..... 241 176 ...... 177 Genéricos y contenedores seguros respecto al tipo ................. .... .. 242 Comportamiento de los métodos polimórficos dentro de los constructores . . .. ... . 181 Conceptos básicos ....... .... . .. .... ...... 244 Herencia y limpieza Tipos de retorno covariantes Diseño de sistemas con herencia .. . . . ..... 183 .... ... ... . 183 Sustitución y extensión .... . .. .. ....... . .... 184 Especialización e infonnación de tipos en tiempo de ejecución .. Resumen ......... . Adición de grupos de e lementos Impresión de contenedores ................ 247 List ...................... . ............ 248 Iterator. . . . . . . . . . . . . . . . . . .186 ......... 187 9 Interfaces ... . ........ ... ..... .. .. . ... 189 .245 Listl te.-ator . . 252 . .. . 254 LinkedList ...... .. .. ... . . .. . . ... .. .. .. . 255 Stack. . . . . . . . . . . . . . . . . . Set. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... 256 . .258 Clases abstractas y métodos abstractos ....... 189 Map ...... . .... . . . . . . . . ... . . .... .... ... 260 Interfaces. . . . . . . . . . .. . . . . . . .. . . . . Queue.... ....... ........ . .. 192 Desacoplamiento completo .... . . . .. . . . .. . . 195 "Herencia múltiple" en Java ......... . ...... 199 PriorityQucue. . . . . . . . . . . . . . ... 263 . 264 Comparación entre CoHection e Iterator ..... 266 xii Piensa en Java La estructura foreach y los iteradores ... 268 El método basado en adaptadores. . . . . Resumen. . . . ........ . .. 270 . ...... . 273 12 Tratamiento de errores mediante excepciones . ...• ........... .. 277 Conceptos ....................... . • . .... 277 . ........ 278 Excepciones básicas ...... . Argumentos de las excepciones. . . 279 Detección de una excepción ................ 279 El bloque try . . . . .. 280 Rutinas de tratamiento de excepciones ......... 280 Creación de nuestras propias excepciones Excepciones y registro. . . . . . . . . . . . . . . . .. 281 . .. 283 La especificación de la excepción .... . .. .... 286 Cómo capturar una excepción .. . . . ..... 286 .. 288 La traza de la pila Regeneración de una excepción ....... . .. .... 289 Encadenamiento de excepciones ... .... . ..... 29 1 Excepciones estándar de Java .............. 294 Caso especial: RuntimeException .. . . . . . ..... 294 Realización de tareas de limpieza con finally .. 295 ¿Para qué sirve finally? . ........... Utilización de finaU)' durante la ej ecución de la instrucción return. Un error: la excepción perdida . .. 296 Restricciones de las excepciones ............ 301 ..303 Localización de excepciones ........•.•.... 307 Enfoques alternativos. . .........•.... 308 .309 Historia .. Perspectivas ... . ......... . .3 10 Paso de las excepciones a la consola ... 312 Conversión de las excepciones comprobadas ...... 3 13 en no comprobadas Directrices relativas a las excepciones . 3 15 Resumen ............................... 315 13 Cadenas de caracteres ... ............ ... 317 Cadenas de caracteres inmutables . . . . . . . . . 317 Comparación entre la sobrecarga de '+' y StringBuilder . . . . . . . . . . . . . . . . Operaciones con cadenas de caracteres ....... 322 printfO· . ... 324 . . . . . ... .. . ..... 324 ..325 . ........ 326 Especificadores de formato .. Conversiones posibles con Formattcr . . 327 String.format(). . 329 Expresiones regulares ......... ........ • ..... 331 Fundamentos básicos. . .......... . 33 1 Creación de expresiones regulares ... .... ...... 333 · . 335 Cuantificadores . ... . 336 Pattcrn y Matcher split(). · . 342 · . 343 Operaciones de sustitución . .. 344 reset( ) . Expresiones regulares y E/S en Java Análisis de la entrada . .... 345 . ............. 346 .348 Delimitadores de Scanner Análisis con expresiones regulares. . ..... 348 StringTokenizer. ............. . .... 349 Resumen. . ..... .. . . . ....... . . .. .... 350 14 Información de tipos . ........... • ... . ... 351 La necesidad de RTTI. ........ . . . ... 35 1 .353 El objeto Class .... .357 Referencias de clase genéricas .. 359 Nueva sintaxis de proyección ................ 36 1 Comprobación antes de una proyección. .361 Utilización de literales de clase · . 367 ..... 368 Instanceof dinámico .. Recuento recurs ivo. . . . . . . . . . . . . . . . . . Factorías registradas . . . . . 369 . ..... ........ 37 1 instanceof y equivalencia de clases .......... 373 Reflexión: infonnación de clases en tiempo de ejecución ............................ 374 Un extractor de métodos de clases . 375 Proxies dinámicos. . . . . . . . 378 Objetos nulos. . . . . . . . . . . . . . . . . . . . . . . . .. 38 1 .387 Objetos maqueta y stubs .. .. 387 Interfaces e información de tipos . .. .... 392 Resumen. . 318 Recursión no intencionada .... ........ ..... 32 1 Formateo de la salida ....... . . . . . . .... La clase Formatter ..... Literales de clase ........ . .. 299 .... 300 Constructores ................... .•. . System.out.format( ) .... .. . . .... ... . . ... . . 324 15 Genéricos ..... .. ..................... 393 Co mparación con c++ .................... 394 Genéricos simples ...... .... . . . .• .•.•.. Una biblioteca de tuplas .394 .396 Contenido xiii ..... 398 Una clase que implementa una piJa .. 399 RandornList ... lnterfaces genéricas Métodos genéricos .. ... ........... 403 Compensación de la carencia de tipos latentes .. 467 Reflexión ............ . 467 ...... 404 Aplicación de un método a una secuencia .. . .. 468 . .... 405 ¿Qué pasa cuando no disponemos de la intcrfaz correcta? . . . . . . . . . . . . 471 Varargs y métodos genéricos. Un método genérico para utilizar con generadores . ............. 406 Un gene rador de propósito general . . .... 407 Simplificación del uso de las tuplas ..... 408 .... ... ............. 409 Clases internas anónimas . . . . ......... 412 Construcción de modelos complejos ......... 4 13 ............. 4 15 El misterio del borrado . .. ............ 417 Compatibi lidad de la migración. .419 El problema del borrado de tipos . .... . . . . . .4 19 .... 42 1 El efecto de los límites Simulación de tipos latentes mediante adaptadores472 Utilizando los objetos de función como estrategias .................. 474 Resumen: ¿realmente es tan malo el ..479 mecanismo de proyección? ..... . Lecturas adicionales .. .......... . . 481 16 Matrices ...... .. .. .................. . . 483 Por qué las matrices son especiales . . 483 Las matrices son objetos de primera clase .... 484 Devolución de una matriz ... . .... 487 ~l .. 488 Matrices multidimensionales .......... .... •.... 491 Matrices y genéricos Creación de datos de prueba .......... 493 Array,.fillO . . . . . . . . . . . . . . . ... 493 Generadores de datos ............. 494 ........... .. ..... ..... 434 Creación de matrices a partir de generadores . ... 498 Compensación del borrado de tipos .... 424 ..425 Creación dc instanc ias de tipos. Matrices de genéricos L~i~s ... Comodines . .462 Tipos latentes . .......................... 464 de tipo La técnica usada en C++ . . .... 461 .......... 399 Aprovechamiento de la inferencia del argumento Una utilidad Set Utilizac ión del patrón Decorador .. Mixins con proxies dinámicos. ............... . .. 42 7 . ...................... ¿Hasta qué punto es inteligente el compilador? .. 436 Contravarianza .... ............... 438 .. 440 Comodines no limitados Conversión de captura. Problemas ..... .. 507 Búsquedas en una matriz ordenada .. 508 ..... 447 Sobrecarga. . .. 449 Secuestro de una interfaz por parte de la clase base Autolimitación . . .. ... 503 Ordenación de una matriz Implementación de interfaces parametrizadas . ... 447 Covarianza de argumentos. Comparación de matrices . ....... . Comparaciones de elementos de matriz . .. 445 Resumen .................... . ..... 450 ... 504 ....... 509 17 Análisis detallado de los contenedores . ... 513 Taxonomía completa de los contenedores ..... 513 Rclleno de contenedores. . 449 .. ......... 450 Gcnéricos curiosamente recurrentes .. . ... 502 . ... 445 de tipo. . . Tipos autolimitados ........ . . . .. 502 Copia en una matriz . .... 444 No pueden usarse primitivas corno parámetros Proyecciones de tipos y advertencias Utilidades para matrices . . 514 Una solución basada en generador .... ... ..... 515 . . 5 16 Generadores de mapas . ... 519 Utilización de clases abstractas .... 451 Funcionalidad de las colecciones .. .. . . ..... 525 . .. 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 Mixins en C++. ............. 459 Mezclado de clases utilizando interfaces.. . .... 460 SortedSet. a~nacenam iento. ......... . . . .. . 533 . 536 Colas ... ............• . •.•.... . . . . •.... 537 xiv Piensa en Java . . . .. . . . . · .... 538 Colas dobles. ....... . .. .. ........ Colas con prioridad. . . 539 Mapas ...... .. ..... . ..... . . . ........... 540 Rendimiento . 541 SortedMap . . .544 LinkedHashMap. . .... 545 Almacenamiento y códigos hash ..... . • . .. . . 545 Funcionamiento de hashCode( ) .. 548 Orígenes y destinos de los datos . . . 601 Modificación del comportamiento de los flujos dc datos. ........... . ... 601 Clases no modificadas .... 602 RandomAccessFile . . . .. . ............... 602 Utilización típica de los flujos de E/S Archivo de entrada con buffer . Entrada desde memoria. . ... 603 . .. .. .... 603 . .......... .. .... 604 Entrada de memoria formateada .... .. .. . ..... 604 Mejora de la velocidad con el almacenamiento hash . . . . . . . . ........ . .. 550 Salida básica a archivo ..... . .... . .... . .... 605 Sustituc ión de hashCodeQ ......... . .... . 553 Almacenamiento y recuperación de datos ...... 607 .... 558 Lectura y escritura de archivos de acceso aleatorio .. Selección de una implementación Un marco de trabajo para pruebas de rendimiento 558 Selección entre listas ....................... 561 Peligros asociados a las micropruebas de rendimiento .... ...... . . . .566 Lectura de la entrada estándar ............... 613 ....................... 572 Cambio de System.out a un objeto PrintWriter . .. 613 .. 575 Almacenamiento de referencias. . . . . . . . .. . . . . .. . . 578 . . . . 580 Contenedores Java 1.0/ 1.1 ...... .. . ........ 581 Vector y Enumeration . . . . . . . . . . . . . . BitSet. Resumen. . . . . .... 58 1 . . .. ..... . ............. . ... 582 . . .. ... . .. . . . ... 582 . . . . . . . .. . . . . . . . . . . . . ..584 . ........ . . . .. . ..... 585 18 Entrada/salida . ....... . ................ 587 La clase File ...... .. ...... . Una utilidad para listados de directorio. Utilidades de directorio .......... . Búsqueda y creación de directorios Redireccionamiento de la E/S estándar Control de procesos · .... 576 Sincronización de una colección o un mapa .... 577 Stack . . . . . . ..... 612 E/S estándar ..................... . ...... 6 12 Creación de colecc iones o mapas no modificables Hashtable . . Lectura de archivos binarios . . . ... 567 Ordenaciones y búsquedas en las listas WeakHashMap. . ....... .. .... 609 Utilidades de lectura y escritura de archivos . . . 609 .. ... 569 Selección de un tipo de conjunto Selección de un tipo de mapa Utilidades Flujos de datos canalizados. .. 608 · ... 587 . ... 587 . ... 590 ..... 594 ... 613 ............ . . 614 Los paquetes new ......• ...... .... ...... 616 Conversión de datos . 618 Extracción de primitivas .. ..... . . ........ 62 1 Buffers utilizados como vistas ... 622 Manipulación de datos con buffers .... . • . . .. 625 Deta lles acerca de los buffers ... 625 Archivos mapeados en memoria. . .. 629 Bloqueo de archivos. . . 632 Compresión .................... . . . •. . ... 634 Compresión simp le con GZIP .. . . 635 Almacenamiento de múltiples archivos con Zip . . 635 Archivos Java (lAR) . . Serialización de objetos Localización de la clase Control en la serialización .. . ................ 637 .. . . .... ... 639 ..... 642 .642 ...................... 596 Utilización de la persistencia ...... ..... . . . . .. 649 Tipos de InputStream ..... .. ... . . . . . . , .... . 597 XML .............................. .. .. 654 Tipos de OutputStream ..................... 598 Preferencias .... . . . .......... . . .. . ....... 656 Entrada y salida Adición de atributos e interfaces útil es Lectura de un flujo InputStream con FiltcrInputStream . . 598 Resumen . . ......... • .•.• .............. . 658 19 Tipos enumerados .......... . .... . ..... 659 . . 599 Escritura de un flujo OutputStream con FilterOutputStream ............. . ...... 599 Lectores y escritores ....... . .. , . . ........ 600 Características básicas de las enumeraciones .. 659 Utilización de importaciones estáticas con las enumeraciones ..... .. . ... ... 660 Contenido xv Adición de métodos a lma enumeración ...... 661 Sustitución de los métodos de una enumeración . 662 Mejora del diseño del código .. ..... ,.. Conceptos básicos sobre hebras .. , , . Enumeraciones en las instrucciones switch ... 662 Definición de las tarcas . El misterio de values( ) La clase Thread ....... Implementa, no hereda ..... o • o o o . .. 663 o o ••••• o Selección aleatoria ... . ......... . , . Utilización de interfaces con propósitos de organización .. ... . . o • •• o o o o 665 ••• 667 • . . 73 0 o ••• o o • • • 671 ........ 672 ylétodos específicos de constante ............ 673 Cadena de responsabilidad en las enumeraciones 676 . 680 Máquinas de estado con enumeraciones. . Despacho múl tiple .. . ................... 684 731 .732 Utilización de Executor .... . 734 Producción de valores de retomo de las tareas . . 736 , ..... .... . . . ... . 737 Prioridad. · . 738 Cesión del control . Uti lización de EnurnMap •• . ....... . .. .... 731 Cómo donnir una tarea . ••••••••••• Utilización de EnurnSet en lugar de ........ indicadores •• .. 666 o . .. 740 Hebras demonio. . , .. 740 Variaciones de código .. 744 Terminología . . .. , 748 Absorción de una hebra ,748 Creación de interfaces de usuario de 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 Utilización de una matriz 2-D .690 Reswnen. .... 691 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 Sintaxis básici:t o o 693 ... 694 Definición de anotaciones. . . .. 694 ........ , Meta-anotaciones ... 695 Escritura de procesadores de anotaciones , .... 696 Elementos de anotación. Restricciones de valor predeterminado. Soluciones alternativas . , . 709 Utilización de @Unit con genéricos .. 716 No hace falta ningún "agrupamiento" .. 717 Implementación de@Unit ,., ...... . . . . . .... 717 El iminación del código de prueba . 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 Clases atómicas . .. . . ... 764 Secciones críticas . .. 770 Almacenamiento local de las hebras ... 77 1 o Interrupción. •• •••• .784 waitO y notifyAIIO ... , ... . o o , .. , 784 • .. ,788 notifyO y notifyAIIO, Productores y consumidores. . .... 791 Productores-consumidores y colas .796 Utilización de canalizaciones para la E/S entre tareas . . . . . . . . . . . . . . . . . . . . . . . . Interbloqueo . . . . . . CountDownLatch .. CyclicBarrier DelayQueue. PriorityBlockingQueue . 772 ... 782 Cooperación en tre tareas Nuevos componentes de biblioteca . . . ....... . 728 0 . ......... , ... .. . 776 724 727 • Comprobación de la existencia de una interrupción . . 723 Las múltiples caras de la concurrencia .. , , . , , . 728 Ejecuc ión más rápida , , 765 Sincron ización sobre otros objetos. .. 775 Uti lización del patrón de diseño Visitante con apt ..... , , , , , , , , .. ........ . ... . . 706 o . . .. . 760 Tcnuinación durantc el bloqueo Uti lización de apt para procesar anotaciones .. 703 21 Concurrencia Atomicidad y volatilidad. .. 772 . 700 . .. , . . 756 El jardín ornamental Implementación del procesador Resumen. . . . . . . . . .... 754 Resolución de la contienda por los recursos compartidos.. ................... Terminación de tareas .... . . 700 Pruebas unitarias basadas en anotaciones Acceso inapropiado a los recursos . . . 697 .. 700 Las anotaciones no soportan la herencia .753 . . 697 ... ...... 697 Generación de archivos externos Compartición de recursos , ..... . , 800 801 o •••• ,805 .. 805 .... .. . 807 .... 809 . • ,.,., . 0 ,., • ... . . 811 xvi Piensa en Java El controlador de invernadero implementado con ScbeduledExecutor . ........... ..... 8 14 Tableros con fichas . . . . . . . . . . . . . . . . Semaphore Menús. . .......... . .. .. .. • ..... 8 17 Exchanger ...... .. . . . . . . .. ..... . . .. ...... 820 Simulación . . . . . . . . . . .. .. . . •.• . • ... .. 82 1 Simulación de un cajero .... 82 1 Simulación de un restaurante. ...826 . .. . 887 Recuadros de mensaje . ... . ............ ..... 888 . .... .. . . . ........ . . . 890 Menús emergentes. 894 Dibujo ..... . ..895 Cuadros de diálogo ... . .. .... .... 898 Cuadros de diálogo de archivos .... 901 . .... ... . . ..... 829 HTML en los componentes Swing .... 902 Optimización del rendimiento .............. 834 Deslizadores y barras de progreso ..... 903 Selección del aspecto y del estilo . 904 Distribución de trabajo . ... Comparación de las tecnologías mutex .. 834 Contenedores libres de bloqueos .. . .... 841 Árboles, tablas y portapapeles . . . . . ... . . .. 906 . . . . . . . .. .847 JNLP Y Java Web Start .................... 906 . ................ 848 Concurrencia y Swing . ....... . . . . . . .. . .... 9 10 Objetos activos ................. . .•.•.... 850 Tareas de larga duración .... . ....... .. ...... 910 Resumen ....................•. . . . ...... 853 Hebras visua les. . . . ............... 916 Programación visual y componentes JavaBean . 9 18 Bloqueo optimista. ReadWriteLocks . . . . Lecturas adicionales . ...... . . . .. . ..... 855 22 Interfaces gráficasde usuario ... .. . . . . ... 857 ¿Qué es un componenle JavaBean? . .... . .. .. . 919 Applets . . ........ .................. ..... 858 Extracción de la información Beanlnfo con lntrospector ... Fundamentos de Swing ..... ... . .......... 859 Una Bean más sofisticada . ... ...920 .. ... .. 924 Sincronización en JavaBeans . . . . 927 Definición de un botón ...........•. •..... 862 Empaquetado de una Bean . ......... . . ... 930 Captura de un suceso ............... . ..... 862 Áreas de texto ....... .... .....•. . .•..... 864 Más información sobre componentes Bean. . Un entorno de visuali zación . .. .. ....... 86 1 Soporte avanzado de componentes Bean . .... . . 931 .932 Control de la disposición ........•......... 865 Alternativas a Swing . ................. .... 932 BorderLayout . ............. .. ............ 866 Construcción de clientes web Flash con Flex .. 932 "lowLayout. .. .. . .. .. . .. . . . .. .. .. .. .. . . 867 G ridLayout . . .867 Grid.BagLayout. .. .. . .... .. .... ..... 868 Helio, Flex ............................... 932 Compilación de MXML. . . . Posicionamiento absoluto ........ .. .... ... 868 Contenedores y controles. . . BoxLayout. ........ . . ... .. .• . . . . ..... 868 Efectos y estil os ¿Cuál es la mejor solución? . ....... ... ..... 868 El modelo de sucesos de Swing ........... 868 Tipos de sucesos y de escuchas ... 869 Control de múltiples sucesos ..... .874 Una selección de componentes Swing ....... 876 Botones. . .... 876 . ..... 933 MXML y ActionScript . .................... . 934 . . ..... . 935 .936 Sucesos . . . . ...... ... . 93 7 Conexión con Java. ........... 93 7 Modelos de datos y acoplamiento de datos .. . . . . 939 Construcción e implantac ión de aplicaciones . ... 940 Creación de aplicaciones SWT ........ Instalación de SWT . . . . . . . . . . . . . . . . . . . . .. .. .... . .941 . 94 1 .. .. 94 1 Iconos . ........ . . . . . . . . . .. . . .. . . . . . .878 Helio, SWT Sugerencias .880 Eliminación del código redundante. . . . Campos de tex to .880 Menús . ................. ....... ....... ... 945 Bordes .88 1 Paneles con fichas, botones y sucesos events . ... 946 .... 882 Gráficos . ............ . Casill as de verificación ... 883 Concurrencia en SWT ... Botones de opción. . . . . ...... 884 Un mini-editor. Cuadros combi nados (listas desplegables) ...... 885 Cuadros de lista. . ..... 886 . 944 . ........ .. 949 .. 950 ¿S WT o Swing? . . . . . . .. . . .. . . . . ......... 952 Resumen ......... . . ... . . . . . . . . . ... . .... 952 Recursos .... . .. ... . . . . .. ... .. . . ... . . ... . 953 Contenido xvii A Suplementos ..................... . .... 955 B Recursos ...... .. .......... . .... .... .. 959 Suplementos descargables ......... . .... 955 Software ............. .... ... ... . ... 959 Thinking in C: fundamentos para Java .. . . . . 955 Editores y entornos !DE ..... . ... ... . ... 959 Libros ........... . ... 959 Análisis y diseño . .. .. ... 960 Python . ........ . .. .. 962 Seminario ThinJ..ing in Java . . . . . . . . . . . . . . 955 Seminario CD Hallds-On Java .... 956 Seminario Thinking in Objects .....•... .... 956 Thinking in Entelprise Java . ....... . . .. ... . 956 Thinking in Patlerns (con Java) ............. 957 Seminario Thinking in Patterns ... 957 Consultoría y revisión de diseño .... 957 Mi propia lista de libros ........ . ... 962 Indice ............ .. .. ... . . . .... . .... . 963 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 fundamental 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) estaba 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ódigo 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ódigo 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 programació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 oportunidades que Java nos proporciona son inmensas, ya que puede incrementar significativamente nuestra productividad como programadores. Java incrementa el ancho de banda de comunicación entre las personas en todos los sentidos: a la hora de crear los programas, 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 tratar de enseñar este lenguaje. Java SES Y SE6 Esta edición del libro aprovecha en buena medida las mejoras realizadas al lenguaje Java en 10 que Sun originalmente denominó JDK 1.5 Y cambió posteriormente a JDK5 o J2SE5 . Posteriormente, la empresa eliminó el obsoleto "2" y cambió el nombre a Java SE5. Muchos de los cambios en el lenguaj e Java SE5 fueron decididos para mejorar la experiencia de uso del programador. Como veremos, los diseñadores del lenguaje Java no obtuvieron un completo éxito en esta tarea, pero en general dieron pasos muy significativos en la dirección correcta. Uno de los objetivos más importantes de esta edición es absorber de manera completa las mejoras introducidas por Java SE5/6, presentarlas y emplearlas a lo largo de todo el texto. Esto quiere decir que en esta edición se ha tomado la dura decisión de hacer el texto únicamente compatible con Java SE5/6, por lo que buena parte del código del libro no puede compilarse con las versiones anteriores de Java; el sistema de generación de código dará errores y se detendrá si se intenta efectuar esa compilación. A pesar de todo, creo que los beneficios de este enfoque compensan el riesgo asociado a dicha decisión. Si el lector prefiere por algún motivo las versiones anteriores de Java, se puede descargar el texto de las versiones anteriores de este libro (en inglés) en la dirección www.MindView.net. Por diversas razones, la edición actual del libro no está en formato electrónico gratuito, sino que sólo pueden descargarse las ediciones anteriores. 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 mejoras 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 edició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 ediciones 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 esencial 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 suficientemente 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 reescrito con el fin de adaptarlo a los cambios principales en las bibliotecas de concurrencia de Java SE5, pero sigue proporcionando 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, especialmente a la hora de enseñar los patrones de diseño, que las personas que asistían a los seminarios obtenían mejores resultados 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 pruebas 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 validar 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 satisfacció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. Sobre el diseño de la cubierta La cubierta del libro está inspirada por el movimiento American Arts & Crafls Movement que comenzó poco antes del cambio de siglo y alcanzó su cenit entre 1900 y 1920. Comenzó en Inglaterra como reacción a la producción en masa de la revolución industrial y al estilo altamente ornamental de la época victoriana. Arts & Crafls enfatizaba el diseño con formas naturales, como en el movimiento art nOllveau, como el trabajo manual y la importancia del artesano, sin por ello renunciar al uso de herramientas modernas. Existen muchos ecos con la situación que vivimos hoy en día: el cambio de siglo, la evolución desde los rudimentarios comienzos de la revolución informática hacia algo más refinado y significativo y el énfasis en la artesanía del software en lugar de en su simple manufactura. La concepción de Java tiene mucho que ver con este punto de vista. Es un intento de elevar al programador por encima del sistema operativo, para transformarlo en un "artesano del software". Tanto el autor de este libro como el diseñador de la cubierta (que son amigos desde la infancia) han encontrado inspiración en este movimiento, ambos poseemos muebles, lámparas y otros objetos originales de este período o inspirados en el mismo. El otro tema de la cubierta sugiere una vitrina coleccionista que un naturalista podría emplear para mostrar los especírnenes de insectos que ha preservado. Estos insectos son objetos situados dentro de los objetos compartimento. Los objetos compartimento están a su vez, colocados dentro del "objeto cubierta", lo que ilustra el concepto de agregación dentro de la programación orientada a objetos. Por supuesto, cualquier programador de habla inglesa efectuará enseguida entre los insectos "bugs" y los errores de programación (también bugs). Aquí, esos insectos/errores han sido capturados y presumiblemente muertos en un tarro y confinados fmalmente dentro de una vitrina, con lo que tratamos de sugerir la habilidad que Java tiene para encontrar, mostrar y corregir los errores (habilidad que constituye uno de sus más potentes atributos). En esta edición, yo me he encargado de la acuarela que puede verse como fondo de la cubierta. 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 personas 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 tendrí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 intentos 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 esenciales a la hora de crear los cursos de formación multimedia, además de ayudar en la resolución de muchos otros problemas. 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 supuesto, 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 mentor 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 tambié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 sorprendentes 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 determinadas ideas. Gracias a los lectores que me han ayudado a aclarar los conceptos enviando sus comentarios; entre esos lectores 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 configuración y mantenimiento del servidor web basados en Linux, así como a la hora de mantener optimizado y protegido el servidor 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 momento . 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 maravillosa contribución a la comunidad de desarrolladores y cabe esperar que se puedan obtener grandes cosas con esta herramienta 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 concretas. 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 gracias 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 (munerosas 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 escribió 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 usuales en el sentido de que practican yoga y otras formas de engrandecimiento espiritual, que me resultan particularmente inspiradoras 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 comida 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 inspiraciones 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 esperaba 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ábitos 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 expresión será significativamente más fácil y flexible que las alternativas a medida que los problemas crecen en tamaño y en complejidad. 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 entender 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 desarrollar 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 instrucciones, 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 programadores de C y más todavía para los de C++, pero tampoco debe autoexcluirse si no tiene experiencia con estos lenguajes (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 abordan 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 demasiados temas muy rápidamente. Por ello, tanto debido a la variedad en el nivel de la audiencia como a la forma de presentar 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 intentar 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étodo de la experimentación e iteración (una técnica que también funciona en el diseño de programas). Desarrollé un curso utilizando 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 fundamentos 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 funciona 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 durante 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 siguiente. 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 comprender 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 programadores nunca conocerán, detalles que sólo sirven para confundir a las personas y que incrementan su percepció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 confundirá 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. Enseñar con este libro La edición original de este libro ha evolucionado a partir de un seminario de una semana que era, cuando Java se encontraba en su infancia, suficiente tiempo para cubrir el lenguaje. A medida que Java fue creciendo y añadiendo más y más funcionalidades y bibliotecas, yo tenazmente trataba de enseñarlo todo en una semana. En una ocasión, un cliente me sugirió que enseñara "sólo los fundamentos" y al hacerlo descubrí que tratar de memorizar todo en una única semana era angustioso tanto para mí como para las personas que asistían al seminario. Java ya no era un lenguaje "simple" que se podía aprender en una semana. Dicha experiencia me llevó a reorganizar este libro, el cual ahora está diseñado como material de apoyo para un seminario de dos semanas o un curso escolar de dos trimestres. La parte de introducción termina con el Capítulo 12, Tratamiento de errores mediante excepciones, aunque también puede complementarla con una introducción a IDBC, Servlets y JSP. Esto proporciona las bases y es el núcleo del CD-ROM Hands-On Java. El resto del libro se corresponde con un curso de nivel intennedio y es el material cubierto en el CD-ROM Intermediale Thinking in Java. Ambos discos CD ROM pueden adquirirse a través de wl:vw.MindView.net. Contacte con Prentice-Hall en www.prenhallprofessional. com para obtener más información acerca del material para el profesor relacionado con este libro. Documentación del JDK en HTML El lenguaje Java y las bibliotecas de Sun Microsystems (descarga gratuita en hllp://java.sun. com) se suministran con documentación en formato electrónico, que se puede leer con un explorador web. Muchos de los libros publicados sobre Java proporcionan esta documentación. Por tanto, o ya se tiene o puede descargase y, a menos que sea necesario, en este libro no se incluye dicha documentación, porque normalmente es mucho más rápido encontrar las descripciones de las clases en el explorador web que buscarlas en un libro (y probablemente la documentación en línea estará más actualizada). Basta con que utilice la referencia "JDK documentation". En este libro se proporcionan descripciones adicionales de las clases sólo cuando es necesario complementar dicha documentación, con el fin de que se pueda comprender un determinado ejemplo. 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 razonable 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. Fundamentos para Java Otra ventaja que presenta esta edición es el seminario multimedia gratuito que puede descargarse en la dirección www.MindVíew.net. Se trata del seminario Thinking in e, el cual proporciona una introducción a los operadores, funciones xxviii Piensa en Java 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 razonamiento suele ser: "Soy un programador inteligente y no quiero aprender e, sino e ++ o Java, por tanto, me salto el e y paso directamente 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 comenzar 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 seminario 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 distribució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 publiquen 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 . Permission to use, eopy, mOdify, and distribute this computer souree code (Source Code) and its documentation wit hout fee and without a wri tten agreement for the pur poses set forth be low is hereby granted, provided that the aboye copyright notice, this paragraph and the fo llowing five numbered paragraphs appear in a l l copies. l. Permiss i on is granted to compile the Souree Code and to include the compiled code, in execu tab le format only , in personal and eommereial software programs . 2. Permission is granted to use the Souree Code wi thout modification in classroom situations, including in presenta tion materials, provided that the book "Thinking in Java 11 is cited as the origino 3. Permi ssion to incorpora te the Souree Code into printed media may be obtained by contact i ng : MindView, lne . 5343 Vall e Vist a La Mesa, California 91941 Wayne@MindView . net 4. The Bouree Code and documentation are copyrighted by MindView, lnc. The Souree eode is provided wi thout express Introducción xxix or implied warranty of any kind, including any implied warranty of merchantability, fitness ter a particular purpose or non - infringement . MindView, lnc. does not warrant that the operation of any program that includes the Sauree Cede will be uninterrupted or error - free . MindView, lnc . makes no representation about the suitability of the Bouree Cede or of any software that includes the Sau ree Cede tor any purpose. The entire risk as to the quality and performance of any program that includes the Sauree Cade is with the user of the Sauree Codeo The user understands that the Sauree Cede was developed for research and instructional purposes and is advised not to rely exclusively for any reason on the Source Cede er any p r ogram that includes the Source Codeo Should the Source Cede er any resulting software prove defective, the user as sumes the cost of all necessary servicing, repair, or correction. 5. IN NO EVENT SHALL MINDVIEW, INC . , OR ITS PUBLI SHER BE LI ABLE TO ANY PARTY UNDER ANY LEGAL THEORY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL , OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION , OR ANY OTHER PECUNIARY LOSS, OR FOR PERSONAL I NJURIES, ARISING OUT OF THE USE OF TH IS SOURCE CODE AND ITS DOCUMENTATION, OR ARISING OUT OF THE INAB I LI TY TO US E ANY RESULTING PROGRAM , EVEN IF MINDVIEW , INC ., OR I TS PUBLISHER HAS BE EN ADV I SED OF THE POSSIBILITY OF SUCH DAMAGE . MI NDVIEW, INC . SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUD I NG, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY ANO FITNESS FOR A PARTICULAR PURPOSE. THE SOURCE CODE ANO DOCUMENTATION PROVIDED HEREUNDER IS ON AN "AS I S" BAS I S, WI THOUT ANY ACCOMPANYING SERVICES FROM MINDVI EW, I NC . , ANO MINDVIEW, INC. HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MOD I FICATIONS. Please note that MindVi e w , lnc. maintains a Web site wh i ch is the sole distribution point f or electronic copies of the Sou r ce Code, http://www . Mi n dView .net (and official mirror sites), where it i s freel y available under the terms stated above . lf you think you 1ve found an error in the Source Code , please submit a correction u sing the feedback system that you will find at http : //www.MindView . net . /// : Puede utilizar el código en sus proyectos y en la clase (incluyendo su material de presentaciones) siempre y cuando se mantenga la nota de copyright en cada uno de los archi vos fuente. 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 negrita 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ácticamente 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 codificació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 primero 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 "Analizamos la Naturaleza, la organ izamos en conceptos y vamos asignando significados a medida que lo hacemos, fundamentalmente porque participamos en un acuerdo tácito suscrito por toda nuestra comunidad de hablantes y que está codificado en los propios patrones de nuestro idioma .. . nos resulta imposible hablar si no utilizamos la organización y clasificación de los datos decretadas por ese acuerdo". Benjamin Lee Whorf (1 897-1941) La génesis de la revolución de las computadoras se halla ba en una máquina. La génesis de nuestros lenguajes de programació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 ("bicicletas para la mente", como se enorgullece en decir Steve Jobs), además de un medio de expresión diferente. Como resultado, 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 programac ión orientada a objetos (POO) es parte de este movimiento dirigido al uso de las computadoras como un medio de expresión. Este capítulo presenta los conceptos básicos de la programación orientada a objetos, incluyendo una introducción a los métodos de desarrollo. Este capítulo, y este libro, supone que el lector tiene cierta experiencia en programación, aunque no necesariamente 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 lenguaje, 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 abstracciones del lenguaje ensamblador. Estos lenguajes constituyen grandes mejoras sobre el lenguaje ensamblador, pero su principal 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 problema, 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, ademá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 anteriormente 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 solicitudes", picliéndole que realice operaciones sobre sí mismo. En teoría, puede tomarse cualquier componente conceptual 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 pensar 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 complejidad 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, siendo "clase" sinónimo de "tipo". La característica distintiva más importante de una clase es "el conjunto de mensajes 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 escribir 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 programació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 Un objeto tiene estado, comportamiento e identidad. Esto significa que un objeto puede tener datos internos (lo que le proporciona el estado), métodos (para proporcionar un comportamiento) y que cada objeto puede ser diferenciado de fanna unívoca de cualquier otro objeto; es decir, cada objeto tiene una dirección de memoria exclusiva. 2 Todo objeto tiene una interfaz Aristóteles fue probablemente el primero en estudiar cuidadosamente el concepto de tipo; hablaba de "la clase de peces y de la clase de pájaros". La idea de que todos los objetos, aún siendo únicos, son también parte de una clase de objetos que tienen características y comportamientos comunes ya se empleó en el primer lenguaje orientado a objetos, el Simula-67, que ya usaba su palabra clave fundamental class, que permite introducir un nuevo tipo en un programa. Simula, como su nombre implica, se creó para desarrollar simulaciones como la clásica del "problema del cajero de un banco". En esta simulación, se tienen muchos cajeros, clientes, cuentas, transacciones y unidades monetarias, muchísimos "'objetos". Los objetos, que son idénticos excepto por su estado durante la ejecución de un programa, se agrupan en "clases de objetos", que es de donde procede la palabra clave class. La creación de tipos de datos abstractos (clases) es un concepto fundamental en la programación orientada a objetos. Los tipos de datos abstractos funcionan casi exactamente como tipos predefmidos: pueden crearse variables de un tipo (llamadas objetos u instancias en la jerga de la POOl y manipular dichas variables (mediante el envío de mensajes o solicitudes, se envía un mensaje y el objeto sabe lo que tiene que hacer con él). Los miembros (elementos) de cada clase comparten algunos rasgos comunes. Cada cuenta tiene asociado un saldo, cada cajero puede aceptar un depósito, etc. Además, cada miembro tiene su propio estado. Cada cuenta tiene un saldo diferente y cada cajero tiene un nombre. Por tanto, los cajeros, clientes, cuentas, transacciones, etc., pueden representarse mediante una entidad unívoca en el programa informático. Esta entidad es el objeto y cada objeto pertenece a una detenninada clase que define sus características y comportamientos. Por tanto, aunque en la programación orientada a objetos lo que realmente se hace es crear nuevos tipos de datos, en la práctica, todos los lenguajes de programación orientada a objetos utilizan la palabra clave "class". Cuando vea la palabra "type" (tipo) piense en "elass" (elase), y viceversa 3 Dado que una clase describe un conjunto de objetos que tienen características (elementos de datos) y comportamientos (funcionalidad) idénticos, una clase realmente es un tipo de datos porque, por ej emplo, un número en coma flotante tamb ién tiene un conjunto de características y comportamientos. La diferencia está en que el programador define un clase para adaptar un problema en lugar de forzar el uso de un tipo de datos existente que fue diseñado para representar una unidad de almacenamiento en una máquina. Se puede ampliar el lenguaje de programación añadiendo nuevos tipos de datos específicos que se adapten a sus necesidades. El sistema de programación admite las nuevas clases y proporciona a todas ellas las comprobaciones de tipo que proporciona a los tipos predefinidos. El enfoque orientado a objetos no está limitado a la creación de simulaciones. Se esté o no de acuerdo en que cualquier programa es una simulación del sistema que se está diseñando, el uso de las técnicas de la POO puede reducir fácil mente un gran conjunto de problemas a una sencilla solución. Una vez que se ha definido una clase, se pueden crear tantos objetos de dicha clase como se desee y dichos objetos pueden manipularse como si fueran los elementos del problema que se está intentado resolver. Realmente, uno de los retos de la programación orientada a objetos es crear una correspondencia uno-a-uno entre los elementos del espacio del problema y los objetos del espacio de la solución. Pero, ¿cómo se consigue que un objeto haga un trabajo útil para el programador? Debe haber una [onua de hacer una solicitud al objeto para que haga algo, como por ejemp lo, completar una transacción, dibujar algo en pantalla o encender un interruptor. Además, cada objeto sólo puede satisfacer ciertas solicitudes. Las solicitudes que se pueden hacer a un objeto se definen mediante su infeljaz y es el tipo lo que determina la interfaz. Veamos un ejemplo con la representación de una bombilla: 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 Interfaz Luz encenderO apagarO brillarO atenuarO Luz 1z = new Luz {) ; 1z.encender () ; La interfaz determina las solicitudes que se pueden hacer a un determinado objeto, por lo que debe existir un código en alguna 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 posible solicitud; cuando se hace una determinada solicitud a un objeto, se llama a dicho método. Este proceso se resume diciendo 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 pueden 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, tampoco es necesario incluir la parte inferior. Un objeto proporciona servicios Cuando se está intentando desarrollar o comprender el diseño de un programa, una de las mejores formas de pensar en los objetos es como si fueran "proveedores de servicios", El programa proporciona servicios al usuario y esto se conseguirá utilizando los servicios que ofrecen otros objetos. El objetivo es producir (o incluso mejor, localizar en las bibliotecas de código existentes) un conjunto de objetos que facilite los servicios idóneos para resolver el problema. Una manera de empezar a hacer esto es preguntándose: "Si pudiera sacarlos de un sombrero mágico, ¿qué objetos resolverían el problema de la forma más simple?". Por ejemplo, suponga que quiere escribir un programa de contabilidad. Puede pensar en algunos objetos que contengan pantallas predefinidas para la introducción de los datos contables, otro conjunto de objetos que realicen los cálculos necesarios y un objeto que controle la impresión de los cheques y las facturas en toda clase de impresoras. Es posible que algunos de estos objetos ya existan, pero ¿cómo deben ser los que no existen? ¿Qué servicios deberían proporcionar esos objetos y qué objetos necesitarían para cumplir con sus obligaciones? Si se hace este planteamiento, llegará a un punto donde puede decir: "Este objeto es lo suficientemente sencillo como para escribirlo yo mismo" o "Estoy seguro de que este objeto ya tiene que existir". Ésta es una fonna razonable de descomponer un problema en un conjunto de objetos. Pensar en un objeto como en un proveedor de servicios tiene una ventaja adicional: ayuda a mejorar la cohesión del objeto. Una alta cohesión es una cualidad fundamental del diseño software, lo que significa que los diferentes aspectos de un componente de software (tal como un objeto, aunque también podría aplicarse a un método o a una biblioteca de objetos) deben "ajustar bien entre sí". Un problema que suelen tener los programadores cuando diseñan objetos es el de asignar demasiada funcionalidad al objeto. Por ejemplo, en el módulo para imprimir cheques, puede decidir que es necesario un objeto que sepa todo sobre cómo dar formato e imprimir. Probablemente, descubrirá que esto es demasiado para un solo objeto y que hay que emplear tres o más objetos. Un objeto puede ser un catálogo de todos los posibles diseños de cheque, al cual se le Introducción a los objetos 5 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 utilizar 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 objetos 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 proceso 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 programadores de clientes4 (los consumidores de clases que emplean los tipos de datos en sus aplicaciones). El objetivo del programador 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 mantenga todo lo demás oculto. ¿Por qué? Porque si está oculto, el programador cliente no puede acceder a ello, lo que significa 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 construir 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 manipule 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 interfaz 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 ejecute más rápidamente. Si la interfaz y la implementación están claramente separadas y protegidas, podrá hacer esto fácilmente. 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 acceder a dicho elemento excepto usted, el creador del tipo, dentro de los métodos de dicho tipo. private es un muro de ladrillos 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 privados (private). 4 Ténnino acuñado por mi amigo Scott Meyers. 6 Piensa en Java 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 reutilizació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, aunque también se puede colocar un objeto de dicha clase dentro de una clase nueva. Esto es 10 que se denomina "crear un objeto 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 existentes se denomina composición (si la composición se realiza de forma dinámica, se llama agregación). A menudo se hace referencia a la composición como una relación "tiene un", como en "un coche tiene un motor". ~__co_c_h_e__~I·~------~ ___ m_o_to_r__ ~ Este diagrama UML indica la composición mediante un rombo relleno, que establece que hay un coche. Nonnalmente, yo utilizo una forma más sencilla: sólo una línea, sin el rombo, para indicar una asoc iación. 5 La composición conlleva una gran fl exibilidad. Los objetos miembro de la nueva clase normalmente son privados, lo que les hace inaccesibles a los programadores de clientes que están usando la clase. Esto le permite cambiar dichos miembros sin disturbar al código cliente existente. Los objetos miembro también se pueden modificar en tiempo de ejecución, con el fin de cambiar dinámicamente el comportamiento del programa. La herencia, que se describe a continuación, no proporciona esta flexibilidad, ya que el compilador tiene que aplicar las restricciones en tiempo de compilación a las clases creadas por herencia. Dado que la herencia es tan importante en la programación orientada a objetos, casi siempre se enfatiza mucho su uso, de manera que los programadores novatos pueden llegar a pensar que hay que emplearla en todas partes. Esto puede dar lugar a que se hagan diseños demasiado complejos y complicados. En lugar de esto, en primer lugar, cuando se van a crear nuevas clases debe considerarse la composición, ya que es más simple y flexible. Si aplica este método, sus diseños serán más inteligentes. Una vez que haya adquirido algo de experiencia, será razonablemente obvio cuándo se necesita emplear la herencia. 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 clase hija) también refleja los cambios. cy 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 composició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Ísticas 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 desperdicio 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 aluminio puede modelarse, el acero puede tener propiedades magnéticas). Además, algunos comportamientos pueden ser diferentes (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írculo, cuadrado, triángulo, etc.), cada una con sus propias características adicionales y comportamientos. Por ejemplo, ciertas 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 Círculo I Cuadrado Triángulo Representar la solución en los mismos ténninos que el problema es muy útil, porque no se necesitan muchos modelos intermedios 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 orientado 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 objetos 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 equivalencia de tipos a través de la herencia es uno de los caminos fundamentales para comprender el significado de la programació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 derivada, 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: simplemente, 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 primitivo 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 Triángulo Cuadrado 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 Círculo dibujarO borrarO I Cuadrado dibujarO borrarO Triángulo dibujarO borrarO 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. Relaciones es-un y es-corno-un Es habitual que la herencia suscite un pequeño debate: ¿debe la herencia sustituir sólo los métodos de la clase base (y no añadir métodos nuevos que no existen en la clase base)? Esto significaría que la clase derivada es exactamente del mismo 1 Introducción a los objetos 9 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 denomina principio de sustitución. En cierto sentido, ésta es la fonna ideal de tratar la herencia. A menudo, en este caso, la relació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 sentido. 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 contiene 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. Termostato Controles bajarTemperaturaO Sistema de frío enfriarO I t Acondicionador de aire enfriarO I Bomba de calor 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 caliente, 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 aplicarse 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. Objetos intercambiables con polimorfismo Cuando se trabaja con jerarquías de tipos, a menudo se desea tratar un objeto no como el tipo específico que es, sino como su tipo base. Esto penmite escribir código que no dependa de tipos específicos. En el ejemplo de las fonmas, los métodos manipulan las fonnas genéricas, independientemente de que se trate de círculos, cuadrados, triángulos o cualquier otra fonna que todavía no baya sido definida. Todas las fonmas pueden dibujarse, borrarse y moverse, por lo que estos métodos simplemente envían un mensaje a un objeto forma, sin preocuparse de cómo se enfrenta el objeto al mensaje. Tal código no se ve afectado por la adición de tipos nuevos y esta adición de tipos nuevos es la fonna más común de ampliar un programa orientado a objetos para manejar situaciones nuevas. Por ejemplo, puede derivar un subtipo nuevo de forma llamado pentágono sin modificar los métodos asociados sólo con las fonmas genéricas. Esta capacidad de ampliar fácilmente un diseño derivando nuevos subtipos es una de las principales fonnas de encapsular cambios. Esto mejora enormemente los diseños además de reducir el coste del mantenimiento del software. Sin embargo, existe un problema cuando se intenta tratar los objetos de tipos derivado como sus tipos base genéricos (círculos como formas, bicicletas como automóviles, cormoranes como aves, etc.). Si un método dice a una forma que se dibuje, O a un automóvil genérico que se ponga en marcha o a un ave que se mueva, el compilador no puede saber en tiempo de 10 Piensa en Java 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 perspectiva 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)? Ave controladorAves reubicarO ¿Qué ocurre cuando se llama a moverO? I moverO t 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 denomina 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 exactamente 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 contenidos 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 acoplamiento 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 programador 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 informació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 significa 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, efectuar una proyección sobre un tipo base, ascendiendo por el diagrama de herencia. "Upcasting" t .- ______ Jo ,-_____J I B o I o : o o Círculo Cuadrado Triángulo 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 teniendo 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ándose 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, habitualmente 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. La jerarquía de raíz única Uno de los aspectos de la POO que tiene una importancia especial desde la introducción de e++ es si todas las clases en últi ma instancia deberían ser heredadas de una única clase base. En Java (como en casi todos los demás lenguajes de POO 12 Piensa en Java 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 proporcionar la misma utilidad que se construye en otros lenguajes de programación orientada a objetos. Y en cualquier nueva biblioteca 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 fundamentales 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 llevar. 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 necesario acomodar cualquier cosa que se quiera introducir en él. Por tanto, no necesitamos saber cuántos objetos pueden almacenarse 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 resolver nuestro problema. Si un mismo tipo de contenedor satisface todas las necesidades, no existe ninguna razón para disponer 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 determinadas 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 ciertas operaciones pueden llevar asociado un coste radicalmente distinto. La operación de acceder aleatoriamente a los elementos 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 operaciones 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. Tipos parametrizados (genéricos) Antes de Java SES, los contenedores albergaban objetos del tipo universal de Java: Objee!. La j erarquía de raíz única indica que todo es de tipo Objeet, por lo que un contenedor que almacene objetos de tipo Object podrá almacenar cualquier cosa. 6 Esto hacía que los contenedores fueran fáciles de reutilizar. Para utilizar uno de estos contenedores, simplemente se añaden a él referencias a objetos y luego se las extrae. Sin embargo, puesto que el contenedor sólo permite almacenar objetos de tipo Object, al añadir una referencia a objeto al contene- dor, esa referencia se transforma en una referencia a Object perdiendo así su carácter. Al extraerla, se obtiene una referencia a Object y no una referencia al tipo que se hubiera almacenado. En estas condiciones, ¿cómo podemos transformar esa referencia en algo que tenga el tipo específico de objeto que hubiéramos almacenado en el contenedor? Lo que se hace es volver a utilizar el mecanismo de transfonnación de tipos (cast), pero esta vez no efectuamos una generalización, subiendo por la jerarquía de herencia, sino que efectuamos una especialización, descendiendo desde la jerarquía hasta alcanzar un tipo más específico. Este mecanismo de transformación de tipos se denomina especialización (downcasting). Con el mecanismo de generalización (upcasting) , sabemos por ejemplo que un objeto Circulo es también de tipo Forma, por lo que resulta seguro realizar la transformación de tipos. Sin embargo, no todo objeto de tipo Object es nece- sariamente de tipo Circulo o Forma por lo que no resulta tan seguro realizar una especialización a menos que sepamos concretamente lo que estamos haciendo. Sin embargo, esta operación no es del todo peligrosa, porque si efectuamos una conversión de tipos y transformamos el objeto a un tipo incorrecto, obtendremos un error de tipo de ejecución denominado excepción (lo que se describe más adelante). Sin embargo, cuando extraemos referencias a objetos de un contenedor, tenemos que disponer de alguna forma de recordar exactamente lo que son, con el fin de poder realizar la conversión de tipos apropiada. El mecanismo de especialización y las comprobaciones en tiempo de ejecución requieren tiempo adicional para la ejecución del programa y un mayor esfuerzo por parte del programador. ¿No sería más lógico crear el contenedor de manera que éste supiera el tipo de los elementos que almacena, eliminando la necesidad de efectuar conversiones de tipos y evitando los errores asociados? La solución a este problema es el mecanismo de tipos parametrizados. Un tipo parametrizado es una clase que el compilador puede personalizar automáticamente para que funcione con cada tipo concreto. Por ejemplo, con un contenedor parametrizado, el compilador puede personalizar dicho contenedor para que sólo acepte y devuelva objetos Forma. Uno de los cambios principales en Java SES es la adición de tipos parametrizados, que se denominan genéricos en Java. El uso de genéricos es fáci lmente reconocible, ya que emplean corchetes angulares para encerrar alguna especificación de tipo, por ejemplo, puede crearse un contenedor de tipo ArrayList que almacene objetos de tipo Forma del siguiente modo: ArrayList formas ~ new ArrayList () ; También se han efectuado modificaciones en muchos de los componentes de las bibliotecas estándar para poder aprovechar el uso de genéricos. Como tendremos oportunidad de ver, los genéricos tienen una gran importancia en buena parte del código utilizado en este libro. Creación y vida de los objetos Una de las cuestiones criticas a la hora de trabajar con los objetos es la forma en que éstos se crean y se destruyen. Cada objeto consigue una serie de recursos, especialmente memoria, para poder simplemente existir. Cuando un objeto deja de Los 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. 6 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 mascotas). A primera vista, el problema parece muy simple: creamos un contenedor para almacenar las aeronaves y luego creamos 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, podrí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 quedan 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 resultar 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 general, 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 almacenamiento 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 objetos 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 alternativa, 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++ fracasaran. 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. Tratamiento de excepciones: manejo de errores Desde la aparición de los lenguajes de programación, el tratami ento de los errores ha constituido un problema peculiann ente dificil. Debido a que resulta muy complicado di señar un buen esquema de tratamiento de errores! muchos lenguajes simplemente ignoran este problema, dejando que lo resue lvan los diseftadores de bibliotecas, que al final terminan desarrollando soluciones parciales q ue funcionan en muchas situaciones pero cuyas medidas pueden ser obviadas fác ilmente; generalmente, basta con ignorarlas. Uno de los problemas princi pales de la mayoría de los esquemas de tratamiento de errores es que dependen de que el programador tenga cuidado a la hora de seguir un convenio preestablecido que no resulta obligatorio dentro del leng uaj e. Si el programador no tiene cuidado (lo cual suele suceder cuando hay prisa por terminar un proyecto), puede olvi darse fáci lmente de estos esquemas. Los mecanismos de tratamiento de excepciones integran la gestión de errores directamente dentro del lenguaje de programación. en ocasiones. dentro incluso del sistema operativo. Una excepción no es más que un objeto "generado" en el lugar donde se ha producido el error y que puede ser "capturado" mediante una rutina apropiada de tratamiento de excepciones diseñada para gestionar dicho tipo particular de error. Es como si el tratamiento de excepciones fuera una ruta de ejecución paralela y dife rente, que se toma cuando algo va mal. Y, como se utili za una ruta de ejecución independiente. ésta no tiene porqué interfe rir con el código que se ejecuta nonnalmente. Esto hace que el código sea más simple de escribir. porque no es necesario comprobar constantemente la existencia de eITores. Además. las excepciones generadas se diferencian de los típ icos valores de error devueltos por los métodos o por los indicadores activados por los métodos para avisar que se ha producido una condición de error; tanto los valores como los indicadores de error podrían ser ignorados por el programador. Las excepciones no pueden ignorarse. por 10 que se garantiza que en algún momento serán tratadas. Finalmente. las excepciones proporcionan un mecanismo para recuperarse de manera fiable de cualquier situación errónea. En lugar de limitarse a salir del programa. a menudo podemos corregir las cosas y restaurar la ejecución , lo que da como resultado programas mucho más robustos. El tratamiento de excepciones de Java resulta muy sobresal iente entre los lenguaj es de programación, porque en Java el tratamiento de excepciones estaba previsto desde el principio y estamos obligados a utili zarlo. Este esquema de tratamiento de excepciones es el único mecanismo aceptable en Java para informar de la existencia de errores. Si no se escribe el código de manera que trate adecuadamente las excepciones se obtiene un error en tiempo de compilación. Esta garantía de coherencia puede hacer que, en ocasiones. el tratamiento de erro res sea mucho más sencillo. Merece la pena resaltar que el tratamiento de excepciones no es una característica orientada a objetos, au nque en los lenguajes de programación orientada a objetos las excepciones se representan normalmente medi ante un objeto. Los mecanismos de tratamiento de excepciones ya existían antes de que hicieran su aparición los lenguajes orientados a objetos. 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 fragmentos que se ejecutan por separado, se denominan hebras y el conjunto general se llama concurrencia. Un ejemplo bastante 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 realizando. Normalmente, las tareas son sólo una forma de asignar el tiempo disponible en un único procesador. Pero si el sistema operativo 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ápido, 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 problema, los recursos que puedan compartirse, como por ejemplo una impresora, deben bloquearse mientras estén siendo utilizados 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 respuesta 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 problemas 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 realmente, 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 consumidores de la información. El repositorio de información, el software que distribuye la información y la máquina o máquinas 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 inserten 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 realizan modificaciones en el software de cliente, es necesario diseñar el software, depurarlo e instalarlo en las máquinas cliente, 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 servidor 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, utilizando 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 distribució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 problema global de la infonnática cliente/servidor de una vez y para siempre. La Web como un gigantesco servidor La Web es, en la práctica, un sistema gigantesco de tipo cliente/servidor. En realidad, es todavía más complejo, ya que lo que tenemos es un conjunto de servidores y clientes que coexisten en una misma red de manera simultánea. El usuario no necesita ser consciente de ello, por 10 que lo único que hace es conectarse con un servidor en cada momento e interactuar con él (aún cuando para llegar a ese servidor haya sido necesario ir saltando de servidor en servidor por todo el mundo hasta dar con el correcto). Inicialmente, se trataba de un proceso muy simple de carácter unidireccional: el usuario enviaba una solicitud al servidor y éste le devolvía un archivo, que el software explorador de la máquina (es decir, el cliente) se encargaba de interpretar, efectuando todas las tareas de formateo en la propia máquina local. Pero al cabo de muy poco tiempo, los propietarios de servidores comenzaron a querer hacer cosas más complejas que simplemente suministrar páginas desde el servidor. Querían disponer de una capacidad completa cliente/servidor, de forma que el cliente pudiera, por ejemplo enviar información al servidor, realizar búsquedas en una base de datos instalada en el servidor, añadir nueva infonnación al servidor o realizar un pedido (lo que requiere medidas especiales de seguridad). Éstos son los cambios que hemos vivido en los últimos años en el desarrollo de la Web. Los exploradores web representaron un gran avance: permitieron implementar el concepto de que un mismo fragmento de infonnación pudiera visualizarse en cualquier tipo de computadora sin necesidad de efectuar ninguna modificación. Sin embargo, los primeros exploradores eran bastante primitivos y se colapsaban rápidamente debido a las demandas que se les bacía. No resultaban peculiarmente interactivos y tendían a sobrecargar al servidor tanto como a la propia red Internet, porque cada vez que hacía falta hacer algo que requería programación, era necesario devolver la infonnación al servidor para que éste la procesara. De esta fonna, podía tardarse varios segundos o incluso minutos en averiguar simplemente que habíamos tecleado incorrectamente algo dentro de la solicitud. Como el explorador era simplemente un mecanismo de visualización no podía realizar ni siquiera la más simple de las tareas (por otro lado, resultaba bastante seguro ya que no podía ejecutar en la máquina local ningún programa que pudiera contener errores o virus). ( Para resolver este problema, se han adoptado diferentes enfoques. Para empezar se han mejorado los estándares gráficos para poder disponer de mejores animaciones y vídeos dentro de los exploradores. El resto del problema sólo puede resol - 18 Piensa en Java verse incorporando la capacidad de ejecutar programas en el extremo cliente, bajo control del explorador. Esto se denomina programación del lado del c1ientc. Programación del lado del cliente El diseño inicial de la Web, basado en una arquitectura servidor/explorador, permitía disponer de contenido interactivo, pero esa interacti vidad era completamente proporcionada por el servidor. El servidor generaba páginas estáticas para el explorador cliente, que si mplemente se encargaba de interpretarlas y mostrarlas. El lenguaje básico HTML (HyperText Markup Language) contiene una serie de mecanismos simpl es para la introducción de datos: recuadros de introducción de texto, casillas de verificación, botones de opción, listas normales y listas desplcgables, así como un botón que sólo podía programarse para bonar los datos del formulario o enviarlos al servidor. Ese proceso de envío se llevaba a cabo a través de la interfaz CGI (Common Gateway lntelface) incluida en todos los servidores web. El texto incorporado en el envío le dice a la interfaz CGI 10 que tiene que hacer. La acción más común, en este caso, consiste en ejecutar un programa ubicado en un servidor en un directorio normalmente llamado "cgi-bin" (si observa la barra de direcciones situada en la parte superior del ex plorador cuando pulse un botón en una página web, en ocasiones podrá ver las palabras "cgi-bin" como parte de la dirección). Estos programas del lado del servidor pueden escribirse en casi cualquier lenguaje. Perl es uno de los lenguajes más utilizados para este tipo de tareas, porque está diseñado específicamente para la manipulación de textos y es un lenguaje inte rpretado, por 10 que se puede instalar en cualquier servidor independientemente de cuál sea su procesador o su sistema operativo. Sin embargo, otro lenguaje, Python (WwHePython.org) se está abriendo camino rápidamente, debido a su mayor potencia y su mayor simplicidad. Hay muchos sitios web potentes en la actualidad diseñados estrictamente con CGI, y lo cierto es que con CGI se puede hacer prácticamente de todo. Sin embargo, esos sitios web basados en programas CGI pueden llegar a ser rápidamente bastante complicados de mantener, y además pueden aparecer problemas en lo que se refiere al tiempo de respuesta. El tiempo de respuesta de un programa CGl depende de cuántos datos haya que enviar, de la carga del servidor y de la red Internet (además, el propio arranque de un programa COI tiende a ser bastante lento). Los primeros diseñadores de la Web no previeron la rapidez con que el ancho de banda disponible iba a agotarse debido a los tipos de aplicaciones que la gente llegaría a desarrollar. Por ejemplo, es casi imposible diseñar de manera coherente una aplicación con gráficos dinámicos, porque es necesario crear un archivo GIF (Graphics !nterchange Formal) y enviarlo del servidor al cliente para cada versión de gráfico. Además, casi todos los usuarios hemos experimentado lo engorroso del proceso de validación de los datos dentro de un formulario enviado a través de la Web. El proceso es el siguiente: pulsamos el botón de envío de la página; se envían los datos al servidor, el servidor arranca un programa CGI que descubre un error, fonnatea una página HTM L en la que nos informa del error y devuelve la página al cliente; a continuación, es necesario que el usuario retroceda una página y vuelva a intentarlo. Este enfoque no sólo resulta lento sino también poco elegante. La solución consiste en usar un mecanismo de programación del lado del cliente. La mayoría de las computadoras de sobremesa que incluyen un explorador wcb son máquinas bastante potentes, capaces de realizar tareas muy com plejas; con el enfoque ori gina l basado en HTML estático, esas potentes máqui nas simplemente se limitan a esperar sin hacer nada, hasta que el servidor se digna a enviarles la siguiente página. La programación del lado del cliente pennite asignar al explorador web todo el trabajo que pueda llevar a cabo, con lo que el resultado para los usuarios es una experiencia mucha más rápida y más interactiva a la hora de acceder a los sitios web. El problema con las explicaciones acerca de la programación del lado del cliente es que no se diferencian mucho de las explicaciones relativas a la programación en general. Los parámetros son prácticamente idénticos, aunque la plataforma sea distinta: un explorador web es una especie de sistema operativo limitado. En último ténn ino, sigue siendo necesario diseñar programas, por lo que los problemas y soluciones que nos encontramos dentro del campo de la programación del lado del cliente son bastante tradicionales. En el resto de esta sección, vamos a repasar algunos de los principales problemas y técnicas que suelen encontrarse en el campo de la programación del lado del cliente. 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 plugin. Se trata de un mecanismo mediante el que un programador puede añadir algún nuevo tipo de funcionalidad a un explorador 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 permiso 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 encarga 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 obtiene 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 (ellenguaje 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 asemejaba 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 realmente 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, realmente, 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 problema 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 internacional, 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áticamente 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 programador 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 través de Internet simplemente para descubrir que hemos escrito mal una fecha o algún otro parámetro; asimismo, la computadora 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 velocidad y la capacidad de respuesta, sino que disminuyen también la carga de trabajo de los servidores y el tráfico de red, evitando 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 migració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 aciago 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 considerable 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á disponible 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 requerí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, equivalente a la máquina virtual Java (NM, Java Virtual Machine; es la plataforma software en la que se ejecutan los programas 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 razones por las que un programador podría sentirse tentado a adoptar ese lenguaje, y han respondido introduciendo significativas 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.gomono.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 Redes Internet e intranet La Web es la solución más general para el problema de las arquitecturas cliente/servidor, por lo que tiene bastante sentido utilizar esta misma tecnología para resolver un cierto subconjunto de ese problema: el problema clásico de las arquitecturas cliente/servidor internas a una empresa. Con las técnicas tradicionales cliente/servidor, nos encontramos con el problema de la existencia de múltiples tipos de computadoras cliente, así como con la dificultad de instalar nuevo software de cliente; los exploradores web y la programación del lado del cliente permiten resolver fácilmente ambos problemas. Cuando se utiliza tecnología web para una red de infonnación restringida a una empresa concreta, la arquitectura resultante se denomina intranet. Las intranets proporcionan un grado de seguridad mucho mayor que Internet, ya que podemos controlar fisicamente el acceso a los equipos de la empresa. En términos de formación, una vez que los usuarios comprenden el concepto general de explorador les resulta mucho más fácil asumir las diferencias de aspecto entre las distintas páginas y applets, por lo que la curva de aprendizaje para los nuevos tipos de sistemas se reduce. El problema de seguridad nos permite analizar una de las divisiones que parecen estarse formando de manera automática en el mundo de la programación del lado del cliente. Si nuestro programa se está ejecutando en Internet no sabemos en que plataforma se ejecutará y además es necesario poner un cuidado adicional en no diseminar código que contenga errores. En estos casos, es necesario disponer de un lenguaje interplatafonna y seguro, como por ejemplo, un lenguaje de script o Java. Sí nuestra aplicación se ejecuta en una intranet, es posible que el conjunto de restricciones sea distinto. No resulta extraño que todas las máquinas sean plataformas Tntel/Windows. En una intranet, nosotros somos responsables de la calidad de nuestro propio código y podemos corregir los errores en el momento en que se descubran. Además, puede que ya dispongamos de una gran cantidad de código heredado que haya estado siendo utilizado en alguna arquitectura cliente/servidor más tradicional, en la que es necesario instalar fi sicamente los programas cliente cada vez que se lleva a cabo una actualización. El tiempo que se pierde a la hora de instalar actualizaciones es, precisamente, la principal razón para comenzar a utilizar exploradores, porque las actualizaciones son invisibles y automáticas (Java Web Start también constituye una solución a este problema). Si trabajamos en una intranet de este tipo, la solución más lógica consiste en seguir la ruta más corta que nos permita utilizar la base de código existente, en lugar de volver a escribir todos los programa en un nuevo lenguaje. Al enfrentarse con este ampl io conjunto de soluc iones para los problemas de la programación del lado del cliente, el mejor plan de ataque consiste en realizar un análisis de coste-beneficio. Considere las restricciones que afectan a su problema y cuál sería la ruta más corta para encontrar una solución. Puesto que la programación del lado del cliente sigue siendo una programación en sentido tradicional, siempre resulta conveniente adoptar el enfoque de desarrollo más rápido en cada situación concreta. Ésta es la mejor manera de prepararse para los problemas que inevitablemente encontraremos a la hora de desarrollar los programas. Programación del lado del servidor En nuestro análisis, hemos ignorado hasta ahora la cuestión de la programación del lado del servidor, que es probablemente donde Java ha tenido su éxito más rorundo. ¿Qué sucede cuando enviamos una solicitud a un servidor? La mayor parte de las veces, la solicitud dice simplemente "Envíame este archivo". A continuación, el explorador interpreta el archivo de la forma apropiada, como página HTML, como imagen, como UI1 applel de Java, como programa en lenguaje de scripl, etc. Las solicitudes más complicadas dirigidas a los servidores suelen implicar una transacción de base de datos. Una situación bastante común consiste en enviar una solicitud para que se realice una búsqueda completa en una base de datos, encargándose a continuación el servidor de dar formato a los resultados como página HMTL y enviar ésta al explorador (por supuesto, si el cliente dispone de un mayor grado de inteligencia, gracias a la utilización de Java o de un lenguaje de script, pueden enviarse los datos en bruto y formatearlos en el extremo cliente, lo que sería más rápido e impondría una menor carga de trabajo al servidor). Otro ejemplo: puede que queramos registrar nuestro nombre en una base de datos para unimos a un grupo o realizar un pedido, lo que a su vez implica efectuar modificaciones en la base de datos. Estas solicitudes de base de datos deben procesarse mediante algún tipo de código situado en el lado del cliente; es a este tipo de programas a los que nos referimos a la hora de hablar de programación del lado del cliente. Tradicionalmente, la programación del lado del cliente se llevaba a cabo utilizando Perl, Python, C++, o algún otro lenguaje para crear programas CG!, pero con el tiempo se han desarrol1ado otros sistemas más sofisticados, entre los que se incluyen los servidores web basados en Java que permiten realizar todas las tareas de programación del lado del servidor en lenguaje Java, escribiendo lo que se denomina servlets. Los servlets y sus descendientes, las páginas JSP, son dos de las principales razones por las que las empresas que desarrollan sitios web están adoptando Java, especialmente porque dichas tecnologías eliminan los problemas derivados de tratar 22 Piensa en Java 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 biblioteca estándar y las numerosas bibliotecas de otros fabricantes que ya están disponibles y que continúan siendo desarrolladas. 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 intermedias 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 orientada 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 evaluar cuáles son nuestras necesidades reales y detenninar si Java permitirá satisfacerlas de fonna óptima o si, por el contrario, 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 restricciones específicas que Java pueda no satisfacer, resulta recomendable investigar otras alternativas (en particular, mi recomendació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 conveniente adoptar la decisión que ftnalmente haya tomado. Todo es un objeto "Si habláramos un lenguaj e diferente, percibiríamos un mundo algo distinto". Ludwig Wittgenstein (1889-1951) 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 importante 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. Los objetos se manipulan mediante referencias Cada lenguaje de programación di spone de sus propios mecanismos para manipular los elementos almacenados en memoria. En ocasiones. el programador debe ser continuamente consciente del tipo de manipulación que se está efectuando. ¿Estamos tratando con el elemento directamente o con algún tipo de representación indirecta (un puntero en C o C++), que haya que tratar con una sintaxis especial? Todo esto se simplifica en Java. En Java, todo se trata como un objeto, utilizando una única sintax is coherente. Aunque f,.olamas todo como un objeto) los identificadores que manipulamos son en realidad "referencias" a objetos. l Podríamos imaginarnos una TV (el objeto) y un mando a distancia (la referencia): mientras di spongamos de esta referencia tendremos una conexión con la televisión, pero cuando alguien nos dice "cambia de canal" o "baja el volumen", lo que hacemos es manipular la referencia, que a su vez modifica el objeto. Si queremos movemos por la habitación y continuar controlando la TV, llevamos con nosotros el mando a distancia/referencia, no la televisión . 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 punteros. 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 personas 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 proviniera 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 \' * System.out.println (new Date ()) ; * */ /1/ , También podemos usar código HTML como en cualquier otro documento web, para dar faonato al texto nomlal de las descripciones: ji : object / Documenta t ion3. j ava / ** * Se puede inc!uso< / em> insertar una lista : * <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 documentación. No utilice encabezados como o
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. Alg unos marcadores de ejemplo He aquí algunos de los marcadores Javadoc disponibles para la documentación de código. Antes de tratar de utilizar Javadoc de fonna seria, consulte la documentación de referencia de Javadoc dentro de )a documentación del IDK, para ver las diferentes formas de uso de Javadoc. @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 marcador 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. {@Iink paquete.clase#miembro etiqueta} Muy similar a @see, exceplO porque se puede ut ilizar en línea y emplea la etiqueta como texto del bipervínculo, en lugar de "See Also". 38 Piensa en Java {@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 documento 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 documentac 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 documentació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 objetos 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 donde nombre-c1ase-complelamente-cualijicado proporciona un nombre no ambiguo de una clase de excepción definida en alguna otra parte y descripción (que puede ocupar las sigu ientes lineas) indica la razón por la que puede generarse este tipo concreto de excepción después de la ll amada al método. @deprecated Se utili za para indicar característi cas que han quedado obsoletas debido a la introducción de alguna otra característica mejorada. 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 advertenc 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 .*; / ** El primer programa de ejemplo del libro. * Muestra una cadena de caracteres y la fecha actual. * @au thor Bruce Eckel * @author www.MindView.net * @version 4.0 */ public class HelloDate / ** Punto de entrada a la clase y a la aplicación. * @param args matriz de argumentos de cadena 1< @throws exceptions No se generan excepciones */ public static vOld main(String[] args) System.out.println {"Hello, it ! s: 11 ) ; System.out.println(new Date ()) ; / * Ou tput: HelIo, it's: (5 5% match) 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 especial 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 comproba 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 correlació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 salida 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 primera 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úscula, 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 excesiva. 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 apertura 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 programa 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 mostraremos 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 valores 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 simplemente 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á utilizando 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 impriman 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 7: (1) Transfonme los fragmento s de código lncremenlable eo un programa funcional. Ejercicio 8: (3) Escriba un programa que demuestre que, independientemente de cuántos objetos se creen de una clase concreta, só lo hay Ejercicio 9: Ulla única instanc ia de un ca mpo static concreto defmido dentro de esa clase. (2) Escriba un programa que demuestre que el mecanismo automático de conversión de tipos funciona para todos los tipos primitivos y sus envoltorios. Ejercicio 10 : (2) Escriba un programa que im prima tres argumentos ex traídos de la línea de comandos. Para hacer esto, necesitará acceder con el índice correspondiente a la matriz de objetos String extraída de la linea de comandos. Ejercicio 11: (1) Transfom1e el ejemplo AIITheColorsOfTheRainbow en un programa que se pueda compi lar y ejecutar. Ejercicio 12: (2) Localice el código para la segunda versión de HelloDate.java, que es el ejemplo simple de documentación mediante comentarios. Ejecute Javadoc con ese archivo y co mpruebe los resultados con su explorado r web. Ejercicio 13: ( 1) Procese Documentationl.java, Documentalion2.java y Documentation3.java con Javadoc. Verifique 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 documentació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 En el nivel inferior, los datos en Java se manipulan utili zando 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 aquellos 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 conferencias, presentaciones, ejercicios y soluciones específicamente diseñados para familiarizarse con los fundamentos necesarios para aprender Java. Instrucciones simples de impresión En el capítulo anterior, ya hemos presentado la instrucc ión de impresión en Java: System . out. println ( "Un montón de texto que escribir " ) i 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 comprender 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.*; public class HelloDate { pubIic static void main (String [] args) print ( "He lIo, it' s: "); print(new Date()); l' Output, { (55 % match) HelIo, it's: Wed Oct 05 14:39:05 MDT 2005 * 111 , Los resultados son mucho más limpios. Observe la inserción de la palabra clave static en la segunda instrucción import. Para emplear esta biblioteca, es necesario descargar el paquete de código del libro desde www. MindView.net o desde alguno de los sitios espejo. Descomprima el árbol de código y añada el directorio raiz de dicho árbol de código a la va riable de entorno CLASSPATH de su computadora (más ade lante proporcionaremos una introducción completa a la variable de ruta CLASSPATH, pero es muy probable que el lector ya esté acostumbrado a lidiar con esa variable. De hecho, es una de las batallas más comunes que se presentan al intentar programar en Java). 44 Piensa en Java Aunque la utilización de net.rn indview.util.Pri nt simplifica en0n11emente la mayor parte del código, su uso no está justificado 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. Utilización de los operadores Java Un operador toma uno o más argumentos y genera un nuevo valor. Los argumentos se incluyen de forma distinta a las llamadas nonnales a métodos, pero el efecto es el mismo. La suma, el operador más unaria (+), la resta y el operador menos unario (-), la multiplicación (*), la división (f) y la asignación (=) funcionan de fonna bastante similar a cualquier otro lenguaje de programación. Todos los valores producen un valor a partir de sus operandos. Además, algunos operadores cambian el valor del operando, lo que se denomina efecto colateral. El uso más común de los operadores que modifican sus operandos consiste precisamente en crear ese efecto colateral, pero resulta conveniente pensar que el valor generado también está disponible para nuestro uso, al igual que sucede con los operadores que no tienen efectos colaterales. Casi todos los operadores funcionan únicamente con primitivas. Las excepciones son '=', '=' y ' != ', que funcionan con todos los objetos (y son una fuente de confusión con los objetos). Además, la clase St r in g soporta '+' y '+='. 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 ltiplicació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ícita. Por ejemplo, observe las instrucciones (1) y (2): 11 : operators/Precedence.java public class Precedence { public static void main(String[] int x l , Y = 2, Z = 3; int a = x + y - 2/2 + z¡ int b = x + (y - args) 2 )/(2 + z ); System.out.println("a + a + " b / / (1 ) (2) / / = " + b) i 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 rvalor. 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 existir 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 cualquier 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 modificació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 referencia, 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; public class Assignment { public static void main (String [] args ) Tank tI = new Tank () ¡ Tank t2 = new Tank {) i tl.level = 9¡ t2.level = 47¡ print ( "I: tI.level: + tI.level + t2.level: + t2 .level ) i tI = t2 ¡ print {"2: tI.level: + tI.level + t2.level: + t2 .level ) ¡ tI.level = 27 i print ( "3: tI.level: + tI.level + t2.level: + t2 .level ) i { 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á apuntada en el mismo objeto (la referencia original contenida en tI, que apuntaba al objeto que tenía un valor de 9, fue sobreescrita 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 objeto? 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 Creación de alias en las llamadas a métodos El fenómeno de la creación de alias también puede manifestarse cuando se pasa un objeto a un método: /1 : operators / PassObject . java // El paso de objetos a los métodos puede no ser /1 lo que cabria esperar. import static net.mindview.util.Princ.*¡ class Letter char c¡ publi c class PassOb j ect { static void f (Letter y ) y.e ::: 'z'; public static void main (5cring[] args ) { Lecter x = new Letter () ¡ x.e = 'a' i print ( "l: x.e: + x. e ) ; f Ix l ; print ( " 2: x.e: + x.e ) ; / * 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étodo, pero aquí, una vez más, lo que se está pasando es una referencia. por lo que la línea: lo que está haciendo es ca mbiar el objeto que está fuera de f( ). El fenómeno de creación de alias y su solución es un tema complejo del que se trata en uno de los su plementos en Línea di sponibles para este libro. Sin embargo, conviene que lo tenga presente desde ahora, con el fin de detectar posibles errores. Ejercicio 3 : ( 1) Cree una clase que contenga un va lor flo.! y utilícela para ilustrar e l fenómeno de la creación de alias durante las llamadas a métodos. 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 lenguaje (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. * ; pubIic cIass MathOps public static void main (String [) args ) { 3 Operadores 47 II Crea un generador de núme r os aleatorios con una cierta semilla : Random rand = new Random {47 ) ¡ int i, j, k; II Elegir valor entre 1 y 1 0 0: j = rand . nextInt ( lOO ) + 1; print {"j 11 + j )¡ k = rand.nextInt ( lO O) + 1, print ( "k + k) ; i = j + k; print {"j + k + i )j i = j - k; print ( lIj - k + i )¡ i = k I j i print ( "k I j + i ); i = k * j; print ( "k * j + i ); i = k % j; print ( "k % j + i ) i j %= k· print ( "j %= k : " + j ) ¡ II Pruebas con números en coma flotante: float u, v, W¡ II Se aplica también a los de doble precisión v = rand.nextFloat () i print ( "v "+ v ) ¡ W = rand . nextFloat () ¡ print ( "w : 11 + w); u = v + w¡ print ( "v + w + u) ; u = v - w¡ print ( "v - w + u) ¡ u = v * w; pri n t ( IIV * W + u); U = v I w¡ print ( "v I w "+ u ) ¡ II Lo siguiente también funciona para char, I I byte , short , int, long y double: U += V; print ( "u += V u - = v; print ( "u v u * = V; print ( "u *= v u 1= v; print ("u 1= v 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 v * w 0.47753322 0 .028358962 + u) ; + u)¡ + u); " + u) i 48 Piensa en Java v / U += w , V u v u *= v u /= v 9.940527 10.471473 9.940527 5.2778773 9 . 940527 *///> Para generar números, el programa crea en primer lugar un objeto Random . Si se crea un objeto Random sin ningún argumento, 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 semilla (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étodos nextln!( ) y nextFloa!( ) (también se pueden invocar nextLong() o nexIDouble( El argumento de nextIn!() establece 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. Operadores unarios más y menos El menos unario (-) y el más unario (+) son los mismos operadores que la suma y la resta binarias. El compilador deduce cuál es el uso que se le quiere dar al operador a partir de la fonna en que está escrita la expresión. Por ejemplo, la instrucción: x = -a¡ 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, aunque 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 equivalente 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 operador ++ aparece antes de la variable, mientras que post-incremento significa que el operador ++ aparece detrás de la variable. 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.*; public class Autolnc p u blic static void main (String [J args ) { int i = 1; print ( tli : + i I ; print ( H+ +i + ++i ) i // Pre-incremento print ( lIi++ + i++ } ; // Post-incremento print ( "i : + i ) ; print ( "- - i + - - i I ; // Pre-decremento print ( lIi-+ i - - ) i // Post-decremento print ( "i + i ) ; / * Output: i 1 ++i i ++ , 2 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 pOS1fija , 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 innecesarias, 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 opera 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 relacionales 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. Comprobación de la equivalencia de objetos Los operadores relacionales = y != también funcionan con todos los objetos, pero su significado suele confundir a los que comienzan a programar en Java. He aquí un ejemplo: JJ : operators J Equivalence.java public class Equivalence { public static void main (String[] args ) { Integer nI = new Integer (47 ) i Integer n2 = new Integer (47 ) i System. o ut . println (nl n2 ) ; System.out . println (nl != n2 ) i 50 Piensa en Java } / * 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 objetos 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 programadores. ¿Qué pasa si queremos comparar si el contenido de los objetos es equivalente? Entonces debemos utilizar el método especia 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 public class EqualsMethod { public static void main {String(] args ) { Integer nl = new Integer (47 ) ; Integer n2 = new Integer (47 ) ; System.out.println (nl.equals {n2 ) ¡ 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¡ pub l ic class EqualsMethod2 { public stati c void main {String(] args ) { Value v1 = new Value () ¡ Value v2 = new Value () ¡ v1.i = v2.i = 10 0 ; System.out.println (vl.equal s( v 2) ¡ 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 obtendremos 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 ahorrar 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á ndose 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. * ¡ public class Bool { public static void ma in (String[] args ) { Random rand = new Random (47 ) ¡ int i = rand.next lnt (lOQ l i int j = rand,nextlnt ( l OO); print ( " i + i) ; print ( "j + j) ; print ( " i > j is " + (i > j) ) ; print( " i < is " + (i < j) ) ; print( " i >= j is + (i >= j) ) ; print( " i <= j is + (i <= j) ) ; j) ) ; print("i j is + (i print("i != j is + (i != j) ) ; // Tratar int como boolean no es legal en Java: // ! print ( ti i && j is + ( i && j) ) ; //! print (" i 11 j is " + (i 11 j) ) ; II ! print ( "!i is " + ti ) i print( " (i < la) && (j < la) is • ((i < 10) && (j < 10)) ); print( " (i < 10) 11 ( j < 10) is + ((i < 10) 11 (j < 10)) ) ; / 0 Output : i 58 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 j 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 ejemplo 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 lugares 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. * ; public class ShortCircuit { static boolean testl(int val) print("testl(" + val + ") " ) i print (" resu lt: " + (val < 1)) return val < 1; i static boolean test2 (in t val) { print (tttest2(" + val + ")"); print( tt result: 11 + (va l < 2)); return val < 2' static boolean test3 (int val) print ( " test3 (" + val + ti),,} ¡ print ( "result: I! + (val < 3}); return val < 3¡ public static void main(String[] args} boolean b = testl(O} && test2(2) && test3 (2); print ("expression is " + b); 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 pruebas produce un res ultado tr ue, por lo que continúa con la evaluación de la expresión. Sin embargo, la segunda prueba produce 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.*¡ public class Literals { public static void main (String [] args ) { int i l = Ox2f¡ /1 Hexadecimal (minúscula ) print ( "i!: " + Integer.toBinaryString ( il )) i int i2 = OX2F¡ / 1 Hexadecimal (mayúscula) print (" i2: 11 + Integer. toBinaryString (i2) ) ; int i3 = 0177; j i Octal (cero inicial) print ( It i3: " + Integer. toBinaryString (i3 l l ; char c = Oxffff¡ II máximo valor hex para char print {"c: 11 + Integer.toBinaryString (c )) ; byte b = Ox7f; II máximo valor hex para byte print ("b: 11 + Integer. toBinaryString (b) ) ; short s = Ox7fff¡ II máximo valor hex para short print { liS: 11 + Integer.toBinaryString {s )l ; long nI 200L¡ II sufijo long long n2 = 2001; II sufijo long (pero puede ser confuso) long n3 = 200; float f1 1; float f2 = IF ¡ /1 sufijo float float f3 = lf¡ II sufijo float double dI = Id; II sufijo double double d2 = ID; II sufijo double 1I (Hex y Octal también funcionan con long) / * 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 significa 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, 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 imo 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. O OX 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". public class Exponents { public static void main(String[] args) JI 'e' en mayúscula o minúscula funcionan igual: float expFloat : 1.3ge-43f¡ expFloat = 1.39E-43f¡ System.out.println(expFloac) ; double expDouble = 47e47ct¡ // 'd' es opcional double expDouble2 = 47e47; // automáticamente double System.out.println(expDouble) i 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 aproximadamente 2,718 (en Java hay disponible un va lor double más preciso, que es Malb .E). Esto se usa en expresiones de exponenciació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 ponencial 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 exponencial 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" simplemente 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 exponencial. 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 bit a bit Los operadores bit a bit penniten manipular bits individuales en un tipo de datos entero primitivo. Para generar el resultado. los operadores bit a bit realizan operaciones de álgebra booleana con los bits correspondientes de los dos argumentos. Los operadores bit a bit proceden de la orientación a bajo nivel del lenguaje e, en el que a menudo se manipula el hardware directamente y es preciso configu rar los bits de los registros hardware. Ja va se diseii.ó originalmente para integrarlo en cod ificadores para te levisión, por lo que esta orientación a bajo nivel seguía teniendo sentido. Sin embargo, lo más probable es que no utilicemos demasiado esos ope radores bit a bit en nuestros programas. El operador bit a bit ANO (& ) genera un uno en el bit de salida si ambos bits de ent rada son iguales a uno; en caso contrario, genera un cero. El operador OR bit a bit (1) genera un uno en el bit de salida si alguno de los bits de entrada es un uno y genera cero sólo si ambos bits de entrada son cero. El operador bit a bit EXCLUS IVE OR o XOR (A) genera un uno en el bit de sal ida si uno de los dos bits de entrada es un uno pero no ambos. El ope rador bit a bit NOT (-. también denominado operador de complemento a lII/O) es lIn operador unario, que sólo adm ite un argumento (todos los demás operadores bit a bit son operadores binarios). El operado r bit a bit NOT genera el opuesto al bit de entrada, es uno si el bit de entrada es cero y es cero si el bit de entrada es uno. Los operado res bit a bit y los operadores lógicos utili zan los mismos caracteres. por lo que resulta útil recurrir a un tmco mnemónico para recordar cuál es el signi fi cado correcto. Como los bits son "pequeños" sólo se utiliza un ca rácter en los operadores bit a bil. Los operadores bit a bit pueden combinarse con el signo = para unir la operación y la asignación: & =. do res legítimos (puesto que - es un operador unari o. no se puede combinar con el signo =). 1= y A= son opera- El tipo boolean se trata como un va lor de un único bit, por lo que es algo distinto de los otros tipos primitivos. Se puede realizar un a operación AN O, OR o XOR bit a bit. pero no se puede real izar una operación NOT bit a bit (presumiblemente, para evi tar la confusión co n la ope ración lógica NOT). Para los va lores booleanos, los operadores bit a bit tienen el mismo efec to que los operadores lógicos, salvo porque no se aplica la regla de cortocircuito. As imismo, las operaciones bit a bit con valores booleanos incluyen un operador lógico XOR que no fomla parte de la lista de operadores " lógicos". No se puede n emplear valores booleanos en expresiones de desplazamiento, las cuales vamos a describir a continuación. Ejercicio 10: (3) Escriba un programa con dos va lores constantes, uno en el que haya un os y ceros binarios alternados, con un cero en el dígito menos significati vo, y el segundo con un va lor también alternado pero con un tino en el dígito menos significati vo (consejo: lo más fácil es usar constantes hexadeci mal es para es to). Tome estos dos valores y combínelos de ladas las fonnas posibles utilizando los operadores bit a bit, y visualice los resultados uti li za nd o Integer.toBin.ryStr ing(). 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 operado 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 positivo, 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: independientemente 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 desplazamientos 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 desplazado 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.*¡ public class URShift public sta tic void main(String[] args) int i = - 1; print(Integer.toBinaryString(i)) ; i »>= 10 i print(Integer.toBinaryString(i)) ; long 1 = -1; print(Long.toBinaryString{l)) ; 1 »>0: la; print(Long.toBinaryString(l)) ; short s = - 1; print(Integer.toBinaryString(s)) ; s :>:»= la; print(Integer.toBinaryString(s)) ; byte b = -1, print(Integer . toBinaryString(b)) ; b »>= 10; print(Integer.toBinaryString(b)) ; b = -1 i print(Integer.toBinaryString(b)) ; print(Integer.toBinaryString(b»>10)) ; 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éndose 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.*; public class BitManipulation ( public static void main(String[] args) Random rand = new Random (47 ); int i = rand.nex tlnt(); int j = rand.nextlnt(); printBinarylnt ( "-1", -1) i printBinarylnt ( " +1!l, +1) i { 3 Operadores 57 int maxpos = 2147483647; printBinarylnt ("maxpos", maxpos) i int maxneg = -2147483648; printBinarylnt ("maxneg", maxneg); printBinarylnt (11 i 11, i ) ; printBinarylnt (11_1 ", -i) i printBinarylnt (11 - i", - i) i printBinarylnt (" j", j) i printBinarylnt (11 i & j", i & j) i printBinarylnt (" i j ", i I j); printBinarylnt (" i A j" I i A j); printBinarylnt("i c< S", i ce 5); printBinarylnt("i » 5", i » 5) j printBinarylnt("(-i} » 5 " , (-i) » 5 ) ; printBinarylnt("i »> printBinarylnt ( " {-i} long 1 = 5", i »> 5", »> 5); (-i) »> S); 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 mi; m" 1 , 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)¡ static void printBinaryInt(String s, int i) print (s + 11, int: 11 + i + ", binary: \n Integer.toBinaryString(i») ¡ static void printBinaryLong(String s, long 1) print (s + It, long: It + 1 + 11, binary: \n Long. toBinaryString (1l ) ¡ /* Output: int: -1, binary: 11111111111111111111111111111111 +1, int: 1, binary: - 1, 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 -i, int: 1172028779, binary: 1000101110110111011110101101011 j, i i i int: 1717241110, binary: 1100110010110110000010 1 00010110 & j, int: 570425364, binary: 100010000000000000000000010100 I j, int: -25213033, binary: 11111110011111110100011110010111 j, " int: -595638397, binary: 110 111 0001111111010001 111 0000011 i 5, « int: 11 49784736, binary: 1000100100010000101001010100000 i » 5, int: -36625900, binary: 111111 011101000 1 00 1 0001000010100 (-i) » S, int : 36625899, binary: 1 00010 111011011101111 01011 i »> 5, int: 97591828, binary: 1011101 0001001000 1 0000 101 00 (-i) »> 5, int: 36625899, binary: 10001011101101110111101011 Los dos métodos del final , printBinarylnt() y printBinaryLong( j, toman un valor int o long, respectivamente, y lo imprimen en formato binario junto con una cadena de caracteres descriptiva. Además de demostrar el efecto de todos los operadores 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 negati 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. Operador ternario if-else El operador ternario, también llamado operador condicional resulta inusual porque tiene tres operandos. Realmente se trata de un operador, porque genera un valor a diferencia de la instrucción if-else ordinaria, que veremos en la siguiente sección del capítulo. La expresión tiene la fomla: exp-booleana ? valorO : valor1 Si exp-booleana se evalúa como true, se evalúa valorO y el resultado será el valor generado por el operador. Si expbooleana es false , se evalúa valorl y su resultado pasará a ser el valor generado por el operador. Por supuesto, podría utilizarse una instrucción if-else ordinaria (que se describe más adelante), pero el operador ternario es mucho más compacto. Aunque e (donde se originó este operador) se enorgullece de ser un lenguaje compacto, y el operador ternario puede que se baya introducido en parte por razones de eficiencia, conviene tener cuidado a la hora de emplearlo de forma cotidiana, ya que el código resultante puede llegar a ser poco legible. El operador condicional es diferente de if-else porque genera un valor. He aquí un ejemplo donde se comparan ambas estructuras: 3 Operadores 59 JI : operators / TernarylfElse.java import static net.mindview.util.Print.*¡ public class TernarylfElse { static int ternary(int i) { return i < 10 ? i * 100 i * 10i static int standardlfElse ( int i) if(i < 10) return i * 100 i else return i * 10; public static void main{String[] args) print(ternary{9)) j 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 versió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 operador ternario; nOffilalmente, puede convenir utilizarlo cuando se quiera configurar una variable con uno de dos valores posibles. Operadores + y += para String Existe un uso especial de un operador en Java: los operadores + y += pueden usarse para concatenar cadenas, corno ya hemos visto. Parece un uso bastante natural de estos operadores, aún cuando no encaje demasiado bien con la fonna tradicional en que dichos operadores se emplean. Esta capacidad le pareció adecuada a los diseñadores de C++, por lo que se añadió a C++ un mecanismo de sobrecarga de e++ pudieran añadir nue vos significados casi a cualquier operador. Lamentablemente, la sobrecarga de operadores combinada con alguna de las otras restricciones de C++, resulta una característica excesivamente complicada para que los programadores la incluyan en el diseño de sus clases. Aunque la sobrecarga de operadores habría sido mucho más fácil de implementar en Java de lo que lo era en C++ (como se ha demostrado en el lenguaje C#, que sí que dispone de un sencillo mecanismo de sobrecarga de operadores), se consideraba que esta característica seguía siendo demasiado compleja, por lo que los programadores Java no pueden implementar sus propios operadores sobrecargados, a diferencia de los programadores de C++ y C#. operadores para que los programadores El uso de los operadores para valores String presenta ciertos comportamientos interesantes. Si una expresión comienza con un valor String, todos los operandos que siguen también tendrán que ser cadenas de caracteres (recuerde que el compilador transforma automáticamente a String lada secuencia de caracteres encerrada entre comillas dobles). 11: ope rators /Stri ngOperators.java import static net.mindview.util.Print . *; public class StringOperators { public static void main{String[] args) int x ~ 0, y = 1, Z = 2; String s = II X , y, Z "¡ print{s + x + y + z ); { 60 Piensa en Java print (x + " " + s); / / Convierte x a String s += It (summed ) = "i / / Operador de concatenación print (s + (x + y + z »); print ( "" + x ) ; / / Abreviatura de Integer . toString () / * Output: x , y, O z 012 x, y, x, y, z 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 estuvieran sumando los valores enteros. Esto es porque el compilador Java convierte x, y y z a su representación String y concatena 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 evaluació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). Errores comunes a la hora de utilizar operadores Uno de los errores que se pueden producir a la hora de emplear operadores es el de tratar de no incluir los paréntesis cuando no se está del todo seguro acerca de la fonna que se evaluará una expresión. Esto, que vale para muchos lenguajes también es cierto para Java. Un error extremadamente común en while Ix = y ) II oo e y e++ seria el siguiente: { •• 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 reali zará ninguna conversión a partir de un valor int, por lo que dará un error en tiempo de compilación y detectará el problema 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áticamente 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 convertir, como en el siguiente ejemplo: 3 Operadores 61 // : operators / Casting.java public class Casting public sta tic void main (String [] args ) { int i = 200; long 1n9 = (10ng ) i; 1n9 = i¡ JI "Ensanchamiento," por lo que no se requiere conversión long lng2 = ( long ) 2 00 ; lng2 = 20 0 ; l/ Una "conversión de estrechamiento ! l . i = (int ) lng2; // Proyección requerida ) /// > Como podemos ver, resulta posible aplicar una proyección de tipo tanto a los valores numéricos como a las variables. Observe que se pueden también introducir proyecciones superfluas, por ejemplo, el compilador promocionará automáticamente un valor ¡ut a long cuando sea necesario. Sin embargo, podemos utili zar esas proyecciones superfluas con el fin de resaltar la operación o de clarificar el código. En otras situaciones, puede que la proyección de tipo sea esencial para que el código llegue a compilarse. En C y C++, las operaciones de proyección de tipos pueden provocar algunos dolores de cabeza. En Java, la proyección de tipos resulta siempre segura, con la excepción de que, cuando se realiza una de las denominadas conversiones de estrechamiento (es decir, cuando se pasa de un tipo de datos que puede albergar más infomlación a otro que no permite albergar tanta), se COITe el riesgo de perder infonnación. En estos casos, el compilador nos obliga a emplear una proyección, como diciéndo nos: "Esta conversión puede ser peligrosa, si quieres que lo haga de todos modos, haz que esa proyección sea explícita". Con una conversión de ensanchamiento, no hace falta una proyección explícita, porque el nuevo tipo pennitirá albergar con creces la infonnación del tipo anterior, de modo que nunca se puede perder infonnación. Java pennite proyectar cualquier tipo primitivo a cualquier otro, excepto en el caso de boolean, que no pennite efechlar ningún tipo de proyección. Los tipos de clase tampoco penniten efectuar proyecciones: para converti r uno de estos tipos en otro, deben existir métodos especiales (posterionnente, veremos que los objetos pueden proyectarse dentro de una/ami/ia de tipos; un Olmo puede proyectarse sobre un Árbol y viceversa, pero no sobre un tipo externo como pueda ser Roca). Truncamiento y redondeo Cuando se realizan conversiones de estrechamiento, es necesario presta r atención a los problemas de truncamiento y redondeo. 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.*¡ public class CastingNumbers { public st atic void main (String[] args ) double aboye = 0.7, below = 0 .4; float fabove = D.7f, fbelow = D.4f¡ print ( " (int l above: " + (int ) above ) ; print {" (int l below: " + í int ) below ) i print ( II (int l fabove: + (int ) fabove ) ; print ( " (int l fbelow: " + (int ) fbelow ) ¡ 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 correspondiente 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 .*; public class RoundingNumbers { public static void main(String[) args) double aboye = 0 . 7, below = 0 .4 ; float fabove = 0 . 7f, fbelow = 0 .4f; print {"Math. round (aboye ) : ti + Math .round{above»; print ( "Math. round (below) : " + Math.round {below»; print("Math.round {fabove ) : + Math.round(fabove» i + Math. round (fbelow»; print ("Math.round{fbelow) : 11 1* Output: Math . round (above) : Math. round (be low) : Math. round (fab ove) Ma th. round (fbelow ) 1 O : 1 : 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 primitivos 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 resultado 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. Java no tiene operador "sizeof" En C y C++, el operador sizeof() nos dice el número de bytes asignado a un elemento de datos. La razón más importante para el uso de sizeof( ) en C y C++ es la portabilidad. Los diferentes tipos de datos pueden tener di fe rentes tamaños en distintas máq uinas, por lo que el programador debe averiguar el tamaño de esos tipos a la hora de realizar operaciones que sean sensibles al tamaño. Por ejemplo, una computadora puede almacenar los enteros en 32 bits, mientras que otras podrían almacenarlos en 16 bits. Los programas podrían, así, almacenar valores de mayor tamaño en variables de tipo entero en la primera máquina. Como puede imaginarse, la portabilidad es un ve rdadero quebradero de cabeza para los programadores de C y C++. Java no necesita un operador sizeof( ) para este propósito, porque todos los tipos de datos tienen el mismo tamaño en todas las máquinas. No es necesario qu e tengamos en cuenta la portabilidad en este nivel, ya que esa portabilidad fanna parte del propio diseño del lenguaj e. 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 /1 para mostrar cuáles son aceptables por el compilador Java. public class AIIOps { 1/ Para aceptar los resultados de un test booleano: void f (boolean b) {} void boolTest(boolean x, boo!ean y) // Operadores aritméticos: //! x x * Yi x j y; x%y; jj! x jj! x //!x X+Yi ji! x x / /! y¡ X++; jj! x--; I/! x = +y; jj! x = -y; 1/ Relacionales y lógicos: jj! f (x > y); j j! f (x >= y); jj! f(x < y); j j! f (x <= y); f (x y); f (x ! = y); f (!y); x = x && y¡ x = x 1/ j j! x x x jj! jj! jj! 11 y; Operadores bit a bit: x x x x x x x -y; & y; y; y; x « x » X >>> 1; 1; 1; // Asignación compuesta: jj ! x += y; -= y; x *= y; jj! x j= y; jj! x %= y; JI! x «= 1; JI! x »= 1; I/! x »>= 1; x &= y¡ x "= y¡ x 1= y; l/Proyección: //! char e = (char) x; jj! byte b = (byte ) x; I/! short s = (short ) x; jI! int i = ( int ) x¡ jj! long 1 = (long )x ; jj! float f = (float)x; jj! double d = (doubl e )x; jj! x JI! void charTest(char x, char y) /1 Operadores aritméticos: x (c har ) (x * y); x = (char) (x j y); 64 Piensa en Java (char) (x % y); x x (char) (x x (char) ( x + y); 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 (char) -y ¡ x = (char) {x a bit: x= & y}; x x = x x x (char ) (x « 1); (char) (x » 1) ¡ (char) (x »> 1); Asignación compuesta: II X (char) ( x 1 y); (char) (x • y); += y; X y; X *= y; x 1= x x x y; %= y; «= 1 ; x »= 1 ; »>= 1 ; &= y; X A= x y¡ 1= y; II Proyección: II! boolean bl x byte b = = (boolean}x; (byte)x; short s = (short)x¡ int i = {int}x¡ long 1 = (long)x¡ float f = (float)x¡ double d = (double)x¡ void byteTest{byte x, byte y) II Operadores aritméticos: x (byte) (x * x (byte) (x x (byte) (x % y); x (byte) (x + y) ; x (byte) (x y); I y); y) ; x++; X--¡ x = x = II (byte) + y; (byte ) - y; Relacionales y lógicos: f (x > y); 3 Operadores 65 f (x f (x f (x f (x f (x // ! //! // ! >; y ) ; < y) ; <= y ) ; -- y ) ; != 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 x ( byte ) ( x ( byte ) ( x • x ( byte ) ( x « 1) i x ( byte ) (x » 1) i ( byte ) (x »> 1 ) i x JI y) ; y) ; Asignación compuesta: x += y; y; *= y ; x /; y ; X X 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 dauble d ( fleat ) X i = (double)x; void shortTest (short x, short y ) { JI Operadores aritméticos: x (s hort ) (x * y ) ; x x ( short ) ( x ( short ) ( x x (short ) (x % y); + y) ; / x ( short) ( x y); y ) ; x ++ ; x--¡ x x = = JI (short)+y¡ (short}-y ; Relacionales y lógicos : f (x > f (x >= f (x < y) ; y) ; y) ; f ( x <= y ) ; f ( x ;= y ) ; f ( x != y); / / ! f (!x); // ! f (x && y ) ; // ! f (x 11 y); JI Operadores bit a bit : 66 Piensa en Java x x x x (short)-y¡ Is hort ) Ix & y); Ishort) Ix y) ; Is hort ) Ix y ); 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 A 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; void intTest (int x, int y ) { II Operadores aritméticos: x x * y¡ x x 1 y; x x % y; x x + y; x x y; x++¡ x - -¡ x x +Yi = -Y¡ Relacionales y lógicos: = II f lx f lx flx f lx f lx f Ix II ! II ! 11 ' II x x x x x x x II > y); y) ; y) ; <= y ) ; -- y ) ; >= < ! = y ); f I! x ) ; f Ix && y ) ; f Ix 11 y); Operadores bit a bit: -y¡ x & y; x 1 y; x y; x « 1; x » 1; x»> 1 i Asignación compuesta: X + = Yi 3 Operadores 67 x y; x *= y; x / = y; x %= y; x «= x :»= 1; 1i X »>= 1; x &= Yi x "'= Yi x 1= y; // Proyección: (boolean ) x¡ // ! boolean bl char e = {char ) x¡ = byte b (byte ) x; short s = (short ) x¡ long 1 = ( long ) x¡ float f (float ) x; = double d = {double )x¡ void longTest ( long x, long y) 1/ Operadores aritméticos: x x x x x x x x x x * Yi / y; % y; + y; - y; X++¡ x--¡ x = +Yi X = -Vi / 1 Relacionales y lógicos: f f f f f f / (x > y); (x >= (x < y ) ; y ) i (x <= y ) ; (x == (x / ! //! // ! y ) ; != y ) ; f ( !x ) ; f (x && y ) ; f (x 1 1 y ) ; JI Operadores b i t a bit: - Yi x & y; x y; x y; x « 1; x » 1; x x»:> 1; /1 Asignación compuesta: X += y; X y; X *= y; x / = y; x %= y; x x x x x x x «= 1¡ x »= 1; x »>= 1; x &= Yi x "= Yi 68 Piensa en Java 1, y; x II Proyección: II! boolean bl char c = = (boolean)x ; (char)x¡ byte b ' (byt e l x; short s = (short) x¡ int i = (int) x¡ float f = (float ) x¡ double d = {double)x¡ void floatTest ( float X, float y) II Operadores aritméticos: X X * y¡ x I y; x % y; x + y; x x x x x - y; x++¡ x--; X = x = II f f f f f +y¡ -y ¡ Relacionales y lógicos: (x > y) ¡ (x >, yl; (x < yl; Ix <, yl; Ix " yl; f (x !, yl; II! f (! x l ; II! II! II f (x && yl; f (x 1 1 yl; Operadores bit a bit: x -y; II! II! x II! x II! x x & y; x 1 y; y; x II! x x« 1; II ! x x» 1; II! x x»> 1; II Asignación compuesta: x += x - y¡ y¡ x *= y¡ x 1, y; x %= y¡ II! x «= 1¡ II! x »= 1; II! x »>= 1 ¡ II ! x &, y; II! x " y; II!xl,y; II II! Proyección ; boolean bl = (boolean )x¡ char c = (cha r ) x¡ byte b = (byte l x; short s = (short)x¡ int i = (in t )x ¡ long 1 double d (longl x; (double l x; = { 3 Operadores 69 void doubleTesc {double x, double y ) { // Operadores aritméticos: x x * Yi x x I y; x x % y¡ x x + y; x x y; X++¡ x--; x = +Yi = X -y; /1 Relacionales y lógicos: f Ix > y l ; f Ix >= y l ; f Ix < y l ; Ix Ix Ix yl; yl ; != y l ; II ! f I 'x l ; II ! f Ix && y l ; I I ! f Ix 1 1 yl; / / Operadores bit a bit: 11 ' x -y; f f f <= == // ! x X&Yi II ! x II ! x x x y; 1; x:>:> 1; x»> 1; // ! x j i! 1 y; x« x ji ! x /1 Asignación compuesta: x +"" y; X Yi x *= Yi x / = y; %= y¡ x « = 1; ji ! x »= 1; // ! x »>= 1; x JI ! II ! x II ! x II ! x y; '= y; &= 1= y; Proyecc ión: //1 boolean bl JI = {boo l ean ) x; c har e = (char ) x; byte b = Ibyte l x; short s = (short ) x; int i = ( int ) x; long 1 float f Ilong l x; = = ( float ) Xi ) 111 ,Observe que boolean es bastante limitado. A una variable de este tipo se le pueden asignar los valores true y false, y se puede comprobar si el va lor es verdadero o falso, pero no se pueden sumar va lores booleanos ni realizar ningún otro tipo de operación con ellos. En char, byte y short, puede ver el efecto de la promoción con los operadores ari tméticos. Toda operación aritmética sobre cualquiera de estos tipos genera un resultado int, que después debe se r proyectado explícitamente al tipo original (una conve rsión de estrechamiento que puede perder información) para realizar la asignación a dicho tipo. Sin embargo, con los va lo- 70 Piensa en Java 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 resultado, como se ilustra en el siguiente ejemplo: JJ : operatorsJOverflow . java JJ ¡Sorpresa! Java permite los desbordamientos. public class Overflow { public static void main(String(} args ) int big = Integer.MAX_VALUE¡ System . out . println("big = " + big) ¡ int bigger = big * 4¡ System.out.println ( lIbigger = 11 + bigger ) ¡ 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 tiempo 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 primitivo. 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, utilizando 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á disponible 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 lenguajes. En Java, las palabras clave incluyen if-else, while, do-whUe, for, return, break y una instrucción de selección denominada 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 permite 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 ejemplo 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 equivalente a un número objetivo: 72 Piensa en Java //: control/lfElse.java import static net.mindview.util.Print.*; public class IfElse { static int result = o; static void test (int testval, int target) if(testval > target) { result = +1; else if(testval < target) result -1; else result O; /1 Coincidencia public sta tic void main (String [] args) { test(lO,5); print(result) ; test(5,10); print(result) ; test(S,5); print (result) ; /* 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 instnacció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 instrucciones 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 iteració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. public class WhileTest { static boolean condition( ) boolean result = Math.random( ) < 0.99; System.out.print(result + tI, n); return result; public static void main(String[] args) { while(condition()) System.out.println(nInside 'while' " ); System. out. println (U Exited 'while''') i J* (Ejecútelo para ver la salida) *JJJ :- 4 Control de ejecución 73 El metodo condition() utiliza el método random( ) de tipo sta tic de la biblioteca Math, que genera un valor double comprendido entre O y 1 (incluye 0, pero no l.) El valor result proviene del operador de comparación <, que genera un resultado 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 primera 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 comprueba 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. public class ListCharacters { public statie void main(String[] args) { for{char c = O; C e 128; c++) if(Character.isLowerCase(c) ) System. out. println ("value: " + (int) c + character: 11 + el i /, 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 correspondiente 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 dentro 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 modo, 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í mismos 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 definiciones 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 public class CommaOperator { public static void main (String [] args) { for(int i = 1, j = i + la; i < 5; 1++, j System.out.println("i = n + i + I! j :: 1* i i i i = U i * 2) + j) i Output: 1 j 11 2 j 4 3 j 6 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 recuento 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 public class ForEachFloat public static void main(String(] Random rand args) { = new Random(47)¡ float f [] far (int i = new float [la] ; = O; i < 10 ; i+ + } f[i] = rand.nextFloat() i for ( float x f) System.out.println(x) 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 foreach en la Iínca: f or (float x fOf , porque debe accederse a ella mediante un índice. Puede ver la sintaxis : 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 public c!ass ForEachString { public static void mai n (S tring[] argsJ for (char e : "An African Swallow". toCharArray () System.out.print(c + " " ); 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 objeto 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áticamente 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 public class ForEachInt { public static void main (S tring [] for {int , range(10)) printnb (i + 11 " ) ; print () i args) { // 0 .. 9 i for {int i , range {5, printnb (i + 11 " ) ; print () i for (int i : range(5, printnb(i + " "); print{)¡ 10)) 20, // 5 .. 9 3)) II 5 . . 20 step 3 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 realizan 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 comunica 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( ) precedente para aprovechar esta característica: 1/ : control/lfElse2.java import static net.mindview.util.Print.*¡ public class IfElse2 { static int test (int testval, int target) if(testval > target } return +1; else if{testval < target ) return -1; el se return O¡ 1I Coincidencia { 4 Control de ejecución 77 p u b l ic sta t ic void main (String[] args l { print ( test (1 0 , S I 1 ; print ( test (S, 1 0 » ; print ( test ( 5,5 )) ; / * 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 comprendido 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 continue. break provoca la saLida del bucle sin ejecutar el resto de la instmcciones. La instrucción continue detiene la ejecució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 .* ; public class BreakAndContinue { public static void main (String[ ] args ) { f or {int i = O; i < 10 0 ; i++ ) { if (i == 74 ) break; // Fuera del bucle if {i % 9 1= O) c o ntinu e; // Siguiente itera c i ó n System. o ut . print (i + " 11 ) ; 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 " ) ; System . out . print ln () ; int i = O; / / Un "bucle infinito ll 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 o 9 18 27 36 45 54 63 72 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 terminació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. La despreciada instrucción "goto" La palabra clave goto ha estado presente en muchos lenguajes de programación desde el principio de la Infomlática. De hecho, goto representó la génesis de las técnicas de control de programa en los lenguajes ensambladores: "Si se cumple la condición A, salta aquí; en caso contrario, salta allí". Si leemos el código ensamblador generado por casi todos los compiladores, podremos ver que el control de programa contiene muchos saltos (el compi lado r Java produce su propio "código ensamblador", pero este código es ejec utado por la máquina virtual Java en lugar de ejecutarse directamente sobre un procesador hardware). Una instmcción goto es un salto en el ni ve l de código fuente, yeso es lo que hizo que adquiriera una mala reputación. Si un programa va a saltar de un punto a otro, ¿no ex iste alguna fonna de reorganizar el código para que el flujo de control no tenga que dar saltos? La instrucción goto llegó a ser verdaderamente puesta en cuestión con la publicación de l famoso artículo "GOlo considered harmfuf' de Edsger Dijkstra, y desde entonces la caza del goto se ha convertido en un deporte Illuy popular, forzando a los defensores de esa instrucción a ocultarse cuidadosamente. Como suele suceder en casos como éste, la verdad está en el punto medio. El problema no está en el uso de goto, sino en su abuso, en detenninadas situaciones especiales goto representa. de hecho, la mejor fonna de estmcturar el flujo . Aunque goto es una palabra reservada en Java, no se utiliza en el lenguaje. Java no dispone de ninguna instrucción goto. Sin embargo, sí que dispone de algo que se asemeja a un salto, y que está integrado dentro de las palabras clave break y con tinue. No es un salto, sino más bien una fornla salir de una instrucción de iteración. La razón por la que a menudo se asocia este mecanismo con las discusiones relativas a la instrucción go to es porque utili za la misma técnica: una etiqueta. Una etiqueta es un identificador seguido de un carácter de dos puntos, como se muestra aquí: labell , 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 hablaremos 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 ) / / .. JI continue labell¡ (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 iteració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 finalizado. 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. *¡ public class LabeledFor { public static void main(String[] args) int i = O; outer: II Aquí no se pueden incluir instrucciones for ( ; true i ) ( II bucle infinito inner: II Aquí no se pueden incluir instrucciones for ( ¡ i < 10; i++) { print ( " i = " + i ) ; if Ii == 2) { print ( "continue " ); continue¡ } if I i == 3) ( print ( "break" ) ; i++; II En caso contrario, i nunca II se incrementa. break; 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; for (int k if l k = O; k < 5; k++ } { == 31 { print ( " continue inner 11 ) continue inner; ; 80 Piensa en Java /1 Aquí no se puede ejecutar break o continue para saltar a etiquetas 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 pasada 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 incremento, 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. * ¡ public class LabeledWhile { public static void main{String[] int i = O; outer: while(true) print ( "Outer while loop"); ",hile (t rue ) { i++¡ print{lIi = " + i) ¡ if (i == 1) { print ( " continue") ; continue; ) if(i == 3) { print ( " continue outer") ¡ continue outer¡ if ( i == 5 ) print ("break 11 l ; break¡ if(i == 7) argsl { 4 Control de ejecución 81 print ("break oute r" ); break outer; 1* Output: Outer while loop i = 1 continue i = 2 i = 3 continue Quter Outer while loop i i = = 4 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 tuado 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 ejecutar 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 etiquetas, 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 correspondiente instmcción. switch La palabra clave switch a veces se denomina instrucción de selección. La instmcción switch permite seleccionar entre distintos fragmentos de código basándose en el valor de una expresión entera. Su fonna general es: switch(selector-entero) case valor-entero! case valor-entero2 case valor-entero3 case valor-entero4 case valor-enteroS // ... default: instrucción ; { instrucción¡ instrucción ¡ instrucción ¡ instrucción¡ instrucción ¡ break; break; break; break; break; 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últiples 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 instnlcció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 ocasiones 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 importante 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ítulo, 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 .* ; public class VowelsAndConsonants { public static void main(String[] args} Random rand = new Random(47) ¡ for(int i = O; i < 1 00; i++) int e = rand.nextInc(26) + 'a'; printnb ( (char) c + ", 11 + C + ": " ) ; switch(c) { case 'a': case 'e': case 'i': case ' o ': case ' u': print("vowel"l i break; case 'y': case 'w': print ("Sometimes a vowel"); break; defaul t: print (" consonant") ; / * Output : y, n, z, b, r, n, y, g, c, f, o, 121, Sometimes a vowel 11 0 , consonant 122, consonant 98, consonant 114: consonant 110, consonant 121 : Somecimes a vowel 103, consonant 99, consonanc 102, consonant 1110 vowel 4 Control de ejecución 83 w, 119: Sometimes a vowel z, 122: consonant , /// ,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 procesando 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 sucede. 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 lenguajes 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 siguiente capítulo cubrirá las importantes cuestiones de la inicialización y limpieza de objetos, a lo que seguirá, en el siguiente capirulo, 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 convertido 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 programador 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 tenninado de utilizar, ya que en ese momento deja de preocuparnos. Al no borrarlo. los recursos utilizados por ese elemento quedan 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 objeto. Java también adoptó el concepto de constructor y además di spone de un depurador de memoria que se encarga de liberar 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. Inicialización garantizada con el constructor Podemos imaginar fácilmente que sería sencillo crear un método denominado in iti a lize( ) para todas las clases que escribiéramos. El nomb re es una indicación de que es necesari o invocar el método antes de utilizar el objeto. Lamen tableme nte, esto indica que e l usuario debe recordar que hay que in voca r ese método. En Java, el diseñador de un a clase puede garantizar la inicialización de todos los objetos proporcionando un constructor. Si una clase tiene un construclOr, Java invoca automáticamente ese constructor cuando se crea un objeto, antes incluso de que los usuarios puedan llegar a utili zarlo. De este modo, la inicializac ión queda garantizada. La siguiente cuestión es cómo debemos nombrar a este método, y existen dos problemas a este respecto. El primero es que cualquier nombre que usemos podría colisionar con otro nombre que quisiéramos emplear como miembro de la c lase. El segundo problema es que debido a que el compilador es responsable de invocar e l constructor. debe siempre conocer qué método invocar. La solución en e++ parece la más fácil y lógica, de modo que también se usa en Java: el nombre del constructor coincide con e l nombre de la clase. De este modo, resulta fácil in vocar ese método automáticamente durant e la inicialización. He aquí una clase simple con un constructor: // : initializationfSimpleConstructor.java // Ilustración de un constructor simple. class Rock { Rock () { / f Éste es el constructor System.out.print(IIRock 11 ) j public class SimpleConstructor { public static void main (String[] args) for(int i '" O; i < 10¡ i++) { 86 Piensa en Java 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 objeto 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 + public class SimpleCanstructor2 { public sta tic void main (S tring[] for (int i = o¡ i < 8 i i++ ) new Rock2 ( i ) i 11 ") args) 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 ejemplo, 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 circunstancia 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 tenemos 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 compilador 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 referencia 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 quesea 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 designar una acción. Utilizamos nombres para referimos a todos los objetos y metodos. Una serie de nombres bien elegida crearú 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 cuando nos perdamos algunas de las palabras. No necesitamos identificadores unívocos: podemos deducir el significado a partir 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 nombre del constructor está predetemlinado por el nombre de la clase, sólo puede haber un nombre de constmctor. Pero entonces. ¿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 constructores, el constructor predeterminado y otro que tome un objeto String como argumento, a través del cual suministraremos 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 + void info(String s) print (s + ": Tree is 11 11 feet tall") ¡ + height + " feet tall") ¡ 88 Piensa en Java public class Overloading { public static void main {String [ ] args ) { for (int i = Di i < 5; i++ } { Tree t = new Tree {i ) ; t. info () ; t. info ( lIoverloaded methad" ) ; 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 comportamiento, 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 adicional, podemos emplear info(String), mientras que utilizaríamos info() cuando no tengamos nada más que decir. Sería bastante 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. Cómo se distingue entre métodos sobrecargados Si los métodos tienen el mismo nombre. ¿cómo puede saber Java a qué método nos estamos refiriendo? Existe una regla muy simple: cada método sobrecargado debe tener una lista distintiva de tipos de argumentos. Si pensamos en esta regla durante un momento, vernos que tiene bastantes sentido. ¿De qué otro modo podría un programador indicar la diferencia entre dos métodos que tienen el mismo nombre, si no es utilizando las diferencias entre los tipos de sus argumentos? Incluso las diferenc ias en la ordenación de los argumentos son suficientes para distinguir dos métodos entre sí, aunque normalmente no conviene emplear esta técnica, dado que produce códi go dificil de mantener: 1/ : initialization/OverloadingOrder.java 1/ Sobrecarga basada en el orden de los argumentos. import static net.mindview.util.Print.*¡ public class OverloadingOrder static void f(String S, int i ) print ( " String : " + S + int: u + i ) ¡ 5 Inicialización y limpieza 89 static void f (int i, String s) { print (" int: 11 + i + ", String: 11 + s); public static void main.(String [] f ("String first", 11) i f {99, !tlnt first") i args) { 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. Sobrecarga con primitivas Una primitiva puede ser automáticamente convertida desde un tipo de menor tamaño a otro de mayor tamaiio, y esto puede inducir a confusión cuando combinamos este mecanismo con el de sobrecarga. El siguiente ejemplo ilustra lo que sucede cuando se pasa una primitiva a un método sobrecargado: 11: initialization/PrimitiveOverloading.java II Promoción de primitivas y sobrecarga. import static net .mindview.util . Print. *¡ public void void void void void void void class PrimitiveOverloading { f1 (c har xl { printnb("f 1 (char) 11) i } f1 (byte x ) ( printnb("f1(byte) " ); } f1 (short x ) { printnb{"f1(short ) 11) i f1 (int x ) { printnb ( "f 1(int) ") ¡ } f1(long x ) ( printnb("fl(long) "); } f1 (fl oat x ) { printnb { "f1 (float ) ,, } i f1 (double x) { printnb ("fl (double) "); void void void void void void f2 (byte x) { printnb ( " f2 (byte ) " ); f2(short x l { printnb(Uf2(short) " }i f2 (int x) ( printnb("f2 (int) " ) ; } f2(long x) ( printnb("f2(long) "); } f2(float x ) { printnb(Uf2(float) ")i } f2(double x) (printnb("f2(double) "); void void void void void f3(short xl { printnb("f3(short) ")i f3(int x) { printnb( lt f3(int) tI)i } f3 (long x) ( printnb("f3 (long) " ); } f3 (f loat x l { printnb{ tl f3 (float ) 11); f3{double xl {printnb(lIf3{double) "), void void void void f4(int x ) { printnb ("f4(int ) ") i } f4 (long x) { printnb ( " f4 (long ) " ); f4 (float x l { printnb { fl f4 {float ) "); f4 (double x ) { printnb("f4 (double ) "), void fS(long x l { printnb ( lIfS (long) " ) ¡ } void f5 ( float x ) { printnb ( "f5 ( float ) " ); void fS{double x l { printnb (" fS (double l "), void f6{float x l { printnb ( "f6 (float) 11 ) i void f6(double x l { printnb( lI f6(double) ") void f7 {double x l { printnb("f7(doublel } i ") , 90 Piensa en Java 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 () ; public static void main (String [] args) PrimitiveOverloading p = new PrimitiveOverloading(); p.testConstVal() ; { p.testChar() ; p. testByte () ; p. testShort () ; p.testlnt() ; p. testLong () ; p. testFloat () i p. testDouble () ; / * 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 objeto ¡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.*; public void void void void void void void class Demotion { fl (char x) { print (Ufl {char} ji) ; fl (byte x) { print ("fl (byte)") ; fl (short x) { print (" fl (short) ") ; fl{int x) { print{ufl{int)")¡ } fl (long x) { print ( "fl (long)"); ) fl(float x) { print("fl(float) " ); fl (double x ) { print (ti fl (double) ti) ; void void void void void void f2 (char x ) { print {U f2 {char} U}; } f2 (byte x) { print ( " f2 (byte) " ) ; ) f2(short xl {print{ Uf2 {shortl " )¡ f2(int x) { print("f2(int)"); } f2 (long x) { print ( "f2 (long)"); ) f2(float x l { print("f2{floatl"); void void void void void f3 (char x) { print ( "f3 (charl U) ; } f3 (byte x) { print ( " O (byte ) " ); ) f3{short xl {print(Uf3{short}")¡ f3(int x) {print("O(int) " ) ; } f3 (long x) { print ("O (long)"); void void void void f4 (char xl print ( "f4 (char) 11); f4 (byte xl print ( "f4 (byte) 11 l ; f4(short xl {print ( Uf4 {short)U)¡ f4 (int xl { print{lIf4 ( int ) 11); } void f5(char xl { print{tlf5(char) It} ¡ void f5(byte x) { print("f5(byte) "); void f5(short x) { print("f5(short)II); void f6{char xl void f6 (byte xl print ("f6 {charl "); print {"f6 (byte) 11) ¡ void f7 (char xl print{l'f7(charl ti) ¡ 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) ; public static void main(String[] Demotion p = new Demotion()¡ p.testDouble() i 1* Output : double argument: args) { 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 mensaje de error. Sobrecarga de los valores de retorno Resulta bastante habitual hacerse la pregunta: "¿Por qué sólo tener en cuenta los nombres de clase y las listas de argumentos de los métodos? ¿Por qué no distinguir entre los métodos basándonos en sus valores de retorno?" Por ejemplo, los siguientes dos métodos, que tienen el mismo nombre y los mismos argumentos, pueden distinguirse fácilmente: void f () ( ) int f () ( return 1; ) Esto podría funcionar siempre y cuando el compilador pudiera detenninar inequívocamente el significado a partir del contexto, como por ejemplo en int x = f(). Sin embargo, el lenguaje nos permite invocar un método e ignorar el valor de retorno, 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 () public class Defau!tConstructo r pu blic stati c v o id main (Str i ng[ ] args l Bird b ~ new Bird () ; II Defau l t! ) / / / o- 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 embargo, 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 {} !."'L:bl ':"c class NoSynthes i s { public s : atic void main (String [J al-gs ) { / / ! Bird 2 b = n ew Bird2 () ; / / No h ay p rede termi nado Bird2 b2 ne w Bird 2(1) ; Bird2 b3 '" r.e \,o¡ Bird2 ( 1. O) ; / 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 quieres 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 argumentos (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 SI.? un objeto de esa clase en main( ) para veriticnr que sintetiza automa¡ieamente el constructor predetcnninado. La palabra clave this Si te nemo s dos objetos del mism o tip o llamados a y b. p0c! emo s preguntam os cómo es posible iIwocar un metodo pl'C'I( ) p,m-l ambos obje lOs (110fl.l: la palabra illgksa peel significa "pelar ulla fruta ". que en este ejemplo de programación es una banana ): ji : initialization/Bana na Peel . java class S a nana { vOld pee l l i nt i) (! * *1 ) public class BananaPe el { p u blic s:atic void main ( String [J Ba nana a al-gs ) { new Banana() I b = ne w Banana() i a . peel tll i = b . peel (2) ; ) I/! , - 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 objeto" , el compilador se encarga de reali zar un cierto trabajo entre bastidores por nosotros. Existe un primer argumento secreto 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 compilador 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étodos 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 necesario 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". public class Leaf { int i = O¡ Leaf increment() i++¡ return this; void print () System.out.println("i = " + i) ¡ public static void main(String[] args) Leaf x = new Leaf()¡ x. increment () . increment () . increment () . print () 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ácilmente 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 in : tialization j Pass:'ngThis . java 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 } pub li c class PassingThis { public static void ma in(String[] args) new Person {) . eat (ne w Apple () ) ; / * OU tput : Yummy * /// :- El objeto Apple (manzana) necesita invocar Peeler.peel( ), que es un metodo de utilidad ex temo que lleva a cabo una operació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 distintas 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 nte para ver cómo funciona el mecan ismo. no debe utilizar esta forma de invocar a los métodos en la práctica. Invocación de constructores desde otros constructores Ulla clas~. existen ocasiones en las que co nviene im'oear a un construc tor desde dentro de otro para no tener que duplicar el código. Podemos cfcewar este tipo de ll amadas Uli li za ndú la pa labrtl cla,'c thi s. CU:lmh) se escriben varios COllSlructor6 para Normalmente. cuando escribi1l1o~ this. es en el sentido de "este objeto" o el "objeto m:tual", y esa palabra cla\-e genera, por si misma. la referencia al objeto actual. Dentro de UIl COllst rlH:tor. la palabra clave this toma un significado distinto cuando se la proporciona una li sta dt:' argumentos: realiza una ll amada explícita al const ructor que se co rrespo nda con esa li sta de argumentos. De este modo, di sponemos de una f0n113 se ncilla de in\'ocar a NroS constmctores: 1/ : in':'tializat.ion/E='lower . java. JI Llamada a conSLructores con "chis" ':'mport static net.mindview.util.?rint.* public class FlOWe r { int petalCount : O; String s = " initial value"; Flower ( int petals ) { petalCount. = petals; Pl·int. ( " Construct.or wl int arg only, petaiCount: + petalCount ) i 96 Piensa en Java Flower (String ss ) { print ( "Constructor s :: ss; w/ String arg only, s " + ss ) ; Flower (String s, int petals ) //1 this (petals ) ; this(s); JI ¡NO podemos realizar dos invocaciones! this.s ~ Si ji Otro uso de "this" print (" String & int args 11) ; ) Flower () ( this ( "hi", 47 ) ; print ( "default constructor (no args) " ) ; void printPetalCount () // ! this (11 ) ; { JI ¡NO dentro de un no-constructor! print ( lIpetalCount = " + petalCount + s = "+ s ) ; public static void main (String{] args ) Flower x = new Flower () ; x.printPetalCount {) i / * 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 podemos 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 numerosos 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étodo 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 propia clase, sin especificar ningún objeto. De hecho, esa es la principal aplicación de los métodos static: es como si estuviéramos 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étodo 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 arg,U1l10;;'1l10 correcto. y si So;;' encll~llIra al guna \t.~Z lIlili 7 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 tambié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.*; II II Cuando se invoca el constructor para crear un obje to Window, aparecerá el mensaje: class Window { Window(int marker) { print("Window(" + marker + tI ) " ) ; class House Window wl House () new Window(l); II } Antes del constructor { II Mostrar que estamos en el constructor: print ("House () " ) ; w3 = new Window(33); II Reinicializar w3 Window w2 = new Window(2); void f () { print ( " f () "); ) Window w3 = new Window(3) i II Después del constructor II Al final public class OrderOfInitialization public static void main(String[] args) House h = new House() i h.f( ) i II Muestra que la construcción ha finalizado 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 definiera un constructor sobrecargado que no inicializara w3 y no hubiera una ü1icialización "predeterminada" para w3 en su definición? 106 Piensa en Java Inicialización de datos estáticos Sólo existe una única área de a lmacenamiento para un dato de tipo sta tic, independientemente del número de objetos que se creen. No se puede aplicar la palabra clave statie a las va riables locales, así que sólo se aplica a los campos. Si un campo es una primiti va de tipo static y no se inicializa, obtendrá el valor inicial estándar correspondiente a su tipo. Si se trata de una referencia a un objeto, el valor predeterminado de inicia lización será null . Si desea colocar la iniciali zac ión en el punto de la defmición, será similar al caso de las variables no estáticas. Para ver cuándo se in iciali za e l almacenamiento de tipo stane, ha aquí un ejemplo: ji : initialization/Staticlnitialization .java // Especificación de valores iniciales en una definición de clase. import static net.mindview.util.Print.*; class Bowl { Bowl (int marker) print {"Bowl (" + marker + " )") ; void fl (int marker) { print("fl(" + marker + ") " ) ; class Table ( static Bowl bowll = new Bowl (l) ; Table () { print{"Table{) " ) ; bow12.fl(l) ; void f2(int marker) print{"f2(" + marker + " ) " ); static Bowl bow12 = new Bowl(2); class Cupboard { Bowl bow13 = new Bowl(3); static Bowl bow14 = new Bowl (4) ; Cupboard () { print (11 Cupboard () " ) i bow14.fl(2) ; void f3(int marker) print{"f3 (u + marker + ") sta tic Bowl bow15 = new U); Bowl(5); public class Staticlnitialization { public static void main(String[] args) print("Creating new Cupboard() in main u ) ; new Cupboard () ; print ( " Creating new Cupboard () in main " ) ; new Cupboard () ; table. f2 (1) ; cupboard. f3 (1) ; static Table table = new Table(); static Cupboard cupboard = new Cupboard (); 5 Inicialización y limpieza 107 ) / * Output, Bowl(l) Bowl (2) Table () f1 (1) Bowl(4) Bowl(S) Bowl(3) Cupboard () f1 (2) Creat ing new Cupboard() Bowl(3) Cupboard () f1 (2) creating new Cupboard() Bowl(3) Cupboard () f1 (2) in main in main 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 objeto 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 cargan antes de que dé comienzo main(). Éste no es el caso usual , porque en los programas típicos no tendremos todo vinculado 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 predeterminados 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 implicar un gran número de actividades, especialmente cuando estén implicados los mecanismos de berencia. 108 Piensa en Java Inicialización static explícita Java pemlile agrupar Olras inicializaciones estáticas dentro de una "cláus ul a" sta tic especial (en ocasiones denominada bloque estático) en una clase. El aspecto de esta cláusula es el siguiente: // : initialization/ Spoon. j ava public class Spoon { static int i; sta tic { i :::: 47; } 111 ,Parece ser un método. pero se trata sólo de la palabra clave sta tic seg uida de un bloque de código, Este código, al igual que otras inicializaciones estáticas sólo se ejecuta una vez: la primera vez que se crea un objclO de esa clase o la primera vez que se accede a un mjembro de tipo static de esa clase (incluso aunque nUllca se cree un objeto de dicha clase). Por ejemp lo: /1 : initialization/ ExplicitStatic . java / / Inicialización static explícita con la cláusula "static". import static net.mindview.util.Print.*¡ class Cup { Cup (int marker ) print("Cup ( 1I + marker + 11 ) " ) ; void f (int marker) { print ( "f ( 1I + marker + 11) " ) ; class Cups { static Cup cupl ¡ static Cup cup2 ¡ static { cupl new Cup ( l) ; new Cup ( 2 ) ; cup2 Cups() print ( IICups () n); public class ExplicitStatic { public static void main(String [] args) print {"Inside main() " ) ; Cups , cupl.f(99 ) ; II { (1 ) } II static cups cupsl II static cups cups2 1* Output: new Cups ( ) ¡ new Cups () ; II II (2 ) (2 ) Inside main () Cup ( l) Cup(2) f (99) *111,Los inicializadores static para Cups se ejecutan cuando tiene lugar el acceso del objeto estático cup] en la línea marcada con (1), o si se desactiva mediante un comentario la línea (1) y se quitan los comentarios que desactivan las líneas marcadas (2). Si se desactivan mediante comentarios tanto (1) como (2), la inicialización sta tic de Cups nunca tiene lugar. como 5 Inicialización y limpieza 109 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. Inicialización de instancias no estáticas Java proporciona una si ntaxis simi lar, denominada inicialización de instancia, para inicializar las variables estáticas de cada objeto. He aquí un ejemplo: 11 : initialization / Mugs.java II "Inicialización de instancia" en Java. import static net.mindview.util.Print.*; cl ass Mug { Mug ( int marker ) print { IIMug(1I + marker + 11 ) 11 ) ; void f ( int marker ) { print (11 f ( " + marker + " ) 11 ) ; public class Mugs { Mug mug1; Mug mug2; ( mug1 = new Mug(l ) ¡ mug2 = new Mug ( 2 ) ¡ print (lImug1 & mug2 initialized" ) ¡ Mugs () print("Mugs () !I ) ; Mugs (int i ) print ( IIMugs (int ) 11 ) ; publi c s t a t i c vo i d main (Stri ng [ ] args l print ( " Inside main () It ) ; new Mugs () ; print ( IInew Mugs () c ompleted" ) i new Mugs (1 ) ¡ print ( IInew Mugs (1 ) c o mpleted" ) ¡ 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 Podemos ver que la cláusula de inicializació n de instancia : mugl : new Mug {l ) ; mug2 = new Mug(2 ) ; print ( "mugl & mug2 initialized" ) ; 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 instancia. 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 juntos , 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" anteriormente comentado. Todo lo que tenemos en este punto es una referencia a una matriz (habiendo asignado el suficiente espacio 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 iniciali zación puede hacerse en cualquier lugar del código, pero también podemos utilizar una clase especial de expresión de inicializació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 al al al al [O] [1] [2] [3] [4] 2 3 4 5 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 travé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 primitivas) 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 elemento 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 pennitirá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.*; public class ArrayNew { public static void main{String[] args) int [] a; Random rand = new Random(47); a = new int [rand.nextlnt {20)] ; pri n t ( n length of a = 11 + a . length) ; print(Arrays.toString(a}} i / * 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 variables 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 asociados 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 eficientes, 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}]; Ésta es la forma preferible de hacerlo. siempre que se pueda. Si se crea una matriz que no es de tipo primitivo, lo que se crea es una matri z de referencias. Considere el tipo envoltorio Integer, que es una clase y no una primitiva: JI : initialization/ArrayClassObj.java JI Creación de una matriz de objetos no primitivos. import java.util.*; import static net . mindview.util.Print .* ¡ public class ArrayClassObj { public static void main(String(] args ) Random rand = new Random(47); Integer[] a = new Integer[rand.next lnt ( 20 }) i print ( " length of a = " + a .length) ; for(inc i = O; i < a . length¡ i++} a[i] = rand.nextlnt(500) i II Conversión automática print(Arrays . toString(al 1; 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[] Integer [] a = args) { ( 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» System.out.println(Arrays . toString(b» 1* Output: 11, [1, 2, 2, *///,- 3] 3] i ; 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 . public class DynamicArray { public static void main (String [] args ) { Other .main (new String[] { "fiddle", "de", class Other { public static void main (Str ing [) for (String s : args) System.out.print(s + " ") i args) Itdum" }) 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 utilizando 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 asignarl 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. Listas variables de argumentos La segunda forma proporciona una sin taxis cómoda para crear e invocar métodos que pueden producir un efecto sim ilar a las lisras variables de arglllne11los de C (conocidas con el nombre de "varargs" en C). Esto puede incluir un número desconocido de argumentos que a su vez pueden ser de tipos desconocidos. Puesto que todas las clases se heredan en última instanc ia de la clase raíz común Object (tema del que hablaremos más adelante en el libro), podemos crear un método que adm ite una matriz de Object e invocarlo del sigui ente modo: /1: initialization/VarArgs.java II USO de la sintaxis de matriz para crear listas variables de argumentos . class A {} public class VarArgs { static void printArray(Object[) args) for(Object obj : args) System . out.print(obj + " tl) i System.out.println() i public static void main(String(] printArray(new Object[] { args) { 114 Piensa en Java new Integer ( 47 ) , new Float(3.14 ) , }) , new Double (11.11 ) printArray (new Object[] {"ane", "two ll , "three" } ) i (new A () , new A () , new A () } ) ; printArray ( new Object[] / * 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 cIases que hemos creado aquí imprimen el nombre de la clase, seguido de un signo de '@' y de un a serie de dígitos hexadecimales. 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 argumentos. 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. public class NewVarArgs { static void printArray(Object. args) for (Object obj : args) System.out.print (obj + " " ) ; System.out.println( ) ; { public static void main (String(] args ) { II Admite elementos individuales: printArray(new Integer (47 ) I new Float (3.14) I new Double(11.11 )) ; printArray (47, 3.14F, 11.11 ) ; printArray ( "one", "two", "three" ) ; printArray (new A( ) , new A(), new A ()) ¡ II O una matriz: printArray l IObject [] ) new Integer[l{ 1 , 2 , 3 , 4 } ) , printArray () ¡ jI Se admite una lista vacía 1* Output: ( 75% ma tch ) 47 3.14 11.11 47 3.14 11.11 one two three A@1babSOa A@c3c749 A@150bd4d 1 2 3 4 * /// , Con varargs, ya no es necesari o escrib ir explícitamente la sintaxis de la matriz: el compi lador se encargará de comp letarla automática mente cuando especifiquemos varargs. Seguimos obteniendo una matriz, lo cual es la razón de que pr int() siga pudiendo uti lizar la sintaxisforeach para iterar a través de la matriz. Sin embargo, se trata de al go más que una simple conversión aut omática entre una lista de elementos y una matriz. Observe la penúltima línea del programa, en la que una matriz de elementos Integer (creados con la característi ca de conversión automática) se proyecta sobre una matriz O bj ect (para evitar que el compi lador genere una advertencia) y se pasa a printA rray(). Obviamente, el compilador determ ina que esto es ya una matri z, por lo que no realiza nin guna conversión con ella. De modo que, si tenemos un grupo de elementos, podemos pasarlos como un a lista, y si ya tenemos una matriz, se aceptará esa matriz C0l110 lista variable de argument os. La última línea del programa muestra que es posible pasar cero argumentos a una lista varmg. Esto resulta úti l cuando ex isten argumentos fina les opcionales: 5 Inicialización y limpieza 115 JI : initialization/OptionalTrailingArguments.java public class OptionalTrailingArguments { static void f (int required, String ... trailing ) System.out,print( ll r equired: ti + required + I! n); for(String s : trailing ) System . out .print(s + " " ); System.out.println() ; public static void main(String[] f (l , args) { "one") i f (2, "two U f (O) ; , "three" ); / * 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 public cIass VarargType { static void f (Character ... args ) { System . out.print{args .getClass()) i System.out.println( " length " + args . length ); static void 9 (int ... args ) { System.out.print{args .getClass()) i System . out . println ( " length " + args.Iength ) ¡ public static void main (Stri ng [] args) { f ( 'a' ) ; fl); g (1); g() ; System.out.println("int[) : " + new int(O) .getClass{))¡ 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 permi 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 public class AutoboxingVarargs { public static void f(Integer . . for(Integer i : args) System.out.print{i + " U); args) { System.out.println{) ; public static void main(String[] args) f(new Integer(l), new Integer(2)); f(4, 5, 6, 7, 8, 9); f(lO, new Integer{ll) I 12); /* 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ática 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 public class OverloadingVarargs { statie void f(Charaeter . .. args) System.out.print("first") ; for(Character e : args) System.out.print(" + e) i System .out.println () ; 11 statie void f(Integer.. args) System.out.print{"second") ; for(Integer i : args) System.out.print(II 11 + i); System.out.println() ; static void f{Long ... args) System.out.println{IIthird"l; publie statie void main (String [J args) f ( ' a' 'b' , 'e' l ; { I f f f f (1) ; (2, 1); (O) ; (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 sobrecargado 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) public class OverloadingVarargs2 ( static void f(float i, Character ... args) System. out. printIn ( n f irst" ) i { static void f(Character ... argsl System.out.print("second" ) ; public static void main (String [] argsl { f (1, 'a'); f ('a ', 'b') i } /// ,El marcador de comentario {CompileTimeError} excluye este archivo del proceso de constmcción Ant del libro. Si lo compila a mano podrá ver el mensaje de error: relerenee /0 I is ambiguous, bo/h me/hod l(floG/Java.lang. Charae/"" .. ) in Overloading Varargs2 and 111et!Jod fOava./ang.Characte¡: ..) in Ove¡·/oadingVarargs2 match Si proporciona a ambos métodos un argumento no-varO/'g, funcionará perfectamente: 11 : initialization/OverloadingVarargs3.java public class OverloadingVarargs3 { static void f(float i, Character ... args) System.out.println(Ufirst U) ; static void f (c har e, Character ... args) System.out.println(Usecond U) ; public static void main(String[] args) { { { f (1, la t) ; f (t al, Ib l ) i 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 cuando 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 necesitan 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 estaban 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 enumerados 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 public class Burrito { Spiciness degree¡ public Burrito (Spiciness degree ) { this. degree public void describe () { System. out. print ( "This burrito is " ) ¡ degree; } 5 Inicialización y limpieza 119 switch(degree) case NOT: System.out.println(lInot spicy at all . tI) break¡ i 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 ) ; public static void main(String[] argsl Burrito plain = new Burrito (Sp iciness.NOT ). greenChile = new Burrito (S piciness.MEDIUM ) , jalapeno = new Burrito (Spiciness.HOT ) ; plain . describe () ; greenChile.describe() ; jalapeno.desc ribe () ; / * 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 complementa 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 descripci ó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 variables es responsable de una parte significativa de los problemas de programación. Este tipo de errores son dificiles de localizar, 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 constructor), 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ícitamente. 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 implementarlo explícitamente). En aquellos casos donde no se necesite un comportamiento simi lar al de los destructores, el mecanismo 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érpretes de Ja va. Aunque a lo largo del tiempo se ha mejorado significativamente la velocidad de Java, un problema de la velocidad 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 mecanismo. 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 disponible para la venta en Inm:MindView.llel. Control de acceso El control de acceso (u ocultación de la implementación) trata acerca de "que no salgan las cosas a la primera". 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 principales 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 (programadores 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 consideraciones 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 elemento 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 clientes? 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 implementación anterior y sustituirla por una nueva? Si se modifica alguno de esos miembros, podría dejar de funcionar el código 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, aunque 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 estará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 especificadores de acceso se verán afectados por el hecho de que una clase se encuentra en el mismo paquete o en otro paquete di stinto. 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. package: la unidad de biblioteca Un paquete contiene un grupo de clases, organizadas conjuntamente dentro de un mismo espacio de nombres. Por ejemplo, existe una biblioteca de utilidad que fomla parte de la di stribución estándar de Java, organizada bajo el espacio de nombres java.util. Una de las clases de java.util se denomina ArrayList. Una fonua de utilizar un objeto Arr.yList consiste en especi ficar elllombre completo java.util.ArrayList. JI : access / FullQualification.java public class FullQualification { public static void main (String[] java.util.ArrayList list args ) { = new java.util.ArrayList( ) ; ) /1 / ,Sin embargo, este procedimiento se vuel ve rápidamente tedioso, por lo que suele se r más cómodo utilizar en su lugar la palabra clave import. Si queremos importar una única clase, podemos indicar esa clase en la instrucción import: JI : access / Singlelmport.java import java.util.ArrayList¡ public c!ass Singlelmport { public static void main (String [] args) ArrayList list = { new java.util .ArrayList ( ) i ) 111 ,Ahora podemos usar Array List sin ningún cualificador. Sin embargo, no tendremos a nuestra disposición ninguna de las otras clases de java.util. Para importar todas, basta con utili zar '*' tal como hemos visto en los ejemplos del libro. import java.util.*; La razón para efectuar estas importaciones es proporcionar un mecanismo para gestionar los espacios de nombres. Los nombres 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 posibilidad 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 senados 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 trataremos de utilizarla siempre que sea posible en el resto del libro, en aras de la simplicidad. Sin embargo, si lo que pretendemos 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 Organización del código Cuando se compila un archivo .j ava. se obtiene un archivo de salida para cada clase del archivo .java. Cada archivo de salida tiene el nombre de una de las clases del archi vo .java, pero con la extensión .class. De este modo, podemos llegar a obtener un gran número de archivos .class a partir de un número pequeño de archivos .java . Si el lector ha programado anteriom1ente en algún lenguaje compilado, esta rá acostumbrado al hecho de que el compilador genere a lgún fonnato intennedio (nannalmente un archi vo "obj") que luego se empaqueta con otros del mismo tipo utilizando un montador (para crear un arch ivo ejec utable) o un gestor de biblioteca (para crear una biblioteca). És ta no es la fonna de funciona r de Java. Un programa funcional es un conju nto de archivos .class, que se puede empaquetar y comprimir en un arch ivo JAR (Java ARrchi ve), utilizando el arch ivador jar de Java. El intérpre te de Java es responsable de localizar, ca rgar e interpretar2 estos archivos. Una biblioteca es un gmpo de estos archivos de clase. Cada archi vo fuente suele tener una clase public y un número arbitrario de c lases no públicas, por lo que no sólo existe un componente public para cada archivo fuente. Si queremos especificar que todos estos co mponentes (cada uno con sus propios arc hi vos .java y .c1ass separados) deben agruparse, podemos utilizar la palabra c lave package. Si usamos una instmcción package, debe aparecer como la primera línea no de comentario en el archivo. Cuando escribimos: package access; estamos indicando qu e esta unidad de compilación es parte de una biblioteca denominada access. Dicho de otro modo, estamos 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úscu 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úsculas): 11 : access/mypackage/MyClass.java package access.mypackage; public class MyClass { // } /// ,Ahora. si alguien quie re utilizar MyClass o cualquiera olra de las clases públicas de access, deberá emplear la palabra clave import para que estén disponibles esos nombres defi nidos en el paquete access. La alternativa consiste en especificar el nombre completamente cualificado: 11: access/QualifiedMyClass.java public class QualifiedMyClass { public static void main(String[] args) access.mypackage.MyClass m = new access.mypackage.MyClass(); 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 public class ImportedMyClass { public static void main (String[] args ) { MyClass m = new MyClass () ; } /// , Merece la pena tener presente que lo que las palabras clave package e import nos penniten hacer. como diseiiadores de bibliotecas, es dividir el espacio de nombres global único, para que los nombres no colisionen, independientemente de cuántas personas se conecten a Internet y comiencen a escribir clases en Java. Creación de nombres de paquete unívocos El lector se habrá percatado de que, dado que un paquete nunca estará realmente "empaquetado" en un solo archivo, podrá estar compuesto por muchos archivos .class, por lo que el sistema de archivos puede llegar a estar un tanto abarrotado. Para evitar el desorden, una medida lógica que podemos tomar seria colocar todos los archivos .class correspondientes a un paquete concreto dentro de un mismo directorio; es decir, aprovechar la estmctura de archivos jerárquica del sistema operativo. Ésta es una de las forma s mediante las que Java trata de evitar el problema de la excesiva acumulación de archivos; vere mos esto de otra fOn1la cuando más adelante presentemos la utilidad jaro Recopilar los archivos de un paquete dentro de un mismo subdirectorio resuelve también otros dos problemas: la creación de nombres de paquete uní vocos y la localización de aquellas clases que puedan estar perdidas en algún lugar de la estructura de directorios. Esto se consigue codificando la ruta correspondiente a la ubicación del archivo .class dentro del nombre del paquete. Por convenio, la primera parte del nombre del paquete es el nombre de dominio Internet invertido del creador de la clase. Dado que está garantizado que los nombres de dominio Internet sean unívocos, si seguimos este convenio nuestro nombre de paquete será unívoco y nunca se producirá una col isión de nombres (es decir, salvo que perdamos el derecho a utilizar el nombre de dominio y la persona que 10 comience a utilizar se dedique a escribir código Java con los mismos nombres de ruta que usted utili zó). Por supuesto, si no disponemos de nuestro propio nombre de dominio, deberemos concebir una combinación que res ulte lo suficientemente improbable (como por ejemplo la combinación de nuestro nombre y apellidos) para crear nombres de paquete unívocos. Si ha decidido comenzar a publicar código Java, merece la pena que haga un requefio esfuerzo para obtener un nombre de dominio. La seg unda parte de esta solución consiste en establecer la correspondencia entre los nombres de paquete y los directorios de la máquina, de modo que cuando el programa Java se ejecute y necesita cargar el archivo .class, pueda localizar el directorio en el que ese archivo .class resida. El intérprete Java actúa de la fomla siguiente. Primero, localiza la variable de entamo CLASSPATH 3 (que se tija a través del sistema operativo y en ocasiones es definida por el programa de instalación que instala Java con una herramienta basada en Java en la máquina). CLASSPATH contiene uno o más directorios que se utilizan como raíces para buscar los archivos .class. Comenzando por esa raíz, el intérprete toma el nombre de paquete y sustituye cada pWltO por una barra inclinada para generar un nombre de ruta a partir de la raiz CLASSPATH (por lo que el paquete package roo.bar.baz se convertiria en foo\bar\baz o foo /bar/baz o, posiblemente, en alguna otra cosa, dependiendo del sistema operativo). Esto se concatena a continuación con las diversas entradas que se encuentren en la variable CLASS PATH . Será en ese subdirectorio donde el intérprete busque el archi vo .class que tenga un nombre que se corresponda con la clase que se esté intentando crear (también busca en algunos directorios estándar relativos al lugar en el que reside el intérprete Java). Para comprender esto, considere por ejemp lo mi nombre de dominio, que es MindView.net. Invirtiendo éste y pasándolo a minúsculas, net.mindview establece mi nombre global uní voco para mis clases (antiguamente, las extensiones com, edu, org, etc., estaban en mayúscu las en los paquetes Java, pero esto se modificó en Java 2 para que todo el nombre del paquete estuviera en minúsculas). Puedo subdi vidir este espacio de nombres todavía más creando, por ejemplo, una biblioteca denominada simple, por lo que tendré un nombre de paquete que será: package net.mindview.simple¡ Ahora, este nombre de paquete puede uti lizarse como espacio de nombres paraguas para los siguientes dos archivos: 3 Cuando nos refiramos a la variable de entamo, utilizaremos letras mayúsculas (CLASSPATH). 6 Control de acceso 125 JI : net / mindview / simple / Vector . java /1 Creación de un paquete . package net . rnindview . s i mp l e; public class Vector { public Vector () ( Syst.em . out . println ( Unet.mindview. simpl e . Vector Ol ) i ) 111 > Como hemos men cionado antes, la instrucción package debe ser la primera línea de no comentario dentro del código del archi vo. El segundo archivo tiene un aspecto parecido: JI : net / mindview/ simple / List . java /1 Creación de un paquete . package net . mindview . simple ¡ public class List { public List 11 { Sys t em.out . prin t ln( "net . mindvie w. simp l e . Lis t" } ; Ambos archivos se ubicarán en el siguiente subdirectorio de mi sistema: C: \ DOC \ JavaT\ net \ mindv iew\s i mple Observe que la primera línea de comentario en cada archivo del libro indica la ubicación del directorio donde se encuentra ese archi vo dentro del árbol del código fuente; esto se usa para la herramienta automática de extracción de código que he empleado con el libro. Si examinamos esta ruta, podemos ver el nombre del paquete net.mindview.simple, pero ¿qué pasa con la primera pane de la ru ta? De esa parte se encarga la vari able de entorno CLASSPATH, que en mi máquina es: CLASSPATH= . ;O , \ JAVA\LIB;C, \ OOC \ JavaT Podemos ver que la variable de en tomo CLASSPATH puede contener W13 serie de rutas de búsqueda allemativas. Sin embargo, ex iste una variación cuando se usan archivos JAR. Es necesario poner el nombre real del archi vo lAR en la vari able de mta, y no simplemente la ruta donde está ubi cado. Así, para un archi vo lAR denominado grape.jar, la variable de mta incluiría: CLASSPATH= . ;O, \ JAVA\ LIB;C, \ flavors \ grape.jar Una vez que la variable de ruta de búsqueda se ha configurado apropiadamente, el siguiente archivo puede ubi ca rse en cualqu ier directorio: 11 : access / LibTest. j ava II Utiliza la biblioteca. i mport net.mindview.simple . *¡ public class LibTest { public stat ic void main (String [J args ) { Vector v : new Vec t or( ) ; List 1 : new List {) ; 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 ejemplo, 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 alguna 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. Una biblioteca personalizada de herramientas Armados con este conocimiento, ahora podemos crear nuestras propias bibliotecas de herrami erllas, para reducir o eliminar la esc ritura de códi go duplicado. Considere, por ejemplo, el alias que hemos estado utili zando para Sysle m.ou t.println(). con el fin de reducir la cantidad de infonnación tecleada. Esto puede ser parte de una clase denorninada Priot, de modo que dispondríamos de una instrucción estática de impresión bastante más legible: ji : net / mindview/ util / Print.java JI JI Métodos de impresión que pueden usarse sin cualificadores, empleando importaciones estáticas de Java SE5: package net.mindview . util; import java.io.*; public class Print JI Imprimir con una nueva línea: public static void print(Object obj) System.out .println{objl; JI Imprimir una nueva línea sola: 6 Controt de acceso 127 public static void print() System.out.println(} ; JI Imprimir sin salto de línea: public static void printnb (Object obj) System.out.print(obj) ; { / / El nuevo printf () de Java SES (de el : public static PrintStream printf (String format, Obj ect. .. args) { return System . out.printf(format, args ); 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 definidas 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.* ¡ public class PrintTest { public static void main(String[] args) print ( !tAvailable frem now on! It) ¡ print ( 1 00); print (100L); print(3.14159) ; 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 II Métodos de creación de matrices que se pueden usar sin cualificadores, con importaciones estáticas Java SES: package net.mindview.util¡ public class Range { II Generar una secuencia [O . . n) public static int[] range(int n) int [] result "" new int [n] ¡ for (i nt i = O ¡ i < n ¡ i++) result[i] = i¡ return resul t ¡ II Generar una secuencia [start .. end) public static int[] range (int start, int end) int sz = end - start¡ int [] result = new int [sz] ; for(int i = O; i < sz; i++) result[i] ~ start + i; { 128 Piensa en Java return result; / 1 Generar una secuencia [start .. end) con incremento igual a step public static int [] range {int start, int end, int step ) { int sz = (end - start l/ step; int[] result = new int[sz); for (int i = O; i < 5Z; i++ l result[i) = start + (i * step ) ; return result; A partir de ahora, cuando desarrolle cualquier nueva utilidad que le parezca interesante la podrá añadir a su propia biblioteca. A lo largo del libro podrá ver que cómo añadiremos más componentes a la biblioteca net.mindview.util Utilización de importaciones para modificar el comportamiento Una característica que se echa en falta en Java es la compilación condicional que existe en e y que pemlite cambiar una variable indicadora y obtener un comportamiento diferente sin variar ninguna otra parte del código. La razón por la que dicha característica no se ha incorporado a Java es, probablemente, porque la mayor parte de las veces se utiliza en C para resolver los problemas interplataforma: dependiendo de la platafomla de destino se compilan diferentes partes del código. Puesto que Ja va está pensado para ser automáticamente un lenguaje interplatafornla no debería ser necesaria. Sin embargo, existen otras aplicaciones interesantes de la compilación condicional. Un uso bastante común es durante la depuración del código. Las características de depuración se activan durante el desarrollo y se desactivan en el momento de lanzar el producto. Podemos conseguir el mismo efecto modificando el paquete que se importe dentro de nuestro programa, con el fin de conmutar entre el código utilizado en la versión de depuración y el empleado en la versión de producción. Esta misma técnica puede utilizarse para cualquier código de tipo condicional. Ejercicio 3: (2) Cree dos paquetes: debug y debu goff, que contengan una clase idéntica con un método debug(). La primera versión debe mostrar su argumento String en la consola, mientras que la segunda no debe hacer nada. Utilice una línea sta tic import para importar la clase en un programa de prueba y demuestre el efecto de la compilación condicional. Un consejo sobre los nombres de paquete Merece la pena recordar que cada vez que creamos un paquete, estamos especificando implícitamente una estructura de directorio en el momento de dar al paquete un nombre. El paquete debe estar en el directorio indicado por su nombre, que deberá ser un directorio alcanzable a panir de la mta indicada en CLASSPATH. Experimentar con la palabra clave package puede ser algo frustrante al principio, porque a menos que respetemos la regla que establece la correspondencia entre nombres de paquete y rutas de directorio. obtendremos un montón de misteriosos mensajes en tiempo de ejecución que nos dicen que el sistema no puede encontrar una clase concreta, incluso aunque esa clase esté ahí en el mismo directorio. Si obtiene un mensaje como éste, desactive mediante un comentario la instrucción package y comp ruebe si el programa funciona, si lo hace, ya sabe dónde está el problema. Observe que el código compilado se coloca a menudo en un directorio distinto de aquel en el que reside el código fuente. pero la ruta al código compilado deberá seguir siendo localizable por la JVM utilizando la variable CLASSPATH. Especificadores de acceso Java Los especificadores de acceso Java public, protected y private se colocan delante de cada definición de cada miembro de O un método. Cada especificador de acceso sólo controla el acceso para esa definición concreta. la clase, ya sea éste un campo Si no proporciona un especificador de acceso, querrá decir que ese miembro tiene "acceso de paquete". Por tanto, de una fonna u otra. todo tiene asociado algún tipo de control de acceso. En las secc iones siguientes. vamos a analizar los diversos tipos de acceso. 6 Control de acceso 129 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 ocasiones, "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 compilació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 interactuar fácilmente entre sí. Cuando se colocan las clases juntas en un paquete, garantizando así el acceso mutuo a sus miembros 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 arbitraria. 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 miembros 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 acceso, 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. public: acceso de interfaz Cuando se utiliza la palabra clave public, ésta quiere decir que la declaración de miembros situada inmediatamente a continuación suya está disponible para todo el mundo, y en particular para el programa cliente que utilice la biblioteca. Suponga que definimos un paquete desscrt que contiene la siguiente unidad de compilación: //: access/dessert/Cookie.java // Crea una biblioteca. package access.dessert; public class Cookie { public Cookie () ( System.out .println(IICookie constructor"); } void bite() { System.out.println("bite"); } /// ,Recuerde que el archivo de clase producido por Cookie.java debe residir en un subdirectorio denominado dessert, dentro de un directorio access (que hace referencia al Capítu lo 6, Control de acceso de este libro) que a su vez deberá estar bajo uno de los directorios CLASSPAT H. No cometa el error de pensar que Java siempre examinará el directorio actual como uno de los puntos de partida de su búsqueda. Si no ha incluido un ... como una de las rutas dentro de CLASS PATH, Java no examinará ese directorio. 130 Piensa en Java Si ahora creamos un programa que usa Cookie: 11 : access/Dinner.java Usa la biblioteca. import access.dessert. * ; II public class Dinner { public static void main{String[] args) Cookie x = new Cookie(); II! x.bite(); II No puede acceder 1* Output: Cookie constructor * jjj ,podemos crear un objeto Cookie, dado que su constructor es public y que la clase también es public (más adelante, profundizaremos 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: II access/Cake.java 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,En un segundo archivo del mismo directorio tenemos: 11: II access/Pie.java La otra clase. 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. private: ino lo toque! La palabra clave private significa que nadie puede acceder a ese miembro salvo la propia clase que lo contiene, utili zando para el acceso los propios métodos de la clase. El resto de las clases del mismo paquete no puede acceder a los miembros privados, así que el efecto resultante es como si estuviéramos protegiendo a la clase contra nosotros mismos. Por otro lado. resulta bastante común que un paquete sea creado por varias personas que colaboran entre sí, por lo que private pennite modificar libremente ese miembro sin preocuparse de si afectará a otras clases del mismo paquete. 6 Control de acceso 131 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 conveniente, 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 palabra 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 veremoS 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{)¡ public class IceCream { public static void main (Str ing[) args ) II ! Sundae x = new Sundae() ¡ Sundae x = Sundae.makeASundae(); 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 anterior, 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 modificar 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 implementació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). protected: acceso de herencia Para poder comprender el especificador de acceso protected, es necesario que demos un salto hacia adelante. En primer lugar, debemos tener presente que no es necesario comprender esta sección para poder continuar leyendo el libro hasta llegar al capítulo dedicado a la herencia (el Capítulo 7, Relllilización de clases). Pero para ser exhaustivos, hemos incluido aquí una breve descripción y un ejemplo de uso de proleeted. La palabra clave protected trata con un concepto denominado herencia, que toma una clase existente (a la que denominaremos clase base) y añade nuevos miembros a esa clase sin tocar la clase existente. También se puede modificar el comportamiento de los miembros existentes en la clase. Para heredar de una clase, tenemos que especificar que nuestra nueva clase amplía (ex tends) una clase existente, como en la siguiente línea: class Foo extends Bar { Existe 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). 4 132 Piensa en Java El resto de la definición de la clase tiene el aspecto habitual. Si creamos un nuevo paquete y heredamos de una clase situada en otro paquete. los únicos miembros a los que tendremos acceso son los miembros públicos del paquete original (por supuesto, si la herencia se realiza dentro del mismo paquete. se podrán manipular todos los miembros que tenga n acceso de paquete). En ocasiones, el creador de la clase base puede tomar un miembro concreto y garantizar el acceso a las clases derivadas. pero no al mundo en generaL Eso es precisamente lo que hace la palabra clave protected . Esta palabra clave también proporciona acceso de paquete, es decir, las restantes clases del mismo paquete podrán acceder a los elementos protegidos. Si volvemos al archivo Cookic.java. la siguiente clase no puede invocar el miembro bite( ) que tiene acceso de paquete: // : access/ChocolateChip.java /1 No se puede usar un miembro con acceso de paquete desde otro paquete. import access.dessert.*; public class ChocolateChip extends Cookie ( public ChocolateChip () { System. out .println ( "ChocolateChip constructor"); } public void chomp() lIt bite()¡ 1/ No se puede acceder a bite public static void main(String[] args ) ChocolateChip x = new ChocolateChip{); x. chomp () ¡ 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¡ public class Cookie { public Cookie () { System.out.println("Cookie const r uctor " ) ¡ 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.*; public class ChocolateChip2 extends Cookie { public ChocolateChip2 () { System. out. println ( "ChocolateChip2 constructor") ¡ } public void chomp () { bite (); } // Método protegido 6 Control de acceso 133 public static void main(String[] ChocolateChip2 x x.chomp() i = args) { new ChocolateChip2() i / * 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 acceder a todos los miembros de la clase. Tenga en cuenta que las clases que se encuentran en el mismo directorio 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 interfaz 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 principio, 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 public class OrganizedByAccess { public void publ () { / * */ .. •1 ... • 1 private void privl () { / * */ private void priv2 () { 1* */ private void priv3 () { /* */ public void pub2 () public void pub3 () { /. { l' private int i¡ /1 /! / ,- 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 palabras, el lector seguirá pudiendo ver el código fuente (la implementación), porque está incluido en la clase. Además, el sistema 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, trabajo 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 disponibles para los usuarios de esa biblioteca. Si queremos que una cierta clase esté disponible para un programa cliente, tendremos 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.*¡ Sin embargo, existen una serie de restricciones adicionales: l . Sólo puede haber una clase pu blic por cada unidad de compilación (archivo). La idea es que cada unidad de compilación tiene una única interfaz pública representada por esa clase pública. Además de esa clase, puede tener tantas clases de soporte con acceso de paquete como deseemos. Si tenemos más de una clase pública dentro de una unidad de compilación, el compilador generará un mensaje de error. 2. El nombre de la clase public debe corresponderse exactamente con el nombre del archivo que contiene dicha unidad de compilación, incluyendo el uso de mayúsculas y minúsculas. De modo que para Widget, el nombre del archivo deberá ser Widget.java y no widget.java ni WIDGET.java . De nuevo, obtendremos un error en tiempo de compilación si los nombres no concuerdan. 3. Resulta posible, aunque no muy normal, tener una unidad de compi lación sin ninguna clase publico En este caso, podemos dar al archivo el nombre que queramos (aunque si lo denominamos de forma arbitraria confundiremos a las personas que tengan que leer y mantener el código). ¿Qué sucede si tenemos una clase dentro de access que sólo estamos empleando para llevar a cabo las tareas realizadas por Widget o alguna otra clase de tipo public de access? Puede que no queramos molestamos en crear la documentación para el programador de clientes y que pensemos que algo más adelante quizá queramos modificar las cosas completamente, eliminando esa clase y sustituyéndola por otra. Para poder disponer de esta flexibilidad, necesitamos garantizar que ningún programa cliente dependa de nuestros detalles concretos de implementación que están ocultos dentro de access. Para conseguir esto, basta con no incluir la palabra clave public en la clase, en cuyo caso tendrá acceso de paquete (dicha clase sólo podrá ser usada dentro de ese paquete). Ejercicio 7 : (1) Cree una biblioteca usando los fragmentos de código con los que hemos descrito access y Widget. Cree un objeto Widget dentro de una clase que no forme parte del paquete access. Cuando creamos una clase con acceso de paquete, sigue siendo conveniente definir los campos de esa clase como private (siempre deben hacerse los campos lo más privados posible), pero generalmente resulta razonable dar a los métodos el mismo tipo de acceso que a la clase (acceso de paquete). Puesto que una clase con acceso de paquete sólo se utiliza normalmente dentro del paquete, sólo hará falta defi nir como públicos los métodos de esa clase si nos vemos obligados a ello; además, en esos casos, el compilador ya se encargará de informamos. 6 Control de acceso 135 Observe que una clase no puede ser private (ya que eso haria que fuera inaccesible para todo el mundo salvo para la propia 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, impidiendo 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 Ilustra los especificadores de acceso a clases. Define una clase como privada con constructores privados: /1 /1 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 () {} 1/ Sólo se permite una clase pública por archivo: public class Lunch { void testPrivate () { II ¡No se puede hacer! Constructor privado: II! Soupl soup = new Soupl() ; 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 referencia 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 constructores 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 operaciones 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 objetos de esa clase desde fue ra del paquete (recuerde que todos los archivos de un mismo di rectori o que no tengan declaraciones 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 A continuación cree el siguiente archivo en un directorio distinto de aceess/local : 11 access/foreign/Foreign.java package access.foreign¡ import access.local.*¡ public class Foreign { public static void main(String[] args) { PackagedClass pe = new PackagedClass() ¡ 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 biblioteca 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 empaquetar 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 husmear 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 impoI1antes 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 reestructuración del código podría aumentar enomlemente la velocidad. Si la interfaz y la implementación están claramente protegidas, podemos realizar las modificaciones sin forzar a los programadores de clientes a reescribir su código. Los mecanis1110S 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 planificación y el diseño, los errores son inevitables. Saber que resulta relativamente seguro cometer esos errores significa que tendremos 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 modificación. Si no definimos correctamente la interfaz a la primera podemos añadir más mélOdos siempre y cuando no eliminemos 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á presente. 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 situaciones, 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 solució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 Una de las características más atractivas de Java es la posibilidad de reutilizar el código. Pero, para ser verdaderamente revolucionario es necesario ser capaz de hacer mucho más que simplemente copiar el cód igo y modificarlo. É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ódigo creamos nuevas clases, pero en lugar de crearlo partiendo de cero, usamos las clases existentes que alguien haya construido 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 programació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 hacemos 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"; public String toString () { return s; } public class SprinklerSystem { private String valvel, valve2, valve3, valve4¡ 140 Piensa en Java new WaterSource () ¡ private WaterSource source pri vate int i i private float fi pu blic String toString () return IIvalvel + valvel + + II v alve2 + valve2 + + + valve3 + "va l ve3 + "valve4 + valve4 + " \ n" + "i = " + i + " " + "f = " + f + " " + 11 source = 11 + source ¡ public static void main (String[] args ) { SprinklerSystem sprinklers = new SprinklerSystem () ¡ System.out.println ( sprinklers ) ¡ 1* Output: WaterSource ( ) valvel = null valve2 i = O f = 0.0 source null valve3 Constructed null valve4 null * / //,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 ultante 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étodos 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, podemos 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 voque 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; } public class Bath { private String /1 Inicialización en el punto de definición: 51 :: I'Happy" , s2 = 11 Happy 11 , 53, 54; private Soap castille; private int ii private float toy; public 8ath 1) { print (U Inside Bath () " ) 53 = "Joy"; toy = 3 .14f; castille = new Soap() i i // Inicialización de instancia: {i= 47;} public String toString () { if (54 == nulll / / Inicialización diferida: 54 = IIJoy"; return "sl + + + + "52 IIs3 "54 "i = 11 + i 51 52 53 54 + " \n" "\n" "\n" "\nn "\n" + + + + + + + + + "toy = " + toy + " \n " + "castille = " + castille; public static void main(String[] Bath b = new Bath(); print lb) ; argsl { /* Output: Inside Bath () Soap () 51 Happy s2 Happy 53 Joy 54 i Joy :::: 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 abertura 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 () public void scrub () ( append (" apply () " ); } ( 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; public class Detergent extends Cleanser { // Cambio de un método : public void scrub () { append(" Detergent.scrub()"); super.scrub() ¡ // Invocar version de la clase base // Añadir métodos a la interfaz: public void foam() ( appendl" foam() "); // Probar la nueva clase: public static void main (String [] args) Detergent x = new Detergent(); x.dilute() ; { x.apply() ; x.scrub(l; x.foam (); print (x l; print ("Testing base class:!I l ; Cleanser.main(argsl; / * 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 cadenas de caracteres con S utilizando el ope rador +=. que es uno de los operado res, junto con que los diseñadores de Java "han sobrecargado" para que funcionen con cadenas de caracteres. '+" 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 acceso. los miembros adoptarán de f0n11a predetenninada el acceso de paquete, lo que sólo pennite el acceso a los otros miembros 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 general 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 algunos 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 exactamente 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( ). Inicialización de la clase base Puesto que ahora tenemos dos clases (la clase base y la clase derivada) en lugar de una, puede resu ltar un poco confuso tratar de imaginar cuál es el objeto resultante generado por una clase derivada. Desde fuera, parece como si la nueva clase tuviera la misma interfaz que la clase base y. quizá algunos métodos y campos adicionales. Pero el mecan ismo de herencia no se limita a copiar la interfaz de la clase base. Cuando se crea un objeto de la clase derivada, éste contiene en su interior un subobjeto de la clase base. Este subobjeto es idéntico a 10 que tendríamos si hubiéramos creado directamente un objeto de la clase base. Lo que sucede, simplemente. es que. visto desde el exterior, el subobjeto de la clase base está envuelto por el objeto de la clase derivada. Por supuesto. es esencial que el subobjeto de la clase base se inicialice correctamente, y sólo hay una forma de garantizar esto: realizar la inicialización en e l cons tructor invocando al constructor de la clase base, que es quien tiene todos los conocimientos y todos los privilegios para llevar a cabo adecuadamente la inicialización de la clase base. Java inserta automáti camente llamadas al constructor de la clase base dentro del constructor de la clase derivada. El siguiente ejem plo mu estra este mecanismo en acción con tres niveles de herencia: 11 : reusing / Cartoon.java 11 Llamadas a constructores durante la herencia. 144 Piensa en Java import static net.mindview.util.Print.*¡ class Art Art () { print ( "Art constructor" ) ; } class Drawing extends Art { Drawing () { print ( "Drawing constructor ti ) i } public class Cartoon extends Drawing { public Cartoon () { print ( "Cartoon constructor" ) i public static void main (String[] args ) { Cartoon x = new Cartoon () i 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 compilador 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 class BoardGame extends Game { BoardGame (int i) { super (i ) ; print ( "BoardGame constructor" ) i public class Chess extends BoardGame { 7 Reutilización de clases 145 Chess (1 { super(ll) ; print ( "Chess constructor lt ) ; public stacic void main(String(] Chess x = new Chess()¡ args) { / * 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 constTuctores predetenninados. Escriba un constructor para e que realice toda la inicialización dentro del constructor 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 predeterminados 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 public void void void void void void void class SpaceShipControls up(int ve1ocity) () down (int velocity) {} 1eft (int ve10cityl {) right (int velocity) {} forward (i nt velocity) {} back (int veloci ty ) {} turboBoost () {} /// 0- Una forma de conslruir la nave espacial consistiría en emplear el mecanismo de herencia: /1 : reusing /SpaceShip .java public class SpaceShip extends SpaceShipControls { private String name; public SpaceShip(String name ) ( this.name = name; public String toString{) { return name; } 146 Piensa en Java pUblic static void main(String [] args) { SpaceShip protector = new SpaceShip ("NSEA Protector" proteceor.forward(lOO) ; 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 espacial (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 public class SpaceShipDelegation { private Sering name; private SpaceShipConerols controls new SpaceShipControls () ; public SpaceShipDelegation (S tring name) this.name = name; jj Métodos delegados: public void back (i nt velocity ) controls .ba c k (velocit y ) ; public void down(int velocity ) controls.down(velocity) ; public void forward (int velocity) controls .forward (ve l ocity) ; public void left ( int velocity ) controls.left(velocity) ; public void right(int velocity ) controls.right (v elocity ) ; public void turboBoost() controls .turboBoost () ; public void up(int velocity ) controls.up(velocity ) ; public static void main (String (] args ) { SpaceShipDelegation protector = new SpaceShipDelegation ( "NSEA Protector" ) ; protector.forward(lOO) ; 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 subconjunto 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 Combinación de la composición y de la herencia Resulta bastante común utilizar los mecanismos de composición y de herencia conjuntamente. El siguiente ejemplo muestra la creación de una clase más compleja utilizando tanto la herencia como la composic ión, junto con la necesaria inicialización mediante los constructores: /1: reusing!PlaceSetting.java I Combinación de la composición y la herencia. import static net.mindview.util.Print.*¡ class Plate { Plate (int i) print ("Plate constructor") i class DinnerPlate extends Plate { DinnerPlate (int i) { super(i) ; print ("DinnerPlate constructor"); class Utensil ( Utensil(int i) print ("Utensil constructor"); class Spoon extends Utens il { Spoon{int i) { super (i) ¡ print ("Spoon constructor " ) ¡ class Fork extends Utensil { Fork lint il { super(i)¡ print ( " Fork constructor " ) i class Knife extends Utensil { Knife (int i) { super(i) ¡ print ("Knife constructo r" ) i // Una forma cultural de hacer algo: class Custom { Custom ( int i) { print ( "Custom constructor!! ) i publ ic class PlaceSetting extends Custom { private Spoon SPi 148 Piensa en Java private Fork frk¡ private Knife kn¡ private DinnerP late pI; public PlaceSetting(int i) super (i + 1); sp = new Spoon(i + 2); frk = new Fork(i + 3}; kn = new Knife(i + 4); pI = new DinnerPlate{i + 5); print (" PlaceSetting constructor"); public static void main(String[] args) PlaceSetting x = new PlaceSetting(9); / * 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 asegura 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). Cómo garantizar una limpieza apropiada Java no tiene el concepto C++ de destructor, que es un método que se invoca automáticamente cuando se destruye un objeto. La razón es, probablemente, que en Java la práctica habitual es olvidarse de los objetos en lugar de destruirlos, pennitiendo al depurador de memoria reclama r la memoria cuando sea necesario. A menudo, esto resulta suficiente, pero hay veces en que la clase puede realizar determinadas actividades durante su tiempo de vida que obligan a realizar la limpieza. Como hemos mencionado en el Capítulo 5, Inicialización y limpieza, no podemos saber cuándo va a ser invocado el depurador de memoria, o ni siquiera si va a ser invocado. Por tanto, si queremos limpiar algo concreto relacionado con una clase, deberemos escribir explicitamente un método especial para hacerlo yaseguramos de que el programador de clientes sepa que debe invocar dicho método. Además, como se descri be en el Capítulo 12, Tratamiento de errores con excepciones, debemos protegemos frente a la aceleración de posibles excepciones incorporando dicha actividad de limpieza en una cláusula finall y. Considere un ejemplo de un sistema de diseño asistido por computadora que dibuje imágenes en la pantalla: JJ : reusingJCADSystem.java JI Cómo garantizar una limpieza adecuada. package reusing¡ import static net.mindview.util.Print.*¡ class Shape { Shape{int i) print (IIShape constructor") ¡ void dispose () { print ("Shape dispose H ) i } 7 Reutilización de clases 149 class Circle extends Shape { Circle (int i) super (i) ; { print{UDrawing Circle") i } void dispose () { print ("Erasing Circle"); super.dispose(} ; class Triangle extends Shape { Trianglelint il ( super (i) ; print ( "Drawing Triangle 11) i } void dispose () { print (" Erasing Triangle"); super.dispose() ; class Line extends Shape { private int start, end; Line(int start, int end} super (start) j this.6tart = start¡ this.end = end¡ print ("Drawing Line: void dispose () { print ("Erasing tine: super.dispose() + start + " + end); " + start + " + end); 11 i public class CADSystem extends Shape { private Circle C¡ private Triangle ti private Line [] lines = new Line [3) ; public CADSystem(int i) { super (i + 1); for{int j = O; j < lines.length; j++) lines [j] = new Line{j j*j) i e = new Circle(1); t = new Triangle{1); print ("Combined constructor"); I public void dispose () { print ("CADSystem . dispose () " } ; II El orden de limpieza es el inverso II al orden de inicialización: t.dispose() ; c.dispose() ; for(int i = lines.length - 1; i >= O; i--) lines[i] .dispose( ); super.dispose() ; public static void main (String [] args) { 1SO Piensa en Java CADSystem x new CADSystem(47); try { /1 Código y tratamiento de excepciones. finally { x.dispose{) ; /* 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 continuació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 protegida 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 confiar 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'; float doh l float f l ( print ( "doh (float ) " ) ; return 1. Of; class Milhouse {} class Bart extends Homer { void doh (Milhouse m) { print ( "doh (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 sobreca rgamos accidentalmente e l método en lugar de sustiruirlo: 152 Piensa en Java JI : reusing / Lisa . java // {CompileTimeError} (Won' t compile ) class Lisa extends Homer { @Override void doh (Milhouse m) { System.out.println("doh (Milhouse J "); } ///,El marcador {CompileTimeError} excluye el archivo del proceso de construcción con Ant de este libro, pero si lo compila a mano podrá ver el mensaje de error: methad does not override a methad fraID its superclass La anotación @Override evitará, así, que sobrecarguemos accidentalmente un método cuando no es eso lo que queremos hacer. Ejerc ici o 13: (2) Cree una clase con un método que esté sobrecargado tres veces. Defina una nueva clase que herede de la anterior y añada una nueva versión sobrecargada del método. Muestre que los cuatro métodos están disponibles en la clase derivada. Cómo elegir entre la composición y la herencia Tanto la composición como la herencia nos permiten incluir subobjetos dentro de una nueva clase (la composición lo hace de forma explícita, mientras que en el caso de la herencia esto se hace de fonna implícita). Puede que se esté preguntando cuál es la diferencia enrTe ambos mecan ismos y cuándo convi ene elegir entre uno y otro. Generalmente, la composición se usa cuando se desea incorporar la funcionalidad de la clase existente dentro de la nueya clase pero no su interfaz. En otras palabras, lo que hacemos es integrar un objeto para poderlo utilizar con el fm de poder implementar ciertas características en la nueva clase, pero el usuario de la nueva clase verá la interfaz que hayamos definido para la nueva clase en lugar de la interfaz de l objeto incrustado. Para conseguir este efecto, lo que hacemos es integrar objetos private de clases existentes dentro de la nueva clase. Algunas veces, tiene sentido permitir que el usuario de la clase acceda directamente a la composición de esa nueva clase. es decir. hacer que los objetos miembros sean públicos. Los objetos miembros utilizan la técnica de la ocultación de la implementación por sí mismos, así que no existe ningún riesgo. Cuando el usuario sabe que estamos ensamblando un conjunto de elementos. normalmente, puede comprender mejor la interfaz que hayamos definido. Un ejemplo seria un objeto car (cocbe): JI: reusingfCar.java II Composición con objetos públicos. 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 public void elose (1 {) public class Car { new Engine () ; public Engine engine public Wheel[) wheel new Wheel [4) ; public Door Ieft = new Door {) , right = new Door(); // 2-door public Car () { for (int i = Oi i < 4; i++ ) wheel [i] = new Wheel () ; public static void main{String[] Car car = new Car () ; args ) { car.left.window.rollup() ; car.wheel [O) .inflate (72) ; Puesto que en este caso la composición de un coche fonna parte del análisis del problema (y no simplemente del diseño subyacente), hacer los miembros públicos ayuda al programador de clientes a entender cómo utilizar la clase, y disminuye tambié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 queremos 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 permitirá conservar siempre el derecho a modificar la implementación subyacente. Entonces, podemos permitir que los herederos 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 public class Ore extends Villain { private int orcNumber; public Ore (String name, int orcNumber) super (name) ; this.orcNumber = orcNumber; public void change (String name, int orcNumber) { set(name); /1 Disponible porque está protegido. this.orcNumber = orcNumber; public String toString () { return "Ore " + orcNumber + ": " + super. toString () ; public static void main(String[] args) Ore ore,: new Orc{"Limburger", 12) i print (ore) i ore . change ("Bob", 19); print (ore) ; 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 definido 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 contener un método protected . Fuera del paquete, trate de invocar el método protected y explique los resultados. 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 entre la nueva clase y la clase base. Esta relación puede resumirse dic iendo que " la nueva clase es un lipo de la clase existente". 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 entos 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 () ; II Los objetos instrumentos de viento II porque tienen la misma interfaz: 7 Reutilización de clases 155 public class Wind extends I nstrument ( public static void ma i n (String[] args ) Wind f lute = ne w Wi nd( ) ¡ Instr umen t. tune ( f lute l ; /1 Gene r ali zación } ;;; ,Lo más interesante de este ejemplo es el método tune( ) (afi nar), que acepta una referencia a un objeto Instrument. Sin embargo, en Wind.main() al método tune( ) se le entrega una referencia a un objeto Wind. Dado que Java es muy estricto en lo que respecta a las comprobaciones de tipos, parece extraño que un método que acepta un detenninado tipo pueda aceptar también otro tipo distinto, hasta que nos demos cuenta de que un objeto Wind también es un objeto Instrument y de que no ex iste ningún método que tune() pudiera invocar para un objeto Instrumeot y que no se encuentre también en Wind . Dentro de tune(), el código funciona tanto para los objetos Instrument como para cualquier otra cosa derivada de Instrument, y el acto de convertir una referencia a un objeto Wind en otra referencia a Instrument se denomina upcasling (generalización). ¿Por qué generalizar? El ténnino está basado en la fonna en que se vienen dibujando tradicionalmente los diagramas de herencia de clase. Con la raíz en la parte superi or de la página y las clases derivadas distribuyéndose hacia abajo (por supuesto, podríamos dibujar los diagramas de cualquier otra manera que nos resultara útil), El diagrama de herencia para Wind.java será entonces: 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 ningú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. Nueva comparación entre la composición y la herencia En la programación orientada a objetos, la fonna más habitual de crear y utilizar código consiste en empaquetar los datos y métodos en una clase y usar los objetos de dicha clase. También utilizamos otras clases existentes para construir nuevas ciases utilizando el mecanismo de composición. Menos frecuentemente, debemos utilizar el mecanismo de herencia. Por tanto, aunque al enseñar programación orientada a objetos se suele hacer un gran hincapié en el tema de la herencia, eso no quiere decir que se la deba usar en todo momento. Por el contrario, conviene emplearla con mesura, y sólo cuando esté claro que la herencia resulta útil. Una de las fonnas más claras de detenninar si debe utilizarse composición o herencia consiste en preguntarse si va a ser necesario recurrir en algún momento al mecanismo de generalización de la nueva clase a la clase base. Si es necesario usar dicho mecanismo, entonces la herencia será necesaria, pero si ese mecanismo no hace falta conviene meditar si verdaderamente hay que emplear la herencia. En el Capítulo 8, Polimotjisl/lo se proporciona una de las razones más importantes para utilizar la generalización, pero si se acuerda de preguntarse u¿ vaya necesitar generalizar en algún momento?" tendrá una buena fonna de optar entre la composición y la herencia. Ejercicío 16: (2) Cree una clase denominada Amphibian (anfibio). A partir de ésta, defina una nueva clase den ominada Frog (rana) que herede de la anterior. Incluya una seríe de métodos apropiados en la clase base. En 156 Piensa en Java main( ). cree un objeto Frog y realice una generalización a Amphibian, demostrando que lodos los métodos 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( ). La palabra clave final La palabra clave de Java final tiene significados ligeramente diferentes dependiendo del contexto, pero en general quiere decir: "'Este elemento no puede modifi carse". Puede haber dos razones para que no queramos pennitir los cambios: diseño y eficiencia. Puesto que estas dos razones son muy diferentes entre sí, resulta bastante posible utilizar la palabra clave final de manera inadecuada. En las siguient es secciones vamos a ver los tres lugares donde final puede utilizarse: para los datos, para los métodos y para las clases. 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í ciertos 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 primitiva, 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 propio objeto sí que puede ser modificado; Java no proporciona ninguna manera para hacer que el objeto arbitrario sea constante (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 separar 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; public class FinalData { private static Random rand = new Random(47)¡ private String id; public FinalData (String id) { this. id = id¡ } II Pueden ser constantes de tiempo de compilación: private final int valueOne = 9; 7 Reutilización de clases 157 private static final int VALUE_TWO = 99; /1 Constante pública tipica: public static final int VALUE_THREE = 39; JI No pueden ser constantes de tiempo de compilación: private final int i4 = rand.nextlnt(20); static final int INT_S = rand.nextlnt(20l i private Value vI = new Value(ll) i private final Value v2 = new Value(22)¡ private static final Value VAL_3 = new Value(33) i 1/ Matrices: private final int[] a = { 1, 2, 3, 4, 5, 6 }i public String toString () { return id + lO: !I + "i4 = " + i4 + ", INT_S "+ INT_5; public static vold main (String [] args) { FinalData fd! = new FinalData("fdl"}; jI! fdl.valueOne++i /1 Error: no se puede modificar el valor fdl.v2.i++; /1 ¡El objeto no es constante! fdl.vl = new Value(9); II OK ~ - no es final for (int i =- O; i < fdl. a .length; i++) fdl.a[iJ++; II ¡El objeto no es constante! II! fdl.v2 = new Value(O); II Error: no se puede II! fdl.VAL_3 = new Value(l); II cambiar la referencia II! fdl.a =- new int [3] ; print (fd1) ; print ("Creating new FinalData"); FinalData fd2 new FinalData (" fd2") ; print (fd1) ; print (fd2) ; 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 valores 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 convenio). 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 compilació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 cambia 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 asociar v2 COIl un nuevo objeto. Podemos ver que la afinnación también es cierta para las matrices, que son otro de tipo de referencia (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. Valores final en blanco Java pennite la creación de valores finales en blanco, que son campos que se declaran como final pero que no se les pro~ porciona un valo r de inicialización. En todos los casos, el valor final en blanco debe ser inicializado antes de utilizarlo, y el compilador se encargará de hacer que esto sea asÍ. Sin embargo, los valores final en blanco proporcionan mucha más fle· xibilidad en el uso de la palabra clave final ya que, por ejemplo, un campo final dentro de una clase podrá con esto ser diferente para cada objeto y mantener aún así su carácter de inmutable. He aquí un ejemplo: JI : reusing / BlankFi na l . j ava JI Campos final "en blanco". cIass Poppet { private int i¡ ii; Poppet (int ii I { i } public cIass BlankFinal { private final int i = Di 1/ Valor final inicializado private final int j i II Va l or final en blanco private final Poppet Pi II Referencia final en blanco II Los valores final en blanco DEBEN inicializarse en el constructor: publie BlankFinal () { j 1i II Inicializar valor final en blanco p = new Poppet (l ) ; II Inicializar referencia final en blanco public BlankFinal {int x ) j Xi II Inicializar valor final en blanco p = new Poppet (x ) ; II Inicializar referencia final en blanco public static void main {String[] args ) { new BlankFinal {) ; new BlankFinal (47 ) i 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 referencia final en blanco dentro de todos los constructores. Demuestre que se garantiza que el va lor final estará 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 () {} public class FinalArguments { void with {final Gizmo g ) { II ! 9 = new Gizmo () ; II Ilegal -- 9 es final 7 Reutil ización de clases 159 .:~:j el wit~c~:IG:zmo = n e',,' ·; :;izmo {; ; ;; . sp:n í;' g. 1/ 8K 9 r.o es :i"o.1 i -Jo ici f (final i::-.1: i ) f i+-I'; } 1/ Ko se puede car:-.:ciar Las p~im~tivas f !nal s510 p ue den leerse: L:c. g; fí::-Ial '-n:. :' i { ret urn i 1" 1.; } rub1'..c static ~ol d mainl$tring[) args l { ?i¡:aLr..:::-g\.:.me"ts bf = ne"'" FinalArgume:-tts ¡;. ; _ _ . '.Vl.thout (nu ll \ ; ti .\';ith ~ null); 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 herede dc esta ca mbi e su sign ificado. Esto se hace por razones de discí10 cuando queremos aseguramos de que se retenga el Cl1lllpon 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 illlplcIllcllfill."ionl!s anteriores de Java, si definíamos un método como final. pemlitíamos al compi lador convenir rodas las Ihllnada::- 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 umentos 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Í 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 leccionado 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 compilador). 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 funciona 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 void playO String whatO void adjustO Woodwind void playO String whatO Percussion void playO String whatO void adjustO Brass void playO void adjustO Stringed void playO String whatO void adjustO 172 Pien sa en Java 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") ¡ class Wind extends Instrument { void play(Note n) {print("Wind .play {) String what () { return "Wind" i } void adjust() { print{"Adjusting Wind"); + n) ¡ class Percussion extends Instrument { void play(Note n) {print("Percussion.play() String what () { return "Percussion "; } void adjust() { print("Adjusting Percussion lt ) ; } + n) ¡ } class Stringed extends Instrument { void playlNote ni { printl"Stringed . playll " + ni; String what () { return "Stringed"; } void adjust() { print("Adjusting Stringed U ) i } class Brass extends Wind { void play(Note n) { print(IIBrass . play() " + n) ¡ } void adj ust () { print (" Adjusting Brass 11) ; class Woodwind extends Wind { void playlNote ni { printl"Woodwind.playll String what () { return IIWoodwind"; } " + ni; public class Music3 { // No importa el tipo, por lo que los nuevos /1 tipos añadidos al sistema funcionan bien : public static void tune I Instrument i l { / / ... i.playINote.MIDDLE_CI; public static void tuneAll (Instrument [] for{Instrument i : e) e) { } } 8 Polimorfismo 173 tune(i) i public static vold main(String[) args) // Upcasting durante la adición a la matriz: Instrument[] orchestra = { new Wind() I new Percussion () new Stringed () I 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 proporciona 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 alrededor 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 ninguna 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, realice una generalización (lIpcasling) al tipo base y llame al primer método. Explique 10 que sucede. Error: "sustitución" de métodos private He aq uí un ej emplo de error de un programa que se puede cometer de manera inadvertida: 11 : polymorphismJPrivateOverride.java JI Intento de sustituir un método privado . package polymorphism¡ import static net . mindview.util.Print.*; 174 Piensa en Java public class PrivateOverride { private void f () { print("private f () 11); public static void main(String[] args) PrivateOverride po = ne w Derived() j po.r O; class Derived extends PrivateOverride { public void r () { print ( "public r () " ) ; } / * Output , private f () * /// ,Podría esperar, razonablemente, que la salida fuera " public f( )", pero los métodos pri vados son automáticamente de tipo final , y están también ocultos a ojos de la clase deri vada. Por esta razón, el método f() de la clase derivada es, en este caso, un método completamente nuevo, ni siquiera está sobrecargado, ya que la versión de f( ) en la clase base no es visible en Derived . El resultado de esto es que sólo los métodos no privados pueden ser sustituidos, así que hay que estar atento al intento incorrecto de sustituir métodos de tipo priva te, ya que esos intentos no generan ninguna advertencia del compilador, sino que el sistema no hará, seguramente, 10 que se espera. Para evitar las confusiones, conviene utilizar en la clase deri vada un nombre diferente al del método private de la clase base. Error: campos y métodos static Una vez familiari zados con el tema del polimorfismo, podemos tender a pensar que todo ocurre polimórficamente. Sin embargo, las únicas llamadas que pueden ser po limórficas son las llamadas a métodos normales. Por ejemplo, si accedemos a un campo directamente, ese acceso se resolverá en tiempo de compilación, como se ilustra en el siguiente ejemplo:1 // : polymorphism / FieldAccess.java // El acceso directo a un campo se determina en tiempo de compilaci6n. class Super { public int field = O¡ public int getField O { return field; } c lass Sub extends Super public int field = 1: public int getField () return field; } public int getSuperField () { return super. field¡ public class FieldAccess ( public static void main(String[] args ) { Super sup = new Sub(); // Upcast System.out.println(ltsup.field = n + sup.field + ", sup . getField () = n + sup. getField () ) : Sub sub = new Sub (} : System.out.println("sub.field = + sub . field + 11 sub. getField () + sub.getField () + ti, sub . getSuperField () = n + sub . getSuperField ()) ; I I Gracias a Randy Nichols por plantear esta cuestión. 8 Polimorfismo 175 / * Output: 0, sup getField{) sup. field sub. field = 1, sub getField{) 1 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 referencia 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 public String dynamicGet () { return u Base dynamicGet () " ; class StaticSub extends StatieSuper { publie statie String statieGet () { return "Derived statieGet(}"¡ public String dynamicGet () { return "Derived dynamicGet() u; public class StaticPolymorphism { public statie void main (String[] args ) { StaticSuper sup : new StaticSub(); JJ Generalización System,out.println(sup.staticGet{)) ; System.out . println(sup.dynamicGet()) 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 complejas y en presencia de polimorfismo. Esta compresión de los fundamentos nos ayudará a evitar errores desagradables. 176 Piensa en Java Orden de las llamadas a los constructores Hemos hablado brevemente del orden de las llamadas a los constmclOres en el Capítulo 5, Inicialización y Iimpie:a, y también el Capítulo 7. Relllili:ación de clases, pero eso fue antes de introducir el concepto de polimorfi smo. El conslructor de la clase base siempre se in voca durante el proceso de construcción correspondiente a un a clase deri vada. Esta llamada provoca un desplazamiento automático hacia arriba en la jerarquía de herencia, invocándose un constructor para todas las clases base. Esto tien e basta nt e sen tido, porque el constructor tiene asignada una tarea especial: garantizar que el objeto se constmye apropiadamente. Una clase deri vada sólo tiene acceso a sus propios miembros y no a los de la clase base (aq uellos mi embros típicamente de tipo private). Sólo el constructor de la clase base dispone del conocimiento y del acceso adecuados para inicializar sus propios elementos. Por tanto, resulta esencial que se invoquen todos los constructores, en caso contrario, no podría co nst ruirse el método completo. Ésta es la razón por la que el compilador impone que se realice una llamada al constructor para cada parte de una clase derivada. Si no especificamos ex plícitamente una llamada a un constructor de la clase base dentro del cuerpo de la clase deri vada, el compilador invocará de manera automática el constructor predetenninado. Si no hay nin gún constmctor predetenninado, el compilador generará un error (en aquellos casos en que una determinada clase no tenga ningún constructor, el compilador sintetizará automáticamente un constructor predeterminado). Veamos un ejemplo que muestra los efectos de la composición, de la herencia y del polimorfismo sobre el orden de construcción: 1/: polymorphism/Sandwich . java / 1 Orden de las llamadas a los constructores. package polymorphism¡ import static net.mindview . util.Print.*¡ 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 class Lunch extends Meal { Lunch () { print ( "Lunch () " ) ¡ class PortableLunch extends Lunch { PortableLunch (l print {"PortableLunch () 11 ) ¡ } public class Sandwich extends PortableLunch private Bread b = new Bread () ¡ private Cheese e = new Cheese{ ) ¡ private Lettuce 1 = new Lettuce()¡ public Sandwich () { print ( " Sandwich () " ) ¡ public static void main (String [] args l { new Sandwich () ¡ 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 derivada. 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 constructor 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: 11 : polymorphism/ Frog. j ava /1 Limpieza y herencia. package polymorphism; import static net.mindview.util.Print.*; class Characteristic { private String S; Characteristic (String s ) { this.s = S; 178 Piensa en Java print ("Creating Characteristic " + s) i protected void dispose() print ( "disposing Characteristic " + s) class Description { private String Si Description {String s} { this.s = Si print ("Creating Description " + s) i i protected void dispose () { print ("disposing Description " + s); class LivingCr eature { private Characteristic p new Characteristic("is alive " ) i private Description t = new Description("Basic Living Creature 'l) i Li vingCreature () { print (" Li vingCreature () ,, ) i protected void dispose () { print ( " LivingCreature dispose") i t.d i spose() i p.dispose () i class Animal extends LivingCreature { private Characteristic p = new Characteristic ("has heart") i private Description t = new Description ("Animal not Vegetable"); Animal () { print ("Animal () "); } protected void dispose () { print ("Animal dispose"); t. dispose () ; p.dispose() i super.dispose() i class Amphibian extends Animal { private Cha racteristic p = n e w Characteristic ( " can live in water " ) i private Description t = new Description ( " Both water and land"); Amphibia n () { print ("Amphibian () " ) ; protected void dispose() print ( "Amphibian dispose"); t.dispose() i 8 Polimorfismo 179 p.dispose() ; super.dispose() ; public class Frog extends Amphibian { privace Characteristic p = new Characteristic ( UCroaks" ) ; private Description t = new Description("Eats Bugs") i public Frog () { print (11 Frog () 11 ) ; } protected void dispose () { print ( 11 Frog dispose 11 ) ; t .dispose () ; p.dispose() ; super.dispose() ; public static void main (String [] args ) { Frog frog = new Frog {); print(IIBye! " ) ; frog .dispose () i / * 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 clases 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); public void addRe f () { refcount+ + i pro t ected void dispose () { if{ - - r efcoun t == O) print ("Disposing n + this) i public String toString() { return "Sha r ed " + id; } 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() ; protected void dispose() print ("disposing " + this) ¡ shared.dispose() ; public String toString () { return "Composing " + id; public class ReferenceCounting { public static void main (String [] args) { Shared shared = new Shared {} ¡ Composi ng[ ] composing = { ne w Compos i n g(shared), new Composing(shared}, n e w Composi ng{s h aredl, new Composing(shared}, new Compos i ng(shared) }¡ for(Composing c : composing) c.dispose() ; / * 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 ejemplos 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( ) llevará 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 determinada 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. Comportamiento de los métodos polimórficos dentro de los constructores La jerarquía de llamada a constructores plantea un dilema interesante. ¿Qué sucede si estamos dentro de un constmctor e invocamos un método con acoplamiento dinámico del objeto que esté siendo constmido? Dentro de un método nonnal, la llamada con acoplamiento dinámico se resuelve en tiempo de ejecución, porque el objeto no puede saber si pertenece a la clase en la que se encuentra el método o a alguna de las clases derivadas de la misma. Si invocamos un método con acoplamiento dinámico dentro de un constructor, también se utiliza la definición sustituida de dicho método (es decir, la definición del método que se encuentra en la clase actual). Sin embargo, el efecto de eSla llamada puede ser inesperado, porque el método sustituido será invocado antes de que el objeto haya sido completamente construido. Esto puede hacer que queden acuitas algunos errores realmente dificiles de detectar. Conceptualmente, la tarea del constructor es hacer que el objeto comience a existir (10 que no es una tarea trivial). Dentro de cualquier constructor, puede que el objeto completo sólo esté fonnado parcialmente, ya que de lo único que podemos estar seguros es de que los objetos de la clase base han sido inicializados. Si el constructor es sólo uno de los pasos a la hora de constmir un objeto de una clase que haya sido derivada de la clase correspondiente a dicho constructor, las partes derivadas no habrán sido todavía inicializadas en el momento en que se invoque al constructor actual. Sin embargo, una llamada a un método con acoplamiento dinámico se "adentra" en la jerarquía de herencia, invocando un método dentro de una clase derivada. Si hacemos esto dentro de un constructor, podríamos estar invocando un método que manipulara miembros que todavía no han sido inicializados, lo cual constituye una receta segura para que se produzca un desastre. Podemos ver el problema en el siguiente ejemplo: JJ : polymorphismJPolyConstructors.java JJ Los constructores en presencia de pOlimorfismo JJ pueden no producir los resultados esperados. import static net.mindview.util.Print.*; class Glyph { void draw() { print(nGlyph.draw() "); } Glyph () { 182 Piensa en Java print ( "Glyph () bef o re draw () " ) ; draw (} ; pr i nt ( "Glyph () after draw () " ) i class RoundGlyph extends Glyph private int radius '" 1; RoundGlyph ( int r ) { radius '" r; print ( "RoundGl}'Ph. RoundGlyph () void draw () { print ( "RoundGlyph. draw () I radius I radius 11 11 + radius ) ; + radius ) ; public class PolyConstructors { public static void main (String [] args) { new RoundGlyph(S ) ; / * 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 inicial 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 resolver 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 invoca el método sustituido draw( ) (sí, se invoca antes de que llame al constructor de RoundGlyph ) y éste descubre 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 perfectamente lógico, a pesar de lo cual el comportamiento es misteriosamente erróneo, sin que el compilador se haya quejado (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. Tipos de retorno covariantes Java SES añade los denominados tipos de retorno covarianles, lo que quiere decir que un método sustinlido en una clase derivada puede devo lver un tipo deri vado del tipo devuelto por el método de la clase base: JI : polymorphism/CovariantReturn .java class Grai n { public String toString () { rE"turn "Grain" i } { return "Wheat"; } class Wheat extends Grain { public String toString () cIass MilI { Grain p r ocess () { return new Grain () ¡ } class WheatMil1 extends MilI { Wheat process () { return new Wheat {) ; public class CovariantRetur n { public static void main{String[] MilI m = new Mill (} ¡ Grain 9 = m.process{}¡ System.out.println(g) ; m = new WheatMill {) i 9 = m.process()¡ System . out.println{g) ; } args ) { / * Output: Grain Wheat *// /0La 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. Diseño de sistemas con herencia Una vez que sabemos un poco sobre el polimorfismo, puede llegar a parecemos que todo debería heredarse, ya que el polimorfismo es una herramienta tan inteligente. Pero la realidad es que esto puede complicar nuestros diseños innecesariamente, de hecho, si decidimos utilizar la herencia como primera opción a la hora de utilizar una clase existente con el fin de formar otra nueva, las cosas pueden volverse innecesariamente complicadas. Una técnica mejor consiste en tratar de utilizar primero la composición, especialmente cuando no resulte obvio cuál de los dos mecanismos debería emplearse. La composición no hace que el diseño tenga que adoptar una jerarquía de herencia. Pero, as imismo, la composición es más flexible, porque permite seleccionar dinámicamente un tipo (y por tanto un compor- 184 Piensa en Java 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 HappyActor extends Act or { public void act () { print ( "HappyActor") class SadActor extends Actor { public void act {) { print ( "SadActor ll i ); class Stage { private Ac t or actor = new HappyActor {); public void change () { actor = new SadActor () ; public void performPlay () { actor. act () ; } public c l ass Transmogrify { public static void main (S tring (] args) Stage stage = new Stage()¡ stage.performPlay() ; stage.change() ; stage .performPlay() ; { / * 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 objeto distinto en tiempo de ejecución, podríamos almacenar una referencia a un objeto SadActor en actor, y entonces el comportamiento 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 distintas para expresar la diferencia en el método act() y Stage utiliza la compos ición para permitir que su estado sea mod ificado. 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 Circle draw() erase() I Triangle Square draw() erase() draw() erase() 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 mental y tiene, además, ot ras características que req ui eren método s adicionales para implementarlas: Uselul voi d I() void g() Suponga qu e esto representa una interfaz compleja MoreUselul "Es-coma-un" void I() void g() void u() void vO void w() Ampliación 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: Habla con el objeto Useful -_._-_._ . . . . . . . . . . . . . . parte de Useful Mensaje parte eUseful Si no estamos haci endo generalizaciones, no debe haber ningún problema, pero a menudo nos encontraremos en situaciones 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. Especialización e información de tipos en tiempo de ejecución Puesto que perdemos la información específica del tipo mediante el proceso de generalización (l/pcast, que consiste en moverse hacia arriba por la jerarquía de herencia), tiene bastante sentido que para extraer la infonnación de tipos; es decir, para volver a descender por la jerarquía de herencia, utilicemos un proceso de especialización (downcasf). Sin embargo, sabemos que una generalización siempre es segura, porque la clase base no puede tener una interfaz más amplia que la clase derivada; por tanto, se garantiza que todo mensaje que enviemos a través de la interfaz de la clase base será aceptado. Pero con una especialización no sabemos realmente si una determinada fonna, por ejemplo, es un círculo u otra cosa: también podría ser un triángulo, un cuadrado o algún otro tipo de forma. Para resolver este problema, tiene que haber alguna manera de garantizar que la especialización se efectúe de fonna correcta, de modo que no hagamos accidentalmente una proyección sobre el tipo inadecuado y luego enviemos un mensaje que el objeto no pueda aceptar. Si no podemos garantizar que la especialización se efectúe de manera correcta, nuestro programa no será muy seguro. En algunos lenguajes (como e++) es necesario realizar una operación especial para poder llevar a cabo una especialización de tipos de forma correcta, pero en Java todas las proyecciones de tipos se comprueban. Por tanto, aunque parezca que estemos utilizando simplemente una proyección de tipos nonnal, usando paréntesis, dicha proyección se comprueba en tiempo de ejecución para garantizar que se trate, de hecho, del tipo que creemos que es. Si no lo es, se obtiene una excepción ClassCastException. Este acto de comprobación de tipos en tiempo de ejecución se denomina información de tipos en tiempo de ejecl/ción (RTTI, rl/ntime type in/ormation). El siguiente ejemplo ilustra el comportamiento de RTTI: (( , polymorphism(RTTI . java II Especialización en información de tipos en tiempo de ejecución (RTTI) . (( {ThrowsException} cIass UsefuI { public void f 1) {} public void g 1) {} cIass MoreUseful extends Use fuI { pubIic void f 1) { } pubIic void gl) { } public void ul! {} public void vI! {} pubIic void w () { } pubIic cIass RTTI { pubIic static void main{String[] args) Useful[] x " { new Use fuI () , new MoreUseful () }; { 8 Polimorfismo 187 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 tambié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. mientras 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 producirse 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 especiali 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 acopIados. 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 diferencia, por ejemplo, del análisis de la instrucción switch), sino que funciona de manera concertada, como parte del esquema 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 necesario 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 normal y una interfaz. Aunque nuestro primer impulso pudiera ser crear una interfaz, la clase abstracta constituye una herramienta importante y necesaria para construir clases que tengan algunos métodos no implementados. No siempre podemos utili zar una interfaz pura. Clases abstractas y métodos abstractos En todos los ejemplos de "instmmentos musicales" del capítulo anterior,los métodos de la clase base Instrument eran siempre "ficticios". Si estos métodos llegan a ser invocados, es que hemos hecho algo mal. La razón es que lnstrument no tiene otro sentido que crear una intelfa= comlÍn para todas las clases derivadas de ella. En dichos ejemplos, la única razón para establecer esta interfaz común es para poder expresarla de manera diferente para cada uno de los distintos subtipos. Esa interfaz establece una forma básica, de modo que podemos expresar todo aquello que es común para todas las clases deri vadas. Otra fonna de decir esto sería decir que Instr ument es una clase base abstracta. o simplemente una clase abstracta. Si tenemos una clase abstracta como Instrument, los objetos de dicha clase específica no tienen ningún significado propio casi nunca. Creamos una clase abstracta cuando queremos manipular un conjunto de clases a través de su interfaz comú n. Por tanto, el propósito de lnstrument consiste simplemente en expresar la interfaz y no en una implementación concreta. por lo que no tiene sentido crear un objeto Instr ument y probablemente convenga impedir que el usua rio pueda hacerlo. Podemos impedirlo haciendo que todos los métodos de Instrument generen errores, pero eso retarda la infonnación hasta el momento de la ejecución y requiere que el usuario realice pruebas exhaustivas y fiables. Generalmente, resulta preferible detectar los problemas en tiempo de compilac ión. Java propo rciona un mecanismo para hacer esto denominado método abstracto. ¡ Se trata de un método que es incompleto: sólo tiene una declaración, y no dispone de un cuerpo. He aquí la si ntaxis para la declaración de un método abstracto: abstract void f(); Una clase que contenga métodos abstractos se denomina clase abstracta. Si una clase contiene uno o más métodos abstractos, la propia clase debe calificarse como abs tr aet, (en caso co ntrario, el compilador generará un mensaje de error). Si una clase abstracta está incompleta, ¿qué es 10 que se supone que el compi lador debe hacer cuando alguien [rate de insranciar un objeto de esa clase? El compilador no puede crear de manera segura un objeto de una clase abstracta, por lo que 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 abstract void playO; String whatO ¡r ... '1} abstraet voi d adjustO; exte ds Wind void playO String whatO void adjustO exte nds ext nds Percussion Stringed voi d playO String whatO void adjustO ext nds extr nds Brass Woodwind void playO String whatO void playO String whatO void adjustO void playO void adjustO 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.*; abstraet elass Instrument { private int i¡ /1 Storage allocated for eaeh publie abstraet void play {Note n)¡ publie String what () { return "Instrument" ¡ } publie abstraet void adjust () ; class Wind extends Instrument public void play (Note n ) { 9 Interfaces 191 + n); print ("Wind. play () public String what () public void adjust () { return "Wind" i {} } class Percussion extends Instrument { public void play (Note n) { print(IIPercussion.playO " + n) i { return npercussion"; {} public String what () public void adjust() clasE Stringed extends Instrument { public void play (Note n ) { print (" Stringed. play () 11 + n) i public String what () public void adj ust () { return "Stringed!1; {} class Brass extends Wind { public void play{Note n) print (JI Brass. play () 11 + n) public void adjust() i { print("Brass.adjust() n); class Woodwind extends Wind { public void play (Note n) { print("Woodwind.play() " + n); public String what () { return "Woodwind 11 i public class Music4 { // No me preocupa el tipo, por lo que los nuevos tipos /1 añadidos al sistema seguirán funcionando: static void tune (Instrument i) { // ... i.play{Note.MIDDLE_C) i static void tuneAll (Instrument [J tor (Instrument i e) tune (i) i e) { public static void main(String[] args) II Generalización durante la inserción en la matriz: Instrument[] orchestra = { new Wind (), new Percussion(), new Stringed () , new Brass () , new Woodwind () }; tuneAll(orchestra) i 192 Piensa en Java } / * 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 instancias 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 versió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étodo 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 implementación correspondiente. La implementación se proporciona de las clases que hereden de la clase actual. La palabra clave interface 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 especificar 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 palabra clave public antes de interface (pero sólo si dicha interfaz está definida en un archivo del mi smo nombre). Si no incluimos 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(); imPleTents impl ments Wind void play() String what() void adjust() Percussion void play() String what() void adjust() ext nds Stringed void play() String what() void adjust() extf nds Brass Woodwind void play() String what() imple~ents void play() void adjust() 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 estrictam 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 .* int erface I nstrumen t { II Constante de t i empo de compilación: int VALUE = 5; II static & final II No puede tener definiciones de métodos : voi d play (Not e n); II Automáticamente público voi d adjust() ¡ class Wind i mplements I nstrume nt { public v oid p l ay (Note n ) { print(t hi s +".p lay () "+ n) ¡ public String toString () { r eturn "Wind " ¡ } publ i c void adjust () {print (this + ". adjust () " ) ; 194 Piensa en Java class Percussion implements Instrument publ ic void play (Note n) { print (this + ".play() " + n); public String toString() { return uPercussion"; } public void adjust () { print (this + ". adjust () ") ; class Stringed implements Instrument public void play (Note n) { print (this + ".play() " + n) i public String toString () { return UStringed" i } public void adjust () { print (this + ". adjust () ,,) ; class Brass extends Wind { public String toString () { return uBrass" i } class Woodwind extends Wind { public String toString () { return "Woodwind!l; } public class MusicS { II No le preocupa el tipo, por lo que los nuevos tipos II que se añaden al sistema seguirán funcionando: static void tune (Instrument i) { // i.play(Note.MIDDLE_C) ; static void tuneAll (Instrument [] for(Instrument i : e) tune (i); e) { public static void main (String [] args) { II Generalización durante la inserción en la matriz: Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed () , new Brass () , new Woodwind () ); tuneAll(orchestra) ; 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 aparecer 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 paquete 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 apropiados) 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 subclases. 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 pueden 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 () ; Object process (Object input) { return input ¡ } class Upcase extends Processor { String process (Obj ect inputl { l/Retorno covariante return ((Stringl input ) .toUpperCase (); class Downcase extends Processor { String process (Object input ) { return «String) input) . toLowerCase () ; class Splitter extends Processor { String process (Object input) { II El método split() divide una cadena en fragmentos: return Arrays. toString ( ( (String) input) . spl i t ( " tl » ¡ 196 Piensa en Java public class Apply { public stacic void process (Processor p, Object s ) { print ( "Using Processor 11 + p. name () ) i print (p.process {s )) ; public sta tic String s "Disagreement with beliefs i5 by definition incorrect"; public static void main (String [ ) args ) { process (new Upcase () , s ) i process (new Downcase () , s) process (new Splitter() s) I i i 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 contiene 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; public class Waveform { private static long counter; private final long id = counter++; public String t o String () { return "Waveform " + id; } 1/ / , // : interfaces / filters / Filter.java package interfaces.filters; public class Filter { public String name () return getClass () .getSimpleName () ; public Waveform process (Waveform input ) { return input; / / /> // : interfaces / filters / LowPass.java package interfaces.filters; public class LowPass extends Filter { double cutoff; public LowPass (double cutoff) { this . cutoff public Waveform process (Waveform input ) return input; // Dummy processing cutoff; } } 9 Interfaces 197 /1: interfacesjfilters/HighPass.java package interfaces.filtersi public class HighPass extends Filter { double cutoff; public HighPass (double cutoff) { this. cutoEf :: cutoff i } public Waveform process (Waveform input) { return input; } ///,1/: interfaces/filters/BandPass.java package interfaces.filtersi public class BandPass extends Filter { double lowCutoff, highCutoff; public BandPass (double lowCut, double highCut) { lowCutoff = lowCut¡ highCutoff = highCut ¡ public Waveform process (Waveforrn input) { return input; } /// , Filter tiene los mismos elementos de interfaz que Processor, pero puesto que no hereda de Processor (puesto que el creador de la clase Filter no tenía ni idea de que podríamos querer usar esos objetos como objetos Processor). no podemos utilizar un objeto Filter con el método Apply.process( ), a pesar de que funcionaría. Básicamente, el acoplamiento entre Apply. process( ) y Processor es más fuerte de lo necesario y esto impide que el código de Apply.processO pueda reutilizarse en lugares que sería útil. Observe también que las entradas y salidas son en ambos casos de tipo Waveform . Sin embargo, si Processor es una interfaz, las restricciones se relajan lo suficiente como para poder reutili zar un método Apply. process( ) que acepte dicha interfaz. He aquí las versiones modificadas de Processor y Apply: //: interfaces/interfaceprocessor/Processor . java package interfaces.interfaceprocessor; public interface Proeessor { String name () ; Objeet process(Object input); /// ,//: interfaces/interfaeeproeessor/Apply.java package interfaees.interfaceprocessor¡ import static net.mindview.util.Print .* ¡ public class Apply { public static void process(Proeessor p, Objeet s) { print ("Using Processor ti + p. name () ) ; print(p.process(s» ; 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.*; publie abstraet class StringProcessor implements Processor{ publ ic String name () { return getClass () . getSimpleName () ¡ public abstraet String proeess(Objeet input); public static String s ~ "If she weighs the same as a duck, shets made of wood"; 198 Piensa en Java public static void main(String[] args) Apply.process(new Upcase(), s); Apply.process{new Downcase(), s); Apply.process(new Splitter(), s); class Upcase extends StringProcessor { public String process (Object input) { / / Retorno covariante return ((String) input) . toUpperCase () ; class Downcase extends StringProcessor { public String process (Object input) { return ((String ) input) . toLowerCase() ; class Splitter extends StringProcessor { public String process (Object input ) { return Arrays. toStríng ( ( (String) input ) . split (It " )) ; 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 desarrollarla. En estos casos, podemos utilizar el patrón de diseiio adaprador. Con dicho patrón de diseño, lo que hacemos es escribir 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.*¡ cIass FiIterAdapter implements Processor Filter filter¡ public FilterAdapter(Filter filter) { this.filter : filter¡ public String nameO { return filter.name() ¡ public Waveform process (Object input) { return filter.process((Waveform)input); public class FilterProcessor { public static void main(String[] args) { Waveform w = new Waveform(); Apply.process(new FilterAdapter(new LowPass(l.O)), w); Apply.process(new FilterAdapter (new HighPass(2.0)}, w) ¡ Apply.process( new FilterAdapter{new BandPass{3 . 0, 4.0 )), w) i 9 Interfaces 199 } / * 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 delegació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(). "Herencia múltiple" en Java Puesto que una interfaz no dispone de implementación (es decir, no hay ningún almacenamiento asociado con una interfaz) no hay nada que impida combinar varias interfaces. Esto resulta muy útil en ocasiones, como por ejemplo cuando queremos impleme ntar el concepto " una x es una a y una b y una c". En e++, este acto de combinar múltiples interfaces de clase se denomina herencia múltiple, y puede 1Iegar a resultar muy completo, porque cada clase puede tener una implementación. En Java, podemos hacer lo mismo, pero sólo una de las clases puede tener una implementación, por lo que los problemas de e++ no aparecen en Java cuando se combinan múltiples interfaces: Clase base I interfaz 1 abstracta o concreta I I Métodos de la clase base I interfaz 1 I interfaz 2 interfaz 2 1 ... ....... 1 ... I intet n I I interfaz n I En una clase deri vada , no estamos obligados a tener una clase base que sea abstracta o concreta (una que no tenga métodos abstractos). Pero si realizamos la herencia de algo que no sea una interfaz, sólo podemos heredar de una de esas clases; los restantes elementos base deberán ser interfaces. Hay que colocar todos los nombres de interfaz detrás de la palabra clave implements y separarlos mediante comas. Podemos incluir tantas interfaces como queramos y podemos realizar general izaciones (upeast) a cada interfaz, porque cada una de esas interfaces representa un tipo independiente. El siguiente ejemplo muestra una clase concreta que se combina con varias interfaces para producir una nueva clase: 11 : interfaces/Adventure.java II Interfaces múltiples . interface CanFight void fight () i interface CanSwim void swim ( ) i interface CanFly { 200 Piensa en Java voidfly(l; class ActionCharacter { public void fight 11 {} class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly public void swim (1 {} public void fly (1 {} public class Adventure { public public public public public static void t (CanFight x ) { x. fight () i static void u(CanSwim x l { x.swim (} i static void v (CanFly xl ( x. fly (); ) static void w (ActionCha racter xl { x. f ight () ; static void main (String [] args) { Hero h = new Hero( ) ; II II v(hl; II w(hl; II t(hl; u (hl; Treat i t Treat it Treat it Treat it as as as as a CanFight a CanSwim a CanFly an ActionCharacter ) 111> Puede ver que Bero combina la clase concreta ActionCharacter con las interfaces CanFight, CanSwim y CanFly. Cuando se combina una clase concreta con illlerfaces de esta forma, la clase concreta debe expresarse en primer lugar y las interfaces indicarse a continuación (en caso contrario, el compilador nos dará un error). La signatura de fight() es igual en la interfaz CanFight y en la clase ActionCharacter. Asimismo, a fight() no se le proporciona una definición en Hero. Podemos amp liar una interfaz, pero lo que obtenernos entonces será otra interfaz. Cuando queramos crear un objeto, todas las definiciones deberán haber sido ya proporcionadas. Aunque "ero no proporciona explícitamente una defini ción para fight(), di cha definición está incl uida en ActionCharacter; por tanto, es posi ble crear objetos Hero. En la clase Adventure, podemos ver que hay cuatro métodos que toman argumentos de las distintas interfaces y de la clase concreta. Cuando se crea un objeto Hero, se le puede pasar a cualquiera de estos métodos, lo que significa que estará siendo generalizado en cada caso a cada una de las interfaces. Debido a la forma en que se diseñan las interfaces en Java, este mecanismo funciona sin que el programador tenga que preocuparse de nada. Recuerde que una de las principales razones para utilizar interfaces es la que se ilustra en el ejemplo anterior: para reali za r generalizaciones a más de un tipo base (y pode r disfrutar de la fl exibilidad que esto proporciona). Sin embargo, una segunda razón para uti li zar interfaces coincide con la raZÓn por la que utili zamos clases base abstrac tas: para impedir que el programador de clientes cree un objeto de esta clase y para establecer que sólo se trata de una interfaz. Esto hace que surja una cuestión: ¿debemos utili zar una interfaz o una clase abstracta? Si resulta posible crear nuestra clase base sin ninguna definición de método y sin ninguna vari able miembro, siempre son preferibles las interfaces a las clases abstractas. De hecho, si sabemos que algo va a ser una clase base, podemos considerar si resultaría conveni ente transformarla en interfaz (hablaremos más sobre este tema en el resumen del capítu lo). Ejercicio 12: (2) En Adventure.java, añada una interfaz llamada CanClimb, sigu iendo el patrón de las otras interfaces. Ejercicio 13: (2) Cree una interfaz y herede de ella otras dos nuevas interfaces. Defina, mediante herencia múltiple, una tercera interfaz a partir de estas otras dos. 2 .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 Ampliación de la interfaz mediante herencia podemos añadir fácilmente nuevas dec laraciones de métodos a una interfaz utilizando los mecanismos de herencia, y también podemos combinar va rias interfaces mediante herencia para crear una nueva interfaz. En ambos casos, obtendremos una interfaz llueva, como se ve en el siguiente ejemplo: /1 : interfaces/HorrorShow.java JI Ampliación de una interfaz mediante herencia. interface Monster void menace () ; interface DangerousMonster extends Monster { void destroy () i interface Lechal void kill () ; c!ass DragonZilla implements DangerousMonster { public void menace () {} public void destroy () {} interface Vampire extends DangerousMonster, Lethal { void drinkBlood(); class VeryBadVampire implements Vampire { public void mena ce () {} pub1ic void destroy () {} pub1ic void kill () {} public void drinkB100d () {} public class HorrorShow { static void u(Monster b) { b.menace(); static void v (DangerousMonster d) { d.menace() ; d.destroy () ; static void w(Letha1 1 ) { l.kill( ) ; } public static void main(String[] args) DangerousMonster barney = new DragonZilla() u (barney ) i v (barney) i new VeryBadVampire() i Vampire vlad i u (v1 ad ) ; v(v1ad) ; w (vladl ; ) /// ,Dangcrous~lonster es una extensión simple de Monster que produce una nueva interfaz. Ésta se implementa en DragonZilla. 202 Piensa en Java 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 interfaz. 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. Colisiones de nombres al combinar interfaces Podernos encontramos con un pequeño problema a la hora de implementar múltiples interfaces. En el ejemplo anterior, tanto CanFight como ActionCharacter tienen sendos métodos idénticos void fight( ). El que haya dos métodos idénticos no resulta problemático, pero ¿q ué sucede si los métodos difieren en cuanto a signatura o en cuanto a tipo de retorno? He aquí un ejemplo: 11 : interfaces/lnterfaceCollision.java package interfaces; interface interface interface class e { Il void f(); } I2 int f(int i) ; int f (); } I3 public int f() { return 1 ; } } class C2 implements 11, 12 public void f () {} public int f (int i) { return 1; } II sobrecargado class C3 extends C implements 12 { public int f (int i ) { return 1; } II sobrecargado class C4 extends C implements 13 II I déntico. No hay problema: public int f () { return 1; } II Los métodos sólo difieren en el tipo de retorno: II! class CS extends C implements 11 {} //! interface 14 extends Il, I3 {} ///,La dificultad surge porque los mecanismos de anulación, de implementación y de sobrecarga se entremezclan de forma compleja. 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étodo idénticos. 9 Interfaces 203 Adaptación a una interfaz Una de las razones más importantes para utili zar interfaces consiste en que con ellas podemos di sponer de múltiples implementaciones para una misma interfaz. En los casos más simples, esto se lleva a la práctica empleando un méwdo que acepta una inte rfaz, lo que nos deja total libertad y responsabilidad para implementar dicha interfaz y pasar nuestro objeto a dicho método. Por tanto, uno de los usos más comunes para las interfaces es el patrón de diseño basado en estrategia del que ya hemos hablado: esc ribimos un método que realice ciertas operaciones y dicho método toma como argumento una interfaz que especifiquemos. Básicamente. lo que estamos diciendo es: " Puedes utilizar mi método con cualquier objeto que quieras, siempre que éste se adapte a mi interfaz". Esto hace que el método sea más flexible , general y reutili zable. Por ejemplo, el constructor para la clase Scanner de Java SES (de la que hablaremos más en detalle en el Capítulo 13 , Cadenas de caracteres) admite una interfaz Readable. Como veremos, Readable no es un argumento de ningún otro método de la biblioteca estándar de Java, fue creado pensando específicamente en Scanner, de modo que Scanner no tenga que restringir su argum ento para que sea una clase determinada. De esta forma , podemos hacer que Scanner funcione con más tipos de datos. Si creamos una nueva clase y queremos poder usarla con Scanner, basta con que la hagamos de tipo Readable, como por ejemplo: /1: interfaces/RandomWords . java /1 Implementación de una interfaz para adaptarse a un método. import java.nio. * ; import java.util .* ; public class RandomWords implements Readable { private static Random rand = new Random(47) ; private static final char[) capitals = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray () ; private static final char [] lowers = "abcdefghijklmnopqrstuvwxyz".toCharArray() ; private static final char[] vowels = "aeiou lt • toCharArray () ; private int caunt; public RandomWords (int count) { this .count count ¡ } public int read (CharBuffer cb) ( if(count-- == O) return -1; // Indica el final de la entrada cb.append(capitals[rand.nextlnt(capitals.length)]) ; for(int i = O; i <: 4; i++) { cb.append(vowels[rand.nextlnt(vowels.length)]) ; cb . append(lowers[rand.nextInt(lowers.length)]) ; cb.append(" ti); return 10; /1 Número de caracteres añadidos public static void main (S tring [] args ) { Scanner S = new Scanner(new RandomWords(10»¡ while(s.hasNext(» System.out.println(s . next(») ; / * 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. *¡ public class RandornDoubles private static Random rand = new Random(47)¡ publ ie double next () ( return rand . nextDouble () i publ ic static void main(String[] argsl { RandomDoubles rd = new RandomDoubles(); for(int i := Di i < 7; i ++) System.out . print(rd.next() + 11 "); 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. *¡ public class AdaptedRandornDoubles extends RandomDoubles implements Readable { private int count; public AdaptedRandornDoubles(int count} { this.count = count¡ public int read(CharBuffer cb) { if(count- - == O) return -1; String resul t = Double. toString (ne xt (» cb. append (resu lt) ; return result.length(); + U 11 ¡ public static void main (String [] args) { Scanner s = new Scanner(new AdaptedRandomDoubles(7» ¡ while(s.hasNextDouble () ) System.out.print(s.nextDouble() + i 11 " ) 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 verdadera 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. Campos en las interfaces Puesto que cualquier campo que incluyamos en una interfaz será automáticamente de tipo static y final , la interfaz constituye una herramienta convenicmc para crear gm pos de valores constantes. Antes de Java SES, ésta era la única fonna de produci r el mismo efecto que con la palabra clave enum en e o C++. Por tanto, resulta habitual encontrarse con código anterior a la versión Java SES que presenta el aspecto sigui ente: ji : interfaces / Months.java 1/ Uso de interfaces para crear grupos de constantes. package interfaces; public interface Months int JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4, MAY = S, JUNE = 6, JULY 7, AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10, NOVEMBER = 11, DECEMBER = 12, /// ,Observe la utilización del esti lo Java, en el que todas las letras están en mayúsc ulas (con guiones bajos para separar las dislintas palabras que formen un determinado identificador) en los casos de valores estáticos finales con inicializadores constantes. Los campos de una interfaz son automáticamente públicos, asi que el atributo public no se especifica explícitamente. Con Java SES. ahora disponemos de la palabra clave enum, mucho más potente y flexible, por lo que rara vez tendrá sentido que utilicemos interfaces para definir constantes. Sin embargo. quizá se encuentre en muchas ocasiones con esta técnica antigua a la hora de leer código heredado (los su plementos de este libro disponibles en Wlvlv.MindView.nef proporcionan una descripción completa de la técnica previa a Java SES para producir tipos enumerados utilizando interfaces). Puede encontrar más detalles sobre el uso de la palabra clave enum en el Capítulo 19, Tipos enumerados. Ejercicio 17: (2) Demuestre que los campos de una interfaz son implícitamente de tipo sta tic y final . Inicialización de campos en las interfaces Los campos defin idos en las interfaces no pueden ser valores "finales en blanco", pero pueden inicializarse con expresiones no constantes. Por ejemplo: 11 : interfaces / RandVals.java II Inicialización de campos de interfaz con II inicializadores no constantes. i mport java.util.*i public interface RandVals Random RAND = new Random (47 } ; int RANDOM_INT = RAND.nextInt (lO } ; long RANDOM_ LONG = RAND.nextLong () * 10; float RANDOM_FLOAT = RAND.nextLong() * 10; double RANDOM_DOUBLE = RAND.nextDouble ( ) * 10; /// ,Puesto que los campos son estáticos, se inicializan cuando se carga por primera vez la clase, lo que tiene lugar cuando se accede por primera vez a cualquiera de los campos. He aquí una prueba simple: 11 : interfaces / TestRandVals.java import static net.mindview.util.Print.*; public class TestRandVals { 206 Piensa en Java public static void main (String [] args ) print (RandVals.RANDOM INT ) i print (RandVals.RANDOM LONG ) ; print {RandVals.RANDOM FLOAT ) i print (RandVals.RANDOM DOUBLE ) ; ( / * 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 class Blmp implements B { public void f () {} private class Blmp2 implements B { public void f () {} public interface C { void f () ; class Clmp implements C { pUblic void f () {} private class Clmp2 implements C { public void f () {} private interface O { void f () ; private class Dlmp implements O { public void f () {} public class Dlmp2 implements O ( public void f () {} public D getD () { return new DImp2 (); pri vate O dRef; public void receiveD (D d) { dRef = d; dRef. 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 {} public class Nestinglnterfaces public class Blmp implements A.S public void f 1) {} class Clmp implements A.e { public void f () {} // No se puede implementar una interfaz privada excepto JI dentro de la clase definitoria de dicha interfaz : // ! class Dlmp implements A . D { II ! public void II ! } f () {} class Elmp implements E public void 9 1) {} class EGlmp implements E.G { public void f 1) {} clas s Elmp2 implements E { public void 9 1) {} class EG implements E . G public void f () {} public static void main (String [] A a = new A () i JI No se puede acceder a A.D: II! A.O ad = a.getOI ); JI args) { S610 puede devolver a A.O: II ! A.Olmp2 di2 = a . getOI ) ; JI No se puede acceder a un miembro de la interfaz: II! a.getOI ) .f(); / / S610 otra A puede utilizar getD () : A a2 = new A () ; a2.receiveD(a.getD()) ; } 111 ,La sintaxis para anidar una interfaz dentro de una clase es razonablemente amplia. Al igual que las interfaces no anidadas, las anidadas pueden lener visibilidad pública o con acceso de paquele. Como característica adicional, las interfaces también pueden ser privadas, como podemos ver en A.D (se necesita la misma sintaxis de cualificación para las interfaces anidadas que para las clases anidadas). ¿Para qué sirve una interfaz anidada privada? Podemos suponer que sólo puede implementarse como clase interna privada, como en DImp, pero A.DImp2 mues- 208 Piensa en Java 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(); class Implementationl implements Service { Implementationl () {} /1 Package access public void methodl () {print (" Implementationl methodl");} public void method2 () {print ( "Implementationl method2");} class ImplementationlFactory implements ServiceFactory { public Service getService () ( return new Implementationl{); class Implementation2 implements Service { Implementacion2 () {} /1 Acceso de paquete public void methodl () {print (" Implementation2 methodl n) j} 9 Interfa ces 209 public void method2 () {print (JI Implementation2 method2") i } class Implementation2Factory implements ServiceFactory { public Service getService () { return new Implementacion2(); public c!ass Factaries { public static void serviceConsumer(ServiceFactory face) Service s = fact . getService(); s.methadl() ; s.method2() ; public static void main(String[] args ) serviceConsumer(new ImplementationlFactory()) i // Las implementaciones son completamente intercambiables: serviceConsumer(new Implementation2Factory()); / * Output: Imp lementacianl Implementatian! Implementation2 Imp lementation2 methadl mechod2 methadl 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 estuviera 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 desarrollo. 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.*¡ interface Game { boolean move(); } inter face GameFactory { Game getGame(); class Checkers implements Game { private int moves = O; private static final int MOVES = 3; public boolean move () { print ("Checkers move " + moves); return ++moves != MOVES; class CheckersFactory implements GameFactory { public Game getGame() { return new Checkers() ¡ class Chess implements Game { private int moves = O; private static final int MOVES public boolean move () { print (ItChess move ti + moves); return ++moves != MOVES; 4; 210 Piensa en Java class ChessFactory implements GameFactory { public Game getGame () { return new Chess () i public clas9 Games { public static void playGame(GameFactory factory) Game s = factory getGame(); while(s.move()) { public static void main(String[] args) playGame(new CheckersFactory{)); playGame (n ew ChessFactory()); / * 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 desarrollar 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 concretas. 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 partes, 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 controlar 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 detalles 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 utilizarlas en los di seilos. La necesidad de las clases internas no siempre resulta obvia, pero después de describir la sintaxis básica 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. Creación de clases internas Para crear una clase interna, el procedimiento que se utiliza es el que cabría suponer: la definición de la clase se incluye dentro de otra clase contenedora: 11 : innerclasses / Parcell.java II Creación de clases internas. public class Parcel1 { class Contents { private int i = 11; public int value () { return i ¡ } cla ss Destination { privace String label; Destination(String whereTo ) label = whereTo¡ String readLabel () { return label; } II La utilización de clases internas se asemeja II a la de cualquier otra cl ase, dentro de Pareel1: 212 Piensa en Java public void ship (String dest) { Contents e = new Contents(); Destination d = new Destination(dest); System.out.println(d.readLabel(» i public static void main(String[] args) Parcell p = new Pareell(); p. ship (UTasmania " ) i / * Output: Tasmania * /// , Las clases internas utili zadas dentro de ship( ) parecen c lases n0n11ales. Aquí, la única diferencia práctica es que los nombres 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. public class Parcel2 class Contents { private int i : 11; public int value () { return i; } class Destination { private String label¡ Destination{String whereTo) label = whereTo¡ String readLabel {) { return label; public Destination to (String s) return new Destination(s) ¡ { public Contents contents() return new Contents()¡ public void ship{String dest) Contents c : contents{); Destination d : to{dest) ¡ System.out.println{d.readLabel()) i public static void main(String[] args) Parcel2 p = new Parcel2() ¡ p.ship{"Tasmania " ) i Parce!2 q = new Parcel2{)¡ II Definición de referencias a clases internas: Parcel2.Contents e = q.contents{) i Parce!2.Destination d = q . to(IBorneo") i 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étodo a Outer que de vuel va un objeto de tipo Inn er . En main(), cree e inicialice una referencia a un objeto Jon er . El enlace con la clase externa Hasta ahora, parece que las clases internas 5011 simplemente un esquema de organización de cód igo y de ocultación de nombres. lo cua l resulta útil pero no especialmente necesario. Sin emba rgo, las cosas son más complejas de lo que parecen, cuando se crea una clase interna, cada objeto de esa clase interna di spone de un enlace al objeto contenedor que lo ha creado. por lo cual puede acceder a los miembros de dicho objeto contenedor sin utilizar ninguna cualificación especial. Además, las c lases internas ti enen derechos de acceso a todos los e lementos de la clase contenedora.! El sigu iente eje mp lo ilustra esta caracleristica: 11 : innerclasses / Sequence . java II Almacena una secuencia de objetos. i nterface Selector { boolean end () ; Object current () ; v o id next ( ) ; public class Sequence { private Object [] items; private int next = O; public Sequence (int size ) public void add (Object x ) if(next < items.length) items[next++] items new Object [size]; } = Xi private class SequenceSelector implements Selector private int i = O; public boolean end () { return i == items.length; publ ic Obj ect current ( ) { return i tems [i]; } public void next () { if (i < items . length ) i++; } public Selector selector () { return new SequenceSelector() i public static void main(String[] argsl { Sequence sequence = new Sequence(lO); for(int i = O; i < 10; i++} sequence.add {Integer . toString(i» ; Selector selector = sequence.selector( } ; while ( ! selector. end (l) { System.out.print {selector.current () + selector.next( ) ; " ") i 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 detalle 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 pueden 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 creació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 ejemplo 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 contenedora. Sin embargo, la clase interna puede acceder a los métodos y campos de la clase contenedora como si fueran de su propiedad. 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 creación. Entonces, cuando hacemos referencia a un miembro de la clase contenedora, dicha referencia se utiliza para seleccionar 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 referencia 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 constructor) e lnner tenga un método toString( ) que muestre este campo. Cree un objeto de tipo (nner y visualicelo. Utilización de .this y .new Si necesita generar la referencia al objeto de la clase externa, basta con indicar el nombre de la clase externa seguido de un punto y de la palabra clave this. La referencia resultante tendrá automáticamente el tipo correcto, que se conoce y se comprueba en tiempo de compilación, por lo que no hay ningún gasto adicional en tiempo de procesamiento. He aquí un ejemplo que muestra cómo utilizar .this: ji : innerclasses/DotThis.java 1/ Cualificación del acceso al objeto de la clase externa. public class DotThis void f () { System.out.println("DotThis.f() ") i } public class Inner { public DotThis outer () { return DotThis.this¡ jj Un "this" haría referencia al "thisl! de Inner public Inner inner () { return new Inner () ; public static void main (St ring [) args) { DotThis dt = new DotThis() ¡ DotThis.Inner dti = dt.inner() ¡ 10 Clases internas 21 5 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 necesario 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. public class DotNew ( public class Inner {} public static void main(String(] args ) DotNew dn = new DotNew( ) ; DotNew.lnner dni = dn.new Inner()¡ 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 embargo, 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. pu blic class Parcel3 cl ass Contents { private int i = 11¡ public int value () { return i ¡ } c lass Destination { private String label¡ Destination (String whereTo ) { label = whereTo¡ String readLabel () { return label ¡ } } public static void main{String[] args) Parcel3 p = new Parcel3{); JI Hay que usar una instancia de la clase externa II para crear una instancia de la clase interna: Parcel3.Contents e = p . new Contents(); Parcel3 . Destination d = p.new Destination(ITasmania"); } 111 > Ejercicio 4: (2) Añada un método a la clase Sequ ence.Seq ue nceSelecto r que genere la re ferencia a la clase ex terna Sequ ence. Ejercicio 5: ( 1) Cree una clase con una clase interna. En otra clase separada, cree una instancia de la clase interna. 216 Piensa en Java Clases internas y generalización Las clases internas muestran su utilidad real cuando comenzam os a generalizar a una clase base y, en particular, a una inter_ faz. (El efecto de generar Wla referencia a una interfaz a partir de un objeto que la implemente es prácticamente el mismo qu e el de reali zar una generalización a una clase base). La razón es que entonces la clase interna (la implementación de la interfaz) puede ser no visible y estar no disponible, lo cual resulta muy útil para ocultar la implementación . Lo único que se obtiene es una referen cia a la clase base o a la interfaz. Podemos crear interfaces para los ejemplos anteriores: 11: innerclasses/Destination. java public interface Destination { String readLabel() i } 111,Ij : innerclassesjContents . java public inter f ace Contents { int value () ; } 1110Ahora Contents y Destination rep resentan interfaces disponibles para el programador de clientes. Rec uerde que una interfaz hace que todos sus miembros sean automáticamente públicos. Cuando obtenemos ulla referencia a la clase base o a la interfaz, es posible que no podamos averiguar el tipo exacto, como se muestra en el siguiente ejemplo: JI : innerclassesjTestParcel . java class Parcel4 { private class PCont en t s implements Contents { private i n t i = 11; public int val ue () { return i; } protected c l ass PDestination implements De stination private String label; private PDestination (String whereTo) { label = whereTo¡ public String readLabel () { return label; public Destination destination (String s) return new PDestination(s); { public Contents contents() return new PContents(); publie class TestParcel { public static void main (String [] args) { parcel4 p = new PareeI4() i Contents c = p.contents(); Destination d = p . destination(UTasmania U ) ; IJ Ilegal -- no se puede acceder a la clase privada : JI! Parcel4 . PContents pe = p . ne w PConte n ts(); } 111 ,En Parcel4 hemos añadido algo nuevo. La clase interna PContents es priva te, así qu e sólo puede acceder a ella Parcel4 . Las clases normales (no internas) no pueden ser privadas o protegidas; sólo pueden tener acceso público o de paquete. PDestination es protegida, por lo que sólo pueden acceder a ella Parcel4, las clases contenidas en el mismo paquete (ya 10 Clases in lemas 217 que protected también proporciona acceso de paquete) y las clases que hereden de Parcel4. Esto quiere decir que el programador de clientes tiene un conocimiento de estos miembros y un acceso a los mismos restringido. De hecho, no podemos 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 completamente 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 paquete 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 protegida, 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. Clases internas en los métodos y ámbitos Lo que hemos visto hasta ahora son los usos típi cos de las clases internas. En gene ral , el código que escribamos y el que podamos leer donde aparezcan clases internas estará compues to por clases internas "simples" que resulten fáciles de comprender. Sin embargo, las sintaxis de las clases internas abarca varias otras técnicas más complejas. Las clases internas pueden crearse dentro de un método o incluso dentro de un ámbito arbi trario. Existen dos razones para hacer esto: 1. Como hemos visto anterionllente, podemos estar implementando una interfaz de algún tipo para poder crear y devolver una referencia. 2. Podemos estar tratando de resolver un problema complicado y queremos crear una clase que nos ayude a encontrar la solución, pero sin que la clase esté públicamente disponible. En los siguientes ejemplos, vamos a modificar el código anterior para utilizar: 1. Una clase definida dentro de un método 2. Una clase definida dentro de un ámbito en el interior de un método 3. Una clase anónima que implemente una interfaz 4. Una clase anónima que amplíe una clase que disponga de un constructor no predeterminado 5. Una clase anónima que se encargue de la inicialización de campos 6. Una clase anónima que lleve a cabo la construcción utilizando el mecanismo de inicialización de instancia (las clases internas anónimas no pueden tener constmctores). El primer ejemplo muestra la creación de una clase completa dentro del ámbito de un método (en lugar de dentro del ámbito de otra clase). Esto se denomina clase interna local: 11 : innerclasses / ParcelS. java 11 Anidamiento de una clase dentro de un método. public class ParcelS { public Destination destination (String s) { class PDestination implements Destination private String label; private PDestination (String whereTo ) { label = whereTo ¡ 218 Piensa en Java public String readLabel () { return label; } return new PDestination(s); public static void main (String [] args) { ParcelS p = new Parce15(); Destination d = p.destination{tlTasmania tl ); 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. public class Parce16 { private void internalTracking(boolean b ) { Hlbl { class TrackingSlip { private String id; TrackingSlip (String s) id = Si { String getSlip I I { return id; } TrackingSlip ts = new TrackingSlip ("s lip" ) ; String s = ts.getSlip() i ji ¡NO se puede usar aquí! Fuera de ámbito: jj! TrackingSlip ts = new TrackingSlip("x tl ) ; public void track() { internalTracking(true}; public static void main{String[] args) { Parce16 p = new Parce16(); p. track (); La clase TrackingSlip está anidada dentro del ámbito de una instrucción ir. Esto 00 quiere decir que la clase se cree condicionalmente; 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 Clases internas anónimas El siguiente ejemplo puede parecer un tanto extraño: JI : innerclasses/Parce17.java // Devolución de una instancia de una clase interna anónima. public class Pareel7 public Contents contents() return new Contents () { / / Inserte una definición de clase private int i = 11¡ publ ic int value () { return i i }¡ /1 En este caso hace falta el punto y coma public static void rnain(String[] args) Pareel7 p = new Parce17()¡ Contents e = p.contents(); { 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 } public Contents contents () { return new MyContents () public static void main(String[] argsl { Parce17b p new Parce17b(); Contents c = p.contents(); j En la clase interna anónima, Co ntents se crea utilizando un constructor predete rminado. El siguiente código muestra lo que hay que hacer si la clase base necesita un constructor con un argumento: 11 : innerclasses/ParceI8.java II Invocación del constructor de la clase base. public class Parcel8 { public Wrapping wrapping(int xl II Llamada al constructor de la clase base: return new Wrapping(x) { II Pasar argumento del constructor. public int value () { return super. value () * 47; }i II Punto y coma necesario public sta tic void main (String [] argsl { 220 Piensa en Java Parcele p = new Parcel8 () ; Wrapping w = p.wrapping (lO ) ; 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 expresió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. public class Parcel9 { /1 El argumento debe ser final para poder utilizarlo /1 dentro de la clase interna anónima: public Destination destination (final String dest ) ( ret u rn new Destination () { private String label = dest; public String readLabel () { return label; } }; public static void main (String [] args l ( Parcel9 p = new ParceI9 () ; Destination d = p.destination ( IOTasmania lO ) ; 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 adecuada. ¿Pero qué sucede si necesitamos reali zar algún tipo de actividad similar a la de los constmclO res? No podemos disponer 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.*; abstract class Base { public Base (int i) { print ( "Base constructor, i public abstract void f () ; " + i); 10 Clases internas 221 pUblic class AnonyrnousConstructor { public static Base getBase(int i) return new Base (i) { { print(IIInside instance initializer") public void f () { print ( " In anonymous f () " ) ; i } }; public stati c void main (String [] args) getBase(47) ; Base base base . f (1 ; / * 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 . public class ParcellO public Destination destination(final String dest, final float price) { return new Dest ination () { private int cost¡ II Inicialización de instancia para cada objeto : ( cost = Math.round(price); if (cost > 100) System . out . println ( "Over budget!"); private String label = dest; public String readLabel () { return label j } }; public static void main{String[] args) ( ParcellO p = new ParcellO(); Destination d = p.destination{"Tasmania", lOl.395F) i 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 podemos 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, porque 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. Un nuevo análisis del método factoría Observe que el ej emplo interfaces/ Facto r ies.java es mucho más atracti vo cuando se utilizan clases internas anónimas: JI: innerc l asses/Factories . java import static net . mindview . util . Print .* ¡ interface Service void methodl () i void method2 () i interface ServiceFactory Service getService( ) i class Implementationl implements Se r vice { private Implementationl () {} public void methodl () {print ( " Implementationl methodl" ) ¡ } public void method2 () {print ( !I Implementationl method2" ) ;} public static ServiceFactory factory new ServiceFactory () { public Service getService () { return new Implementationl () ; } }; class Implementation2 implements Service { private Implementation2 () { } public void methodl () {print ( " Implementation2 methodl" ) ¡ } public void method2 () {print ( " Implementation2 method2" ) ; } public static ServiceFactory factory new ServiceFactory () { public Service getService () { return new Implementation2( ) ; } }; public class Factories { public static void serviceConsumer(ServiceFactory fact ) Service s = fact.getService () ¡ s . methodl (} ; s.method2 () ; public static void main (String{) args ) { serviceConsumer ( Implementationl . factory ) ; 10 Clases internas 223 /1 Las implementaciones son completamente intercambiables : serviceConsumer{Implementation2.factory) i 1* Output: Implementationl rmplementationl Implementation2 Implementation2 methodl method2 methodl 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 .* interface Game ( boolean move() i } interface Game Factory ( Game getGame(); class Checkers implements Game { private Checkers () {} private int moves = O; private static final int MOVES = 3i public boolean move() { print ("Checkers move " + moves); re t urn ++moves != MOVESi public static GameFactory factory = new GameFactory() public Game getGame () { return new Checkers () i } }; class Chess implements Game private Chess() {} private int moves = O; private static final int MOVES = 4; public boolean move () print ("Chess move " + moves); return ++moves != MOVES; public static GameFactory factory = new GameFactory(} public Garne getGame () { return new Chess (); } }; public class Garnes { public static void playGame(GameFactory factory) Garne s = factory getGame(); while{s.move()) public static void main(String[] args) playGame(Checkers . factory) ; playGame(Chess . factory) ; /* 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 definir la clase interna como estática. Esto es lo que comúnmente se denomina una clase anidada. 2 Para comprende r el signi ficado 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 clases 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). public class Parcel11 { private static class ParcelContents implements Contents { private int i = 11; public int value() { return i¡ } protected static class ParcelDestination implements Destination private String label¡ private ParcelDestination (String whereTo ) { label = whereTo¡ public String readLabel () { return label ¡ } jj Las clases anidadas pueden contener otros elementos estáticos: pubEe statie void f () {) sta tic int x = 10; static class AnotherLevel { pubEe statie void f (1 {) static int x = 10; 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 sueede en Java. 10 Clases internas 225 public static Destinatían destination(String s) return new ParcelDestination(s) i { public static Contents contents() return new ParcelContents() i public static void main(String[J args) Contents e = contents () i Destinatian d = destinatían ("Tasmania" ) i } 111 ,En main(), no es necesario ningún objeto de Pareel1 ); en su lugar, utilizamos la sintaxis nornlal para seleccionar un mi embro estático con el que invocar los métodos que devuelven referencias a C ontents y Desti nation. Como hemos visto anterionnente en el capítulo. en una clase interna normal (no estática), el vínculo con el objeto de clase externa se utili za empleando una referencia this especiaL Una clase anidada no tiene referenc ia this especial, lo que hace que sea análoga a un mélodo eSlálico. Ejercicio 18 : (1) Cree una clase que contenga una clase anidada. En ma in( ), cree una instancia de la clase anidada. Ejercicio 1 9 : (2) Cree una clase que contenga una clase interna que a su vez contenga otfa clase interna. Repita el proceso utilizando clases anidadas. Observe los nombres de los archivos .class generados por el compilador. Clases dentro de interfaces Normalmente, no podemos incluir cualquier código dentro de una interfaz, pero una clase anidada puede ser parte de Ulla interfaz. Cualquier clase que coloquemos dentro de una interfaz será automáticamente pública y estática. Puesto que la clase es estática no viola las reg las de las interfaces: simplemente, la clase anidada se incluye dentro del espacio de nombres de la interfaz. Podemos incluso implementar la interfaz contenedora dentro de la clase contenedora de la fonna siguiente, como por ejemplo en: jJ: innerclasses/classInInterface . java jJ {main: Classlnlnterface$Test} public interface ClassInInterface void howdy () i class Test implements ClasslnInterface public void howdy () { System.out.println(HHowdy! ") i public static void main (String [J args) new Test () . howdy () i { 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 emplear 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 mecanismo 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} public class TestBed { 226 Piensa en Java public void f{) { System.out.println(tlf() 11); public statie elass Tester { public static v oid main (String [] args) { TestBed t = new TestBed(); t . f () ; 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. Acceso al exterior desde una clase múltiplemente anidada No importa con qué profundidad pueda estar anidada una clase interna: la clase anidada podrá acceder transparentemente a todos los miembros de todas la clases dentro de las cuales esté anidada, como podemos ver en el siguiente ejemplo:3 11 : innerclasses/MultiNestingAeeess.java II Las clases anidadas pueden acceder a todos los miembros de II todos los niveles de las clases en las que está anidada. elass MNA private void f () {} class A pri vate void 9 () {} publ ie elass B { void h() ( g (); f (); publie elass MultiNestingAeeess { publie static void main{String[] MNA mna = new MNA () ; MNA.A mnaa = mna.new A{); MNA.A.B mnaab = mnaa.new B (); mnaab.h() ; args ) { 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. 3 Gracias de nuevo a Martin Danncr. 10 Clases internas 227 ¿Para qué se usan las clases internas? Hasta este momento, hemos analizado buena parte de los detalles sintácticos y semánticos que describen la fanna de funcionar de las clases internas. pero esto no responde a la pregunta de para qué sirven las clases internas. ¿Por qué los diseñadores de Java se tomaron tantas molestias para añadir esta característica fundamental al lenguaje? Nonnalmente, la clase interna hereda de otra clase o implementa una interfaz y el código incluido en la clase interna manipula el objeto de la clase externa dentro del cual hubiera sido creado. Así pues, podríamos decir que una clase interna proporciona lm3 especie de ventana hacia la clase externa. Una de las cuestiones fundamentales acerca de las clases internas es la siguiente: si simplemente necesitamos una referencia a una interfaz, ¿por qué no hacemos simplemente que la clase externa implemente dicha interfaz? La respuesta es que: "Si eso es todo lo que necesita, entonces esa es la manera de hacerlo". Por tanto, ¿qué es lo que distingue una clase interna que implementa una interfaz de una clase externa que implementa la misma interfaz? La respuesta es que no siempre disponemos de la posibilidad de trabajar con interfaces, sino que en ocasiones nos vemos forzados a trabajar con implementaciones. Por tanto, la razón más evidente para utilizar clases internas es la siguiente: 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 problemas 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 internas 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 {}; public class Multilnterfaces static void takesA (A al {} static void takesB (8 b) {} public static void main (String [) argsl X x = new X (l i y Y = new Y () i takesA (x) i takesA (y) i takesB (x) i takesB(y . makeB()) i { 228 Piensa en Java 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 utilizada 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 internas 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) {}; public class Multilmplementation static void takesDID d ) {} static void takesE(E e) {} public static void main(String[] Z z '" new Z () ; takesD{z) ; takesE(z.makeE{)) ; } args ) { 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ísticas adicionales: 1. La clase interna puede tener múltiples instancias, cada una con su propia infonnación de estado que es independiente 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 objeto Selecto r ", y sólo podría existir un objeto Selec to r para cada objeto Sequ ence concreto. Sin embargo, podríamos fácilmente 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 configure una referencia en la matriz (especificada mediante el argumento del método) con el valor null, y un terce 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 cuenta 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 automá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 permitir 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 lenguaje 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 punteroS. Veamos un ejemplo: 11 : i nnerclasses/Callbacks .java Utili za c i ón de clases internas para las retrollamadas package innerclassesi import static net . mindview . util .Print .*i II interface Incrementable void increment() i II Muy simple class Calleel private int public void para limitarse a implementar la interfaz: implements I ncrementab l e { i = Oi increment () { i++i print (i) i class Mylncrement { public void increment () print ("Other operation") static void f(Mylncrement mi) { mi. increment () ; } i II II Si nuestra clase debe implementar increment() de alguna otra forma, es necsario utilizar una clase interna: class Callee2 extends Mylncrement private int i = Oi public void increment() super.increment(} i i++i print (i) i private class Closure implements Incrementable { publ ic void increment () { II Especifique un método de la clase externa ; en caso II contrario, se produciría una recursión infinita: Callee2 . this.increment() i Incrementable getCallbackReference(} return new Closure() i 230 Piensa en Java elass Caller { private Inerementable eallbaekReferenee; Caller (Inerementable ebh) { eallbaekReferenee void 90 () { eallbaekReference. inerement (); } cbh; } publie elass Callbaeks { publíe statíc voíd maín (String [] args) { Calleel el = new Calleel(); Callee2 e2 = new Callee2()¡ Mylnerement.f{c2)¡ Caller callerl new Caller{el)¡ Caller ealler2 = new Caller(e2.getCallbaekReferenee()); callerl. 90 () ¡ eallerl. 90 () ; caller2 .90 () ; caller2. 90 () ¡ / * 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 sustituido 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 separació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 increment() 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 invocado 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). Clases internas y marcos de control Un ejemplo más concreto de l uso de clases internas es el que puede encontrarse en lo que de nominamos marco de control. 10 Clases internas 231 Un marco de lrabajo de una aplicación es una clase o un conjunto de clases diseñado para resolver un tipo concreto de problema. 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étodos 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 pennanece 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 responder 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 infonnació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 verdadera 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¡ public abstraet class Event { private long eventTime¡ protected final long delayTime¡ publie Event (long delayTime) { this.delayTime = delayTime¡ start () ; public void start() { II Permite la reinicialización eventTime = System.nanoTime() + delayTime¡ public boolean ready () { return System.nanoTime () >= eventTime¡ public abstraet void action() ¡ ///,El constructo r captura el tiempo (medido desde el instante de creación del objeto) cuando se quiere ejecutar el objeto Event, y luego invoca start( ), que toma el instante actual y añade el retardo necesario, con el fin de generar el instante en el que el suceso tendrá lugar. En lugar de incluirlo en el constructor, start() es un método independiente. De esta forma, se puede reinicializar el temporizador después de que el suceso haya caducado, de manera que el objeto Event puede reutilizarse. Por ejemplo, si queremos un suceso repet iti vo, podemos invocar simplemente start() dentro del método action(). rcady() nos dice cuándo es el momento de ejecutar el método .ction( ). Por supuesto, ready() puede ser sust ituido en una clase derivada. con el fin de basar el suceso Event en alguna otra cosa distinta del tiempo. El siguiente archivo contiene el marco de control concreto que gestiona y dispara los sucesos. Los objetos Event se almacenan dentro de un objeto contenedor de tipo List (una lista de sucesos), que es un tipo de objeto que analizare- 232 Piensa en Java 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.*; public class Controller 1I Una clase de java.util para almacenar los objetos Event: private List eventList new ArrayList () i public void addEvent (Event c ) ( eventList.add (c ) i } public void run () ( while {eventList.size {) > O) II Hacer una copia para no modificar la lista II mientras se están seleccionando sus elementos: for {Event e : new ArrayList(eventList ) i f ( e . ready ( )) ( System.out.println {e) ; e.action {) ; eventList.remove {e ) i 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últiples 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 JI de control, dentro de una única clase. Las clases JI internas permiten encapsular diferente funcionalidad 1/ para cada tipo de suceso. import innerclasses.concroller .* ; public class GreenhouseControls extends Controller { private boolean light = false; public class LightOn extends Event public LightOn(long delayTime) public void aetian () { { super (delayTime) ; /1 Poner código de control del hardware aquí 1/ para encender físicamente las luces. light = true; public String toString() { return "Lighe is on" ; } public class LightOff extends Event public LightOff (long delayTime) public void aetian () { { super (delayTime); JI Poner código de control del hardware aquí para apagar físicame nte las luces . light = false¡ JI public String toString {} { return "Light is off" ¡ } private boolean water = false¡ public c l ass WaterOn extends Event public WaterOn (long delayTimel { super (delayTime) ¡ public void action () { II Poner el código de control del hardware aquí. water = true; public String toString(} return !1Greenhouse water is on" i public class WaterOff extends Event public WaterOff ( long delayTime) super(delayTimel¡ public void action () { II Poner el código de control del hardware aquí. water = false¡ public String toString() return "Greenhouse water is off" i private String thermostat = "Day" ¡ public class ThermostatNight extends Event public ThermostatNight (long delayTime) { super (delayTime) ; public void action() II Poner el código de control del hardware aquí. thermostat = " Night"; public String toString() return uThermostat on night setting lt ¡ 234 Piensa en Java public class ThermostatDay extends Event public ThermostatDay{long delayTime) { super (delayTime ) i public veid actien () // Poner el código de control del hardware aquí. thermostat = "Day" ¡ public String toString{) return IIThermostat on day setting" i } // 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)); public String toString () { return "Bing!" i public class Restart extends Event { private Event[] eventList¡ public Restart (long delayTime, Event [] eventList) { super {delayTimel ; this . eventLis t = eventList¡ for{Event e : eventListl addEvent (e) i public void actien () fer(Event e : eventList ) e.start(); // Re-ejecutar cada suceso. addEvent (el i start(); // Re-ejecutar cada suceso addEvent (t his ) i public String toString() return "Restarting system"; public static class Terminate extends Event { public Termina te (long delayTime) { super (delayTime) ; public void action () { System.exit(O) ¡ } public String toString () { return "Terminating" i } ///,Observe que light, water y thermostat pertenecen a la clase externa GreenhouseControls. a pesar de lo cual las clases internas pueden acceder a dichos campos si n ninguna cualificación y sin ningún penniso especial. Asimismo, los método:t action() suelen requerir algún tipo de control del hardware. La mayoria de las clases Event parecen similares, pero BeJl y Restart son especiales. B.JI hace sonar una alarma y luego añade un nuevo objeto SeU a la lista de sucesos, para que vuelva a sonar posteriormente. Observe cómo las clases internas casi parecen un verdadero mecanismo de herencia múltiple. SeU y Restart tienen todos los métodos de Event y también parecen tener todos los métodos de la clase externa GreenhouseControls. A Restart se le proporciona una matri z de objetos Event y aquélla se encarga de añadirla al controlador. Puesto que Restart() es simplemente otro objeto Event, también se puede añadir un objeto Restart dentro de R.start.action() para que el sistema se reinicialice a sí mismo de manera periódica. 10 Clases internas 235 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 .* ¡ public class GreenhouseController public static void main (String[] args) GreenhouseControls gc = new GreenhouseControls( ) i /1 En lugar de fijar los va l ores, podríamos analizar JI información de con f iguración incluida JI en un archivo de tex to: gc . addEvent (gc.new Bell(900)) Event(] eventList = { gc .new ThermostatNight(O), gc.new LightOn(200), gc .new LightOf f (400), gc.new WaterOn(600), gc . new WaterOff(800 ) , gc . new ThermostatDay(1400) i }; gc.addEvent(gc . ne w Restart(2000, eventList)) if(args . length == 1) gc .addEvent( new GreenhouseControls . Terminate( new Integer(args[OI)) ) i gc . run () i i 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 comandos 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 utili zan las clases internas para definir las acciones de una interfaz gráfica de usuario. Al tenninar ese capítulo, espero haberle 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 apagar 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. Cómo heredar de clases internas Puesto que el constructor de la clase interna debe efectuar la asociación como una referencia al objeto de la clase contene· dora, las cosas se complican ligeramente cuando fratamos de heredar de Ulla clase interna. El problema es que la referencia "secreta" al objeto de la clase contenedora debe inicializarse. a pesa r de lo cual en la clase derivada no hay ningún objeto predetenninado con el que asocia rse. Es necesario utili zar una sinrax is especial para que dicha asociación se haga de fonna explíc ita: // : innerclassesjlnheritlnner.java jI Heredando de una clase interna. class Withlnner class Inner {} public class Inheritlnner extends WithInner.Inner II! Inheritlnner () {} lINo se compilará Inheritlnner (Withlnner wi) { wi. super () ¡ public static void main (S tring [) args) { Withlnner wi = new Withlnner() ¡ Inheritlnner ii = new Inheritlnner(wi) i 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 argumentos). Cree una segunda clase con una clase interna que herede de la primera clase interna. ¿Pueden sustituirse las clases internas? ¿Qué sucede cuando creamos una clase interna, luego heredamos de la clase contenedora y redefinimos la clase interna? En otras palabras, ¿es posible "sustituir" la clase interna completa? Podría parecer que esta técnica resultaría muy útil , pero el "sustituir" un a clase interna como si fuera otro método cualquiera de la clase externa no tiene, en realidad, ningún efecto: 11 : innerclasses/BigEgg.java II No se puede sustituir una clase interna como si fuera un método. import static net.mindview.util.Print .* ¡ 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 public class BigEgg extends Egg { public class Yolk { publ ic Yolk () { print (" BigEgg Yolk (1 ") ; public static void main (String [J new BigEgg () i args) { / * 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 propio 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()");} private Yolk y = new Yolk (); public Egg2 () { print ( "New Egg2 () "); } public void insertYolk{Yolk yy) { Y = yy¡ public void 9 () { y . f (); } public class BigEgg2 public class Yolk public Yolk () { public void f () extends Egg2 { extends Egg2.Yolk print (" BigEgg2 . Yolk () "); } { print ("BigEgg2. Yolk. f () ") ; public BigEgg2 () { insertYolk (new Yolk () ) ; public static void main (String [] args) { Egg2 e2 = new BigEgg2(); e2.g() ; 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 Clases internas locales Corno hemos indicado anterionnen te. también pueden crearse clases internas dentro de bloques de código, nonnalmente dentro del cuerpo de un método. Una clase interna local no puede tener un especificador de acceso, porque no fonna pane de la clase externa, pero si que tiene acceso a las variables finales del bloque de código actual y a todos los miembros de la clase contenedora. He aquí un ejemplo donde se compara la creación de una clase interna local con la de una clase interna anónima: 11: innerclasses/LocallnnerClass.java II Contiene una secuencia de objetos. import static net.mindview.util.Print.*; interface Counter int next () ; public class LocallnnerClass { private int count = O; Counter getCounter (fi nal String name) { II Una clase interna local: class LocalCounter implements Counter public LocalCounter () { II La clase interna local puede tener un constructor print{IILocaICounter(}11) ; public int next () { printnb(name); II Acceso a variable local final return count++; return new LocaICounter(); II Lo mismo con una clase interna anónima : Counter getCounter2 (final String name) { return new Counter() { II La clase interna anónima no puede tener un constructor II nominado , sino sólo un inicializador de instancia: { print ( 11 Counter () 11 ) ; public int next () { printnb(name)¡ II Acceso a una variable local final return count++; ) ) ; public static void main (String [] args) { LocallnnerClass lic = new LocallnnerClass(); Counter el = lic . getCounter(IILoeal inner "), c2 = lic.getCounter2 ( IIAnonymous inner " ); for(int i = O; i < 5; i++) print(el.next ()) ; for(int i = O; i < 5; i++) print (e2. next () ) ; 1* Output : LocalCounter () Counter () 10 Clases internas 239 Local Local Local Local Local inner O inner 1 inner 2 inner 3 inner 4 Anonyrnous Al1onyrnous inner 5 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. Identificadores de una clase interna Puesto que todas las clases generan un archivo .class que almacena toda la infonnación re lativa a cómo crear objetos de dicho tipo (esta infornlación genera una "metaclase" denominada objeto Class), podemos imaginar fácilmente que las cIases internas también deberán producir archivos .class para almacenar la infonnación de sus objetos Class. Los nombres de estos archivos Iclasses responden a una fórnlUla estricta: el nombre de la clase contenedora, seguido de un signo '$', seguido del nombre de la clase interna. Por ejemplo, los archivos .class creados por Locallnne r Class.java incluyen: Counter.class LocallnnerClass$l.class LocallnnerClass$lLocalCounter.class Locallnne rClass.class Si las clases internas son anónimas, el compilador simplemente genera una serie de números para que actúen como identificadores de las clases internas. Si las clases internas están anidadas dentro de otras clases internas, sus nombres se añaden simplemente después de un '$' y del identificador o identificadores de las clases externas. Aunque este esquema de generación de nombres internos resulta simple y directo, también es bastante robusto y pennite traLar la mayoría de las sittlaciones. 5 Puesto que se trata del esquema estándar de denominación para Java, los archivos generados son automáticamente independientes de la platafomla (tenga en cuenta que el compilador Java modifica las clases internas de múltiples maneras para hacer que funcionen adecuadamente). Resumen Las interfaces y las clases internas son conceptos bastante más sofisticados que los que se pueden encontrar en muchos lenguajes 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 embargo, 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 cuestiones 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 tiempo 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¡ ya que no podemos saber cuántas de estas referencias vamos a necesitar. La mayoría de los lenguajes proporciona alguna mane ra de resolver este importante problema. Java dispone de varias formas para almacenar objetos (o más bien, referencias a objetos). El tipo soportado por el compilador es la matriz, de la que ya hemos hab lado antes. Una matriz es la fonna más efic iente de almacenar un gmpo de objetos, y recomendamos utilizar esta opción cada vez que se quiera almacenar un grupo de primitivas. Pero una matriz tiene un tamaño fijo y. en el caso más genera l, no podemos saber en el momento de escribir el programa cuántos objetos vamos a necesitar o si hará falta una fomla más sofisticada de almacenar los objetos, por lo que la restricción relativa al tamaí'io fijo de una matriz es demasiado limitanteo La biblioteca j ava.u til tiene un conjunto razonablemente completo de clases contenedoras para resolver este problema, siendo los principales tipos básicos List, Set, Q ueuc y Map (lista, conjunto. cola y mapa). Estos tipos de objetos también se conocen COD el nombre de clases de colección, pero como la biblioteca Java utiliza el nombre Collection para hacer referenci a a un subconjunto concreto de la biblioteca, en este texto utilizaremos el ténnino más general de ¡'contenedor". Los contenedores proporcionan fonnas so fi sticadas de almacenar los objetos, y con ellos podemos resolver un sorprendente número de problemas. Además de tener otras características (Set, por ejemplo, sólo almacena un objeto de cada valor mientras que Map es una matriz asociativa que pennite asociar objetos con otros objetos), las clases contenedoras de Java tienen la funcionalidad de cambiar automáticamente de tamai'io. Por tanto, a diferencia de las matrices, podemos almacenar cualquier número de objetos y no tenemos que preocupamos, mientras estemos escribiendo el programa, del tamaño que tenga que tener el contenedor. Aún cuando no tienen soporte directo mediante palabras clave de Java, I las clases contenedoras son herramientas fundamentales que incrementan de manera significativa nuestra potencia de programación. En este capítulo vamos a aprender los aspectos básicos de la biblioteca de contenedores de Java poniendo el énfasis en la ut ilización típica de los contenedores. Aquí, vamos a centramos en los contenedores que se uti lizan de mane ra cotidiana en las tareas de programación. Posterionnente, en el Capítulo 17, Análisis de/aliado de los cOnlenedores, veremos el resto de los contenedores y una serie de detalles acerca de su funcionalidad y de cómo utilizarlos. ! Diversos lenguajes como PerL Payton y Ruby tienen soporte nativo para los contenedores. 242 Piensa en Java Genéricos y contenedores seguros respecto al tipo Uno de los problemas de utili za r los con tenedores anteriores a Java SE5 era que el compilador penl1itía insertar un tipo inco~ rrecto dentro de un contenedor. Por ejemplo, considere un contenedor de objetos Apple que utilice el contenedor más bási~ co general, ArrayList. Por ahora. podemos considerar que ArrayLis t es "una matriz que se expande automáticamente". La utilización de una matTiz Ar ra yList es bastante simple: basta con crear una, insertar objetos utilizando addQ y acceder a ellos mediante getQ, utilizando un índice: es lo mismo que hacemos con las matrices, pero sin emplear corchetes.2 ArrayList también dispone de un método size( ) que nos pennite conocer cuánros elementos se han añadido, para no utili~ zar inadvertidamente índices que estén más allá del contenedor que provoquen un error (generando una excepción de tiem~ po de ejecución; hablaremos de las excepciones en el Capítulo 12, Tratamiento de errores medio1l1e excepciones). En este ejemplo. insertamos en el contenedor objetos App le y Orange y luego los extraemos. Nornlalmente, el compilador Java proporcionará una advertencia, debido a que el ejemplo no lisa genéricos. Aquí, empleamos una anotación de Java SES para suprimir esas advertencias del compilador. Las anotaciones comienzan con un signo de '@', y admiten un argumento; es ta anotación en concreto es @S uppress\Varnings y el argumento indica que sólo deben suprimirse las advertencias no comprobadas ("lIl1checked'): 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 {} public class ApplesAndOrangesWithoutGenerics @SuppressWarn íngs ( "unchecked" ) public static void main(String[] args) ArrayList apples = new ArrayList() ¡ for(int i = O; i < 3; i++) apples.add(new Apple()); II No se impide añadir un objeto Orange: apples.add(new Orange(»; for(int i = O; i < apples.size() ( (Apple) apples .get (i) ) . id i i++) 11 ; II Orange sólo se detecta en tiempo de ejecución 1* (Execute to see output) * 1I 1: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 compleja. Sin embargo, la aplicación de c lases genéricas predefinidas suele resultar sencilla. Por ejemplo. para defmir un contenedor ArrayList en el que almacenar objetos Apple, tenemos que indicar ArrayList 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 pueden 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 contenedor.} He aquí de nuevo el ejemplo utilizando genéricos: ji : holding/ApplesAndOrangesWithGenerics . java import java.util. *¡ public class ApplesAndOrangesWithGenerics public static void main (String [] args) { ArrayList apples = new ArrayList(); for(int i = o; i < 3¡ i++) apples.add{new Apple(»; 1/ Error en tiempo de compilación: 11 apples.add(new Orange (»; for{int i == O; i < apples.size(); i++) System . out. println (apples. get (i) . id () ) ¡ 1/ Utilización de foreach: for(Apple c : apples) System.out.println(c.id( » ¡ / * 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 invocamos get(). Por tanto. con los genéricos no sólo podemos estar seguros de que el compilador comprobará el tipo de los objetos 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.*; class GrannySmith extends Apple {} class Gala extends Apple {} ) 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 class Fuji extends Apple {} class Braeburn extends Apple {} public class GenericsAndUpcasting public static void main(String[] args} ArrayList apples = new ArrayList(}; apples.add(new GrannySmith()); apples.add(new Gala()}; apples.add(new Fuji()}; apples.add{new Braeburn(}}; for(Apple e : apples} System.out.println(c) ; 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 seguido 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 constructor. 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 elementos 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 buscar 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 apples = new ArrayList (); Observe que el contenedor ArrayList ha sido general izado a un contenedor List, por contraste con la fonna en que lo habíamos tratado en los ejemplos anteriores. La idea de utilizar la interfaz es que, si posteriormente queremos cambiar la implementació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 apples = new LinkedList(}; 11 Almacenamiento de objetos 245 Así, nonna lmente crearemos un objeto de una clase concreta. lo generalizaremos a la correspondiente interfaz y luego utilizaremos 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. * ¡ public class SimpleCollection public static void main(String[] argsl ( COl lection e = new ArrayList (); for (in t i = O¡ i < 10 ¡ i+ +) c . add(il ¡ II Autoboxing for(Integer i : el System out.print(i + 11 ); 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 embargo, la documentación indica expresamente que add() "garanti za que este objeto Collection contenga el e lemento especificado". 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", porque 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. Adición de grupos de elementos Existen métodos de utilidad en las clases de matrices y colecciones de java.util que añaden grupos de elementos a una colección. El método Arrays.asList( ) toma una matri z y una lista de elementos separados por comas (utilizando varargs) y lo transforma en un objeto List. Collections.addAII( ) loma un objeto Collection y una matriz o una lista separada por comas y aliade los elementos a la colección. He aquí un ejemplo donde se ilustran ambos métodos, así como el método addAII( ) más convencional que forma parte de todos los tipos Collection : 11 : holding/AddingGroups . java II Adición de grupos de elementos a objetos Collection. import java.util.*; publie elass AddingGroups public statie void main(String[] argsl { Collection eollection new ArrayList (Arrays . asList (1, 2, 3,4,5»; Integer[l morelnts " { 6, 7, B, 9, 10 }; 246 Piensa en Java collection.addAll (Arrays.asList (morelnts )) ; II Se ejecuta bastante más rápido, pero no se puede II construir una colección de esta forma: Collections.addAll (collection, 11, 12, 13, 14, 15 ) ¡ Collections.addAll (collection, morelnts) i II Produce una lista 11 respaldada 11 en una matriz: Listclnteger> list = Arrays.asList ( 16, 17, 18, 19, 2 0) ; list.set ( 1, 99 ) ¡ II OK - - modificar un elemento II list.add(21 ) ¡ II Error de ejecución porque la matriz II subyacente no se puede cambiar de tamaño. 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 class class class class class Snow {} Powder extends Snow {} Light extends Powder { } Heavy extends Powder {} Crusty extends Snow { } Slush extends Snow {} public class AsListlnference public static void main {String(] args l List c Snow> snow1 = Arrays.asList { new Crusty {) , new Slush () , new Powder ()) II 11 11 11 11 11 i No se compilará: ListcSnow> snow2 = Arrays.asList { new Light{ ) , new Heavy ( ) ) ; El compilador dirá: found java.util.List required: java.util.ListcSnow> 1I Collections .addAll () no se confunde : ListcSnow> snow3 = new ArrayListcSnow> () ¡ Collections.addAll (snow3, new Light ( ), new Heavy {») II Proporcionar una indicación utilizando una II especificación explícita del tipo del argumento: ListcSnow> snow4 = Arrays.cSnow>asList { new Light{), new Heavy {)); i 11 Almacenamiento de objetos 247 Al tratar de crear snow2, Arrays.asList( ) sólo di spone del tipo Powder, por lo que crea un objeto List en lugar de List, 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 ninguna 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.*¡ public class PrintingContainers { static Collection fill(Collection collection) collection .add ( urat U) ; collection.add(Ucat U) ; col lection. add ( " dog 11) ; collection . add(Udog U) ; return collection¡ sta tic Map fill (Map map) map.put(IIrat", "Fuzzyll); map .put ( "cat U, "Rags"); map .put ( IIdog", "Bosco") ¡ map.put("dog", "Spot"); return map; { { public static void main{String[1 args) print(fill(new ArrayList())); print(fill(new LinkedList())); print(fill(new HashSet())); print(fill(new TreeSet())); print(fill(new LinkedHashSet())); print(fill(new HashMap())); print(fill(new TreeMap())); print{fill(new LinkedHashMap())) i 1* Output: [rat, [rat, [dog, [cat, [rat, cat, dog, dog] cat, dog, dog] cat, rat] dog, 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 almacenar 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 capitales 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 clavevalor, 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 automá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 aparente. 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 Existen dos tipos de objetos Lis!: • El objeto básico ArrayList, que es el que mejor permite acceder a los elementos de fanna aleatoria, pero que resulta más Icnto a la hora de insertar y eliminar elementos en mitad de una lista. • El objeto LinkedList, que proporciona un acceso secuencial óptimo. siendo las inserciones y borrados en mitad de una lista enormemen te rápidos. LinkedList resulta relati vamente lento para los accesos aleatorios. pero tiene muchas más func ionalidades que Ar r ayList. El siguiente ejemplo se adelanta un poco dentro de la estructura del libro, utilizando UDa biblioteca del Capítulo 14, Información de tipos para importar t)'peinfo.pets. Se trata de una biblioteca que contiene una jerarquía de clases Pet (mascota), junto algunas herramientas para generar aleatoriamente objetos Pet. No es necesario entender todos los detalles en este momento, si no que basta con sabe r que existe: (1) una clase Pet y varios subtipos de Pet y (2) que el método Pets.arrayList() estático devuelve un método ArrayList lleno de objetos Pet aleatoriamente seleccionados: 11: hOlding/ListFeatures.java import typeinfo.pets.*¡ import java.util.*; import static net.mindview.util.Print.*¡ public class ListFeatures { public static void main(String[] args) Random rand = new Random(47)¡ List pets = Pets.arrayList{7); print ("1 : " + pets) ¡ Hamster h = new Hamster{)¡ pets.add(hl ¡ II Cambio de tamaño automático print ("2 : " + pets) i print("3: " + pets.contains(h»; pets.remove(h) ¡ II Eliminar objeto a objeto Pet p = pets.get(2); print ("4 : "+ P + " " + pets. indexOf (p) ) ¡ Pet cymric = new Cymric()¡ print ( "5: !l + pets. indexOf (cymric» ¡ print ("6 : " + pets. remove (cymric» i II Debe ser el objeto exacto: print (" 7: " + pets.remove(p» i print (118 : + petsl ¡ pets.add(3, new Mouse(») ¡ II Insertar en un determinado índice print (" 9: " + pets) ¡ List sub = pets.subList{l, 4); print ("subList: " + sub); print ( " 10: " + pets. containsAll (sub) ) ; Collections . sort (sub) ; II Ordenación de la colección print ("sorted subList: " + sub) i II El orden no es importante en containsAll(): print("ll: " + pets.containsAll(sub»; Collections.shuffle(sub, rand) i II Mezclar los elementos print ("shuffled subList: " + sub) ¡ print("12: " + pets.containsAll(sub); List copy = new ArrayList (pets) ; sub = Arrays.asList{pets . get(1), pets.get(4»; print("sub: " + sub); copy.retainAll(sub) ; print("13: " .. copy) i copy = new ArrayList (pets) i II Obtener una nueva copia copy.remove(2); II Eliminar según un índice print("14: " + copy) i copy.removeAll(sub); II Sólo elimina objetos exactos print (" 15: " .. copy); 250 Piensa en Java copy.set(l, new MauSe()); print("16: " + copy); JI Sustituir un elemento copy.addAll(2, sub), // Insertar una lista en el medio print("17: " + copy); print ("18: " + pets. isEmpty () ) ; pets . clear() i /1 Eliminar todos los elementos print("19: print("20: " + pets); " + pets.isEmpty())¡ pets.addAll(Pets.arrayList{4)) print(I'21: + i pets); Object[] o = pets.toArray(); print("22: " + 0[3]); Pet[] pa • pets . toArray(new Pet[O]); print("23, " + pa[3] .id()); / * OUtput: 1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug) 2: (Rat, true Pug, 3, 4, 5, 6, 7, Manx, Cymric, Mutt, Cymric, Pug, Hamster] Cymric 2 -1 false true B, [Rat, Manx, Mutt, Pug, Cymric, Pug] 9, [Rat, Manx, Mutt, Mouse, Pug, Cymric, Pug] subList: [Manx, Mutt, 10: true Mouse) 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 elementos 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 objeto 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 creamos 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 ento 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 acceso 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 fica 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 implementació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 problema 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 elementos) 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 primer 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á preparado 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.*; public class Simplelteration public static void main{String(] args ) List pets = Pets.arrayList (12 ) i Iterator it = pets.iterator () ; while (i t . hasNext () I ( Pet p = it.next()i System.out.print(p.id () + " .11 + p + " ti ) i 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étodos 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 potente 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 .*¡ public class CrossContainerlteration public static void display(Iterator it) whilelit.hasNext()) { ( Pet p = it.next()¡ System.out . print (p.id() + ". " + p + 11 " ) ; System.out.println () i public static void main (String[] args ) { ArrayList pets = Pets.arrayList ( B) ; LinkedList petsLL = new LinkedList (pets l i HashSet petsHS = new HashSet (pets ) ; TreeSet petsTS = new TreeSet(pets ) ; display(pets.iterator (» ; display(petsLL.iterator(» i display (petsHS.iterator(» i display(petsTS . iterator (» 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 muestra la verdadera potencia del objeto 1tcrator: la capacidad de separar la operación de recorrer una secuencia de la estructura 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. * ; public class Listlteration public static void main (String [] args) { List pets = Pets.arrayList(S) ¡ Listlterator it = pets.listlterator{) ¡ while (it .hasNext ()) System.out.print(it .next() + ", " + it.nextlndex{) + " , " + it.previouslndex( ) + "¡ ti ) ¡ System.out.println() ; 1/ Hacia atrás: while(it.hasPrevious()) System.out.print(it.previous() .id() + " " ) ¡ System.out.println() i System.out.println(pets) ; it = pets.listlterator(3) ¡ while (it . hasNext () I { it . next () ; it.set(Pets . randomPet(» ¡ 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. Cree un segundo objeto List 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 eficiente 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 implementar 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étodos 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 repite 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.*¡ public class LinkedListFeatures ( public static void main(String[] args) { LinkedList pets = new LinkedList(Pets.arrayList(S» ¡ print (pets) ¡ II Idénticos : print ("pets. getFirst (): " + pets. getFirst () ) ¡ print ( "pets. element () : " + pets. element () ) ¡ II Sólo difiere en el comportamiento con las listas vacías: print(Upets.peek(}; 11 + pets.peek(»); II Idénticos; elimina y devuelve el primer elemento: print ( "pet s. remove () : + pets.remove{» ¡ print ("pets. removeFirst (): " + pets. removeFirst () ) ; 11 Sólo difiere en el comportamiento con las listas vacías: print("pets.poll(), " + pets.poll()); print (pets) ¡ pets.addFirst(new Rat(»; print("After addFirst(): " + pets); pets.offer(Pets.randomPet(» i print("After offer(): ti + pets); pets.add(Pets.randomPet(» ¡ print (" After add () : + pets) ¡ pets.addLast(new Hamster(}} i print("After addLast(}: It + petsl; print ("pets. removeLast () : 11 + pets. removeLast () ) ; 11 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, pets.removeLast(): Hamster HamsterJ * ///> 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 Completos 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 enteros 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; public class Stack { private LinkedList storage = new LinkedList(); public void push (T v) { storage. addFirst (v); } public T peek() { return storage.getFirst(}; } public T pop () { return storage. removeFirst () ; public boolean empty () { return storage. isEmpty () i } public String toString() { return storage.toString(); ///,Esto nos pennite introducir el ejemplo más sim ple posible de defin ición de una clase mediante genéricos. La que sigue al nombre de la clase le dice al compilador que se trata de un tipo paramelrizado y que el parámetro de ti po (que será su s~ tituido por un tipo real cuando se utili ce la clase) es T . Básicamente, lo que estamos diciendo es: "Estamos definiendo una pila Stack que almacena objetos de tipo TOO. La pila se implementa utili zando un objeto LinkedList, y también se define dic ho objeto LinkedList para que almacene el tipo T. Observe que push() (el método que introduce objetos en la pila) toma un objeto de tipo T , mientras que peek() y pop() devuel ven el objeto de tipo T . El método peek( ) devuelve el elemento superior de la pila sin eliminarlo, mientras que pop() extrae y devuelve dicho elemento superior. Si lo único que queremos es disponer del comportamiento de pila, el mecanismo de herencia resulta inapropiado, porque generaría una clase que incluiría el resto de los métodos de LinkedList (en el Capítulo 17, Análisis derallado de los contenedores, podrá ver que los diseñadores de Java 1.0 cometi eron este error al crear java.utiI.Stack). 11 He aquí una se ncilla demos trac ión de esta nueva clase Almacenamiento de objetos 257 St~lck : 1/: holding/StackTest.java import net.mindview.util.*¡ public class StackTest { public static void main(String[] args) Stack stack = { new Stack() for(String s "My dog has fleas".split(" stack.push(s) ; i lO» while(!stack . empty() ) System.out.print(stack.pop() + " ") i / * 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 nOI11bre 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.*¡ public class StackCollision public static void main (St ring [] args) ( net . mindview.util.Stack stack = new net.mindview.util.Stack() i for(String s : "My dog has fleas".split(" lO»~ stack.push(s) i while(!stack.empty() ) System.out . print(stack . pop() + " lO ) ¡ System . out . println() i java .util.Stack stack2 new java.util.Stack() i for(String s "My dog has fleas".split(II ti » stack2.push(s) ¡ while(!stack2.empty() ) System.out.print(stack2.pop() + " lO) i 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 public class SetOflnteger public static void main{String [] argsJ { Random rand = new Random(47); Set intset = new HashSet(); for(int i = Di i < 10000; i++) intset.add(rand.nextlnt(30)) ; System.out.println(intset ) ; 1* Out put: [15, 8, 23, 16, 28, 20, 25, 10, 7, 5, 22, O] 9, 21, 6, 1, 29, 14, 24, 4, 19, 26, 11, 18, 3, 12, 27, 17, 2, 13, ,/ // ,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 tendrá 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 contenedores. 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. *; public class SortedSetOflnteger public static void main(String[] args) { Random rand = new Random (47); SortedSet intset = new TreeSet() i for(int i = o; i < 10000; i++) intset.add(rand.nextlnt(30)} ; System.out.println(intset) ; 1* [O, Output: 1, 2, 3, 4, 5, 16, 17, 20, , /// ,- 18, 19, 6, 7, 21, 8, 22, 9, 23, 1 0, 24, 11, 12, 13, 14, 15, 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 determinado miembro usando contains( ). pero hay otras operaciones que nos recuerdan a los diagramas Venn que enseñan en el colegio: 1/ : holdingfSetOperations.java import java.util. * ; import static net.mindview.util.Print.* public class SetOperations { public stacic void main(String(] args) { Set setl = new HashSec () ; Collections.addAll(setl, ti A BCD E F G H 1 J K L". spl i t (" 11)) i setl . add ( "M" ) i print ("H: " + setl. contains (" H" ) ) ; print ("N: It + setl. contains (UN") ) ; Set set2 = new HashSet(); Collections.addAll(set2, "H 1 J K L".split { 1I "¡)i print{"set2 in setl: " + setl.containsAll {set2 »; setl. remove ( "H" ) ; print("setl: " + setl ) ; print ( "set2 in setl: " + setl.containsAll(set2»; setl.removeAll{set2) ; print(lIset2 removed fram setl: " + setl); eallections.addAll(setl, "X y Z".split(" 11»; print("'X Y Z' added to setl: " + setl); / * 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 *1110Los 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 quisiera 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.*; public class OniqueWords { public static void main (String [] args) { Set words = new TreeSet( new TextFile ("SetOperations.java" , " \\ w+" »; System.out.println(wordsl i 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. 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 .* ; public class UniqueWordsAlphabe tic public static void main (String[) arg s ) Set word s = ne w Tr eeSet (String.CASE_I NSENS IT IVE_ORDER) ; words . addAll ( new Text File ( ISetOperations . java " , n\\ w+ ,,» i System . out . pr i n tl n( words) i 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 . *; public class Statistics public static void main (String [) argsl { Random rand = new Random (47); Map m = new HashMap( ) ; for(int i = O; i < 1 0000; i ++) { 11 Generar un numero ent r e O y 20: int r = rand.nextlnt(20); Integer freq = m. get(r ) ; m. pu t {r, f req == null ? 1 freq + 1); System . out . println (m) ; 11 Almacenamiento de objetos 261 / * Output: {15=497, 4=481, 19=464, 8=468, 11=531, 16=533, 18=478, 3=508, 7=471, 2=489, 13=506, 9=549, 6=519, 1=502, 14=477, 10=513, 5=503, 0 =481) 12=521, 17=509, ' 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.*¡ public class PetMap { public static void main(String[] args) { Map petMap = new HashMap() petMap. put ("My Cat", new Cat ("Molly") ) i petMap. put ( "My Dog", new 009 ("Ginger " ) ) ; petMap.put(ltMy Hamster", new Hamster ( ltBosco" » ; print (petMap) i Pet dog = petMap.get {"My 009"); print (dog) ¡ print {petMap. containsKey ("My Dog"» i print(petMap.containsValue(dogJ) ; i / * 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. · ¡ public class MapOfList { public static Map, 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 el contador asociado con dicha palabra; esto es, en la práctica, la ubicació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 seguridad 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. * ¡ public class QueueDemo public static void printQ (Queue queue) while(queue . peek() != null) System.out.print(queue.remove() + System.out.println(} ¡ { 11 11); public static void main(String[] args) { Queue queue = new LinkedList(); Random rand = new Random(47); for(int i :: O; i < 10; i++) queue.offer(rand.nextlnt(i + ID}); printQ(queue) ; Queue qc = new LinkedList (); for (c har e : 11 Brontosaurus" . toCharArray () ) qc . offer(c) ; printQ (qc) ; 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 disponer 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 oper 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étodo 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 contenedor Pr iori tyQ ueue ha sido ailadido en Java SES para proporcionar una implementación automática de este tipo de comportamiento. 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 modificar 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.*; public class PriorityQueueDemo public static void main(String[] args) PriorityQueue. 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 óperando en la cola. 11 Almacenamiento de objetos 265 i = o; i <:: 10; i++ ) priorityQueue , offer {rand.nextlnt ( i for ( fnt + 10 » ; QueueDemo. printQ {priorityQueue } ; List ints = Arrays . asList (25, 22, 20 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, I 25) ; priorityQueue = new PriorityQueue (ints ) ; QueueDemo. printQ (priorityQueue ) i priorityQueue = new PriorityQueue ( ints. s i ze (), Collections. reverseOrder () ) priorityQueue.addAll(intsJ i QueueDemo.printQ(priorityQueuel i i String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION " i ListcString> serings = Arrays.asList {fact . split("" )} ; PriorityQueue stringPQ = new PriorityQueue (strings) ; QueueDemo .p rintQ(stringPQ) ; stringPQ = new PriorityQueue ( serings. si ze () , Collections . reverseOrder ( ) ) i seringPQ . addAll ( serings ) ; QueueDemo . prineQ(stringPQ) ; Set charSet = new HashSet(); for (char e fact.toCharArray( » charSet.add ( c ) ; II Autoboxing PriorityQueue characterPQ new PriorityQue ue< Character> (charSet) i QueueDemo.printQ(characterPQ) i 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 y la segunda llamada a PriorítyQueue utilizan el comparador de orden inverso generado 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 generar 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 Comparación entre Collection e Iterator CoUection es la interfaz raíz que describe las cosas que ti enen en común todos los co ntenedores de secuencias. Podríamos considerarla como una especie de " interfaz incidental", que surgió debido a la existencia de aspectos comunes entre las otras interfaces. Además, la clase java.util.AbstractCollection proporciona una implementación predeterminada de Collection, para poder crear un nuevo subtipo de AbstractCollection sin duplicar innecesariamente el código. Un argumento en favor de disponer de una interfaz es que ésta nos permite crear código genérico. Escribiendo como una interfaz en luga r de como una implementación, nuestro código puede aplicarse a más tipos de objetos. 6 Por tanto, si escri. bimos un método que admita un método Collection, dicho método podrá aplicarse a cualquier tipo que implemente CoUection, y esto permite implementar Collection en cualquier clase nueva para poderla usar con el método que hayamos escrito. Resulta interesante resaltar, sin embargo, que la bibl ioteca estándar e++ no dispone de ninguna clase base común para sus contenedores: los aspectos comunes entre los contenedores se consiguen utili za ndo iteradores. En Java, podría pare· cer adecuado seguir la técnica utilizada en e++ y expresar los aspectos comunes de los contenedores utilizando un iterador en lugar de una colección. Sin embargo, ambos enfoques están entrelazados, ya que implementar Collection también impli. ca que deberemos proporcionar un método iterator() : 11 : holding/lnterfaceVs l terator . java import typeinfo.pets.*; import java .util.*; public class InterfaceVslterator public static void display(Iterator it) while(it.hasNext()) { ( Pet p = it.next(); System . out.print (p .id () + 11.11 + p + 11 11 ); System.out.println() ; public static void display(Collection pets) for(Pet p : pets) System.out.print(p.id() + 11. 11 + P + 11 " )i System.out.println() ; public static void main (String [] args) { List petList = Pets.arrayList(B); Set petSet = new HashSet (petList ) ; Map petMap = ne w LinkedHashMap{); String [] names = ("Ralph, Eric, Robín, Lacey, "Britney, Sam, Spot, Fluffy " ) .split( " , 11 ); for(int i = O; i < names.length; i++) petMap.put (names [i], petList.get (i)); display{petList) ; display(petSet) ; display(petList.iterator()) ; display(petSet . iterator()) i System.out.println (petMap) ; System.out.println(petMap.keySet()) ; display(petMap.values()) ; display(petMap . values() .iterat or()) i ti + 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 O:Rat l:Manx 2:Cymric 3 : Mutt 4:Pug 5:Cymric 6 : Pug 7:Manx 4:PU9 6:Pug 3:Mutt l:Manx 5:Cymric 7:Manx 2:Cymric O:Rat {Ralph~Rat, Eric=Manx, Robin=Cymric, Lacey=Mutt, Britney=Pug, Sam=Cymric, Spot=Pug, Fluffy=Manx} [Ralph, Eric, Robin, Lacey, Britney, Sam, Spot, Fluffyl 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 5 : Cymric 6:Pug 7:Manx - /// ,Ambas versiones de display() funcionan con objetos Map y con subtipos de Collection, y tanto la interfaz Collection como lterator penniten desacoplar los métodos display(). sin forzarles a conocer ningún detalle acerca de la implementación concreta del contenedor subyacente. En este caso, los dos enfoques se combinan bien. De hecho, Colleetion lleva el concepto un paso más allá porque es de tipo Iterable, Y por tanto, en la implementación de display(Collection) podemos usar la estmcturajóreac/¡, lo que hace que el código sea algo más limpio. El uso de Iterator resul ta muy recomendable cuando se implementa una clase externa, es decir, una que no sea de tipo Collection, ya que en ese caso resultaría dificil o incómodo hacer que esa clase implementara la interfaz Collection. Por ejemplo. si creamos una implementación de Collection heredando de una clase que almacene objetos Pet, deberemos implementar todos los métodos de Collection, incluso aunque no necesitemos utilizarlos dentro de l método display( ). Aunque esto puede llevarse a cabo fácilmente heredando de AbstractCollection . estaremos forzados a implementar iterator( ) de todas fomlas,j unto con size(), para proporcionar los métodos que no están implementados en AbstractCollcction. pero son uti lizados por los otros métodos de AbstraetColleetion: jI : holdingfCollectionSequence.java import typeinfo.pets.*; import java.util.*; public class ColleccionSequence extends AbstractCollection private Pet(] pets = Pets . createArray(B); public int size( ) { return pets.length; } public Iterator iterator () { return new Iterator () { private int index = o; public boolean hasNext ( ) return index < pets.length¡ public Pet next () { return pets [index++J i public void remove () { // No implementado throw new UnsupportedOperationException (} } i }; pu blic stati c void main (String (] args ) { Col lectionSequence e = new CollectionSequence ( ); In t erfaceVslterator.display (c ) i InterfaceVslterator.display(c.iterator( )) ; 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 ) ¡ publie class NonCollectionSequenee extends PetSequence { public Iterator iterator () { return new Iterator () { private int index = O; public boolean hasNext ( ) return index < pets . length¡ publ ic Pet next () { return pets [index++] ¡ publ ic void remove () { / I No implementado throw new UnsupportedOperationException() i }; publie static void main (String[) args) { NonCollectionSequenee ne = new NonCollectionSequence () InterfaceVslterator.display(nc.iterator {) ; i 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 consuma esa secuencia; además, se imponen muchas menos restricciones a la clase correspondi ente a la sec uencia que si implementamos Collectio n. Ejercicio 30: (5) Modifique CollectionSeq uence.j.va para que no herede de AbstractCollection , sino que implemente Collection. La estructura foreach y los iteradores Hasta ahora, hemos utili zado principalmente la si ntaxis foreach con las matrices, pero dicha simaxis también fUllciona con cua lquier objeto de tipo ColJection . Hemos visto algunos ejemplos en los que empleaba ArrayList, pero he aquí una demostración general: / 1 : holding / ForEachCollections.java II All eollections work with foreach. import java.util.*; public elass ForEachCollections publie static void main{String() args ) { Collection es = new LinkedList {) ¡ Collections.addAll {es, "Take the long way home" . split {" " ) i for (String s : es) System. out. print (" '" + s + '" ,,) i / * 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.*; public class IterableClass implements Iterable pro tected String [] words = ( "And that is how " + "we know the Earth to be banana-shaped . " ) . spIit ( 11 " ) ¡ pubIic Iterator i terator () { return new Iterator () { private int index = O; public boolean hasNext ( ) { return index < words.length¡ public String next () { return words [index++ 1 ¡ public void remove () { / / No implementado throw new UnsupportedOperationException{) ; } }; public static void main (String[] args ) for {String s : new IterabIeClass (») System. out. print (s + 11 " ) ¡ / * 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 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.*¡ public class EnvironmentVariables public static vo id main (String[] args ) for {Map.Entry entry: System.getenv () .entrySet {» System. out. printIn (entry. getKey () + ": 11 + entry.getVaIue ()} ¡ / * (Execute to see output ) */// :- System.getenv()' devuelve un objeto Map, entrySet() produce un conjunto de elementos Map.Entry y un conjunto (Set) de tipo Iterable. por lo que se le puede usar en un bucleforeach. La instrucción.loreach fun ciona con una matri z o cualquier cosa de tipo Iterable, pero esto no quiere decir que una matri z sea automáti camente de tipo Iterable, ni tampoco que se produzca ningún tipo de meca nismo de autoboxing: 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 11: holding/ArrayIsNot I terable . java import java.util. * ; publie elass ArrayIsNotIterabl e statie void test (Iterable ib) for(T t , i b) System . out.print(t + 11 " ) ; { public static void main (String [] args) { test (Arrays .asList(l, 2, 3)); String [] strings = { "A", " B II , "e" }; II Una matri z f un ciona eon f o rea c h, pe ro no e s de tipo Iterab le : I I ! test (strings) i II Ha y que convert ir l a exp líci t a me nte al t ipo Iterable: test{ Arrays . as List(st r i ng s )) i 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 automática a Iterable; sino que debe realizarse de forma manual. (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 genere antes de pararse. Verifique que el programa funciona. Ejercicio 3\: El método basado en adaptadores ¿Qué sucede si tenemos una clase existente que sea de tipo Iterable, y queremos añadir una o más formas nuevas de utilizar esta clase en una instrucción foreach? Por ejemplo, suponga que queremos poder decidir si hay que iterar a través de una lista de palabras en dirección directa o inversa. Si nos limitamos a heredar de la clase y sustituimos el método iterator(), estaremos sustituyendo el método existente y no dispondremos de la capacidad de opción. Una solución es utili zar lo que yo denomino Método basado en adaptadores. Cuando se dispone de una interfaz y nos hace falta otra, podemos resolver el problema escribiendo un adaptador. Aqui, lo que queremos es añadir la capacidad de generar un iterador inverso, pero sin perder el iterador directo predetenninado. así que no podemos limitarnos a sustituir el método. En su lugar, lo que hacemos es añadir un método que genere un objeto Iterable que pueda entonces utilizarse en la instrucción fo reach. Como puede ver en el ejemplo siguiente. esto nos pennite proporcionar múltiples fonnas de usar joreac/¡ : 11 : holding / AdapterMethodIdiom.java II El método basado en adap tadores permite uti l izar II foreaeh con tipos adiciona l es de objetos i terables . import java.util .* ; elass ReversibleArrayList extends ArrayList { public ReversibleArrayList (Collection e) { super (e) i public Iterable reve rsed () { return new It e rable () { publie Iterator iterator () return new Iterator () { int eurrent = size () - 1; public boolean hasNext () { return cur rent > - 1 ; public T next() { return get(eu rrent- - ); } public void remove ( ) { liNo impl e ment a do throw new UnsupportedOperationExce p tion () ; ) }; } 11 Almacenamie nto de objetos 271 ) ); public class AdapterMethodldiom { public static void main(String[] args) ReversibleArrayList ral = new ReversibleArrayList( Arrays.asList(OITo be or not ta be".split(" ")) // Toma el iterador normal vía iterator(): ter (S tring s : ral) System.out.print (s + 11 " ) i System.out.println () ; JI Entregar el objeto Iterable de nuestra elección for{String s : ral.reversed() ) System.out.print (s + 11 " ) ; ) i ; * 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. *¡ public class MultilterableClass extends IterableClass ( public Iterable reversed () ( return new Iterable () ( public Iterator iterator () return new Iterator () ( int current = words.length - 1; public boolean hasNext () { return current > -1; } public String next () { return words (current- -] ¡ } public void remove () { // No implementado throw new UnsupportedOperationException() ¡ ) }; ) }; public Iterable randomized () ( return new Iterable () ( public Iterator iterator () List shuffled = new ArrayList (Arrays.asList (words »; Collections.shuffle(shuffled, new Random (47 » ¡ return shuffled.iterator () ¡ ) ); public static void main (String(] args ) { MultilterableClass mic = new MultilterableClass( ) ; for(String s : mic.reversed( » Systern.out.print (s + ¡ 11 " ) 272 Piensa en Java 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 referencias 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 .* ¡ publie elass ModifyingArraysAsList publie stat i c void main(Stri ng[] a r gs) Random rand = n ew Random(47) i Integer[) ia = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; List listl = new ArrayList (Arrays . asList (ia»; System.out.println ( IIBefore shuf f ling: 11 + listl ) i Colleetio ns.shuffle(listl, rand ) i System.out.println("After shuff ling : " + listl) ¡ System. out . pri n tln ( " array : + Ar rays . toS t ring(i a »; List list2 = Arrays . asList{ia ) i System.out.println {"Before shuffling: " + list2 ) i Colleetions.shuffle (list2, rand ) i System.out.println ( "After shuffling: " + list2 ) i System . out. println ("array: " + Arrays. toString (ia ) ) ¡ 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 directamente 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 muestre 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 multidimcnsionales 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 efectuar 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 permiten 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 ordenadas y no es tan rápido como HashMap. Un mapa LinkedHashMap mantiene sus elementos en orden de inserció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 antiguos). En este diagrama se incluyen únicamente las interfaces y clases que podemos enCOlHrar de fonna habitual. r-------. r-------. .-------. : Jterator :-. ------------: Collection :-. ------------: ~-------.. Genera ~-- ¡¡ ---.. , r-----,..----, Genera Gene,. "- ~ -" : ,____ L _____ , :Li;t¡-t~;at~r:.-- ------: L~st: : S~t : :Q~;u~: " _______ " Map '---Lf---" .- ~-" "- ~-- " I Tree~ap I . . ,..------------1 ,..--- ,-----1I • 1,' ~~~~ , • , r-~----, PriorityQueue I Has~~~g i-~:e~Set I : Comparable :......... : Comparator : .----------" ~----------. r LinkedHashSet I Utilidades Collections Arrays Taxonomía simple de los contenedores Como puede ver, sólo hay realmente cuatro componentes contenedores básicos: Map, List, Set y Queue, y sólo dos o tres implementaciones de cada uno (las implementaciones de java.util.concurrent para Queue no están incluidas en este diagrama). Los contenedores que más habinlalmente se utili zan son los que tienen líneas gmesas de color negro a su alrededor. 274 Piensa en Java 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.*; public class ContainerMethods public static void main{String[] args) { ContainerMethodDifferences.rnain{args} i j * Output: (Sample) Collection: {add, addAII, clear, contains, containsAII, equals, hashCode, isEmpty, iterator, 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 interfaz proporciona información para aquellos algoritmos que quieran modificar dinámicamente su comportamiento depend iendo 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 embargo, 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 ligeramente 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, debemos 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 cotidiana para hacer nuestros programas más simples, más potentes y más efectivos. Puede que tardemos un poco en aCOSUlnlbramas 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 la venta en WII'lI'.Alindr'iew.net. se leccionado~ en el documento electrónico rile Thinláng in JaI'tl AnnOfafed So/utioll Guide, disponible para Tratamiento de errores • mediante excepciones La fi losofia básica de Java es que "el código erróneo no será ejecutado". El momento ideal de detectar un error es en tiempo de compilación, antes incluso de poder ejecutar e l programa. Sin embargo, 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 recuperació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 programas 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 problemas 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ódigo 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 programadores que uti lizaban una biblioteca tendían a pensar en sí mismos como si fueran inmunes al erro r; era casi como si dijeran: "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 ! El programador en e puede. por ejemplo. consultar el valor de retomo de printfO. 278 Piensa en Java podían, a pesar de todo, construir sus sistemas con estos lenguajes. se resistían a admitir la realidad: que esta técnica de gestió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 llamada 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 cuando 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 excepciones 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 necesaria 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 condición, pero ¿qué implica que el denominador sea cero? Puede que sepamos, en el contexto del problema que estemos intentando 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 estuviera 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á gestionado 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 mecanismo de tratamiento de excepciones en la informática distribuida. Las transacciones son el equivalente informático de los contratoS 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 programa 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. Argumentos de las excepciones Al igual que sucede con cualquier objeto en Java, las excepciones siempre se crean en el cúmulo de memoria utilizando new, lo que asigna el correspondiente espacio de almacenamiento e invoca un constructor. Existen dos constructores en todas las excepciones estándar. El primero es el constructor predetenninado y el segundo toma un argumento de cadena de caracteres para poder aportar la información pertinente a la excepción: throw new NullPointerException(tlt :::o null ll ) i 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 simplificadora 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 devuelve 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 excepción). Detección de una excepción Para ver cómo se detecta una excepción, primero es necesario entender el concepto de región protegida. Se trata de una sección de código que puede generar excepciones que esté seguida por el código necesario para tratar dichas excepciones. 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 bloque 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 tratamiento 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. Rutinas de tratamiento de excepciones Por supuesto, la excepción generada debe acabar siendo tratada en algún lugar. Dicho " lugar" es la rutina de tratamiento de excepciones. existiendo una de dichas rutinas por cada tipo de excepción que queramos captu rar. Las rutinas de tratamienlO de excepciones es tán situadas inmediatamente a continuación del bloque try y se denotan mediante la palabra cla ve eateh : try { II Código que podría generar excepciones catch(Tipol idl) { // Tratamiento de las excepciones de Tipol catch (Tipo2 id2) { // Tratamiento de las excepciones de Tipo2 catch (Tipo3 id3 ) { // Tratamiento de las excepciones de Tipo3 // 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 exactamente igual que un argumento de un método. En ocasiones. nunca utilizamos e l identificador, porque el tipo de la excepció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 excepción, el mecanismo de tratamiento de excepciones trata de buscar la primera mtina de tratamiento cuyo argumento se ajuste 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 despué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. J Como la mayoria de los lenguajes. incluyendo C+---. C#. Python, O, etc. 12 Tratamiento de errores mediante excepciones 281 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 principal 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. Creación de nuestras propias excepciones No tenemos porqué limitamos a utili zar las excepciones Java existentes. La jerarquía de excepciones de Java no puede prever todos los errores de los que vayamos a quere r ¡nfonnar, así que podemos crear nuestros propios errores para indicar un problema especial con el que nuestra biblioteca pueda encontrarse. Para crear nuestra propia clase de excepción, debemos hereda r de una clase de excepción existente, preferiblemente de una cuyo significado esté próximo al de nuestra propia excepción (aunque a menudo esto no es posible). La fonl1a más tri vi al de crear un nuevo tipo de excepción consiste, simplemente, en permitir que el compilador cree el constructor predetenllinado por nOSO LTOS, por lo que no hace fa lta prácticamente ningún código: //: exceptions/InheritingExceptions.java // Creac ión de nuestras propias excepciones. class SimpleExceptian extends Exception {} public class InheritingExceptions { public void f() throws SimpleException System .out.println( "Throw SimpleException from f() " ) i throw new SimpleExcepti on(); public sta tic void main (String [] args) { InheritingExceptions sed = new InheritingExceptions() try { i sed. f (1; catch(SimpleException e) System. out. println ( "Caught i t! ") i / * Out put: Throw SimpleException fram f() Caught it! ' /// ,El compilador crea un constmctor predetemlinado, que de forma automática (e invisible) invoca al constructor predeterminado de la clase base. Por supuesto, en este caso no obtenemos un constmctor SimpleException(String), pero en la práctica 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 class MyException extends Exception { public MyException () {} public MyException (String msg) { super (msg) ; public cI ass FullConstructors ( public statie void f() throws MyException { System. out. println ( II Throwing MyException fram f () 11 ) ; throw new MyException() j public statie void g() throws MyException ( System.out.println( IIThrowing MyException fram g{) " ); throw new MyException ( " Originat ed in 9 () 11 ) ; public statie void main(String[) args ) try { í () ; catch(MyException el ( e.printStackTrace(System.out) i } try { g(); catch (MyException el { e.printStackTrace{System . out) ; /* 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 segundo 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() ; la ¡nfonnación va a la salida de error es tándar. 12 Tratamiento de errores mediante excepciones 283 Ejercicio 1: (2) Cree una clase con un método main() que genere un objeto de la clase Exception dentro de un bloque 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.*; class LoggingException extends Exception { private static Logger logger = Logger .getLogger("LoggingException U ) ; public LoggingException () ( StringWr iter trace = new StringWriter(); printStackTrace(new PrintWriter(trace»; logger . severe(trace.toString(» ; public class LoggingExceptions { public static void main(String[] args) try { throw new LoggingException () i catch (LoggingException e ) { System.err.println ( "Caught " + el; } try { throw new LoggingException(); catch (LoggingException el { System.err.println(UCaught " + el i / * Output: (85% match) Aug 30, 2005 4:02:31 PM LoggingException SEVERE : Logging Ex ception at LoggingEx ceptions.main(LoggingExceptions.java :1 9 ) Caught LoggingException 284 Piensa e n Java Aug 30, 2005 4:02:31 PM LoggingException SEVERE: LoggingException at LoggingExceptions.main{LoggingExceptions.java:24) 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. *¡ public class LoggingExceptions2 private static Logger logger = Logger.getLogger(ULoggingExceptions2") ; static void logException (Except i on e) { StringWriter trace = new StringWriter()¡ e.printStackTrace{new PrintWriter(trace)); logger.severe(trace.toString()) ; public static void main(String(] args) try { throw new NullPointerException(); catch (NullPointerException e) { logException(e) ; 1* Output: (90% match) Aug 30, 2005 4:07:54 PM LoggingExceptions2 logException SEVERE: java.lang .NullPointerException at LoggingExceptions2.main{LoggingExceptions2.java:16) * ///,El proceso de creación de nuestras propias excepciones puede llevarse un paso más allá. Podemos ai'ladir constructores y mi embros adi cionales: 11: exceptions/ExtraFeatures.java Mejora de las clases de excepción. import static net.mindview.util.Print.*; JI class MyException2 extends Exception { private int x¡ public MyException2 () {) public MyException2 (String msg) { super (msg) ¡ public MyException2 {String msg, int x l { super (msg) ; 12 Tratamiento de errores mediante excepciones 285 this.x = Xi public int val () { return Xi public String getMessage () { return "Detail Message: "+ X + " "+ super.getMessage(); public class ExtraFeacures { public static void f(} throws MyException2 print ("Throwi ng MyException2 from f () ,,) i throw new MyException2() i public static void g() throws MyException2 { print ("Throwing MyException2 from 9 () ") i chrow new MyExcepcion2 ("Originated in 9 () ") public static void h{) throws MyException2 { print ("Throwing MyException2 from h () ") i throw new MyException2 ( "Originated in h (l ", public static void main (String [] try { args) i 4 7) ; { f () ; catch (MyException2 e) { e.printStackTrace(System.out) i 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étodo 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 te. Demuestre que dichas clases funcionan. Ejercicio 7: (1) Modifique el Ejercicio 3 para que la cláusula ealeh registre los resultados. automáticamen~ 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 biblioteca 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 excepciones 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 ... Sin embargo, si decimos void f () { 11 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 provoca 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 cierta 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 tendremos que real izar cambios en el código existente. También resulta importante esta característi ca para crear interfaces y cIases 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ómo capturar una excepción Resulta posible crear una rutina de tratamiento que capture cualquier tipo de excepción. Para hace r esto, podemos capturar el tipo de excepción de la clase base Exception (existen otros tipo de excepciones base, pero Exception es la base que resu lta apropiada para casi todas las acti vidades de programación): 12 Tratamiento de errores mediante excepciones 287 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 cuando 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 incluye 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.*; public class ExceptionMethods { public static void main {String[] args ) { try { throw new Exception ( "My Excepti o n" ) ; catch (Exception e) { print ("Caught Exception"); print ( "getMessage ( ) :" + e. getMessage ( ) ) ; print ( "getLocalizedMessage () : 11 + e.getLocalizedMessage( )) ; print ("toString () :" + e ) ; print ( "printStackTrace () : " ) ; e.printStackTrace(System.out) ; 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. public class WhoCalled { static void f () { /1 Generar una excepción para rellenar la traza de pila. try { throw new Exception(); catch (Exception e) { for(StackTraceElement ste : e.getStackTrace()) System . out.println{ste.getMethodName{)) ; static void g() { fl); ) static void h () { g (); ) public static void main (String [] args) { f(); System. out .println ( " - - -- - - - - - --- - -- - - - - -- - - - - - - - - - - -" ) ; g(); System.out.println("--------------------------------") ; h(); / * 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 Regeneración de una excepción En ocas iones. con viene regenerar una ex cepción que hayamos capturado, particularmente cuando se utiliza Exception para captu rar todas las excepciones. Puesto que ya di spo nemos de la re ferencia a la excepción actual , podemos simplemente volver a ge nerar dicha re ferencia : cacch(Exception e) { System . out . println(IIAn exception was thrown lt ) ; throw e; 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 preserva 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 queremos insertar nueva infonnación de traza de la pila podemos hacerlo invocando fillInStackTrace(), que devuelve un objeto 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() pub l ic class Rethrowing { public s t atic voi d f{) t hrows Excep tion { System . out . println( lI ori ginating the e xception i n f() " ); throw new Exception ( " t h rown from f () " ) i public static void g() throws Exception { try { f II ; catch (Exc e ption e l { System . out . prin t ln (" Inside 9 () ,e . pri n tStackTrace () ") ; e . printStackTr ace(System.ou t ) ; throw e¡ public static void h() throws Exception { try { fll; catch(Exception e) System. out. println (" Inside h () , e . printStackTrace () ") ; e . printStackTrace(System.out) ¡ throw (Ex cep ti on) e. f i ll I nStackTrace(); public static void ma i n (String [] argsl { try { gil; ca t ch(Ex ception e) Sys t em. out . pri n tln (IImain: printStackTrace () 11) e . printStackTrace(System . outl; try hll; catch(Exception el Sys t em.out . println(lImain : printStackTrace() 11) e . printStackTrace(System . out) ; i i 290 Piensa en Java / * Output: originating the exception in f () Inside g {) ,e.printStackTrace () thrown froID f () at Rethrowing.f {Rethrowing.java:7 ) at Rethrowing.g (Rethrowing.java:ll ) j ava.lang.Exception: 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 ) La línea donde se invoca filllnStackTrace() será el nu evo punto de origen de la excepción. También resulta posible volver a generar una excepción distinta de aquella que hayamos capturado. Si hacemos esto, obte· nemos un efecto similar a cuando usamos filllnStackTrace() : la infonnación acerca del lugar de origen de la excepción se pierde, y lo que nos queda es la información correspondiente a la nueva instrucción throw: /1 : exceptions / RethrowNew.java /1 Regeneración de un objeto distinto de aquél que fue capturado. class OneException extends Exception { public OneException (String s ) { super (s ) i c lass TwoException extends Exception { public TwoException(String s ) { super (s} i public class RethrowNew { public static void f( ) throws OneException { System. out. println ( ltoriginating the exception in f () " ) ; throw new OneException ( " thrown from f () " ) i public static void main(String [] args ) { try { try ( f () ; catch (OneException e ) { System.out.println( "Caught in inner try, e. printStackTrace ( ) " ) ; e . printStackTrace(System.out) i throw new TwoException ( .. from inner try"); catch(TwoException e) System.out .println ( 12 Tratamiento de errores mediante excepciones 291 "Caught in outer try, e.printStackTrace() "); e.printStackTrace(System.out l ; 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 programadores tenían que escribir su propio código para preservar la información de excepción original, pero ahora todas las subclases 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 constnlCtor 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.*¡ class DynamicFieldsException extends Exception {} public class DynamicFields ( private Object [] [] fields¡ public DynamicFields(int initialSize) fields ::: new Object [initialSizel [2] ¡ for{int i ::: O; i < initialSize¡ i++) fields[i] = new Object[] { null, null }; public String toString () { StringBuilder result = new StringBui lder {); for(Object[] obj , fields) resul t . append (obj [O] ) ; resul t . append ( ": 11 ); resul t. append (obj [1] ) ; result.append ( U\n U) ; return result . toString() ¡ { 292 Piensa en Java private int hasField (String id ) { far ( int i = O; i < fields.length; if (id.equa1s ( fie1ds[iJ [OJ )) i++ ) return i· return -1; private int getFieldNumber(String id ) throws NoSuchFieldException { int fieldNum = hasField {id ) i if ( fieldNum == - 1 ) throw new NoSuchFieldException () ; return fieldNuID; private int makeFie1d (String id) { far(int i = O; i < fields.length; i++l i f (fie1ds [iJ [OJ == null) { fie1ds [iJ [OJ = id; return i; // No hay campos vacios. Añadir uno : Object[J[J tmp = new Object[fie1ds.1ength + 1J[2J; far {int i = O; i < fields.length; i++l tmp [iJ = fie1ds [iJ ; far(int i = fields.length; i < tmp.length; i ++ } tmp[iJ = new Object[J { null, null j; fields = tmpi // Llamada recursiva con campos expandidos: return makeField (id ) ; public Object getField(String id) throws NoSuchFieldException return fields [getFieldNumber(id)] [1]; public Object setField (String id, Object value) throws DynamicFieldsException ( if(va1ue == nu11 ) { J J La mayoría de las excepciones no tienen un constructor con "causa". JI JI En estos casos es necesario emplear initCause () , disponible en todas las subclase de Throwable. DynamicFieldsException dfe = new DynamicFieldsException( ) i dfe.initCause(new NullPointerException( )} ¡ throw dfe¡ int fieldNumber = hasField (id ) ¡ if (fieldNumber == -1 ) fieldNumber makeField ( id } i Object result = null¡ try { result = getField(id) i IJ Obtener valor anterior catch (NoSuchFieldException e) { J J Utilizar constructor que admite la "causa": throw new RuntimeException(e) i fields [fieldNumber] [1] = value¡ return result¡ public static void main(String[] argsJ 12 Tratamiento de errores mediante excepciones 293 DynamicFields df print (df ) ; new DynamicFields (3 ) ; try { df.setField {"d", "A value ter d" ) i df.setField ( nnumber", 47 ) ; df. setField ( "number2 " , 48 ) i print (df ) ; df . setField ( "d", "A new value fer d" ) ; df. setField ( "number3" , 11 ) ; print ( lIdf: " + df ) ; print ( "df.getField {\ "d\ U) u + df.getField ( "d" )) ; Object field = df.setField ( "d", null ) ; II Excepción catch (NeSuchFieldException e l { e.printStackTrace (System . out ) i catch (DynamicFieldsException e ) e.printStackTrace (System.out) i 1* Output: null: null null: null nu ll: null d: A value for d number: 47 number2: 48 df: d: A new value for d number: 47 number2: 48 number3: 11 df.getField ( "d" l : A new value tor d DynamicFieldsException at DynamicFields.setField (DynamicFields.java:64 ) at DynamicFields.main (DynamicFields.java:94 l Ca used by: java.lang . NullPointerException at DynamicFields.setField (DynamicFields . java:66 ) ... 1 more * /!/ , Cada objeto DynamicFiclds contiene una matriz de parejas Object-Object. El primer objeto es el identificador del campo (de tipo String), mientras que el segundo es el valor del campo, que puede ser de cualquier tipo, salvo una primiti va no envuelta en otro tipo de objeto. Cuando se crea el objeto, tratamos de adivinar cuántos campos vamos a necesitar. Cuando invocamos setField(), dicho método localiza el campo existente que tenga dicho nombre o crea un nue vo campo, colocando a continuación el valor en él. Si se queda sin espac io, se añade nuevo espacio creando una matriz de longitud igual a la anterior más uno y copiando en ella los antiguos elementos. Si tratamos de almacenar un valor null , se genera una excepción DynamicFieldsException creando una excepción y utili zando initCause() para insertar una excepción NullPointerException como la causa. Como valor de retomo, setField() también extrae el antiguo valor situado en dicbo campo utili zando getField( ), que podría generar la excepc ión NoSuchFieldException (no existe un campo con dicho nombre). Si el programador de clientes invoca getField(), entonces será responsable de trata r la excepción NoSuchFieldException, pero si esta excepción se genera dentro de setField(), se tratará de un error de programación, por lo que NoSuchFieldException se convierte en una excepción RuntirneException utili zando el constructor que admite un argwnento de causa. Como podrá obervar, toString() utiliza un objeto StringBullder para crear su resultado. Hablaremos más en detalle acerca de StringBuilder en el Capítulo 13, Cadenas de caracreres, pero en general conviene utilizar este tipo de objetos cada vez que estemos escribiendo un método toString() que implique la utili zación de bucles como es el caso aquí. 294 Piensa en Java 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. Excepciones estándar de Java La clase Java Throwable describc todas las cosas que puedan generarse como una excepción. Existen dos tipos generales de objetos Throwable ("tipos de "= "que heredan de"). Error representa los errores de tiempo de compilación y del sistema de los que no tencmos que preocupamos de capturar (salvo en casos muy especiales). Exception es el tipo básico que puede generarse desde cualquiera de los métodos de la biblioteca estándar de Java. así como desde nuestros propios métodos y también cuando sc producen enores de ejecución. Por tanto. el tipo base que más interesa a los programadores de Ja\'a es usualmente Exception. La mejor fonna de obtener una panorámica de las excepciones consiste en examinar la documentación dcl JDK. Conviene hacer esto al menos una vez para tencr una idea de las distintas excepciones, aunque si lo hace se dará cuenta pronto de que no existen diferencias muy grandes entre una excepción y otra, salvo en lo que se refiere al nombre. Asimismo, el número de excepciones en Java continua creciendo; es por ello que resulta absurdo tratar de imprimirlas todas en un libro. Cualquier nueva biblioteca que obtengamos de un fabricante de software dispondrá probablemente, asimismo, de sus propias excepciones. Lo importante es comprender el concepto y qué es lo que debe hacerse con las excepciones. La idea básica es que el nombre de la excepción represente el problema que ha tenido lugar y que ese nombre de excepción trate de ser autoexplicativo. No todas las excepciones están definidas en java.lang; algunas se defmen para dar soporte a otras bibliotecas como util. Det e io, lo cual pueue deducirse a partir del nombre completo de la clase correspondiente o de la clase de la que heredan. Por ejemplo. todas las excepciones de E/S beredan de java.io.lOException. Caso especial: RuntimeException El primer ejemplo de este capítulo era: i f (t == nulll throw new NullPointerException{) ; Puede resultar un poco atenador pensar que debemos comprobar si todas las referencias que se pasan a un método son iguales a ouU (dado que no podemos saber de antemano si el que ha realizado la invocación nos ha pasado una referencia válida). 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 llamada a una referencia null, Java generará automáticamente la excepción NullPointerException . Por tanto, el anterior fragmento 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Ínadas 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 () static void 9 () ti) i { f II ; public static void main (String[] args ) { g il ; 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 programas, ya que el compilador obliga exhaustivamente a tratar todas las excepciones comprobadas. El razonamiento que explica 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 ejemplo 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 introducir demasiados elementos. Realización de tareas de limpieza con finally A menudo. existe algún fragmento de código que nos gustaría ejecutar independientemente de si la excepción ha sido generada dentro de un bloque try. Usualmente, ese fragmento de código se relaciona con alguna operación distinta de la de recuperación de memoria (ya que esta operación es realizada automáticamente por el depurador de memoria). Para conseguir este efecto, utili zamos una cláusula finally4 después de todas las rutinas de tratamiento de excepciones. La estructura completa de una sección de tratamiento de excepciones se ría, por tanto: " El mecanismo de tratamiento de excepciones de cabo estas tarcas de limpieza. e++ no dispone de la cláusula finally, porque depende de la utilización de destructores para llevar a 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 {} public class FinallyWorks { static int count = Di public static void main(String[] args) { while(true) try { /1 El post-incremento es cero la primera vez: if(count++ == Ql throw new ThreeException () i System.out.println(tlNo exception n ) ; catch (ThreeException e l { System. out .println ( "ThreeException ll ) ; 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 continuar con la ejecución a partir del punto donde se generó la excepción, como ya hemos indicado anteriormente. Si incluimos 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. ¿Para qué sirve finally? En un lenguaje que no tenga depuración de memoria y que no tenga llamadas automáticas a destructores,5 la cláusula fin ally es importante porque pem1ite al programador garantizar que se libera la memoria, independientemente de lo que suceda en 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 problema. 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 conexió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.*¡ public class Switch { private boolean state = false; public boolean read () { return state; } public void on () ( state = true¡ print (this ) ; public void off () ( state = false; print(this ) ; public String toString () { return state ? "on" : "off"; /// ,/1 : exceptions / OnOffExcept ionl.java public class OnOffExcept i on l extends Exception {} 11 1:1/ : exceptions/OnOffExcept ion2.java public class OnOffException2 extends Exception {} ///:- JI: exceptions/OnOffSwitch . java // ¿Por qué usar finally? public class OnOffSwitch { private static Switch sw = new Switch () i public static void f () throws OnOffExceptionl,OnOffException2 {} public static void main (String [] args ) { try { sw.on () j // CÓdigo que puede generar excepciones .. f {) , sw.off l) , catch (OnOffExceptionl e l { System. out. println ( "OnOffExceptionl" l ; sw.offl) , catch(OnOffException2 e l { System . out. println ( "OnOffException2") ; sw.off {) , 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 genere 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 . public class WithFinally 298 Piensa en Java static Switch sw ~ new Switch(); public static void main (S tring [] args) { try { sw.on() i // Código que puede generar excepciones ... OnOffSwitch.f() ; catch(OnOffException1 el { System.out.println(nOnOffException1") ; catch(OnOffException2 e l { System.out .println("OnO ffEx ception2") ; finally { sw . off(); / * 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. * ¡ class FourException extends Exception {} public class AlwaysFinally { public static void main(String[] args) print("Entering first try block " ); try { print ( "Entering second try block " ) ; try { throw new FourException()¡ finally { print{"finally in 2nd try block"); 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. Utilización de finally durante la ejecución de la instrucción return Puesto que ulla cláusula finall y siempre se ejecuta, resulta posible vo lver desde múltiples puntos dentro de un método sin dejar por e llo de garanti zar que se realicen las tareas de limpieza importantes : JJ : e x c e ptions JMultipleReturns . java import s t atic net . mindvie w. util . Print .* ; public class MultipleRetu r ns { public static void f ( int i ) print ( " Initialization that requires cleanup " ) ; try { print ( " Point 1" ) ; i f {i == l ' return; print (" Poi n t 2 " ) i i f {i == 2) return ; print (" Point 3" ) i i f ( i == 3 ) return; print ( "End'" i return; finally { print { "Performing cleanup" } ; public static void main (String [) for ( int i = 1; i <= 4; i++ ) f (i l ; 1* Output: Initialization that Poi nt 1 Performing cleanup Initialization that Po int 1 Po int 2 Performing cleanup Initialization that Po int 1 Point 2 Po int 3 Perfo r mi ng cleanup Initiali za tion that Po int 1 Po int 2 Point 3 args) { requires cleanup requires cleanup requires cleanup requires c l eanup 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 instmcción retu r n en mitad de la estructura try-finall y. Un error: la excepción perdida Lamentablemente, existe un fallo en la implementación del mecanismo de excepciones de Java. Aunque las excepciones Son una indicación de que se ha producido una crisis en el programa y nunca deberían ignorarse. resulta posible que una excepción se pierda si n más. Esto sucede cuando se utili za un a con figuración concreta con una cláusula fi na lly: 11 : exceptions / LostMessage.java II Forma de perder una excepción. class VerylmportantException extends Exception public String toString () { return nA very important exception!"; class HoHumException extends Exception public String toString() { return "A trivial exception" i public class LostMessage { void f () throws VerylmportantException throw new VerylmportantException () ; void dispose () throws HoHumException throw new HoHumException () i public static void main (String [] args ) { try ( LostMessage 1m = new LostMessage () ; try ( 1m. f () ; finally lm.dispose () ; 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 perderse 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 public class Ex ception Silencer { public static void main(S t ring[] args) try { throw n ew RuntimeException(} ¡ f inally { 1I La utili zación de t r eturn t dentro de un bloque f inally I1 h ará que se pie r da la excepción g e n e rada . return¡ 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 . Restricciones de las excepciones Cuando sustituimos un método, sólo podemos generar aquellas excepciones que hayan sido especificadas en la versión del método correspondiente a la clase base. Se trata de una restricción muy útil. ya que implica que el código que funci one con la clase base funcionará también automáticamente con cualquier objelO derivado de la clase base (lo cual , por supuesto, es un concepto fundamental den tro de la programación orientada a objetos), incluyendo las excepciones. Este ejemplo ilustra los tipos de restricciones impuestas a las excepciones (en tiempo de compilación): 11 : e x cep ti ons/Stormy l nning . java Los método s sustituidos sólo puede n generar l as exc e pcione s especificadas en sus v e rsione s de la clase base , II o e x cep ciones de r ivadas de las exc epcione s de la clase base . II II class Basebal l Excep t ion extends Ex ception {} class Foul extends BaseballExce ption {} class Strike extend s BaseballException {} abstract c l ass Inning { public Inning() throws BaseballException {} public void event() throws Basebal l Exception II No tiene por qué generar nada public abstract void atBat{) throws Strike, Faul¡ publ ic void walk () {} liNo genera ninguna excepción comprobada class StormEx ception e x t e nds Ex c ep tion {} class RainedOut e xt ends StormExc ep t i an {} class PopFoul extends Foul {} int erface Storm { pub li c v o i d e v ent () throws RainedOut¡ public v oid rainHard() throws RainedOut¡ public class Stormyl nning extends Inning implements Storm { 302 Piensa en Java II II Se pueden añadir nuevas excepciones para los constructores, pero es necesario tratar con las excepciones del constructor base: public Stormylnning() throws RainedOut, BaseballException {} public Stormy lnning (String s) throws Foul, BaseballException {} II Los métodos normales deben adaptarse a la clase base: II! void walk() throws PopFoul {} // error de compilación // Una interfaz NO PUEDE añadir excepciones a los métodos // existentes en la clase base: ji! public void event() throws RainedOut {} 1/ Si el método no existe ya en la clase // base, la excepción es válida: public void rainHard() throws RainedOut {} // Podemos definir no generar ninguna excepción, // aun cuando la versión base lo haga: public void event I I {} // Los métodos sustituidos pueden generar excepciones heredadas: public void atBat() throws PopFoul {} public static void main{String[] args) { try ( Stormylnning si = new Stormylnning{) ¡ si.atBat() i catch IPopFoul e l { System. out . println ( ti Pop foul") i catch (RainedOut e) { System . out. println ( tlRained out ti) i catch{BaseballException e) { System. out. println ( "Generi c baseball exception") ¡ // Strike no se genera en la versión derivada. try ( // ¿Qué sucede si generalizamos? Inning i = new Stormylnning()¡ i.atBat() i // Hay que capturar las excepciones de la 1/ versión del método correspondiente a la clase base: catch(Strike el { System. out. println ( " Strike i 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 tl ) } ///,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 excepciones 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 constructor puede generar todo aquello que desee. independientemente de lo que genere el constructor de la clase base. Sin embargo. 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 constructor 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 derivada de Inning. podrían generarse excepciones. con Jo que nuestro código fallaria. Obligando a los métodos de la clase derivada 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 genera excepciones. Podemos aplicar una lógica similar atBat( ), que genera PopFoul. una excepción que deriva de la excepció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 tratamien10 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 exaclamente 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 capturar 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 especificaciones 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 bastante de las reglas nomlales de herencia, según las cuales un método de la clase base deberá también existir en la clase derivada. 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 opuesta 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étodos 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 apropiadamente?"' La mayor parte de las veces, podemos eslar razonablemente seguros, pero con los constructores existe un problema. 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 apropiadamente. 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 excepciones 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 especificaciones 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.*; public class InputFile private BufferedReader in; public InputFile(String fname) throws Exception { try ( = new BufferedReader (new FileReader(fname»; Otro código que pueda generar excepciones catch(FileNotFoundException el { System.out.println("Could not open " + fname) ¡ II No estará abierto, por lo que no hay que cerrarlo. throw e¡ catch(Exception e l II Todas las demás excepciones deben cerrarlo in II try { in. close () ; catch (IOException e2) { System.out .println(lIin.close() unsuccessfuI U ) ; throw e¡ II Regenerar finally ( II ¡ j i No cerrarlo aquí!!! public String getLine () String Si { try ( s = in.readLine()¡ catch(IOException el throw new RuntimeException("readLine() failed ll ); return s; public void dispose() try ( in.close (); System.out .println ("dispose() successful"); catch (IOException e2) { throw new RuntimeException (" in. clase () failed 11) ; 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 particularmente ú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 encontrado 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 excepció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 simplemente 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 vocació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 abierto 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 excepción alguna. Uno de los problemas de diseño relati vos a las excepciones es el de si debemos tratar una excepción completam 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 apropiado, 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 liberen 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 objeto 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 programadores 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: ji exceptions jCleanup . java Forma de garantizar la apropiada limpieza de un recurso. public class Cleanup public static void main (St ring [] args) { try { InputFile in = new InputFile ("Cleanup.java") i try { String Si int i == 1 i while((s = in.getLine()) != null) i II Realizar aquí el procesamiento línea a línea. ca tch (Except ion e) { System.out.println("Caught Exception in main") i e.printStackTrace(Syscem.outl; finally { in . dispose() i 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 construcción tiene éxito, entonces hay que asegurarse de que el objeto se limpie, por lo que inmediatamente después de la construcció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 class NeedsCleanup { II Construction can't fail private static long counter = 1; private final long id = counter++¡ publ ie void dispose () { System.out.println ( "NeedsCleanup " + id + 11 disposed" ) ; class ConstructionException extends Exception {} class NeedsCleanup2 extends NeedsCleanup { II La construcción no puede fallar : public NeedsCleanup2() throws ConstructionException {} public class CleanupIdiom { public static void main (String [] args) { II Sección 1: NeedsCleanup ncl = new NeedsCleanup( ) ; try ( 11 ... finally ncl.dispose() ; II II Sección 2: Si la construcción no puede fallar, NeedsCleanup nc2 new NeedsCleanup{)¡ NeedsCleanup nc3 = new NeedsCleanup(); podemos agrupar los objetos: try ( 11 finally nC3.dispose {) ; nc2.dispose ( ) ; II II II Orden inverso al de construcción Sección 3: 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 catch (ConstructionException e) System . out .println(e) i finally ( nc4 . dispose ( ) ; { II const ru ctor de ncS 12 Tratamiento de errores mediante excepciones 307 catch (ConstructionException e l { // constructor de nc4 System.out.println (e l ; / * Output: NeedsCleanup NeedsCleanup NeedsCleanup NeedsCleanup NeedsCleanup 1 disposed 3 disposed 2 disposed 5 disposed 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 objeto 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 proceso de constmcción y generar una excepción. En ma in(). escriba el código que pemlita protegerse apropiadamente 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ódigo 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 cercanas", 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 tratamiento. 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. class Annoyance extends Exception {} class Sneeze extends Annoyance {} public class Human { 308 Piensa en Java public static void main (String [] args) { 11 Capturar el tipo exacto: try ( throw new Sneeze() ¡ catch(Sneeze sl { System.out. println (lICaught Sneeze ll ) ; catch (Annoyance a) { System.out.println(IICaught Annoyance ll ) i 11 Capturar el tipo base: try ( throw new Sneeze()¡ catch (Annoyance al { System. out. println ( 11 Caught Annoyance 11 ) ¡ 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 primera. 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 ejecutarse. 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 ejecución de la secuencia nonna! de instrucciones. Ese mecanismo especial se utiliza cuando tiene lugar una "condi ción excepcionar', 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ódigo de tratamiento de errores del punto en el que los errores se producen. Esto nos pennite concentramos en lo que queremos 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 lugares 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 excepciones: 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 compilador nos fuerza a escribir código desde el principio para tratar la excepción, incluir una rutina de tratamiento vacía parece 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 problema 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 algunos 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) resulta 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 lenguaje 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 comprobació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 incorporaron 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 estandarizació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 también 5011 poco ejicie11les, lo que tiende a desanimar a los programadores a la hora de .,·eliali:ar y tratar 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 continú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 descompuestas 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 resultado, los programadores solian abordar el problema de la misma manera que en e. ignorando los errores en el código y utilizando 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 excesiva. 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áticamente en la signatura del método las excepciones que pueden generarse como resultado de la llamada al método. La especificació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 resultado 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 especificació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 sobrecargadas 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 emplearse 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 (inspiradas claramente en las especificaciones de excepciones de e++ y en el hecho de que los programadores e++ no suelen ocuparse 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 antculo 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 incrementarse 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 problema 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 mayorí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 especificaciones 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 significado. "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 lasfimciones. 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 mecanismos 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 compatibilidad 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 modelo e++ para que las excepciones fueran la única fonna de infom1ar de los errores, la imposición 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 excepciones. 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, seguiremos 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 pueden 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 ado 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 Directrices relativas a las excepciones Utilice las excepciones para: 1. Tratar los problemas en el nivel apropiado (evite cap turar las excepciones a menos que sepa qué hacer con ellas). 2. Corregir el problema e invocar de nuevo el método que causó la excepción. 3. Corregir las cosas y continuar, sin volver a ejecutar el método. 4. Calcular algunos resultados altemativos en lugar de aquellos que se supone que el método debía producir. 5. Hacer lo que se pueda en el contexto actua l y regenerar la misma excepción, entregándosela a un contexto de nivel superior. 6. Hacer lo que se pueda en el contexto actual y generar una excepción diferente, entregándosela a un contexto de nivel superior. 7. Terminar el programa. 8. Simplificar (si el esquema de excepciones utilizado hace que las cosas se vuelvan más complicadas, entonces será muy molesto de utilizar). 9. Hacer que la biblioteca y el programa sean más seguros (esto es una inversión a corto plazo de cara a la depuración y una inversión a largo plazo en lo que respecta a la robustez de la aplicación) 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 incluso 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 escribamos 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 La manipulación de las cadenas de caracteres es probablemente una de las actividades más comunes en la programación . Esto resulta especialmente cierto en los sistemas web, en los que Java se ut ili za ampliamente. En este capítul o, vamos a examinar 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. Cadenas de caracteres inmutables Los objetos de clase String son inmutables. Si examina la documentación del JDK referente a la clase String, verá que todos los métodos de la clase que parecen modificar una cadena de caracteres, 10 que hacen, en realidad, es devolve r un objeto String co mp letamente nuevo que contiene dicha modificación. El objeto String original se deja sin modificar. Considere el siguiente cód igo: //: strings/lrnmutable .java import static net . mindview util .Pr int .* public class Immutable { public static String upcase (Stri ng s) return s.toUpperCase{); public static void main (String [] args) String q = "howdy"; printlq); II howdy String qq = upcase(q); printlqq); II HOWDY printlq); II howdy { { /* 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 referencia existe sólo mientras que se ejecuta el cuerpo de upcase(). Cuando se completa upcase( ), la referencia local s desaparece. 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. Comparación entre la sobrecarga de '+' y StringBuilder Puesto que los objetos String son inmutables, podemos establecer tantos alias como queramos para un objeto String concreto. Puesto que lm objeto String es de sólo lectura, no hay ninguna posibilidad de que una referencia modifique algo que pueda afectar a otras referencias. La inmutabilidad puede presentar problemas de rendimiento. Un ejemplo claro es el operador'+' que está sobrecargado para Jos objetos String. La palabra "sobrecargado" significa que hay una operación a la que se le ha proporcionado UIl significado adicional cuando se la usa con una clase concreta (los operadores '+' y '+=' para objetos String son los únicos operadores sobrecargados en Java, y el lenguaje no pemlite que el programador sobrecargue ningún otro operadorV El operador '+' nos pemlite concatenar cadenas de caracteres: JJ: stringsJConcatenation.java public class Concatenation { public static void main (Str ing [] argsl { String mango = "mango" i String s = "abe" + mango + "def" + 47; System.out.println(s) ; 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 posteriormente 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á incluida 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 circunstancia 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 new #3; I/class StringBuilder dup 3: 6, 7: 10: 12: 15: 16: invokevirtual #6; / /StringBuilder append: (Stri ng ) aload 1 invokevirtual #6¡ / /StringBuilder append: (String) 19, lde #7; //String def 21: 24: 26: 29: 32: 33: 36: invokevirtual #6; bipush 47 37: 40: invokespecial #4; //StringBuilder."lt: () ldc #5; I/String abe / /StringBuilder append: (String) invokevirtual #8; //StringBuilder append: (I) invokevirtual #9; / /StringBuilder. toString: () astore_2 getstatic #10; / /F ield System. out: PrintStream; aload 2 invokevirtual #11; JI PrintStream.println: (St ringl 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, porque 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 codificación de fonna manual con StringBuilder: jj: stringsjWhitherStringBuilder.java public class WhitherStringBuilder public String implicit (String [1 fields) String result = ""; for(int i = O¡ i < fields.length¡ i++) result += fields [i] ¡ return result¡ public String explicit (String (] fields) { StringBuilder result ~ new StringBuilder{) ¡ for(int i = O; i < fields.length; i++) result .append(fields [iJ); return result.toString{); Ahora, si ejecutamos javap -c WitherSlringBuilder. podemos ver el código (simplificado) para los dos métodos diferentes. 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: 7: aload 1 arraylength 8: if_icmpge 38 11: new #3; I/class StringBuilder 14: dup 15: 18: 19: 22: 23: 24: 25: 28: invokespecial #4; alead 2 invokevirtual #5; alead 1 iload 3 31: 32: 35: 38: 39: aaload invokevirtual #5; invokevirtual #6 i astare 2 iinc 3, 1 goto 5 aload 2 areturn // StringBuilder."":() // StringBuilder.append: () // StringBuilder.append: () / / StringBuilder. toString: () 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: 7: 8: 9: 10: 11: 12: 13: 16: 17: 18 : 19: 20: 23, 24: 27: invokespecial #4; astore 2 iconst O istore 3 iload 3 aload 1 arraylength lf lcmpge 30 alead 2 aload 1 iload 3 aaload invokevirtual #5; 30: 31: 34: / / StringBuilder." " : () /1 StringBuilder.append: () pop iinc 3, 1 goto 10 aload 2 invokevirtual #6; areturn jj StringBuilder.toString: () 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 import java . util.*; public class UsingStringBuilder public static Random rand = new Random(47)¡ public String toString () { StringBuilder result = new StringBuilder(" (11); for{int i = Oi i < 25; i++) { result.append(rand,nextInt{lOO)) ; result .append (", ") j result .delete (result .length () -2, result.append("l ") result .length () ) ; i return result.toString(); public static void main(String(] UsingStringBuilder usb System.out.println(usb) args) { = new UsingStringBuilder(); i / * Output: [58, 55, 93, 11, 22, 4] 61, 6 1, 29, 68, O, 22, 7, 88, 28, 51, 89, 9, 78, 98, 61, 20, 58, 16, 40, *///,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 incluso reverse( l, los que generalmente se usan son append( ) y toString( ). Observe el uso de delcte( ) para eliminar la última 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 seguridad en lo que respecta a las hebras de programación (véase el Cap ítulo 21 , Concurrencia) y era, por tanto, significativa mente 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 representació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.*¡ public class ArrayListDisplay public static void main(String[] args) { ArrayList coffees = new ArrayList() new CoffeeGenerator(lO)) for (Coffee e coffees.add(c}¡ System . out.println{coffees) i /* Output: i 322 Piensa en Java [Americano 0, Latte 1, Americano 2, Mocha 3, Mocha 4, Breve S, Americano 6, Latte 7, Cappuccino 8, Cappuccino 9] * /// ,Suponga que quisiéramos que el método toString( ) im primiera la dirección de la clase. Parece que tendría bastante do hacer referencia simplemente a this : sen ti~ jj : stringsjlnfiniteRecursion . java jj Recursión accidental. // {RunByHand} import java.util.*; public class InfiniteRecursion public String toString () { return 11 In f initeRecursion address: " -+ this -+ " \n "; } public static void main (Str ing [] args ) { List v = new ArrayList () ; f or(int i = O; i < la; i-+-+) v.add(new InfiniteRecursion() ) ; Sys tem. out .println (v) ; } 11/ , Si creamos un objeto InfiniteRecursion y luego lo imprimimos, obtendremos una secuencia muy larga de excepciones. Esto también se produce si colocamos los objetos InfiniteRecursion en un contenedor ArrayList e imprimimos dicho contene· dar como aquí se muestra. Lo que está sucediendo es una conversión automática de tipos para las cadenas de caracteres. Cuando decimos: "InfiniteRecursion address: 11 -+ this El compilador ve un objeto String seguido de un símbolo '+ ' y algo que no es un objeto Slring, por lo que trata de convertir Ihis a String. El compilador reali za esta conversión llamando a toString(), que es lo que produce una llamada recursiva. 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. Operaciones con cadenas de caracteres He aquí algunos de los métodos básicos disponibles para objetos String. Los métodos sobrecargados se resumen en una única fila : Método Argumentos, sobrecarga Constructor Sobrecargado: predetenninado, String, StringBuilder, StringBuffer, matrices char, matrices byte. l USO Creación de objetos String. Número de caracteres en el objeto String. length( ) charAt( ) índ ice ¡nt El carácter char en un a posición dentro del objeto String. getChars( ), gelBytes( ) El principio y el final del que hay que copiar, la matriz a la que hay que copiar, un índice a la matriz de dest ino. Copia caracteres o bytes en una matriz ex· tema. 13 Cadenas de caracte res 323 Argumentos, sobrecarga Método IOCbarArray( ) Uso Genera un char[] que contiene los caracteres contenidos en el objeto String. equals( ), equals-lgnoreCase( ) Un objeto String con el que comparar. Una comprobación de igualdad de los contenidos 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á contenido en el objeto String. contentEquals( ) Un objeto CbarSequence o St rin gB uffer con el que comparar.. El resultado es true si hay una co rrespondencia 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, el otro objeto Stri ng junto con su desplazamiento y la longitud para comparar. La sobrecarga aiiade la posibilidad de "ignorar mayúsculas y minúsculas". Un resultado booleano que indica si la región se corresponde. starlSWith( ) Objeto String con el que puede comenzar. La sobreca rga aiiade el desplazamiemo dentro de un argumento. Un resultado booleano que indica si el objeto String comienza con el argumento. endsWith( ) Objeto Slring que puede ser sufijo de este objeto String. Un resu ltado booleano que indica si el argumento es un sufijo. indexOf( ), lastlndexOf( ) Sobrecargado: char, char e índice de inicio, String. String e índice de inicio. Devuel ve - 1 si el argumento no se encuen tra dentro de este objeto String; en caso contrario, devuelve el índice donde empieza el argumento. lastlndexOf( ) busca hacia atrás el final. Sobrecargado: índice de micio: Índice de inicio + índice de fin. Devuelve un nuevo objeto String que conliene el conjunto de caracteres especificado. concat( ) El objeto String que haya quc concatenar. Devuel ve un nuevo objeto StriDg que cOllliene los caracteres del objeto String original seguidos de los caracteres del argumento. replace( ) El antiguo canlcter que hay que buscar, el nuevo carácter con el que hay que reemplazarlo. También puede reemp lazar un objeto CharSequeoce con otro objeto CharSequence. Devuelve un nuevo objeto String en el que se han efectuado las sustituciones. Utiliza el antigua objeto String si no se encuentra ninguna correspondencia. substring( ) (también subScquencc( » toLowerCase( ) toUpperCase( ) Devuelve un lluevo objeto String en el que se 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 necesario realizar ningún cambio. 324 Piensa en Java Método Argumentos, sobrecarga Uso valueDr( l Sobrecargado: Object, charl! . charll y desplazamiento y recuento, boolean. charo int. long, noat. double. Devuclvc un objeto String que contiene una representación en fonna de caracteres del argumento. Produce una y sólo una referencia a un objeto String por cada secuencia de caracteres distinta. intern( ) 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 estilo 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( ) utiliza marcadores especiales para indicar dónde deben insertarse los datos. Los argumentos que se insenan en la cadena de formato 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 riable 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 public class SimpleFormat { public static void main (String (J args ) { int x ::: 5¡ double y : 5.332542, JJ A la antigua usanza: System. o ut .println ( "Row 1, [" + x + " + y + "] " ) ; /1 A la nueva usanza: System. out. format ( "Row 1, [\d UI \ n", x, y ) , // o 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 Systern.out.printf(tlRow 1: [%d %fl\n", X, y); 1* Output : Row 1, Row 1, Row 1, [5 5. 332542J [5 5.3325421 [5 5. 332542J * /// ,Puede ver que format() y printf() son equivalentes. En ambos casos, hay una única cadena de ronnata, seguida de un argumento 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 COllsiderarFormatter 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 infon11ación al constmctor: //: strings/Turtle.java import java.io.*; import java.util.*; public class Turtle private String name; private Formatter f; public Turtle (String name, Formatter f) this.name : name; this.f = fi { public void move (int x, int y) { f. format ("%s The Turtle is at (%d, %d) \n", name, x, y); public static void main(String[] args) PrintStream outAlias = System.out¡ Turtle tommy = new Turtle ("Tommy", new Formatter(System.out)) ¡ Turtle terry = new Turtle ("Terry", new Formatter(outAlias))¡ tommy.move(O,O) ; terry.move{4,8)¡ tommy.move(3,4) ¡ terry.move{2, 5) ¡ tommy.move(3,3) ¡ terry.move(3,3) ¡ /* Output: Tommy Terry Tommy Terry Tommy Terry The The The The The Turtle Turtle Turtle Turtle Turtle The Turtle is is is is is is at at at at at at (O, O) (4,8) (3,4) (2,5) (3,3) (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 elaborados. 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 incluyendo ;-' 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ñadiendo 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 c lass Receipt private double cotal = O; private Formatter f = new Formatter (S yscem.out ); public void printTitle () { f . format( "% - 15 s %56 %lOs\n", "Item", "Qty", "Price ll )¡ f.format ( "%-15s %56 %lOs\n ", tI _ _ _ _ _ " ) ¡ public void print (St ring name, int qty, double price) { f.format ( "%-15.1Ss %5d %lO .2f \n", name, qty, price); total += price; public void printTotal() f.format ("%-15s %5s %10.2f\n", "Tax", " ", tocal*0.06); f.format("%-15s %5s %10s\n", "", "", "-----"); f. format (" %-155 %55 %10. 2f\n", "Total", total * 1.06); public static void main{String[] arg6) Receipt receipt = new Receipt(); receipt.printTitle{) ; receipt. print ("Jack' s Magic Beans", 4, 4.25); receipt. print ( " Princess Peas", 3, 5.1); receipt .pri nt("Three Bears Porridge", 1, 14.29); receipt.printTotal{) ; 1* Output: Item Jack's Magic Be Princess Peas Three Bears Por Tax Qty 4 3 1 Price 4.25 5.10 14.29 1. 42 13 Cadenas de caracteres 327 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 apropiado. Ejercicio 4: (3) Modifique Receipt.java para que todas las anchuras estén controladas por un único conjunto de va lores constantes. El objetivo es poder cambiar fácilmente una anchura modificando un único valor en un determinado lugar. Conversiones posibles con Formatter Éstas son las conversiones con las que más frecuentemente nos podremos encontrar: Caracteres de conversión d Entero (como decimal) e Carácter Uni code b Valor booleano s Cadena de caracteres f Coma flotante (como decimal) e Coma fl otantes (en notación cientifica) x Entero (como hexadecima l) h Código IwsJ¡ (as hexadeci mal ) % Literal " %" He aquí un ejemplo que muestra estas conversiones en acción: jj: strings;Conversion .java import java.math.*¡ import java.util.*¡ public class Conversion public static void main{String [] args) { Formatter f = new Formatter(System.out)¡ 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 f.format {"d: %-d \ nll, v ) ; f.format ( "c: %c \ n", v ) ; f.format. ( l1b: %b \ nl1, v ) ; f.format {"s: %s \ n", v ) ¡ 11 f.format l "f, H\n", v ) ; II f.format(lIe: %-e \ n", v}; f.format("x: %x\n", v ) ¡ f.format("h: %h\n", v); Biglnteger w = new Biglnteger(1150000000000000 1' }¡ System.out.println ( "w = new Biglnteger (\ "50000000000000 \ " ) " ) ; f. format ( lid: %d \ nll, w) ; II f.format ( "c: %-c \ n", w) ; f.format ( lIb: %b \ n", w ) ; f.format {"s: %s \ n", w) ; II f.format("f: %f \ n", w) ; II f.format {"e: %e \ n", w ) ; f . format ( " x: %-x \ n ", w) ; f.format ( "h: %h\n", w) ; 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 ¡ Conversion y = new Conversion () ; System . out.println ( "y = new Conversion () " ) ; II f. format ( lid : %d\ n ti, y ) ; II f.format ( "c: %c \ n", y l ; f.format ( "b: %b \ n", y ) ; f.format ( lIs: %s \ n", y ) ; 11 f. format 1" f , H \ n", y ) ; I I f.format("e: %-e\n", y}; 1/ f.format{"x: %x\n", y); f.f o rmat("h: %h \ nll, y ) ; 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 79 79 x: h, w d, = new Biglnteger("SOOOOOOOOOOOOO" ) 50000000000000 true s, 50000000000000 x, 2d79883d2000 b: 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 argumento 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 documentació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 public class DatabaseException extends Exception { public DatabaseException ( int transactionID, int queryID, String messagel { super(String.format{ti (t%d, q%d) %SU, transactionID, queryID, message}); 330 Piensa en Java public static void main(String(] args) try ( throw new DatabaseException(3, 7, "Write failed"); catch(Exception el { System.out . println(e) ; 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 hayamos proporcionado, pero utili zar este método puede resultar a menudo más claro y más fácil que hacerlo de fonna manual. Una herramienta de volcado hexadecimal Como segundo ejemplo, a menudo nos interesa examinar los bytes que componen un archivo binario utilizando formato hexadecimal. He aquí una pequeña utilidad que muestra una matriz binaria de bytes en un formato he xadecimal legible, utilizando Striog.format() : 11 : net/m i ndview/util/ He x .java pack age ne t . mindview.u t il; impo r t java . io. * ¡ public class Hex { public static String f ormat (byte (] data) { StringBui l der result = new StringBuilde r() ; int n = O; for (byte b , data) { i f (n % 16 == O) r e sult.append(String.format( U%05X: n»); r e sult . app end(String .format( " %02X ", b ) ; n ++ ; if(n % 16 = = O) r esult . append(U\n U); result.append{ u\n U) ; return resul t .toSt ri ng() i public static void main(String[] args) throws Exception if(args . length == O) II Comprobar mostrando este archivo de clase: System.out.println( format(BinaryFile.read{UHex class U») i el se System . out.println( format{BinaryFile.read{new File{args[O] » » ; / , Output: 00000 , CA FE 00010 , 00 23 00020, 00 27 00030, 00 2C 0004 O, 31 08 00050, 36 00 (Sample) BA DA DA 00 00 37 BE 00 00 2D 32 07 00 02 28 08 DA 00 00 00 00 00 00 38 00 22 29 2E 33 DA 31 08 DA DA 00 00 00 00 00 00 34 12 52 24 02 02 DA 00 DA 07 00 00 00 39 00 00 2A 2F 15 OA 05 25 08 09 00 00 00 DA 00 00 35 33 22 00 2B 30 DA 00 07 26 DA 00 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 comparamos 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 describe mediante '\d', Si tiene experiencia con las expresiones regulares en otros lenguajes, obsenrará inmediatamente la diferencia 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 utilizan 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 "posiblemente 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 public class IntegerMatch { public static void main(String[] args) { System.out.println("-1234".matches("-?\\d-t")) ; System.out.println(15678 I ,matches("-?\\d-t")) i System.out.println("+911",matches(II-?\\d+ II ) ) ; System.out.println("+911".matches("(-I\\-t)?\\d+")) ; / * 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 expresiones 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ácter '+ ' 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 caracteres juslO donde se produ zcan las co rrespondencias con la ex presión regul ar indicada". /f : strings f Splitting.java import java.util. * ¡ pub l ic class Splitting ( public static String knights "Then, when you have found the shrubbery, you must " + "cut down the mightiest tree in the forest... It + "with ... a herring! " ¡ public static void split(String regex) System.out.println( Arrays . toString{knights.split{regex) ) ) ; public static void main (String [] args) { split ( " II ) ¡ II No tiene porqué contener caracteres regex split ( " \\ w+" ) ¡ II Caracteres no pertenecientes a una palabra split(lIn \\ W+ ") ¡ 1/ 'n ' seguida de caracteres no II pertenecientes a una palabra / * 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 parti 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 pertenezcan 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 primera apari ción o todas elJas: // : strings / Replacing . java import static net.mindvie w.util.Print. * ¡ public class Replacing { static String s = Splitting . knights ¡ public static void main(String[] args) print (s. replaceFirst ( " f \ \ w+ I t , tt located " ) ) ¡ 13 Cadenas de caracteres 333 print(s.replaceAll("shrubbery!treelherring","banana ll / * Output: Then, when yau have forest ... with . .. a Then, when you have forest ... with ... a ) i located the shrubbery, you must cut down the mightiest tree in the herring! found the banana, you must cut down the mightiest banana in the 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 expresió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. Creación de expresiones regulares Podemos comenzar a aprender ex presiones regulares con un subconjunto de las estrucUlras posibles. En la documentación del JDK correspondiente a la clase Pattern de java.util.regex podrá encontrar la lista completa de las estrucUlras que pueden emplearse para construir las expresiones regulares. Caracteres B El ca rácter específico B \xhh Carácter con el valor hexadecimal Oxhh luhh hh El ca rácter Unicode con la representación hexadecimal Oxh hh h 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í algunas fonnas típ icas de crear clases de caracteres, junto con algunas clases predefinidas: 334 Piensa en Java Clases de caracteres Cua lqui er carácter label Cualquiera de los caracteres a , b o (' (lo mi smo que a lblc) I' abel Cualquier carácter excepto a , b y e (negación) la-zA-ZI Cualquier carácter de la a a la z o A a la Z (rango) labcl hij ll Cualquiera de la-z&& lhijll Puede ser h. i o j (intersección) Is Un carácter de espaciado (espacio. tabulador, nueva linea, avance de pág ina. retomo de carro) IS Un carácter que no sea de espaciado (I"\s)) Id Un dígito numérico (0-91 ID Un carácter que no sea un dígito 1"'0-91 \w Un carácter de palabra (a-zA-Z_0-91 \W Un carácter que no sea de pa labra ("'\wl a.b.c,b,i.j (lo mismo que . lblelbliÜ) (unión) 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 (X) Un grupo de captllra. Puede referirse posteriormente al i-ésimo grupo capturado en la expresión mediante \i. Localizadores de contorno , Comi enzo de línea $ Fin de linea lb Frontera de palabra \B Frontera de no palabra IG Fin de la correspondencia anterior Por ejemplo, cada una de las siguientes ex presiones pennite localiza r la secuencia de caracteres "Rudo lph": jj , strings j Rudolph.java public class Rudolph { public static veid main (String [] args l { fer (String pattern : new String [] { "Rudolph", 13 Cadenas de caracteres 335 "[rRludolph", "[rR] [aeioul [a-z]ol.*", "R.*" }) System. out .println (II Rudo lph" . mat c hes (pattern ) ) ; / * 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 realizar 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 establezca 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. Avaricioso I Reluctante Posesivo Correspondencia con X? X?? X?+ X, una o ninguna X* X*? X*+ X, cero o más X+ X+? X++ X, una o más X{n} X{n}? X{n}+ X, exactamente n veces X{n,} X{n,}? X{n, }+ X, al menos n veces X{n,m} X{n,m} ? X{n,m} + X, al menos n pero no más de m veces 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 entrada ' abcabcabc' , obtendremos de hecho tres correspondencias. Sin embargo, lo que la expresión dice en realidad es: "locali 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) toString() ¡ i 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 bastante 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 correspondencias, seguida de una o más expresiones regulares que haya que aplicar a la entrada. En Un ix/Linux, las exp resiones regulares 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.*¡ public class TestRegularExpression public static void main{String[] args) if(args.length < 2) { print ( "Usage: \njava TestRegularExpression "characterSequence regularExpression+") ¡ System.exit{Q) ; + print(IIInput: \"" + args[Ol + "\"") i for (String arg : args) { print (" Regular expression: \"" + arg + "\"" ) i Pattern p = Pattern.compile(arg) i Matcher m = p.matcher {args[O]); while(m.find()) ( print("Match \ "" + m.group() + "\" at positions " + m.start() + 11_" + (m .end () 1)); 13 Cadenas de caracteres 337 } 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 boolean boolean boolean matches() lookingAt() f ind () 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 localizar la correspondencia: . . Java \8reg. * n.w\s+h(ali)s s? s' s+ S(4} S(l} . s(a,3} Ejercicio 11 : (2) Apl ique la expresión regular (?i) (( A laeiouJ) I (\s+ laeiouJ)) \ w+? laeioul \b a "Arline ate eight apples and one orange while Anita hadn' t any" 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.*; public class Finding { public static void main (String [] args) { Matcher ID = Pattern.compile{"\\w+"l .matcher(UEvening is full of the linnet's wings ll ) ; while (m. Eind () ) princnb(m.group() + " ")i print() ; int i :: O i whilelm.find(i)) printnb (m .group () i++; + 11 11); / * 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 cadena 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 existen tres grupos: el grupo O es ABCD, el grupo 1 es BC y el ¡''fUpO 2 es C. El objeto Malcher dispone de mélodos para proporcionamos información acerca de los grupos: public int gro up Co unl( ) devuelve el número de grupos que hay en el patrón. El grupo O no se incluye dentro de este recuento. public Slring group( ) devuelve el grupo O (la correspondencia completa) de la operación anterior de establecimiento de correspondencias (por ejemplo, find ( )). public String gro up(inl i) devuelve el número de grupo indicado dentro de la operación de establecimiento de correspondencias anterior. Si esa operación ha tenido éxito, pero el grupo especificado no se corresponde con ninguna parte de la cadena de entrada, devuelve el valor null . public inl slarl(in t group) devuelve el índice de inicio del grupo encontrado en la operación anterior de establecimiento de correspondencias. public int end(int group) devuelve el índice del último carácter, más uno, del grupo encontrado en la anterior operación de establecimiento de correspondencias. He aquí un ejemplo: // : strings/Groups.java import java.util.regex.*¡ import static net.mindview.util.Print.*¡ public class Groups { static public final String POEM 13 Cadenas de caracteres 339 "Twas brillig, and the slithy toves\n " + "Did gyre and gimble in the wabe . \n" + "All mimsy were the borogoves , \n" + "And the mame raths outgrabe. \n\nll + "Beware the Jabberwock, rny son, \n" + "The jaws that bite, the claws that catch. \ n" + IIBeware the Jubjub bird, and shun\n" + "The fru mious Bandersnatch ." i public static void main(String[] argsl Matcher ID ~ Pattern. compile (" (?ml (\ \8+1 \ \8+ ( (\ \8 +1\ \ 8+ (\ \8+11 $" 1 .matcher (POEM) i while(m.find(11 ( for (int j = O; j <= m.groupCount{); j++) printnb(" [" + m.group(j) + lO] " ) ; print ()¡ 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 consiste en hacer corresponder "$' con el final de la secuencia de entrada comp leta, por lo que es necesario decir expLícitamente 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 correspondencia, start() devuelve el índice de inicio de la correspondencia anterior, mientras que cnd() devuelve el índice del último 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 genera 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.*¡ public class StartEnd { public static String input "As long as there is injustice, whenever a \ n" + "Targathian baby cries out, wherever a distress \ n" + "signal sounds among the stars ... We I 11 be there. \ n" + "This fine ship, and this fine crew ... \ n" + "Never give up! Never surrender!"¡ 1 El texto indicado es una cita de uno de los discursos del Comandante Taggart en Gala-cy Quest. 340 Piensa en Java private static class Display { private boolean regexPrinted = false; priva te String regex; Display (String regex) { this . regex = regex; void display(String message) { if (! regexPrinted) { print (regex) ; regexPrinted = true; print (message); static void examine(String s, String regex) Display d new Display(regex); Pattern p = Pattern.eompile(regex); Matcher m = p.matcher(sl; whilelm.find() ) d.display("findO '" + m. group() + '" start = "+ m.start{) + " end = " + m.end(»; if(m.lookingAt{» II No reset(} necessary d. display (" lookingAt () start = " + m.start() + " end = " + m. end(»; if(m.matches(» II No reset() necessary d. display ("matehes () start + rn.start() + " end = " + m.end(»; public statie void mai n {String[] args) for(String in : input.split("\n"» { print (" input : ti + in); for{String regex : new String[) {H\\w*ere\\w*" , "\\w*ever", "T\\W+", "Never. * ?!"}) examine {in, regex) ; 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 lookingAt() start = O end = 5 Never. *?! find() 'Never give up!' start O end = 14 find() 'Never surrender!' start 15 end = 31 looki ngAt () start O end = 14 matches () start = O end = 31 -/1/,Observe que find () permite localizar la expresión regula r en cualquier lugar de la entrada, mientras que lookingAt() y matchcs() sólo tienen éxito en la búsqueda si la expresión regu lar se corresponde desde el pri nc ipio de la entrada. Mientras que matc hes( ) sólo tiene éx ito en la búsqueda si loda la en trada se corresponde con expresión regular, lookingAt()4 tiene éxito en la búsq ueda au nque sólo se corresponda la expresión reg ula r con la primera parte de la entrada. Eje rcici o 13: (2) Modifique Start End.java pa ra que ut ilice Gro ups.PO EM como entrada, pero siga produciendo resultados positivos para find (), lookingAt( ) y matches( ). Indicadores de Pattern Hay un método compile( ) altemativo que acepta indi cadores que afectan al co mportamiento de búsqueda de co rrespondencias: Pattern Pattern.compile{String regex, int flag) donde flag puede ser una de las siguientes constantes de la clase Pattero : Efecto Indicador de compilación 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 correspondencias 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 presupone 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úsculas. 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. (?i) Patte rn .COM M ENTS (?x) En es te modo. se ignoran los caracteres de espaciado, y también se ignoran los comentarios 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 (?s) En este modo, la expresión'.' se corresponde con cualquier carácter, incluyendo el tenninador de línea. Por omisión, la expresión'.' no se corresponde con los tenninadores de línea. Pattern.M ULTI U NE (? m) Patte rn.UN ICOD E_CASE (?u) Pancrn.UNIX _ LI NES (?d) - -En el modo multilínea. las expresiones 'A' y 'S' se corresponden con el principio y el final de una 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. La correspondencia sin distinción de mayúsculas y mi núsculas, si está habi litada por el indicador CASE_lNSENSITIV E. se realiza de manera coherente con el estándar Unicode. De manera predetenninada, 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. En este modo, sólo se reconoce el tenni nador de línea '\n ' en el comportamien to de ;.', ,'" y '$ '. No 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. 4 342 Piensa en Java De especial utilidad entre todos estos indicadores son Pattern.CASE_INSENSITlVE, Pattern.MULTlLlNE y Pattern.COMMENTS (que resulta útil para mejorar la cla ridad y/o con propósitos de dcoumentación). Observe que el comportamiento de la mayor parte de los indicadores puede obtenerse también insertando en la expresión regular los caracteres entre paréntesis que se muestran en los indicadores de la tabla, j usto antes del lugar donde se quiera que ese modo tenga efecto. También podemos combinar el efecto de estos y otros indicadores mediante una operación "OR" el'): 11 : strings / ReFlags.java import java.util.regex. * ; public class ReFlags { public static void main (String [) args ) { Pattern p = Pattern . comp i le ( lI"'java lt , Pattern . CASE_ I NS ENSITIVE I Pattern . MULTILINE ) ; Matcher m = p.matcher ( Itjava has regex \ nJava has regex \ n ll + IIJAVA has pretty good regular expressions \ n ll + "Regular e x pressions are in Java lt ) ; while (m.find () ) Sys t em . out .prin tln {m.group {)) i 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á buscar una correspondencia con cada línea que fonne parte de un conjunto multilínea (correspondencias que comienzan al principio 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 expresión regular. String[] String[] split {CharSequence input ) 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. * ¡ publi c c lass SplitDemo { public static void main (String [] args ) { String input = "This! !unusual use! ! o f e x c l amation! !poi n ts tl print (Arrays . toStri ng ( Pattern . compile ( II ! ! 11 ) . split(input ))) ¡ II Hacer sólo las t r es p r imeras : print(Arrays . toString ( Pattern . compile ( " !!" ) .split (input, 3 ))) ¡ 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 caractercS. 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 sustinJir 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. * ¡ /*! Here's a block of text to use as input to the regular express ion matcher. Note that we'll first extract the block of text by looking tor the special delimiters, then process the extracted block. ! * I public class TheReplacements public static void main(String(] args) throws Exception { String s ::: TextFile. read ( UTheReplacements. java") ¡ II Establecer correspondencia con el bloque de texto con II comentarios especiales mostrado anteriormente: Matcher mlnput ::: Pattern.compile("I\\*! (.*) !\\ * /". Pattern.DOTALL) .matcher(s) ¡ if(mlnput.find()) s ::: mlnput.group(l)¡ II Capturado por paréntesis Sustituir dos o más espacios por un único espacio: s ::: s.replaceAII(to {2, }II, " "); II Eliminar dos o más espacios al principio de cada II línea. Hay que habilitar el modo MULTILíNEA: s = s.replaceAII("{?m) '" +", ""J¡ print(s) ¡ s = s. replaceFirst ( " [a eiou] ". " (VOWEL1J 11) ; StringBuffer sbuf ::: new StringBuffer{); Pattern p ::: Pattern. compile (11 [aeiou] ") ¡ Matcher m = p.matcher(s); II Procesar la información de localización a medida II que se realizan las sustituciones: II while (m . find ()) 344 Piensa en Java m.appendReplacement {sbuf, m.group () .toUpperCase ()) i // Insertar el resto del texto: m.appendTail (sbuf) i print (sbuf ) ; / * 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 mostrará 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 situados 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 necesario 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 sustitució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 capulradas mediante la notación "$g", donde 'g' es el número de grupo. Sin embargo, este método sólo sirve para tareas de procesamiento 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.*; publi c class Resetting { public static void main(String[] args) throws Exception Matcher m = Pattern.compile {tI {frbl {aiu] [gx] ti ) .matcher(lIfix the rug with bags ll ) ; while (m.find () ) System . out.print {m. group {) + 11 ti) i System.out . println( ) ; m.reset ( "fix the rig with rags ll ) ; while(m.find() ) System.out.print (m.group () + 11 11 ) ; / * Output: 13 Cadenas de caracteres 345 fix rug bag fix rig rag ' 111 ,rcsct() sin ningún argum ento bace qu e Matcher se situe al princi pio de la secuencia actu al. Expresiones regulares y E/S en Java La mayoría de los ejemplos vistos hasta ahora mostraban la apli cación de las expresiones regulares a cadenas de caracteres es táticas. El siguiente ejemplo muestra una forma de aplicar expresiones regulares a la búsqueda de corres pondencias en un archivo. Inspirada en la utilidad grep de Unix, JGrep.java lOma dos argum entos: un nombre de archi vo y la ex presión regular con la que se qui ere buscar correspondencias. La salida muestra cada línca en la que se ha detec tado una correspondencia y la posición o posiciones de las correspondencias dentro de la línea: // : strings/JGrep.java / / Una versión muy simple del programa "grep". II {Args, JGrep.java " \\b[Ssctl\\w+"} import java.util.regex. * ¡ import net.mindview . util .* ¡ public class JGrep { public static void ma i n{Stri ng[] args) throws Exception i f (args . leng t h < 2) { System.out . println(ItUsage : java JGrep file regex " ) ¡ Sy stem . exit(O) ¡ Pattern p Pattern . compile (args (1 ]) ; /1 Iterar a través de las lineas del archivo de entrada: int index Oi Matcher m p . matcher ("") ; for(String line : new TextFile(args[O))) m. reset (line) ; while (m. find()) System.out.println(index++ + 11: " + m.group() +" : " +m.start()); 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 lineas del archivo en un contenedor tipo ArrayList. Esto significa que podemos utilizar la sintaxi s foreach para iterar a travé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 argumento (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 sintáctico de Integer, Double, etc., para analizar los datos: 11: strings/SimpleRead.java impert java.io.·; public class SimpleRead public static BufferedReader input = new BufferedReader( new StringReader("Sir Robin of Camelot\n22 1.61803")); public static void main(String (] args) { try ( System.out.println("What is your name?"); String name = input. readLine () ; System.out.println(name) ; System.out.println( "How old are you? What is your favorite double?"); System.out . println(l!(input: shapeList = Arrays.asList( new Circle(), new Square(), new Triangle() ) ; for(Shape shape : shapeList ) shape.draw() ¡ /* 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 concatenació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. 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 simplemente 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 correcció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 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 objetos. UIlO de los objetivos generales que se persiguen con la programación orientada a ¿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 geomé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á apuntand 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 infonnació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 compilamos una nueva clase. también se crea un determinado objeto Class (y ese objeto se almacena en un archivo .class de nOI11bre 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 normalmente 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 servidor web, o descargar clases a través de una red). entonces disponemos de una manera de enlazar cargadores de clases adicionales. Todas las clases se carga n en la JVM dinámicamente, cuando se utiliza la clase por primera vez. Esto sucede cuando el programa 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 distintos fragmentos del programa a medida que so n necesarios. Esto difiere de muchos lenguajes tradic ionales. El mecani smos 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 ) ; } public class SweetShop { public static void main (String (] args) print (11 inside main " ); new Candy () ; print(IIAfter creat ing Candy " ); try ( Class. forName ("Gum") i catch (ClassNotFoundExcep tion e) { print ("Couldn' t find Gum") ; { print ( " After Class. f orName (\ "Gum\ " ) 11 ) ; new Cookie () ; print("After creating Cookie lt ) ; / * 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 primera 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étodo devuelve una referencia de tipo Class, que en este ejemplo se ignora; la llamada a forName() se rcali za debido a su efec10 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. generará 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 {} class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots { FancyToy 11 { super 111; } public class ToyTest { static void print l nfo (Class ce) { print( "Class name: u + ce.getName() + u is interface? [ " + cc.islnterface() + I']U); print (ti Simple name: " + ce. getSimpleName () ) ; print ( "Canonical name : " + cc. getCanonicalName (l ) ; public static void main(String[] args) Class e = null i { try { c o:: Class. forName ( "typeinfo. toys. FaneyToytl) ; catch (ClassNotFoundException e) { print ( "Can I t find FancyToy"); System.exit(1) ¡ 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 Canonical name : typeinfo.toys.HasBatteries Class name: typeinfo. toys. Waterproof is interface? [true] Simple name: Waterproof Canonical name : typeinfo.toys .Waterproof Class name: typeinfo . toys . Shoots is interface? [true] Simple name: Shoots Canonical name : typeinfo.toys.Shoots Class name: typeinfo . toys . Toy is interface? (false] Simple name: Toy Canonical name : typeinfo.toys.Toy *///,FancyToy hereda de Toy e implementa las interfaces HasBatteries, Waterproof y Shoots. En main( ), se crea una referencia Class y se la inicializa para que apunte al objeto Class FancyToy Class utili zando forName() dentro de un bloque Iry apropiado. Observe que hay que utilizar el nombre completamente cualificado (incluyendo el nombre del paquete) en la cadena de caracteres que se pasa a forName( ). printlnfo() utiliza geIName() para generar el nombre de clase completamente cualificado, y geISimpleName() y geICanonicaIName() (introducidos en Java SE5) para generar el nombre sin el paquete y el nombre completamente cualificado. respectivamente. Como su propio nombre indica, islnterface() nos dice si este objeto Class representa un interfaz. Por tanto, con el objeto Class podemos averiguar casi todo lo que necesitemos saber acerca de un determinado tipo. El método Class.getlnlerfaces( ) invocado en m.inO devuelve una matriz de objetos Class que representa las interfaces contenidas en el objeto Class de interés. Si tenemos un objeto Class, también podemos preguntarle cuál es su clase base directa utilizando geISuperclass(). Este método devuelve una referencia Class que podemos, a su vez, consultar. Por tanto, podemos detenninar la jerarquía de cIases completa de un objeto en tiempo de ejecución. El método newlnstance( ) de Class constituye una forma de implementar un "constructor virtual" que nos pennite decir: "No sé exactamente de qué tipo eres, pero crea una instancia de tí mismo de todas formas". En el ejemplo anterior, up es simplemente una referencia Class de la cual no se conoce ninguna infoffi1ación de tipos adicional en tiempo de compilación. Y cuando creamos una nueva instancia, obtenemos como resultado una referencia Object. Pero dicha referencia apunta a un objeto Toy. Por supuesto, antes de poder enviar a ese objeto ningún mensaje diferente de los que admite Object, es necesario in vestigar un poco acerca del objeto y efectuar algunas proyecciones de tipos. Además, la clase que se crea con newlnstance() debe disponer de un constructor predeterminado. Posterionnente en este capítulo, veremos cómo crear objetos dinámicamente de una cierta clase utilizando cualquier constructor, empleando para ello la API de reflexión de Java. Ejercicio 1: (1) En ToyTest.java, desactive mediante un comentario el constmctor predetenninado de Toy y explique lo que sucede. Ejercicio 2: (2) Incorpore un nuevo tipo de interfaz en ToyTest.java y verifique que dicha interfaz se detecta y se muestra adecuadamente. Ejercicio 3: (2) Anada Rhomboid a Shapes.java. Cree un objeto Rhomboid y generalícelo a Shape, y vuelva a especializarlo a Rhomboid . Trate de especializarlo a un objelO Circle y vea lo que sucede. Ejercicio 4: (2) Modifique el ejercicio anterior para que utilice instanceof con el fin de comprobar el tipo, antes de efectuar la especialización. Ejercicio 5: (3) Implemente un método rotate(Shape) en Shapes.java, que compmebe si está girando un círculo (y, en caso afirmativo, no realice la operación). Ejercicio 6: (4) Modifique Shapes.java para que permita "resaltar" (acti vando un indicador) todas las formas de un tipo concreto. El método toString( ) para cada objeto derivado de Shape debe indicar si dicho objeto Shape está ··resaltado". Ejercicio 7: (3) Modifique SweelShop.java para que la creación de cada tipo de objeto esté controlada por un argumento de la línea de comandos. En otras palabras, si la línea de comandos es "java SweetShop Candy", entonces sólo se creará el objeto Candy. Observe cómo se pueden controlar los objetos Class que se cargan, utilizando argumentos de la línea de comandos. 14 Información de tipos 357 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: .... equIvIIInee .... boolcan.cla ss Boolean.T"PE char.cJass C haracter.TYPE byte.class Byte.TYPE short. cl ass Short.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 normales. 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 objeto 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"); public class Classlnitialization { public static Random rand = new Random(47 ) ; pUblic static void main(String[] args) throws Exception Class initable = Initable.class; System .out. println ("A fter creating Initable ref"); II No provoca la inicialización: System.out.println(Initable.staticFinal) ; II Provoca la inicialización: System.out.println(Initable.staticFinal2) i II Provoca la inicialización: Systern.out.println(Initable2.staticNonFinal) i Class initable3 :: Class. forName ( 11 Initable3 11 ) ; System .out. println ( "After creating Initable3 ref") System.out.println(Initable3.staticNonFinal) i i 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 creació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 almacenamienw) antes de que el va lor pueda ser leído, como puede ver analizando el acceso a Ini table2 .staticNo n Final. Referencias de clase genéricas Una referencia C lass apunta a un objeto C lass, que genera instancias de las clases y contiene todo el código de los métodos correspond ientes a dichas instancias. También contiene los valores estáticos de dicha clase. Por tanto, una referencia Class realmente indica el tipo exacto de aquello a lo que está apuntando: un objeto de la clase C lass. Sin embargo, los di señadores de Java SES vieron la oportunidad de hacer esto un poco más específico, pennitiendo restringir el tipo de objeto C lass al que la referencia C lass apunta, utilizando para ello la sintaxis genérica. En el siguiente ejemplo, ambos tipos de sintaxis son correctos: /1: typeinfo/GenericC lassReferences.java public class GenericClassReferences public static void main{String[] args) Class intClass = int.class; Class genericlntClass int.class¡ genericlntClass = Integer .class¡ II Lo mismo intClass = double . class¡ II genericlntClass = double . class¡ II Ilegal 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 asignarse a su tipo declarado. Utilizando la sintaxis genérica, permilimos que el compilador imponga comprobaciones adicionales 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: Class genericNumberClass = int.class¡ 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 analizaremos 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 public class WildcardClassReferences { public static void main (String[} args) Class intClass = int.class; intClass = double . class¡ En Java SE5, se prefiere utilizar Class en lugar de Class, aún cuando ambos son equivalemes y la referencia Class normal. como hemos visto, no genera ninguna advertencia del compilador. La ventaja de Class es que indica que no es tamos 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 combinar un comodín con la palabra clave ex tends para crear un limite. Por lanlO, en luga r de decir simplemente C lass. lo que diríamos seria: 11: typeinfo/BoundedClassReferences.java public class BoundedClassReferences { public static void main(String[] args) Class bounded = int.class; bounded = double.class; bounded = Number.class; II o cualquier otra cosa derivada de Number. ) /11 ,La razón de aiiadir la si ntax is genérica a las refe rencias Class estriba únicamente, en real izar una comprobación de los tipos en tiempo de compilación, de modo que si hacemos algo incorrecto lo detectaremos un poco antes. No es posible realizar nada realmente deslnlctivo con las referen cias Class normales, pero si cometemos un error no podremos detectarlo hasta el tiempo de ejecución, lo que puede resultar incómodo. He aquí un ejemplo donde se utiliza la si ntaxis de clases genéricas. El ejemplo almacena una referencia de clase y luego genera un contenedor List relleno con objelOs generados mediante newlnstance( ): 11 : typeinfo/FilledList.java import java.util.*; class Countedlnteger private static long counter; private final long id = counter++; public String toString() ( return Long. toString (i d ) ¡ public class FilledList { private Class type¡ public FilledList (Class type) { this. type public List create (int nElements) { List result = new ArrayList(); type; ) try { for(int i = O; i < nElements; i++) result.addCtype.newlnstance()) ; catch (Exception e) ( throw new RuntimeException(e); return resul t; public static void main(String[] args) ( FilledList fi = new FilledList (Countedlnteger.class) ; System . out.println(fl.create(15)) ; 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 advertencia 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 limitado : 14 Información de tipos 361 JI : typeinfo / toys / GenericToyTest.java JI Prueba de la clase Class. package typeinfo.toys¡ public class GenericToyTest public static void main(String[] args ) throws Exception { Class ftClass = FancyToy.classi /1 Produce el tipo exacto: FancyToy fancyToy = ftClass.newlnstance() i Class up = ftClass.getSuperclass{) i JI Esto no se compilará: = ftClass.getSuperclass{); Sólo produce Object: /1 Class up2 JI Object obj = up . newlnstance () ; } / // , Si obtenemos la supcrclase, el compilador sólo nos pcnnitirá decir que la referencia a la superclase es "alguna clase que es superclase de FancyToy" , co mo podemos ver en la expresión C lass. No aceptará una declaración de Class. Esto parece un poco ex traño, porque getS up ercl ass( ) devue lve la clase base (no una interfaz) y el compilador conoce en tiempo de compilación lo que esa clase es: en este caso, Toy.c lass, no simplemente "al guna superclase de FancyToy". En cualquier caso, debido a la vaguedad, el valor de retorno de up. new lnsta nce( ) 110 es de un tipo preciso, sino sólo de tipo Obj cct. Nueva sintaxis de proyección Java SE5 también ha añadido una sintaxis de proyección para utilizarla con las referencias Class, nos referimos al método cast( ): /1 : typeinfolclassCasts.java class Building {} class House extends Building {} public class ClassCasts { public static void main(String[] argsl ( Building b = new House(); Class houseType = House.class; House h = houseType. cast (b ) ¡ h = (House ) b¡ 1/ o haga simplemente esto. ) ///,El método cast() toma el objeto proporcionado como argumento y lo proyecta sob re el tipo de la referencia Class. Por supuesto, si exa minamos el código anterio r parece que es demasiado trabajo adicional, si lo comparamos con la última línea de maine ), que hace exactamente lo mismo. La nueva sintaxis de proyección resulta útil en aquellas situaciones en las que no podemos utili zar una proyección ordinaria. Esto sucede, usualmente, cuando estamos escribiendo código genérico (de lo que hablaremos en el Capítulo 15, Genéricos), y hemos almacenado un a referencia Class que queremos utilizar en algún momento posterior para efectuar la proyecc ión. Este caso no resulta muy frecuente ; de hecho, sólo he podido encontrar una única ocasión en la que cast( ) se use dentro de la biblioteca de Ja va SE5 (concretamente en co m.sun.mir ro r.utiI.Declara tion Filter). Hay otra nueva funcionalidad que 110 se Uliliza en absoluto cnla biblioteca Java SE5: C lass.asS ubclass(). Este método permite proyectar el objeto de clase sob re un tipo más específico. Comprobación antes de una proyección Hasta ahora, hemos visto varias fonuas de RTTI , inclu yendo: 362 Piensa en Java J. La proyección clás ica, por ejemplo, "(Shape);' que utili za RTTl para asegurarse de que la proyección es correcta. 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 objeto 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 disposición históricamente utilizada en los diagramas de jerarquías de clases. Si la proyección de Circle sobre Shape es una generali 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 compilador que disponemos de información adicional que nos permite saber que se trata de un tipo concreto (el compilador comprobará 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 instancia 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 disponemos 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 anali 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; public class Person extends Individual { public Person{String name } ( super(name); ) /// ,// : typeinfo/pets/Pet.java package typeinfo pets; public class Pet extends Individual { public Pec(String name) { super(name); public Pet () { super () ; ) ///,// , typeinfo/pets/Dog.java 14 Información de tipos 363 package typeinfo pets; public class 009 extends Pet { public Dog (String name ) { super(name ) ¡ public Dog () { super () ; } 1/ 1,- 11 , typeinfo/pets/Mutt.java package typeinfo.pets i public class Mu tt extends 009 { public Mutt (String name ) { super (name) ; public Mutt () { super () ; } 111 > 1/ : typeinfo / pets/Pug . java package typeinfo.pets¡ public class Pug extends 009 { public Pug (String name ) { super (name ) public Pug () { super () ; } i 1/ 1 ,- JI : typeinfo / pets / Cat.java pac kage typeinfo.pets i pu blic cIass Cat extends Pet { public Cat (String name ) { super (name ) ; public Cat () { super () ; } 1/ 1 , ji : typeinfo / pets / EgyptianMau . java package typeinfo.pets; public class EgyptianMau extends Cat { public EgyptianMau (String name ) { super (name ) ; public EgyptianMau (1 { super () ; } 1/ 1 ,// : typeinfo / pets / Manx.java pac kage typeinfo.pets; public cIass Manx extends Cat { public Manx (String name ) { super (name ) ; public Manx (1 { super () ; } 111 , // : typeinfo / pets / Cymric . java package typeinfo.pets; public class Cymric extends Manx { public Cymric (String name ) ( super (name ) ; public Cymric (1 { super () ; } 111 ,- JI : typeinfo / pets / Rodent . java package typeinfo.pets; public class Rodent extends Pet { 364 Piensa en Java public Rodent(String name) public Rodent (1 { super 11 ; super (name) ; } 111> //: typeinfo/pets/Rat.java package typeinfo . petsi public class Rat extends Rodent { public Rat (String name) { super (name) ; public Rat (1 { super (1; ) 111,11: typeinfo/pets/Mouse.java package typeinfo.pets; public class Mouse extends Rodent { public Mouse (String name) { super (name) ; public Mouse (1 { super (1; ) 111,/1: typeinfo/pets/Hamster .java package typeinfo.pets; public class Hamster extends Rodent { public Hamster(String name) { super (name) ¡ public Hamster () { super (); } 111,A continuación, necesitamos una fanna de crear aleatoriamente diferentes tipos de mascotas, y por comodidad, vamos a crear matrices y listas de mascotas. Para pemlitir que esta herramienta evolucione a través de varias implementaciones diferentes. vamos a definir dicha herramienta como una clase abstracta: 1/ : typeinfo/pets/PetCreator.java // Crea secuencias aleatorias de objetos Peto package typeinfo.pets¡ import java.util.*¡ public abstract class PetCreator { private Random rand = new Random(47)¡ II La lista de los diferentes tipos de Pet que hay que crear: public abstract ListeClasse? extends Pet» types() ¡ public Pet randomPet () { / / Crear un obj eto Pet aleatorio int n = rand.nextlnt (types (l .size())¡ try { return types () . get (n) . newlnstance () ; catch (InstantiationException e) { throw new RuntimeException{e); catch{IllegalAccessException el throw new RuntimeException(e); public Pet(] createArray{int size) Pet[] result = new Pet[size]; for(int i = O; i < size; i++) result[iJ = randomPet() i return resul t; public ArrayListePet> arrayList (int size) { ArrayListePet> result = new ArrayListePet>(); Collections.addAll(result, createArray(size)); 14 Información de tipos 365 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 "cualquier 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 generar 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 excepciones se tratan en las cláusulas catch que siguen al bloque try. De nuevo, los nombres de las excepciones son casi autoexplicativos 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.*¡ public class ForNameCreator extends PetCreator { private static List public void CQunt (String type) ( Integer quantity = get(type); if(quantity == null) put (type, 1); else put(type, quaneity + 1); public static void countPets (PetCreator creator) { PetCounter counter= new PetCounter(); for(Pet pee : creator . createArray(20)) II Enumerar las mascotas individuales : printnb (pet. getClass () . getSimpleName () + if(pet instanceof Pet) counter.count(UPet") ; if(pet instanceof Dog) counter.count(OIDog " ) ; if(pet instanceof Mute ) counter.count ( "Mutt U ) ; if(pet instanceof Pug) couneer . count ( "Pug" ) ; if(pet instanceof Cat) counter.count {IICat") ; if(pet instanceof Manx ) counter. count ("EgyptianMau") ; if(pet instanceof Manx) counter. count ( "Manx " ) ; if(pet instanceof Manx) counter. count ("Cymric") ; if(pet instanceof Rodent) counter . count ( "Rodent 11) ; if(pet instanceof Rat ) counter. count (" Rat" ) ; if(pet instanceof Mouse ) counter. count ( "Mouse" ) ; if(pet instanceof Hamster ) counter . count ( "Hamster" ) ; ) II Mostrar las cantidades: print (); print (counte r ) ; public static void main(String[) args) countPets(new ForNameCreator()); u U); 14 Información de tipos 367 } / * 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} */// 0En countPets(), se rellena aleatoriamente un a matri z con objetos Pet utilizand o un objeto PetCreator. Después, cada obj eto 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. Utilización de literales de clase Si reimplernentamos la clase PctCreator usando literales de clase, el resultado es mucho más limpio en muchos aspectos: 11 : typeinfo/pets / LiteralPetCreator.java II Utili z ación de literales d e clase. package typeinfo . pets; import java . util. · ; public class LiteralPetCreator extends PetCreator { 11 No hac e f alta b l oque t r y. @SuppressWarn ings{"unchecked n ) public static final List arrayList (int size) { return creator .arrayList (size) ¡ } 111,Esto proporciona también una indirección para acceder a randomPet( ), createArray( ) y .rrayList(). Puesto que PetCount.countPets() toma como argumento PetCreator, podemos probar fácilmente la clase de LiteralPetCreator (mediante el envoltorio anteriormente definido): //: typeinfo/PetCount2 . java import typeinfo.pets.*¡ public class PetCount2 { public static void main(String[] args) PetCount.countPets(Pets.creator) ; /* (Execute to see output) *///:- La salida es igual que la de PetCount.java. 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.*¡ public class PetCount3 { static class PetCounter extends LinkedHashMap,Integer:> { public PetCounter () { super (MapData.map (LiteralPetCreator. allTypes, O)); public void count (Pet pet) ( // Class .islnstance {) elimina instrucciones instanceof: for(Map.Entry,Integer:> pair : entrySet()) if (pair .getKey() . islnstance (pet)) put(pair.getKey(), pair.getValue() + 1); public String toString () 14 Información de tipos 369 StringBuilder resul t = new StringBuilder (" {") ; for(Map.Entry,Integer> pair entrySet ()) { result.append(pair.getKey() .getSimpleName(» i result,append("=") i result.append(pair.getValue(» ; resul t . append ( ", ") i result.delete{result.length()-2, result. append ( "} ") ; result.length(»; return result.toString(); public static void main (String [J PetCounter petCount = args) { new PetCounter(); for (Pet pet : Pets.createArray(20» { printnb (pet . getClass () . getSimpleName () petCount.count(pet) ; + I! " ); 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 loPara 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 significa que podemos añadir nuevos tipos de Pet si mplemente cambiando la matriz LiteraIPetCreator.types; el resto del programa 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ípica 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 sobrecargar el mapa, podemos utili za r Class.ísAssignableFrom() y crear una herramienta de propósito general que no esté limitada 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 . *; public class TypeCounter extends HashMap type) { Integer quantity = get(type); put(type, quantity == null ? 1 : quantity Class superClass = type.getSuperclass() if(superClass != null && baseType.isAssignableFrom(superClass» countClass(superClass) ; + 1) i i public String toString () { StringBuilder result = new StringBuilder (" {") ; for(Map.Entry,Integer> pair : entrySet{» result.append(pair.getKey() . getSimpleName(»; result.append("= " ) ; result.append(pair.getValue(» ; resul t. append (", "); resul t . delete (resul t .length () - 2, result .length () ) ; result.append("} " l; return result.toString(); 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.*; public class PetCount4 { public static void main(String[] args) { TypeCounter counter = new TypeCounter(Pet.classl; for(Pet pet : Pets.createArray(20» { printnb (pet. getClass () . getSimpleName () + " "); counter.count(pet) ; 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 adaptarlos 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 Ejercicio 13 : (3) Ulilice TypeCounter con el ejemplo RegisteredFactories.java de esle capítulo. 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. probablemente, 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 interfaz 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{); static List<:Factory<:? extends Part» partFactories new ArrayList<:Factory<:? extends Part»(); static { II Collections.addAll( ) genera una advertencia "unchecked generic / I array creation ... for varargs parameter". partFactories.add(new FueIFilter.Factory()) j partFactories.add(new AirFilter.Factory()); partFactories . add(new CabinAirFilter.Factory()); partFactories.add(new OilFilter.Factory()); partFactories.add(new FanBelt.Factory()); partFactories.add{new PowerSteeringBelt.Factory()); partFactories.add(new GeneratorBelt.Factory()); private static Random rand = new Random{47) j public static Part createRandom () { int n = rand.nextlnt{partFactories.size()); return partFactories. get (n) . create () ; 372 Piensa en Java class Filter extends Part {} class FuelFilter extends Filter // Crear una factoría de clases para cada tipo específico: public static class Factory implements typeinfo.factory.Factory public FuelFilter create() { return new FuelFilter() i class AirFilter extends Filter { public static class Factory implements typeinfo.factory.Factory public AirFilter create{) { return new AirFilter( ); class CabinAirFilter extends Filter { public sta tic class Factory implements typeinfo.factory . Factory public CabinAirFilter create () { return new CabinAirFilter(); class oilFilter extends Filter { public static class Factory implements typeinfo .factory.Factory public OilFilter crea te () { return new Oi lFil ter () i class Belt extends Part {} class FanBelt extends Belt public static class Factory implements typeinfo.factory.Factory public FanBelt create() ( return new FanBelt() i class GeneratorBelt extends Belt ( public static class Factory implements typeinfo.factory.Factory public GeneratorBelt crea te () ( return new GeneratorBelt(); class PowerSteeringBelt extends Belt { public static class Factory implements typeinfo . factory.Factory public PowerSteeringBelt create () { return new PowerSteeringBelt(); 14 Información de tipos 373 public class RegisteredFactories { public static void main(String [] argsl { far(int i = O; i < 10; i++) System out.println(Part.createRandom()); / * 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 objeto 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. instanceof y equivalencia de clases Cuando tratamos de extraer iufonnación sobre los tipos, existe una diferencia imporlante entre ambas fonnas de instanccof (es decir, instanceof o isInst.nee(), que produce resultados equivalentes) y la comparación directa de los objetos Class. He aquí un ejemplo que ilustra la diferencia: 11 : typeinfo/Fami1yVsExactType.java II La diferencia entre instanceof y los objetos clase package typeinfo¡ import static net.mindview.uti1 . Print.*¡ c1ass Base {} c1ass Derived extends Base {} pub1ic c1ass Fami1yVsExactType static void test (Object x) { print (IITesting x of type + x. getClass () ) ; print ("x instanceof Base + (x instanceof Base»; 374 Piensa en Java print (" x instanceof Deri ved "+ (x instanceof Deri ved) ) ; print ("Base . islnstance (xl "+ Base. class. islnstance (x l) ; print("Derived.islnstance{x) " + Derived.class.islnstance(x)) ; print{"x.getClass() == Base.class " + (x.getClass() == Base.class)); print("x.getClass() == Derived.class (x .getClass () == Derived.class)); 11 + print (!1 x. getClass () . equals (Base . class)) n + (x .getClass () .equals (Base.class))) i print("x.getClass() .equals(Derived.class)) " + (x . getClass () . equals (Deri ved. class) ) ) ; public static void main (String [] argsl test (new Base()); test(new Derived()); { / * 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 pruebas 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. Reflexión: información de clases en tiempo de ejecución Si no conocemos el tipo concreto de un objeto, el mecanismo RTIl nos los dirá. Sin embargo, existe una limitación: el tipo debe ser conocido en tiempo de compilación, para poder detectarlo utilizando RTfI y para poder hacer algo úti l con la información. Dicho de otro modo, el compilador debe conocer todas las clases con las que estemos trabajando. A primera vista, esto no parece que sea una limitación importante, pero suponga que nos entregan una referencia a un objeto que no se encuentra en nuestro espacio de programa. De hecho, suponga que la clase del objeto no está ni siquiera disponible para nuestro programa en tiempo de compilación. Por ejemplo, suponga que extraemos una se rie de bytes de un archivo de disco o de una conexión de red, y nos dicen que esos bytes representan una clase. Dado que esta clase aparece después de que el compilador haya generado el código de nuestro programa, ¿cómo podríamos utilizar esta clase? En un entorno de programación tradicional, este escenario parece un poco futurista. Sin embargo, a medida que nos desplazamos hacia un mundo de programación más amplio, aparecen casos de gran importancia en los que lo que sucede es pre- 14 Información de lipos 375 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 fonnulario 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 instanciables, 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 nombres 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 ejecutar 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 modificación del software). En la misma línea, la informática d istribuida también soporta la utilización de hardware especializado 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 objeto 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 compilació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 necesario 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. Un extractor de métodos de clases Nom13 lmente, no vamos a necesitar utilizar las herramientas de reflexión directamente, pero sí que pueden resultar útiles cuando necesitemos crear código más dinámico. La reflexión se ha incluido en el lenguaje para soportar otras características de Java, como la serialización de objetos y JavaBeans (ambos temas se tratan posterionnente en el libro). Sin embargo, hay ocasiones en las que resulta muy úti l extraer dinámicamente la infonnación acerca de una clase. Consideremos el caso de un extractor de métodos de clases. Examinando el código fuente de la definición de una clase o la documentación del JDK, sólo podemos conocer los métodos definidos o sustiruidos dentro de dicha definición de clase. Pero puede haber otra docena de métodos disponibles que procedan de las clases base. Localizar estos métodos es muy tedioso 376 Piensa en Java 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.*; public class ShowMethods { private static String usage "usage:\nll + "ShowMethods qualified.class.name\n ll + "To show all methods in class or:\n" + "ShowMethods qualified.elass.name word\n ll + "To search for methods involving 'word ' ''; private static Pattern p = Pattern . compile{II\\w+\\ ." )¡ public statie void main {String (] args) { if (args .length < 1) { print(usage) ¡ System.exit (O) ¡ 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++¡ for(Construetor etor : etors) if(ctor.toString() . indexOf(args[l]) != -1 ) { print(p.mateher( etor.toString(») .replaeeAll(""»); 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 public public public public public public static void main(String[]) native int hashCode() final native Class getClass () final void wait(long,int ) throws InterruptedException final void wait() throws InterruptedEx ception final native void wait (long) throws InterruptedException 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, argumentoS y va lores de retomo de los métodos que representan. Pero también podemos utili zar toString(), como se hace en el ejemplo, 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 infonnació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 mecanismo 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 mentació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 informació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 sintetizado 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 predetenninado. 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 RealObject implements Interface { public void doSomething () ( print ( ldoSomething" ) ; public void somethingElse (St ring arg) ( print("somethingElse + arg) i 11 class SimpleProxy implements Interface { private Interface proxied¡ public SimpleProxy (Interface proxied) ( this.proxied = proxied; public void doSomething() print{"SimpleProxy doSomething") ¡ proxied.doSomething() ; public void somethingElse (String arg) { print (IISimpleProxy somethingElse 11 + arg ); proxied . somethingElse (arg) i class SimpleProxyDemo { public static void consumer (Interface iface) iface.doSomething() ; iface. somethingElse ( "bonobo lt ) ; ( public static void main(String[] args) ( consumer{new RealObject(»; consumer(new SimpleProxy(new RealObject(»); 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é sucede 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ámicamente 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. *¡ class DynamicProxyHandler implements InvocationHandler private Obj ect praxied¡ public Dynami cProxyHandler (Object proxied) { this .proxied = proxied¡ 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") ¡ { public static void main{String[] args) RealObject real = new RealObject() ¡ consumer (real) i IJ Insertar un proxy y llamar de nuevo: Interface proxy = ( Interface )Proxy.newProxyInstance( Interface.class.getClassLoader() , new Class [J { Interface. class }, new DynamieProxyHandler(real)); consumer(praxy) ¡ 1* Output: (95% match) doSamething somethingElse bonabo **** proxy: class $ProxyO, methad: public abstraet void Inter faee.doSomething (), args: null doSomething **** praxy: class $ProxyO, methad: public abstraet vaid Interface. samethingElse (java .lang.String) , args: [Ljava.lang.Object¡@42e816 banaba 380 Piensa en Java 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, (aunque 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 soliciJd 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 .* ; class MethodSelector i mplements InvocationHandler private Object proxied; public MethodSelector{Object proxied) ( this.proxied = proxied; 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 Implementation implements SomeMethods { public void boringl () { print ( lI boringl " ) i } public void boring2() ( print("boring2") i } public void interesting (String arg) { print (" interesting " + arg); public void boring3 () ( print (Uboring3") ; 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 signatura 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 encargue de confirmar /0 transacción si la llamada realizada al objeto real tiene éxito (no genera ninguna excepció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 complejo. 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 comprobando 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 ocasiones, 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 . Descubieno 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 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 cliente (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). .3 Nlllo es el patrón de disello de 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 utili 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¡ public String toString () { return "Person: " + first + " " + last + " " + address; public sta tic class NullPerson extends Person implements Null private NullPerson() { super("None", "None", "None") ¡ public String toString() { return "NullPerson"; } public static final Person NULL = new NullPerson() ¡ /! 1 ,En general. el objeto nu lo se rá un objeto simple (no una serie de objetos agrupados en contenedores), por lo que aquí se crea como una instancia estática final. Esto fun ciona porq ue Perso" es inmutable: sólo podemos fijar los va lores en el co nstructor, y luego leer d ichos valores, pe ro no modi fi carlos (porq ue los propios String son inherentemente inmutables). Si desea modificar un objeto NullPerson , sólo puede susti tuirl e con un nuevo objeto Perso n oObserve que dispo nemos de la opción de detectar el genérico Null o el más específico NullPerson utilizando instanceof, pero como se trata de un va lor simp le también podemos utilizar equals( ) o incluso = pa ra com parar co n Pcrson.NULL. Aho ra suponga que estuviéramos en la época dorada de las empresas de Internet y que alguien hubiera invertido una gran cantidad de dinero en una maravi llosa idea que hub iéramos ten ido. imagine que estamos listos para recl utar personal pero que. mientras espe ramos a que las vacantes sean cubiertas. podemos util iza r objetos nul os Perso" para asignarlos a cada puesto de lrabajo (Posítioll): // : typeinfo/Position.java class Position { private String title¡ private Person person¡ public Position (String jobTitle, title = jobTitle¡ person = employee; if(person == null) person = Person.NULL¡ public Position{String jobTitle) title = jobTitle; Person employeel { 14 Información de tipos 383 person = Person . NULL; public String getTitle() { return title; public void setTitle (String newTitle) { title = newTitle¡ publ ic Person getPerson () { return person; public void setPerson(Person newPerson) { persan = newPerson; i f {pe rs on == null) person = Person.NULL; public String toString(} return "Position : " + title + " It + person¡ } 111 > Con Position , no ten emos necesidad de crear un objeto nulo, porque la existencia de Person.NULL implica un objeto Position nulo (es posible que más adelante descubramos que sí se necesita añadir un objeto nulo explícito para Position. pero hay una regla qu e dice que siempre debemos implementar la solución más simple que funcione en nuestro primer diseño, y esperar a que algún aspecto del programa requiera que añadamos la característica adicional, en lugar de asumir desde el principio que esa característica es necesaria).4 La clase Staff puede ahora buscar los objetos nulos a la hora de rellenar las vacantes: 1/ : typeinfo/Staff. java import java . util .*¡ public class Staff extends ArrayList { public void add{String title, Person person) { add(new Position{title, person) ) ¡ public void add{String . titles) for{String title : titles) add{new Position(title)) ¡ public StaffIString . .. titlesl ( addltitles); } public boolean positionAvailable (String title) { for(Position position this) if (pos ition.getTitle () .equals(title ) && position.getPerson() == Person.NULL) return true; return false¡ public void fillPosition{String title, Person hire) for(Position position this) if(position.getTitle() .equals (title) && position.getPerson() == Person.NULL) position. setPerson (hire) ; return¡ { throw new RuntimeException( "Position " + title + " not available"); public static void main (Str ing [1 args) { Staff staff = new Staff("President", "CTOII, "Marketing Manager", "Product Manager", "Project Lead", "Software Engineer", 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 "Software Engineer", "Software Engineer", "Software Engineer", "Test Engineer", "Technical Writer"); staff.fillPosition("President", new Person(IOMe", "Last", "The TOp, Lonely At"))¡ staff. fillPosition ("Project Lead", new Person( nJanet", "Planner", "The Burbs")); if(staff.positionAvailable("Software Engineer"l) staff. fillPosition ("Software Engineer", new Person ( "Bob", "Coder", "Bright Light City")); System.out.println(staffl ¡ 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 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 public interface Operation String description() ¡ void command () ; /// , Podemos acceder a los servicios de un objeto Robot invocando operations( ): // : typeinfo /Robot.java import java . util .* ; import net.mindview.util.*; public interface Robot String name () ; String model () ; List operations() ¡ class Test { public static void test (Robot r) { if(r instanceof Null) System.out.println(" (Null Robot} ") ¡ System . out.println("Robot name: + r . name(}); System. out . println ("Robot model: " + r. model () } ; for(Operation operation : r.operations(}) { System.out . println{operation . description()) ; operation.command() ; 11 } /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. * ; public class SnowRemovalRobot implements Robot { private String name; public SnowRemovalRohot (String name) { this . name "" name;} publ ic S t ring name () { return name ; } publ ic String model () { return "SnowBot Se ries 11 " i } public Lisc operations () { return Arrays . asList{ new Operatian() { public String description() return name + " can shove l snow"; public void cornmand{) System . out .prin tln(name + " s h oveling snow" ); }, n e w Operatian () public String description () return name + " can chip ice"; public void command() System.out.println(name } + I! chippi ng ice " ); }, new Operation() public String desc ri ption(} return name + " can clear the roof"; public void command() System . out.println(name + " cl e aring roof " ) i 1; public static va id main (String [J args) { Robot . Test.test(new SnowRemovalRobot("Slusher")} i / * 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.*¡ class NullRobotProxyHandler implements InvocationHandler private String nullName; private Robot proxied = new NRobot(); NullRobotProxyHandler(Class type ) nullName = type .getSimpleName () + Ir NullRobot"; private class NRobot implements Null, Robot { public String name() { return nullName¡ } public String model () { return nullName ¡ } public List operations () { return Collections.emptyList{); public Object invoke{Object proxy, Method method, Object[] args) throws Throwable { return method. invoke {proxied, args) ¡ public class NullRobot public sta tic Robot newNullRobot{Class type) return (Robot)Proxy.newProxylnstance( NullRobot.class .getClassLoader() , new Class[] { Null.class, Robot.class }, new NullRobotProxyHandler(type» i public static void main(String(] args) Robot [) bots = { new SnowRemovalRobot ( "SnowBee"), newNullRobot{SnowRemovalRobot.classl }; for(Robot bot : bots) Robot.Test.test(bot) ; 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 Objetos maqueta y stubs Hay dos tipos \ ariantes del objeto nulo: el objeto maq/leta y el SI/lb. Al igual que el objeto nu lo. ambos tipos de objetos se utilizan en lugar del objeto "real" que empleará el programa terminado. Sin embargo. tanto el objeto maqueta como el sllIb pretenden se r objetos vivos que entregan infonl18Ción real en lugar de ser un sustituto un poco más inteligente de null, como es el caso del objeto nulo. La diferenc ia entre el objeto maqueta y un s/IIb es bastante sutil. Los objetos maqucla tienden a ser ligeros (poco complejos) y tienen capacidad de auto-comprobación. y usualmente se crean muchos de ellos para gestionar di stintas situaciones de prueba. Los swbs son típicamente más pesados y a menudo se reutilizan entre una pmeba y otra. Los stubs pueden configurarse para cambiar de comportamiento. dependiendo de cómo se los invoque. Por tanto. un slIIb es un objeto sofisti cado que lleva a cabo una de esas tareas; mientras que para hacer esas mismas ta reas con objetos maqueta lo que nomlalmente haríamos es crear muchos objetos maqueta pequClios y simples. Ejercicio 24: (4) Aliada objetos nulos a RegistercdFactories.java. Interfaces e información de tipos Un objetivo clave de la palabra clave interface es pemlitir al programador aislar componentes. reduciendo así el acopIamiento. Si escribimos el código basándonos en interfaces. conseguimos este objetivo. pero con la información de tipos es posible saltarse los controles: las interfaces no So n una garantía de desacoplamiento. He aquí un ejemplo comenzado con una interfaz: 11 : typeinfo / interfacea / A.java package typeinfo.interfacea¡ 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) {} {} public class InterfaceViolation { public static void main (String [) args) { A a = new B () ¡ a . f 11 ; 11 a.g()¡ 11 Error de compilación System.out . println(a.getClass() .getName()); ifla instanceof E) { B b = lB) a; b . gl) ; 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 ccmas 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, Conviene 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) ¡ } public class HiddenC { public static A makeA () { return new C () ¡ } ) /// , La única parte pública de este paquete, HiddenC, produce una interfaz A cuando se la invoca. Lo que es interesante acerca de este ejemplo es que incluso si devolviéramos un objeto C desde ma keA( ), seguiríamos sin poder utilizar ninguna otra cosa distinta de A desde fuera del paquete, ya que no podemos nombrar e fuera del paquete. Ahora, si tratamos de efectuar una especialización sobre fuera del paquete: 11 : e, no podemos hacerlo, porque no hay ningún tipo 'C' disponible typeinfo/Hiddenlmplementation.java II Sorteando el acceso de paquete. import typeinfo . interfacea.*¡ import typeinfo.packageaccess.*¡ import java.lang.reflect.*; public class Hiddenlmplementation public static void main{String(] args) throws Exception ( A a = HiddenC.makeA{); a . E () ; System. out. println (a. getClass () . getName () ) ¡ II Error de compilación: no se puede encontrar el símbolo 1* if(a instanceof C) { e e = (C) a; c. 9 () ; ICI: */ II ¡Caramba! La reflexión nos permite invocar g(): callHiddenMethod (a, "glI ); II ¡E incluso métodos que son menos accesibles! 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 conjunto no publicado pero visible de funciones que podiamos descubrir e invocar. Para resolver los problemas. los programadores utilizaban las funcione s ocuttas 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, callHiddenMethod (a, callHiddenMethod (a, "u"); "v"); "w"); static void callHiddenMethod(Object a, String methodName) throws Exception { Method 9 = a. getClass () . getDec!aredMethod (methodName) ; g.setAccessib l e{true ) ; g. invoke (a) ; /* 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 reflexi ó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 objeto 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() "); } public static A makeA () { return ne w C(); } 390 Piensa en Java public class Innerlmplementation public static void main(String[] args) chrows Exception { A a = InnerA.makeA()¡ a. f 1) ; Syscem.out . println(a . getClass(} . getName()) ¡ II La reflexión sigue permitiendo entrar en la clase privada: Hiddenlmplementation . callHiddenMethod (a, "g") ¡ Hiddenlmplementation . callHiddenMethod (a "u " ) ¡ Hidde nlmplementation . callHiddenMethod (a "v") ¡ Hi ddenlmplemen ta t ion . callHi ddenMet hod (a "w") ¡ I I I 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 } }; public class Anonyrnouslmplementation { publi c stat ic void main (String[) args ) throws Ex ception { A a = AnonymousA.makeA () i a. f 11 ; System . ou t . println (a.getClass () .getName ()) ¡ II La reflexión sigue pudi e ndo entrar en la clase anónima : Hiddenlmplementation.caIIHiddenMethod (a, "g" ) ; Hiddenlmplementation. callHi ddenMethod (a , "u" ) ; Hi ddenlmplementation. callHiddenMethod (a, "VII ) ; Hidden l mpleme nt a tion. cal l Hidde nMethod ( a, IIW tl ) ; 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; public class ModifyingPrivateFields { public static void main(String[] argsl throws Exception { WithP r ivateFinalField pf = new WithPrivateFinalField(); System . out . println(pf) i Field f = pf. getClass () . getDeclare d Field (" i " ) ; f . setAccessible(true) ; System . out.println("f . ge t 1nt( pf l: " + f . g et1 nt( pf l); f . set ln t(pf,47}; System . out.println(pf) ; f = p f. getC l ass() .get DeclaredField("s"); f . setAccessible(true) ; System.out . println("f . get(p f } , " + f . get(pf}}; f . se t (pf, "No , you' r e not ! " ) ; Sy st em.out.println(pf ) ; f = pf. getClass () . getDeclaredF ield ( 11 s2 11 1 ; f . s etAccess i bl e (true) ; Sys tem. out.p r int ln ( lIf. g e t( pf ) : 11 + f. g e t( pf )); f . set (pf, "No, you' re not! " ) ; System.out . println(pf ) i / * Outp ut : i = 1, 1 'm totally safe, Am I saf e? f .getInt (pf) , 1 = 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 ! i */// , Sin embargo, los campos de tipo final sí que están protegidos frente a los cambios. El sistema de tiempo de ejecución acepta 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 llamadas 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 intenció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 descubrir 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 necesitamos. En el resto del código podemos entonces detectar ese tipo concreto que hemos añadido y llamar a ese método especial. 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 obligados a sustituir métodos abstractos cuando derivan otra clase a partir de esa clase base. Por ejemplo, considere una jerarquí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 solución más lógica, que en este caso consistiría en incluir un método prepararlnstrumento(). Sin embargo, puede que no veamos 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 apropiadamente el polimorfismo, pero resulta que uno de los objetos reacciona a este código de propósito general de una manera terriblemente 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 decidir 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 mecanismo de reflexión resulta bastante perturbadora. El hecho de que podamos hacer cosas que sólo pueden comprobarse en tiempo 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 perso 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 contenga ningún bloque try ni ninguna especificación de excepción. En lugar de ello, en mi opinión, la existencia de un modelo 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 proporcionará 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 implementen 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 establecer un vínculo entre jerarquías de clases, siempre y cuando tengamos la opc ión de crear una nueva clase para implementar ese vínculo. Algunas veces, incluso una interfaz resulta demasiado restrictiva. Las interfaces siguen requiriendo que nuestro código funcione 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 expresiva 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 alcance tan grande; de hecho, podríamos cuestionamos si el término "genérico" resulta siquiera apropiado para esta funcionalidad 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. 2 O clases que dispongan de un consfructor privado. 394 Piensa en Java 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 manera más efectiva. Comparación con C++ Los diseñadores de Java han dejado claro que buena parte de la inspiración del lenguaje proviene de C++. A pesa r de ello, resulta perfectamente posible enseñar a programar en Java sin hacer apenas referencia a C++. y en este libro hemos intentado hacerlo así, salvo en aquellos casos en los que la comparación puede faci litar entender mejor el lenguaje. Los genéricos requieren que realicemos una comparación más detallada con C++ por dos razones. En primer lugar, comprender ciertos aspectos de las plantillas C++ (la principal inspiración de los genéricos, incluyendo su sintaxis básica) nos pennitirá entender los fundamentos de l concepto, así como (y esto es particularnlente importante) las limitaciones que afectan a lo que se puede hacer con los genéricos de Java, y los motivos subyacentes de la existencia de esas limitaciones. El objetivo último es que el lector comprenda claramente dónde están los límites. porque entendiendo esos límites se puede llegar a ser un programador más eficiel1le. Sabiendo lo que no puede hacerse. podemos emplear mejor aquellas cosas que sí podemos hacer (en parte porque no nos vernos obligados a perder tiempo rompiéndonos la cabeza contra una pared). La segunda razón es que existen muchas concepciones erróneas en la comunidad Java acerca de las plantillas C++, y estos conceptos erróneos pueden aumentar nuestra confusión acerca del objetivo de los genéricos. Por tanto, vamos a introducir unos cuantos ejemplos de plantillas C++ en este capítulo, aunque tratando siempre de limitar al máximo las explicaciones acerca del lenguaje C++. 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 distintas 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 {} public class Holderl private Automobile a¡ public Holderl(Automobile a) {this . a Automobile get {) { return a¡ } a;} ///> Pero esta herramienta no es muy reutili zable, ya que no puede emplearse para almacenar ninguna otra cosa. Preferiríamos no tener que escribir una nueva clase de este estilo para cada tipo con el que nos encontremos. Antes de Java SES , lo que haríamos simplemente es hacer que la clase almacenara un objeto de tipo Object: 11 : generics/Holder2.java public c!ass Holder2 { 15 Genéricos 395 private Object a¡ public public public public Holder2(Object al { this . a = a; } void set(Object al { this.a = a¡ } Object get () { return a¡ } 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 compilador. Por tanto, en lugar de emplear Object, lo que querríamos es poder utilizar un tipo no especificado, lo que podremos decidir en algún momento posterior. Para hacer esto, incluimos un parámetro de tipo entre corchetes angulares después del nombre 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 public class Holder3 private T a¡ public Holder3 (T al public void set(T al public T get(1 { this.a { this.a { return a; = a; } = a¡ } } public static void main(String[] argsl Holder3 h3 = new Holder3{new Automobile()) ¡ Automobile a = h3.get()¡ II No hace falta proyección II h3.set("Not an Automobile") ¡ II Error // h3.set(11; // Error 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 nonnales es en que tiene parámetros de tipo. Pero, como veremos en breve, podemos ut ilizar los genéricos simplemeOle nombrándolos 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 Una biblioteca de tuplas Una de las cosas que a menudo hace falta hacer es devolver múltiples objetos de una llamada a método. La instmcción return sólo pennite especificar un único objeto, por lo que la respuesta consiste en crear otro objeto que almacene los múltiples objetos qu e queramos devo lver. Por supu esto, pode mos escribir un a clase especial cada vez que nos encontremos COn esta situación, pero con los genéricos es posible resolver el problema de tilla vez y ahorrarnos un montón de esfuerzo en el fulUro. Almisl110 tiempo estaremos garantizando la seguridad de los tipos en tiempo de compil ación. Este concepto se denomina tupla, y consiste simplemente en un grupo de objetos que se envuelven juntos dentro de otro obje to úni co. El receptor del objeto estará autorizado a leer los elementos, pero no a introducir otros nuevos (este concepto también se conoce como Objeto de tramlerel1cia de datos o Mensajero). Las tuplas pueden ten er, nom1almente, cualqui er longi tud, y cada objeto de la nlpla puede tener un tipo distinto. Sin embargo, lo que 110S interesa es especificar el tipo de cada objeto y garantiza r que, cuando el receptor lea los va lores, obtenga el tipo correcto. Para tratar con el problema de las múltiples longinldes, podemos crear múltiples tuplas diferentes. He aquí una que almacena dos objetos: // : net/mindview/ util / TwoTuple.java package net.mindview.uti l ; public class TwoTuple public final A first¡ public final B second¡ public TwoTuple (A a, B b ) { first = a¡ second public String toString () { + second + " ) It; return " ( " + first + ) b;} /// ,El cons tmctor captura e l objeto que hay que almacenar y toString() es una función de utilidad que pennite mostrar los valores de ull a lista. Observe que una tupla co nserva implícitamente sus elementos en orden. Al examinar el ejemplo por primera vez, podria pensarse que viola los principios comunes de seguridad en la programación Java. ¿No deberían se r first y second pri vados, y no debería accederse a ellos úni camente con los métodos denominados getFirst( ) y getSecond( )? Considere la seguridad que se obtendría en dicho caso: los clientes podrían seguir leyendo los objetos y hacer lo que quisieran con los mismos, pero no podrían asignar first o second a ninguna otra cosa. La declaración final nos proporciona esa misma seguridad, pero la fonna empleada en el ejemplo es más corta y más simple. Otra observación de di seiio imponante es que puede que queramos permitir a un programador de clientes que haga apuntar a first o seco nd a algún otro objeto. Sin embargo, es más seguro dejar el eje mplo tal cual está, y limitarse a obliga r al usuari o a crear un nuevo objeto TwoTuple si desea di sponer de uno que tenga diferentes elementos. Las tuplas de mayor longitud puede crearSe med iante herencia. Co mo podemos ver, añadir más parámetTos de tipo resulla bastante si mple: // : net / mindview / util/ThreeTuple.java package net.mindview.util¡ public class ThreeTuple extends TwoTuple { public final C third¡ publie ThreeTuple lA a, B b, e e l { super (a, b ) ; third = c¡ public String toString () return It ( " + first + { " + second + } /// , / / : net / mindview / util / FourTuple.java package net.mindview.util¡ " + third +" ) It ¡ 15 Ge néricos 397 public class FourTuple extends ThreeTuple public final D fourth¡ public FourTuple(A a, B b , e e, D d) super (a, b, el j fourth = d; public String toString () { + second + return "(" + first + ", third + 11 I 11 + fourth + ") 11 i " + } ;;;,jI: net/mindview/util/FiveTuple.java package net.mindview.util; public class FiveTuple extends FourTuple { public f inal E fifth; public FiveTuple(A a, super(a, b , e, d); fi f th = e¡ B b, public String toSt r ing () e E el { { return ,,(" + first + ", third + ", e, D d, " + fourth + + second + 11 I " ", + fifth + 11 + ") " j } ;;;,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 {} public class TupleTest static TwoTuple f() /1 El mecanismo de autoboxing convierte int en Integer: return new TwoTuple ("hi", 4 7) ; static ThreeTuple 9 () { return new ThreeTuple( new Amphibian(), "hi " , 47); static FourTuple h() return new FourTuple( new Vehicle(), new Amphibian (}, "h i n, 47) i static FiveTuple k() return new FiveTuple ( new Vehicle(), new Amphibian(), "hi " , 47, 11 . 1) ¡ public static void main(String[] args} TwoTuple t t si ~ f(); 398 Piensa en Java System.out.println(ttsi) i JI ttsi.first = "there"¡ // Error de compilación: System.out.println(g()) ; System.out.println(h()) ; System.out.println{k()) i final / * Output : (80% match) (hi, 47 ) (Amphibian@l f6a7b9, hi, 47 ) (Vehicle@3Sce36, Amphibian@757aef, hi, 47 ) (Veh icle@9ca b16, Amphibian@la46e30, hi, 47, 11.1 ) * ///,Gracias a los genéricos, podemos crear fácilmente cualquier tupla para devolver cualquier grupo de tipos, simplemente escribiendo la expresión correspondiente. Podemos ver cómo la especificación final en los campos públicos impide que sean reasignados después de la construcción. puede observarlo viendo cómo falla la instrucción ttsi.first = " there". Las expresiones new son demasiado complejas. Posterionnente en el capítulo veremos cómo simplificarlas utilizando méto~ dos genéricos. Ejercicio 3 : (1 ) Cree y pruebe un genéri co Si,Tuple con seis elementos. Ejercicio 4 : (3) Reescriba innerclasses/Sequence.java utili zando genéricos. Una clase que implementa una pila Exami nemos algo ligeramente más complicado: la típica estructura de pila. En el Capítulo 11 , Almacenamiento de objelos, vi mos cómo implementar una pila utili zando un contenedor LinkedList en la clase net.mi ndview. utiI.Stack. En dicho ejemplo, podemos ver que LinkedList ya dispone de los métodos necesarios para crear una pila. La clase Stack que implementaba la pila se construyó componiendo una clase genérica (Stack { private static cIass Node U item¡ Node next¡ Node () { item = null ¡ next Node (U item, Node next ) this.item item; this.next = next ¡ boolean end () nuH; { return i tem "'''' null } && next == null ¡ } private Node top = new Node () ¡ II Indicador de fin public void push(T item) { top '" new Node (item, top) ; } pUbli c T pop () ( T result = top.item¡ if ( !top.end ()) top = top. next i return resul ti 15 Genéricos 399 public static void main (String[] args ) LinkedStack lss ~ new LinkedStack () ; for (String s : "Phasers on stun!".split ( U It } ) lss.push (s ) i String Si while { {s = lss.pop {) ) != null ) System . out.println (s ) ; / * 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 y se enlaza con el objeto Node anterior. Cuando se invoca a pop(). siempre se devuelve top.item, y luego se descarta el objeto Node 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 aleatoriamente 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.*; public class RandomList prívate ArrayList storage = new ArrayList(); private Random rand = new Random(47); public void add(T item) { storage.add(item); } public T select () { return storage.get(rand .nextlnt (storage .size ())); public static void main (String [] args) { RandornList rs = new RandomList() ; tor (String s: ("The quick brown fox jumped over " + "the 1azy brown dog " ) . sp1it(rt " )) rs.add(s) ; for{int i = O; i < 11; i++ ) System. out . print (rs. select () + 11 "); / * 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áctica, 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 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 diferente 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; public class Coffee { private static long counter ~ O; private final long id = counter++¡ public String toString () { return getClass () . getSimpleName () + " " + id¡ } ///,- 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 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.*¡ public class CoffeeGenerator implements Generator, Iterable { private Class[] types = { Latte.class, Mocha.class, Cappuccino. class, Americano. class, Breve. class, }; private static Random rand = new Random(47); public CoffeeGenerator () {} II Para iteración: 15 Genéricos 401 private int size = o; sz; ) public CoffeeGenerator (int sz) { size public Coffee next () { try ( return (Coffee) types (rand. next lnt (t-ypes .length) ] . newlnstance () ; // Informar de errores del programador en tiempo de ejecución: catch(Exception el { throw new RuntimeException(el i class Coffeelterator implements Iterator int count = size; public boo!ean hasNext () { return count :> Oi } public Coffee next () { count--¡ return CoffeeGenerator . this.next() i public void remove() { JI No implementado throw new UnsupportedOperationException(); ) ); public Iterator iterator() return new Coffeelterator(); public static void main(String[] args) { CoffeeGenerator gen = new CoffeeGenerator() for(int i = O; i < Si i++l System.out . println{gen.next() l; for(Coffee e : new CoffeeGenerator(S)) System.out.println{c) ; i 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, 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.*; public class Fibonacci implements Generator private int count = O; public Integer next() { return fib(count++); } pri vate int f ib (int n ) { if(n < 2) return 1; return fib(n-2) + fib (n-1); 402 Piensa en Java p u b lic stat i c v o id main (String [ ] args l Fibonacci gen = new Fibonacci () ; f o r {int i :: O; i < 18; i++ l System. o ut.print (gen.next () + " 11 ) ; / * 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 envoltorio, para poder efectuar las conversiones fácilmente. Podemos ver el efecto en este ejemplo porque los va lores in t se utili 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.*¡ public class IterableFibonacci extends Fibonacci implements Iterable private int n¡ public IterableFibonacci ( int count l n = c ount¡ public Iterator iterator( ) return new Iterator () { public boolean hasNext () { return n > Oi } public Integer next ( ) ( n--; return IterableFibonacci.this . next ( ); public v o id remo ve () { li No implementado throw new UnsupportedOperationException () i } }; public static void main (String [] args ) { for ( int i : new IterableFibonacci(18 } ) System.out.print ( i + 11 11 ) ; 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étodos 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 public class GenericMethods public void f (T x ) { System. out. println (x . getClass () . getName () ) ; public statie void main(String[] args) { GenericMethods gm = new GenericMethods() gm.f (" 10 ); i gm. f ( 1 ) ; gm.f(l.O) ; gm.f(l.OF) ; gm.f ( 'e' ) i gm.f(gm) ; } / * 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ámetros 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 detenninar 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, envolviendo de manera transparente los tipos primitivos en sus objetos asociados. De hecho, los métodos genéricos y el mecanismo 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 Aprovechamiento de la inferencia del argumento de tipo Una de las quejas acerca de los genéricos es que aiiaden todavía más texto a nuestro código. Considere el programa holdinglMapOfLiSl.java del Ca pitulo 11 , Almacenamiento de objetos. La creación del contenedor Map de List tiene el aspecto sigui ente: Map Map map () return new HashMap{); { pub1ic static List 1ist() return new ArrayList{); public static LinkedList lList () return new LinkedList () ¡ { public static Set set () return new HashSet(); public static Queue queue () return new LinkedList () i II Ejemplos, public static veid main (String [] args) { Map ls = New.list(); LinkedList lIs = New.lList( ) i Set ss = New.set() ¡ Queue qs = New.queue( ); i } 111 ,En main() podemos ver ejemplos de cómo se emplea esta herramienta: la inferencia del argumento de lipa elimina la necesidad 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.*; pub1ic class Simp1erPets { public static void main (String [] args) { Map List makeList(T ... argsl 406 Piensa en Java List result = new ArrayList(); for(T item : args) result.add(item} ¡ return resul t ¡ public static void main(String[] args) List ls = makeList("A"); System.out.println(ls) ; ls = makeList ( "A", "B", "e" ) ; System.out.println(ls) ; ls = makeList ("ABeDEFFH1JKLMNOPQRSTUVWXYZ" . split (" ") ) ; System.out.println (ls) ; / * 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 ioteca estándar. Un método genérico para utilizar con generadores Resulta bastante cómodo utili zar genérica esta operación: Wl generador para rellenar un objeto COllection , y también tiene bastante sentido hacer 11: generics/Generators.java 11 Una utilidad para utilizar con generadores. import generics.coffee.*; import java.util.*; import net.mindview . util.*¡ public cIass Generators { public sta tic eollection fill (eolleetion eoll, Generator gen, int n) for(int i = O; i < n; i++} coll.add(gen.next(» ; return eoll; { public static void main(String[] args) { eollection eoffee = fill ( new ArrayList (), new eoffeeGenerator(), 4); for(eoffee e : coffee) System.out . println(c) ; eollection fnumbers = fill( new ArrayList() new Fibonacci{) 12); for (int i : fnumbers) System.out.print(i + ", ") ¡ I I / * 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 especificos de Collection : List, Queue y Set. De esta fonna, no pe rdemos el tipo de contenedor. ¿Podemos utiliza r el mecanismo de sobreca rga para distinguir entre List y LinkedList? Un generador de propósito general He aquí una clase que produce un obj eto Generator para cualquier clase que disponga de un constructor predetemli nado. Para reduc ir la cant idad de tex to tecleado, también inc luye un método genérico pa ra crea r un objeto BasicGenerator: /1 : net / mindview/ util / BasicGenerator.java JI Crear automáticamente un generador, dada una clase con un / / constructor predeterminado (sin argumentos ) . package net . mindview.util¡ public class BasicGenerator implements Generator { private Class type¡ public BasicGenerator(Class type) { this.type ~ type¡ public T next ( ) { try { / / Asume que el tipo es una clase pública: return type.newlnstance(); catch(Exception el { t hrow new RuntimeException(e l ¡ // Pr oducir un generador predeterminado dado un i ndicador de tipo: public static Generator create(Class t ype) { return new BasicGenerator (type) ; } /// , Esta clase proporciona una implementación básica que producirá objetos de una clase que ( 1) sea pública (ya que BasicGenerator está en un paquete separado, la clase en cuestión debe tener acceso público y no simplemente de paquete) y (2) tenga un constructor predetenninado (uno que no torne ningún argumento). Para crear uno de estos objetos BasicGenerator, invocamos el método create( ) y pasamos el indicador de tipo para el tipo que queramos generar. El método genéri co create() pennite escribir BasicGenerator.create(MyType.class) en lugar de la instrucción new BasicGenerator(MyType.c\ass) que es más complicada. Por ejemplo, he aqu í una clase simple que dispone de un constructor predetenninado: // : generics / CountedObject.java public class CountedObject private static long counter = O; private fi nal l ong id = counter++¡ public long id ( ) { return id; } public String toString( ) { return "CountedObject 11 + id¡} /// , La clase CountedObject lleva la cuenta de cuántas instancias de sí misma se han creado e informa de la cantidad total mediante toString( ). Utili zando BasicGenerator, podemos crear fácilm ente un objeto Generator para CountedObject: / / : ge ne rics/ BasicGeneratorDemo . java import ne t.mindview . util .* ; public class BasicGeneratorDemo public static void main(String[] args) { Generator gen = BasicGenerator.create (CountedObject . class ) i 408 Piensa en Java for ( int i ;;; Oi i < Si i++ ) System.out.println {gen.next ()) ; / * Output: CountedObj ect O CountedObj ect 1 CountedObject 2 CountedObject 3 CountedObject 4 */// 0Podemos 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. Simplificación del uso de las tuplas El mecanismo de inferencia de l argum ento de tipo, junto con las importaciones de tipo static, nos pennite reescribir las tuplas que hemos presentado anterionnente, para obtener una biblioteca de propós ito más general. Aquí, las tuplas pueden crearse utilizando un método estático sobrecargado : 11: net/mindview/uti l /Tuple . java II Biblioteca de tuplas utilizando el mecanismo de I I inferencia del argumento de tipo. package net . mindview.util¡ publie elass Tuple { publie statie TwoTuple tuple {A a, B b ) { return new TwoTuple {a, b)¡ publie static ThreeTuple tuple lA a, B b, e e l { return new ThreeTuple {a, b, e l i publie static FourTuple tuple lA a, B b, e e, D d i ( return new FourTuple (a, b, e, d ) ¡ publie static FiveTuple tuple (A a, B b, C e, O d, E e l return new FiveTuple (a, b, e, d, e l ¡ ) /// 0He aquí una modificación de TupleTest.java para probar Tuple.java: 11: generies/TupleTest2.java import net . mindview.util .* ¡ import statie net . mindview.util.Tuple.*¡ public class TupleTest2 { static TwoTuple f () return tuple(Uhi U, 47 l ; { statie TwoTuple f2 () { return tuple ( lIhi" 47 ) ¡ } static ThreeTuple 9 () { return tuple (new Amphibian () , "hi", 47 ) i I I 15 Genéricos 409 statie FourTuple h() return tuple{new Vehicle () , new Amphibian(), tlhi" , 47) statie Fi veTuple k () return tuple(new Vehicle(), new Amphibian{), tlhí" , 47, i { 11.1); public statie void main(String(] args) TwoTuple ttsí : f() ; System . out . println(ttsi) ; System.out.println(f2()) ; System.out.println(g()) ; System . out.println(h()) ; System.out.println(k{)) ; / * Output: (h i, 47) (h i, 47) (80% match) (Amphi bian@7d772e, hi, 47) (Vehicle@757aef, Amph ibian@d9f9c3, hi, 47) (Vehicle@la46e30, Amphibian@3e25aS , hi, 47, 11 . 1) * /// ,Observe que f() devuelve un objeto parametrizado 1\voTuple, mientras que 1'2( ) devuelve un objeto TwoTuple no paramelri zado. El compi lador no proporciona ninguna advertencia acerca de n() en este caso porque el valo r de retomo no está siendo utilizado de fanna parametrizada; en un cierto se ntido, está siendo "generalizado" a un objeto TwoTuple no parametrizado. Sin embargo, si quisiéramos calcular el resultado de f2( ) en un objeto parametrizado TwoTuple. el compilador generaría una advertencia. Ejercicio 15: (1) Verifique la afirmación anterior. Ejercicio 16: (2) Añada una tupla SixTuple a Tuple.java y pruébela mediante TupleTest2,java. Una utilidad Set Vamos a ver otro ejemplo del uso de método genérico. Considere las relac iones matemáticas que pueden expresarse utilizando conjuntos. Estos conjuntos pueden definirse de fom18 cómoda como método genérico, para utili zarlos co n todos los diferentes tipos: ji : net / mindview/util/S ets.java package net.mindview . util¡ import java .util.*¡ public class Sets { public static Set union (Set a, Set result : new HashSet(a) ¡ result . addAll(b) ; return result¡ Set b) public static Set intersection(Set a, Set b ) Set result = new HashSet (a) ; result .retainAll(b) ; return result; II Restar subconjunto de un superconjunto: public static Set difference(Set superset, Set subset) { 410 Piensa en Java Set result = new HashSet (superset) ; result.removeAll(subset) ; return result; 11 Reflexivo--todo lo que no esté en la intersección: public static Set complement(Set a, Set b) return difference{union(a, b), intersection(a, b)); 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; public enum Watercolors { ZINC, LEMON_YELLOW, MEDIUM_YELLOW, DEEP YELLOW, ORANGE, BRILLIANT_RED, CRIMSON, MAGENTA, ROS E_MADDER, VIOLET, CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER, BURNT_UMBER, PAYNES_GRAY, IVORY_BLACK ///,Por comodidad (para no tener que cualificar todos los nombres) importamos esta enumeración estáticamente en el ejemplo siguiente. Este ejemplo utili za EnumSet, que es una herramienta de Java SES que pemlite crear conjuntos fác ilmente a partir de enumeraciones (aprenderemos más de EnumSet en el Capítulo 19, n pos enumerados). Aquí, al método estático EnumSct.rangc( ) se le pasan el primer y el último elemento del rango que hay que utilizar para crear el objeto Set resultante: 11: generics/WatercolorSets.java import import import import import generics.watercolors.*; java.util. * ; static net.mindview.util.Print.*; static net.mindview.util.Sets.*; static generics.watercolors.Watercolors. * ; public class WatercolorSets { public static void main (String [] args) { Set setl = EnumSet. range (BRILLIANT_RED, VIRIDIAN_HUE) i Set set2 : EnumSet. range (CERULEAN_BLUE_HUE, BURNT_UMBER); print("setl: " + setl) i print (ti set2: ti + set2); print{"union(setl, set2): " + union(setl, set2»; Set subset = intersection(setl, set2); print ( 01 intersection (setl, set2): + subset) ; print{ "difference{setl, subset): + diffe r ence(setl, subset); print ( "differe nc e (set2, subset) : + difference(se t 2, subset); print ( "complement (se t l, set2): " + 15 Genéricos 411 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, BRILLIANT_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.·; public class ContainerMechodDifferences statie Set methodSet(Class type) Set result = new TreeSec(); for(Method m : type.getMechods(» resulc.add(m.getName( ) i return resul t; statie void interfaces{Class typel System. out. print (It Interfaces in " + type.getSimpleName() ... ": "); List result = new ArrayList(); for(Class e type.getlnterfaces(» result.add(c.getSimpleName(» i System.out.println(result) ; statie Set object = methodSet(Object.class); statie { object.add{lIclone U } ; } static void difference(Class supersec, Class subset) System . out.print(superset .ge tSimpleName() + ti extends " + subset . getSimpleName () + ", adds: " ) ; Set comp = Sets.difference( methodSet(superset), methodSet(subset»; comp.removeAll(object ) ; II No mostrar métodos tObject t System.out.println(comp ) ; interfaces (superset ) ; public static void main(String[] args) System. out. println ( "Collection: " + methodSet(Collection.class» i interfaces(Collection.class ) ; difference(Set.class, Collection.class); difference(HashSet.class, Set.class); 412 Piensa en Java 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? Clases internas anónimas Los genéricos también pueden utilizarse con las clases internas y con las clases internas anónimas. He aquí un ejemplo que implementa la interfaz Generator uti li zando clases internas anónimas: JJ: genericsJBankTeller.java JI Una simulación muy simple de un cajero automático. import java.util.*; import net.mindview.util.*¡ 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 generator() return new Generator(} ( 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 generator new Generator () { public Teller next () { return new Teller () ¡ }; public class BankTeller { } 15 Genéricos 413 public static void serve(Teller t, Customer el System.out.println(t + " serves + el i 11 public static void main(String[] args) Random rand = new Random(47)¡ Queue(); Generators.fill(line, Cuscomer.generator(), 15); List tellers = new ArrayList(); Generators.fill(tellers, Teller.generator, 4) ; for{Customer e , line) serve {tellers .get (rand.nex tlnt (tellers .size ()}) , el i / ' Output : Teller 3 serves Customer Teller 2 serves Customer Teller 3 serves Customer Telle r Teller Teller Teller Tel ler Teller Teller Teller Teller Teller Teller Teller 1 2 3 1 serves Customer 4 1 serves Customer 5 3 serves Customer 6 1 serves Customer 7 2 serves Customer 8 3 serves Customer 9 3 serves Customer 10 2 serves Customer 11 4 serves Customer 12 2 serves Customer 13 1 serves Customer 14 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 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 formar pal1e de una interfaz, así que no hay fonDa de hacer genérica esta fu nción concreta. A pesar de ello func iona razonablemente 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). Construcción de modelos complejos Una ventaja importante de los genericos es la capacidad de crear modelos com plejos de forma simple y segura. Por ejemplo. podemos crear fáci lmente una lista de tuplas: 11: generics/TupleList.java 11 Combinación de tipos genéricos para crear tipos genéricos complejos. import java.util.*¡ import net.mindview.util.*¡ public class TupleList extends ArrayList tI = new TupleList() ¡ tl.addITupleTest.h()) ; 414 Piensa en Java tl.add ITupleTest.h l)) ; for {FourTuple i: System . o ut.printl n{ i ) ; tI ) 1* Output: (75% match ) (Vehicle@11b86e7, Amphibian@35ce36, hi, 47 ) (Vehicle@757aef, Amphibian@d9f9c3, hi, 47 ) * /// ,Aunque hace falta bastante texto (especialmente en la creación del iterador). obtenemos una estnlctura de datos bastante potente sin necesidad de utilizar demasiado código. He aquí otro ejemplo donde se muestra lo fácil que es crear modelos complejos utilizando tipos genéricos. Cada clase se crea como un bloque componente y la solución total tiene múltiples partes. En este caso, el modelo corresponde a un comercio de venta al por menor con pasillos, estanterías y productos: / 1 : generics/Store.java JI Construcción de un modelo complejo utilizando contenedores genéricos. import java . util.*; import net.mindview.util.*¡ class Product { private final int id; private String description¡ private double price; public Product{int IDnumber, String descr, id = IDnumber¡ description = descr i this.price = price; System.out.println(toString()) ; double price) ( public String toString{) return id + ti: " + description + ", price: $" + price ¡ public void priceChange(double change) price += change¡ public static Generator generator new Generator () { private Random rand = new Random(47); public Product next () ( return new Product(rand.nextlnt{lOOO), "Test", Math.round(rand.nextDouble{) * 1000.0) + 0.99); } }; class Shelf extends ArrayList { public Shelf (int nProducts) { Generators.fill(this, Product.generator, class Aisle extends ArrayList { public Aisle(int nShelves, int nProducts) for(int i = O; i < nShelves; i++) add(new Shelf(nProducts)); class CheckoutStand {} nProducts); 15 Genéricos 415 class Off ice {} public class Store extends ArrayList { private ArrayList checkouts new ArrayList(); private Off ice of f ice = new Office(); public Store(int nAisles, i n t nShelves, int nProductsl far(int i = O; i < nAis!es¡ i+ + ) add(new Aisle (nShelves, nProducts)) i pub l ic Stri ng toStr i ng() Stri ngBuilder result fo r (Aisl e a : this) n ew Stri ngBui l de r() ; t or {S he l f s : al f or IProdu c t p , si { r e s u l t.append (p ) i r es ul t .append ( "\ n" ) ; return result.t o String {) i publ i c s t at i c vo id main (String [] args ) { System.out.println (new Store (14, 5, 1 0)) i / * Output: 25 8, 861 , 868 , 207 , 55 1, 27 8, 520 , 140 , Test, Test, Test, Test, Test, Test, Test, Test, price: price: price: pr i ce: price: price: priee: priee: $400.99 $160 . 99 $417.99 $268.99 $114.99 $804 . 99 $554.99 $530 . 99 * /// ,Como puede ver en Store.toString(), el resultado son muchos niveles de contenedores que, a pesar de la complejidad, resultan manejables y son seguros en lo que al tratamiento de tipos se refiere. Lo más impresionante es que no resulta demasiado 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 contenedores metálicos para las mercancías. El misterio del borrado A medida que nos sumergimos más profundamente en los genéricos, aparecen una serie de aspectos que parecen no tener sentido a primera vista. Por ejemplo, aunque podemos escribir ArrayList.class, no podemos escribir Array List.class. Considere también lo siguiente: // : generics / ErasedTypeEquivalenee.java import java.util. * ¡ public elass ErasedTypeEquivalenee publie statie void main ( String[] args ) Class el = new ArrayList ( ) . getClass () ; Class e2 = new ArrayList () .getClass () ; System.out.println (el == e2 ) ¡ / * Output: t rue * /1/ ,- 416 Piensa en Java ArrayList 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, obtendremos un comportamiento diferente (la operación falla) que si tratamos de almacenar ese objero Intcger en un contenedor ArrayList (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 cIass cIass cIass Frob {} Fnorkle {} Quark {} ParticIe {} pubIic cIass Lostlnformation { public static void main(String[] args) { List Iist = new ArrayList (); Map map = new HashMap{); Quark quark = new Quark{); ParticIe p = new Particle{); System.out.println(Arrays.toString( list.getClass() .getTypeParameters{))); System . out . println(Arrays.toString( map. getClass () . getTypeParameters () ) ) ; System.out.println(Arrays.toString( quark. getClass () . getTypeParameters () ) } ; System.out.println(Arrays . toString( p.getClass() .getTypeParameters( »); } / * Output, [E] [K, [O] V] [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 capaces de averiguar cuáles son los tipos de parámetro. Sin embargo, como podemos ver analizando la salida, lo único que podemos 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 usando un objeto. Por tanto, List y List 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 La técnica usada en e++ He aquí un ejemplo e++ que utiliza plamillas. Observará que la sintaxis para los tipos parametri zados es bastante similar, ya que Ja va se ha inspirado precisamen te e n e++: ji : generics / Templates . cpp #include using namespace std; template class Manipulator { T obj ; public: Manipula t or (T x l ( obj = x; ) void manipulate (1 { obj . f (1 ; ) ); class HasF public : void f () caut « tlHasF;: f ( ) " « endl; } ); int main () { HasF h f ; Manipulator 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 comprobación cuando instanciamos la plantilla, por lo que el punto de instantac ión de M anipulator 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 seguridad 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 conoce 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 public class HasF { public void f {) { System.out.println ( "HasF.f( ) " } ¡ } ) ((( ,- Si tomamos el resto de l ejempl o y lo traducimos a Java no se podrá co mpi lar: ji : generics / Manipulation . java // {Compi leTimeError} (Won' t compile ) cl ass Manipulator { private T obj¡ public Manipulator(T x ) { obj = Xi } / / Error : no se puede encontrar el símbolo: método f () : public void manipulate (1 ( obj. f ( ) ; ) public class Manipulation { public static void main(String[) args } HasF hf = ne w HasF()¡ Manipulator manipulator new Manipulator (hf ) i 418 Piensa en 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 class Manipulator2 pri vate T obj i public Manipulator2(T x ) obj = x; ) public void manipulate() obj . f (); ) ///,El limite dice que T debe ser de tipo HasF' o algo derivado de HasF'. Si es asi, entonces resulta seguro invocar f( l sobre obj . Decimos. a este respecto, que el parámetro de tipo genérico se borra de acuerdo con su primer límite (es posible tener múl· tipl es limites, como veremos posteriomlente). También hablamos en relación con esto, del borrado del parámetro de lipo . El compi lador, en la práctica, sustituye al parámetro de tipo por lo que el límite indique, de modo que en el caso anterior T se borra y se sustituye por HasF, lo cual es lo mismo que sustituir T por HasF en el cuerpo de la clase. El lector podría pensar, correctamente, que en Manipulation2.java , los genéricos no proporcionan ninguna ventaja. Podríamos perfectamente realizar el borrado de tipos nosotros y crear una clase sin genéricos: 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 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 class ReturnGenericType priva te T obj; public ReturnGenericType (T x l { obj = x; public T get () ( return obj; ) /// ,Es preciso examinar todo el código y detenninar si es lo suficientemente complejo como para merecer el uso de genéricos. Examinaremos el tema de los límites con más detalle más adelante en el capítulo. Ejercicio 20: (1) Cree una interfaz con dos métodos y una clase que implemente dicha interfaz y añada un tercer método. En otra clase, cree un método genérico con un tipo de argumento que esté limitado por la interfaz y demuestre que los métodos de la interfaz son invocables dentro de este método genérico. En main( l , pase una instancia de la clase implementadora al método genérico. 15 Genéricos 419 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 mecanismo 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 pueden 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 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, existiria un punto de partida en el que lodo hubiera sido hecho genérico a la vez. En la realidad, incluso aunque los programadores 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 existentes siguen siendo legales y continúan significando lo que antes significaban) sino que también tienen que soportar la compatibilidad 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 trabajando en el problema, decidi ero n que el borrado de tipos era la única solución fac tible. El mecanismo de borrado de tipos permite 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 detectar 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 riesgo 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 problema del borrado de tipos Por tanto, la justificación principal para el borrado de tipos es el proceso de transición de código no genérico a código genérico, y la incorporación de genéricos dentro del lenguaje sin hacer que dejen de funcionar las bibliotecas existen tes. El borrado de tipos permite que el código de cliente existente, no genérico, continúe pudiendo ser usado sin modificación, hasta que los clientes estén listos para reescribir el código de cara a utili zar genéricos. Se trata de una mot ivac ión muy noble, porque no hace que de repente deje de funcionar todo el código existente. 420 Piensa en Java 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 var; pod ría parecer que al crear una instancia de Foo : Foo f = new Foo(); 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 { prívate T element; public void set (T arg) { arg = element; publ ic T get () { return element; } class Derivedl extends GenericBase {} class Derived2 extends GenericBase {} 11 11 Ninguna advertencia class Derived3 extends GenericBase {} // Extraño error: unexpected type found : ? // required: class or interface without bounds // public class ErasureAndlnheritance @SuppressWarnings ("unchecked" ) public static void main(String[] args) Derived2 d2 = new Derived2{); Object obj " d2.get(); d2.set(obj); 11 ¡Advertencia aqui! } ///,- Derived2 hereda de GenericBase si n ningún parámetro genérico y el compilador no genera ninguna advertencia. La advertencia no se genera hasta que se invoca set(). Para qüe no aparezca la advertencia, Java proporciona una anotación, que es la que podemos ver en el listado (esta anotación no estaba soportada en las versiones anteriores a Ja va SE5): @SuppressWarni ngs (lunchecked") 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. El efecto de los límites Debido al mecan ismo de borrado de tipos, el aspecto más confuso de los genéricos es el hecho de que podemos representar cosas que no tienen ningún significado. Por ejemplo: 1/ : generics/ArrayMaker.java import java.lang.reflect.*¡ import java.util.*; public class ArrayMaker private Class kind¡ public ArrayMaker (Class kind) { this. kind @SuppressWarnings ("unchecked") T [] create (int size) { return (T[] )Array.newlnstance(kind, size) ¡ kind; } public static void main(String[] argsl ArrayMaker stringMaker : new ArrayMaker (String .class ) i String(] stringArray = stringMaker.create(9) ¡ System.out.println(Arrays.toString(stringArray) i 1* Output: [nu ll, null, null, null, null, null, nul1, null, null] . /// ,Aunque kind se almacena corno Class, el mecanismo de borrado de tipos significa que en realidad se está almacenando 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.*¡ public class ListMaker List crea te () { return new ArrayList (); } public static void main (String [] args) ( ListMaker stringMaker= new ListMaker(); List stringList = stringMaker.create{); El compi lador 110 genera ningun a advert encia, aún cuando sabemos perfec tamente (debido al mecanismo de borrado de tipos) que la en ncw ArrayList() dentro de create() se elimina: en tiempo de ejecución no hay ninguna dentro de la clase, por lo que parece que no tiene ningún significado. Pero si hacemos caso de esta idea y cambiamos la expresió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. *; public class FilledListMaker List create (T t, int n ) { List result = new ArrayList () for (int i = o; i < n; i++ ) result.add (t) ; return result; i public static void main (String(] args ) FilledListMaker stringMaker = new FilledListMaker{); List list = stringMaker.create {"Hello", 4) System.out.println(list) ; i 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 tiempo de compilación) que lo que pongamos dentro de result es de tipo T , de modo que concuerde con ArrayList. 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 public class SimpleHolder { private Object obj¡ public void set(Object obj) { this.obj obj; public Obj ect get () { return obj; } public static void main(String[] args) SimpleHolder holder = new SimpleHolder(); holder.set("Item" ) ; String s = (String)holder.get(); } Si descompilamos el resultado con javap -c SimpleHolder, obtenemos (después de editar la salida): public void set{java.lang.Objectl i o: aload O 1: aload 1 2, putfield #2; l/Campo obj ,Object; 5: return public java .lang. Object get () i O: aload_O getfield #2; l/ Campo obj ,Object; areturn public static void main{java.lang.String[]); 0, new #3; l/Clase SimpleHolder 3, dup 4, invokespecial #4¡ II Método "": () V 15 Genéricos 423 7: 8: 9, 11: 14: 15: 18: 21: 22: astare 1 aload 1 ldc #5; I/ String Item invokevirtual #6; JI Método set: {Object;)V aload 1 invokevirtual #7; // Método get: () Object¡ checkcast #8; j jclass java/ langjString astore_2 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 public class GenericHolder { private T obj; public void set (T obj) { this. obj = obj; public T get () { return obj; } public static void main (String [] args) { GenericHolder holder = new GenericHolder() i holder. set (" Item") i String s = holder.get ( ); 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: public veid set (java.lang.Object l O; alead O 1; alead 1 i 2, putfield #2; l/ Campo obj,Objeet; 5; return public java.lang.Object get () ; O: alead O getfield #2; l/ Campo obj ,Object; areturn public static veid main(java.lang.String[]) 0, new #3; l/ Clase GenericHolder 3, 4, 7, B, 9, 11, 14, 15, lB, 2l, 22, i dup invokespecial #4; l/Método "cinit>": ()V astore 1 aload 1 lde #5; I/ String Item invokevirtual #6; // Método set: (Object; ) V aload 1 invokevirtual #7; // Método get: ()Object; checkcast #8; //class java/lang/String astore_ 2 return El código resultante es idéntico. El trabajo adicional de comprobar el tipo entrante en set( ) es nulo, ya que es el compilador 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ímites: en concreto, se tra ta de la comprobación adicional en tiempo de compilación para los valores entrantes y de la proyecció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". Compensación del borrado de tipos Como hemos visto, el borrado de tipos hace que perdamos la posibilidad de realjzar ciertas operaciones en el códi go genéri co. En concreto, no funcionará ninguna cosa que requiera conocer el tipo exacto en tiempo de ejecución: 11 : generics / Erased.java II {CompileTimeError} (no se compilará ) public class Erased { private final int SIZE ~ 100; public stat i c void f (Object arg ) if (arg instanceof T ) {} T var = new T () ; T(] array new T (SIZE) i T () array = (T ) new Obj ect [SIZE ) II Error /1 i Erro r I I Error 1/ Advertencia de no comprobación } 111 ,Ocasionalmente, podemos solventar mediante programa estos problemas, pero en ocasiones nos vemos forzados a compensar el mecanismo de borrado de tipos introduciendo lo que se denomina un marcador de tipos. Esto quiere decir que pasamos explícitamente el objeto Class correspondiente a nuestro tipo para poder utilizarlo en expresiones donde los tipos entran en Juego. Por ejemplo, el intento de utilizar instanceof en el programa anterior falla porque la información de tipos ha sido borrada. Si introducimos un marcador de tipos, podemos utili za r en su lugar un método islnstance() dinámico: 11 : generics / classTypeCapture . java class Building {} class House extends Building {} public class Cl assTypeCapture Class kind¡ pub l ic ClassTypeCapture (Class kind ) this . kind = kind¡ { p u blic boolean f (Obje c t a r g ) r et u rn kind. islns t ance (arg ) ; public static void main {String(] args) ClassTypeCapture ctt1 = new ClassTypeCapture (Building.class ) System.out . println (ctt1.f (new Building ())) i System.out . println (ctt1.f {new House (») ); ClassTypeCapture ctt2 = new ClassTypeCapture(House.class ) ¡ System.out . println (ctt2.f (new Building ( » )) ¡ System.out . println (ctt2 . f (new Hous e ( » )) ; 1* Output : true true false true *111 ,- i 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 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. Creación de instancias de tipos El intento de crear un objeto con new T( ) en Erased.j ava no fu nciona, en parte debido al mecanismo de borrado de tipos y en parte po rque el compilador no puede verificar que T tenga un constructor predeterminado (sin argumentos). Pero en e H esta operación es natural, sencilla y segura (se compmeba en tiempo de compilación): ji : generics!InstantiaceGenericType.cpp ¡C+ +, no Java! // template class Foo T X i JI Crear un campo de tipo T T* y¡ / / Puntero a T public : // Inicializar e l puntero : Foo l) ( y = new T I) ; J J; class Bar (J; in t main () Foo tb¡ Foo 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 x¡ public ClassAsFactory (Class 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 fe = n ew ClassAsFactory (Employee . class ) ; p r int ( "ClassAsFactory succeeded" ) ¡ try ( ClassAsFactory ti = n ew ClassAsFactory (Integer.class ) ¡ 426 Piensa en Java catch ( Exception e) ( print(UClassAsFactory failed" ) ; J* Output: ClassAsFactory succeeded ClassAsFactory failed * /// ,Este ejemplo se puede compilar, pero falla si utilizamos ClassAsFactory 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 admita una clase que implemente dicha factoría: JJ : generics/FactoryConstraint.java interface FactoryI T create () ¡ class Foo2 { private T x¡ public { public Integer create () { return new Integer {O) i class Widget { public static class Factory implements FactoryI { public Widget create () return new Widget( ) ¡ public class FactoryConstraint { public static void main (St ring[] args ) new Foo2 (new IntegerFactory ()) ¡ new Foo2 (new Widget.Factory ()) ¡ } /// ,Observe que esto no es más que una va riación del hecho de pasar C1ass. Ambas técnicas pasan objetos factoría; pero Class resulta ser el objeto factoría predefinido, mientras que en el ejemplo anterior creamos un objeto factoría explícito. Lo importante es que conseguimos que se realice una comprobación en tiempo de compilación. Otra técnica consiste en utilizar el patrón de diseño basado en plantillas. En el siguiente ejemplo, get( ) es el método plantilla y create( ) se define en la subclase para generar un objeto de dicho tipo: JJ: generics/CreatorGeneric.java abstract class GenericWithCreate final T element¡ GenericWi thCreate () { element = create () ; 15 Genéricos 427 abstract T create() i class X {} class Creator extends GenericWithCreate X create() { return new XI); } void f () { System.out.println(element.getClass() .getSimpleName ()); public class CreatorGeneric { public static void main(String[] Creator e = new Creator() i c. f () ; args) / * 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 contenedor ArrayList en lodos aquellos lugares donde necesitemos crear una matriz de genéricos: // : generics/ListOfGenerics.java import java.util.*; public class ListOfGenerics private List array = new ArrayList( ) ¡ public void add (T item ) { array . add(item); } public T get (int indexl { return array.get(index); /1/ , Aquí obtenernos el comportamiento de una matriz, sin renunciar por ello a las comprobaciones de tipos en tiempo de compilación que los genéricos penniten. En ocasiones, seguiremos necesitando crear una matriz de tipos genéricos (A rrayList, por ejemplo, utiliza matrices internamente). Resulta interesante saber que podemos definir una referencia de una fonna tal que haga que el compilador no se queje. Por ejemplo: // : generics/ArrayOfGenericReference.java class Generic {} public class ArrayOfGenericReference static Genericclnteger>[] gia¡ } /1/ ,El compilador acepta esta sintax is sin generar ninguna advertenc ia. Pero no podemos crear nunca una matriz de ese tipo exacto (incluyendo los parámetros de tipo), por lo que la cuestión resulta algo confusa. Puesto que todas las matrices tienen 428 Piensa en Java la misma estrucnlra (tamaño de cada posición de la matriz y disposición de la matri z) independientemente del tipo que almacene, 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 public class ArrayOfGeneric static final int SIZE = 100; static Generic(] gia¡ @SuppressWarnings("unchecked ll ) public static void main(String(] args) 1/ Se compila; genera ClassCastException: JI! gia : (Gene ric[] )new Object[SIZE] i /1 El tipo en tiempo de ejecución es el raw (después del borrado): gia = (Generic[] )new Generic(SIZE] ¡ System.out.println(gia.getClass() .getSimpleName {)); gia[oJ :::: new Generic(); II! gia[lJ = new Object()¡ II Error de tiempo de compilación II Descubre la discordancia de tipos en tiempo de compilación: II! gia [2] = new Generic (); 1* Output: Generic [] 'j j j> 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[], 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 ejecución, sigue siendo Ulla matriz de tipo Object, yeso hace que se produzcan problemas. La única forma de crear adecuadamente 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 public class GenericArray { private T[] arraYi @SuppressWarnings ( "unchecked") public GenericArray (int SZ) { array = (T []) new Object [sz) ; public void put(int index, T item) array[index] = item; public T get{int index) { return array[index] i } II Método que expone la representación subyacente: public T [1 rep () { return array; public static void main{String(] args) GenericArray gai = new GenericArray (lO ) ; JI Esto provoca una excepción ClassCastException: II! Integer [1 ia :::: gai. rep () i II Esto es correcto: Objectl] ca = gai.rep(); } j j j ,- 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 dentro de l proceso de diseño, lo mejor que podemos hacer, una vez que verifiquemos que se produce una advertencia, es desactivarla 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 proyectamos 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 dentro 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 public class GenericAr ray2 { private Object[] arraY i public GenericArray2(int sz) array = new Object[sz] i public void put (int index, T item) array[index] = item; { @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 public static void main{String[] args) GenericArray2 gai = new Ge nericArray2 (10 ) ; fer (int i = O; i < 10 i i ++) gai.putU, i ) i fer (int i :; O; i < 10; i ++ ) System.out .print(gai . get(i) + " " ) i System.out.println() i try ( Integer [] ia = gai . rep () ; } catch(Exceptien e) { System.out.println(e); 1* Output : (Sample ) 0 12 3 4 5 678 9 430 Piensa en Java java.lang.ClassCastException: (Ljava.lang.Object¡ cannot be cast to [Ljava.lang.lnteger¡ * ///,Inicialmente, parece que las cosas no son muy distintas, salvo por el hecho de que hemos desplazado de lugar la proyección de tipos. Sin las anotaciones @SuppressWarnings, seguimos obteniendo advertencias que nos dicen que faltan comprobaciones. Sin embargo, la representación interna es ahora Objectll en lugar de TII . Cuando invocamos get(), se efectúa la proyección del objeto sobre T, que es de hecho el tipo correcto, por lo que la operación es segura. Sin embargo, si invocamos rep(), el método vuelve a intentar proyectar Objectll sobre TI!' lo que sigue siendo incorrecto y genera una advertencia en tiempo de compilación y una excepción en tiempo de ejecución. Por tanto, no bay ninguna manera de cambiar el tipo de la matriz subyacente, que sólo puede ser Objectll . La ventaja de tratar array internamente como Objectll en lugar de como T[J es que resulta mellas probable que nos olvidemos del tipo de la matriz en tiempo de ejecución e introduzcamos accidentalmente un error (aunque la mayoría de esos errores. y quizá todos, se detectarían rápidamente en tiempo de ejecución). Para el nuevo código que desarrollemos, lo que debemos hacer es pasar un testigo de tipos. En dicho caso, GenericArray tendría el aspecto siguiente: 1/ : generics/GenericArrayWithTypeToken.java import java.lang.reflect.*¡ public class GenericArrayWithTypeToken private T(] arraYi @SuppressWarnings (lunchecked") public GenericArrayWithTypeToken (Class type, int sz) array = (T[] ) Array.newlnstance (type, sz); { public void put(int index, T item) array[index] = item; public T get (int index) { return array [index] ; II Exponer la representación subyacente: public T [] rep () { return array i } public static void main(String[] args) GenericArrayWithTypeToken gai new GenericArrayWithTypeToken( Integer.class, la) ¡ II Esto ahora funciona: Integer [] ia = gai. rep () i El testigo de tipos Class 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 resultados 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, yespecialmente 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 compilador 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, aunque 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 interface HasColor { java. awt. Color getColor (); } class Colored { T item; Colored(T item) { this.item -::o item; T getltem() { return item; } II El límite nos permite invocar un método: java . awt. Color color () { return i tem. getColor () ; class Dimension { public int x, y, Z; } II Esto no funcionará -- primero hay que definir la clase II y luego las in terfaces: II class ColoredDimension JI Múltiples límites: class ColoredDimension 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 interface Weight ( int weight(); JI JI Al igual que con la herencia, sólo se puede tener una clase concreta, pero puede haber múltiples interfaces: cIass Solid ( T item; Solid(T item) { this.item = item; } T getltem () { return itero; } java. awt . Color color () { return item. getColor (l ; int getX () { return item.x; } int gety() { return item.y; } int getZ() { return item.z; } int weight () ( return itero. weight () ; cIass Bounded extends Dimension implements HasColor, Weight ( public java. awt. Color getColor () { return null i public int weight () { return O; } public cIass BasicBounds { public statie void main(String[] args) Solid solid = new Solid(new Bounded()); salid.color () ; salid.gety{) ; salid. weight () ; } /// , Habrá observado que BasicBounds.java parece contener redundancias que podrían eliminarse recurriendo al mecanismo de herencia. En el siguiente ejemplo, podemos ver cómo cada ni vel de herencia añade también restricciones de límite: 11: generics/lnheritBounds.java class Holdltem { T item; Holdltem(T item) ( this.item T getltem() ( return item; ) item; } class Colared2 extends Holdltem Colored2 (T i tem) { super (i tem) ; java . awt . Color color () { return item. getColor (); } class ColoredDimension2 extends Colored2 ( ColoredDimension2(T item) { super (item) ; int getX () return i tem. x; } int getY() return item.y; } int getZ() return item. Z; } class Solid2 extends ColoredDimension2 { Solid2 (T item) { super{item); int weight () { return i tem. weight () ; 15 Genéricos 433 public class InheritBounds { public static vo i d ma i n{Str i ng(] args) Solid2 solid2 = new Solid2{new Bounded()) { i solid2 . color () ; solid2. gety () ; solid2 . weight() i ) 111> Holdltem simplemente almacena un objeto, por lo que este comportamiento es heredado dentro de Colored2, que requiere que también su parámetro se corresponda con HasColoT. ColoredDimension2 y Solid2 ex ti enden todavía más la jerarquía y añaden límites en cada ni ve l. Ahora, los métodos son heredados y no tienen por qué repetirse en cada clase. He aquí un ejemplo con más niveles: jI: generics/EpicBattle . java // Ejemplo de límites para genéricos de Java . import java . util .* ; interface SuperPower {} interface XRayVision extends SuperPower { void seeTh r oughWalls{); interface SuperHearing extends SuperPower { vo i d hearSubtleNoises(); interface SuperSmell extends SuperPower { vo id trackBySmell(); class SuperHero { POWER power ¡ SuperHero(POWER power) { this.power = power¡ POWER getPower() { return power; } class SuperSleuth extends Supe r Hero { SuperSleuth(POWER power) { super(power)¡ void see () { power . seeThroughWalls () ¡ } class CanineHero exte nds SuperHero { Cani neHero(POW ER power) { super (power) ¡ void hear () { power. hearSubtleNoises () ; void srnell () ( power . trackBySrnell () ; ) class SuperHearSmell implements SuperHearing, Sup erSmell { public void hearSubtleNoises () {} public void tra ckBySrnell () () class DogBay e x tends CanineHero DogBoy () ( super (new SuperHearSrnell () ); ) public class EpicBattle { 434 Piensa en Java II Limites en métodos genéricos: static void useSuperHearing (SuperHero hero) { hero.getPower() .hearSubtleNoises(); static void superFind(SuperHero hero) { hero.getPower{) .hearSubtleNoises{); hero.getPower() . trackBySmell{); public static void main(String[] args) DogBoy dogBoy = new DogBoy(); useSuperHearing(dogBoy) ; superFind(dogBoyl; II Podemos hacer esto: List audioBoys; II Pero no podemos hacer esto : II List dogBoys; 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 argumento de parámetro esté limitado por la primera interfaz y otro cuyo argumento de parámetro esté limitado 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 class class class Frui t {} Apple extends Fruit {} Jonathan extends Apple {} Orange extends Fruit {} public class CovariantArrays { public static void main(String[] args) { Fruit(] fruit = new Apple[lO]; fruit[O] = new Apple(); // OK fruit[l] = new Jonathan(); //OK II El tipo en tiempo de ejecución es Apple (], no Fruit {l ni Orange {] : try { II El compilador permite añadir Fruit: fruit(O] = new Fruit(); II ArrayStoreException } catch(Exception el { System.out . println(e); } try { II El compilador permite añadir Oranges: fruit [O] = new Orange (); II ArrayStoreException catch(Exception el { System.out.println(e) i } 15 Genéricos 435 } 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 subtipo 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 realizadas 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.*¡ public class NonCovariantGenerics II Error de compilación: tipos incompatibles : Li st flis t = new ArrayList{) ; 111 > Aunq ue pudiéram os sentimos tentados de concluir, a la vista de este ejemplo, que no se puede asignar a un contenedor de objetos Apple a un contenedor de objetos Fruit, rec uerde que los genéricos no se refieren sólo a los contenedores. Lo que este ejemplo nos dice realmente es que no se puede asignar un genérico relacionado con objetos Apple a un genérico relacionado con objetos Fruit. Si el compilador, como sucede en el caso de las matrices, supi era lo suficiente acerca del códi go como para detenninar que hay contenedores impli cados, quizá podría ser algo más penn isivo. Pero el compilador no sabe que es así, por lo que rehusará pemlitir la ·'generalización". De todos modos, tampoco se trata de una "generalización rea''': una lista de objetos Apple no es una lista de objetos Fruit. Una lista de objetos Apple penllitirá almacenar objetos Apple y subtipos de Apple, mientras que una lista de objetos Fruit permitirá almacenar cualquier clase de objetos F'ruit. Es cierto que esto incluye a los objetos Apple, pero eso no la hace una lista de objetos Apple; seguirá siendo una lista de objetos Fruit. Una lista de objetos App le no es equivalente en lo que respecta a tipos a una lista de objetos Fruit, aún cuando un objeto Apple sea un tipo de objeto Fruit. La cuestión real es que de lo que estamos hablando es del tipo de contenedor, no del tipo de los objetos que el contenedor almacena. A diferencia de las matrices, los genéricos no tienen mecanismo de covarianza integrado. Esto se debe a que las matrices están definidas completamente en el lenguaje y pueden incorporar, por tanto, comprobaciones tanto en tiempo de compilación como en tiempo de ejecución; sin embargo, con los genéri cos, el compi lador y el sistema de ejecución no pueden saber lo qu e queremos hacer con los tipos y cuáles son las reglas que deberían apl icarse. En ocasiones, sin embargo, puede qu e queramos establecer algún tipo de relación de generalización entre los dos, y esto es, precisamente, lo que los comodines penniten. 11 : generics / Generi c sAndCovariance.java impo rt java.util.*¡ 436 Pien sa en Java public class GenericsAndCovariance public static void main(String[] args) II Los comodines permiten la covarianza: List flist = new ArrayList() i II Error de compilación: no se puede añadir cualquier tipo de objeto: II flist.add(new Apple()); II flist.add(new Fruit()); II flist.add(new Object()) ; flist . add{null); II Legal pero poco interesante II Sabemos que devuelve al menos Fruit: Fruit f = flist . get (O) ; ) 111 ,El tipo de flist es ahora List, lo cual puede leerse como "una lista de cualquier tipo que herede de Fruit". Sin embargo, esto no significa que la lista pueda almacenar cualquier tipo de objeto Fruit. El comodín hace referencia a un tipo concreto, por lo qu e significa "algún tipo especí fi co que la referencia flist no especifique". Por tanto, la lista que se asigne tiene que estar almacenando algún tipo especificado como Fruit o Apple, aunque para poder realizar la generalización a rust, ese tipo se es pecifica como "no importa cuál sea". Si la úni ca restricción es que la lista almacene un tipo o subtipo de Fruit específico, pero no nos importa en realidad cuál sea éste, entonces ¿qué es lo que podemos hacer con di cha lista? Si no sabemos el tipo de objeto que la lista está almacenando, ¿cómo podemos añadir con seguridad un objeto? Al igual que sucede con la "'generalización" de la matriz CovariantArrays.java, no podemos añadir cualquier objeto que queramos, lo único que sucede es que, en este caso, el compilador prohíbe las operaciones no pennitidas antes, en lugar de que sea el sistema de ejecución el que se encargue de prohibirlas. En otras palabras, esto nos pennite descubrir el problema bastante antes. Podríamos pensar qu e las cosas se han ido un poco de las manos, porque ahora ni siquiera podemos añadir un objeto Applc a una li sta qu e habíamos dicho que sí podía almacenar objetos Apple. En efecto, así es, pero es que el compilador no tiene ningún conocimiento de eso. Un contenedor List puede apuntar a una lista List. Una vez qu e hacemos este tipo de "generalización", perdemos la posibilidad de pasar ningún objeto, ni siquiera de tipo Object. Por otro lado, si invocamos un método que devuel va Fruit, esa operación sí es segura porque sabemos que cualquier cosa que haya en la lista deberá ser al menos de tipo Fruit, por lo que el compilador pernutirá la operación. Ejercicio 26: (2) Ilustre la covarianza de matrices utili zando objetos de tipo Number e Inleger. Ejercicio 2'7: (2) Demuestre que la cova ri anza no funciona con las listas utilizando objetos de tipo Number e Inleger. y luego introduzca comodines. ¿Hasta qué punto es inteligente el compilador? Ahora, podríamos pensar que no podemos in vocar ningún método qu e admita argumentos, pero vamos a anali za r este ejemplo: 11: generics/compilerlntelligence . java import java.util. * ; publ i c class Compilerlntelligence public static void main{String[] args) List flist = Arrays . asList(new Apple()); Apple a = (Apple) f list.get{O) i 1I Ninguna advertencia flist.contains(new Apple{)} i II El argumento es ' Object' flist . indexO f (new Apple()) i 1I El argumento es 'Object ' ) 111,Podemos ver llamadas a conlains( ) e indexOf( ) que toman objetos Apple como argum entos, y esas llamadas son perfectamente válidas. ¿Significa esto qu e el compilador si que examina el código para ver si un método concreto modifica su obj eto? 15 Genéricos 437 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. 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 invocar 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 compilador 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 comodines, 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 publíc class Holder prívate T value¡ public Holder() () public Holder{T val) { value ::: val; } public void set (T val) { value "" val; public T get () { return value¡ ) public boolean equals (Object obj) { return va l ue.equals(obj) ¡ public static void main(String[] args) { Holder Apple = new Holder{new Apple ()); Apple d = Apple.get(); Apple.set(d) ; II Holder Fruit = Apple¡ II No se puede generalizar Holder fruit = Apple¡ 11 OK Fruit p = fruit.get(); d = (Apple)fruit.get(); II Devuelve ' Object' try { Orange c = (Orangelfruit.get{)¡ 11 Ninguna advertencia } catch(Exception el { System.out.println(el ¡ } 11 fruit.set(new Apple() ¡ 11 No se puede invocar set() 11 fruit.set(new Fruit ()) ¡ 11 No se puede invocar set{) System.out.println( fruit.equals(d)); II OK /* 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, no podemos generalizarlo a Holder, pero sí que podemos generalizarlo a Holder. 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 predicció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 o incluso utilizando un parámetro de tipo: (aunque no se puede dar un límite de supertipo a un parámetro genérico, es decir, no podemos escribir apples) { apples.add {new Apple {»; apples.add(new Jonathan()) i II apples . add{new Fruit {»; II Error } 111 ,El argumento apples es una lista de algún tipo que es el tipo base de Apple; por tanto. sabemos que resulta seguro añadir un objeto Apple o un subtipo de Apple. Sin embargo, puesto que el limite inferior es Apple, no sabemos si resulta seguro añadir un objeto Fruit a dicha li sta, porque eso penniriría que la lista se abriera a la adición de tipos distintos de Apple, lo que violaría la seguridad estática de tipos. Por tanto, podemos pensar en los límites de subtipo y de supertipo en térnlinos de cómo se puede "escrib ir" (pasar a un método) en un tipo genérico y cómo se puede "leer" (devolver desde un método) de un tipo genérico. Los límites de supertipo relajan las restricciones de 10 que podemos pasar a un método: JI : genericsJGenericWriting.java import java . util .* ¡ public class GenericWriting static void writeExact (List< T:> list, list.add (it em ) ; T item) { static List apples = new ArrayList () i static List fruit = new ArrayList( ) i static void f l () ( writeExact(apples, new Apple(») i JI writeExact (fruit, new Apple {) ; II Error: IJ Tipos incompatibles: se encontró Fruit, se requiere Apple static void writeWithWildcard(List list, list.add(item) ¡ T item) static void f2 () ( writeWithWildcard(apples, new Apple (») writeWithWildcard(fruit, new Apple {» ; i public static void main(String[] args) { { fl () ¡ f2( ) ¡ } 111 ,El método writeExacl() utiliza un tipo de parámetro exacto (comodines). En fl() podemos ver que esto funciona correctamente, siempre y cuando nos limitemos a insertar un objeto Apple en un contenedor List. Sin embargo. writeExact() no permite insertar un objeto Apple dentro de un contenedor List, aún cuando nosotros sepamos que eso deberia ser posible. En wrileWilhWildcard(), el argumento es ahora Lisl, por lo que la lista almacena un tipo especifico derivado de T; por tanto. resulta seguro pasar T o cualquier cosa que se derive T como argumento a los métodos de la lista. 15 Genéricos 439 podemos ver esto en f2( ), donde sigue siendo posible insertar un objeto Apple en una lista List, como antes, pero ahora resulta posible insertar un objeto Apple en una lista List, 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. * ; public class Ge n ericReading static T readExact (List list) return l i st . get(Q); { static List app les = Arrays . as List(new Apple()); static List fruit = Arrays.asList(new Fruit(»); JI Un método estático se adapta a cada llaroma da: static void fl () { Apple a = readExact(apples); Frui t f = readExact(fruit); f = readExact(apples) i 1/ Sin embargo, s i tenemos una clas e , su t ipo se 11 establece en el moment o de i nstanciarla : static class ReadereT> { T readEx act(ListcT> list) ( return list . get(O); sta tic void f2 () { ReadercFruit> fruitReader = new ReadercFruit>(); Fruit f = fruitReader . readEx act(fruit); 11 Fruit a = frui tRead e r . readExact (apples) ; 11 Error : 11 readExact( ListcFruit» no pu e d e 11 apl i carse a (ListcApple» . static class Cova r iantReadereT> T readCovariant (Liste? extends T> list) return list.get(O)¡ { static void f3 () { CovariantReadercFruit> fruitRead e r = new CovariantReadercFruit>() i Fruit f fruitReader.readCovariant(fruit); Fruit a = fruitReader . readCovariant(apples) public static void main (String [] nll; f211; 0 11 ; args) 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 y un objeto Fruit de una lista List, como puede verse en fJ() . Por tanto, si podemos resolver el problema 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, puesto que ese es su tipo concreto. Pero una lista List 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, 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. Ejercicio 28: (4) Cree una clase genérica Genericl con un único método que tome un argumento de tipo T. Cree una segunda clase genérica Generic2 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 parece 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.*¡ public class UnboundedWildcardsl static List listl¡ static List list2¡ static List list3¡ static void assignl{List list) { listl = list¡ list2 = list ¡ II list3 = list¡ II Advertencia: conversión no comprobada II Found: List, Required: List static void assign2(List list) listl list ¡ list¡ list2 list3 list¡ { static void assign3(List list) listl list¡ list¡ list2 list¡ list3 public static void main(String[] args) { assignl(new ArrayList()} ¡ assign2(new ArrayList()} ¡ II assign3(new ArrayList{»¡ II Adevertencia: II conversión no comporbada. No encontrado: ArrayList II Requerido: List assignl(new ArrayList(» ¡ assign2(new ArrayList(» ¡ assign3(new ArrayList(» ¡ II Ambas formas son aceptables como List wildList = new ArrayList {) ¡ wildList = new ArrayList(); assignl(wildList) ; assign2(wildList) i assign3(wildList) ¡ 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 utilizando 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 asigna un tipo concreto al otro parámetro: 1/ : generics/UnboundedWildcards2 . java i mport java.util.*; public class UnboundedWildcards2 static Map mapl i static Map map2; static Map map3; { mapl c: mapi } s tatic void assign2 (Map map ) { map2 = map; static void assign3 (Map map ) { map3 = mapi p ublic static void main (String (] args) { static void assignl (Map map ) assignl (new HashMap ()) ; assign2 (new HashMap (» i /1 assign3 {new HashMap (» i /1 Advertencia: 11 Conversión no comprobada. Encontrado: HashMap 11 Requerido : Map assignl (new HashMap (» ; assign2 {new HashMap (» ; assign3 (new HashMap (» ; } /// > 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 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, y List es de hecho equiva lente a List. Sin emba rgo, estas afinnaciones no son completamente ciertas. List quiere decir en la práctica "una lista simple que almacena 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 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. public class Wildcards { /1 Argumento normal: static void rawArgs (Holder holder, Objec t arg ) { // holder.set {arg); / 1 Advertencia: /1 Llamada no comprobada a (T ) como miembro 1/ del tipo normal Holder // holder.set{new Wildcards {») ; I1 Misma advertencia /1 No se puede hacer esto; no se dispone de ninguna 'T': // T t holde r.get () ; II OK, pero la información de tipos se ha perdido : Object obj ~ holder.get () ; /1 Similar a rawArgs {) , pero c on errores en lugar de advertencias: 442 Piensa en Java static void unboundedArg(Holder holder, Object arg) II holder.set(arg); II Error, II set(capture of ?) en Holder 1I no puede aplicarse a (Object) II holder.set{new Wildcards(); II Mismo eTror I1 No se puede hacer esto; no se dispone de ninguna II T t { lT': holder.get(); II OK, pero la información de tipos se ha perdido: Object obj = holder.get{); static T exactl (Holder holder) T t = holder.get(); return t; { static T exact2(Holder holder, holder.set(arg) ; T t = holder.get(); return t; T arg) static T wildSubtype{Holder holder, 1/ holder.set(arg); II Error, II set(capture of ? extends T) en II Holder 1I no se puede aplicar a (T) T t = holder.get(); return t i { T arg) { static void wildSupertype(Holder holder, T arg) { holder.set(arg) ; liT t = holder. get (); II Error, I1 Tipos incompatibles: encontrado Object, requerido T /1 OK, pero la información de tipos se ha perdido: Object obj = holder.get(); public static void main(String[] args) Holder raw = new Holder{); 1/ O bien, raw = new Holder(); Holder qualified = new Holder() ¡ Holder unbounded = new Holder(); Holder bounded = new Holder{); Long lng = lL¡ rawArgs{raw, lng}; rawArgs{qualified, lng); rawArgs{unbounded, lng); rawArgs(bounded, lng); unboundedArg(raw, lng); unboundedArg{qualified, lng); unboundedArg{unbounded, lng); unboundedArg(bounded, ln9); II Object rl = exactl(raw)¡ II Advertencias: 15 Genéricos 443 // // Conversión no comprobada de Holder a Holder Invocación de método no comprobado: exactl(Holder // Invocac ión de método no comprobado: exact2 {Holder,T ) // aplicada a (Holder, Long ) Long r6 = exact2{qualified, ln9); JI Long r 7 = exact2 (unbounded, 1n9); JI Erro r: JI exact2 (Ho lder,T ) no puede aplicarse a (Holder ,Long) 11 11 Long rB = exact2 (bounded, ln9); JI Error: exact2 {Holder,T ) no puede aplicarse a 11 to (Holder, Long ) 11 // Long r9 = wildSubtype (raw, lng ) i / / Advertencias: // Conversión no comprobada de Holder // a Ho lder // Invocación de método no comprobado: // wildSubtype{Holder,T) // aplicada a {Holder, Long ) Long rlO = wildSubtype{qualified, ln9 ) ; // OK, pero sólo puede devolver Object: Object rll = wildSubtype {unbounded, ln9 ) ; Long r12 = wildSubtype (bounded, lng ) ; // wildSupertype (raw, lng ); // Advertencias: Conversión no comprobada de Holder a Holder // Invocación de método no comprobado: // wildSupertype (Holder,T ) // aplicada a (Holder, Long ) wildSupertype (qualified, lng) i // wildSupertype {unbounded, 1ng ) ; // Error: wildSupertype(Holder,T ) no puede 11 11 aplicarse a (Holder,Long) 11 wildSupertype(bounded, 1ng ) i / / Error: wildSupertype (Holder,T ) no puede 11 11 aplicarse a (Holder, Long) 11 11 } 111 ,En rawArgs( l, el compilador sabe que Holder es un tipo genérico, por lo que aún cuando se lo exprese aquí como un tipo nonnal, el compilador sabe que pasar un objeto de tipo Object a sct( l no resulta seguro. Puesto que se trata de un tipo normal, podemos pasar un objeto de cualquier tipo a set( l y dicho objeto se generaliza a Object. Por tanto, siempre que tengamos un tipo nonnal, estaremos sacrificando posibilidades de comprobación en tiempo de compilación. La llamada a get( l muestra el mismo problema: no hay ningún T , por lo que el resultado sólo puede ser de tipo Object. Resulta tentador pensar que el tipo Holder y Holder son aproximadamente iguales. Pero unboundedArg( l demuestra que son distintos: este método hace aflorar el mismo tipo de problemas, pero infonna de ellos como de errores en lugar de como advertencias, porque el tipo Holder pennitirá almacenar una combinación de objetos de cualquier tipo, mientras que Holder almacena una colección homogénea de algún (ipo específico, y no podemos limitarnos a pasar un objeto de tipo Object. En exactl( l y exact2( l, podemos ver los parámetros genéricos exactos utilizados si n comodines. Como vemos, exact2( l tiene limitaciones distintas que exactl( l, debido al argumento adicional. 444 Piensa en Java 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 contenedor Holder. Para impedir que se inse rte un objeto Orange en un contenedor Holder, 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 sabiendo que cualquier cosa que extraigamos de un contenedor Holder 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( ): holder 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 derivado (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 disponemos 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 referencia 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 y de un argumento de tipo T , y debido a ello genera errores o advertencias a menos que le ent reguem os los argumentos exactos. 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 argumento 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 invocar otro método que uti lice el tipo exacto. El siguiente ejemplo muestra esta técnica que se denomina conversión de caplllra 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 public class CaptureConversion static void fl {Holder holder ) T t = holder.get () i System. out. println (t. getClass () . getSimpleName () ) i static void f2 (Holder holder ) { f1 (holder ) i / / Llamada con tipo capturado @SuppressWarnings ( tlunchecked ti ) public static void main(String[] args ) Holder raw = new Holder (1) i 15 Genéricos 445 1/ fl(raw ) i JI Genera advertencias f2 (raw ) i tI Sin advertencias Holder rawBasic = new Holder () ; rawBasic.set (new Object ( ) ) ; 1/ Advertencia f2 (rawBasic ) ; / / Sin advertencias // Generalización a Holder, sigue sabiendo cómo manejarla : Holder wildcarded = new Holder ( l.O ) i f2 (wildcarded ) ; / * 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. La solución consiste en utili zar las clases envoltorio de las primitivas en conjunción con el mecanismo de conversión automática de Ja va SE5. S i c reamos un contenedor ArrayList y utilizamos enteros primitivos con este contenedor, descubriremos que e l mecanismo de conversión automática entra en acción y se encarga de convertir entre enteros y objetos Integer automáticamente, por tanto, es como si di spusiéramos de un contenedor ArrayLis t: 11 : generics / ListOflnt.java II El mecanismo de conversión automática compensa la incapacidad II de utilizar primitivas en los genéricos. import java.util. * ; public class ListOf ln t public static void main {String [] args ) { Lisc li new ArrayList () ; for ( int i :: O; i < 5; i++ ) 1i .add l i ) ; for ( int i : li) Syscem.out.print (i + " 1* Output: O 1 2 3 4 ' /// ,- 11 ) ; 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 valores 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 primitivos; 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.*; public class ByteSet { Byte[] possib1es = { 1,2,3,4,5,6,7,8,9 j; Set mySet = new HashSet{Arrays.asList{possibles)) ¡ 11 Pero no se puede hacer esto: 11 Set mySet2 = new HashSet { /1 Arrays . asList{1,2,3,4,S,6,7,e,9)); 111,Observe que el mecanismo de conversión automática resuelve algunos problemas, pero no todos ellos. El sigui ente ejemplo muestra una interfaz Generator genérica que especifica un método next() que devuelve un objeto con el tipo especi fi cado del parámetro. La clase FArray contiene un método genérico que utili za un generador para rellenar una matriz con objetos (hacer la clase genérica no funcionaría en este caso porque el método es estático). Las implementaciones de Generator provienen del Capitulo 16, Matrices, y en maine ) podemos ver cómo se utili za FArray.fill( ) para rellenar matrices con objetos: /1: generics/PrimitiveGenericTest.java import net.mindview.util.*; 1I Rellenar una matriz utilizando un generador: class FArray { public static T [] fill (T [] a, Generator gen ) { for{int i = O; i < a.length¡ i++) a[iJ = gen.next{); return a; public class PrimitiveGenericTest { public static void main{String[] args) String {] strings = FArray. fill ( new String[7] , new RandornGenerator . String{lO)); for{String s : strings) System.out.println(s) ; Integer [] integers = FArray. fill ( new Integer(7] , new RandomGenerator.Integer{)); for{int i: integers) System . out.println(i) ; II El mecanismo de conversión automática no funciona en este caso. 11 Lo siguiente no podrá compilarse : 11 int[] b = II FArray.fill (new int[7], new RandIntGenerator {)) ; 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, 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 automá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. Implementación de interfaces parametrizadas Una clase no puede implementar dos variantes de la misma interfaz genérica. Debido al mecanismo de borrado de tipos, ambas serían la misma interfaz. He aquí una situación dond e se produce este tipo de colisión: // : generics/MultiplelnterfaceVariants.java / / {CompileTimeError} (No se compilará) interface Payable {} /// ,- Hourly no se compilará debido a que el mecanismo de borrado de tipos reduce Paya ble y Paya ble a la misma clase, Payable, y el código anterior significaría que estaríamos tratando de impleme ntar la misma interfaz dos veces. Lo que resulta interesante es que, si eliminamos los parámetros genéri cos en ambos usos de Payable (como hace el compilador durante el borrado de tipos), el código si que puede compilarse. Esta cuesti ón puede resultar bastante frustrante cuando estemos trabajando con alguna de las interfaces más fundamentales de Java, como Comparable, como podremos ver más adelante en esta sección . Ejercicio 31: ( 1) Elimine todos los genéricos de Mu ltiplelnterfaeeVariants.java y modifique el ejemplo para que el código pueda compilarse. Proyecciones de tipos y advertencias Ut ilizar una proyección de tipos o instanceof con un parámetro de tipo genérico no tiene ningún efecto. El siguiente contenedor almacena los va lores internamente como objetos de tipo Objeet y los proyecta de nuevo sobre T en el momento de extraerlos: // : generics /GenericCast.java class FixedSizeStack strings = new FixedSizeStack (S IZE ) ; for lS tring s : "A BCD E F G H 1 JII.Split(l' strings.push(s) ; for{int i = O; i < SIZE; i++) String s = strings.pop(); System.out .print{s + I! " ) i 1') ) 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. *; public class NeedCasting @SuppressWarnings ( "unchecked") public void f(String[] args) throws Exception { ObjectlnputStream in = new ObjectlnputStream( new FilelnputStream (args(O] )); List shapes = (List List shapes = (L ist lwl = List.class.cast (in.readObject () ) i List lw2 = List.class.cast(in.readObject()); } /// ,Sin embargo, no podemos efectuar una proyección sobre el tipo real (List .class.cast (in .readObject ()} e incluso si aiiadimos otra proyección como la siguiente: (L ist void f (List v) {} void f (List void fl (List v) {} /1 / ,Afortunadamente, el compi lador detecta este tipo de problemas. Secuestro de una interfaz por parte de la clase base Suponga que disponemos de una clase Pet que es Comparable con otros objetos Pet: jj : genericsjComparablePet.java public class ComparablePet 450 Piensa en Java i mplements Comparable{ II Error: Comparable no puede heredarse con II diferentes argumentos: y p u blic int compareTo (Cat arg ) { return O; } /1 / ,Lamentablemente, esta solución no funciona. Una vez que se establece el argumento Compar ablePet para Comparable, no puede ya compararse ninguna otra clase implementadora con ninguna cosa, salvo con ComparablePet: /1 : generics / RestrictedComparablePets . java class Hamster e x tends Compa r ablePet implements Comparable public int compareTo (ComparablePet arg ) { return O; } /1 O simplemente : class Gecko extends ComparablePet { public int compareTo (ComparablePet arg ) { return O; } } /1 /,Harnster demuestra que es posible reimplementar la misma interfaz que podemos encontrar en ComparablePet, siempre y cuando sea exactamente la misma, incluyendo los tipos de parámetro. Sin embargo, esto es lo mismo que limitarse a sustituir los métodos en la clase base, como puede verse en Gecko. 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 {} 15 Genéricos 451 public class CuriouslyRecurringGeneric extends GenericType {} /1/:- Este tipo de estructura podría denominarse genérico curiosamente recurrente siguiendo el título del artículo Curiously Recurring Template Pattern de Jim Coplien aplicado a C++. La parte "curiosamente recurrente" hace referencia al hecho de que nuestra clase, lo cual resulta muy curioso, en su propia clase base. Para com prender 10 que esto significa, tratemos de enunciarlo en voz alta: "Estamos creando una nueva clase que hereda de un tipo genérico que toma el nombre de nuestra clase como parámetro", ¿Qué es lo que puede hacer el tipo base genérico cuando se le da el nombre de la clase derivada? A este respecto, tenemos que tener en cuenta que los genéricos en Java están relacionados con los argumentos y los tipos de retomo, así que se puede defin ir una clase base que utilice el tipo derivado en sus argumentos y en sus tipos de retomo. También puede utilizar el tipo derivado para definir el tipo de los campos, aún cuando esos tipos serán borrados y sustituidos por Object . He aquí una clase genérica que nos permite expresar esto: // : generics/BasicHolder.java public class BasicHolder T element¡ void set(T arg) { element T get () { return element; void f () arg; ) ( System. out. println (element. getClass () . getSimpleName () ) ; 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 class Subtype extends BasicHolder {} publ ic class CRGWithBasicHolder { public static void main{String[] args) { Subtype stl = new Subtype(), st2 = new Subtype() stl.set (st2) i stl.get () ; Subtype st3 i stl.f() ; / * 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 cierta 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 exactamente 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 class BasicOther extends BasicHolder {} public class Unconstrained { public static void main(String(] args) BasicOther b = new BasicOther (), b2 b.set(new Other()) i Other other = b.get () ; b. f 11 ; = new BasicOther() ¡ 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 SelfBounded set(T arg) element = arg ¡ return this¡ T get () { return e leme n t ¡ } class A extends SelfBounded {} class B extends SelfBounded {} II También OK class C extends SelfBounded C setAndGet IC arg) ( set la rg ); return get () ; } class O {} II No se puede hacer esto: II class E extends SelfBounded {} 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 {} public class SelfBounding { public static void rnain (String [] A a = new A() ; a.set (new A() 1 ¡ a a. set (new AII) .get 11; a a. get 11 ; C e = new C(l i c c.setAndGet(new C{))¡ argsl { 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 {} 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 compilaría: JI : generics/NotSelfBounded . java public class NotSelfBounded T element¡ NotSelfBounded set(T arg) element = arg i return this; T get () { return element; } class A2 extends NotSelfBounded {} class B2 extends NotSelfBounded {} class C2 extends NotSelfBounded C2 setAndGet IC2 arg) ( set larg); return get 1) ; } elass D2 () // Ahora esto es OK: class E2 extends NotSelfBounded {} ///:- Así que, obviamente, la restricción de autolimitación sólo sirve para imponer la relación de herencia. Si se utiliza la autolimitació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 public class SelfBoundingMethods static {} public class GenericsAndReturnTypes void test (Getter g) { Getter result = g.get() ¡ GenericGetter gg = g.get(); jj También el tipo base 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) ") ¡ class DerivedSetter extends OrdinarySetter { void set (Derived derived) { System.out.println ("DerivedSetter. set (Derived) ti) public class OrdinaryArguments ( public static void main{String(] args) Base base = new Base (); Derived derived = new Derived()¡ ¡ 15 Genéricos 455 DerivedSetter ds = new DerivedSetter(); dS.set(derived} ; ds.set(base); /1 Se compila : sobrecargado, no sustituido / * 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 derivado como argumento, no el tipo base: 1/ : generics/SelfBoundingAndCovariantArguments .java interface SelfBoundSetter {} public class SelfBoundingAndCovariantArguments { void testA(Setter 51, Setter 52, SelfBoundSetter sbs) sl.5et{s2) ; II sl.set{sbs}; II Error, // set(Setter) en SelfBoundSetter // no puede aplicarse a (SelfBoundSetter ) { 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 sobrecarga, al igual que sucede en el caso no genérico: //: generics/PlainGenericlnheritance.java class GenericSetter { // Sin autolimitación void set (T arg) { System. out. println ("GenericSetter. set (Base) ,, ) ; class DerivedGS extends GenericSetter { void set(Derived derived) { System.out.pr intln ( "DerivedGS.set (Derived ) 11 ) i public class PlainGenericlnheritance { public static void main(String[] args) Base base = new Base(); Derived derived = new Derived () i DerivedGS dgs = new DerivedGS(); dgs.set(derived) i dgs . set(basel i / / Se compila: sobrecargado, no sustituido / * 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 contiene un conjunto set(Base). Aquí, DerivcdGS hereda de GenericSetter 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 sobrecargar 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étodo 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. Seguridad dinámica de los tipos Dado que podemos pasar contenedores genéricos a los programas anteriores a Java SES, sigue existiendo la posibilidad de que código escrito con el esti lo antiguo corrompa nuestros contenedores. Java SES dispone de un conjunto de utilidades en java.utU.Collections para resolver el problema de comprobación de tipos en esta situación: los métodos estáticos checkedCollection( ), checkedList( ), checkedMap( ), checkedSct( ), checkedSortcdMap( ) y checkedSortcdSet( ). Cada uno de estos métodos toma como primer argumento el contenedor que queramos comprobar dinámicamente y como segundo argumento el tipo que queramos imponer que se utilice. Un contenedor comprobado generará una excepción ClassCastException en cualquier lugar en el que tratemos de insertar un objew inapropiado, a diferenc ia de los conrenedores anteriores a la introducción del mecanismo de genéricos (conrenedores nonna les y corrientes), que lo que harían sería inforruamos de que hay un problema en el momento de tratar de extraer el objeto. En este último caso, sabremos que existe un problema, pero no podremos detenninar quién es el culpable; por el contrario, con los contenedores comprobados, sí que podemos averiguar quién es el que ha tratado de insertar el objeto erróneo. Examinemos este problema de "inserción de un gato en una lista de perros" utili zando un contenedor comprobado. Aquí, oldStyleMethod( ) representa un cierto código heredado, porque admite un obejto List nomlal, siendo necesaria la anotación @SuppressWarnings("uDchecked") para suprimir la advertencia resultante: 11 : generics / CheckedList.java II Using Collection.checkedList {) . import typeinfo . pets.*; import java.util.*; public class CheckedList @SuppressWarnings ( "unchecked" ) static void oldStyleMethod (List probablyDogs } { probablyDogs . add (new Cat( )} ; public static void main (String(] args ) { List dogsl = new ArrayList () ; oldStyleMethod(dogslJ; II Acepta sin rechistar un objeto Cat List dogs2 = Collections.checkedList( new ArrayList(), Dog.class); try { oldStyleMethod (dogs2 ) ; II Genera una excepción catch (Exception e ) { System.out.println (e ) ; I I Los tipos derivados funcionan correctamente: List pets = Collections.checkedList( new ArrayList(), Pet.class); pets.add (new Dog {) ) ; 15 Genéricos 457 pets . add{new Cat()) i / * 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 perfectamente 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 excepció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 .* ; interface Processor { void p r ocess{List resultCellector) throws E¡ class ProcessRunner extends ArrayLis t processAII{) throws E { List r e sultCollector = new ArrayList(); for{Processor proc e ssor : this) p r ocessor . process(resultCollector) ; return resul t Collector; class Failure1 extends Exception {} class Processor1 implements Processor static int count = 3; public void process(List resultCollector) throws Failurel { if(count-- > 1) resultCollector. add ( " Hep! " ) ; else resultCollector . add ("Ha! U) i if(count < O) throw new Failure1(); class Failur e2 ext e nds Ex ception {} class Processor2 impleme nts Processar 458 Piensa en Java static int count = 2; public void process(List resultCollector) if(count-- == O) resultCollector.add(47) ; else ( resulcCollector.add(ll) ; throws Failure2 { if (count < O) throw new Failure2(); public class ThrowGenericException public static void main(String[] args) ProcessRunner runner = new ProcessRunner () ; for(ínt i = Oi i < 3; i++) runner.add(new Processorl{»; try ( System.out.println(runner.processAll{» catch(Failurel e) { System.out.println(e) ; ; ProcessRunner runner2 = new ProcessRunner(); 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 resultCollector (esto se denomina parámetro de recolección). Un objeto 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 programació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, precisamente, 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ámetro 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 temporal y otro que añade un número de sen e para cada instancia de objeto: JI: generics/Mixins.cpp #include #inc!ude #include using namespace std¡ template class TimeStamped public T { long timeStamp¡ public: TimeStampedO { timeStamp = time(O) ¡ } long getStamp () { return timeStamp¡ } ); template class SerialNumbered public T { long serialNumber¡ static long counter¡ public: SerialNumbered () { serialNumber = counter++ ¡ } long getSerialNumber () { return serialNumber; } }; 11 Definir e inicializar el almacenamiento es t ático: template long SerialNumbered: : counter = 1¡ class Basic { string value¡ public: vaid set (string val) { value string get () { return value¡ }; val; ) int main () { TimeStamped > mixinl, mixin2¡ mixinl.set("test string 1"); mixin2 . set ( " test string 2 t' ) ; caut « mixin1. get () « " " « mixin1 . getStamp () « endl; " " « mixin1. getSerialNumber () « 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 > 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 infonnació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. Mezclado de clases utilizando interfaces Una solución comúnmente sugerida consiste en utilizar interfaces para generar el efecto de los mixins, de la f0n11a siguiente: 11: generics/Mixins.java import java.util.*; interface TimeStamped { long getStamp (); } class TimeStampedlmp implements TimeStamped private final long timeStamp; public TimeStampedlmp () { timeStamp = new Date() .getTime{); public long getStamp() { return timeStamp¡ interface SerialNumhered { long getSeriaINumber(); } class SerialNumberedlmp implements SerialNumbered private static long counter = 1¡ private final long serialNumber = counter++¡ public long getSerialNumber () { return serialNumber; interface Basic { public void set(String val); public String get() ¡ class Basiclmp implements Basic private String value; public void set (String val) { value public String get() { return value¡ val; } class Mixin extends Basiclmp implements TimeStamped, SerialNumbered private TimeStamped timeStamp = new TimeStampedlmp(); private SerialNumbered serialNumber = new SeriaINumberedlmp() ¡ public long getStamp() { return timeStamp.getStamp(); public long getSerialNumber () { return seriaINumber.getSerialNumber(); public class Mixins { public static void main(String(] args) Mixin mixinl = new Mixin(), mixin2 new Mixin(); mixinl. set ( " test string 1 11 ) ; mixin2.set("test string 211); System. out. println (mi xinl. get () + " 11 + mixinl. getStamp () + 11 11 + mixinl. getSerialNumber () ) i System. out. println (mixin2 . get () + " " + mixin2. getStamp () + 11 11 + mixin2. getSerialNumber () ) ¡ 15 Genéricos 461 /* Output: {Sample} test string 1 1132437151359 1 cest string 2 1132437151359 2 *///0La 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. Utilización del patrón Decorador Cuando examinamos la forma en que se utiliza, e l concepto de mixin parece estar estrechamente relacionado con el patrón de diseño Decorador. 5 Los decoradores se utilizan a menudo en aquellas ocasiones donde, para poder satisfacer cada una de las combinaciones posibles, el mecanismo simple de creación de subclases produce tantas clases que llega a resultar poco práctico. El patrón Decorador utiliza objetos en di stintos niveles para añadir responsabilidades, de fomla dinámica y transparente, a distintos objetos indi vidua les. El Decorador especifica que todos los objetos que envuelven al objeto inicial de partida tienen la misma interfaz básica. En cierto modo, lo que hacemos es definir que un cierto objeto es "decorable" y luego agregarle funcionalidad por el método de envolver alrededor de ese objeto otra serie de clases. Esto hace que el uso de los decoradores sea transparente: existe un conjunto de mensajes comunes que podemos enviar a un objeto, independientemente de si éste ha sido decorado o no. La clase decoradora también tener métodos pero, como veremos, esta posibilidad tiene sus limitaciones. Los decoradores se implementan utilizando los mecani smos de composición juma con estructuras fonnales (la jerarquía de objetos decorables/decoradores), mientras que los mixins están basados en el mecanismo de herencia. Por tanto, podriamos decir que los mixins basados en tipos parametrizados son una especie de mecanismo genérico decorador que no requiere que se utilice la estructura de herencias definida en el patrón de diseño Decorador. El ejemplo anterior puede rehacerse con el patrón de diseño Decorador: /1: generics/decorator/Decoration.java package generics.decorator¡ import java.util.*¡ class Basic { private String value¡ public void set (String va l ) { value public String get () { return value ¡ val; } class Decorator extends Basic { protected Basic basic¡ public Decorator (Basic basic) { this .basic = basic ¡ public void set{String val) { basic.set{val) ¡ } public String get() { return basic.get{) ¡ } class TimeStamped extends Decorator private final long timeStamp¡ public TimeStamped (Basic basic) { 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 (); public long getStamp () { return timeStamp; class SerialNumbered extends Decorator { private static long counter = 1; private final long serialNumber = counter ++ ; public SerialNumhered (Basic basic) { super (basic) ; public long getSerialNumber () { return serialNumber; public class Decoration { public static void main (String [1 args) { TimeStamped t = new TimeStamped(new Basic()); TimeStamped t2 = new TimeStamped( new SerialNumbered(new Bas i c())); II! t2.getSerialNumber () ; II No disponible SerialNumbered s = new SerialNumbered{new Basic ()) ; SerialNumbered s2 = new SerialNumbered( new TimeStamped (ne w Basic ())) ; II! s2.getStamp(); II No disponible } 111 ,La clase resultante de un mixin contiene todos los métodos de interés, pero el tipo de objeto que resulta de la utilización de decoradores es el tipo con que el objelO haya sido decorado. En otras palabras. aunque es posible añadir más de un nive l, el tipo real será el último de esos niveles, de modo que sólo serán visibles los métodos de ese nivel fina l: por el contrario, el tipo de un mixin es lodos los tipos que se hayan mezclado. En consecuencia, una desventaja significativa del patrón de diseño Decorador es que sólo trabaja, en la práctica. con uno de los ni veles de decoración (el nivel final) mientras que la técnica basada en mixin resulta bastante más natura l. Por tamo, el patrón de diseilo Decorador sólo constituye una solución limitada para el problema que los mixins abordan. Ejercicio 38: (4) Cree un sistema Decorador simple comenzando con una clase que represeme un café normal y luego proporcionando una serie de decoradores que representen la leche, la espuma, el chocolate. el caramelo y la crema batida. Mixins con proxies dinámicos Resulta posible utili zar un pro.\)' dinámico para un mecanismo que pennita modelar los mixins de forma más precisa que lo que se puede conseguir utilizando el patrón de diseiio Decorador (consulte el Capítulo 14, Información de tipos, para ver una explicación acerca de cómo funcionan los proxies dinámicos en Java). Con un prox)' dinámico, el tipo dinámico de la clase resultante es igual a los tipos combinados que hayamos mezclado. Debido a las restriCCIOnes de los proxies dinámicos, cada una de las clases que intervengan en la mezcla deberá ser la implemen tación de una interfaz: 11 : generics/DynamicProxyMixin.java import import import import java . lang .ref lect.*; java.util .*; net.mindview.util.*; static net.mindview.util.Tuple.*; class MixinProxy implements Invocati onHandler Map delegatesByMethod; public MixinProxy(TwoTuple(); for(TwoTuple 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 mecanismo 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 public interface Performs { void speak () ; void sit(); /// ;- 11 : generics/DogsAndRobots.java II No hay tipos latentes en Java import typeinfo.pets.*¡ import static net.mindview.util.Print.*¡ class PerformingDog extends Dog implements Performs { public void speak () { print ("Woof! ") i } public void sit() { print ( tlSitting" ); } public void reproduce () {} class Robot implements Performs { public void speak () { print ("Click! It) i public void si t () ( print ( "Clank! "1; public void oilChange (1 () ) class Communicate { public static void perform(T performer) performer.speak{) ; performer . sit() ¡ public class DogsAndRobots { public static void main (S tring [] args ) { PerformingDog d = new PerformingDog{); 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 lenguaje 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 Robot r : new Robot () ; Communicate.perform( d J ; Communicate.perform (r ) ; / * 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 especificar 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 p ublic class SimpleDogsAndRobots { public static void main (String [) args ) { CommunicateSimply.perform (new PerformingDog ( »; CornmunicateSimply.perform(new Robot ( »; / * 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. Compensación de la carencia de tipos latentes Aunque Java no soporta el mecanismo de tipos latentes, resulta que esto no significa que el código genérico con límites no pueda aplicarse a través de diferentes jerarquías de tipos. En otras palabras: sigue siendo posible crear cód igo verdaderamente genérico. aunque hace falta algo de esfuerzo adicional. 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 latentes: 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 public void pushlnvisibleWalls () {} publ le String toString () { return "Mime 11 i // 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; public class LatentReflection { public static void main{String[J argsl { CommunicateReflectively.perform(new SmartDog()) CommunicateReflectively_perform(new Robot()); CommunicateReflectively.perform(new Mime()); i / * 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 deseados están disponibles e in vocarlos. Incluso es capaz de gestionar el hecho de que Mime sólo tiene uno de los métodos necesari os, cumpliendo parcialmente con su objeti vo. Aplicación de un método a una secuencia El mecanismo de refl ex ión proporciona algunas posibilidades interesantes, pero relegan todas las comprobaciones de tipos a tiempo de ejecución, por lo que resulta indeseable en muchas situaciones. Nonnalmente, siempre es preferible conseguir que las comprobac iones de tipos se realicen en tiempo de compilación. Pero, ¿es posible tener una comprobación de tipos en tiempo de compilac ión y tipos latentes? 15 Genéricos 469 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.*¡ public class Apply { public static extends ArrayList public FilledList(Class type, int size) { try ( for (int i = O; i < size¡ i++ ) 11 Presupone un constructor predeterminado: add (type.newlnstance (» ¡ catch (Exception e l { throw new RuntimeException (e ) ; class ApplyTest public static void main (String[] args ) throws Exception List shapes = new ArrayList( ) ; 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 s = new ArrayList( ) ; 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 Apply.apply (new FilledList (Shape.class, lO } , Shape.class.getMethod ( t!rotate " ) ; Apply. apply (new FilledLis t (Sguare. class, 1 0) , Shape.class.getMetho d ( "rotate " ») ; SimpleQueue shapeQ = new SimpleQu eue () ¡ for {int i = D i i < Si i++ ) shapeQ.add l new Shape l)) ; shapeQ.add (new Square {» ; Apply. apply (shapeQ, Shape.class.getMethod ( lIrotate" }) i / * (Execute to see output ) * /// :En Apply, tenemos sllert e. porque se da la circunstancia de que Java incorpora una interfaz Iterable que es utili zada por la biblioteca de contenedores de Ja va. Debido a esto, el método apply() puede aceptar cualquier cosa que implemente la interfaz Iterable, lo que incluye todas las clases Collection, como List. Pero también puede aceptar cualquier otra cosa, siempre y cuando hagamos esa cosa de tipo Iterable: por ej emplo. la clase SimpleQueue definida a continuación y que se utili za en el ejemplo anterior en main(): // : ge n erics / SimpleQueue.java / / Un tipo diferente de contenedor que es Iterable import java.uti l . * ¡ public class SimpleQueue implements Iterable { private Li nk edList storag e = new LinkedList () ¡ public void add (T t ) { storage . offer ( t); } public T get 1) { return storage . poll 1); } public Iterator iterator () { return storage.iterator () i } 111 0En Apply.java, las excepciones se convierten a RuntimeException porque no hay mucha posibilidad de recuperarse de las excepciones, en este caso, representan realmente errores del programador. Observe que hemos tenido que incluir límites y comodines para poder utili zar Apply y FilledList en todas las situaciones deseadas. Pruebe a experimentar quitando los límites comodines y descubrirá que Apply y FilledList no funcionan en algunas situaciones. FilIedList representa un cierto dilema. Para poder utilizar un tipo, éste debe disponer de un constructor predeterminado (si n argumentos). Java no ti ene ninguna fonlla de desc ubrir eso en ti empo de compilación, asi que el problema se pasa a tiempo de ejecución. Una sugerencia muy común para poder garanti za r la comprobación de tipos en tiempo de ejecución consiste en definir lma interfaz faetona que disponga de un método que genere objetos, entonces FilledList aceptaría di cha interfaz en lugar de la "factoría normal" correspondiente al tipo especificado. El pmblema con esto es que todas las clases que se utilicen en FilledList deben entonces implementar esa interfaz factoría. Y el caso es que la mayoría de las clases se crean sin tener conocimiento de nuestra interfaz, por lo que no pueden implementarla. Posterionnente veremos una posible solución utilizando adaptadores. Pero la técnica utilizada, consistente en emplear un indicador de tipo, constituye probablemente un compromiso razonable (al menos como primera solución). Con esta técnica, utilizar algo como FilledList es lo suficientemente fácil como para que el programador se sienta tentado de utilizarlo, en lugar de ignorarlo. Por supuesto, dado que los errores se descubren en tiempo de ejecución, será necesario preocuparse de que dichos errores aparezcan lo antes posible durante el proceso de desarrollo. Observe que esta técnica basada en un indicador de tipos es la que se recomienda en diversos libros y artículos, como por ejemplo el artículo Generics in the Java Programming Language, de Gilad Bracha9 . En dicho artículo, el autor seiiala que: 9 V¿ase la cita al fina l de este capítulo. 15 Genéricos 471 "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 ¿Qué pasa cuando no disponemos de la interfaz correcta? El ejemplo anterior aprovechaba el hecho de que la interfaz Iterable ya está disponible, siendo esa interfaz precisamente lo que necesitábamos. Pero ¿qué sucede en el caso general, cuando no existe todavía una interfaz que se ajuste a nuestras necesidades? Por ejemplo, vamos a generalizar la idea de FilledList y a crear un método fill() parametrizado que admita una secuencia y la rellene utilizando un objeto Gcnerator. Al tratar de escribir esto en Java nos encontramos con un problema, porque no existe ninguna interfaz adecuada "Addablc" (una interfaz que permita añadir objetos), mientras que en el caso anterior sí que teníamos ulla interfaz Iterable. Por tanto, en lugar de decir "cualquier cosa para la cual podamos invocar el método add( nos vemos forzados a decir "un subtipo de Collection". El código resultante no es particulamlente genérico, ya que debe restringirse para funcionar con implementaciones de Collection. Si tratamos de utilizar una clase que no implemente Collection, el código genérico no funcionará. He aquí el aspecto que tendría este ejemplo: r', 11: generics/Fill.java II Generalización de la idea de FilledList II {main, FillTest} import java.util.*; lINo funciona con tlcualquier cosa que tenga un método add () 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. public class Fill { public static ',oid fill (Collection collection, Class classToken, int size) { for(int i ::: O; i < size¡ i++) // Presupone un constructor predeterminado: try { collection.add(classToken.newlnstance()) ; catch (Exception e) { throw new RuntimeException{e); class Contract { private static long counter = O; private final long id = counter++i public String toString () { return getClass{) .getName() + " 11 + id; class TitleTransfer extends Contract {} ti. No hay 472 Piensa en Java class FillTest { public static void main(String [] args ) { List contracts = new ArrayList(); Fill.fill(contracts, Contract.class, 3} i Fill.fill{contracts, TitleTransfer.class, 2); for(Contract e: contracts) System.out . println(c) i SimpleQueue contractQueue new SimpleQueue(); 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. Simulación de tipos latentes mediante adaptadores De modo que los genéricos de Java no disponen de tipos latentes y necesitamos algo como los tipos latentes para poder escribir código que pueda aplicarse traspasando las fronteras entre las clases (es decir, código "genérico"). ¿Hay alguna forma de salvar esta limitación? ¿Qué es lo que no pennitiría hacer los tipos latentes? Los tipos latentes implicarían que podríamos escribir código que dijera: "No me importa qué tipo estoy usando, siempre y cuando ese tipo disponga de estos métodos". En la práctica, los tipos latentes crean una in/e/faz implícita que contiene los métodos deseados. Por tamo, si escribimos la interfaz necesaria a mano (ya que Java no lo hace por nosotros), eso debería resolver el problema. Escribir código para obtener una interfaz que necesitamos a partir de otra interfaz de la que disponemos constituye un ejemplo del patrón de diseño Adaptador. Podemos utili za r adaptadores para adaptar las clases existentes con el fin de producir la interfaz deseada, utilizando para ello una cantidad de código relativamente pequeña. La solución, que utiliza la jerarquía Coffee anterionnente definida, ilustra las diferentes fonnas de escribir adaptadores: 11 : generics/Fil12.java II Utilización de adaptadores para simular tipos latentes. II {main, Fil12Test} import import import import generics.coffee.*; java.util.*i net.mindview . util.*; static net . mindview . util.Print.*; interface Addable { void add(T t); } public class Fil12 { II Versión con indicador de clase: public static void fill{Addable addable, Class classToken, int size) { 15 Genéricos 473 for ( int i try ( = O; i < size¡ i++ ) addable.add (classToken.newlnstance ()) ; catch (Exception e l { throw new RuncimeException (e ) ; ) // Versión con generador : public static void fill {Addable addable, Generator generator, int size ) { for ( int i = O; i < size; iT+ ) addable.add (generator.next () ) ; JI Para adaptar un tipo base, es necesario utiliza r composición. Definir Addable como contenedor Collection usando composición: cl ass AddableCollectionAdapter implements Addable { private Collection C¡ public AddableCollectionAdapter (Collection e l ( this . c = C; JI public void add(T item) ( c . add(item ) ; ) JI Un método auxiliar para capturar el tipo automáticamente : c lass Adapter ( publie static Addable colleetionAdapter (Colleetion e ) return new AddableColleetionAdapter(c ) ; II II Para adaptar un tipo especifico, podemos usar la herencia . Hacer Addable un contenedor SimpleQueue utilizando la herencia: e lass AddableSimpleQueue e xtends SimpleQueue implements Addable ( public void add (T item) ( super.add (item) ; } e lass Fil12Test ( publie statie vo id main (String (] args ) ( II Adaptar una colección: List earrier = new ArrayList () ; Fi1l2. till ( new AddableCollectionAdapter (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 coffeeQueue new AddableSimpleQueue {) ; 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 Mocha Mocha Mocha Latte 5 6 7 8 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 indicador 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 utilizarse 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 bastante 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 permiten 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. Utilizando los objetos de función como estrategias Este ejemplo fina l nos pennitirá cód igo verdaderamente genérico utilizando la técnica de adaptadores descrita en la sección anterior. El ejemplo comenzó co n un intento de crear una suma de un a secuencia de elementos (de cualquier tipo q ue pueda sumarse), pero tem1inó evolucionando hacia la reali zación de operaciones generales. usando un estilo de programación jimciona/. Si nos fijamos exclusivamente en el proceso de sumar objetos, podemos ver que este es un caso en el que tenemos operaciones comunes entre clases, pero dichas operaciones no están representadas en ninguna clase base que podamos especificar: en ocasiones se puede incluso utili zar un operador <+' y otras veces puede haber algún tipo de método "sum a". Ésta es, general mente la situación con la que nos encontramos cuando tratamos de escribir código genérico, porque lo que queremos es que el código se pueda ap licar a múltiples clases; especialmente, como en este caso, múltiples clases que ya existan y que no tengamos posibi lidad de "corregir". Incluso si restringiéramos este ejemplo a las subclases de Number, di cha superclase no incluye nada acerca de la "sumabil idad". 15 Genéricos 475 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 comporta. 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 nOfmal). 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 principalmente 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 import import impo rt java.math.*; java.util.concurrent . atomic.*¡ java.util.*; static net.mindview.util.Print.*¡ II Distintos tipos de objetos de función: interface Combiner { T combine(T x, T yl¡ interface UnaryFunction { R function(T x l ¡ interface Collector extends UnaryFunction T result(); II Extraer resultado del parámetro de recopilación interface UnaryPredicate { boolean test(T x ) j pubIic cIass Functional { II Invoca al objeto Combiner para cada elemento con el fin de II combinarlo con un resultado dinámico, devolviéndose II al final el resultante: pubIic static T reduce{Iterable seq, Combiner combiner) { Iterator it = seq.iterator() i i f (i t . hasNext ()) { T result = it.next() i while{it.hasNext()) resul t = combiner. combine (resul t, i t. next () ) ; return result; II Si seq es la lista vacía: return null; II O generar una excepción } II II II II Tomar un objeto de función e invocarlo para cada objeto de la lista, ignorando el valor de retorno. El objeto de función puede actuar como un parámetro de recopilación, así que se lo devuelve al final. public static Collector forEach (Iterable seq, Collector tune ) { tor (T t : seq) func.tunction(t) ; 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; // Crea una lista de resultados invocando un objeto // de función para cada objeto de la lista : public static List transform (Iterable seq, UnaryFunction func ) { List result = new ArrayList () ; for (T t : seg ) result.add (func.function (t )) ; return resul t; // Aplica un predicado unario a cada elemento de una secuencia y devuelve / / una lista con los elementos que han dado como resultado "true": public static List filter ( Iterable seg, UnaryPredicate pred) { List result = new ArrayList(); fer (T t , seq ) if (pred.test(t ) ) result.add (t ) ; return result; // Para utilizar los anteriores métodos genéricos, necesitamos crear // objetos de función para adaptarlos a nuestras necesidades concretas: static class IntegerAdder implements COmbiner public Integer combine (Integer x, Integer y) { return x + y; static class IntegerSubtracter implements Combiner { public Integer combi ne ( Integer x, Integer y ) { return x - y; static class BigDecimalAdder implements COmbiner { public BigDecimal cOmbine (BigDecimal x, BigDecimal y ) { return x. add (y ) ; static class BiglntegerAdder implements Combiner { public Biglnteger cOmbine (Biglnteger x, Biglnteger y) return x.add(y ) ; static class AtomicLongAdder implements Combiner { public AtomicLong cOmbine (AtomicLong x, AtomicLong y ) { // No está claro si esto es significativo: return new AtomicLong (x.addAndGet (y.get( ))) ; / I Podemos incluso hacer una función unaria con un Itulp" / / (Units in the last place, las unidades en el último lugar ) : static class BigDecimalUlp implements UnaryFunction { 15 Genéricos 477 public BigDecimal function (BigDecimal xl return x.ulp() i { static class GreaterThan { private T bound¡ public GreaterThan(T bound) public boolean test (T xl { return x. compareTo (bound) this.bound bound; } > Di static class MultiplyinglntegerCollector implements Collector { private Integer val = 1¡ public Integer function(Integer xl val *= Xi return val; public Integer result () { { return val; } public static void main{String[] args) // Genéricos, varargs y conversión automática funcionando conjuntamente: List li == Arrays,asList{l, Integer result print(result) i = 2, 3, 4, 5, reduce (li, new IntegerAdder() result = reduee(li, new IntegerSubtracter ()) print(result) i 6, 7}; j i print(filter(li, new GreaterThan(4))) i print(forEaeh(li, new MultiplyinglntegerCollector()) .result()) i print(forEaeh(filter(li, new GreaterThan(4)), new MultiplyinglntegerCollector () ) . result () ) ; MathContext me = new MathContext(7)¡ List lbd = Arrays.asList{ new BigDecimal(1.1, me), new BigDeeimal{2.2, me), new BigDecimal(3.3, me), new BigDeeimal(4.4, me)); BigDeeimal rbd = reduce (lbd, new BigDeeimalAdder()); print (rbd) i printlfilterllbd, new GreaterThan(new BigDeeimal(3)))} ¡ II Utilizar la funcionalidad de generación de primos de Biglnteger: List lbi = new ArrayList(); Biglnteger bi = Biglnteger.valueOf{11); for{int i = O; i < 11; i++} { lbi. add Ibi I ; bi = bi.nextProbablePrime(} i print (lbi) i Biglnteger rbi print (rbi) i reduce (lbi, new BiglntegerAdder{)); 478 Piensa en Java JI La suma de esta lista de primos también es prima: print(rbi,isProbablePrime(S) ; List lal = Arrays,asList ( new AtornicLong(ll), new AtomicLong(47) , new AtomicLong (74), new AtomicLong(133»; AtomicLong ral = reduce (lal, new AtomicLongAdder()); print (ra l ) ; print(transform(lbd, new BigDecimalUlp (})) i / * Output: 28 -26 (5, 6, 71 5040 210 11.000000 (3.300000, 4.4000001 [11, 311 13, 17, 1 9, 23, 29, 31, 37, 41, 43, 47 ] 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 medida 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 tienen 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 producen 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 multitud 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 invocan 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 funció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 disponer 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 cIases (estas operaciones no tienen que ser aritméticas como son las de Fu nctional.java). Resumen : ¿realmente es tan malo el mecanismo de proyección? Habiendo estado trabajando en explicar las plantillas de C++ desde que éstas fueron concebidas, probablemente yo baya estado haciendo la pregunta que da título a esta sección durante más tiempo que la mayoría de los demás autores. Pero sólo recientemente me he detenido realmente a pensa r hasta qué punto esta pregunta es válida en muchas situaciones: ¿cuántas veces merece la pena complicar las cosas para el problema que estamos tratando de describir? Podríamos argumentar de la fonna siguiente, Uno de los lugares más obvios para utilizar un mecanismo de tipos genéricos es co n clases contenedores tales como List, Set, Map, etc" es decir con las clases que ya hemos visto en el Capítulo 11, Almacenamiento de objetos , y de lo que hab laremos más en detalle en el Capítulo 17, Análisis detallado de los contenedores. Antes de Java SE5, cuando incluíamos un objeto en un contenedor, éste se generalizaba a Obj ect , perdiéndose así la infomlación de tipos. Cuando se quería extraerlo de nuevo para hacer algo con él, era necesario volver a proyectarlo sobre el tipo apropiado. Un ejemplo sería una lista List de objetos Cat (gatos). Sin la versión genérica de los contenedores introducida en Java SE5, lo que baríarnos se ria introducir objetos de tipo Object y extraer objetos de tipo Object , con lo cual resulta perfectamente posible insertar un perro (Dog) en una lista de objetos Cat. Sin embargo, lo que las versiones de Java anteriores a la aparición de genéricos no nos pennitían era mal utilizar los objetos introducidos en un contenedor, Si introducimos un objeto Dog en un contenedor donde estamos almacenando objetos Cat y luego intentamos tratar todo lo que hay en el contenedor como si fuera un objeto Cat , se obtiene una excepción Ru ntim eException al extraer la referencia Dog del contenedor Ca t y tratar de proyectarla sobre Cal. Así pues, e l problema lennina por descubrirse; la única desventaja es que se descubra el problema en tiempo de ejecución en lugar de en tiempo de compilación, En las ediciones anteriores del libro, yo decía que: Esto no es sólo una molestia, En ocasiones, puede dar lugar a errores de pmgramación dificiles 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 contenedOl; 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 completamente natural anadir un objeto F ile (archivo) a files, por lo que babria sido mejor denominar al objeto liIeNa mes (nombres 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 permanecería oculto ese problema antes de ser descubierto? Parece lógico pensar que, tan pronto como empezáramos a ejecutar 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 podido 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ñadidas a Java? En mi opinión. la intención de esa funcionalidad del lenguaje de propósito general denominada "genéricos" (no necesariamente 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 existencia 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 indica, 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 ejemplo, 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 proporcionar. 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 primer 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 mecanismo 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 derivados 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 soluciones 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. seleccionar elementos de las mismas utilizando índices enteros y diciendo. además, que las matrices no cambian de ño. La mayoría de las veces, eso es todo 10 que necesitamos saber pero en ocasiones hay que realizar operaciones más ticadas con las matrices. y también puede que tengamos que comparar la utilización de una matriz con la de contenedores más llexibles. En este capítulo veremos cómo analizar las matrices con un mayor detalle. luego tamasofisotros Por qué las matrices son especiales Existen diferentes maneras de almacenar objetos. así que ¿qué es lo que hace que las matrices sean especiales? Existen tres aspectos que distinguen a las matrices de airas tipos de contenedores: la eficiencia, el tipo y la capacidad de almacenar primitivas. La matriz es la forma más eficiente en Java para almacenar una secuencia de referencias a objetos y para acceder aleatoriamente a ell as. Una matriz es una secuencia lineal simple, lo que hace que el acceso a los elementos sea rápido. El coste que hay que pagar por esta velocidad es que el tamaño de un objeto matriz es fijo y no puede cambiarse a Jo largo de la vida de la matriz. Podríamos pensar, como alternativa, en utilizar un contenedor de tipo ArrayList (consulte el Capítulo 11, Almacenamiento de objetos), que se encargará de asignar automáticamente más espacio, creando un nuevo contenedor y desplazando todas las referencias desde el contenedor antiguo hasta el nuevo. Aunque generalmente resulta preferible emplear un contenedor ArrayList en lugar de una matriz, esta flexibilidad adicional tiene un cieno coste asociado, de manera que un contenedor ArrayList es perceptiblemente menos eficiente que una matriz. Tanto las matrices como los contenedores incluyen mecanismos necesarios para garantizar que no podamos abusar de ellos. Independientemente de si estamos utilizando una matriz o un contenedor obtendremos una excepción RuntimeException si nos pasamos de los límites, un hecho que indica que se ha producido un error del programador. Antes de la adopción de los genéricos, las otras clases de contenedores trataban con los objetos como si éstos no tuvieran ningún tipo específico. En otras palabras, trataban con ellos como si fueran de tipo Object, la clase raíz de todas las clases de Java. Las matrices son más co nvenientes que los contenedores anteriores a los mecanismos de genéricos, porque podemos crear una matri z para almacenar un tipo específico. Esto significa que se dispone de una comprobación de tipos en tiempo de compilación, con el fin de impedir que insertemos un objeto de tipo incorrecto o que confundamos el tipo de los objetos que estemos extrayendo. Por supuesto, Java impedirá que enviemos un mensaje inapropiado a cualqu ier objeto, tanto en tiempo de compilación como en tiempo de ejecución, de modo que ninguno de los dos enfoques es más arriesgado que el otro. Simplemente resulta mucho más cómodo que el compilador nos avise de los errores, y con las matrices existe una menor probabilidad de que el usuario final pueda verse sorprendido por la generación de una excepción. Una matriz puede almacenar primitivas mientras que un contenedor de los anteriores a la adopción de mecanismo de genéricos no puede albergar primitivas. Sin embargo, con los genéricos, los contenedores tienen que especificar y comprobar el tipo de los objetos que almacenan y, gracias a los mecanismos de conversión automática, los contenedores pueden actuar COmo si fueran capaces de almacenar primitivas, ya que la conversión es transparente. He aqui un ejemplo donde se comparan las matrices con los contenedores genéricos: ji: arrays/ContainerComparison . java import java.util.*í import static net.mindview.util.Print.*¡ 484 Piensa en Java class BerylliumSphere private static long counter¡ private final long id = counter++; public String toString () { return "Sphere " + id; } public class ContainerComparison { public static void main(String[] argsl BerylliumSphere[] spheres for(int i "" O; i < 5; = new { BerylliumSphere(lO]; i++) spheres[i] = new BerylliumSphere()¡ print(Arrays.toString(spheres» ; print(spheres[4]) ; List<8erylliumSphere> sphereList = new ArrayList(); 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] ) ; List intList = new ArrayList{ Arrays . asList{O, 1, 2, 3, 4, 5»; intList.add(97) ; print(intList) ; print(intList.get{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, 4 1, 2, 3, 4, 5, 97] * ///,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 contenedores 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 primitivas como las matrices. La única ventaja que le queda, en consecuencia, a las matrices es la eficiencia. Sin embargo, cuando 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. Las matrices son objetos de primera clase Independientemente de l tipo de matri z con el que estemos trabajando, el identificador de la matriz es de hecho una referencia a un ve rdadero objeto que se crea dentro del cúmulo de memoria. Éste es el objeto que almacena las referencias a los otros objetos (los que están almacenados en la matriz) y puede crearse tanto implícitamente como parte de la sintaxis de ini- 16 Matrices 485 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 matrices 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 almacenan 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.*; public class ArrayOptions { public static void main (String [] args) { II Matrices de objetos : BerylliumSphere[] a; II Variable local no inicializada BerylliumSphere[] b = new BerylliumSphere[S] i II Las referencias dentro de la matriz se inicializan II automáticamente con null: print ("b : + Arrays. toString (b) ) ; BerylliumSphere[] c = new BerylliumSphere[4]; fortint i = O; i <: c.length ; i++} if te [i] == null) II Se puede comprobar si es una referencia nula c[i) = new BerylliumSphere{); II Inicialización agregada: BerylliumSphere[] d = { new BerylliumSphere(), new BerylliumSphere(), new BerylliumSphere{) }; I1 Inicialización agregada dinámica : a = new BerylliumSphere[] { new BerylliumSphere(), new BerylliumSphere{), }; I1 (La coma final es opcional en ambos casos) print (lIa .length + a .length) ; print (lIb .length + b .length ) ; print("c.length + c .length ) ; print (lid .length + d .length) ; a = di print (" a .length + a .length) ; 11 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: + Arrays. toString {f) ) ; int[] 9 = new int(4) i for{int i = O; i < g.length; i++) g[i] = i*i; (11 int[J 11 h = { 11, 47, 93 }; II Error de compilación: variable e no inicilizada: II!print(lIe.length = + e.length) i print ( " f .length + f .length ) ; print (lIg .length + 9 .length ) ; print ("h .length + h . length) i e = h; print ( "e .length + e .length ) ; 11 486 Piensa en Java e ~ new int [1 { 1, print ( "e.1ength = / * Output: b: [nu11, nu11, nu11, a .1ength .2 b.1ength 5 4 e .1ength d .length 3 a .1ength 3 f, [O, O, O, 0, 01 f . 1ength 5 4 9 .1ength h .length 3 e .1ength 3 2 e .1ength 2 }; + e.1ength ) ; nu11, nu11] * ///,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 inicializació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 argumento 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 podemos 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 objetos, 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étodo 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 Devolución de una matriz Suponga que estamos escribiendo un mélOdo y que no queremos devo lver un único valor, si no un conjunto de ellos. Los lenguajes C0l11 0 e y e++ hacen que esto sea dific il, porque no se puede devolver un a matriz, sino sólo un puntero a una matriz. Esto ge nera problemas. porque resulta complicado controlar el tiempo de vida de la matriz, lo que a su vez condu- ce a fugas de memoria. En Java. basta con devo lver directamente la matri z. Nunca tenemos por qu é preocupamos por esa matri z: la matriz pervivirá mientras que sea necesari a y el depurador de memoria se encargará de bo rrarl a una vez que hayamos te nninado de utili- zarla. Como ejemplo, va mos a ver cómo se devolvería una matri z de objetos String: // : arrays / lceCream.java II Devolución de matrices desde métodos. import java . util.*; public class IceCream private static Random rand ;; new Random (47 ) ; static final String [] FLAVORS = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin " , "Praline Cream ll , "Mud Pie" }; public static String [] flavorSet (int n) if(n > FLAVORS . lengt h) throw new IllegalArgumentException ( "Set too big"); String [] results ne w String [nI ; boolean[] picked ne w boolean [FLAVORS . length] ; for (int i = O; i < n; i++) ( int t· do t = rand.nextlnt( FLAVORS.length); while (picked[tj) ; results[i] = FLAVORS[t] ¡ picked[t] = true ¡ return resul ts; public static void main(String[] args) { for (int i = O; i < 7 ; i++) System.out.println(Arrays.toString(flavorSet(3))) ; 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á determinado por el argumento que le pasemos al método. A continuación, el método selecciona una serie de valores aleatoriamente 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 aleatorias 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 locali 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 .* ¡ public class MultidimensionalPrimitiveArray public static void main{String[] argsJ { int [][] a = { { l,2,3,}, {4,5,6,}, }; System.out.println (Arrays ,deepToString (a) ) ¡ 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 objetos 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 ,*¡ public class ThreeDWithNew public static void main(String[] args} /1 Matriz 3-D con longitud fija: int [] [ 1 [] a = new int [2] [2] [ 4 ] ; System,out.println(Arrays.deepToString(a)) i 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 inicializació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, * ¡ public class RaggedArray publ ic static void main (String [] args) { 16 Matrices 489 Random rand = new Random(47) i /1 Matriz 3-D con vectores de longitud varaib!e: int [) [) [) a = new int [rand.nextlnt (7)) [) [); for(int i = O; i < a.length; i++) { a [i] = new int [rand.nextlnt (S) 1 [] i for(int j O; j < a[i] .length; j++) a[i] [j] = new int[rand.nextlnt(S)]; System.out.println(Arrays.deepToString(a) ; } / * Output, [[), [[O), [ [O, O, [O), [)), [o), O), [O), [[O), [O I [O, [), OI 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.*¡ public class MultidimensionalObjectArrays public static void main(String[] args) { Beryl liumSphere [] [] spheres = { ( new BerylliumSphere (), new BerylliumSphere () }, { new BerylliumSphere() , new BerylliumSphere() , new BerylliumSphere() , new BerylliumSphere () }, new BerylliumSphere (} , new BerylliumSphere() , new BerylliumSphere() , new BerylliumSphere() , new BerylliumSphere(), new BerylliumSphere(} , new BerylliumSphere(), new BerylliurnSphere () }. }; System.out . println{Arrays.deepToString(spheres}) ¡ 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.*¡ public class AutoboxingArrays public static void main (String [1 args) { Integer [] [] a = { / j Conversión automática: { { { { 1, 2, 3, 4, 21, 22, 23, 51, 52, 53, 71, 72, 73, 5, 6, 7, 8, 24, 25, 26, 54, 55, 56, 74, 75, 76, 9, 10 }, 27, 28, 29, 57, 58, 59, 77, 78, 79, 30 60 80 }; System.out.println(Arrays.deepToString(a)} ; }. }, }, 490 Piensa en Java } 1* Output: [[1, 2, 3, 4, 5, 27, 28, 29, 301, [7 1, 72, 73, 74, 6, 7, 8, 9, lO], [21, 22, 23, 24, 25, 26, [51, 52, 53, 54, 55, 56, 57, 58, 59, 601, 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 '/ // 0La 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", }; System . out. println ( ti al: System. out. println (ti a2 : System.out.println("a3: tlfriend" }, + Arrays.deepToString(al)); + Arrays.deepToString (a2)); + Arrays.deepToString(a3)); 1* Output: aL [[1, 2, a20 [[[1.1, [9.9, 1.2], 3], [4, 5, 611 2.2], [3.3, 4.41], [2.3, 3.4111 a3: [[The, Quick, Sly, Fox] , Brown, Dog, and, friendll ' /// 0- [[5.5, 6.6], [Jumped, Over) , [7.7, 8.81], [The, Lazy, 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 segundo 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 parametrizados: Peel [ ) lSi El compilador admite este tipo de sintax is sin ernüir ni guna queja. V, aunque no podemos crear un objeto matri z real que almacene genéricos, sí que podemos crear una matri z del tipo no genéri co y efectuar una proyección de tipos: 11 : arrays / ArrayOfGenerics.java Es posible crear matrices de genéricos. impo rt java.util. * ¡ II public class ArrayOfGenerics @SuppressWarnings ( "unchecked") public static void main (String [] args ) { List [] lSi List [] la = new List [10] ; 15 = (List[] ) la¡ II Advertencia no comprobada 15 [O] = new ArrayList {) ; II La comprobac i ón en tiempo de compilación produce un error : II ! lS[l] = new ArrayList< I nteger>(); II El problema: List es un subtipo de Object Object[] objects = ls¡ II Por loq eu la a signación es correcta II Se compila y se ejecuta sin n i ngún p r oblema : objects [1] = ne w ArrayList< Integer> () ; II II II Sin embargo, si nuestras neces i dades son simples se puede crear una matriz de genéricos, aunque con una advertencia no comprobada : List [] s p he r es = (L i st[])new Li st[lO]; for( i nt i = O; i < spheres . length¡ i++) spheres [ i] = new ArrayList() ¡ Una vez que disponemos de una referencia a List lI. podemos ver que se obtiene una cierta comprobación en tiempo de compilación. El problema es que las matrices son covariantes, por lo que una matri z List 1I es también una matri z ObjectrL y podemos utili zar esto para asignar un objeto ArrayList 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 compilac 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 : II arrays / ArrayofGenericType.java Las matrices de tipos genéricos no se p ue d en compilar. public class ArrayOfGenericType T[] ar r ay; II OK @SuppressWarnings ( "unchecked") public ArrayOfGenericType(int size) JJ ! array = new T[si z e] ¡ II I legal array = (T[])new Objec t [size] ¡ IJ Advert e ncia no comprobada II Ilegal, JI ! public c:U> Uf] 111 ,- makeArray( ) { r e turn ne w U[lO] ¡ } 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 obtendremos 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 compilació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 y demuestre que el compilador no lo acepta. 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. Creación de datos de prueba Cuando se experimenta con las matrices y con los programas en general, resulta útil poder generar fácilmente matrices llenas de datos de pnleba. Las herramientas de esta sección penniten rellenar una matriz con valores u objetos. 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.*¡ public class FillingArrays { public static void main(String[] args) int size = 6; boolean[] al = new boolean[size] i byte[] a2 = new byte [size] i char[] a3 = new char[sizel; short(] a4 = new short[sizel i int [] aS = new int (size] i long [] a6 = new long [size] ; float(l a7 = new float[sizel i double[] aS = new double[size) ¡ String [] a9 = new String [size] ¡ Arrays.fill(al, true) i print (11 al = 11 + Arrays.toString(a1)) ; Arrays.fill(a2, (byte) 11) ; print(lI a 2 = 11 + Arrays.toString(a2)) i Arrays.fill(a3, 'x'); print (lIa3 = 11 + Arrays. toString (a3) ) ; Arrays. fill (a4, (short) 17) ¡ print (lIa4 = 11 + Arrays. toString (a4) ) i Arrays.fill(aS, 19) ¡ print{"aS = " + Arrays.toString(aS)); Arrays.fill(a6, 23); print ("a6 = " + Arrays. toString (a6) ) i Arrays.fil l{a7, 29); print{lIa7 = 11 + Arrays.toString(a7)); Arrays.fill{a8, 47); 494 Piensa en Java print ( "aS '" " + Arrays.fill {a9, print ( "a9 = 11 + II Manipulating Arrays.fill (a9, print ( "a9 = 11 + / * Output: [true, true, Arrays. toString (aS) ) i "HelIo"); Arrays. toString (a9) ) i ranges: 3, 5, "World") ¡ Arrays. toString (a9 )) ; true, trueJ a7 [11, 11, 11, 11, 11, 11J [x, x, x, x, x, xJ [17, 17, 17, 17, 17, 17J [19, 19, 19, 19, 19, 19J [23, 23, 23, 23, 23, 23J [29. O, 29 . O, 29.0, 29.0, 29 . 0, 29 . 0J aS a9 a9 [47.0, 47.0, 47.0, 47.0, 47 . 0, 47 . 0J [HelIo, HelIo, HelIo, HelIo, HelIo, HelIo] [HelIo, HelIo, HelIo, World, Wor l d, HelIo] al a2 a3 a4 a5 a6 true, true, * /// , Podemos rellenar la matriz completa o, como muestran las dos últimas instrucciones, rellenar tan solo un rango de elementos. 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 introducido 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 pueden 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; public class CountingGenerator public static class Boolean impleme nts Generator private boolean value = false¡ public java.lang.Boolean next() value = !value; II s610 salta hacia atrás y hacia adelante return value; public static class Byte implements Gen erator { private byte value = O; public java . lang.Byte next{) { return value++; 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 static char[] chars = (tlabcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJK.LMNOPQRSTUVWXYZ") . toCharArray () ; public static class Character implements Generator int index := -1; publ ie java .lang. Character next () { index = (index + 1) % chars .length; return chars[index]; public static class String implements Generator prívate int length = 7; Generator eg = new Character{); public String O () public String ( int length) ( this.length = length; ) public java .lang. String next () ( char[J buf = new char[length]; for(int i = O; i < length; i++) buf[i) = cg.nex tO; return new java.lang.String(buf); public static class Short implements Generator { prívate short value = O; public java.lang.Short next{) ( return value++; public sta tic class Integer implements Generator private int value = O; public java .lang. Integer next () { return value ++; public static class Long implements Generator ( private long value = O; public java .lang. Long next () ( return value++; public static class Float implements Generator private float value = O; public java.lang.Float next{) float result = value¡ value += 1.0; return result; public static class Double implements Generator private double value = 0.0; public java.lang.Double next {) double result = value; value += 1.0; return resul ti } ///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 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 manera 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.*¡ public class Generators Test public static int size = 10; public static void test (C lass surroundingClass) { for (Cl ass type : surroundingClass.getClasses(}) System.out . print (type.getSimpleName () + lO: "); try { Generator 9 = (Generator 16 Matrices 497 public java.lang.Boolean next() return r.nextBoolean() i public static class Byte implements Generator public java .lang. Byte next () return (byte)r .next lnt() { i public static class Character implements Generator public java .lang . Character next () { return CountingGenerator.chars[ r.nextlnt(CountingGenerator . chars.length)] i public static class String extends CountingGenerator.String /1 I nsertar el generador aleatorio de caracteres: ( eg = new Character(); } JI Inicializador de instancia public String () {} public String(int length) { super (length) ; public static class Short implements Generator public java .lang. Short next () { return (short)r .nextlnt (); public static class Integer implements Generator { private int mod = 10000 ¡ public Integer () {} public Integer (int modulo) { mod = modulo¡ } public java .lang. Integer next () { return r.nextlnt{mod); public static class Long implements Generator private int mod = 10000; public Long() {} public Long(int modulo) mod = moduloi } public java.lang.Long next() { return new java.lang.Long(r.nextlnt(mod)) i public static class Float implements Generator public java .lang. Float next () { JI Eliminar todos los decimales salvo los dos primeros: int trimmed = Math.round(r . nextFloat{) * 100); return (( float ltrimmed) J 100; public static class Double implements Generator< java .lang.Double> { public java .lang. Double next () { 498 Piensa en Java long crimmed = Math.round(r.nextDouble() return ((double)trirnrned) 1 100; * 100) i 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.*; public class RandomGeneratorsTest public static void main(String[1 args) GeneratorsTest.test(RandomGenerator.class) ; 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. Creación de matrices a partir de generadores Para tomar un objeto Generator y generar una matri z, necesitamos dos herramientas de conversión. La primera ut iliza cualquier generador para producir una matriz de subtipos Object. Para resolver el problema de las primitivas, la segunda herramienta toma cualquier matri z de tipos envolto ri o de primitivas y produce la matriz de primitivas asociada. La primera berramienta tiene dos opciones, representadas por un método estático sobrecargado que se denomina array( ). La primera ve rsión del método toma una matriz existente y la rellena utili zando un generador, mientras que la segunda versión toma un objeto Class, un generador y el número deseado de elementos y crea una nueva matriz, de nuevo reUenándola mediante el generador elegido. Observe que esta herramienta sólo produce matrices de subtipos Object y no pennite crear matrices de primitivas: 11 : net/mindview/util/Generated.java package net.mindview.util; import java . util .*; public class Generated 1/ Rellenar una matriz existente public static T(] array(T[] a, Generator gen) { return new CollectionData (gen, a.length) .toArray (a); 11 Crear una nueva matriz: @SuppressWarnings ("unchecked" ) 16 Matrices 499 public static cT> T[] array(ClasscT> type, GeneratorcT> gen, int sizel ( T [) a = (T[J ) java.lang.reflect.Array.newlnstance (type, sizel; return new CollectionDatacT> (gen, size) .toArray(a) i 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 segundo argumento del constmctor. Todos los subtipos de CoUection tienen un método toArray() que rellena la matriz argumento 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.*¡ public class TestGenerated { public static void main{String(] argsl Integer [) a = { 9, 8, 7, { 6 }; System.out.println(Arrays.toString{a)) ; a = Generated.array(a,new CountingGenerator.lnteger()) System.out.println(Arrays.toString{a)) i Integer[] b = Generated.array{Integer.class, new CountingGenerator.lnteger(), 15) i System.out.println(Arrays.toString{b)) i i 1* Output: [9, [O, [O, 8, 1, 1, 7, 2, 2, 6) 3) 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 sustinlye 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 primitivas. 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¡ public class ConvertTo { public static boolean(] primitive (Boo lean [] in ) boolean[] result = new boolean[in.lengthl ; for (int i = O; i < in .length i i++) result [il = in [i]; 11 Conversión automática return result ¡ public static char [] primitive (Character [] char [] result = new char [in .lengthl ¡ for(int i = O; i < in.length¡ i++} result[il = inri] i in) 500 Piensa en Java return result; public static byte [] primitive {Byte (] in) byte[] result = new byte [in.length] ; for(int i = O; i < in.length; i++) result (i] = in ti] ; return result; public static short [] primitive (Short [] in) short[] result = new short[in . length] i for(int i = O; i < in . length¡ i++) result (i] = in ti] ; return result; public static int[] primitive{Integer[] in) int [] result = new int [in.length] ¡ for{int i = O; i < in . length; i++) result(i] = inri] i return result; ( public static long [] primitive (Long (] in) long[) result = new long(in.length]; for(int i = O; i < i n. length; i++) result (i] = in ti] ; return result; public static float [] primitive {Float (] in) float[J result = new float(in . length]; f or(int i = O; i < in.length; i++) resu l t (i] = in [i] ; return result; public static double [] primitive {Double (] in) double[] result = new double[in.length]; for(int i = O; i < in.length; i++) result (i] = in [i] ; return resul t; ) /// , Cada versión de primitive() crea una matri z de primitivas apropiada con la longitud correcta, y luego copia los elementos desde la matri z in de tipos envoltori o. Observe que el mecanismo de conversión automáti ca entra en acción en la expresión: result ti] = in ti] ; 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 .* ; public class PrimitiveConversionDemonstration public static void main (String [] args) ( Integer[] a = Generated.array{Integer . class, new CountingGenerator.l nteger(), 15); int[] b = ConvertTo.pr imitive(a); System.out.println{Arrays . toString(bl) ¡ boolean[] e = ConvertTo . primitive{ Generated.array{Boolean . class, new CountingGenerator.Boolean(), 7»; System.out.println{Arrays . toString(c» ; 16 Matrices 501 / * Output: [O, 1, [true, 2, 3, false, 4, 5, 6, 7, 8, 9, 10, 11, 12, true, false, true, false, 13, 14] 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)) print ("al = 11 i + 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)) 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 i /* Output: al (true, false, a2 [104, a3 a4 a5 a6 a7 a8 true, false, false, true] -79, -76, 126, 33, -641 (Z, n, T, e, Q, r] [-13408, 22612, 15401, 15161, -28466, [7704, 7383, 7706, 575, 8410, 63421 [7674, 8804, 8950, 7826, 4322, 8961 [0.01, 0.2, 0.4, 0 .79, 0.27, 0.451 [0.16, 0.87, 0.7, 0.66, 0 .87, 0.591 -126031 *///,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 resultados. 502 Piensa en Java Ejercicio 13: (2) Rellene un objeto String utilizando CountingGenerator.Cbaracter. Ejercicio 14: (6) Cree una matriz de cada tipo primitivo y luego rellene cada matriz utilizando CountingGenerator. fmprima cada matriz. Ejercicio 15: (2) Modifique ContainerComparison.java creando un generador para BerylliumSphere y efectúe los cambios necesarios en main() para utilizar dicho generador con Generated.array( ). Ejercicio 16: (3) Comenzando con CountingGenerator.java, cree una clase SkipGenerator que produzca nuevos valores por el procedimiento de aplicar un incremento que se fijará de acuerdo con un argumento del constructor. Modifique TestArrayGeneration.java para demostrar que la nueva clase funciona correctamente, Ejercicio 17: (5) Cree y pruebe un objeto Generator para BigDecimal, y compruebe que funciona con los métodos Generated. Utilidades para matrices En java.util, podrá encontrar la clase Arrays, que contiene un conjunto de métodos estáticos de utilidad para matrices. Hay seis métodos básicos: equals(), para comprobar si dos matrices son iguales (y un método deepEquals() para matrices multidimensionales); ml(), del que ya hemos hablado en este capítulo; sort( ), para ordenar una matriz; binarySearch(), para encontrar un elemento en una matriz ordenada; toString(), para generar una representación de tipo String para una matri z; y hashCode(), para generar el valor hash de una matriz (veremos qué significa esto en el Capítulo 17, Análisis detallado de los contenedores), Todos estos métodos están sobrecargados para poder usarlos con todos los tipos primitivos y con objetos. Además, Arrays.asList( ) toma cualquier secuencia o matriz y la transfomla en un contenedor de tipo List; ya hemos hablado de este método en el Capítulo 11 , Almacenamiento de objetos. Antes de analizar los métodos de Arrays, existe otro método útil que no fOnTIa parte de Arrays. Copia en una matriz La biblioteca estándar de Java proporciona un método estático, System.arraycopy(), que permite copiar matrices de fonua bastante más rápida que se si utiliza un bucle for para reali zar la copia manualmente, System.arraycopy( ) está sobrecargado para aceptar todos los tipos. He aquí un ejemplo donde se manipulan matrices de valores ¡ot: JI : arrays/CopyingArrays.java 1/ Utilización de System . arraycopy() import java.util.*; import static net,mindview.util.Print. * ¡ public class CopyingArrays { public static void main(String[] args) { int[] i = new int[7]; int [] j = new int {lO] ; Arrays.fill(i, 47); Arrays.fill(j, 99); print ( " i = !I + Arrays. toString (i) ) ; print ( n j = " + Arrays. toStríng (j) ) ; System.arraycopy(i, O, j, 0, i.length ) i print ("j = " + Arrays. toStríng (j) ) i int[] k = new int[S]; Arrays.fill(k, 103); System.arraycopy(i, O, k, O, k.length); print("k = " + Arrays.toString(k)); Arrays.fill(k, 103); System . arraycopy (k, O, i , O, k.length} print ( " i = " + Arrays. toString (i) } i II Objetos, Integer[] u new Integer(lO] i Integer [] v new Integer(5] i i 16 Matrices 503 Arrays.fill Arrays.fill print ( "u = print ( "v =- (u, new Integer (47 ) (v, new I nteger (99 » " + Arrays . toString " + Arrays. toString ; ; (u ) ) ; (v » ; System. arraycopy (v, o , u, u.length/ 2, v.length ) i print ( "u = " + Arrays. toString (u ) ) ; / * Output: i j j [47, [99, k [47, i [103, u v u [47, [47 , [99, [47, 47, 99, 47, 47, 47, 99, 4 7, 47 , 47, 99, 47, 47, 47, 99, 47, 4 7J 47, 99, 47, 47J 99, 47, 99, 99, 99, 99, 103, 103, 103, 103, 47, 47J 47, 47, 47, 47, 47, 47, 47, 99, 99, 99J 47, 47 , 4 7, 99, 99 , 99, 99, 47, 99, 4 7, 99J 99J 47J 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 generará 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 exactamente 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étodo 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 .* ; public class ComparingArrays { public static void mai n (String[] argsl int [] al = new i nt[lO ] ; i nt[J a2 = ne w int[lOl ; Ar r ays.fill(al, 47 } ; Ar r ays. f ill(a2, 47 ) ; p r int {Arrays.equals (al, a2 )} ; a2 [3J { = 11; print (Arrays.equals (al, a2 ) ); String[J sI = new String(4]; Arrays.fill(sl, "Hi " ); String [] s2 = { new String ( IIHi JI) new Stri ng ("Hi" ) , new String ( UHi Jl ) , new String ( "Hi ll ) } ; print {Arrays.equals {51, s2 )) ; I 504 Piensa en Java / * 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 elementos, 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. Comparaciones de elementos de matriz Las operaciones de ordenación deben realizar comparaciones basadas en el tipo real de los objetos. Por supuesto, una solución consiste en escribir un método de ordenación distinto para cada uno de los posibles tipos, pero dicho código no será reutili zable para tipos nuevos. Uno de los objetivos principales del diseño en el campo de la programación consiste en "separar las cosas que cambian de las cosas que no lo hacen", y aq uí el código que no varía es el algoritmo de ordenación general, siendo la única cosa que cambia entre un uso y el siguiente la forma en que se comparan los objetos. Por tanto, en lugar de incluir el código de comparación en muchas rutinas de ordenación diferentes, se utiliza el patrón de diseño basado en estrategia2 . Con una Estrategia, la parte del código que varía se encapsula dentro de una clase separada, (el objeto Estrategia). Lo que se hace es entregar un objeto Estrategia al código que permanece invariable, el cual utiliza dicha Estrategia para implementar su algoritmo. De esa fonna podemos hacer que los diferentes objetos expresen diferentes formas de comparación y entregarles a todos ellos el mismo código de ordenación. Java dispone de dos maneras para proporcionar la funciona lidad de comparación. La primera es con el método de comparación "natural" que se añade a una clase implementando la interfaz java.lang.Comparable. Se trata de una interfaz muy simple con un único método, compareTo( ). Este método toma como argumento otro objeto del mismo tipo y produce un valor negativo si el objeto actual es inferior al argumento, cero si el argumento es igual y un valor positi vo si el objeto actual es superior al argumento. He aqui una clase que implementa Comparable e ilustra la comparabilidad empleando el método de la biblioteca estándar de Java Arrays.sort( ): // : arrays/CompType.java // Implementación de Comparable en una clase. import java.util.*; import net.mindview.util.*; import static net.mindview.util.Print.*; public class CompType implements Comparable int i; int ji private static int count = 1; public CompType(int nI, int n2 ) i nI; j = n2; public String toString () String result = [i = 11 11 + i + ", j = 11 + j + "]"; 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 == result += "\n H O) ¡ return result; public int compareTo(CompType rv) { return (i < rV.i ? -1 : (i == rV.i ? O private static Random r public static 1)); = new Random(47)¡ Generator{} { public CompType next{) { return new CompType(r.nextlnt{lOOl ,r.nextlnt(lOO)}; } }; public static void main (String [] CompType [] a args) { = Generated.array(new CompType[12] , generator()) i print ("befare sorting:") i print (Arrays,toString (a)) i Arrays.sort (a) ; print ( "after sorting: 11) i print{Arrays.toString(a)) ; / * Output: befare sorting: [[i = 58, j = 55], [ i = 9 3 , j [i 68, j O], [i = 22, j [i 89] , [i 51, j 9, j [i 58] , [i = 16, j 20 I j 61], [ i = 6 1 , j = 2 9 ] 7], [i = 88, j = 28] 78], [i = 98, j = 61] = 40], [i = 11, j = 22] after sorting: [[i = 9, j 78], [i [i 20, j 58], [i 58, j 55], [i 88, j 28], 22], [i = 16, j = 40] 7], [i = 51, j = 89] 29], [i 68, j O] 61], [i = 98, j = 61] j 22, 61, 93, = 11, [i [i [i j j j * /// > 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 11: arrays / ReverSe.java II El comparador Collections.reverseOrder() import java.util.*; import net.mindview.util.*¡ import static net.mindview.util.Print.*; public class Reverse { public static void main(String(] args ) { CompType[J a = Generated.array ( new CompType[12], CompType.generator()); print ("before sorting:" ) i print (Arrays . toString (a )) ; Arrays.sort (a, Collections.reverseOrder( )) ¡ print ( "after sorting:"); print (Arrays.toString(a)) ¡ 1* Output: before sorting: [ [i [i [i [i ~ 58, 68, 51, 20, i 551 , [i 01 , [i 891 , [i 581 , [i ~ i i i ~ ~ ~ 93, 22, 9, 16, i i i 93, 61, 22, i i 611 , [i 71 , [i 781, [i 401 , [i ~ ~ i ~ ~ 61, 88, 98, ~ i i ~ ~ i ~ i 11, 291 281 611 221 ~ after sorting: [ [i [i [i [i ~ 98, 68, 51, 16, i 611 , [i 01 , [i 891 , [i ~ i i i 40] , [i ~ ~ ~ ~ i i 11, 611, [i 291 , [i 71 , [i 221 , [i ~ i i i i 88, 58, 20 , 9, 281 551 581 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. * ; class CompTypeComparator implements Comparator public int compare(CompType 01, CompType 02) { return (0 1. j < 02. j ? -1 : (0 1. j == 02. j ? O : 1)) ¡ public class ComparatorTest { publ ic static void main (String (1 args ) { CompType(] a = Generated.arra y( new CompType[12] , CompType.generator()) print ("before sorti ng: ") ¡ print(Arrays.toString(a)) ; Arrays.sort(a, new CompTypeComparator())¡ print (" a fter sorting: " ); print(Arrays . toString(a)) ¡ i 1* Output : befare sorting : [[i ~ 58, [i ~ 68, i ~ i ~ 551, 01, [i [i 93, 22, i i 611, [i 71, [i ~ ~ 61, 88, i i ~ ~ 291 281 16 Matrices 507 [i [i 51, 20, j i 89], 58], after sorting: [ [i = 68, i = O] , [i [i 88, i 28]. 55] , 58, i li 61] , 98, li 9, j 16, [i [i = [i [i [i 22, i 61, 20, 9, = 78], i = 40], [i [i = 98, i = 61] = 11, j = 22] 7] , [i = 29] , = 581 , = 78] , = 11, i i i i = 22] li = 16, i = 40] li = 93, i = 61] li = 51, i = 89] * ///0Ejercicio 21: (3) Trate de ordenar una matri z de objetos del Ejercicio 18. Implemente Comparable para corregir el problema. Ahora cree un objeto Comparator para disponer los objetos en orden inverso. Ordenación de una matriz Con los métodos de ordenación predefinidos, podemos ordenar cualquier matriz de primitivas o cualquier matriz de objetos que implementen Comparable o dispongan de un objeto Comparator asociado. 3 He aquí un ejemplo que ge nera objetos Stri ng aleatorios y los ordena: /1 : arrays/StringSorting.java // Ordenación de una matriz de objetos String. import java.util.*; import net.mindview.util. *¡ import static net.mindview.util.Print.*¡ public class StringSorting { public static void main (S tring[] args) String(] sa = Generated.array{new String[20], new RandomGenerator.String {Sl l; print ( "Before sort: " + Arrays. toString (sal 1 ; Arrays.sort (sa) ; print ( "After sort: ti + Arrays. toString (sa) ) ; Arrays.sort (sa, Collections.reverseOrder() ) ; print ( "Reverse sort: " + Arrays. toString (sal) ; Arrays.sort (sa, String.CASE_INSENSITIVE_ORDER ) ; print ( nCase-insensitive sort: u + Arrays. toString (sal) i /* 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 ordenació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úsculas 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 óptima para el tipo concreto que se esté ordenando: un algoritmo Quicksort para primitivas y un a ordenación estable por combinació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. Búsquedas en una matriz ordenada Una vez ordenada una matriz, podemos real izar una búsqueda rápida de un elemento concreto invocando Arrays. binarySearch( ). Sin embargo, si tratamos de utilizar binarySearch( ) en una matri z desordenada los resultados serán imp redecibles. El siguiente ejemplo utiliza un objeto RandomGenerator.lnteger para rellenar una matriz y luego emplea el mismo generador para producir valores de búsqueda: 11: arrays/ArraySearching .java II Utilización de Arrays.binarySearch{) . import java.util. *; import net.mindview.util.*¡ import static net.mindview.util.Print.*¡ public class ArraySearching { public static void main(String(] args) Generator gen = new RandamGeneratar.lnteger(lOOO); int[J a = CanvertTa . primitive( Generated.array(new Integer(25] , gen» ¡ Arrays. sort (a) ¡ print( "Sorted array: " + Arrays.toString(a» while (true) ¡ { int r = gen.next() ¡ int location = Arrays.binarySearch(a, r); if(location >= O) { print("Location af " + r + " is " + lacation + ", a [" + lacatian + "J = " + a [location); break; 11 Salir del bucle while 1* Output: Sorted array' [128, 140, 200, 207, 258, 258, 278, 589,693,704,809,861,861,868,916,961,9981 Location of 322 is 8, a[8] = 288, 322, 429, 511, 520, 522, 551, 555, 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á manteniendo 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 encontrará. El algoritmo de búsqueda no está diseñado para soportar elementos duplicados, aunque sí que los tolera. Si necesitamos 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 ordenaciones 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.*¡ public class AlphabeticSearch public static void main (Str ing [) args) { String[} sa ~ Generated.array(new String[30] , new RandomGenerator.String(5)); Arrays.sort{sa, Strlng.CASE INSENSITIVE ORDER), System.out.println (Arrays. toString (sa) ) ; int index = Arrays.binarySearch(sa, sa[lO], String.CASE_INSENSITIVE_ORDER) i System . out.println("Index: 11+ index + n\n"+ sa(indexl) i / * 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 enden a ser preferibles a las matrices desde todos los puntos de vista, salvo el de rendimiento. Incluso en lo que al rendimiento 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 sencillo 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 dispone 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) # 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] class MyList (list): # Heredar de una lista # Definir un método, puntero 'this' es explícito: def getReversed{self) : reversed = self[:J # Copiar lista utilizando fragmentos reversed.reverse () # Método predefinido de la lista return rever sed list2 MyList{aList) # No hace falta 'new' para crear objetos print type{list2) # print list2.getReversed() # [8, 7, 6, 5, 4, 3, 2, 1] #,- La sintaxis básica de Python se ha presentado en el capítulo anterior. En este ejemplo, se crea una lista simplemente insertan do una secuencia de objetos separados por comas entre corchetes. El resultado es un objeto cuyo tipo en tiempo de ejecució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 especi 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 bereda 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 ecució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 dejaran 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 encontraremos 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 presentaron 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 versió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. Taxonomía completa de los contenedores La sección "Resumen" del Capínilo 11 , Almacenamiento de objetos, mostraba un diagrama simplificado de la biblioteca de contenedores de Java. He aquí un diagrama más completo de la biblioteca de colecciones, incluyendo las clases abstractas y los componentes heredados (con la excepción de las implementaciones de Q ueue) : Genera ~-I-t- - -t --~ , era or Genera ~-C--II--t· --~ ,-4-------------, ~--t¡---4 • -------. ._L_ • • _l_ .. • _.J __ • 'Listlterator ,..-_ql!n-e!·L-' List' , , .- - ---- - - " .________ : AbstraetCollection : ~~ .. ~ . --.& ---------~t>. I I Set , ,'Queue ,' I ·A-e, )J, ,,1. __ J_ ---" : SortedSet : ~q --- ..~.. • .--e,--- - ---- : AbstraetSet : " ____ 4 : , i_ • AbstractMap : • • _ L __ ______ • : SortedMap -- --- ._----("--- . .__ L_____ .,:.:' \ ____ /~ __ .~---: AbstractList: ap ~-~---tr-4 r-----~----, • _____________ ~--M-----~ o ee Ion ,... ------------, ~ - -- - -- - . ____ 4 I TreeMap I I HasMap I f I LinkedHashMap I IWeakHashMa pi /ldentilYHaShMap Hashtable (heredado) I Colleetions Arrays Taxonomía completa de los contenedores I 514 Piensa en Java Java SES añade: • La interfaz Queue (para implementarla se ha modificado LinkedList como vimos en el Capítulo 11. A/macenomienro de objetos) y sus implementaciones PriorityQueue (cola con prioridad) y diversas variantes de BlockingQueuc (cola bloqueante) que se verán en Capitulo 21. Concurrencia. • Una interfaz ConcurrentMap y su implementación ConcurrentHashMap (mapa hash concurrente), que también se utiliza para el mecanismo de hebras de programación que se aborda en el Capinllo 21. Concl/rrencia. • CopyOnWríteArrayList y CopyOnWriteArraySet. también para concurrencia. • EnumSet y EnurnMap. implementaciones especiales de Set y Map para utilizar con enumeraciones, como se explica en el Capítulo 19, Tipos enumerados. • Varias utilidades en la clase Collections. Los recuadros de trazos representan clases abstractas y podemos ver varias clases cuyos nombres comienzan por "Abstraet". Estas clases pueden resultar un poco confusas al principio, pero son simplemente herramientas que implementan parcialmente una interfaz conc reta. Si estuviéramos creando nuestra propia clase Set. por ejemplo, no comenzaríamos con la interfaz Set e implementaríamos todos los métodos; en lugar de ello heredaríamos de AbstractSet y haríamos el trabajo rnín.imo necesario para definir nuestra nueva clase. Sin embargo, la biblioteca de contenedores contiene la suficiente funcionalidad como para satisfacer nuestras necesidades casi siempre, por lo que nom1almente podemos ignorar cualquier clase que comience con "A bstract". 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 versió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) public String toString ( ) { return super. toString ( ) + { this. s 11 Si} + S; public class FillingLists { public static void main (String [] args) { List list= new ArrayList( Collections.nCopies(4, new StringAddress("Hello"))) i System.out.println(list) i Collections.fill(list, new StringAddress("World!")); System.out.println{list) i / * 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 reprePodemos ver. analizansentación hexadecimal sin signo del código hash del objeto (generada por un método hashCode( 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. ». Una solución basada en generador Casi todos los subtipos de CoUection tienen un constructor que toma como argumento otro objeto Collection, a partir del cual puede rellenar el nuevo contenedor. Por tanto, para poder crear fácilmente datos de prueba, todo lo que necesitamos es construir una clase que tome como argumentos del constructor un objeto Generator (definido en el Capítulo 15, Genéricos, y analizado con más detalles en el Capítulo 16, Matrices) y un valor quantít)' (cantidad): 11: net/mindview/util/CollectionData.java II Una colección rellena con datos utilizando un objeto generador. package net.mindview.utili import java . util.*; public class CollectionData extends ArrayList { public CollectionData(Generator gen, int quantity} for {int i = Di i < quantitYi i++} addlgen.nextl)) ; } /1 Un método genérico de utilidad: public static CollectionData list(Generator gen, int quantity) return new CollectionData (gen, quantity); 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 instancia. También puede utilizarse el método addAU() que fonma parte de todos los subtipos de Collection para rellenar un objeto 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. *¡ class Government implements Generator { String() foundation = ("strange women lying in ponds + "distributing swerds is no basis fer a system of " + "government tl ) .split(" 11 ); private int indexi public String next() { return foundation[index++] ¡ } 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 espiritu de este palrón. 516 Piensa en Java public class CollectionDataTest { public static void main (String [] argsJ { Set set = new LinkedHashSet( new COllectionData(new Government(), 15)); /1 Utilización del método de util i dad: set . addA l l{CollectionData.list(new Governmen t {), 1 5)); Systern.out.println(set) ; /* 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 enlazada 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. * ¡ public class CollectionDataGenerat i on public static void mai n (String [] args) System . out.println(new ArrayList( CollectionData.list( // Convenience method new RandomGenerator . String(9) , 10))) ¡ Sy stem . out.p r intln(new Has hSet( new CollectionData( new RandornGenerator . lnteger(), la})) i / * 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¡ public class Pair { public final K keYi pub l ic final V va l ue¡ public Pair(K k, V v) key = k; value = Vi } /// : Los campos key (clave) y v. lue (valor) son de tipo público y final , de modo que Paí .. se convierte en un objeto de transferencia de dalos de sólo lectura (o mensajero). 17 Análisis detallado de los contenedores 517 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. * ; public class MapData extends LinkedHashMap { // Generador de un único par: public MapData {Generator p i = < quantity; gen.next () ; put(p.key, p.value) i++ ) { i 11 Dos generadores separados : public MapData(Generator genK, Generator genV, int quantity ) { for ( int i = O; i < quantity; put (genK , next () i++ } genV . next () ); I } /1 Un generador de clave y un único valor: public MapData (Generator genK, V value, for {int i "" O; i < quantitYi i++ ) { put {genK.next () , value ) ; int quantity) { } II Un iterable y un generador de valor: public MapData ( Iterable genK, Generator genV ) { for ( K key , genK) { put (key, genV . next ( » i } II Un iterab le y un único valor: public MapData (Iterable genK, V value ) { for ( K key genK ) { put (key, value ) ; II Mé t odo s genéricos de ut i lidad: public static MapData map (Generator (gen, quantiey ) i public s tatic MapData map (Generator genK, Generator genV, int quantity ) return new MapData (genK, genV, quantity ) i public static MapData map (Generator genK, V v alue, int quantity) { return new MapData {genK, value, quantity); public static MapData map ( Iterable genK, Generator genV ) return new MapData (genK, genV ) ; public static MapData 518 Piensa en Java map (Iterable genK, V value) return new MapData (genK, { valuel i ) ///,Esto nos permite decidir si queremos utili zar un único objeto Generator { private int size = 9; private int number = 1; private char letter = 'A'¡ public Pair next() return new Pair( n umber++, "" + letter++) i public Iterator iterator() return new Iterator () { public Integer next () { return number++; public boolean hasNext () { return number < size¡ public void remove{) { throw new UnsupportedOperationException(); } }; public class MapDataTest { public static void main{String[] args) { II Generador de un par: print(MapData.map(new Letters(), 11)) i II Dos generadores separados: print{MapData.map(new CountingGenerator . Character(), new RandomGenerator.String{3), 8)) ¡ II Un generador de clave y un único valor: print{MapData . map(new CountingGenerator . Character(), "Value",6)); II Un Iterable y un generador de valor : print(MapData.map(new Letters(), new RandomGenerator.String(3))) ¡ II Un Iterable y un único valor: print(MapData.map(new Letters(), "Pop")) ¡ } / * Output , {l=A, 2=B, 3=C, 4=D, 5=E, 6=F, 7=G, {a=YNz, b=brn, c=yGc, d=FOW, e=ZnT, {a=Value, b=Value, c=Value, d=Value, {1 =mJM, 2=RoE , 3=suE, 4=cUO, 5=neO, {l=Pop, 2=Pop, 3=Pop, 4=Pop, 5=Pop, * /// ,- 8=H, 9=I, lO=J, ll=K} f=cQr, g =Gse, h=GZM} e=Value, f =Value} 6=EdL, 7=smw, 8=HLG} 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 inicializar 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. Utilización de clases abstractas Una solución alternati va al problema de generar datos de prueba para contenedores consiste en crea r imp lementac iones perso nalizadas de Collectio n y Ma p. Cada contenedor de j av a.u t il tien e su propia clase abstracta que proporciona una implementac ión parcia l de di cho contenedor, por lo que lo único que hace falta es implementar los métodos necesari os para obtener el contenedor deseado. Si el contenedor resultante es de sólo lectura, como suele suceder para los datos de prueba, el número de métodos que es necesari o proporcionar se minimi za. Aunque no resulta parti cul anneme necesari o en este caso, la sigui ente solución también proporciona la oportu nidad de ilustrar otro patrón de di seño: el patrón de di seño denominado Peso mosca. Utilizamos este patrón de di se ño cuando la solución nonn al requiere demas iados objetos, o cuando la producción de objetos nonnales requi ere demasiado espac io. El patrón de di seño Peso mOSCa ex te mali za parte del objeto de modo que, en lugar de que todo lo del objeto esté contenido en el propio objeto, parte del objeto o la totalidad del mismo se busca en una tabla externa, más eficient e (o se genera medi ant e algún otro cálcul o qu e perm ita ahorrar espacio). Un aspecto important e de este ejemplo consiste en demostrar lo relat ivament e simple que es crear sendos objetos Ma p y Colleetion perso nalizados heredando a partir de las clases de java.utiJ.Abstraet. Para crear un mapa Map de sólo lectura hereda mos de Abstract M ap e implementamos entrySe t( ). Para crear un conjunto Set de sólo lecru ra, hereda mos de Abs tr ae tSet e implementamos iter a to r( ) y size( ). El conjunto de datos de este ejemplo es un mapa de los paises del mundo y sus capitales2 El método ea pitals() genera un mapa de paises y capitales. El método na mes() genera una lista de los nombres de paises. En ambos casos, podemos obtener un listado parcial proporcionando un argumen to int que indique el tamaño deseado: // : net / mindview / util / Countries.java // Mapas y listas nPeso mosca " de datos de ejemplo. package net.mindview.util¡ import java.util .* ; import static net . mindvi e w.util.Print.* ¡ public class Countries { public sta tic final String [J [J DATA = { // Áf r ica {"ALGERIA", IIAlgiers"}, {"ANGOLA II , "Luanda"}, {"BENIN", " Po rto-Novo"}, {"BOTSWANA", 11 Gaberone" }, { "BULGARIA", "Sof ia"}, { " BURKINA FASO " , 11 Ouagadougou " }, { "BURUNDI", "Bu j umbura " }, { II CAMEROONII , "Yaounde"}, {"CAPE VERDE", IIpraia ll } , {"CENTRAL AFRICAN REPUBLIC", "Bangui"}, {"CHAO", "N I djamena"}, { " COMOROS II , "Moron i "}, { 11 CONGO" , "Brazzaville"}, { " DJIBOUTI 11 , "Dij ibouti" }, { "EGYPT", II Cairo "}, { "EQUATORIAL GUINEA", "Malabo"}, { "ERITREA", IIAsmara"} , {"ETHIOPIA","Addis Ababa"}, {"GABON" , IILibreville"} , { " THE GAMBIA II ,"Banjul"}, { "GHANA", "Acera"}, {"GUINEA", "Conakry"}, {"BISSAU", "Bissau"}, {"COTE D' IVOIR ( IVORY COASTl ", "Yamoussoukro"}, { "KENYA" , "Nairobi"}, {"LESOTHOII, "Maseru " } , { "LIBERIA " , "Monrovia"}, {"LIBYA 11 , IITripoli " } , {"MADAGASCAR " , "Antananarivo " }, {"MALAWI" , 11 Lilongwe " }, { "MALI " , 11 Bamako 11 } , { "MAURITANIA 11 , "Nouakchot t " } , { "MAURITIUS", " Po r t Louis " }, { "MOROCCO ", "Rabat " }, 2 Estos datos se han extraído de Internet. A lo largo del tiempo diversos lectores me han enviado correcciones. 520 Piensa en Java {"MOZAMBIQUE", "Maputo"}, {"NAMIBIA", "Windhoek"}, {"NIGER", ItNiamey"}, {"NIGERIA", "Abuja"}, {"RWANDA", "Kigali"}, {"SAO TOME E PRINCIPE" "Sao Tome"}, {IISENEGAL", "Dakar"}, {"SEYCHELLES II , "Victoria"}, {"SIERRA LEONEII, tlFreetown"}, {"SOMALIA", "Mogadishu"}, { "SOUTH AFRICA 11 , "Pretoria/Cape Town 11 } , {"SUDAN", "Khartoum"}, I {"SWAZlLANDII,IIMbabane"}, {IITOGO II , "Lome"}, {"TANZANIA". "Dedama"}, {IITUNISIA" "Tunis"} I {IIUGANDA", "Kampala"}, {"DEMOCRATIC REPUBLIC OF THE CONGO I (ZAIRE) ", "Kinshasa") , {"ZAMBrA", "Lusaka"}, { "ZIMBABWE", "Harare"}, II Asia { "AFGHANISTAN" I "Kabul " } I { " BAHRAIN" , "Manama " } , {"BAN'GLADESH " , "Dhaka II}, {"BHUTAN", "Thimphu 11 } {uBRUNEI II ,"Bandar Seri Begawan"}, {"CAMBODIA", IIPhnom Penh"}, { " CHINA " , "Beij ing" }, {"CYPRUS ", "Nicosia"} , {" INDIA" ,"New Delhi u}, {" INDONESIA u, "Jakarta" } , {"IRAN", "Tehran"} , {"IRAQ","Baghdad ll } , {" ISRAEL", "Jerusalem" }, {"JAPAN", "Tokyo"}, {"JORDAN", "Amman"}, {"KUWAIT", "Kuwait City"}, {"LAOS" , "Vientiane" }, { " LEBANON" , "Beirut" } , {"MALAYSIA ", "Kuala Lumpur"}, { "THE MALDIVES", "MaIe"}, {HMONGOLIA", "UIan Bator"}, {"MYANMAR {BURMA) ", "Rangoon H}, {"NEPAL", "Katmandu"}, {"NORTH KOREA", "p'yongyang"}, {"OMAN", "Muscat"}, {"PAKISTAN", "Islamabad"}, { "PHILIPPINES" , "Manila" }, {IIQATAR", "Doha"} , {"SAUOI ARABIA", "Riyadh" }, {"SINGAPORE", "Singapore"}, { "SOUTH KOREA"," Seoul " }, {" SRI LANKA", " Colombo" } , {"SYRIA", "Damascus"}, {"TAIWAN (REPUBLIC OF CHINA}", "Taipei"), {"THAlLAND", "Bangkok"}, { "TURKEY", "Ankara"}, {"UNITED ARAB EMlRATES","Abu Dhabi ll } , {"VIETNAM", "Hanoi"}, {"YEMEN", "Sana ' a"}, II Australia y Oceania {"AUSTRALIA" , "Canberra"}, {" FIJI" , "Suva" } , {" KIRIBATI" , "Bairiki"} , {"MARSHALL ISLANDS", "Dalap-Uliga-Darrit"}, { "MICRONESIA 11 , "Pal ikir"}, { "NAURU" , " Yaren"} , {"NEW ZEALAND", "Wellington ll } , {"PALAU", "Koror"}, {"PAPUA NEW GUINEA", "Port Moresby"}, {" SOLOMON ISLANDS", "Honaira"}, {"TONGA", "Nuku' alofa"} , {"TUVALU", "Fongafale"}, {"VANUATU", "< Port-Vila"}, {"WESTERN SAMOA", "Apia"}, II Europa del Este y la Unión Soviética {"ARMENIA", "Yerevan"}, { "AZERBAIJAN", "Baku"}, {"BELARUS (BYELORUSSIA) ", "Minsk"), {"GEORG lA" , "Tbilisi"} , {"KAZAKSTAN", "Almaty"} , {"KYRGYZSTAN", HAlma-Ata"}, {"MOLDOVA", "Chisinau"}, {"RUSSIA", "Moscow"}, {"TAJIKISTAN", "Dushanbe ll } , {"TURKMENISTAN", "Ashkabad"}, {"UKRAINE", "Kyiv"}, { IIUZBEKISTAN", "Tashkent"}, I II Europa {" ALBANIA" , "Tirana" } , {" AUSTRIA" , "Vienna" } , {"ANDORRA", 11 Andorra la Vella"}, {"BELGIUM", IIBrussels"}, 17 Análisis detallado de los contenedores 521 {"BOSNIA", II_II}, {IIHERZEGOVINA", "Sarajevo"}, { "CROATIA" , 11 Zagreb"}, {"CZECH REPUBLIC"," Prague" } I { 11 DENMARK 11 I "Copenhagen 11 } I { " ESTONIA 11 I "Tall inn 11 } I {" FINLAND" , {"GERMANY", {"HUNGARY", {ltIRELAND" , "Helsinki" }, {" FRANCE 11 , "Pa ris"} , "Berlin"}, {"GREECE", "Athens ll } , "Budapest"} I {"ICELAND", "Reykjavik"}, "Dublin"} , {"ITALY" , "Rome"}, {"LATVIA ", "Riga"}, {"LIECHTENSTEINII, "Vaduz"} I {" LITHUANIA ", "Vilnius"}, {"LUXEMBOURG", "Luxembourg"}, {"MACEDONIA" , 11 Skopj e t1} t {"MALTA 11 , "Valletta" } , {"MONACO" "Monaco"}, {"MONTENEGRO" I 11 Podgorica "} I {"THE NETHERLANDS", "Amsterdam"} {"NORWAY", "Oslo"} I I I {"POLAND" "warsaw"} I {"PORTUGAL", "Lisbon" }, {"ROMANIA ti, "Bucharest"}, {uSAN MARINO", 11 San Marino"}, {IISERBIAu, IIBelgrade ll }, {IISLOVAKIAu, IIBratislava"}, {"SLOVENIA", "Ljuijana u }, {"SPAIN", "Madrid"}, {"SWEDEN", "Stockholm"}, {"SWITZERLAND", "Berne"}, {"UNITED KINGDOM" , ULondon u } , {"VATICAN CITY","--_u}, 1I América del Norte y América Central { "ANTIGUA ANO BARBUDA"," Saint John' Sil} , {"BAHAMAS", "Nassau"}, {"BARBADOS", IIBridgetown" }, {"BELIZE ", uBelmopan"}, { "CANADA", "Ottawa"}, {"COSTA RICA", "San Jose ll }, {"CUBAI!, IIHavana"}, {"DOMINICA u , "Roseau" }, {"DOMINICAN REPUBLIC " , IISanto Domingo"}, {"EL SALVADOR", 11 San Salvador"}, { "GRENADAII , "Saint George' s''}, {uGUATEMALA", "Guatemala City"}, {uHAITI", UPort-au-Prince"}, {"HONDURAS ", "Tegucigalpa"}, {IIJAMAICA II , "Kingston"}, {uMEXICO",IIMexico City"}, {IINICARAGUA " "Managua" }, {UPANAMA", " Panama Cityll}, {"ST. KITTS", u_,,}, {"NEVIS", "Basseterre"}, {"ST. LUCIA", "Castries ll } , {"ST. VINCENT ANO THE GRENADINES", "Kingstown u }, {"UNITED STATES OF AMERICAu, "Washington, D. C."}, II América del Sur {" ARGENTINA" , "Buenos Aires"}, {"BOL IVIA", "Sucre (legal) ILa Paz (administrative) u}, {uBRAZILII, "Brasilia u }, { " CHILE", "Santiago"}, {uCOLOMBIA", "Bogota"} I {"ECUADOR", "Quito"}, {"GUYANA II , "Georgetown"}, { "PARAGUAY", "Asuncion"}, {II PERU" 11 Lima "}, {u SURINAMEII , u paramaribo"} , {"TRINIDAD ANO TOBAGO", "Port of Spain"}, {"URUGUAY", "Montevideo"}, {"VENEZUELA", "Caracas lI } , I I I }; 11 Utilización de AbstractMap implementando entrySet() private static class FlyweightMap extends AbstractMap ( private static class Entry implements Map.Entry int index¡ Entry(int index) { this.index = index¡ public boolean equals (Object o) { return DATA[index] [O] .equals(o); public String getKey () { return DATA [index] [O]; } public String getValue () ( return DATA [index] [1] ; public String setValue(String value) { throw new UnsupportedOperationException(); 522 Piensa en Java public int hashCode () ( return DATA [index] [O] .hashCode(); JJ Utilizar AbstractSet implementando size{) e iterator() static class EntrySet extends AbstractSet DATA.length) this. size DATA.length¡ else size¡ this.size public int size () { return size; } private class lter implements lterator next() entry.index++; return entrYi public void remove() throw new UnsupportedOperationException(); public lterator select (final int size) { return new FlyweightMap () { public Set map = new FlyweightMap {); public static Map capitals () { return map; JJ El mapa completo public static Map capitals (int size) return select(size) i JI Un mapa parcial { 17 Análisis detallado de los contenedores 523 sta tic List names ~ new ArrayList(map.keySet()); JI Todos los nombres: public statie List names{) return names; JI Un lista parcial: public statie List names (int size) { return new ArrayList (select (size) . keySet () ) i public statie void main (String [] args) { print(capitals{lO)) ; print (names (10)) i print{new HashMap{capitals(3))); print{new LinkedHashMap(capitals(3))); print{new TreeMap(capitals(3))); print(new Hashtable(capitals(3))); print(new HashSet (names (6) )) i print(new LinkedHashSet(names(6})); print(new TreeSet (names (6) )) i print(new ArrayList(names(6))) i print(new LinkedList(names(6))) i print (capi tal s () . get ("BRAZIL") ) i } /* 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 almacena su índice, en lugar de almacenar la clave y el valor reales. Cuando invocamos getKey() o getYalue(), utiliza el índice 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 siguiente 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 contenedores 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.*; public class CountinglntegerList extends AbstractList { priva te int size; public CountinglntegerList ( int size ) this.size = size < O ? O : size¡ public Integer get (int index ) return Integer.valueOf (index ) ; public int size () { return size; public static void main (String[] args ) System.out.println(new CountinglntegerList (30 )) ; 1* Output: [O, 17, 1, 2, 3, 4, 18, 19, 20, 5, 6, 7, 8 , 9, 10, 11, 12, 13, 14, 15, 21, 22, 23, 24, 25, 26, 27, 28, 29] 16, * /// ,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.*¡ public class CountingMapData extends AbstractMap private int size; private static String[] chars "A BCD E F G H 1 J K L M N O P Q R S T U V W X y Z" .split ( " " ) ; public CountingMapData(int size) if {size < O) this.size = O; this.size = size; private sta tic class Entry implements Map.Entry int index¡ Entry (int index ) { this. index =: index; public boolean equals (Object o ) { return Integer. valueOf (index ) . equals (o ) ; public Integer gecKey () { return index; } public String getValue ( ) { return chars[index % chars . length) + Integer . toString{index I chars.length ) ; 17 Análisis detallado de los contenedores 525 public String setValue (String value ) { throw new UnsupportedOperationException () ; public int hashCode ( ) { return Integer. valueOf (index) . hashCode () i public Set iterator( ) Devuelve un objeto lterator 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 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 Cosas que se pueden hacer con todas las colecciones. import java.ut i l.*¡ import net.mindview.util.*¡ import static net.mindview.util.Print.*¡ II public elass CollectionMethods { publie static void main (String[] args ) Collection e = new ArrayList ( ) ¡ e.addAII (Countries . names (6 }) ¡ c.add("ten ll ) ¡ e.add("e l even") ¡ p:r'int (c) ¡ II Hacer una matriz a partir de la lista: Object[] array = c.toArray()¡ II Hacer una matriz de objetos String a partir de la lista: String[] str = c.toArray(new String[O] ) ¡ II Enontrar elementos max y min elements¡ esto significa II diferentes cosas dependiendo de la forma II en que esté implementada la interfaz Comparable: print (IICollections . max (e ) + Collections. max (e ) ) ; print("Co lle e tions.min (c ) = " + Collectio ns.min (c )) ¡ 17 Aná lisis detallado de los contenedores 527 JI Añadir una colección a otra colección Collection c2 = new ArrayList () ; c2.addAll (Countries.names (6 )) ; e .addAll ( e2) ; print (e ) i C.remove ( Countries . DATA [O] [O] ) i print(c } ; c. remove (Countries.DATA [1] [O)); print(c) ; /1 Eliminar todos los componentes contenidos // en la colección proporcionada como argumento: c.removeAll(c2 ) ; print (e ) ; e. addAll ( e2 ) ; print (e ) ; // ¿Está un elemento en esta colección? String val = Countries.DATA[3J [O] ; print ("c. contains ( " + val + It ) = It + c. contains (val ) ) ; 1/ ¿Está una colección dentro de esta colección? print ( "e. containsAll (c2 ) = Collection c3 ti + c. containsAll ( c2 ) ) ; = (( List () ; c.addAll (Countries.names (6 )) ; print (e ) ; c . clear () ; 11 Eliminar todos los elementos print ( "after c.clear () :" + e ) ; 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 funciona s . Además, la mayoría de los métodos que toman un contenedor Collection como argumento sólo leen de dicha colecció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 describir 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 excepció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. 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". 4 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) genere 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.*; public class Unsupported static void test (String msg, List list) System.out . println{ " --- " + msg + 1I ___ 1'); Col l ection e = list; COllection subList = l ist . subList(l,S) i /1 Copia de la sublista: Collection c2 = new ArrayList (subList) ; try ( c.retainAll (c2); ) eateh( Exeeption e) ( System.out . println("retainAll() : " + e) i t r y { c. removeAl l (c2); } catch ( Exc e ption e) System.out . p r intln("removeAll() : 11 + e) ; t ry ( e.elear() ; } eateh(Exception e) ( System.out.println("clear{): " + e); try ( e . add (" X" ); } eateh (Exeeption e) System.out.println( " add(): n + e); t ry ( e . addAll(e2); } eate h (Exeeption e) Sy stem.out.println( " addAll(): " + e) ; try { c.remove("C " ); } catch(Exception e) System . out.println( " remove(): " + e); // El método List.set() modifica el valor pero no // cambia el tamaño de la estructura de datos: try ( list.set(O, "X") i catch(Exception e) System.out.println( " List.set(): " + e) i public static void main(String[] args) { List list = Arrays.asList{I' A B e o E F G H I J K L ". split(" "); test ( "Modifiable Copy", new ArrayList (list» ; test (l'Arrays.asList () " , l ist); test ( " unmodifiableList()11, Collections . unmodifiableList( new ArrayList(list») i / * Output: --- Modif i able Copy - -- - - Arrays. asList () - -reta i nAll (): java . lang. Un supportedOperat i onException removeAll(): java.lang.UnsupportedOperationException 530 Piensa en Java clear(): java.lang UnsupportedOperationException add(): java.lang.UnsupportedOperationException addAll(): java.lang.UnsupportedOperationException remove(): java.lang.UnsupportedOperationException --- unmodifiableList () --retainAll (): java. lang. UnsupportedOperationException removeAll{): java.lang.UnsupportedOperationException clear(): java.lang.UnsupportedOperationException add(): java.lang.UnsupportedOperationException addAll (): java .lang. UnsupportedOperationException remove(): java.lang.UnsupportedOperationEx ception List . set(}: java.lang.UnsupportedOperationException '/1/,Como Arrays.asList( ) produce un contenedor List que está respaldado por una matriz de tamaño fijo. tiene bastante sentido que las únicas operaciones soportadas sean aquellas que no cambien el famailo de la matriz. Cualquier método que provoque un cambio del tamaño de la estructura de datos subyacente generará una excepción UnsupportedOperationExeeption, para indicar que se ha producido una llamada a un método no soportado (un error de programación). Observe que siempre podemos pasar el resultado de Arrays.asList() como argumento de un constructor de cualquier colección (o utilizar el método addAll() o el método estático Collections.addAll()) para crear un contenedor nonnal que permita el uso de todos los métodos; esta técnica se muestra en la primera llamada a test() de main(). Dicba llamada produce una nueva estructura de datos subyacente de tamaño variable. Los métodos "no modificables" de la clase Colleetions envuelven el contenedor en un proxy que genera una excepción UnsupportedOperationException si se realiza cualquier operación que modifique el contenedor de alguna romla. El objetivo de utilizar estos métodos es producir un objeto contenedor "constante". Posteriomlente, describiremos la lista completa de métodos "no modificables" de Collections. El último bloque try de test( ) examina el método set( ) que fom,. parte de Lis!. Este bloque es interesante, porque podemos ver cómo nos resulta útil la granularidad de la técnica de "operaciones no soportadas": La "interfaz" resultante puede variar en un método entre el objeto devuelto por Arrays.asList( ) y el que devuelve Collections.unmoditiableList( ). Arrays.asList( ) devuelve una lista de tamaii.o fijo, mientras que Colleetions.unmodifiableList() genera una lista que no se puede modificar. Como puede verse analizando la salida, resulta posible modificar los elementos de la lista devuelta por Arrays.asList( ), porque esto no viola la característica de "tamaño tija" de dicha lista. Pero es obvio que el resultado de unmodifiableList( ) no debe ser modificable de ninguna forma. Si usáramos interfaces, esto habría requerido dos interfaces adicionales: una con un método set( ) funcional y otra sin dicho método. Asimismo, podrían requerirse interfaces adicionales para diversos subtipos no modificables de Colleedon. La documentación de un método que tome un contenedor como argumento deberá espec ificar cuáles de los métodos opcionales debe implementarse. Ejercicio 6: (2) Observe que List tiene operaciones "opcionales" adicionales que no están incluidas en Colleetion. Escriba una versión de Unsupported.java que pruebe estas operaciones opcionales adicionales. Funcionalidad de List Como hemos visto, el contenedor List básico es bastante simple de usar: la mayor parte del tiempo nos Limitaremos a invocar 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 listas 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 import static net.mindview.util.Print.*¡ public class Lists { private sta tic boolean b¡ private static String Si private static int i; private static Iterator it; private static Listlterator lit; public static void basicTest(List a) a.add(l, "x") i /1 Agregar en la posición 1 a.add("x!1); 1/ Agergar al final // Agregar una colección: a.addAll(Countries.names(25» i // Agregar una colección comenzando en la posición 3: a.addAll(3, Countries.names(25» i b "" a.contains{"l"l¡ JI 15 it in there? 1/ ¿Está toda la colección contenida? b = a.containsAll {Countries.names (25l ) i /1 Las listas permiten el acceso aleatorio, que es poco 1/ costoso para ArrayList, y caro para LinkedList: s a.get(1); II Obtener objeto (con tipo) en la posición 1 i = a. indexOf ("1 11 ) ; II Determinar índice del obj eto b = a.isEmpty(); II ¿Hay algún elemento? it = a.iterator(); II Iterador normal lit = a.listlterator(); II Listlterator lit = a.listlterator{3)¡ II Empezar en la posición 3 i = a .lastlndexOf ( " 1"); II Úl tima correspondencia a.remove{l); II Eliminar posición 1 a.remove("3 11 ) ; II Eliminar este objeto a.set(l, "yU) ¡ II Asignar "y" a la posición 1 II Conservar todo lo que forme parte del argumento II (la intersección de dos conjuntos): a.retainAll(Countries.names{25» ; II Eliminar todo lo que forme parte del argumento: a.removeAll(Countries.names(25» ; i = a.size(); II ¿Qué tamaño tiene? a.clear() i II Eliminar todos los elementos public static void iterMotion(List al Listlterator it = a.listlterator(); b it.hasNext(); b it.hasPrevious(); s i t . next () ¡ it.nextlndex()¡ i s it.previous(); i it.previouslndex(l ¡ public static void iterManipulation (List al { Listlterator it = a.listlterator(); it.add("47") ¡ II Hay que desplazarse a un elemento después de add () : it.next() ¡ II Eliminar el elemento situado después del recién generado: it.remove() ; II Hay que desplazarse un elemento después de remove () : it . next () i II Cambiar el elemento situado después del borrado: it. set ("47 11 ) ¡ 532 Piensa en Java public statle vold testVisual (List al print (al; { List b Countries . names(2S); print("b: " + b); a.addA11 lb) ; a. addA11 lb) ; print(a) ; 1/ Insertar, eliminar y reeemplazar elementos 1/ utilizando Listlterator: Listlterator x = a,listlterator(a.size()/2} x .add (tiene") ; print (al; print(x.next()) ; x.remove() ; print (x.next ()) i x.set("47") ; print (al; // Recorrer la lista hacia atrás: x = a.listlterator(a.size()); while(x.hasPrevious()} printnb(x.previous() + 11 "}i print 1); print ("testVisual finished"); JI Hay algunas cosas que s610 los contenedores 1/ tipo LinkedLists pueden hacer: public statie void testLinkedtist () { LinkedList 11 = new LinkedList(); ll.addAll(Countries . names(2S)) ; print (11); II Tratarlo como una pila, insertando : ll.addFirst(tlone tl ) ; ll.addFirst("two tl ) ; print (11); II Como si se "cons\J.ltara" la cima de la pila: print 111. getFirst ()) ; II Como si se extrajera de una pila: print (ll.removeFirst () ); print(ll.removeFirst{)) ; II Tratarlo como una cola, extrayendo elementos II del final, print{ll.removeLast()) ; printlll); public static void main(String[] args) JI Crear y rellenar una nueva lista cada vez: basicTest( new LinkedList(Countries.names(25))); basicTest( new ArrayList(Countries.names(25))); iterMotion( new LinkedList(Councries.names(25))); iterMotion( new ArrayList(Countries.names(25))); iterManipulation( new LinkedList (Countries.names (25) )); iterManipulation{ new ArrayL i st{Countries.names{25))); testVisual( i 17 Análisis detallado de los contenedores 533 new LinkedList(Countries.names ( 25 }) ); testLinkedList( ) ; / * (Execute to see output l * /// :En basicTest() e iterMotio n(), las llamadas se reali zan en orden para mostrar la sintaxis apropiada, y aunque se captura el va lor de retomo. dicho valor no se utili za . En algunos casos, el va lor de retomo no se captura en absoluto. Consulte los detalles completos de utilización de cada uno de estos métodos en la documentación del JDK antes de utilizarlos. Ejercicio 7: (4) Cree tanto un contenedor ArrayList como otro de tipo Li n kedL ist y rellénelos utilizando el generador Countries.names( ). Imprima cada lista utili zando un iterador normal y luego inserte una lista en la otra empleando un iterador Listlterator , realizando las inserciones en una de cada dos posiciones. Ahora realice la inserción comenzando por el final de la primera lista y desplazándose hacia atrás. Ejercic io 8: (7) Cree una clase que represen te una lista genérica simplemente enlazada denominada SList, la cual, para hacer las cosas simples, 110 implemente la interfaz Lisl. Cada objeto Li n k (enlace) de la lista puede contener una referencia al sigui ente elemento de la lista, pero no al anterior (LinkedList, por contraste, es una lista doblemente enlazada, lo que significa que mantiene enlaces en ambas direcciones). Cree su propio iterador SLisllterato r que, de nuevo por simplicidad, no implemente Listlterator. El unico método de SList aparte de toString(), debe ser iterator(), que generará un elemento SListlterator. La unica manera de insertar y eliminar elementos de un contenedor SList es mediante SListlterator. Escriba el códi go necesario para ilustrar el uso de SList. Conjuntos y orden de almacenamiento Los ejemplos del contenedor Set del Capítulo 11 , Almacenamiento de objetos, proporcionan una buena introducción a las operaciones que pueden realizarse con los conjuntos básicos. Sin embargo, di chos ejemplos utili zan, por comodidad, tipos de Java predefinidos como Intege r y String, que estaban disenados para poderlos utilizar dentro de contenedores. A la hora de crear nuestros propios tipos, debemos tener en cuenta que un cont enedor Set necesita una forma de mantener el orden de almacenamiento. El cómo se mantenga ese orden de almacenamiento varía de una implementación de Set a otra. Por tanto, las diferentes implementaciones de Set no sólo tienen diferentes comportamientos, sino tambi én diferentes requisitos, adaptados al tipo de objeto que puede introducirse dentro de un contenedor Set concreto: Set (interfaz) HashSet* 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. 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 ordenada 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 iteramos 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á optimizada 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 almacenamiento 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 implementación del conjunto) o L inkedHasbSet. Sin embargo, con el fin de mantener un buen estilo de programación, conviene 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 implementació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; } class HashType extends SetType { public HashType (int ni { super(nl; } public int hashCode () { return i; } class TreeType extends SetType implements Comparable public TreeType (int n) { super (n); public int compareTo(TreeType arg} return (arg . i < i ? -1 : (arg.i i ? O 111 ; public class TypesForSets { static Set fill(Set set, Class typeJ try { for(int i = O; i < la; i++) set.add( type.getConstructor{int.class) .newI nstance{i)); catch(Exception el { throw new RuntimeException{e) i return set i static void test(Set set, Class type} { fill (set, type) i fill(set, typel; II I ntentamos añadir duplicados fill(set, type}¡ System . out.println(set) i public static void main (Str i ng [] a r gs) { test(new HashSe t {), Ha shType.c l ass); test(new LinkedHashSet(), HashType . classl; test(new TreeSe t (), Tr eeType . classl i II Cosas que no funcionan: test(new HashSet{l, SetType . classl; test(new Has h Set(), TreeType . class); test(new LinkedHashSet(), SetType.class) i test(new LinkedHashSet(}, TreeType . c l assl; try { test(new TreeSet(), SetType.class} j } catch (Exception el { 17 Analisis detallado de los contenedores 535 System.out.println {e . getMessage ()) i ) try ( test(new TreeSet(), HashType.class ) i catch (Exception e ) { System.out.println (e.getMessage ()) i 1* Output: (Sample ) [2, 4, 6, 1, (O, 1, 2, 3, 4, 5, 9, 8, 7, 6, 5, 9, 7, 5, 1, 4, 3, 5, O, (O, 5, 5, 6, 5, 7, 1, 9, 6, 2, (O, 1, 2, 3, 4, 9, O, 1, 2, 3, (9, 8, (9, °, 4, 2, 8, O, 1, 5, 4, [O, 1, 2, 3, 4, S, 9, O, 1, 2, 3, 4, 3, 7, 6, 7, 3, 2, 6, 3, 8, 8, 3, 1, 8, 2, 6, 7, 5, 6, 6, 7, 5, 6, 5, 8, 1, O) 9) °, O) 7, 2, 4, 6, 5, 1) 9, 8, 4, 2, 8, 6, 7) 8, 9, O, 1, 7, 8, 9) 8, 7, 9, 8, O, 1, 4, 7, 9, 1, 3, 6, 2, 3, 9, 7, 3, 4, 4, O, 2, 3, 4, 5, 6, 7, 8, 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 contenedor 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 funcionarí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 devolver 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 tenderemos 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 incorrecto. La única fonn8 fiable de garanti zar la corrección de ese programa consiste en incorporar código de pruebas en el sistema 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 adicional 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 comprendidos 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 iguales 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.*; public class SortedSetDemo { public static void main{String[] args) { SortedSet sortedSet = new TreeSet {) Collections.addAll{sortedSet, "one two three four five six seven eight" .split{" " )); print(sortedSet) i String low = sortedSet.first() i String high = sortedSet.last{); print (low) ; print Ihigh) ; Iterator it = sortedSet.iterator{); for(int i = O; i <= 6; i++) { if(i 3) low = it.next {); if (i 6) high = it .next () ; else it.next(); 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 i 17 Análisis detallado de los contenedores 537 one two [ane, seven, six, three] [eight, five, four, ane, [one, seven, six, three, seven, two] six, three] * /// ,Observe que SortedSet quiere decir "ordenado de acuerdo con la función de comparación del objeto", no "arde.n de inserció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.*; pub li c class QueueBehavior { priva te static int count = 10; static void test (Queue queue, Generator gen) for(int i = O; i < count; i++} queue.offer{gen.next(»; while(queue.peek{) != null) System . out.print(queue.remove() + " "); System.out.println{) ; static class Gen implements Generator String[] s = ("one two three four five six seven "eight nine ten").split(1I 11); int i; public String next () { return s (i++J; 11 { + public static void main(String[] args) { test(new LinkedList() I new Gen(»; test(new PriorityQueue(), new Gen(»; test (new ArrayBlockingQueue (count) , new Gen(»; test(new ConcurrentLinkedQueue(} I new Gen(»; test(new LinkedBlockingQueue(), new Gen(»; test(new PriorityBlockingQueue(), new Gen(» i 1* Output: o n e two three four five six seven eight nine eight five four nine one seven six ten three one two three four five six seven eight nine one two three four five six seven eight nine one two three four five six seven eight nine eight five four nine one seven six ten three */ // ,- ten two ten ten ten 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. Colas con prioridad Ya hemos proporcionado una breve introducción a las co las con prioridad en el Capínlio 11. Almacenamiento de objetos. Un problema más interesante que los que allí analizamos sería el de un a lista de tareas que hacer, en la que cada objeto contenga una cadena de caracteres y sendos va lores de prioridad principal y secundaria. La ordenación de esta lista es tá, de nuevo, control ada por la implementación de Comparable: JI: containersjToDoList.java JI Un uso más complejo de PriorityQueue. import java.util. *; class ToDoList extends PriorityQueue { static class ToDoItem implements Comparable { private char primarYi private int secondary¡ private String item¡ public ToDoItem(String td, char pri, int sec) { primary = pri; secondary = sec¡ item = td; public int compareTo(ToDoItem arg) { if(primary > arg.primary) return +1; if{primary == arg.primary) if(secondary > arg.secondary) return +1; else if(secondary == arg.secondary) return O; return -1; public String toString() return Character.toString(primary) + secondary + " : u + i tem; public void add (String td, char pri, int sec) super.add(new ToDoItem(td, pri, sec)); public static void main(String[) args) ToDoList toDoList = new ToOoList(); toDoList.add(UEmpty trash 'C', 4); toDoList.add(uFeed dog 'A', 2); toDoList.add(UFeed bird", 'B', 7); toDoList.add(UMow lawn u , 'C', 3); toDoList.add(IIWater lawn 'A' 1); toDoList .add{UFeed cat", 'B', 1); while(!toDoList.isEmpty() ) System.out.print1n(toDoList.rernove()) ; ll ll ll / * Output: Al, A2, BL B7, C3, C4: Water lawn Feed dog Feed cat Feed bird Mow lawn Empty trash * /// ,- , , , I 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 demostrar 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 efecmar una generalización a una interfaz Deque (cola doble), a diferencia de lo que podríamos hacer con Queue en el ejercicio 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.*¡ public class Deque private LinkedList deque = new LinkedList(); public void addFirst{T e) ( deque.addFirst{e) ¡ public void addLast(T el { deque.addLast(e); } public T getFirst () { return deque. getFirst () ¡ public T getLast () ( return deque. getLast () ¡ } public T removeFirst () ( return deque. removeFirst () ; public T removeLast () { return deque. removeLast () ¡ } public int size () { return deque. size () ¡ } public String toString () { return deque. toString () ; II y otros métodos según sean necesarios ... ///,Si utilizamos esta clase Deque en nuestros propios programas, es posible que descubramos que necesitamos añadir otros métodos para que la clase resulte práctica. He aqui un ejemplo simple de prueba de la clase Deque: 11: containers/DequeTest.java import net.mindview.util.*¡ import static net . mindview.util.Print.*; public class DequeTest { static void fillTest{Deque deque) for(int i = 20; i < 27; i++} deque.addFirst(i) ; for(int i = 50; i < 55; i++} deque.addLast{i) ; public static void main(String [] args) { Deque di = new Deque(); fillTest (di); print(di) ; while(di.size() != O) printnb(di.removeFirst(} print (); fillTest (di); while (di . size () != O) + 11 11) ¡ { 540 Piensa en Java printnb(di.removeLast {) + 11 "); 1* Output: [26, 25, 24, 23, 22, 21, 20, 50, 51, 26 25 24 23 22 21 20 50 51 52 53 54 54 53 52 51 50 20 21 22 23 24 25 26 52, 53, 54] * /// ,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 asociativa) 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 presentan 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 importancia 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.*¡ public class AssociativeArray private Object [] [] pairs; private int indexi public AssociativeArray(int length } pairs = new Object [length] [2] i public void put (K key, V value ) { if(index >= pairs.length ) throw new ArraylndexOutOfBoundsException( ) ; pairs (index++l = new Object [] { key, value }; @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 public String toString( ) StringBuilder result = new StringBuilder( ) ; for(int i = O; i < index; i++ ) { result. append (pairs [i] [O) . toString ( ) ) ¡ resu lt. append{" : 11 ); result.append (pairs[i] [11.toString ()) ; if (i < index - 1 ) result.append {"\n") ; return result.toString {) ; 17 Análisis detallado de los contenedores 541 public static void main (String ( ] args ) { AssociativeArray map = new AssociativeArray (6); map.put ( "sky", "blue" ) i map.put ( "grass", "green" l i map.put {"ocean", "dancing" ) ; map.put ( lItree", "tall" ) ; map.put ( "earth", IIbrown" ) ; map .put ( "sun", "warm" ) ; try { map.put ( "extra", "object" l i JI Past the end catch (ArraylndexOut OfBoundsException e l print ( "Too many objects!" ) ; 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 sustituido 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 asociado, 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 emplearse 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.mindview.util.TextFile de este libro, abra un archivo de tex to y extraiga las palabras de dicho archivo utilizando 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 lineal 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 rendimiento. 6 He aquí las implementaciones básicas de Map. El asterisco sinlado junto a HashMap indica que, en ausencia de otras restri cciones, ésta debería ser la opción preferida, ya que está optimizada para maximizar la velocidad. Las otras implementaciones 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 utilizado). Es ligeramente más lento que HashMap , sa lvo cuando se está realizando una iteración, en cuyo caso es más rápido debido a que se emplea una lista enlazada para mantener 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 conserva 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 sincronizació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 Cosas que se pueden hacer con mapas. import java.util.concurrent.*; import java.util.*; import net.mindview.util.*; import static net.mindview.util.Print.*; JI 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 proyecciones 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 desbordamiento 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 public static void printKeys(Map map) printnb("Size = " + map.size() + ." printnb ("Keys: 11) i "); print{map.keySet()); // Generar un conjunto con las claves public static void test(Map map) { print(map.getClass() .getSimpleName()}; map.putAll(new CountingMapData(25)} i 1/ El mapa presenta el comportamiento de un conjunto para las claves: map.putAll(new CountingMapData(2S)); printKeys (map) ; /1 Generación de una colección con los valores: printnb ("Values: "); print(map.values()) i print (map) ; print("map . containsKey(ll): 11 + map.containsKey(ll); print{"map.get(ll): "+ map.get(ll))¡ print("map.containsValue(\"FO\II): 11 + map . containsValue (" FO") ) ; Integer key = map. keySet () . i terator () . next () ; print (ti First key in map: ti + key); map. remove (key) ; printKeys (map) ; map. clear () ; print (IImap . isEmpty() : " + map.isEmpty()); map.putAll(new CountingMapData(25)); 11 Las operaciones efectuadas sobre el conjunto modifican el mapa: map.keySet() .removeAll (map.keySet () ); print ("map . isEmpty () : " + map. isEmpty () ) ; public static void main(String[] args) test (new HashMap()); test(new TreeMap()); test (new LinkedHashMap()); test(new IdentityHashMap()); test(new ConcurrentHashMap()) ; test(new WeakHashMap ()); 1* Output: HashMap Size = 25, Keys: 24, 4, Values: 19, 11, [15, B, 23, 16, 7, 22, 9, 21, 6, 1, 14, 18, 3, 12, 17, 2, 13, 20, 10, 5, O] [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 fromKey. COI1 las claves que sean iguales o superiores a 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.*; public class SortedMapDemo { public static void main (String [J args) { TreeMap sortedMap new TreeMap (new CountingMapData(lO}}; print(sortedMap} i Integer low = sortedMap.firstKey() ¡ Integer high = sortedMap.lastKey() i print(low) ; print Ihigh) ; Iterator< Integer> it = sortedMap keySet(} .itera tor{) for (int i = O¡ i <'= 6; i++} { if{i == 3} low = it.next (); ¡ ifli == 6) high = it.next(); else it.next(}; print {low} ; print (high ) ; print (sort edMap. subMap(low, high); print (sortedMap.headMap{high)} ; print (sortedMap.tailMap {low)} ; } 1* Output, {O=AO, °9 1=80, 2 =CO. 3=DO, 4=EO, 5=FO, 6=GO, 7=HO, 8=IO, 9=JO} 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 algoritmo LRU (leasl-recenlly-used) basado en el acceso a los elementos, de modo que los elementos a los que se haya accedido 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 linkedMap new LinkedHashMap< In teger,String>( new CountingMapData(9)); print(linkedMap) ; // Orden LRU, linkedMap = new LinkedHashMap (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, {O=AO, l=BO, {6 =GO, 7=HO, {6=GO, 7=HO, 2=CO, 3=DO, 3=00, 8=IO, O=AO, 8=IO, l=BO, 2=CO, 4=EO, 4=EO, 1=80, 2=CO, 5=FO, 6=GO, 7=HO, 5=FO, 6=GO, 7=HO, 2=CO, 3=DO, 4=EO, 3=00, 4=EO, S=FO, 8=IO} 8=IO} 5=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. Almacenamiento y códigos hash Los ejemplos del Capínllo 11, Almacenamiento de objetos, utilizan clases predefinidas como claves para HashMap. Dichos ejemplos funcionaban porque esas clases predefinidas contenían todos los elementos necesarios para poder comportarse correctamente como claves. 546 Piensa en Java Uno de los problemas más comunes es el que se produce cuando creamos nuestras propias clases para utilizarlas como claves 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 objetos 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. public class Groundhog { protected int number¡ public Groundhog(int n) {number public String toString () { return "Groundhog #" + number; 11 : n;} containers/Prediction.java 1I Predicción del clima mediante marmotas . import java.util . *; public class Prediction private static Random rand = new Random(47) ; priva te boolean shadow = rand nextDouble () > 0.5 ; public String toString () { if {shadow} return "Six more weeks of Winter!" ¡ el se return "Early Spring!" i } /// > 11: containers/SpringDetector.java II ¿Qué tiempo hará? import java.lang.reflect.*; import java.util.*; import static net . mindview.util.Print.*; public class SpringDetector { 11 Utiliza Groundhog o una clase derivada de Groundhog: public static void detectSpring{Class type) throws Exception { Constructor ghog = type.getConstructor(int.class) ¡ Map map = new HashMap(); for(int i = O; i < 10; i++ ) map.put {ghog.newInstance (i) , new Prediction()}; print ("map = !I + map); Groundhog gh = ghog.newInstance{3); print ( " Looking up prediction for !I + gh) ¡ if(map.containsKey{gh» print(map.get(gh)) ; else print (" Key not found: " + gh) i public static void main(String[] args) detectSpring(Groundhog.class) ; 1* Output: throws Exception { 17 Análisis detallado de los contenedores 547 map = {Groundhog #3=Early Spring!, Groundhog #7=Early Spring!, Groundhog #5=Early Spring!, Groundhog #9=Six more weeks of Winter!, Groundhog #8=Six more weeks of Winter!, Groundhog #O=Six more weeks of Winter!, Groundhog #6=Early Spring!, Groundhog #4=Six more weeks of Winter!, Groundhog #l=Six more weeks of Winter!, Groundhog #2=Early Spring!} Looking up prediction for Groundhog #3 Key not found: Groundhog #3 * ///> A cada objeto Groundhog se le da un número identificador, de modo que se puede buscar un objeto Prediction en el contenedor HashMap diciendo: "Dame el objeto Prediction asociado con el objeto asociado Groundhog #3". La clase Prediction contiene un valor de tipo boolean que se inicializa utilizando java.util.random() y un método toString( ) que interpreta el resultado por nosotros. El método detectSprillg() (detectar la primavera) se crea empleando el mecanismo de reflexión para instancias y usa la clase Groundhog o cua lquier clase derivada de Groundhog. Esto nos será útil posteriormente. cuando heredemos una nueva clase a partir de Groundbog para resolver el problema ilustrado en este ejemplo. Rellenamos un objeto HashMap como objetos Groundhog y sus objetos Prediction asociados. El mapa HashMap se imprime para poder ver que ha sido rellenado. Después, se utiliza un objeto Groundhog con número identificador igual a 3 como clave para buscar la predicción para Groundhog #3 (que, corno vemos, debe encontrarse en el mapa). El ejemplo parece lo suficientemente simple, pero no funciona ; ya que no se puede encontrar la clave correspondiente a #3. El problema es que Groundhog hereda automáticamente de la dase raíz común Object, y se está utilizando el método hasheode() de Object para generar el código hash correspondiente a cada objeto. De manera predeterminada, dicho método se limita a utilizar la dirección de su objeto. Por tanto, la primera instancia de Groundhog(3) no produce un código has/¡ igua l al código has11 de la segunda instancia de Groundhog(3) que hemos tratado de utilizar como clave de búsqueda. Podríamos pensar que lo único que hace falta es esc ribir un método de sustitución apropiado para hashCode( ). Sin embargo, esta solución seguirá sin funcionar hasta que hagamos una cosa más: sustituir el método equals( ) que también fonna parte de Object. El método equals() es utilizado por HashMap a la hora de detenninar si la clave es igual a cualquiera de la claves contenidas en la tabla. Un método equals() apropiado deberá sat isfacer las siguientes cinco condiciones: 1. Reflexiva: para cualquier x, x.equals(x) debe devolver true. 2. Simétrica: para cualesquiera x e y, x.equals(y) debe devolver troe si y sólo si y.equals(x) devue lve true. 3. Transitiva: para cualesqu iera x, y y z, si x.equals(y) devuelve true e y.equals(z) devuelve true, entonces x.equals(z) devolverá true. 4. Coherencia: para cualesquiera x e y, múltiples invocaciones de x.cquals(y) deberán devolver continuamente true o continuamente false, en tanto que no se modifique ninguna infornlación utili zada en las comparaciones de igualdad de los objetos. 5. Para cualquier x distinta de null, x.equals(null) debe devolver false . De nuevo, el método predetenninado Object.equals() simplemente compara las direcciones de los objetos. por lo que una insta ncia Groundhog(3) no es igual a la otra instancia Groundhog(3). Por tanto, para poder usar nuestras propias clases como cla ves en un contenedor HasbMap, debemos sustituir tanto hashCode( ) como equals( ), como se muestra en la siguiente solución al problema de las mannotas: //: containers/Groundhog2 .java JI Una clase utilizada como clave en un contenedor HashMap / / debe sustituir hashCode () y equals () . public class Groundhog2 extends Groundhog { public Groundhog2 (int n) { super{n) i } public int hashCode () { return n urnber; public boolean equals (Object o) { return o instanceof Groundhog2 && (number == ((Groundhog2)o) .number) i 548 Piensa en Java JI: containersjSpringDetector2.java 1/ Una clave adecuada. public class SpringDetector2 public static void main(String[] args) throws Exception SpringDetector.detectSpring(Groundhog2 . class) ; / * 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 responsable 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úmero 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 problemas 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 objeto empleando otro objeto. Pero también podríamos hacer esto con un contenedor TreeMap, o incluso podríamos implementar 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.*¡ public class SlowMap extends AbstractMap private List keys = new ArrayList() ¡ prívate List values = new ArrayList() ¡ public V put(K key, V value) { V oldValue = get(key) ¡ II El valor anterior o null if ( ! keys. contains (key)) { keys. add Ikey) ; values.add(value) else i 17 Análisis detallado de los contenedores 549 values.set (keys.indexOf (key ) , value ) ; return oldValue¡ public V get (Object key ) { /1 La clave es de tipo Object, no K if ( !keys.contains {key » return null; return values . get (keys. indexOf (key) ) ; public Set ki Iterator vi = keys.iterator () ; = values.iterator () ; while lki.hasNext l)) set.add (new MapEntry (ki.next () , vi.next (») ; return set; publi c static void main (String[] args ) { SlowMap m= new SlowMap () ; m.putAll (Countries . capitals ( lS » ; System.out.println (m) ; System.out.println (m. ge t( "BULGARIA" ») ; System.out.println (m. entrySet ()) ¡ 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 hubieran 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, deberem 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 .* ; public class MapEntry implements Map.Entry { private K key; private V value¡ public MapEntry (K key, V value ) { 550 Piensa en Java this. key = key i this. value = value; public K getKey{) ( return key; ) public V getValue () { return value; public V setValue (V v) v resule = value; value { = Vi return resul t i public int hashCode() return (key==null ? O : key. hashCode ()) ... (value= =null ? o : value.hashCode{»); public boolean equals (Object o) { if(! (o instanceof MapEntry)) return false; MapEn try me = (MapEntry)o ; return (key == null ? me.g e tKey () == null , key.equalslme . gec Keyl))) && (value == null ? me . getVa l ue ()== nul l : value , equa l s{me. ge t Value(})) ; public String toStr i ng () { r e turn key + "=" + va l ue i } /// , Aquí, una clase muy simple denominada MapEntry almacena y extrae las claves y valores. Ésta se usa en entrySet( ) para generar un conjunto de parejas clave-valor. Observe que entrySet() utiliza un conjunto HashSet para almacenar las parejas, y MapEntry adopta una solución sencilla consistente en limitarse a utilizar el método hashCode( ) de la clave key. Aunque esta solución es muy simple y parece funcionar en la prueba trivial realizada en SlowMap.main(), no es una implementación correcta porque se realiza una copia de las claves y valores. Una implementación correcta de entrySet() proporcionaría una vista del mapa en lugar de una copia, y esta vista pennitiria la modificación del mapa original (lo que una copia no pennite). El Ejercicio 16 proporciona la oportunidad de corregir este problema. Observe que el método equals() en MapEntry debe comprobar tanto claves como valores. El significado del método hashCode( ) se describirá en breve. La representación del tipo String del contenido del mapa SlowMap se genera automáticamente mediante el método toString() definido en AbstractMap. En SlowMap.main(), se carga un mapa SlowMap y luego se muestran los contenidos. Una llamada a get() demuestra que la solución funciona. Ejercicio 15 : (1) Repita el Ejercicio 13 utilizando un mapa SlowMap. Ejercicio 16 : (7) Aplique las pmebas de Maps.java a SlowMap para verificar que funciona. Corrija cualquier cosa de SlowMap que no funcione correctamente. Ejercicio 17: (2) Implemente el resto de la interfaz Map para SlowMap. Ejercicio 18 : (3) Utilizando como modelo SlowMap.java, cree un conjunto SlowSet. Mejora de la velocidad con el almacenamiento hash SlowMa p.java muestra que no resulta tan dificil producir un nuevo tipo de mapa. Pero, como su nombre en inglés sugiere, SlowMap no es muy rápido, por lo que lo más nonnal es que no lo utilicemos si disponemos de alguna otra alternativa. El problema está en la búsqueda de la clave; las claves no se conservan en ningún orden concreto, así que no hay más remedio que usar una simple búsqueda lineal. La búsqueda lineal es la forma más lenta de encontrar algo. El almacenamiento hash tiene como único objetivo la velocidad: este almacenamiento pennite realizar las búsquedas rápidamente. Puesto que el cuello de botella se encuentra en la velocidad de búsqueda de las claves, una de las soluciones al 17 Análisis detallado de los contenedores 551 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 (observe 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 valores. 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.*¡ public class SimpleHashMap extends AbstractMap // Seleccione un número primo como tamaño de la tabla hash, // para conseguir una distribución uniforme: static final int SIZE = 997¡ II No se puede tener una matriz física de genéricos, II pero sí que podemos generalizar para obtener una: @SuppressWarnings("unchecked") LinkedList pair = new MapEntry (key, value) i boolean found = false; Listlterator iPair = it.next{) i ifliPair.getKey{) .equalslkey)) { oldValue = iPair.getValue() i 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 it.set(pairl; /1 Sustituir antiguo por nuevo found = true; break; if ( ! found) buckets (index] . add (pai r ) ; return oldValue; public V get(Object key) int index = Math.abs(key.hashCode()} % SIZE¡ if (buckets [index] == null) return null¡ for (MapEntry iPair : buckets[index]) if ( ipair. getKey () . equals (key) ) return iPair.getValue() i return null; public Set mpair : bucket) set.add{mpairl; return set; public static void main (String [] args) SimpleHashMap m = { new SimpleHashMap(); m.putAll (Countries.capita ls (25) ) ; System.out.println(m) ; System.out.println(m.get("ERITREA")) ; System.out.println(m.entrySet()) ; 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 correspondencia. 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écnica 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 producen 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 objeto 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 segmentos). 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 obje10 que produjera un valor hashCodc() al insertarlo con pu t() en un contenedor HashMap y otro valor distinto al extraerlo 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ácter distintivo, en concreto. el valor de this genera un código has hCode() no muy bueno. porque eI1lonces es imposible generar 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 precisamente 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 programa 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 public class StringHashCode { public static void main{String[] args) { String[] helIos = "HelIo Hello".split{" ")i System.out.println(hellos(O] .hashCode ()) i System . out. println (helIos (1] . hashCode () ) ; 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 interesa es la velocidad no la unicidad) pero enrre has hCode() y equals(), la identidad del objeto dcbe ser completamente especificada . 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 valores. 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ásica 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: Tipo del campo Cálculo boolean c = (f? O: 1) byte, char, short o ¡nt e = (int)f long e = (int)(f ' (f »>32) noat c = Float.f1oatTolntBits(f)j 17 Análisis detallado de los contenedores 555 Tipo del campo Cálculo double long I = Doublc.doubleToLongBits(f); e Object, donde equals( } invoca a equals( ) para este campo e = f.ha,heode( ) Matriz Aplicar las reglas an teriores a cada elemento = (inl)(1 A (1 >>> 32» ---- --------' 3. Combine el códi go hash recién calcul ado: result = 37 * result + e; 4. Devuelva resulto S. Examine el cód igo hashCode( ) resullanle y asegúrese de que instancias iguales tengan códigos hash iguales. He aquí un ejemplo construido sobre estas directrices: // : c ontainers / CountedString.java Creación de un método hashCode () adecuado. import java.util.*; import static net.mindview.util . Print.*; JI public class CountedString { private static List created new ArrayList(); private String Si private int id = O; public CountedString (String str) s { = str; created.add(s ) i // id es el número total de instancias // de esta cadena que CountedString está usando : for (String s2 : created ) i f (s2.equals (s )) id++i public String toString {) return "String: + s + It id: It hashCode () : It + hashCode () It + id + i public int hashC0de( ) // La técnica simple: // return s.hashCode ( ) * idi // Utilización de la receta de Joshua Bloch: int result '" 17 i result '" 37 * result + s.hashCode ( ) ; result '" 37 * result + id; return result i public boolean equals (Object o ) { return o instanceof CountedString && s. equals ( ( (CountedString ) o) . s) && id ='" (( CountedString) o ) . id; public static void main(String[] args) { Map map new HashMap(); CountedString[] es '" new CountedString[S] i for(int i = O; i < cs .length; i++) { cs[iJ '" new CountedString("hi") i map.put(cs[i] i) i / / Autobox int -> Integer I 556 Piensa en Java 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 1 Looking 2 Looking 3 Looking 4 up String : hi id: 2 hashCode() , 146448 up String: hi id: 3 hashCode() , 146449 up String : hi id: 4 hashCode() , 146450 up String: hi id: 5 hashCode () , 146451 * /// ,CountedString incluye un objeto String y un id que representa el número de objetos CountedString que contienen un objeto 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 duplicados 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 definida 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; public class Individual implements Comparable private static long counter : O; private final long id : counter++; private String name; public Individual (String name) { this. name name; } jj 'name' es opcional: public Individual () () public String toString() return getClass() .getSimpleName() + (name :: null ? "" : " " + name ) ; public long id () ( return id; public boolean equals(Object o) return O instanceof Individual && id ::::: (( Individual ) o) . id; public int hashCode() int result : 17; if(name != null ) 17 Análisis detallado de los contenedores 557 result = 37 * result + name.hashCode(); result = 37 * result + (int) id; return result; public int compareTo(Individual arg) JI Comparar primero por el nombre de la clase: String first = getClass(} . getSimpleName{); String argFirst = arg.getClass() .getSimpleName() int firstCompare = first.compareTo(argFirstl; i if(firstCompare != O) return firstCompare; if (name ! = null && arg. Dame ! = null) { int secondCompare = name.compareTo(arg.namel if(secondCompare != O) return secondCompare; return (arg .id < id ? -1 : (arg .id id ? O i 111; 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.*¡ public class IndividualTest public sta tic void main (String (] args) { Set pets = new TreeSet(); for(List lp : MapOfList.petPeople.values() ) for (Pet p : lp) pets. add (pi; System.out.println(petsl; 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 herramientas 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 respuesta 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ñadiendo hashCode( ), equals(), e implementando Comparable para cada tipo de Tuple. 558 Piensa en Java Selección de una implementación A estas alturas. el lector deberia entender. que aunque sólo hay cuatro tipos de contenedores (Map. List. Se! y Queue) hay mas de \lna implementación de cada interfaz. Si necesitamos utilizar la funcionalidad ofrecida por una interfaz concreta. ¿cómo podemos decidir qué implementación empIcar? Cada una de las diferentes implementaciones tiene SlIS propias características, ventajas e inconvenientes. Por ejemplo, puede \'er en la imagen incluida al principio de este capítulo que la "ventaja" de I-Iashtable, Vector y Stack es que son clases heredadas. de modo que el código antiguo 110 dejará de funcionar (aunque lo mejor es que no utilice esos contenedores en los programas nuc\'os). 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 embargo. 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 referencias a los elementos anterior y siguiente de la lista. Debido a esto, si queremos hacer muchas inserciones y eliminaciones 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 proporciona 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 implementació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. Un marco de trabajo para pruebas de rendimiento Para evitar la duplicación de código y para que las pmebas sean coherentes. he ülcluido la funcionalidad básica del proceso de pmebas en un marco de trabajo que define la parte principal del programa. El código siguiente establece una clase base a partir de la cual se crea una lista de clases internas anónimas, una clase para cada una de las diferentes pruebas. Cada una de estas clases internas se invoca como parte del proceso de pruebas. Esta solución permite aiiadir y eliminar fácilmente distintos tipos de pmebas. Se trata de otro ejemplo del patrón de disello basado en el Método de plolllillas. Aunque se sigue la solución tipica del método de plantillas consistente en sustituir el método Tes t.tes t( ) para cada prueba concreta, en este caso la parte fundamental del código (que no cambia) se encuentra en una clase separada Tester. lo El tipo de cont\!l1edor que estamos probando es el parámetro genérico C: 11 : containers / Test.java II Marco de trabako para realizar pruebas temporizadas de contenedores. public abstract class Test { String name¡ public Test (String name) { this. name = name ¡ } 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 JI Sustituir este método para las diferentes pruebas. // Devuelve el número real de repeticiones de la prueba. abstraet int test(C container, TestParam tpl i ///0Cada objeto Test almacena el nombre de dicha prueba. Cuando se invoca el método test( ). hay que pasarle el contenedor que hay que probar junto con un "elemento de transferencia de datos" o " mensajero" que almacene los diversos parámetros correspondientes a dicha prueba co ncreta. Los parámetros incluyen size, que indica el número de elementos del contenedor y loops. que controla el número de iteraciones de la prueba. Estos parámetros pueden o no utilizarse en todas las pruebas. 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 alternantes, 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". public class TestParam { public final int size¡ public final int loops¡ public TestParam(int size, this.size = size¡ this.loops = loopsi int loops) /1 Crear una matriz de TestParam a partir de una secuencia de varargs: public static TestParam[} array(int ... values) { int size = values.length/2¡ TestPa:::.-am[} result = new TestParam[sizeJ i int n = O¡ for(int i = Di i < size¡ i++) result[iJ = new TestParam(values[n++}, values[n++}) ¡ return result; 11 Convertir una matriz de tipo String a una matriz TestParam: public static TestParam[] array(String[} values) { int [J vals = new inc [values .length] i for(int i = O¡ i < vals.lengthi i++l vals[i] = Integer.decode(values[i] ); return array(vals) i 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.*; public class Tester public static int fieldWidth = 8; public static TestParam [] defaultParams= TestParam.array ( 10, 5000, 100, 5000, 1000, 5000, 10000, 500); 560 Piensa en Java 1/ Sustituir esto para modificar la inicialización anterior a la prueba: protected C initialize(int size} { return container; protected C container; private String headline = "11; private List void run(C cntnr, List (cntnr, tests} .tirnedTest(}; tests) { public static void run(C cntnr, List (cntnr, tests, pararnList} .timedTest(); private void displayHeader() /1 Calcular anchura y rellenar con '-': int width = fieldWidth * tests.size() + sizeWidth¡ int dashLength = width - headline .length () 1; StringBuilder head = new StringBuilder(width) for {int i = O; i < dashLength/2 i i+·t} head. append { , - • 1 i head. append (t t); head. append (headline) ¡ head.append(t t); for(int i = D¡ i < dashLength/2; i++) head.append(t-t) ; System.out.println(head) ¡ II Imprimir cabeceras de columnas: System.out. format (sizeField, ttsize ll ) i for(Test test: tests) System.out. format (stringField{) , test . name} ¡ System.out.println(} i II Ejecutar las pruebas para este contenedor: public void timedTest() ( displayHeader() ¡ for(TestParam param : paramList) 17 Analisi s detallado de los contenedores 561 System.out . format{sizeField, for(TestcC> test param . size) i tests} { e kontainer ~ initialize(param . size); long start = System . nanoTime() i // Invocar el método sustituido: int reps = test . test(kontainer, param); long duratian = System . nanoTime{) - start¡ long timePerRep = duratian I reps¡ /1 Nanosegundos System.out.format(numberField(), timePerRep) i System.out.println() ; } ///,Los métodos stringField() y numberField() producen cadenas de fonnateo para imprimir los resultados. La anchura estándar de formateo puede variarse modificando el val or estáti co fieldWidth . El método displayHeader() da fonnato e imprime 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 almacenado 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 diferentes contenedores. Selección entre listas He aquí una prueba de rendimiento para las operaciones de List más esenciales. Por comparación, también se mues tran las operaciones de Queue más importantes. Se crean dos li stas separadas de pmebas, con el fin de probar cada clase de contenedor. En este caso, las operaciones de Queue sólo se aplican a listas de tipo LinkedList. jj: containersjListPerformance.java jj Ilustra las diferencias de rendimiento en las listas. jj {Args: 100 sao} Pequeño para que las pruebas sean cortas import java.util. * ¡ import net.mindview.util.*¡ public class ListPerformance static Random rand = new Random()¡ static int reps = 1000; static List tests new ArrayList(); sta tic List qTests new ArrayList(); static { tests . add(new Test list, Tes t Param tp) int loops = tp.loopsi int listSize = tp . size; for(int i = O¡ i < loops; i++) list . clear () ; for(i n t j = O; j < l istSize; j++) list.add(j) ; 562 Piensa en Java return loops * listSize¡ } }I ; tests. add (new TestcListclnteger» ("get") int test(Listclnteger> list, TestParam tpl int lcops = tp.loops * reps; int listSize = list.size(); for(int i = O; i < loops; i++) list.get(rand.nextlnt(listSize)) ; return loops; } }I ; tests.add(new TestcListclnteger»("set " ) int test(Listclnteger> list, TestParam tp) int lcops = tp.loops * reps; int listSize = list . size()¡ for(int i = O; i e lDops; i++) list.set(rand.nextlnt(listSize), 47); return lDOpS; } }I ; tests. add (new TestcListclnteger» (11 iteradd") { int test(Listclnteger> list, TestParam tp) { final int LOOPS = 1000000; int half = list.size{) / 2; Listlteratorclnteger> it = list.listlterator(half); for(int i = O; i < LOOPS; i++) it.add(471; return LOOPS¡ } }I ; tests.add(new Test list, TestParam tp) int loops = tp.loops¡ for(int i = O; i < loops; i++) list.add(5, 47}; // Minimizar el coste del acceso aleatorio return loops; } }I ; tests. add {new Test list, TestParam tp} int loops = tp.loops; int size = tp.size; for(int i = O; i < loops; i++) list. clear () ; list.addAll{new CountinglntegerList(size)); while(list.sizeO > 5) list.remove(5); // Minimizar el coste del acceso aleatorio return loops * size; } }I ; // Pruebas de comportamiento de las colas: qTests.add(new Test list, TestParam tp) ( int loops = tp.loops; int size = tp . size¡ for(int i = O; i < loops; i++) list. clear () ; 17 Análisis detallado de los contenedores 563 for(int j = o; j size; j++l <: list.addFirsc(47) ; return loops * size¡ } }I ; qTests. add (new Test<:LinkedList<:Integer» ("addLast ") { int tesc{LinkedList list, TestParam tp) int loops = ep.loops¡ int size = tp.size; { for(int i = o; i < loops; i++) { list . clear () ; for(int j = o; j <: size; j++) list.addLast(47) i return loops * size; } }I ; qTests. add ( new Testc::LinkedLisc list. TestParam tp) { inc loops = tp.loops; int size = tp.size; for(int i = O; i list. clear () ; <: loops; i++} list.addAll(new CountinglntegerList(size}); while(list.size() > O) list.removeFirst() ; return loops * size; } }I ; qTests. add (new Test list, TestParam tp) int loops = tp.loops; int size = tp.size; for(int i = O; i < loops; i++) { list. clear () i list.addAll(new CountinglntegerList(size)) i while (list.size () > Ol list.removeLast() i { { return loops * size¡ } }) ; static class ListTester extends Tester container, List tests) { super (container, tests); II Rellenar con el tamaño apropiado antes de cada prueba: @Override protected List initialize(int size) { container.clear() ; container.addAll(new CountinglntegerList{size)); return container; II Método de utilidad: public static void run(List list, 564 Piensa en Java List tests) { new ListTester(list, tests) .timedTest(}; public static void main{String[] argsl if(args.length > O) Tester.defaultParams = TestParam,array(args) j // Sólo se pueden hacer estas dos pruebas en una matriz: Tester initialize(int size) { Integer (] ia = Generated. array (Integer. class new CountingGenerator.lnteger() size); return Arrays . asList(ia) i I I } }; arrayTest. setHeadline ( "Array as List 11 ) ; arrayTest . timedTest() ; Tester.defaultParams = TestParam.array( 1 0, 5000, 100, 5000, 1000, 1000, 10 000, 200); if(args . length > O) Tester . defaultParams = TestParam. array (args) ; ListTester.run(new ArrayList() tests); ListTester.run(new LinkedList() tests); ListTester.run(new Vector() tests) i Tester.fieldWidth = 12; Tester(), qTests); qTest . setHeadline ( "Queue tests"); qTest.timedTest() ; I I I / * 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 446 3952 100 72 141 191 247 3934 296 1000 98 141 194 839 2202 923 10000 122 144 190 14042 7333 6880 - - - - - - -- - - - - - - - - - - - - - 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 239 136 10000 172 13648 13187 435 255 239 - - - - - - - - - - - - - - - - - - - - - - - Vector - - - -- - - - - - - - - - - - - - - - - - size add get set iteradd ir.sert remove 10 129 145 187 290 253 3635 100 72 144 190 263 3691 292 1000 99 145 193 846 2162 927 1 0000 108 145 7135 186 6871 14730 17 Análisis detallado de los contenedores 565 -- - - - - -- - -- - - ---- --- Queue tests - - - - -- - - size addFirst addLast rmFirst 10 100 1000 10000 199 98 99 111 163 92 93 109 251 180 216 262 -- - - - - - - - - -rmLast 253 179 212 384 */1/,Cada una de las pruebas requiere una consideración cuidadosa para garantizar que estemos produciendo resultados significativos. Por ejemplo, la prueba "add" borra la lista y luego la rellena de acuerdo con el tamaño de lista especificado. La llamada elear( ) para borrar fonua parte, por tanto, de la pmeba y puede tener un impacto sobre el tiempo de ejecución, especialmente para las pruebas más pequeñas. Aunque los resultados parecen bastante razonables en este caso, podríamos perfectamente pensar en reescribir el marco de trabajo de pruebas para crear una llamada a un método de preparación, (que en este caso incluiría la llamada a ele.rO) ji/era del bucle de cronometrado. Observe que, para cada prueba, es necesario calcular con precisión el número de operaciones que tienen lugar y devolver dicho valor desde test(), para que la temporización sea correcta. Las pruebas "get" y "set" utilizan el generador de número aleatorios para realizar accesos a la lista. Analizando la salida podemos ver que, para una lista respaldada por una matriz y para un contenedor ArrayList, estos accesos son rápidos y muy coherentes independientemente del tamaño de la lista, mientras que para un contenedor LinkedList, el tiempo de acceso aumenta de manera muy significativa para las listas de mayor tamaño. Claramente, las listas enlazadas no representan una buena elección si vamos a realizar muchos accesos aleatorios. La prueba Hiteradd" utiliza un lterador en mitad de la lista para insertar nuevos elementos. Para un contenedor ArrayList, esta operación resulta costosa a medida que el tamaño de la lista crece, pero para otro de tipo LinkedList es relativamente barata y ese coste es constante independientemente del tamaño. Esto tiene bastante sentido porque un contenedor ArrayList debe crear espacio y copiar todas sus referencias durante una inserción. Esta operación resulta muy costosa a medida que crece el tamaño del contenedor ArrayList. El contenedor LinkedList, por el contrario, sólo necesita enlazar un nuevo elemento, y no necesita modificar el resto de la lista, por lo que cabe esperar que el coste sea aproximadamente el mismo independientemente del tamaño de la lista. Las pruebas "insert" y "remove" utilizan la posición número 5 como punto de inserción y de eliminación, en lugar de utilizar uno de los extremos de la lista. Un contenedor LinkedList trata los extremos de la lista de manera especial, lo que permite mejorar la velocidad cuando se usa el contenedor LinkedList como una cola. Sin embargo, si se añaden o eliminan en mitad de la lista, hay que incluir el coste del acceso aleatorio, que ya hemos visto que varía para las diferentes implementaciones de la lista. Realizando las inserciones y eliminaciones en la posición 5, el coste del acceso aleatorio debería ser despreciable y sólo deberíamos ver el coste de la inserción y la eliminación, pero no podremos observar los resultados de las optim izaciones especiales que se aplican a los extremos de un contenedor LinkedList. Podemos ver, analizando la salida, que el coste de adición y eliminación en un contenedor LinkcdList es bastante bajo y que 110 varía con el tamailO de la lista, pero con un contenedor ArrayList, las inserciones son especialmente caras y el coste se incrementa con el tamaño de la lista. A partir de los datos correspondientes a Queue, podemos ver la rapidez con que LinkedList pennite insertar y eliminar elementos de los extremos de la lista, lo cual es el comportamiento óptimo para una cola. Nonoalmente, podemos limitamos a invocar Tester.run(), pasando el contenedor y la lista tests. Aquí, sin embargo. debemos sustituir el método initialize() para que la lista se borre y se rellene antes de cada prueba; en caso contrario, el control del tamaño de la lista se perdería durante las diversas pruebas. ListTester hereda de Tester y realiza esta inicialización empleando CountinglntegerList. El método de utilidad run( ) también se sustituye. También queremos comparar el acceso a una matriz con el acceso a un contenedor (principalmente con el acceso a ArrayList). En la primera prueba de main(), se crea un objeto Test especial utilizando una clase interna anónima. El método initialize( ) se sustituye para crear un nuevo objeto cada vez que se le invoca (ignorando el objeto container almacenado, de modo que null es el argumento container para este constructor Tester). El nuevo objeto se crea utilizando Generated.array() (que se ha definido en el Capítulo 16, Matrices) y Arrays.asList(). Sólo dos de las pruebas pueden realizarse en este caso, porque no se pueden insertar o eliminar elementos cuando se usa una lista respaldada por una matriz, así que se emplea el método List.subList( ) para seleccionar las pruebas deseadas de entre la lista tests. 566 Piensa en Java 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. Ejercicio 32: (2) Repita el ejerc icio ant erior para un comenedor de iut y compare el rendimient o con e l de ArrayList . En la comparación de rendimiento, incluya el proceso de incrementar cada objeto en el contenedor. 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 recorridos rápidos de los elementos y operaciones get() rápidas. Pmebc la so lución moditic3ndo ListPerformance.java. Peligros asociados a las micropruebas de rendimiento A la hora de escribir las denominadas micl'Opruebas de rendimiento, hay que tener cuidado de no dar demasiadas cosas por supues to y de enfocar las pruebas lo más posible, de modo que sólo se cronometren los elementos de interés. También hay que garantizar que las pruebas se ejecuten durante una ca ntidad de tie mpo lo suficientemente grande como para producir datos interesantes y hay que tener tambi én en cuenta que algunas de las tecnologías HotSpot de Java sólo ent ran en acción cuando un programa se ha estado ejecutando durante un determ inado período de tiempo (también es impol1ante lener esto en cue nt a para los programas de corta duración). Los resultados serán distintos. depend iendo de la computadora y de la máquina JVM que estemos utilizando, por lo que conviene que ejecute estas pmebas por sí mismo con el fin de verifi car que los resultados son simi lares a los que se mu estran en este libro. No deben preocuparle tanto los valores abso lutos como las comparaciones de rendimiento entre un tipo de contenedor y otro. Asimismo, una herramienta de perfilado puede reali zar un mejor análisis de rendimiento del que nosotros podemos llevar a cabo. Ja va incluye un perfilador (consulte el suplemento en hllp://MindVie"wet/Books/BellerJava) y también ha y perfiladores de otros fabricantes, tanto graruitos/código abierto como comerciales. Un ejemplo relacionado es el que afecta a Math.random( ). Este método produce un valor comprendido entre cero y uno , pero ¿eso incluye o excluye el valor" l "? En lenguaje matemático, ¿se trata del intervalo (O, 1), o [0.1]. o (0,1] o [0.1)0 (el corchete indica ,oinclusión", mientras el paréntesis indica 'ono inclusión"). Un programa de pmebas podría proporcionar la respuesta: 11 : containers/RandomBounds.java II ¿Permite Math.random{) generar 0.0 y 1 . Q? 17 Análisis detallado de tos contenedores 567 // {RUnByHand} import static net.mindview.util.Print.*; public class RandomBounds static void usage{) { print ("Usage : 11) ; print("\tRandomBounds lower") i print (" \tRandomBounds upper"); System.exit (l); public static void main (Str ing [] if(args . length lo: 1 } usage(); if(args(O].equals("lower")) args) { { while(Math . random() != 0.01 ; /1 Continuar intentándolo print (11 Produced O. O! ") i else if (args [O] . equals (lI upper") ) while(Math.random() != 1 . 0) ; // Continuar intentándolo print ("Produced 1. O! 11); el se usage() ; Para ejecutar el programa. hay que escribir la línea de comandos: java RandomBounds lower o java RandomBounds upper En ambos casos. estamos obligados a intemlmpir la ejecución del programa manualmente, por lo que podría parecer que Math.random( ) nunca genera ni 0.0 ni 1.0. Pero es precisamente aquí donde este lipa de experimentos pueden resultar engañosos. Si tenemos en cuenta que la cantidad de números fraccionarios comprendidos entre O y 1 son lmas 262 fracciones diferentes. la probabilidad de obtener cualquiera de esos dos valores experimentalmente podría exceder el tiempo de \'ida de ulla computadora, o incluso del propio experimentador. En realidad, 0.0 sí que está inc:hddo en el rango de salida de Math.raodom( ). O. dicho eo jerga matemática. el intervalo es [0.1 l. Por tanto, debemos tener cuidado a la hora de realizar nuestros experimentos co n el fin de asegurarnos de que entendemos sus limitaciones. Selección de un tipo de conjunto Dependiendo del comportamiento que deseemos, podemos elegir entre TreeSet, HashSet o LiokcdHashSet. El siguieote programa de pruebas proporciona una indicación de los comp rom isos de rendimiento que afectan a estas implementacione;): // : containers/SetPerformance.java /1 Ilustra las diferencias de rendimiento entre conjuntos. II {Args: 100 SOOO} pequeño para que las pruebas sean cortas import java.util .*; public class SetPerformance sta tic List tests = new ArrayList(); static { tests. add (new Test set, TestParam tp ) int loops = tp.loops; int size = tp.size; 568 Piensa en Java for(int i = O; i < loops; i++) { set. clear () i for(int j = o; j < size; j++) set.addljl; return loops * size; } }I ; tests.add(new Test set, TestParam tp) { int loops int span = tp.loops; tp.size * 2; = for(int i = o; i < loops¡ i++l for(int j = O; j < span¡ j++) set. contains (j) ; return loops * span; } }I ; tests .add (new '!'est set, TestParam tp) int loops = tp.loops * la; for(int i = O; i < loops¡ i++) Iterator it while(it.hasNext{}) it.next() i = { { { set.iterator() j return loops * set.size(); } }I; public static void main (String [] args) { if(args.length > O) Tester.defaultParams = TestParam.array(args}; Tester.fieldWidth = 10; Tester.run(new TreeSet(), tests) i Tester.run(new HashSet(), tests); Tester.run(new LinkedHashSet(), tests); 1* Output: (Samp le ) TreeSet ------------ size add contains iterate 746 10 173 89 100 501 264 68 1000 714 410 69 1975 552 10000 69 ------------- HashSet - - - - - - - - - - - - size add contains iterate 91 94 10 308 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 soportar 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 mantener 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. Selección de un tipo de mapa Este programa nos proporc iona una indicac ión de los compromisos de ren di miento en las distintas implementaciones de Map : 1/: containers / MapPerformance.java // Ilustra la diferencias de rendimiento entre mapas. // {Args: 100 SOOO} pequeño para mantener corto el tiempo de prueba import java.util. * ¡ public class MapPerformance st a tic ListcTestcMapclnteger,Integer»> tests = new ArrayListcTestcMapclnteger,Integer»>{ ) ; static { tests.add (new TestcMapclnteger,Integer» ( lIput" ) int test (Mapclnteger,Integer> map, TestParam tp l int loops = tp.loops; int size = tp.size¡ for(in t i = O; i c loops; i++) map.clear(} ; for{int j = O; j <: size¡ j++) map.put ( j. j ) ; return loops * size¡ ) ) 1; tests. add {new Test map, TestParam tp) int loops = tp.loops; int span = tp . size * 2; for ( int i = O; i < loops; i++ ) for ( int j = O; j < span; j++ ) map.get ( j ) ; return loops * span; ) )) ; tests. add {new Test map, TestParam tp) { int loops = tp.loops * 10; for (int i = O; i < loops; i ++) Iterator it = map.entrySet ( ) . iterator ( ) ¡ while(it.hasNext()) it . next () ¡ return loops * map.size(); ) )) ; 570 Piensa en Java public static void main (String [] args) { if(args.length > O) Tester.defaultParams = TestParam , array (args) ; Tester.run(new TreeMap{), tests); Tester.run(new HashMap(}, tests); Tester.run(new LinkedHashMap< I nteger, Integer> () ,tests); Tester.run( new IdentityHashMap() tests) i Tester , run(new WeakHashMap{), tests); Tester.run(new Hashtable(), tests) i I / * 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 287 204 132 100 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 10 100 1 000 1 00 0 0 264 181 260 1245 get iterate 11 3 105 201 13 4 113 76 80 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 ordenarlos 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 supuesto. esto sólo tiene semido si el co mportamicmo de un contenedor HashMap no es aceptable, ya que HashMap está diseñado 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 utilizando 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 iteración es más rápida. ldentit)'l:JashMap tien e un rendimi ento di sti nto porque utiliza = WeakHashMap se describe más adelante en el capitulo. en lugar de equals( ) para hacer las co mparaciones. 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 utilizar Collections.binarySearch() con el fin de buscar la clave. Compare el rendimiento de la nu eva ve rsió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. Factores de rendimiento que afectan a HashMap Resulta posible optimizar manualmente un contenedor HashMap para incrementar su rendimiento de cara a una apli cación concreta. Pero para poder comprender las cuestiones de rendimiento a la hora de optimizar un contenedor HashMap, 110S hace falta algo de tenninología: Capacidad: el número de segmentos de la tabla. Capacidad inicial: el número de segmentos en el momento de crear la tabla. HashMap y HashSet tienen constnlclores que penniten especi ficar la capacidad inicial. TalllUlio: el núm ero de entradas que hay actualmente en la tabla. Factor de carga: Tamaño/capacidad. Un faclor de carga de O representa un a tabla vacía, 0.5 es un a tabla medi o llena, etc. Una tab la Ligeramente cargada tendrá menos colisiones y por tanto resulta óptima de ca ra a las inserciones y búsquedas (aunque ralentizará el proceso de recorrer el contenedor con un iterador) l-IashMap y HashSet tienen constructores que pemliten especificar el factor de carga, lo que significa que cuando se alcanza este factor de carga, e l contenedor incrementará automáticamente la capacidad (el número de segmentos), por el procedimi ento de aproximadamente duplicar dicho número y luego redistribuir los objetos existentes en el nuevo conjunto de segmentos (este proceso se denomina rehashing). El fac tor de carga predetenninado utilizado por HashMap es 0.75 (no efeclúa una redi stribución hasta que la tabla está llena en sus tres cuartas partes). És te parece ser un buen compromiso entre los costes de tiempo y de espacio de almacenamiento. Un factor de carga más alto reduce el espacio requerido por la tabla pero incrementa el coste de búsqueda, lo cual es importante. porque la mayor parte del tiempo es búsquedas (incluyendo tanto get() como put( )). Si sabemos de antemano que vamos a almacenar numerosas entradas en un contenedor HashMap, crearlo con una capacidad inicial suficientemente grande evitará el gasto adicional del cambio de tamaño automático. 11 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 exceda 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, Class type) checkedList(List, Cla ss type) checkedMap(Map, Class keyType, Class valucTypc) checkedSet(Set, Class I)'pe) checkedSortcdMap(Sorted Map, C lass keyType, Class valueType)checkedSortedSet(SorledSet, C lass type) Produce una vista dinámicamente segura con respecto é1 tipos de Collection, o de un subtipo específico de Collection. Utilice este método cuando no sea posible emplear la versión con comprobación esuitica. Ya hemos visto estos métodos en el Capitulo 15, Genéricos, en la secc ión "Seguridad dinámica de tipos". max(Collection) rnin(Col1eetion) Devuelve el elemento máximo o mínimo contenido en el argumento utilizando el método de comparación natural de los objetos de la colección. max(Co llection, Comparator) min(Co llection, Comparator) Devue lve el elemento máximo o mínimo del objeto Co lleerion utilizando el 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. replaceAII(List, T oldVal, T ncwVal) Reemplaza todos los va lores oldVal por newVal. re\'erse(List) Invierte el orden de IOdos los elementos en la li sta . reverseOrd er( ) re\'crseOrder(Comparator. La segunda versión invierte el orden del objeto Co mparator sumin istrado. rotate(List, inl distanee) Mueve todos los elementos hacia adelante una distancia distance, extrayéndolos 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 shuffle(List) shuffle(Lisl, Rando m) Pennuta de manera aleatoria la lista especificada. La primera forma proporciona su propio mecanismo de aleatorizac ión, o bien podemos proporcionar nuestro propio mecanismo con la segunda fonna. so rt (List, Comparator e) Ordena la lista List utilizando una ordenación natural. La segunda forma permite proporcionar un objeto C ompara tor para la ordenación. copy(Li st des l, List src) Copia elementos desde src a des t. 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 nosotros. fill (List, T x) Sustituye todos los elementos de la lista por x. nCo pies(int n, T x) Devuelve una lista inmutable List de tamaiio n cuyas referencias 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( ) emlltyM ap( ) emptySetO Devuelve un objeto List. M ap o Set vacío inmutable. Son objetos genéricos, por lo que el objeto Co llec tion resultante estará parametrizado con el tipo deseado. sin gleton (T x) si ngletonList(T x) sin gletonMa p( K kcy, V valu e) Produce un objeto Sct, List, o Map< K,V> inmutable que contiene una única entrada basada en los argumentos proporcionados. list(E num eration e) Produce Wl objeto ArrayList 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 de estilo antiguo para el argumento. apun~ Observe que min() y max( ) fu ncionan con objetos Collectio n no con listas, por lo que no es necesario preocuparse acerca 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.*¡ public class Utilities { static List list = Arrays.asList { Ilone Two three Four five six one".split ( 1I " » i public static void main (String(] args ) { print (list ) i print (" list disjoint (Four)?: + 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, I I 11 574 Piensa en Java String.CASE_"NSENS"TIVE_ORDER» ; print ("min w/ comparator: " + Collections. min (list, String.CASE_INSENSITIVE_ORDER» ; List 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 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 e = Collections.enumeration(dups); Vector v = new Vector() ¡ 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 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 dups: [snap, snap, snapl 'list' disjoint 'dups'?: true arrayList: * jjj ,- (snap, snap, snap] 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. Ordenaciones y búsquedas en las listas Las utilidades para realizar ordenaciones y búsquedas en las listas tienen los mismos nombres y signaturas que se emplean para ordenar matrices de objetos, pero se trata de métodos estáticos de Collections en lugar de ser métodos de Arrays. He aquÍ un ejemplo que emplea la lista de datos list de Utilities.java : JI: containers / ListSortSearch . java JI Ordenación y búsqueda en listas con las utilidades de Co!lections. import java.util.*¡ import static net.mindview.util.Print .*¡ public class ListSortSearch { public static void main(String[] args) List list = new ArrayList (Utilities.list) i list.addAll(Utilities.list) i print(list) i Callections.shuffle(list, new Random(47» i print("Shuffled: " + list) ¡ II Utilizar Listlterator para eliminar los últimos elementos: Listlterator it = list.listlterator {lO); while (i t. hasNext ()) ( it. next () ; it . remove() ¡ print ("Trimmed : " + list) i Colleccions.sort(listl ¡ print{"Sorted: " + list) i String key = list . get(7) ¡ int index = Collections.binarySearch(list, key) ¡ print ("Location of " + key + " is " + index + ", list.get(tI + index + ti ) = " + list.get{index»¡ Collections.sort(list, String.CASE_INSENSITIVE_ORDERl ¡ print ("Case-insensitive sorted: " + list) i key = list.get(7); index = Collections . binarySearch{list, key, String.CASE_INSENSITIVE_ORDER) ; print ("Location of " + key + is + index + ", list.get(II + index + ") = " + list.get(index»¡ It 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 six, three, three, Two] Location of three is 7, lisc.get ( 7 ) = three *// / , Al igual que cuando se reali zan búsquedas y ordenaciones con matrices, si ordenamos utili zando un objeto Comparator, debemos efectuar la búsqueda con binarySearch( ) usando el mismo objeto Comparalor. Este programa también ilustra el método shuftle( ) de Colleclions, que aleatoriza el orden de una lista. Se crea un objeto Listlterator en una posición concreta de la lista al eatori zada y se utili za para eliminar los elementos comprendidos entre dicha posición y el final de la lista. Ejercicio 40: (5) Cree una clase que contenga dos objetos Slring y haga que sea de tipo Comparable de modo que la co mparación sólo tenga en cuenta el primer objeto String. Rellene una matriz y un contenedor ArrayList con objetos de esa clase, utilizando el generador RandomGenerator. Demuestre que la ordenaci ón funciona apropiadamente. Ahora defina un objeto Compa rator que sólo ten ga en cuenta el segundo objeto String y demuestre que la ordenación fun ciona correctamente. Asimismo, realice una búsqueda binaria utitizando ese objeto Comparator. Ejercicio 41: (3 ) Modifique la clase del ejercicio anterior para que funcione con contenedores HashSel y como clave en contenedores HashMap. Ejercicio 42: (2) Modifique el Ejercicio 40 para que se utilice una ordenación alfabética. Creación de colecciones o mapas no modificables A menudo, resulta con veniente crear una versión de sólo lectura de una colección o mapa. La clase Collections pernlite hacer esto, pasando el contenedor original a un método que devuelve una versión de sólo lectura. Hay diversas variantes de este método, para colecciones (si no se puede tratar un objeto Collection como si fuera de tipo más específico), li stas, conjuntos y mapas. El siguiente ejemplo muestra la fonna de construir versiones de sólo lectura de cada uno de estos contenedores: // : containers/ReadOnly . java // Utilización de los métodos Collections . unmodifiable. import java . util. * ; impor t net.mindview.util .* ¡ import static net.mindview . util.Print. * ¡ public class ReadOnly { static Collection data = new ArrayList (Countries.names (6 )) i public s t atic void main (String[] args ) { COllection c = Collections.unmodifiableCollection ( new ArrayList (data )) ¡ print (c ) i / / Se puede leer // ! c. add ( "one" ) i / / No se puede modificar List a = Collections.unmodifiableList { new ArrayL i st (data) ) ; Listlterator lit = a.listlterator {) ¡ print(lit . next{) ) ; // Se puede leer j i! lit.add ( " one" ) i / / No se puede modificar Set s = Col l ections.unmodifiableSet ( new HashSet{data)); print(s ) ; // Se puede leer ji ! s . add {"one " ) ; // No se puede modificar // Para un contenedor SortedSet : Set ss = Collections.unmodifiableSortedSet ( 17 Análisis detallado de los contenedores 577 new TreeSet (data) ); Map ro = Collections . unmodifiableMap( new HashMap(Countries . capitals(6))) print(m); // Se puede leer JI! m.put("Ralph", "Howdy!") i i JI Para un contenedor SortedMap: Map sm = Collections.unmodifiableSort edMap( new TreeMap(Countries.capitals{6))); / * 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 devolver una referen cia de sólo lectura a di cho co ntenedor desde una llamada a método. De este modo, podemos cambiar el contenedor dentro de la clase, pero desde cualquier otro lugar sólo podrá leerse. Sincronización de una colección o un mapa La palabra clave synchron ized es una parte importante del tema de la programación multihebra, un tema más complicado del que no hablaremos hasta el Capínilo 2 1, Concurrencia. Aquí. nos limüaremos a resaltar que la clase Collections contiene una fonna de sincronizar aut omáticamente un contenedor completo. La sintaxis es simila r a la de los métodos '\mmodifi able": jj: containersjSynchronization.java jj Utilización de los métodos Collections.synchronized. import java . util.*¡ public class Synchronization { public static void main(String[] args) { Collection c = Collections . synchronizedCollection{ new ArrayList{)) i List list = Collections . synchronizedList{ new ArrayList()) ¡ Set s = Collect i ons.synchronizedSet( new HashSet()) i Set ss = Collections . synchronizedSortedSet( new TreeSet{)) i Map m = Collections.synchronizedMap( new HashMap{)) i Map sm = Collections . sync h ronizedSortedMap{ new TreeMap()) i 578 Piensa en Java 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 contenedor 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ñadir 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.*¡ public class FailFast public static void main (String [J args} { Collection e = new ArrayList () ; Iterator it = c . iterator () ; c.add ( "An object" } ¡ try { String s ~ it . next(); catch(ConcurrentModificationException e} { System.out.println (e } ; 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 adquirir 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 impiden 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 objeto 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 intermedios. 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 implementar "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 flexible 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 obligatoriamente 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; id; ) public VeryBig (String id) { ident public String toString () { return ident; } protected void finalize() { System.out.println(!!Finalizing !! + ident); public class References { private static ReferenceQueue rq new ReferenceQueue() ¡ public static void checkQueue () { Reference inq = rq.poll() ¡ if(inq !::; null) System.out.println(ltln queue: " + inq.get()); public static void main(String[] args) int size = 10; II O bien elegir el tamaño a través de la línea de comandos: if(args.length > O) size = new Integer(args[Ol}; LinkedList{ new VeryBig(ItSoft 11 + i) ,rq)) ¡ System.out.println("Just created: 11 + sa.getLast())¡ checkQueue{) ; LinkedList( new VeryBig ("Weak " + i), rq)); System. out. println ("Just created: checkQueue() ; 11 + wa. getLast () ) SoftReference s = new SoftReference (new VeryBig ( IISoft ji) WeakReference w = new WeakReference (new VeryBig ( "Weak H ) i ) ; ; System.gcll; LinkedLisc( new VeryBig ( " Phantom " + i), rq}); System . out.println(IIJust created: " + pa.getLast()); checkQueue() ; / * (Execute to see output) * /// :- Cuando se ejecuta este programa (conviene redirigir la salida a un archivo de texto para poder ver la salida página a página), podemos ver que los objetos se depuran de la memoria, aún cuando seguimos teniendo acceso a ellos a través del También podemos ver que el objeto objeto Reference (para obtener la referencia rea l al objeto se utili za get( RefercnccQ ueuc siempre genera un objeto Refe rence que contiene un objeto nuLl. Para usar éste, herede de una clase Referencc concreta y aliada otros métodos más úti les a la nueva clase. ». 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 almacenamiento 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 proceso 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 permita 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) ¡ protected void f inalize () { System. out. println ( " Finalizing getClass() .getSimpleName {) + + + ident) ¡ 17 Análisis detallado de los contenedores 581 class Key extends Element public Key (String id) { super (id) i class Value extends Element public Value (String id) super (id) i public class CanonicalMapping { public static void main(String[] args) int size = 1000; // O bien elegir tamaño a través de la línea de comandos: if(args . length > O) size = new Integer(args[O]) i Key(] keys = new Key[size); WeakHashMap map = new WeakHashMap () i for {int i = O; i < size¡ i++} { Key k = new Key(Integer.toString(i)} i Value v = new Value {Integer.toString ( i » i f (i % 3 == O) keys [i ) = k; map.put(k, v); i /1 Guardar como referencias "reales" System.gc() i / * (Execute to see outputl */ // :- La clase Key debe tener sendos métodos hashCode( ) y equals(), puesto que está siendo usada como clave en una estructura de datos hash. Ya hemos hablado anterionnente en el capítulo del método ha,hCode(). Cuando se ejecuta el programa, vemos que el depurador de memoria se saltará una de cada tres claves, porque en la matriz keys se ha incluido también una referencia nOffi1al a dichas claves, por lo que esos objetos no pueden ser depurados de la memoria. Contenedores Java 1.0/1.1 Lamentablemente, hay una gran ca ntidad de código que se escribió util izando los contenedores de Java 1.011.1 , incluso hoy día se sigue describiendo algo de código nuevo empleando dichas clases. De modo que aunque nunca vayamos a uti lizar los contenedores antiguos a la hora de escribir nuevo código, sí que tenemos que ser conscientes de su existencia. De todos modos, los contenedores antiguos eran bastante limitados, as í que es mucho lo que se puede contar acerca de ellos. Y, como son anacrón icos, trataremos de evitar poner demasiado énfasis en alguno de los detalles relati vos a las decisiones de diseño que con esos contenedores se tomaron. 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 frecuencia. Sus defectos son demasiado numerosos como para describirlos aquí (véase la primera edición de este libro, disponible 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 adaptó Vector para que pudiera funcionar como un contenedor de tipo a Collection y de tipo List. Esta solución no es excesivame 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 enumeración contiene más elementos y Object nextElement( ) devuelve el siguiente elemento de esta enumeración si es que existe (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 encontramos 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.*¡ public class Enumerations { public static void main(String[] args) { Vector e = v.elements{); while(e.hasMoreElements()) System .out.print (e.nextElement () + ", It); /1 Generar un objeto Enumeration a partir de otro Collection: e = Collections.enumeration(new ArrayList()) i / * 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 iteradar 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 contenedor Stack de Java 1.0/ 1.1 es que en lugar de utilizar un Vector empleando el mecanismo de composición, Stack hereda 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 programas (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 JI: containers/Stacks . java // Ilustración de la clase Stack. import java.util.*; import static net.mindview.util.Print. *; enum Month ( JANUARY, FEBRUARY, MARCH, APRIL, MAY, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER ) JUNE, public class Stacks { public static void main (String [J args) { Stack stack = new Stack(); for(Month ID : Month.values()) stack.push(m.toString()) ; print("stack = ,. + stack); // Tratar una pila como un Vector: stack. addElement ( "The last line n ); priot ( "element S = 11 + stack. elementAt (5) ) priot ("popping elements: 11 ) ; while(!stack.empty()) printnb(stack.pop() + " " ) i i /1 Utilizar un objeto LinkedList como una pila: LinkedList lstack = new LinkedList (); for(Month ro : Month.values()) Istack.addFirst (m .toString ()) ; print("lstack = " + lstack); while (!lstack .isEmpty () ) printnb (lstack. removeFirst () + " " ); II Uso de la clase Stack del Capítulo 11, II Almacenamiento de objetos: net.mindview.util . Stack stack2 new net.mindview.u til.Stack (); for (Month m : Month.values()) stack2.push(m.toString()) ; print ( n stack2 = " + stack2); while(!stack2.empty()) printnb (stack2 . pop () + It 11); 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 tambié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 ligeramente 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 almacenando algún conjunto de bits men or, como por ejemplo, 8 bits, con BitSet estaremos desperdiciando buena parte del espacio; 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 contengan 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. · ¡ pub l ic class Bits { public sta tic void printBitSet (BitSet b ) print ( "bits: " + b ) ; StringBuilder bbits = new StringBuilder () ; for (int j = O; j < b.size () ; j++ ) bbits . append(b .get(j) ? " 1 " : "O"); print (" b it pattern : " + bbit s ) i public static vo id main (String[] args ) Random rand = new Random (47 ) ¡ II Tomar bit de menos peso de nextInt () : byte bt = (byte)ra nd.nextInt(); BitSet bb = new BitSet () ; for (int i = 7 ¡ i >= O; i-- ) if ((( l « i ) bb.set ( i ) ; & bt ) != O) el se bb.clear (i ) ; print ( "byte value : " + bt); printBitSet (bb ) ; 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 /1 Probar conjunto de bits >= 64 bits: BitSet b127 = new BitSet () ; b127.set(127} ; print{"set bit 127: " + b127) i BitSet b255 = new BitSet(65) i b255 . set(255} ; print("set bit 255: 11 + b255); BitSet bl023 = new BitSet (S 12 ) ; bl023.set (1023 } ; bl023.set(1024} ; print("set bit 1023: " + bl023 ) ; / * 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 posibilidad de cometer erro res en el programa. EnumSet también nos impide añadir accidentalmente nuevas posiciones binarias, 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 operaciones 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 mayorí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 distintas 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 contenedores. 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 diferentes. Evidentemente. esta solución era mejor que la que había disponible antes de que aparecieran las clases de contenedores 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 equilibrio. 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 primeras bibliotecas Java, esos aspectos extraños no son simples accidentes, sino decisiones de diselio cuidadosamente adoptadas 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 caracteres, 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 cIases 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 suplementada 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 "antiguas") 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 utilidad 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 realidad no es asÍ. De hecho, "FilePath" (ruta de archivo) hubiera sido un nombre mucho mejor para esa clase. Puede represe nta 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 flexibles, 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 . Una utilidad para listados de directorio Suponga que deseamos obtener un listado de directorio. El objeto File puede uti liza rse de dos maneras distintas. Si invocamos list() sin ningún argumento, obtendremos la lista completa contenida en el objeto File. Sin embargo, si queremos una 588 Piensa en Java lista restringida, por ejemplo, todos los archivos que tengan la extensión .java, entonces debemos utilizar un "filtro de directorio", 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 : JI io / DirList.java Mostrar un listado de directorio utilizando expresiones regulares. jj {Args: "D .* \ .java"} import java . util . regex .* ; import java . io .*¡ import java.util. * ; public class DirList public static void main (String[] args ) { File path St r ing [] = new File ( " . ~ ) ; list; if ( args.length == O) list path . list () ; else list path . list (new DirFilter (args[O] )) ¡ Ar r ays . sort(list, String . CASE_INSENSITIVE_ORDER ) ; fo r (String dirItem : list ) System.out . println (dirItem) ¡ class DirFi l ter implements Fi l enameFilter private Pattern pattern¡ public DirFilter(String regex) { pattern = Pattern . compile ( regex ) ¡ public boolean accept {File dir, String name ) return pattern.matcher (name ) . matches {) ¡ / * 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 "retrollamar" a accept() con el fin de determinar qué nombres de archi vos deben incluirse en la lista. Debido a esto, esta estmctura 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 implemente 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. Clases internas anónimas Este ejemplo es ideal para reescribirlo empleando una clase ¡ntema anónima (concepto descrito en el Capítulo 10, Clases internas). Como primera aproximación se crea un método filter( ) que devuelve una referencia a un objeto filenameFiJter : jI : io / DirList2 .java // Utilizac ión de clases internas anónimas. // {Args, "D.* \ .java" } import j ava.util.regex.*¡ import java.io .*; impo rt java.util.*¡ public class DirList2 public static FilenameFilter filter (final String regex ) { II Creación de la clase interna: return new FilenameFilter {) { private Pattern pattern = Pattern.cempile(regex ) ¡ public boolean accept (File dir, String name) { return pattern . matcher(name) .matches() ¡ }¡ I I Fin de la clase interna anónima public static void main (String {] args ) { File path = new File ( " ." ) ¡ String [] list¡ if (args.length == O) list path.list () ; else list path .list (f ilter (args {O] ) ) ; Arrays.sort (list, String.CASE_ INSENSITIVE_ORDER ) ; for (String dirItem : list ) System.out.println {dirltem ) ¡ 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.*; public class DirList3 590 Piensa en Java publi c sta tic vo id main (f inal String [1 args ) { File path = new File ( "." } ; String [J list; if(args.length == O) list path.list () ; else list path .list (new FilenameFilter () { private Pattern pattern = Pattern.compile (args [Ol ) ; public boolean accept (File dir String name ) { return pattern.matcher (name) .matches ( ); } }) ; Arrays.sort (list, String.CASE_ INSENSITIVE_ORDER ) ; for(String dirltem ; list ) System.out.println (dirltem ) ; I 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 archivos 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 archivos para nosotros. La siguiente clase de util idad produce una matri z de objetos File en el directorio local utili zando el método local( ) o un objeto List 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 public final class Oirectory { public static File[] local (File dir, final String regex) { return dir.liscFiles(new FilenameFilter() private Pattern pattern = Pattern.compile(regex); public boolean accept (File dir, String name) { return paccern.maccher( new File (name) .gecName(» .matches{) i } }) ; public static File(] local (String path, final String regex) { II Sobrecargado return local(new File (pach) , regex); II Una tupla de dos elementos para devolver una pareja de objetos: public sta tic class Treelnfo implements Iterable public List files = new ArrayList(); public List dirs = new ArrayList(); II El elemento iterable predeterminado es la lista de archivos: public Iterator iterator () { return files.iterator(); void addAll(Treelnfo other) files.addAll(other.files) ; dirs.addAll(other.dirs) ; public Str i ng toString () { + PPrint . pformat(dirs) + return "dirs : lI\n\nfiles : 11 + PPrint . pformat (files); public static Treelnfo walk (String start, String regex) { /1 Comenzar recursión return recurseDirs{new File{start), regex); public static Treelnfo walk (File start, String regex) { II Sobrecargado return recurseOirs(start, regex); public static Treelnfo walk (File start) { lITado return recurseDirs (start, ". * ti) i public static Treelnfo walk(String start) return recurseOirs(new File(start), ". * "); static Treelnfo recurseDirs(File startDir, String regexl { Treelnfo result = new Treelnfo(l i for(File item startOir.listFiles() if{item.isDirectory()) ( result.dirs.add(item) ; result . addAll (recurseDirs (item, regex»; else II Archivo normal i f (item.getName() .matches(regex» result.fi l es.add(item) ; return resul t; II Prueba simple de validación : 592 Piensa en Java public static void main(String[] args) if{args.length == O) System.out.println(walk(u.,,}) ; el se for(String arg : args) System out.println{walk(arg)); 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 normales 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, 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(); publie static void pprint (Colleetion e) System.out.println{pformat(c) i { public statie void pprint {Object [] el { System.out.println(pformat(Arrays.asList(c») ; 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 exista 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 ejemplo 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.*; public class DirectoryDemo { public statie void main (String [] args) { 1/ Todos los directorios: PPrint. pprint (Directory. walk ( " . ") . dirs) ; 1/ Todos los archivos que comiencen con 'T' for(File file: Directory.local(II.", print (file) ; "T.*")) print(II----------------------l') ; JI Todos los archivos Java que comienzan con 'T': for (File file: Directory.walk(".", "T.* \\.j ava" )) print(file) ; print("====== === =============") ; / / Todos los archivos de clase que contengan "Z" o "z": for (File file: Directory.walk{".",".*[Zz] .* \\ .class"» print (file) i / * Output: (ej emplo ) l. \x filesl . \TestEO F.class . \ TestEOF. java .\TransferTo.class .\TransferTo.java . \ 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 ) ¡ private Strategy strategy¡ private String ext; public ProcessFiles (St rategy strategy, String ext ) { this.strategy = strategy¡ this. ext = ext i public void start (String [] args) try { if(args.length == O) { 594 Piensa en Java processDirectoryTree(new File{".!!)); else far (String arg File fileArg args) { 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 / * (Execute to see output) * /// :La interfaz Strategy está anidada dentro de ProcessFiles, de modo que si queremos implementarla debemos implementar ProcessFiles.Strategy, que proporciona más infonnación al lector. ProcessFiles realiza todo el trabajo de localizar los archivos que tengan una extensión concreta (el argumento ext del constructor), y cuando localiza un archivo que cumpla con el criterio simplemente se lo entrega al objeto Strategy (que también es un argumento del constructor). Si no le damos ningún argumento, ProcessFiles asume que queremos recorrer todos los directorios a partir del direclOrio actual. También podemos especificar un archivo concreto, con o sin la extensión (el programa añadirá la extensión en caso necesario) o UIlO o más directorios. En maine ) podemos ver un ejemplo básico de utili zación de la herramienta; en el que se imprimen los nombres de todos los archivos fuente Java de acuerdo con la linea de comandos que proporcionemos. Ejercicio 4: (2) Utilice Directo ry.wa lk( ) para sumar los tamalios de todos los archivos de un árbol de directorios cuyos nombres se correspondan con una expresión regular concreta. Ejerci cio 5: (1) Modifique ProcessFiles.java para que busque correspondencias con una expresión regular en lugar de con una extensión fija. Búsqueda y creación de directorios La clase File es algo más que una mera representación de un directorio o un archivo existente. También podemos utilizar un objeto FUe para crear un nue vo directorio o una ruta completa de directorios, si es que no existe. También podemos examinar las características de los archivos (tamaño, fecha de la última modificación, pennisos de lectura/escritura), para ver si un objeto File representa a un archivo o a un directorio, y borrar un archivo. El ejemplo siguiente muestra algunos de los otros métodos disponibles en la clase File (véase la documentación del JDK disponible en http://jovo.sun. com para conocer el conjunto completo de métodos): 18 Entrada/salida 595 JI: io/MakeDirectories.java /1 Ejemplo de uso de la clase File para crear /1 directorios y manipular archivos. // {Args: MakeDirectoriesTest} import java.io.*; public class MakeDirectories private static void usage () { System.err.println( UUsage:MakeDirectories pathl ... \n" + "Creates each path\n" + "Usage:MakeDirectories -d pathl ... \n'! + "Deletes each path\n" + "Usage:MakeDirectories -r pathl path2\n" "Renames fram pathl to path2 11 ) ; System.exit(l) i private static void fileData(File fl + { System.out.println( "Absolute path: " + f,getAbsolutePath() "\n Can read: 11 + f .canRead () + + "\n Can write: + f.canWrite() + + f. getName () + getParent : + f. getParent () + getPath: 11 + f.getPath( ) + length: " + f.length() + lastModified: u + f .lastModified () ) ; "\n getName: 11 " \n lI\n " \n " \n if I f . isFile (1 1 System.out.println(IIIt's a file"); else if(f.isDirectory()) System.out . println(IIIt's a directory"); public static void main(String[] if (args.length < 1) usage(); if largs [O) . equals 1" -rOO 11 args) { { if(args .length != 3) usage() ¡ File old = new File (args[1] ), rname = new File (args(2) ) ¡ old.renameTo(rname) ¡ fileData (old) ¡ fileData(rname) ; return¡ II Salir de main 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 fileData (f) 11 + f); i 1* Output: (80% match) created MakeDirectoriesTest Absolute path: d:\aaa-TIJ4\code\io\MakeDirectoriesTest Can read: true Can write: true getName: MakeDirectoriesTest getParent: null getPath : MakeDirectoriesTest length: O lastModified: 1101690308831 It's a directory */// ;- En fileData() podemos ve r diversos métodos de consulta de archivos utilizados para mostra r información ace rca del archivo o de la ruta de directorios. El primer método utilizado por main() es renameTo(), que pennite renombrar (o desplazar) un archivo a una ruta de directorios nueva, representada por el argumento, que es otro objeto File. Esto fu nciona también con directorios de cualquier longitud. Si experimenta con el programa anterior, verá que puede construir una ruta de directorios todo lo compleja que quiera, porque mkdirs() se encarga de hacer el trabajo por nosotros. Ejercicio 6: (5) Utilice PrneessFiles para encontrar todos los arch ivos de código fuente Java en un subárbol de directorios concreto que hayan sido modificados después de una fecha concreta. 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 inados 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 ilaremos 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. Clase Función Argumentos del constructor Cómo utilizarlos ByteA rray lnpu tStrea m Pennile utilizar un bllffer de memoria como un Inp utStream. - - El buffer de l cual hay que extraer los bytes. Como origen de datos: conéctelo a un objeto FilterInputStrea m para proporcionar una interfaz útil. 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. Como origen de datos: conéctelo a un objeto Filter lnputStrearn para proporcionar una interfaz útil. 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 Seq ue neeI nputStrearn Genera los datos que estén siendo escritos en el objeto PipedOutput-Strearn asociado. Implementa el concepto de "canalización " de flujos de dalas. Convierte dos o más objetos ln putStream en un único InputStream. PipedO utp utStrearn Como origen de datos en programación multihebra: conéctelo a un objeto Filter lnputStream para proporcionar una interfaz útil. Dos objetos InputStrea m o un objeto Enumeration para un con tenedor de objetos In putStream. Como origen de datos: conéctelo a un objeto Fil terln putStrea m para proporcionar una interfaz útil. Filterln p utStream Clase abstracta que actúa como interfaz para las clases decoradoras que proporcionen funcionalidad útil a las otras clases InputStrea m. Véase la Tabla E/S.3. 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. Tabla E/S.2. Tipos de OutputStream. [ Clase ByteArrayOutputStream Función Argumentos del constructor .. -e Cómo utilizarlos Crea un bl~tJe,. en memoria. Todos los datos que se envíen al flujo de datos se colocan en este buffer. II Tamaño inicia l opcional del bl!Uer. Para designar el destino de los datos: conéctelo a un objeto F'ilterOutputStream para proporcionar una interfaz útil. FileOutputStream Para enviar infornlación a un arch ivo. Un objeto String que representa el nombre del archivo o un objeto File o FileDescriptor. Para designar el destino de los datos: conéctelo a un objeto FilterOutputStream para proporcionar una interfaz útil. PipedOutputStrcam FilterOutputSt.-eam Cualqui er infonnación que escribamos en este flujo de datos tennina siendo automáticamente la entrada para el objeto PipcdlnputStream asociado. Impl ementa el concepto de "canalización" de fluj os de datos. Clase abstracta que actúa como interfaz para las clases decoradoras que proporcionan funcionalidad útil a las otras clases OutputStream. Véase la Tabla EISA. PipcdlnputStream Para designar el destino de los datos en programación multihebra: conéctelo a un objeto FilterOutputSt.-eam para proporcionar una interfaz útil. Véase la Tabla EISA. - Véase la Tabla E/S.4. Adición de atributos e interfaces útiles Los decoradores ya han sido introducidos en el Capítulo 15, Genéricos. La biblioteca de E/S de Java requiere muchas co mbinac iones diferentes de caract erísti cas, y ésta es la justificación de utilizar el patrón de di sefi.o Decorador. 1 La razón de la exislencia de las clases "filtro" de la biblioteca de E/S de Java es que la clase abstracta de " filtro" es la clase base para todos los decoradores. Un decorador debe lener la misma interfaz que el objeto al que deco re. pero e l deco rad or también puede ampliar la interfaz, que es algo que ocurre en varias de las clases de " filtro". Existe, sin embargo, un problema con la estrategia Decorador. Los decoradores nos dan mucha más fle xibilidad a la hora de escribir un programa. (ya que se pueden mezclar y ajuslar fácilmente los atributos). pero aumentan la complejidad del código. La razón de que la biblioteca de E/S de Java sea tan complicada de utilizar es que es necesario crear muchas c lases (la clase de E/S "básica" más todos los decoradores) para poder di sponer de ese único objeto de E/S que desea mos. Las clases que proporcionan la interfaz de decoración para controlar un flujo de datos lnputStream o OutputStream concreto so n FilterlnputStrcam y FilterOutputStream, que no tien en nombres muy intuiti vos. FilterInputStream 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 lenguajes. 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 requisito clave de los decoradores (para que puedan proporcionar la interfaz común para todos los objetos que están siendo decorados). Lectura de un flujo InputStream con FilterlnputStream Las clases FilterlnputStream realizan dos tareas significativamente distintas. DatalnputStream pennite leer difcrcmcs tipos de datos primitivos, así como objetos String (todos los métodos empiezan con "rcad", como por ejemplo readByte(), readFloat( ), etc. Esta clase, junto con su compañera DataOutputStream, pennitc desplazar datos primitivos de un lugar a otro a través de un flujo de datos. Esos "l ugares" están detemlillados por las clases de la Tabla E/S.I. Las clases FilterlnputStream restantes modifican la forma en que se comporta illtemamente un flujo InputStream : indican si éste dispone de huffer o no, si llevan la cuenta de las líneas que están leyendo (10 que nos permite preguntar por los números de línea o asignar números de línea), y si se puede retroceder un único carácter. Las dos últimas clases parecen pensadas para permitir la escritura de un compilador (probablemente se añadieron para soportar el experimento de "construir un compilador de Java en Ja va"), por lo que probablemente no los use nunca en tareas de programación general. La mayor parte de las veces será necesario almacenar la entrada en un buffer, independientemente del dispositi vo de E/S con el que nos estemos conectando, así que habría tenido más sentido que la biblioteca de E/S dispusiera de un caso especial (o simplemente una llamada de método) para la entrada sin buffer en lugar de para la entrada con bllffe/: Ta bla E/S .3. Tipos de FilterlnpulStream .-. Clase Argumentos del const ructor Función Cómo utilizarlos DatalnputStream B ufferedl nputStream I',",'"m~,' "'"",",m Se utiliza conjulltamente con DataOutputStream, para poder leer primitivas (int, char , long, etc.) de un flujo de datos de una manera portable. Utilice ésta para evitar una lectura física cada vez que desee más datos. Lo que estamos diciendo es: "Utiliza un buffer". Contiene una interfaz completa con la que leer tipos primilivos. InputStream , con un tamailo de blljJer opcional. No proporciona una interfaz per se. Só lo aiiade la capacidad de hujIer al proceso. Asocie un objeto interfaz. Lleva la cuenta de los número ~ de línea Input8tream en el flujo de dalos de entrada: podemos invocar a getLincNumber() y se t LincNumber(int). PushbacklnputStream InputStream Dispone de un buffer de retroceso de un único byte para poder retroceder al último carácter leído. I línea~ Sólo añade la I1l11nerac16n de probablemente la utIlicemos aSOCIando un obJe10 interfaz. InputStrcam -- Generalmente, se utiliza en el análisis sintáctico realizado por un compilndor. Probablemente no utilice nunca esta clase. Escritura de un flujo OutputStream con FilterOutputStream El complemento a Data Illp utStream es Da taOut putStream , que fonnatea cada uno de los tipos primitivos y objetos String en un flujo de daws de tal manera que cualquier objeto Da ta ln pu tStrea m, en cualquier máquina, pueda leerlos. Todos los métodos comienzan con " write" , como por ejemplo wr iteByte(). writeFloat(), etc. 600 Piensa en Java La intención original de PrintStream era impri mir todos los tipos de datos primitivos y objetos Strin g en una forma legible. Esto difiere de DataOutputStream , cuyo objeti vo es insertar los elementos de datos en un flujo de datos de tal manera 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ícitamente 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 problemas 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 escrituras 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. Tabla E/S.4. Tipos de FilterOutpulStream . Clase - -- - DataOutputStrcam PrintStream Función - - - Argumentos del constructor ~-- Se uti li za en conjunción con DatalnputStream para poder escri bir pri~ mitivas (int, char, long, etc.) en un flujo de datos en una fonna ponab le. - Cómo utilizarlos - ~- OutputStream Contiene una interfaz comp leta que permite escribir tipos primitivos. OutputStream, con un valor boolean opcional que Para generar salida fonnateada. Mientras 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 escrinlra fisica cada vez que se envíe un elemento de datos. Estamos diciendo: ··Utiliza un buffer". Puede utilizar flush( ) para vaciar el buffer. OutputStream, con un tamaño de buffer opcional. No proporciona una in terfaz per se. Simplemente añade un buffer al proceso. Asócielo a un objeto 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 intentaban 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 intenció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": InputStreamReader 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 16bits), las jerarquías Reader y Writer se añadieron para soportar Unicode en todas las operaciones de E/S. Además, las nuevas bibliotecas están diseñadas para que sus operaciones sean más rápidas que las de las bibliotecas antiguas. Orígenes y destinos de los datos Casi todas las clase originales para flujos de datos de E/S de Java disponen de clases Reade r y Writer correspondientes con el fin de pennitir la manipulación nativa de datos Unicode. Sin embargo, existen algunas ocasiones en las que los flujos InputStream y OutputStream orientados a bytes constituyen la solución correcta; en particular, las bibliotecas java.util.zip están orientadas a bytes, en lugar de a caracteres. Por tanto, el enfoque más apropiado consiste en fratar de utilizar las clases Reader y Writer siempre que se pueda y descubrir aquellas situaciones en las que haya que utilizar las bibliotecas orientadas a bytes; resulta fácil descubrir esas situaciones, porque los programas no podrán compilarse en caso contrario. He aquí una tabla que muestra la correspondencia entre los orígenes y los destinos de infonnación (es decir, de dónde vienen y a dónde van fisicamente los datos) en las dos jerarquías. Origenes y destinos: clase Java 1.0 Clase Java 1.1 correspondiente JnputStream Reader adaptador: JnputStreamReader Output5trcam Writer adaptador: OutputStream\Vriter Filelnput5tream FileReader FileOutput5tream FileWriter StringBu fferl n putSt rea m (desaconsejado) StringReader (no hay clase correspondiente) String\Vriter ByteArrayl nputStrearn CharArrayReader ByteArrayOutputStream CharArrayWriter Pipedl nputStream PipedReader PipedOutputStream Piped\Vriter En general, encontrará que las interfaces para las dos jerarquías son similares, sino idénticas. Modificación del comportamiento de los flujos de datos Para los flujos de tipo InputStrearn y OutputStream, los flujos de datos se adaptaban para cada necesidad particular utilizando subclases "decoradoras" de FilterlnputStream y FilterOutputStream. Las jerarq uías de clases de Reader y Writer continúan utilizando esta idea, pero no exactamente. En la siguiente tabla, la correspondencia es algo menos precisa que en la tabla anterior. La diferencia se debe a la organización de las clases; aunque HufferedOutputStream es una subclase de FilterOutputStrearn, BufferedWriter no es una subclase de FilterWriter (la cua l, aunque es abstracta no tiene subclases y parece, por tanto, que se ha incluido simplemente para poder utilizarla en el futuro o para que no nos rompamos la cabeza preguntándonos dónde está). Sin embargo, las interfaces de las clases sí que se parecen bastante. 602 Piensa en Java ~ Filtros: clase Java 1.0 Clase Java 1.1 correspodiente filterlnputStream --- ~ FilterOutputStrcam FilterReader - - - - - Filtcr\Yriter (clase abstracta sin subclases) f-----Bu ffcred 11l1)utStrea nI BuffercdReadcr (también tiene readLine(» BufferedOutputStream Buffered\Vriter DatalnputStream Utilice DatalnputStream (excepto cuando necesite usar readLine(), en cuyo caso debe emplear BufferedRcadcr) PrintStrcam Print\\'riter LineNumberlnputStream (desaconsejado) LineNumberReader St rcamTokenizer StreamToke nizer (u tili ce el constructor que admite un objeto Reader) 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 objeto 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: Clases Java 1.0 sin clases Java 1.1 correspondientes DataOutputStream File RandomAccessFile Sequencel npu tStream DataOutputStream, en concreto, se utili za sin modificación, por lo que para almacenar y extraer datos en un fonnato transportable, 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 desplazarnos 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 interfaces DataInput y DataOutput (que también son implementadas por DataInputStroam y DataOutputStroal11). Ni siquiera 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. descendiente 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 encontramos, 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. Utilización típica de los flujos de E/S Aunque podemos combinar las clases de nujos de E/S de muchas maneras distintas, lo más probable es que en nuestros programas utilicemos sólo unas cuantas combinaciones. Los siguientes ejemplos pueden usarse como referencia básica de lo que constituye una utilización típica de los mecanismos de E/S. En estos ejemplos, simplificaremos el tratamiento de excepciones pasando las excepciones a la consola, pero esta forma de proceder sólo resulta apropiada en utilidades y ejemplos de pequeño tamaño. En el código de los programas reales, conviene utilizar técnicas de tratamiento de errores más sofisticadas. Archivo de entrada con buffer Para abrir un archivo con entrada orientada a caracteres, utilizamos un objeto FilelnputReader con un objeto String o File como nombre de archivo. Para aumentar la ve locidad, conviene asociar con el archivo un buffer, para lo cual se proporciona al constructor la referencia a un objeto BuffercdReader. Puesto que BuffcredRcader también proporciona el método readLine( ), éste será nuestro objeto final y la interfaz a través de la cual efectuaremos las lecturas. Cuando readLine( ) devuelva null , habremos alcanzado el final del archivo. 11 : io/BufferedlnputFile.java import java.io.*; public class BufferedlnputFile II Pasar excepciones a la consola: public static String read(String filename) throws IOException 1I Leer la entrada línea a línea: BufferedReader in : new BufferedReader ( new FileReader(filename) ) ; String s; StringBuilder sb = new StringBuilder(); while{ (s : in.readLine()) 1= null) sb.append(s + to\n to ); in. close () ; return sb.toString(); 604 Piensa en Java public static void main(String[] args ) throws IOException { System. out. print (read ( "BufferedlnputFile. java") ) ¡ 1* (Ejecutar para ver la salida) *1 1/ :El objeto Strin gBuilder sb se utiliza para acumular el contenido completo del archivo (incluyendo los avances de línea que haya que añadir, ya que readL ine( ) los elimina). Finalmente, se invoca c1ose( ) para cerrar el archiv0 2 Ejercic io 7: (2) Abra un archivo de texto para poder leer el contenido de línea en línea. Lea cada línea en forma de una cadena de caracteres y sitúe dicho objeto Strin g dentro de un contenedor Li nked List. Imprima todas las lineas de Linked Lis t en orden inve rso. Ejercicio 8 : ( 1) Modifique el Ejercicio 7 para proporcionar como argumento de la línea de comandos el nombre del archivo que haya que leer. Ejercicio 9 : (1) Modifique el Ejercicio 8 para pasar a mayúsculas todas las líneas del contenedor Li nk edList y envíe los resultados a System.out. Ejercicio 10: (2) Modifique el Ejercicio 8 para adm itir argumentos adicionales en la línea de comandos que especifiquen palab ras que haya que encontrar en el archivo. Imprima todas las líneas que contengan alguna de las palabras. Ejercicio 11: (2) En el ejemplo inner classes/G reenhouseCo nt r oUer.java, G ree nhouseController contiene un conjunto precodificado de sucesos. Modifique el programa para que lea dos sucesos y sus instantes relativos de un arch ivo de texto (nivel de dificultad 8): utilice un patrón de diseño basado en el Método defoctoria para construir los sucesos; consulte Thinking in Palterns (wi/h Java) en \Vw\V.MindView.l1el. Entrada desde memoria Aquí, el objeto Strin g resultante de BufferedlnputFile.read() se uti liza para crear un objeto StringReader . A continuación, se utiliza rea d() para leer de carácter en carácter y enviar los datos a la consola: /1 : io/Memorylnput.java import java.io . *; public class Memorylnput public static void main(String[] args) throws IOException { StringReader in = new StringReader( BufferedlnputFile. read (uMemorylnput. java") ) ¡ int c¡ while ((c = in.read()) != -1 ) System.out.print( (cha r le); / * (Ejecutar para ver la salida) * 11/ :Observe que read( ) devue lve el sigui ente carácter como un valor int y debe, por tanto, proyectarse el resultado sob re un valor cha r para imprimirlo adecuadamente. Entrada de memoria formateada Para leer datos "fonnateados", ut ilizamos un flujo Data InputStrea rn, que es una clase de E/S orientada a bytes (en lugar de a caracteres). Por tanto, debemos uti lizar todas las clases I npu tSt rea m en lugar de clases Reader. Por supuesto, pode2 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 segura 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 utiliza es una cadena de caracteres: JI : io/FormattedMemorylnput.java import java.io.*; public class FormattedMemorylnput public static void main(String[] args) throws IOException { try ( DatalnputStream in = new DatalnputStream{ new ByteArraylnputStream( BufferedlnputFile.read{ "Format tedMemorylnput. java") . getBytes () ) ) ; while(true) System.out.print((char)in.readByte{)) ; catch (EOFException el { System.err.println{"End of stream"); / * (Execute to see output) * /// :A un flujo ByteArraylnputStream hay que proporcionarle una matriz de bytes. Para generarla, String dispone de un método getBytes(). El flujo ByteArraylnputStream resultante es un objeto InputStream apropiado para entregárselo a un flujo DatalnputStream . Si leemos los caracteres de un flujo DatalnputStream de byte eo byte usando readByte( ), todo valor de tipo byte es un resultado legítimo, por lo que el valor de retorno no puede utilizarse para detectar el final de la eotrada. En lugar de ello, puede emplearse el método available( ) para detenninar cuántos caracteres más hay disponibles. He aquí un ejemplo que muestra cómo leer un archivo de byte en byte: 11: io/TestEOF.java II Comprobación del final del archivo mientras se lee de byte en byte . import java . io.*¡ public class TestEOF public static void main(String[] args) throws IOException { DatalnputStream in = new DatalnputStream( new BufferedlnputStream( new FilelnputStream (tlTestEOF . java U) ) ) while (in. available () ! = O) System.out .print((char)in.readByte()) ; i 1* (Ejecutar para ver la salida) * 111:Observe que avaílable( ) funciona de manera distinta dependiendo del tipo de medio del que estemos leyendo; literalmente, ese método nos da "el número de bytes que pueden leerse sin que se produzca un bloqueo". Con un archivo, esto significa todo el archivo. pero con otro flujo de datos distinto podríamos obtener alguna otra cosa, así que utilice el método con cuidado. También podemos detectar el final de la entrada en casos como éste tratando de capturar una excepción. Sin embargo. el uso de excepciones para control de flujo se considera una técnica poco recomendable. Salida básica a archivo Un objeto FileWriter escribe datos en un archivo. Prácticamente en todas las ocasiones nos convendrá añadir un bujfer a la salida, envolviendo el objeto en otro objeto BufferedWriter (trate de eliminar este objeto envoltorio para ver el impacto sobre el rendimiento: la utili zación de buffel's ayuda a incrementar enormemente el rendimiento de las operaciones de E/S). En este ejemplo, se utilizaPrintWriter como decorador para que se encargue de las tareas de fomlateo. El archivo de datos creado de esta fonna se puede leer como un archivo de texto nonna!. 606 Piensa en Java JI: io/BasicFileOutput.java import java.io.*; public class BasicFileOutput static String file = "BasicFileOutput.out"i public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader( new StringReader( BufferedlnputFile. read ("BasicFileOutput. java") ) ) i PrintWriter out = new PrintWriter( new BufferedWriter{new FileWriter(file))); int lineCount = 1; String Si while((s = in . readLi n e()) != null} out .println (lineCount++ + ": " + sl out. clase () ; i /1 Mostrar el archivo almacenado: System , out . println(BufferedlnputFile , read(file)} ; / * (Ejecutar para ver las salida) * ///:A medida que se escriben li neas en el archivo, se añaden los números de lí nea. Observe que no se utiliza LineNumberReader, porque es una clase muy simple y no resulta necesaria. Como podemos ver en este ejemplo, resulta trivial llevar la cuenta de nuestros propios números de línea. Una vez que se han terminado los datos del flujo de entrada, readLine( ) devuelve null . El ejemplo muestra una llamada explícita a close( ) para out, porque si no invocamos a close( ) para todos los archivos de salida, podríamos encontrarnos con que los buffers no se vaciarán, con lo que el archivo estaría incompleto. Un atajo para realizar la salida correspondiente a un archivo de texto Java SES ha añadido W1 constructor a PrintWriter para que no tengamos que encargamos de reali zar a mano todas las tareas de decoración cada vez que queramos crear un archivo texto y escribir en él. El ejemplo siguiente muestra el archivo BasicFileOutput.java reescrito con esta nueva técnica: //: io/FileOutputShortcut . java import java.io .*¡ public class FileOutputShortcut static String file = FileOutputShortcut .out" i public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader{ new StringReader( Buf f eredInputFile . read{ tl Fil e OutputSho rt cut . java lt ) ) ) // He aquí la técnica más simple: PrintWriter out = new PrintWriter{file) ¡ int lineCount = 1; String s; whi l e{{s = in . readLine{)) != nul l } out . println {lineCount+ + + ": " + s}; out . close{} ; // Mostrar el archivo almac e nado : Sy stem.out .pri ntln{Buffe r e d Input File . re a d(fil e }} ; ti /* ; (Ejecuta r pa r a ver la salida) */// : - 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 iremos 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úmeros 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 Almacenamiento y recuperación de datos Un obj eto PrintWriter formatea los datos para que res ulten legibles. Sin embargo, para sacar los datos de manera que puedan ser recuperados por otro /lujo de datos, se llti~ za DataOutputStream para escribir los datos y DatalnputStream para recuperarlos. Por supuesto, estos flujos de datos pueden ser cualquier cosa, pero en el sigui ente ejempl o se utiliza un archivo, usándose buffers tanto para lectura como para escritura. DataOutputStream y DataInputStream están orientados a bytes y requi eren por tanto, fluj os de datos InputStream y OutputStream : 11: io/StoringAndRecoveringData.java import java.io. * ; public class StoringAndRecoveringData pub l ic static void main(String[] args) throws IOEx ception { DataOutputStream out = new DataOutputStream( new BufferedOutputStream( new FileOutputStream(uData . txt ll } ) ) j out.writeDouble(3 .141S9) j out . writeUTF("Th at was pi"} j out.writeDouble(1 . 41413} i out . wri t eUTF ( " Square root of 2") j out.close() ; DatalnputStream in = new DatalnputStream( new BufferedlnputStream( new FilelnputStream(IIData.txt"))}; System . out.println(in.readDouble()} i II Sólo readUTF() permite recuperar la cadena II de caracteres Java-UTF apropiadamente: System . out . println(in.readUTF()) j System.out . println(in.readDouble{)) i System.out.println(in.readUTF()} i 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 enormemente ú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 XML 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. J 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 ejemplo 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() permite 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. Lectura y escritura de archivos de acceso aleatorio Utili zar RandomAecessFile es como emplear sendos flujos DatalnputStream y DataOutputStream compilados (porque implementa las mismas interfaces: Datalnput y DataOutput). Además, podemos utilizar seek() para desplazamos por el archivo y cambiar los va lores. Para poder usar RandomAccessFile, debemos conocer la disposición del archivo para poder manipularlo adecuadamente. RandomAccessFiJe tiene métodos específicos para leer y escribir primitivas y cadenas de cara cteres UTF-8. He aq uí un ejemplo: JI : io/UsingRandomAccessFile.java import java.io.*¡ public class UsingRandomAccessFile static String file = "rtest .dat"; static void display() throws IOException RandomAccessFile rf = new RandomAccessFile (file, for(int i = O; i < 7; i++) System.out.println( "Value " + i + ": " + rf. readDouble () ) ; System.out.println(rf . readUTF()) ; rf . close () ; public static void main(String[] argsl throws IOException { RandomAccessFile rf = new RandomAccessFile ( file, for ( int i = O; i < 7; i++} rf.writeDouble(i*1.414l; rf.writeUTF ( "The end of the file" ) ; "r") ¡ "rw" ) ; 18 Entrada/salida 609 rf . clase () ; display () ; rf = new RandomAccessFile ( file, "rw" ) ; rf.seek {5 *8 ) ; rf.writeDouble{47.0001) rE. clase () ; display () ; i / * Output: Value Value Value Value Value Value Value o: 0.0 1, 1.414 2, 2.828 ), 4.242 4, 5.656 5, 7 . 069999999999999 6 , 8.484 The end of the file Value Value Value Value Value Value Value o, 0.0 1. 414 2.828 4.242 5.656 47.0001 8.484 The end of the file 1, 2, 3, 4, 5, 6, * 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 desplazarlos 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úsqueda. 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 decoració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 tenemos 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. Flujos de datos canalizados En este capitulo sólo hemos mencionado brevemente las clases PipedOutputStream, PipedReader y PipedWriter. Esto no quiere decir que dichas clases no sean útiles, pero la ventaja que proporcionan no se comprende adecuadamente hasta que no se comienza a analizar el tema de la concurrencia, ya que los flujos de datos canali zados se emplean para la comunicación entre tareas. Este tema se tratará junto con un ejemplo en el Capitulo 21, Concurrencia. Utilidades de lectura y escritura de archivos Una tarea de programación muy común consiste en leer un archivo de memoria, modificarlo y luego volverlo a escribir. Uno de los problemas con la biblioteca de E/S de Java es que nos obliga a escribir bastante código para poder realizar estas ope- 610 Piensa en Java 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 nosotros. 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 conviene 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. *; public class TextFile extends ArrayList { /1 Leer un archivo como una única cadena de caracteres: public static String read (String fileName) { StringBuilder sb = new StringBuilder(); try { BufferedReader in= new BufferedReader(new FileReader{ new File (fileName) .getAbsoluteFile ()) ; try { String S; while ( (s = in. readLine (» ! = null) { sb.append(sl; sb.append("\n") ; } finally { in. close () ; catch(IOException e) { throw new RuntimeException(e); return sb.toString() i 1/ Escribir un archivo en una llamada a método: public static void write(String fileName, String text) try { PrintWriter out = new PrintWriter{ new File (fileName) .getAbsoluteFile(»); try { out.print(text) ; finally { out.close() ; 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 1/ cadena de caracteres vacía en la primera posición: if(get(O) .equals ('''')) JI remove(O); Leer normalmente línea a línea: public TextFile (String fileName) this (fileName, "\n ll ) ; { public void write (S tring fileName ) { try { PrintWriter out ~ new PrintWriter{ new File (fi leName ) . getAbsoluteFile () ) ; try { for(String item : this) out.println{item) finally { out.close() i i 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 words = new TreeSet{ 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áusula 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 reescribir 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 embargo, 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 encuentra 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, 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 . Lectura de archivos binarios Esta utilidad es similar a TextFile.java, en el sentido de que pemlite simplificar el proceso de lectura de archivos binarios: 11: net/mindview/util/BinaryFile.java I I Utilidad para leer archivos en forma binaria. package net.mindview.util; import java.lo.*; public class BinaryFile public static byte(] read (File bFile) throws IOException{ BufferedlnputStream bf = new BufferedlnputStream( new FilelnputStream(bFile)); try ( byte[1 data = new byte[bf.available () ]; bf.read (data ) ; return data; finally ( bf .elose () ; public static byte(] read (String bFile ) throws IOException { return read (new File (bFile ) .getAbsoluteFile ()) ; ) /// ,Un método sobrecargado admite un argumento File; el segundo admite un argumento String, que representa el nombre del archivo. Ambos devue lven la matriz de tipo byte resultante. Se utiliza el método available( ) para obtener el tamaño apropiado de la matriz y esta ve rsión concreta del método read( ) sobrecargado se encarga de rellenar la matriz. Ejercicio 19: (2) Utilizando BinaryFilc y un contenedor Map. cree un programa que cuente el número de apariciones de los diferentes bytes dentro de un archivo. Ejercicio 20 : (4) Utilizando Directory.wa lk( ) y BinaryFile, verifique que todos los archivos .e1ass en un árbol de directorios comienzan con los caracteres hexadecima les ·CAFEBABE '. 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 programa (esta idea se reproduce en cierta manera en Windows y muchos otros sistemas operativos). Toda la entrada de un programa 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ácilmente 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 Lectura de la entrada estándar Siguiendo el modelo de la E/S estándar, Java dispone de Systcm.in , Systcm.out y Systcm.cr r. A lo largo de este libro hemos visto cómo escribir en la salida estándar utilizando System.out, que está ya pre-envuelta en un objeto PrintStream. System. er r es también un objeto PrintStream , pero System.in es un objeto lnput Stream simple sin ningún tipo de envoltorio. Esto significa que aunque podemos uti lizar Systcm.out y Systcm .err de mane ra directa, es necesario envolver System.in antes de leer el mismo. Nomlalmente, leeremos la entrada de línea en línea empleando readLin e(). Para hacer esto, envuelva Systcrn.in en un objeto Bu ffered Read er , lo que requiere que convierta Syste m.in en un objeto Reader mediante In p utStreamReader . He aquí un ejemplo que se limita a devolver como un eco cada línea que escribamos: jj , iojEcho.java JJ Cómo leer de la entrada estándar. jj {RunByHand} import java.io . *¡ public class Echo public static void main(String[] args) throws IOException { BufferedReader stdin = new BufferedReader( new InputStreamReader (Syst em.in )) ; String Si while( (s = stdin.readLine{)) 1= null && s.length{)!= O) System.out . println(sl; JJ Una línea vacía o Ctrl-Z hace que se termine el programa } // j,- 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). Cambio de System.out a un objeto PrintWriter System.out es un objeto PrintStream , que a su vez es de tipo O utputStrea m . PrintWriter tienen un constructor que toma un objeto O utp utStrea m como argumento. Por tanto, si queremos convertir System. out en un objeto PrintWrite r utilizando dicho constructor: JJ: ioJChangeSystemOut.java /J Transformación de System.out en un objeto PrintWriter. import java.io.*; public class ChangeSystemOut { public static void main (String [] args ) { PrintWriter out = new PrintWriter (System .out, out .println ( "Hello, world"); truel i 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. Redireccionamiento de la E/S estándar La clase Systc m de Java pernlite redirigi r los flujos de E/S estándar de entrada, de salida y de error ut iliza ndo simples llamadas a los métodos estáticos: 614 Piensa en Java 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 entrada 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.*; public class Redirecting public static void main(String[] args) throws IOException { PrintStream console = System.out; BufferedlnputStream in = new BufferedlnputStrearn ( new FilelnputStream ( "Redirecting . java") ) ; PrintStream out = new PrintStream { new BufferedOutputStream( new FileOutputStream (" tes t. out It) ) ) i System . setln(in) i System . setOut(out) i System.setErr(out) i BufferedReader br = new BufferedReader( new InputStrearnReader{System . in»); String s¡ while «s = br.readLine ()) != null} System . out . println(sl; out. close () ¡ II Remember this! System.setOut(console) ¡ } /// ,Este programa asocia la entrada estándar con un archivo y redirige la salida estándar y la salida de error estándar a otro archivo. Observe que almacena al principio del programa una referencia al objeto System.out original y que restaura la salida del sistema hacia dicho objeto al final del programa. El redireccionamiento de E/S manipula flujos de bytes, no flujos de caracteres; es por eso que se utili zan objetos InputStream y OutputStream en lugar de Reader y Writer. Control de procesos A menudo es necesario ejecutar otros programas del sistema operativo desde dentro de Java y controlar la entrada y la salida 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 presentar 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¡ 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. 4 18 Entradaisalida 615 public class OSExecuteException extends RuntimeException public OSExecuteException(String why) { super(why); } } //1,Para ejecutar un programa, hay que pasar a OSExecute.command() una cadena de caracteres command, que es el mismo comando que esc ribiríamos para ejecutar el programa en la consola. Este comando se pasa al constructor java.lang. ProcessBuilder (qu e requiere que se le suministre en fa nna de una secuencia de objetos String), después de lo cual se inicia un obj eto ProcessBuilder: JI: net/mindview/util/OSExecute.java /1 Ejecutar un comando del sistema operativo // y enviar la salida a la consola. package net.mindview.util; import java.io. * ; public class OSExecute public static void command(String command) { boolean err ~ false; try ( Process process ~ new ProcessBuilder{command.split{1I " )) .start(); BufferedReader results ~ new BufferedReader( new InputStreamReader(process.getInputStream())) i String Si while ((s ::o resul ts . readLine () ) ! = nulll System . out .println(sl i BufferedReader errors = new BufferedReader( new InputStrearnReader(process.getErrorStream())) i // Informar de los errores y devolver un valor distinto de // cero al proceso llamante si existen p r oblemas: while((s = errors . readLine())!= null) { System.err.println(s) i err = true; 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 cornmand) ; ti + } // /,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 errores, 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.*¡ public class OSExecuteDemo { public static void main (String (] args ) ( OSExecute.command ( "javap OSExecuteDemo"); /* 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. Los paquetes new La "nueva" biblioteca de E/S de Java introducida en los paquetes java.nio.* el1 el JDK lA tiene como principal objetivo aumentar la velocidad. De hecho, los "antiguos" paquetes de E/S se han reimplementado uti lizando Dio para poder aprovechar este incremento de la velocidad, así que nos podremos beneficiar de esa mayor velocidad incluso aunque no escribamos código con nio. El incremento de velocidad se hace patente tanto en la E/S de archivos, que es la que vamos a explorar aquí, como en la E/S de red. de la que se trata en T/¡inking in Enlelprise Java. La mayor velocidad se obtiene utilizando estructuras que están más próximas a la fonna en que se realiza la E/S en el sistema operativo: canales y bu./Jers. Podríamos utilizar el símil de una mina de carbón: el canal sería la mina que contiene la veta del carbón (los datos) y el buffer sería la vagoneta que introducimos en la mina. La vagoneta sale de la mina llena de carbón y nosotros extraemos el carbón de la vagoneta. En otras palabras, nosotros no interactuamos directamente con el canal sino que lo hacemos con el buffer, enviando el bujIer al canal. El canal extrae datos del buffer o pone datos en el buffel: El único tipo de buffer que se comunica directamente con el canal es ByteBuffer, un bujIer que almacena bytes sin ningún tipo de fonnato. Si examinamos la documentación del JDK para java.nio.ByteBuffer, podremos ver que se trata de una clase muy básica: creamos uno de estos buffer:;; diciéndole cuánto espacio de almacenamiento hay que asignar y existen métodos para insertar y extraer datos, bien como bytes sin fonnato o como tipos de datos primitivos. Pero no existe manera de insertar o de extraer un objeto, ni siquiera de lipa String. Es una clase de nivel bastante bajo, precisamente porque esto hace que se pueda implementar de una forma más eficiente la mayoría de los sistemas operativos. Tres de las clases del "antiguo" esquema de E/S han sido modificadas para poder generar un objeto FileChannel: FileInputStream , FileOutputStream y, tanto en lectura como en escritura, Ra ndomAccessFile. Observe que se trata de los flujos de datos para manipulación de bytes, en consonancia con la naturaleza de bajo ni vel de ni o. Las clases Reado r y \Vriter en modo carácter no generan canales, aunque la clase java.nio.channels.Channels dispone de métodos de utilidad para generar objetos Reader y Writer a partir de canales. He aquí un ejemplo simple donde se pmeban los tres tipos de flujo de datos con el fin de generar canales de escritura, de lectura/escritura y de lectura: 11 : io / GetChannel.java II Obtención de canales a partir de flujos de datos import java.nio.*; import java.nio.channels.*¡ import java.io.*; public class GetChannel private static final int BSIZE 1024; 18 Entrada/salida 617 public static void main{String[] args) throws Exception { // Escribir un archivo: FileChannel fe = new FileOutputStream ("data. txt") . getChannel () i fc,write{ByteBuffer.wrap("Some text ".getBytes ())}; fe. clase () ; JI Añadir al final del archivo: fe = new RandomAccessFile ( "data. txt", "rw" ) .getChannel () ; fc.position {fc .size( )}; JI Desplazarse al final fe. wri te {ByteBuffer. wrap (" Sorne more". getBytes () ) ) i fe. clase () ; JI Leer el archivo: fe = new Fi lelnputStream ("data. txt 11) • getChannel () ; ByteBuffer buff fc.read(buffl; = ByteBuffer.allocate(BSIZE); buff .flip l) ; while (buff.hasRemaining ()) System.out.print( (char)buff.get ()); / * Output: Sorne text Sorne more *///;Para cualquiera de las clases de flujos de datos mostradas aquí, getChannel( J generará un objelO F'ileChannel. Los canales 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 hacemos 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 allocate( 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 adicional 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.·; public class ChannelCopy { private static final int BSIZE = 1024; public static void main(String[] args) throws Exception { i f (args .length ! = 2 ) { System. out. println ( lIarguments: sourcef ile destfile!l) i System.exit (1); 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 cuando FileC ha nn el.read( ) devuelve - ( (una reminiscenc ia, sin lugar a duda, de Unix y C), querrá decir que hemos alcanzado 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á estando 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 Utilización de transferTo() entre canales {Args: TransferTo.java TransferTo.txt} import java.nio.channels.*¡ import java.io.*; II II public c lass TransferTo public static void main(String[) if(args.length != 2) args) throws Exception { { System. out .println ("arguments: System.exit(l) ; sourcefile destfile") ¡ 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 conveniente conocerlas. Conversión de datos Si volvemos a examinar GetChannel.j ava , observaremos que para imprimir la información del archivo, estamos extrayendo 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 analizando 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. * ; public class BufferToText prívate static final int BSIZE 1024; public static void main(String[] args) throws Exception FileChannel fe = new FileOutputStream ( " data2. txt") .getChannel () ; fe. write (ByteBuffer. wrap ( "Sorne text .getBytes () ) ) i fe. clase () ; 11 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 introducimos (para que tengan significado cuando se los extraiga) o decodificar/os a medida que salen del bllffer. Esto se puede conseguir 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.*¡ public class AvailableCharSets { public static void main(String[] args) SortedMap charSets = Charset.availableCharsets() i Iterator it = charSets.keySet() .i terator() i while(it.hasNext()) { String csName = it.next() i printnb(csName) ¡ Iterator aliases = charSets. get (csName) . aliases () . i terator () i if(aliases.hasNext(}) printnb(": "); while{aliases.hasNext()) printnb(aliases.next()) i if(aliases.hasNext()) printnb{H, 11 ) i 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, ksc56011987, 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 decode( ), 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 nombre 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 convertirlo 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 (analizaremos 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 posició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 valores primit ivos a parti r de los bytes que contiene. Este ejemplo ilustra la inserción y la extracción de varios valores empleando 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.*; public class GetData { private static final int BSIZE = 1024; public static void main(String(] args} ByteBuffer bb = ByteBuffer.allocate(BSIZE} ¡ II El proceso de asignación pone a cero II automáticamente el objeto ByteBuffer: int i = O¡ while(i++ < bb.limit()} iflbb.get() [o 01 print ("nonzero " ) ; print("i = " + i }¡ bb.rewind(} ; II Almacenar y leer una matriz char: bb. asCharBuffer () . put ("Howdy! ") ¡ char C; while(c = bb.getChar(») != O) printnb(c + " "J¡ print() ; bb.rewind() ; II Almacenar y leer un valor short: bb. asShortBuffer 11 . put Ilshort 14 711421 ; printIbb.getShort11 1; bb.rewind() ; II Almacenar y leer un valor int: bb.aslntBufferll .putI994711421; print Ibb.getlnt 11 1 ; bb.rewind() ¡ II Almacenar y leer un valor long: bb.asLongBuffer() .put (99471142) ¡ printlbb . getLonglll; bb.rewind() ¡ II Almacenar y leer un valor float: bb . asFloatBuffer{) .put{99471142); print Ibb.getFloat 11 1 ; bb.rewind() ; II Almacenar y leer un valor double: bb.asDoubleBuffer() .put(99471142); print(bb.getDouble{») i bb . rewind() ; 622 Piensa en Java / * 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 proyecció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( ). Buffers utilizados como vistas Los ubuffers utili zados como vistas" penniten examinar un objeto ByteBuffer subyacente a través de la ventana que proporciona cada tipo primitivo concreto. El objeto ByteBuffer seguirá siendo el almacenamiento real que está "respaldando" a esa vista. por lo que cualquier cambio que se realice en la vista se verá reflejado en las correspondientes modificaciones de los datos contenidos en el objeto ByteBuffer. Como podemos ver en el ejemplo anterior, esto nos permite insertar cómodamente tipos primitivos en un buffer de tipo ByteBuffer. Una vista pennite también leer tipos primitivos a partir de un objeto ByteBuffer, bien de uno en uno (tal como lo pemlite ByteBuffer) o por lotes (almacenando en matrices). He aquí un ejemplo en el que se manipulan objetos int en un objeto ByteBuffer a través de una vista IntBuffer: // : io/lntBufferDemo.java // Manipulación d e valores int en un objeto ByteBuffer mediante IntBuffer import java.nio.*; public class IntBufferDemo private static final int BSIZE = 1024; public static void main(String[] args } ByteBuffer bb = ByteBuffer.allocate (BSIZE ) ; IntBuffer ib = bb.aslntBuffer(}; // Almacenar una matriz de valores int: ib.putlnew intlJ{ 11, 42, 47, 99, 143, 811, 1016 }); // Lectura y escritura en posiciones absolutas: System . out.println(ib . get(3)) ; ib.putI3,1811); 1/ Establecimiento de un nuevo límite antes rebobinar el buffer. ib.flipl) ; while (ib .hasRemaining ()) int i = ib.get( }; System.out .println(il; / * 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.*; public class ViewBuffers { pUblic static void main (String[] args ) { ByteBuffer bb = ByteBuffer.wrap( new byte [] { O, O, O, O, O, O, O, ' a' }); bb.rewind {) ; printnb ("Byte Buffer ") ; while(bb.hasRemaining(}) printnb {bb.position{)+ " -> " + bb.get () + " ); print () ; CharBuffer eb = ( (ByteBuffer ) bb. rewind () ) .asCharBuffer () ; printnb ( "Char Buffer " ) ; while (cb.hasRemaining (» printnb(eb.position() + " -> " + eb.get() + U); print () ; FloatBuffer fb = (( ByteBuffer ) bb.rewind (» printnb ( "Float Buffer "); while(fb.hasRemaining()} printnb(fb.position()+ .asFloat Buffer () ; ->" + fb.get () + n); print () ; IntBuffer ib = ( (Byte Buffer ) bb. rewind () ) . aslntBuffer () ; printnb("Int Buffer "); while (ib.hasRemaining {» printnb(ib.position{)+ " -> " + ib.get() + "); + "); + U) ; print (); LongBuffer lb = (( ByteBuffer ) bb.rewind ()) .asLongBuffer(); printnb ( "Long Buffer "); while ( lb.hasRemaining (» printnb(lb.position()+ " -> " + lb.get () print () ; ShortBuffer sb = (( ByteBuffer )bb.rewind( )) .asShortBuffer () ; printnb ( "Short Buffer" ) ; while (sb.hasRemaining( » printnb (s b.position{)+ -> " + sb.get() print () ; DoubleBuffer db = (( ByteBuffer)bb.rewind( )) . asDoubleBuffer() ; printnb ( "Double Buffer ") ; 624 Piensa en Java while (db.hasRemaining () printnb(db position{)+ " -> n + db.get() + ") i / * Output: Byte Buffer O ->0,1->0, 2 - > 0 , 3 - > O, 4 - > O, 2 -> , 3 -> a, Char Buffer O -> , 1 -> 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, ° S - > O, 6 - > O, 7 - > 97, ° ° * /// ,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 O I O O O I O O O O 97 0. 0 l.3 6E-4 3 J byte a char 97 shOI1 97 int 97 Ooat long doubl e 4.8E-322 Estos va lores se corresponderían con la salida del programa. Ejercicio 24: (1 ) Modifique IntBufferDemo.java para utilizar valores double. 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 memori 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 necesario 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 inación elegida: ji: io/Endians . java JI Diferencias de terminación y almacenamiento de datos . import java . nio . *¡ import java . util. *¡ i mport static net.mindview.util . Print .* ¡ public class Endians ( public static void main(String[] ByteBuffer bb = args) { ByteBuffer,wrap(new byte [12] ) i bb . asCharBuffer() . put("abcdef " ) i print(Arrays.toString(bb . array())) bb . rewind{) i bb . order(ByteOrder . BIG_ENDIAN ) i i bb . asCharBuffer () . pu t ("a h cdef " ) i print{Arrays . toString(bb . array())) ; bb . rewind() ; bb . order{ByteOrder . LITTLE_ENDIAN) ; bb . asCharBuffer() .put(tlab cdefll); print(Arra y s . toString{bb.array())) i / , Output : [0, 97, 0 , 98, [ O, 97, 98, [97 , 0 , 98 , °, , /// , - °, 0 , 99, 0, 99, 99 , °, °, °, 0, 1 0O, 0, 101, 102] 0 , 1 0O , 0 , 101, 102] 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. Manipulación de datos con buffers El diagrama de la página sigui ente ilustra las relac iones entre las clases nio, para poder entender mej or cómo se transfieren y se con vierten los datos. Por ejemplo, si queremos esc ribir una matri z de tipo byte en un archi vo, tenemos que en vo lve r la matri z byte utilizando el método ByteBuffcr.wrap( ), abrir un canal en el flujo FileOutputStream usando el método getChannel( ) y luego escribir los datos en el can al FileChannel a partir de obj eto By te Buffer. Observe que ByteBuffer es la únjca fonna de transferir datos hacia y desde los canales y que nosotros sólo podemos crear un buffer autónomo con un tipo de datos primitivo, u obtener uno a partir de un objeto ByteBuffer empleando un método "as". En otras palabras, no se puede con vertir un buffer con tipo de datos primitivo en un objeto ByteBuffer. Sin embargo, puesto que podernos transferir datos primitivos hacia y desde un objeto ByteBuffer a través de un buffer de vista, esta res~ tricción realmente no es tal. Detalles acerca de los buffers Un objeto Buffer está compuesto por datos y por cuatro índices que penniten acceder a estos datos y manipularlos eficientemente: marca, p osición, límite y capacidad. Existen métodos para asignar valores a estos índices, para reinicializarlos y para consultar su valor (véase la tabla de las Páginas 626·627). 626 Piensa en Java ceí 1) l Sistema de archivos subyacente o red t t Socket DatagramSocket ServerSocket FilelnpulStream FileOutputStream RandomAccessFile r1 t Utilidades Canales getChannelO write(BvteBuffer) FileChannel ByteBuffer I read(ByteBuffer) map(FileChannel.MapMode , position , size) ;- -- -- - -~ - ------- -- ;, :, I MappedByteBuffer I ,, : Apare?8 en el espacio de : : direCCiones del proceso : ~--- - --------------_. arraYO/get(byteO) I byteO ~ wrap(byteO) arrayO/get(cha rO) 1 charD 1" wrap(charO) I doubleo 1" a rray( )/get(d ou bleO) wrap(doubleO) .1 1 charBuffer • 1DoubleBuffer l arrayO/get(ftoatO) 1ftoatO 1- wrap(ftoatO) .1 FloatBuffer 1 .1 In!Buffer 1 .1 LongBuffer 1 array(Vget(intO) 1 intO 1" wrap(intO) 1 10ngO 1_ arrayO/get(longO) wrap(longO) 1shortO 1- arraYO/get(shortO) wrap(shortO) • 1ShortBuffer 1 asCharBufferO asDoubleBuffer() asFloatBufferO aslntBuffer() asLongBufferO asShortBuffer() ,. ' _ . - Codifi cación/decodifi cación utilizando ByteBuffer - . _ . _ . _ . _ . _ . _ . _ . _ . - ' , A un flujo de bytes codificado' encode(CharBuffer) Charset newDecoder() decode(ByteBuffer) De un flujo de bytes codificado capacity( ) Devuelve la capacidad del bujJ"eI: 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 utiliza para preparar el buffer para una lectura después de haber escrito datos en él. limit( ) Devuelve el valor del límite. 18 Entrada/sal ida 627 Iimit(int lim) Establece el valor del/ímire. mark( ) Establece la marca en el valor correspondiente a posición. position ( ) Devuelve el valor de posición. position(int pos) Establece el valor de posición. remaining( ) Devuelve (límife - posición). 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 caracteres contenidos en un objeto CharBuffer: /1 : io j UsingBuffers. j ava import java.nio.*¡ import static net.mindview.util.Print.*¡ public class Us i ngBuffers { private static void symmetricScramble (CharBuffer buffer ) { while (buffer. hasRemaining (» { buffer.mark () ; char e l = buffer.get () ; ehar e 2 = buffer.get () ; buffer.reset () ; bu ffe r .put (e 2 ) .put (el ) ; publie static void main {String (] args ) ehar() data = "UsingBuffers".toCharArray () i ByteBuffer bb ByteBuffer.alloeate {data.length * 2 ) ; CharBuffer eb = bb.asCharBuffer () ; cb.pu t (data l ; print (eb.rewind {» i symmetri c Sc r amble (eb ) i print {cb.rewind {) ; symmetrie Se ramble (cb ) i print (cb.rewind {}} i / * 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( ): Los dos métodos put() escriben e2 y luego el : 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 Contenido 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 restaura a su estado original. Archivos mapeados en memoria Los archi vos mapeados en memoria penniten crear y modificar arch ivos que sean demasiado grandes como para cargarlos en memoria. Con un archi vo mapeado en memoria, podemos aCnlaf como si todo el archivo se encontrara en la memoria y podemos acceder a él tratándolo simplemente como si fuera una matriz de muy gran tamaño. Esta técnica simplifica enormemente el código que es necesario escribir para poder modi fi car el archi vo. He aquí un ejemplo simple: 11 : io/LargeMappedFiles.java II Creación de un archivo de muy gran tamaño II utilizando el mapeado de memoria. // {RunByHand} import java .ni o .*; import java.nio.channels.* import java .i o.*; i mport static net.mindview.util.Print.*¡ public class LargeMappedFiles { static int length = Ox8FFFFFF; 1I 128 MB public static void main(String[] args) throws Exception MappedByteBuffer out = new RandomAccessFile ( " test. dat " , "rw" ). getChannel () .map(FileChannel.MapMode.READ_WRITE, O. length ); for(int i = O; i < length; i++) out .put ((byte) 'x'); print ("Finished writing") ; for (int i = length/2; i < length /2 + 6; i++) printnb ( (char)out .get (il); } /// , Para efectuar tanto leculras como escrituras, comenzamos con un objeto RandomAccessFile, obtenemos un canal para dicho archivo y luego invoca mos map( ) para generar un bnffer MappedByteBufTer, que es un tipo particular de bujIer directo. Observe que es necesari o espec ificar el punto de inicio y la longitud de la región en la que queramos mapear el archivo; esto implica que tenemos la posibilidad de mapear regiones pequeñas de un arch ivo de gran tamaño. MappedByteBuffer hereda de ByteBufTer, asi que dispone de todos los métodos de dicha clase. Aquí sólo mostramos los usos más simples de put( ) y get( ), pero también podemos emplear métodos como asC harBuffer( ), etc. El archi vo creado en el programa anterior tiene 128 MB de longitud, lo cual es un tamaño probablemente mayor de lo que el sistema operati vo pennitirá residi r en memoria en cualqu ier momento detenninado. El archivo parece estar completamente accesible, aunque en rea lidad só lo se cargan en memoria partes del mismo , intercambiándose por otras partes a medida que es necesario. De esta fonna, puede modificarse fácilmente un archivo de muy gran tamaño (hasta dos 2 GB). Observe que se utiliza la funcionalidad de mapeo de arch ivos del sistema operativo subyacente para maximizar el rendimiento. 630 Piensa en Java Rendimiento Aunque el rendimiento del "antiguo" mecanismo de flujos de datos de E/S se ha mejorado al implementarlo con oio, el acceso 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. *¡ public class MappedIO private static int numOflnts = 4000000; private static int numOfUbufflnts = 200000; private abstraet static class Tester { private String name; public Tester (String name) { this. name name; } public void runTest () { System.out.print(name + ": "); try ( long start = System .nanoTime (); test (); double duratían = System . nanoTime{) - start¡ System.out.format("%.2f\n", duration/l.Oe9); catch (IOExeeption e) { throw new RuntimeException(e); public abstract void test() throws IOExeeption; private statie Tester[] tests = { new Tester (" Stream Wri te") { publie void test () throws IOException DataOutputStream dos = new DataOutputStream( new BufferedOutputStream( new FileOutputStream (new File (lttemp. tmplt) » ) ; for(int i = O; i < numOflnts; i++) dos.writelnt(i) ; dos.clase() ; ), 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, . asIntBuffer () ; O, fC.size{» 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 [] for(Tester test : tests) test.runTest() ; args) { / * Output, 190\ match) Stream Write: 0.56 Mapped Write: 0.12 Stream Read: 0.80 Mapped Read: 0.07 Stream Read / write: 5.32 Mapped Read /Write: 0.02 * /// , - Como hemos visto en ejemplos anteriores de este libro, runTest() se utiliza con el método de plantillas para crear un marco de pruebas para diversas implementaciones de test( ) definidas en subclases internas anónimas. Cada una de estas subclases realiza un tipo de prueba, por lo que los métodos tcst( ) también nos proporcionan un prototipo para realizar las distintas actividades de E/S. 632 Piensa en Java Aunque podría parecer que una escritura mapeada debería utilizar un flujo FileOutputStream , todas las operaciones de salida en el mecanismo de mapeo de archivos deben utilizar un objeto RandomAccessFile, al igual que se hace con las operaciones 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 aunque 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. *; public class FileLocking public static void main(String[] args) throws Exception { FileOutputStream fos= new FileOutputStream(ltfile.txt n ) i FileLock fl = fos. getChannel () . tryLock () i i f (fl '= null) { System . out.println("Locked File" ); TimeUnit . MILLISECONDS.sleep(lOO) ; fl. release () ; System . out . println("Released Lock"J i 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 inherentemente 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, entonces 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 argumentos 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 bloqueo exclusivo. El tipo de bloqueo (compartido o exclusivo) puede consultarse utilizando FileLock.isS hared(). Bloqueo de partes de un archivo mapeado Como hemos mencionado anterionnente, el mecanismo de mapeo de archivos se utiliza principalmente para archivos de muy gran tamaiio. Puede que necesitemos bloquear partes de d icho archivo de gran tamaiio, de modo que se pennita a otros procesos modificar pa11es del archivo no bloqueadas. Esto es lo que sucede, por ejemplo, con una base de datos. de tal manera que ésta pueda ser utili zada po r ITIuchos usuarios a la vez. He aquí un ejemplo con dos hebras de programación , cada una de las cuales bloquea una parte distinta de un archivo: 11 : io/LockingMappedFiles.java II Bloqueo de partes de un archivo mapeado . // (RunByHand) import java.nio.*; import java . nio.channels.*¡ import java.io.*; public class LoekingMappedFiles static final int LENGTH ~ Ox8FFFFFF¡ II 128 MB static FileChannel fe; public static void main(String[] args) throws Exception fe = new RandomAecessFile("test.dat", tlrw tl ) .getChannel () ; MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, O, LENGTH) ¡ far (int i = Di i < LENGTH¡ i+ +} out . put ( (byte) 'x i new LockAndModify(out, O, O + LENGTH/3)¡ new LockAndModify(out, LENGTH/2, LENGTH/2 + LENGTH/4); I ) private static class LockAndModify extends Thread { private ByteBuffer buff; private int start, end; LockAndModify(ByteBuffer mbb, int start, int end) { this.start ~ start; this. end = end; mbb. limit (end) ; mbb.position(start) i buff o mbb . slice(); start() ; public void run() { try ( /1 Bloqueo exclusivo sin solapamiento: FileLock fl : : fe . lock (start, end, false) ¡ System.aut.println{"Locked: "+ start +" ta "+ end); II Realizar modificación: while(buff.pasition() < buff.limit() ~ 1) buff . put ((byte) (buff .get () + 1)); 634 Piensa en Java fl.release () ; System.out.println ( "Released: catch (I OException e l { "+start+" to "+ end ) i 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). Clase de compresión Función 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). DenaterOutputStream Clase base para clases de co mprens ión. ZipOutputStream UIl fl ujo DeflaterOutputStream que comprime datos en el fonnato de archivos Zip. GZIPOutputStream UIl fl uj o DenaterOutputStream que compri me datos en el fo nnato de archivos GZLP. lnflaterl nputStream Clase base para clases de descomprensión. 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 Compresión simple con GZIP La interfaz GZIP es simple y resuha, por tanto, apropiada cuando disponemos de un único flujo de datos que queramos com- primir (en lugar de un contenedor de fragmentos de datos poco similares). He aquí un ejemplo en el que se comprime un único archivo: //: io/GZIPcompress.java II {Args, GZIPcompress.java} import java.util.zip.*¡ import java.io.* ; public class GZIPcompress public static void main(String[) argsl throws IOException { i f (args .length == O) { System.out.println( "Usage: \nGZIPcompress file\n" + n\tUses GZIP compression to compres S " + "che file ta test.gz ll ) ; System.exit(l) ; BufferedReader in = new BufferedReader( new FileReader(args[Ol}); BufferedOutputStream out new GZIPOutputStream( = new BufferedOutputStream( new FileOutputStream ( 11 test. gz 11) ) ) ; System.out.println(IIWriting file"); int C; while«c = in.read()) != -1) out .write(c) i in. close () ; out.close() ; System. out. println ("Reading f ile U ) ; BufferedReader in2 = new BufferedReader( new InputStrearnReader(new GZIPlnputStream( new FilelnputStream (11 test . gz") ) ) l ; String Si while«s = in2.readLine()) != null) System.out.println(s) i /* (Execute to see output) * ///:La uti lización de Jas clases de compresión resulta senc illa; basta con envoJver el flujo de salida en un objeto GZIPOutputStroam o ZipOutputStream, y eJ Oujo de datos de entrada en un objeto GZIPlnputStream o ZiplnputStream. Todo Jo demás son lecturas y escrituras de E/S nom1ales. Éste es un ejemplo de mezcla de los flujos de datos orientados a caracteres con Jos Oujos de datos orientados a bytes; in utiliza Jas clases Reader, mientras que el constructor de GZIPO utputStr.am sóJo puede aceplar un objeto OutpuIStre.m , no un objeto Writer. Cuando se abre eJ archivo, el objeto GZIPlnputStre.m se convierte en un objeto Reador. Almacenamiento de múltiples archivos con Zip La biblioteca que soporta eJ fonnato Zip es más amp lia. Con este fonnato, podemos almacenar fácilmente múltipJes archivos y existe incluso una clase separada para facilitar el proceso de lectura de un archivo Zip. La biblioteca utiliza el fonnato Zip estándar, as í que funciona de fonna transparente con todas las herramientas Zip que podemos descargar actualmente a través de Internet. El siguiente ejemplo tiene la misma fonna que el ejemplo anterior, pero pennite tratar tantos argumentos de la línea de comandos como queramos. Además, ilustra el uso de las clases Checksum para calcular y verificar la suma de comprobac ión del archivo. Existen dos tipos de cJases C hecksum : AdJer32 (que es Ja más rápida) y CRC32 (que es más lenta, pero ligeramente más precisa). 636 Piensa en Java 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 . *¡ public cIass ZipCompress { public statie void main (String[] args ) throW5 IOException { FileOutputStream f = new FileOutputStream (11 test. zipl!) i CheckedOutputStream esum = new CheckedOutputStream (f, ZipOutputStream zas new Adler32 (» ; new ZipOutputStream {csum ) ; = BufferedOutputStream out = new BufferedOutputStream ( z os ) ; zas. setComment ( "A test of Java Zipping" ) ; JI Sin embargo, no hay un método getComment (} correspondiente . f or (String arg : argsl { print {IIWriting file" + arg l ; BufferedReader in = new BufferedReader(new FileReader(arg » ¡ zos.putNextEntry (new ZipEntry {arg }) ¡ int c¡ while « c = in.read (» != -1 ) out.write {c) i in . close () i out.flush {) ¡ 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 ) ; int x¡ while « x = bis . read (» ! = -1 ) System.out.write (x ) ; 11 i f (args.length = = 1) print ( "Checksum: + 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 () ; 11 18 Entrada/sal ida 637 print(UFile: " + ze2 ) i // ' .. y extraer los datos como antes / * if (args.length /* == 1) (Execute to see outpUC) */ * / / 1 :- Para cada archivo que baya que añadir al archivo comprimido, es preciso invocar putNextEntry( ) y pasarle al método un objeto ZipEntry. El objeto ZipEntry contiene una amplia interfaz que permite consultar y configurar todos los datos disponibles en esa entrada concreta del archivo Zip: nombre, tamaiios comprimido y sin comprimi r, fecha, suma de comprobación eRe, campo adiciona l de datos, comentario, método de compresión e indicador de si se trata de un directorio. Sin embargo, aún cuando el f0n11ato Zip dispone de una fonna para establecer una contraseña, esta característica no está soportada en la biblioteca Zip de Java. Y aunque CheckedloputStream y CheckedOutputStream so portan las sumas de comprobación Adler32 y C RC32 , la clase ZipEntry sólo proporciona una interfaz para CRC. Ésta es una restri cción de l fonnato Zip subyacen te, pero se trata de una restricción que puede impedirnos uti li zar la c lase Adlcr32 que es más rápida. Para extraer los archivos, ZiplnputStream dispone de un método getNextEntry( ) que de vuel ve la siguiente entrada ZipEntry, si es que existe alguna. Como altelllativa más sucinta, podemos leer el archi vo utilizando un objeto ZipFile, que dispone de un método entries( ) para devol ver un objeto de tipo E numeration con las entradas del archi vo Z ip. Para leer la suma de comprobación, debemos conseguir acceder de a lguna forma al objeto Checksum asociado. En el ejemplo, retenemos una referencia a los objetos CheckedOutputStream y CheckedlnputStream, pero tamb ién podríamos habemos limitado a conservar una referencia al objeto C hecksum . Un método bastante absurdo dentro de la biblioteca Zip es setComment( j. Como se muestra en ZipComprcss.java, podemos fijar un comentario a la hora de escribi r un archivo, pero no existe forma de recuperar el comen tario en e l objeto Ziplnput5tream. Parece que los comentari os sólo se soportan de manera completa accediendo entrada por entrada mediante ZipEntry. Por supuesto. no tenemos por qué Limitarnos a emplear archivos a la hora de utili zar las bibliotecas GZIP o Zip; podemos comprimi r cua lquier cosa, incluyendo datos que vayan a enviarse a través de una conexión de red. Archivos Java (JAR) El formato Zip también se utili za en el forma to de archivo JAR (Java ARchive), que es una fonna de recopilar un grupo de archivos en un único archivo comprimido, igual que Zip. Sin embargo, corno todos los demá s componentes de Java, los archivos lAR son archivos interplatafonna, así que no hay necesidad de preocuparse acerca de los problemas de portabilidad. Pueden incluirse tambi én archi vos de audio y de imagen, además de los archivos de clases. Los archivos JAR resultan particularmente úti les a la hora de trabaj ar con Internet. Antes de que aparecieran los archivos JAR, el explorador web tenía que reali zar solicitudes repetidas a un servidor web para poder descargar todos los archivos que co nfom1aban un appler. Además, estos archivos no estaban co mprimidos, al combinar todos los arc hi vos de un applel concreto en un único arch ivo JAR, sólo es necesa ria una solicitud al servidor y la transferencia se reali za más rápidamente, grac ias a la compresión. Además, la entrada de un archivo JAR puede estar firmada digitalmente para aumentar la seguridad. Un arc hi vo JAR está compuesto por un único archivo que contiene una colección de archivos comp rimidos en fonnato Zip, junto con un "manifiesto" que los describe (podemos crear nuestro propio manifiesto, pero si no lo hacemos, el programa jar lo hará por nosotros). Puede encontrar más infonnac ión acerca de los manifi estos JAR en la documentación del JDK. La utilidad jar incluida en el IDK de Sun comprime automáticamente los arc hi vos que e lijamos. Esta utilidad se invoca mediante la línea de comandos: jar [opciones] destino [manifiesto] archivo(s ) de entrada 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 e Crea un archivo nuevo o vacío. • Muestra la tabla de contenido . x Ex trae lodos los archivos. x archivo Ex trae el archivo indicado. 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 salida 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). M No crea automáticamente un archivo de manifiesto. 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 mientras 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, tampoco 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 platafonna (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 componentes 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 programa 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 informació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 detalles 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 regenerar 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 persistencia ligera. El concepto de persistencia quiere dec ir que el ti empo de vida de un objeto no está detem1inado por si un programa 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 podemos 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-serializar 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 comporten 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, generalmen te, en tiempo de diseño. Esta infonnación de estado debe almacenarse, para poder recuperarse posteriormente cuando 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 modificaron muchas clases de la biblioteca estándar para hacerlas serial.izables. incluyendo todos los envoltori os de los tipos primitivos, 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 enviará 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 correctamente. 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ínculos 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 stinta, 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.*; class Data implements Serializable private int n; public Data(int n ) ( this.n = n; public String toString () { return Integer . toString (n ) ; } public class Worm implements Serializable { private static Random rand = new Random(47); private Data[] d = ( new Data{rand . nextlnt(lO)), new Data(rand.nextlnt(10)), new Data(rand.nextlnt(lO») }; private Worm next; private char c; II Valor de i == número de segmentos public Worm(int i, char x l { print{"Worm constructor: " + i); e = X; if(--i :> O) next new Worm{i, (char) (x + 1)); public Worm () { print("Default constructor"}; public String toString() StringBuilder result new StringBuilder(":") result.append{c) ; resul t. append (" (") ; for(Data dat : d) result.append(dat) i result. append ( " ) ti ) ; if(next != null) result.append(next) ; return result.toString{); i public static void main(String[J argsl throws ClassNotFoundException, IOException Worm w = ne w Worm(6, 'a'); print("w = " + w); ObjectOutputStream out = new ObjectOut putStream ( new FileOutputStream(ltworm.outltl l ; out. writeObj ect ("Worm storage\n"); out.writeObject{wl; out.close{) i II También vacía la salida 18 Entrada/salida 641 ObjectlnputStream in = new ObjectlnputStream( new FilelnputStream("worm.out")) i String s = (Stringlin . readObject(); Worm w2 = (Worm)in.readObj ect () i print (s + "w2 = " + w2) i ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(bout) out2 .writeObject(tlWorm storage\n"); out2 .wri teObject(w) j out2 . flush () ; ObjectlnputStream in2 = new ObjectlnputStream( new ByteArraylnputStream(bout.toByteArray{))) j S = i (Stringlin2 . readObject(); Worm w3 (Worm)in2.readObject(); print (s + "w3 = ti + w3); / , Output: Worm Worm Worm Worm Worm Worrn w = constructor: constructor: constructor: constructor: constructor: constructor: 6 S 4 3 2 1 ,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 aleatorios (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 generación rec ursiva de la li sta enlazada de objetos Worm . Cuando se crea un Worm, se le dice al constructor la longitud que queremos 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, utilizando 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 original. 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 InputStream . 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 ejemplo. 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á contenido 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.*; public class FreezeAlien public static void main(String[] args ) throws Exception { ObjectOutput out = new ObjectOutputStream( new FileOutputStream("X.file")); Alien quellek = new Alien () ; out.writeObject(quellek) i ) 111 ,En lugar de capturar y tratar las excepciones. este programa adopta la poco elegante solución de pasar las excepciones hacia fuera de ma in( ), con lo que se infonnará de su existencia a través de la consola. Una vez compilado y ejecutado, el programa genera un archivo denominado X.fiIe en el directorio io. El siguiente código está incluido en un subdirectorio denominado xftles: 11: io/xfiles/ThawAlien.java II Tratar de recuperar un archivo serializado sin la II clase del objeto que está almacenado en dicho archivo. II (RunByHand) import java.io.*; public class ThawAlien public static void main(String(] args) throws Exception ObjectInputStream in = new ObjectInputStream( new FileInputStream (ne w File (" .. ", IIX. file") l l ; Object mystery = in.readObject{); System . out.println(mystery.getClass{)l; 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 correspondiente 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 tenemos 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.*¡ class Slipl implements Externalizable public Blipl () { print ( "Slip! Constructor" ) i public void writeExternal (ObjectOutput out ) throws IOException { print ("Slipl. writeExternal") i public void readExternal(Objectlnput in) throws IOException, ClassNotFoundException print ( "Blipl.readExternal U ) i class Blip2 implements Externalizable Blip2 () { print ( "Blip2 Constructor" ) i public vo id writeExternal (ObjectOutput out ) throws IOException { print ( "Blip2 . wri teExternal" ) i public void readExternal (Objectlnput in ) throws IOException, ClassNotFoundException print(IISlip2.readExternal U ) i public class Blips { public static void main (String[] args ) throws I OException, ClassNotFoundExc eption print ( "Constructing objects: U) i Blipl bl = new Blipl () ; Blip2 b2 = new Blip2 (); Objec tOutputStream o = new Ob j ectOutputStream ( new FileOutputStream ( "Blips .out " ) ) ¡ print ( USaving obj ects: U) ; o.writeObject (bl ) ¡ o.writeObject {b2 ) i o.close () ¡ // Ahora obtenerlos de nuevo : ObjectlnputStream in = new ObjectlnputStream( new FilelnputStream("Blips.out U » ; print ( "Recovering bl : " ) ; 644 Piensa en Java bl = (Blipl) in. readObj ect () ; 1/ ¡Ha fallado! Genera una excepción: / /! print (URecovering //! b2 } = b2: 11) i (Blip2) in. readObject (); } / * Output, Constructing objects: Slipl Constructor Blip2 Constructor Saving objects : Blipl.writeExternal Blip2 . writeExternal Recovering bl: Blip! Constructor Blipl.readExternal * /// ,La razón de que el objeto Blip2 no se recupere es que, al tratar de hacerlo, se genera una excepción. ¿Puede ve r la diferencia entre Blipl y Slip2? El constmctor de Blipl es público, mientras que el constmctor de Blip2 no lo es, yeso es lo que provoca la excepción al intentar efectuar la recuperación. Pruebe a definir como público el constructor de Blip2 y elimine los comentarios I/! para ver los resultados correctos. Cuando se recupera bl , se in voca el constructor predetenninado de BHpl . Esto difi ere del proceso nonnal de recuperación del objeto Serializable, durante el cual el objeto se reconstruye enteramente a partir de los bits almacenados, sin efectuar ninguna llamada a un constructor. Con un objeto Externalizable, tienen lugar todas las tareas normales predetenninadas de constmcción (incluyendo las inicial izaciones en los puntos donde se definen los campos), después de lo cual se in voca readExternal(). Es necesario tener esto en cuenta (en particular el hecho de que tienen lugar todas las tareas predeternlinadas de construcción), para poder obtener el comportamiento correcto de los objetos Externalizable. He aquí un ejemplo que muestra qué es lo que hay que hacer para almacenar y recuperar un objeto Externalizable: / / : io/Blip3. java // Reconstruccción de un objeto externalizable. import java . io.*¡ import static net.rnindview.util . Print. * ¡ public c lass Blip3 implements Externalizable private int i¡ private String S i / / Sin inicialización public Blip3 () { print ("Blip3 Constructor" 1 i // s, i no inicializados public Blip3 (String x, int al { print ("Blip3 (String x, int al" 1 i s = x¡ i = a¡ // s & i inicializados sólo en el constructor no predeterminado. public String t oSt ring {) { return s + i ¡ } public void writeExternal(ObjectOutput out} throws IOException { print ( "Blip3. writeExternal" J ¡ // Hay que hacer esto: out.writeObject(sJ; out.write Int (i) ¡ public void readExternal(Object I nput in) throws IOException, ClassNotFoundException print ( "Blip3 . readExternal") ¡ 18 Entrada/salida 645 JI s i Hay que hacer esto: (String)in.readObject()¡ = in.readlnt(); public static void main(Stringr] args) throws IOException, ClassNotFoundException print("Constructing objects: II ) ; Blip3 b3 = new Blip3{"A String 11, 47}; print (b3) i ObjectOutputStream o = new ObjectOutputStream{ new FileOutputStream ( "Blip3 .Qut ll »; print ( "Saving object:")¡ o.writeObject(b3) ; o.cIase{); JI Ahora extraer los datos: ObjectlnputStream in = new ObjectlnputStream( new FilelnputStream{"Blip3.out ll )); print (IIRecovering b3: 11) ; b3 = (Blip3)in.readObject{); print Ib3 I ; / * 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 almacenamiento 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áticas. 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 cuando las dos líneas fonnan parte del programa. 646 Piensa en Java La palabra clave transient Cuando estamos controlando la serialización. puede que exista un subobjeto concreto que no queramos que sea automáticamente guardado y restaurado por el mecanismo de serialización de Java. Esto sue le suceder cuando dicho subobjeto representa infonnación confidencial qu e no queramos serializa r, como por ejemplo una contraseña. Incluso si esa información es de tipo príva te dentro del obj eto, una vez que ha sido serializada resulta posible que alguien acceda a ella leyendo un arc hivo o interceptando una tran smisión de red. Una fomla de evitar que las partes confidenciales del objeto sean se ria lizadas consiste, como hemos visto previamente, en implementar la clase Exte r na lizable. En ese caso, no hay nada que se serial ice automáticamente y podemos serial izar ex plícitamente sólo aquellas partes que sean necesarias dentro de writeExt er nal( ). Sin embargo, si estamos trabajando con un objeto de tipo Seria Hzable, toda la tarea de seriali zación tiene lugar automáticamente. Para controlar esto, podemos desactivar la serialización campo a campo utilizando la palabra clave t rans ient, que lo que viene a decir es: "No te preocupes de guardar o restaurar esto, yo me haré cargo de ello". Por ejemplo, considere un objeto Logon que mantenga información acerca de un inicio de sesión concreto. Suponga que, una vez verificados los da tos de inicio de sesión, queremos almacenar los datos, pero sin la co ntraseña. La forma más fácil de hacer esto es implementando Serializa ble y marcando el campo password como tra nsient. He aquí un ejemplo: / / : io/Logon.java l/Ilustra la palabra clave 11 transient u . import java.util.concurrent. * ; import java.io.*; import java.util. *; import static net.mindview.util.Print.*; public class Logon implements Serializable private Date date: new Date(}; private String username; private transient String password; public Logon (String name, String pwd } username name; password : pwd; public String toString(} return "logon info: \ n username:" + username + "\ n date:" + date + " \ n password:" + password; public static void main(String[) args } throws Exception Logon a : new Logon ("Hulk", "myLittlePony"); print ("logon a : " + a) i ObjectOutputStream o = new ObjectOutputStream ( new FileOueputStream ( " Logon. out" ) ) ; o.writeObject(a ) ; o.close{); TimeUnit.SECONDS.sleep {1); 1/ Retardo / 1 Ahora recuperar los datos: ObjectlnputStream in : new ObjectlnputStream ( new FilelnputStream {IILogon.out lt ) ) ; print ("Recovering obj ect at " + new Date ( ) ) ; a : (Logon)in.readObject() i print (" logon a = " + a); 1* Output: (Sample) logon a = logon info: username: Hulk date: Sat Nov 19 15:03:26 MST 2005 password: myLittlePony 18 Entrada/salida 647 Recovering object at Sat Nov 19 15:03:28 MST 2005 logan a = logan info: username: Hulk date: Sat Nov 19 15:03:26 MST 2005 password: null * /// ,Podemos ver que los campos date y username son normales (no de tipo transient), por lo que se los serial iza automáticamente. 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 referencias 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. Una alternativa a Externalizable Si no desea implementar la interfaz Externalizable, existe otra técnica alternativa. Puede implementar la interfaz Scrializable y aFwdir (observe que decirnos "añadir" y no "sustituir" o "implementar") sendos métodos denominados writeObject() y readObjcct( ) que se invocarán automáticamente cuando el objeto se serialice o des-seria lice, respectivamente. En otras palabras, si proporcionamos estos otros métodos se usarán esos métodos en lugar del mecani smo predeterminado de serialización. Los métodos deben tener estas signaturas exactas: private void writeObject(ObjectOutputStream stream) throws IOExceptíon¡ prívate void readObject(ObjectInputStream stream) throws IOExceptíon, ClassNotFoundException 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 definidos 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éndome para no entrar en una larga discusión acerca de lo inapropiado que resulta utiliza r aquí los mismos nombres de métodos; 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 respuesta 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 ajustamos 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( ) personalizado. 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 decidir 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 controlarse 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. * ¡ public class SerialCtl implements Serializable { private String a¡ private transient String bi public SerialCtl (String aa, String bb) { a UNat Transient: 11 + aa i b = "Transient : " + bb¡ public String toString () { return a + "\n 11 + b i } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject() i stream.writeObject{b) i private void readObject(ObjectlnputStream stream) throws IOExeeption, ClassNotFoundExeeption stream.defaultReadObjeet(} ; b = (String}stream.readObjeet(); publie statie void main(String[] args) throws IOExeeption, ClassNotFoundExeeption SerialCtl se = new SerialCtl ("Testl t i , "Test2"); System.out.println("Before:\n ll + se); ByteArrayOutputStream buf= new ByteArrayOutputStream{); ObjeetOutputStream o = new ObjeetOutputStream{buf); o.writeObjeet (se); // Ahora recuperar los datos: ObjeetlnputStream in = new ObjeetlnputStream{ new ByteArraylnputStream(buf.toByteArray())); SerialCtl se2 = (SerialCtl) in.readObject () ; System.out.println(IAfter:\n" + sc2) i /* 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 objeto, debemos invocar defaultWriteObject() como primera operación en writeObject(), y defaultReadObject() como primera 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 argumento, 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 atentamente lo que sucede: en maín( j, se crea un objeto Seríalet! y luego se seriali za en un flujo ObjeelOulputStream (observe 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 reflexió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 probable es que no tengamos que recurrir a él más que en casos especiales; el mecanismo requiere un análisis más en profundidad 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 programa, 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-serializamos 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 .* ¡ class House implements Serializable {} class Animal implements Serializable private String name; private House preferredHouse; Animal (String nm, House h) { name = nm; preferredHouse = h¡ public String toString() return name + 11 [11 + super . toString () 11], 11 + preferredHouse + 11 \n ti i + 650 Piensa en Java public class MyWorld { public static void main(String[] args) throws IOException, ClassNotFoundException House house = new House() i List animal s = new ArrayList() i animals. add (new Animal ("Bosco the dog" I house}) i anima!s. add (new Animal ("Ralph the hamst e r tl , house)) i animals. add (new Animal ("Molly the cat ll , house}) ; print ("animals: 11 + animals) i ByteArrayOutputStream buf! = new ByteArrayOutputStream(} i ObjectOutputStream 01 = new ObjectOutputStream(bufl); ol.writeObject(animalsl i ol . writeObject(animals) i JI Escribir un segundo conjunto 1/ Escribir en un flujo de datos diferente: ByteAr rayOutputStream buf2 = new ByteArrayOutputStream(} i ObjectOutputStream 02 = new Obj ectOutputSt r eam (buf2) ; 02.writeObject(animals) i // Ahora recuperar los datos: ObjectlnputStream inl = new ObjectlnputStream( new ByteArrayl nputStream{bufl.toByteArray {»); ObjectlnputStream in2 = new ObjectlnputStream{ new ByteArrayl nputStream{buf2.toByteArray()}) i List animalsl (List) inl . readObject () animals2 (List) inl.readObject () (List) in2.readObject () ; animals3 print ("animalsl : + animalsl); print ("animals2 : + animals2); print ( "an i mals3: + animals3); I I / * Output: (Samplel animals : [Bosco t he dog[Animal@addbfl] House@42 e B16 Ralph the hamster[Animal@9304bl] House@42eB16 , Molly the cat(Animal@190dll], House@42eBl6 I I 1 animalsl: [Bosco the dog [Animal@de6f34], House@156eeBe Ralph the hamster[Animal@47b4BO), House@156eeBe , Molly the cat[Animal@19b4ge6], House@l56eeBe 1 animals2: [Bosco the dog[Animal@de6f34] House@156eeBe Ralph the hamster[Animal@47b4BO), House@l56eeBe Molly the cat[Animal@l9b4ge6], House@156eeBe I I 1 animals3: (Bosco the dog [Animal@lOd448], House@eOelc6 Ralph the hamster[Animal@6calc], House@eOelc6 Molly the cat[Animal@lbf2l6a], House@eOelc6 I 1 , /// , Un aspecto interesante del ejemplo es que resulta posible utili zar el mecanismo de serialización de objetos con una matriz de tipo byte, corno fOfila de obtener una "copia profunda" de cualquier objeto de tipo Serializable (una copia profunda quiere decir que estamos duplicando la red completa de objetos, en lugar de sólo los objetos básicos y sus referencias). La copia de objetos se cubre en detalle en los suplementos en línea del libro. Los objetos de tipo Animal contienen campos de tipo House. En main(), se crea una lista de estos objetos Animal y se la seria liza dos veces en sendos fluj os de datos. Cuando se des-serializan e imprimen esos flujos de datos, podemos ver un ejemplo de la salida que se obtendría (en cada ejecución las posiciones de memoria correspondientes a los objetos serán diferentes). 18 Entrada/salida 651 Por supuesto, lo que cabria esperar es que los objetos des-serial izados tuvieran direcciones diferentes de las de sus originales. 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 completamente distinta. Mientras estamos seria lizando todo en un único flujo de datos, recuperaremos la mi sma red de objetos que hayamos escrito, 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 responsabilidad; los objetos se escribirán en el estado en que se encuentren (y con cualesquiera conex iones que tengan con otros objetos) 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á restaurarlo 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 ilustra 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 simplemente el objeto C lass . En cualqu ier caso. parece una so lución ll ena de sent ido común. 11: io/StoreCADState .java II Almacenamiento del estado de un supuesto sistema CAD. import java.io.*¡ import java.util.*¡ abstract class Shape implements Serializable public static final int RED = 1, BLUE = 2, GREEN private int xPos, yPos, dimension; private static Random rand = new Random(47); private static int counter = O; public abstract void setColor(int newColor); public abstract int getColor(); public Shape (in t xVal, int yVal, int dim) { xPos = xVal¡ yPos = yVal; dimension = dim¡ 3¡ public String toString() return getClass() + "color [" + getColor () + ,,) xPos [11 + xPos + 11 J yPos [" + yPos + 11 J dim [" + dimension + "1 \n " ; public static Shape randomFactory () int xVal = rand.nextlnt(lOO); int yVal = rand.nextlnt(100); int dim = rand.nextlnt(100); switch (counter++ % 3 ) { default: case O: return new Circle(xVal, yVal, dim); case 1: return new Square (xVal, yVal, dim); case 2: return new Line(xVal, yVal, dim); class Circle extends Shape { 652 Piensa en Java private sta tic int color : RED; public Circle(int xVal, int yVal, int dim) super(xVal, yVal, dim); ( newColor; } public void setColor (int newColor) { color public int getColor () { return color; } class Square extends Shape { private static int color; public Square (int xVal, int yVal, super (xVal, yVal, dim); color :: RED; int dim) public void setColor (int newColor) { color public int getColor () { return color; } ( newColor; } class Line extends Shape { private static int color :: RED; public static void serializeStaticState(ObjectOutputStream os) throws IOException { os. writelnt (color); } public static void deserializeStaticState(ObjectlnputStream os) throws IOException { color: oS.readlnt(); public Line (int xval, int yVal, int dim) { super (xVal , yVal, dim) i public void setColor{int newColor) ( color public int getColor () { return color; } newColor; } public class StoreCADState ( public static void main(String{) args) throws Exception { List shapes : new ArrayList() i II Construir algunas forma geométricas: for(int i :: O; i < 10; i++) shapes.add(Shape.randomFactory()) ; II Configurar todos los colores estáticos como GREEN: for(int i "" O; i < 10; i++) ((Shape )shapes .get (i)) .setColor(Shape.GREEN); II Guardar el vector de estado: ObjectOutputStream out :: new ObjectOutputStream( new FileOutputStream ( "CADState .out n 1) ; out.writeObject(shapeTypes) ; Line. serializeStaticState (out) ; out.writeObject{shapes) ; II Mostrar las formas geométricas: System.out.println(shapes) i 1* Output: 18 Entrada/sa lida 653 [class Circlecolor[3] xPos(58) yPos[SS) dim[93] class Squarecolor[3] xPos(61 } yPos[61] dim[29] class Linecolor[3] xPos[68] yPos[O] dim(22] class Circlecolor[3] xPos(7] yPos[88] dim[28] class Squarecolor[3] xPos(Sl] yPos[89] dirn[91 class Linecolor[3] xPos[78] yPos[98] dim[61] class Circlecolor(3] xPos(20] yPos[58] dim[16] class Squarecolor[3] xPos(401 yPos[ll] dim[22] class Linecolor[3J xPos[4] yPos[83] dim(6] class Circlecolor[3] xPos[75] yPos[lO] dim[42] 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 determina 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 sustituidos para configurar el color de los diferentes tipos (los métodos estáticos no se acoplan dinámicamente, así que son métodos 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 definició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 geomé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. *; public class RecoverCADState @SuppressWarnings (tlunchecked " ) public static void main(String[] args) throws Exception ObjectlnputStream in = new ObjectlnputStream( new FilelnputStream ( tlCADState .out tl )) i II Leer en el mismo orden en que fueron escritos: List shapes = (List people = Arrays.asList( new Person ("Dr. Bunsen", "Honeydew" ), new Person ( "Gonzo", "The Great"), new Person("Phillip J. ", "Fryll)); System.out.println(people} ; Element root = new Element ( "people" ) ; for{Person p : peoplel root.appendChild(p.getXML()) ; Document doc = new Document(root); format(System.out, dac}; format(new BufferedOutputStream{new FileOutputStream{ "People.xml" )), doc) ; / * Output: [Dr. Bunsen Honeydew, Gonzo The Great, Phillip J. Fry] Dr. Bunsen Honeydew Gonzo The Great Phillip J. Fry * /// ,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. * ¡ public class People e x tends ArrayList { public People {String fileName ) throws Exception Document doc = new Builder {) . build {fileName ) ; Elements elements = doc . ge t RootElement ( ) . getChi l d El ements ( ) ; for {int i = o¡ i < elements . size () i i++ ) add {new Person (elements.get(i ») ; publ i c stat i c void main(Stri ng(] args ) t h rows Ex c e ption { People p -:: new Pe ople ( II Pe ople . x ml" ) i System . out . println (p ) ; 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étodo get( ), Harold no quería obligar a los programadores a utili zar Java SES, pero seguía queriendo disponer de un contenedor 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 esperamos, 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 concreta 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 nuestra 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 informació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 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 objetos, porque se encarga de almacenar y recuperar automáticamente infonnación. Sin embargo, su uso está restringido a conjuntos 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 aplicación seria). Como su propio nombre sugiere, la API Preferences está diseñada para almacenar y extraer preferencias de usuario 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.*¡ public class PreferencesDemo { public static void main(String[] argsl throws Exception Preferences prefs = Preferences ,userNodeForPackage (P referencesDemo. class) ; prefs.put(tlLocation", ItO z "); prefs.put ( tlFootwear", "Ruby Slippers"); prefs.putlnt("Companions", 4); prefs.putBoolean("Are there witches?", true) i int usageCount = prefs.getlnt ( "UsageCount", O) i usageCount++; prefs .putlnt ("UsageCount", usageCount) i for (String key : prefs . keys()) print(key + ": "+ prefs.get(key. null ) ; // Siempre hay que proporcionar un valor predeterminado: print ( "How many companions does Dorothy have? " + prefs . getlnt ("Companions", O)) i / * 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 elementos 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 segundo 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 nominada, 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 estructura bastante típica en las líneas: int usageCount = prefs.getlnt("UsageCount", O); usageCount++i prefs.putlnt("UsageCount", usageCount) i De esta fonna , la prime ra vez que ejecutemos el programa, Us ageCount tendrá el valor cero, pero en las subsiguientes invocaciones 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 programa. 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 travé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 liabilidad 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 ejemplo, 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 resulta intuitivo, por 10 que existe una cierta curva de aprendizaje y también requiere más esfuerzo a la hora de explicar el funcionamiento 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 mejoras en Java SE5, añadiendo por ejemplo los mecanismos de fom18teo de salida que siempre han estado soportados en prácticamente 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 situaciones 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 enumeració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 adicionales. Características básicas de las enumeraciones Co mo hemos visto en e l Capítulo 5, inicialización y limpieza, podemos recorrer la li sta de constantes enum invocando el método values() para dicha enumeración. El método values( ) genera una matriz co n las constantes enum en el orden en que fueran declaradas, de modo que podemos utili zar la matriz resultante en, por ejemplo, un bucleforeach. Cuando se crea una enumeración, el compilador genera por nosotros una clase asociada. Esta clase hereda automáticamente de java.lang.Enum, lo que proporciona ciertas capacidades que se ilustran en el siguiente ejemplo: 11 : enumerated/EnumClass.java II Capacidades de la clase Enum import static net.mindview . util.Print . *¡ enum Shrubbery { GROUND, CRAWLING, HANGING public class EnumClass { public static void main (String [] args) { forlShrubbery s , Shrubbery.valuesl)) { print(s + ordinal: " + s.ordinal()); printnb(s.compareTo(Shrubbery.CRAWLING) + " ") ¡ printnb(s.equals(Shrubbery . CRAWLING) + " ") ¡ print(s == Shrubbery . CRAWLING) ¡ p ri nt (s .getDeclaringClass () ) ¡ print(s.name{)) ; print("---------------------- " ) ¡ 11 1I Generar un valor enum a partir de una cadena de caracteres : for(String s : "HANGING CRAWLING GROUNDII.split(U n )) Shrubbery shrub = Enum.valueOf (S hrubbery.class, s); print (sh rub ) ; \ Joshua Bloch me ha ayudado enormemente en el desarrollo de este capítulo. { 660 Piensa en Java /* 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. Utilización de importaciones estáticas con las enumeraciones Analizamos una variante del programa Burrito.java del Capítulo 5, inicialización y limpieza: 1/: enumerated/Spiciness.java package enumerated; pUblic enum Spiciness NOT, MILO, MEDIUM, HOT, FLAMING } //1,- JI : enumeratedfBurrito.java package enumerated; import static enumerated.Spiciness.*¡ public class Burrito { Spiciness degree; public Burrito(Spiciness degree) ( this.degree = degree¡) public String toString{) { return "Burrito is "+ degree;} public static void main{String(] args) { System.out.println(new Burrito (NOT) ); System.out.println(new Burrito(MEDIUM)); System.out.println(new Burrito (HOT) ); /* 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 necesario 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 situaciones, 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 predetem'¡nado (al parecer, se produjeron algunas di scusiones dentro de Sun sob re si debía pennitir hacer esto). Adición de métodos a una enumeración Salvo por el hecho de que no podemos heredar de ella, una enumeración puede tratarse de fonna bastante similar a las cIases nonnales. Esro quiere decir que podemos añadir mérodos a una enumeración. Resulta incluso posible que una enumeración disponga de un método main( ). Podemos, por ejemplo, generar para una enumeración, desc ripciones que difieran de la proporcionada por el método toString() predetenninado, que simplemente proporcione el nombre de esa instancia eoum . Para hacer esto, debemos proporcionar el constructor con el fm de capnlrar infonnación adic ional y métodos adicionales que proporcionen una descripción ampl iada, como en el ejemplo sigu iente: ti: enumerated/OzWitch . java // Las brujas en la tierra de Oz. import static net.mindview.util.Print.*¡ public enum OzWitch // Las instancias deben definirse primero, antes de los métodos: WEST (11 Miss Gulch, aka the Wicked Wi tch of the West n) , NORTH(tlGlinda, the Good Witch of the North"), EAST("Wicked Witch of the East, wearer of the Ruby It + tlSlippers, crushed by Dorothy's house"), SOUTH(uGood by inference, but missing"); private String description; // El constructor debe tener acceso de paquete o privado: private OzWitch(String description) this.description = description¡ pUblic String getDescription () { return description ¡ public static void main(String [] args) { for{OzWitch witch : OzWitch.values{)) print(witch + 11: u + witch.getDescription (}); / * 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 definirlas 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 definición de la enumeración. Sustitución de los métodos de una enumeración He aquí otra técnica para generar diferentes valores de cadena para las enumeraciones. En este caso, los métodos de las instancias son correctos, pero queremos reformatearlas de cara a su visualización. La sustitución del método toStri ng() en una enumeración es igual que la sustitución en una clase nonnal: 11: enumerated/SpaceShip .java public enum SpaceShip { SCOUT, CARGO, TRANSPORT, CRUISER, BATTLESHIP, MOTHERSHIP; public String toString () { String id = name{) ¡ String lower = id . substring (1) .toLowerCase () i return id.charAt{O) + lower¡ pub1ic static void main{String[] args) for {SpaceShip s : values (}) { System.out.println(s ) ¡ { 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. Enumeraciones en las instrucciones switch Una funcionalidad muy útil de las enumeraciones es la forma en que pueden utilizarse en las instrucciones switch. Normalmente, una instrucción switch sólo funciona con valores enteros, pero como las enumeraciones tienen un orden entero asociado y la posición de una instaocia puede obtenerse mediante el método ordinal() (aparentemente esto es lo que hace el compilador), las enumeraciones pueden emplearse también dentro de las instrucciones switch. Aunque normalmente es preciso cualificar cada instancia enum con su tipo, esto no es necesario dentro de una instrucción case. He aquí un ejemplo que emplea una enumeración para crear un pequeña máquina de estados: 11: enumerated/TrafficLight . java II Enumeraciones en instrucciones switch. import static net.mindview.util.Print.*¡ II Define un tipo enum: enum Signal { GREEN, YELLOW, RED, } public class TrafficLight { Signal color = Signal.RED¡ public void change () { switch (color) { II Observe que no hay por qué escribir Signal.RED 19 lipos enumerados 663 JI dentro de la instrucción case: case RED: color = Signal . GREEN¡ break; case GREEN, color = Signal.YELLOW¡ break; case YELLOW, color = Signal.RED¡ break; public String taSeríng () { return "The traffic light is 11 + color; public sta tic void main (Stri ng[] args) new TrafficLight(); TrafficLight t for(int i = O; i<7¡i++l{ print (t) ; t.change() ; / * Output: The The The The The The The traffic traffic traffic traffic traffic traffic traffic light light light light light light light is is is is is is is RED GREEN YELLOW RED GREEN YELLOW 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 mediante un comentario, el compilador seguirá sin quejarse. Esto quiere decir que es necesario tener cuidado y comprobar explícitamente 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 cIases 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 programa 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 import import import java.lang.reflect .* ¡ java . util.*¡ net.mindview.util. * ¡ static net.mindview . util.Print. *¡ enum Explore { HERE, THERE public class Reflection { public static Set rnethods = new TreeSet () ; for (Method m : enurnClass . getMethods (» rnethods . add (rn . getNarne () ) ; print (rnethods ) i re t urn rnethods i public static void rnai n (String (] args ) { Set exploreMethods = analyze {Explore. classl ; Set enurnMethods = analyze (Enurn . class l i print ( "Explore. containsAl1 {Enum}? " + exploreMethods.containsAII(enurnMethods ) ; pr i ntnb(IIExp l ore.removeAl l(Enum ) : ") i exploreMethods.rernoveAII(enurnMethods) i print(exploreMethodsJ; II Descornp i lar el código para l a enumeración: OSExecute. command ( " j avap Explore" ) ; 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 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étodo 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 inforruación completa acerca de EDum. por lo que muestra la clase base de Explore como una clase Enum simple, en lugar de Enum. 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 obteniendo 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 enum Search { HITHER, YON public class UpcastEnum { public static void main(String[] args) Search[] vals = Search.values(); Enum e = Search . HITHER; // Upcast JI e.values(); /1 No hay método values() en Enum for(Enum en : e . ge t Cl ass() . ge t EnumConstants()) System . out . pri n tl n {en) ; 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 public class NonEnum { public static void main{String[] args) { Class intClass = I nteger . class ; try ( for(Object en : intClass.getEnumConstants{)) System.out . pr intl n{e n ) ; catch {Ex cept i o n e l { System.out . println{e) ; 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 SLAPPY, S PANKY , PUNCHY, SILLY, SOUNCY, NUTTY, SOS; 666 Piensa en Java private Random rand ~ new Random(47)¡ public CartoonCharacter next () { return values () [rand. nextlnt (values () . length) ] ; public class Enumlmplementation { public static void printNext(Generator rg) System.out.print(rg.next() + ", ti} i { public static void main(String(} args) JI Elegir cualquier instancia: CartoonCharacter ce = CartoonCharacter.BOB¡ for(int i '" O; i < 10; i++) printNext (ce) ; /* Output: B08, PUNCHY, B08, NUTTY, SLAPPY, SPANKY, NUTTY, PUNCHY, SLAPPY, NUTTY, * ///,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 ventajas 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 biblioteca común: 11 : net/mindviewjutil/Enums.java package net . mindview.ucil; import java.util.*; public class Enums { private static Random rand = new Random(47); public static ec) return random(ec.getEnumConstants()) i { public static T random(T(] values) return values[rand.nextlnt(values.length)); La extraña sintaxis , 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 necesita 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.*; enum Activity { SITTING, LYING, STANDING, HOPPING, RUNNING, DODGING, JUMPING, FALLING, FLYING ) 19 Tipos enumerados 667 public class RandomTest { p ublic static void main {String[] for ( int i = Oi i < 20; i++ } args ) { System.out.print (Enums . random {Activity.class ) + ti U) ; / * 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 duplicaciones. Las dupl icaciones tienden a generar erro res, así que eliminar esas dupl icaciones es un objetivo bastante importante. Utilización de interfaces con propósitos de organización La imposibilidad de beredar de una enumeración puede resultar un tanto frustante en algunas ocasiones. La razón para trata r de hereda r de una enumeración proviene en parte del deseo de aum entar el número de elementos de la enumerac ión original, y por otro lado del deseo de crear subcategorías empleando subtipos. Podemos realizar la categorización agrupando los elementos dentro de una interfaz y creando un a enumeraci ón basada en esa interfaz. Por ej emplo, supongamos que tenemos diferentes clases de alimentos y queremos definirlas como e nul11 e ra ci o~ nes, pero sin que por ello las distintas clases de alimentos dej en de ser un tipo de una clase denominada Food . He aqu í un ejemplo: 11: enumerated/menu/Feed.java 11 Subcategorización de enumeraciones dentro de interfaces . package enumera t ed.menu ¡ public interface Food { enum Appetizer implements Feod SALAD, SOUP, SPR I NG_ROLLS; enum MainCourse implements Food LASAGNE, BURRITO, PAD_THAI, LENT ILS, HUMMOUS, VINDALOO; enum Dessert implements Food { TIRAMISU, GELATO, BLACK_FOREST_CAKE, FRUIT, CREME_CARAME L ; enum Coffee implements Food BLACK_COFFEE, DECAF_ COFFEE, ESPRESSO, LATTE, CAPPUCC I NO, TEA, HERB_TEA; Puesto que el úni co mecanismo de subtipos disponible para una enumeración está basado en la implementación de interface, 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 . *¡ public class TypeOfFood { public static void main(String[] Food food = Appetizer . SALAD¡ focd = MainCourse . LASAGNE¡ args ) { 668 Piensa en Java foad foad Dessert . GELATO; Coffee.CAPPUCCINO¡ ) /// ,La generalizaci ón a Food funciona para cada tipo enum que implementa Food, así que todos ellos son tipos de Food. Sin embargo, una interfaz no resulta tan útil como una enumeración c uando queremos tratar con un conjunto de tipos. Si deseamos di sponer de una "enumeración de enumeraciones" podemos crear una enumeración de ni vel superi or co n una instancia para cada enumeración de Food : / 1 : enumerated/menu/Course.java package enumerated . menu; import net.mindview . util. * ¡ public enum Course { APPETIZER ( Food . Appetizer . class ) MAINCOURS E (Food . Ma inCou r se.class ) , DESSERT (Food.Dessert.class ) COFFEE (Food . Coffee . c l ass ) ; priva t e Food[] values¡ private Course (Classc::? e x tends Food> kind ) { values ~ kind . getEnumConstants () i I I public Food randomSelection () { r et urn Enums . random (values l i ) /// ,Cada una de las enum erac iones anteriores toma el correspondiente objeto Class como argumento del constructor, pudiendo extraer de él todas las instancias enum utilizando getEnumConstantsO y almace narlos. Estas instancias se utilizan posterionnente en randomSelection( ), por lo que ahora podemos crear un menú generado al eatoriamente seleccionando el elemento de Food de cada plato (eO"rse): JJ : enumerated / menu / Meal . java package enumerated.menu¡ public class Meal { public static void main (String[] args l { for (int i = Oi i <: 5 i i+ +) { for (Course course : Course.values (» Food food = course . randomSelecti on () System. out . println (food ) ; System.out.println ( It- -- It l / * Output : SPRING ROLLS VINDALOO FRUIT DECAF COFFEE SOUP VINDALOO FRUIT TEA SALAD BURRI TO FRUIT i i 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 enumeraciones, 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) Security[] values¡ SecurityCategory(Class kind) values = kind.getEnumConstants(); i interface Security { enum Stock implements Security { SHORT, LONG, MARGIN enum Bond implements Security { MUNICIPAL, JUNK } public Security randomSelection() return Enums.random(values}; public static void main(String[] args) for(int i = O; i < 10i i++} { SecurityCategory category = Enums.random(SecurityCategory.class) ; System.out.println(category + ": "+ category.randomSelection()) ; /* 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 Si ut ilizamos esta solución con el ejemplo Food . el resultado sería: 11: enumerated/menu/Mea12.java package enumerated.menu; import net.mindview.util. * ; public enum Mea12 { APPETIZER(Food.Appetizer.class) , MAINCOURSE(Food.MainCourse.class) , DESSERT(Food.Dessert.class) , COFFEE(Food.Coffee.class) ; private Food[] values; private Meal2 (Class kind) values = kind.getEnumConstants(); { public interface Food { enum Appetizer implements Food SALAD, SOUP, SPRING_ROLLS; enum MainCourse implements Food LASAGNE, BURRITO, PAD_THAI, LENTILS, HUMMOUS, VINDALOO; enum Dessert implements Food { TIRAMISU, GELATO, BLACK_FOREST_CAKE, FRUIT, CREME_CARAMEL; enum Coffee implements Food BLACK_COFFEE, DECAF_COFFEE, ESPRESSO, LATTE, CAPPUCCINO, TEA, HERB_TEA; publ ic Food randomSelection () { return Enums. random (values) ; public static void main(String[] args) for(int i == O; i < 5; i++} { for (Meal2 meal : Meal2. values () { Food food = meal.randomSelection(); System.out.println(food) ; System.out.println(II---II) ; 1* Misma salida que en Meal.java *111:- Al final, se trata simplemente de una reorganización del código, pero pennite obtener una estructura más clara en algunos casos. Ejercicio 3: ( 1) Añada un nuevo objeto Course a Course,java y demuestre que funciona en Meal.java. Ejercicio 4: ( 1) Repita el ejemplo anterior para MeaI2.java. Ejercicio 5: (4) Modifique controlNowelsAndConsonants.java para que utilice tres tipos de enum : VOWEL, SOMETlMES_A_ VOWEL y CONSONANT. El constructor enum debe admitir las distintas letras que describen cada categoría concreta de vocales y consonantes. Consejo: utilice varargs y recuerde que éstos crean automáticamente una matriz. Ejercicio 6: (3) ¿Ex iste alguna ventaja especial en anidar Appetizer, MainCourse, Dessert y Coffee dentro de Food en lugar de definirlos como enumeraciones independientes que se limiten a implementar Food? 19 lipos enumerados 671 Utilización de EnumSet en lugar de indicadores Un contenedor Set es una especie de colección que sólo pennite añadir un ejemplar de cada tipo de objeto. Por supuesto, una enumeración requiere que todos SlI S miembros sean diferentes, por lo que podría parecer que tiene un comportamiento si mi lar al de los conjuntos, pero como se puede añadir o eliminar elementos, las enumeraciones no resultan demasiado útiles como conjuntos. La clase EnumSet se ha añadido a Ja va SES para funcionar de manera conjunta con las enumeraciones, con el fin de crear un sustituto para los tradicionales Ó points = EnumSet.noneOf (AlarmPoints.class ) ; II Conjunt o vacío points.add (BATHROOM ) ; print(pointsl; points.addAll IEnumSet.of ISTAIRl, STAIR2, KITCHEN )) ; print (points ) ; points = EnumSet.allOf (AlarmPoints.class); points.removeAII ( EnumSet.of (STAIRl, STAIR2, KITCHEN )) i print (points ) ; points.removeAII (EnumSet.range{OFFICEl, OFFICE4) ) i print(points) i points = EnumSet.complementOf (points ) ; print (points) ; 1* Output: [BATHROOM] [STAIRl, STAIR2, [LOBBY, OFFICEl, [LOBBY, BATHROOM, [STAIRl, STAIR2, *///,- BATHROOM, KITCHEN] OFFICE2, OFFICE3, OFFICE4, BATHROOM, UTILITY] UTILITY] 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 invocamos 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 ere un bit para indicar la presencia o ausencia. Esto sign ifica que podemos tener un conjunto EnumSet para una enumeració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 .* ¡ public class BigEnumSet ( enum Big ( AO, Al, A2, A3, M, AS, A6, A7, AS, A9, A1O, A11, A22, A33, A44, A5S, A66, A12, A23, A34, MS, A5 6, A67, A13, A24, A3S, M6, AS7, MB, A14, A2S, A36, M7, Asa, M9, A15, A26, A37, MB, AS9, A70, A16, A27, A3a, M9, A60, A71, A17, A2B, A39, ASO, Ml, A72, A1B, A29, A40, AS1, A62, A73, A19, A30, A41, AS2, M3, A?4, A20, A2l, A3l, A32, A42, A43, AS3, AS4, A64, MS, A7S ) public static void main (String [] args ) ( EnumSet bigEnumSet = EnumSet.allOf(Big.class) ; System.out.println(bigEnumSet) ; / * Output : [AO, Al3, A25, A37, M9, A6l, A73, Al, A2, A3, M, AS, Al4, A1S, A16, A17, A26, A27, A2a, A29, A3B, A39, MO, Ml, ASO, ASl, AS2, AS3, A62, A63, A64, A65, A74, A7SJ A6, A7, AB, A9, A1O, All, A12, A1B, Al9, A20, A21, A22, A23, A24, A30, A31, A32, A33, A34, A3S, A36, A42, M3, M4, MS, M6, M7, A4a, A54, ASS, AS6, AS?, A5B, A59, A60, A66, A67, A6B, A69, A70, A71, A72, * ///,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úsquedas 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 comportamiento 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.*¡ interface Command { void action () ; } public class EnumMaps { public static void main (String [] args) { EnumMap em = new EnumMap (AlarmPoints.class ) ; em.put(KITCHEN, new Cornmand() { public void action () { print (UKitchen fire! It ) i } )) ; em.put(BATHROOM, new Command() ( public void action () { print ( "Bathroom alert!"); } )l; for(Map.Entry e : em.entrySet{)) printnb (e . getKey () + ": "); e.getValue() .action () ; try { JI Si no hay ningún valor para una clave concreta: em.get(UTILITY) .action() i catch (Exception e ) { print (e) ; / * 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í. Métodos específicos de constante La enumeraciones Java tienen una característica muy interesante que nos permite asignar a cada instancia enum un comportam iento distinto, creando métodos para cada una de ellas. Para hacer esto, definimos uno o más métodos abstractos como parte de la enumeración, y luego definimos los métodos para cada instancia enum . Por ejemplo: // : enumerated/ConstantSpecificMethod.java import java.util.*¡ import java.text . *¡ public enum ConstantSpecificMethod DATE TIME { 674 Piensa en Java 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 () ) ; /* ( Execute to see output) * /// :- Podemos buscar e invocar los métodos a través de su instancia enum asociada. Esto se denomina a menudo código conducido por tablas (observe, en especial, la similitud con el patrón de diseño Comando mencionado anterionnente). En la programación orientada a objetos, se asocia un comportamiento distinto con las diferentes clases. Dado que cada instancia de una enumeración puede tener su propio comportamiento mediante métodos específicos de constante, esto sugiere que cada instancia es un tipo de datos distinto. En el ejemplo anterior, cada instancia eflum se trata como el "tipo base" ConstantSpccificMcthod, pero obtenemos un comportamiento polimórfico con la llamada a método getInfo( ). Sin embargo, esa similitud no puede llevarse más lejos. No podernos tratar las instancias enum como si fueran tipos de clases: 11 : enumerated / NotClasses.java II {Exec: javap -c LikeClasses} import static net.mindview.util.Print.*¡ enum LikeClasses { WINKEN { void behavior ( ) { print ("Behaviorl"); ) ), BLINKEN { void behavior () { print ("Behavior2" ) ; ) ), NOD { void behavior () { print ("Behavior3"); ) ); abstract void behavior( ) ; public class NotClasses { II void fl (LikeClasses. WINKEN instance ) {} lI No se puede ) / * Output, Compiled fr om "NotClasses.java" abstract class LikeClasses extends java.lang.Enum{ public static final LikeClasses WINKEN; public static final LikeClasses BLINKEN¡ public static final LikeClasses NOO; En fl(), podemos ver que el compilador no permite utili za r una instancia enum corno un tipo de clase, lo que tiene bastante 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.*¡ public class CarWash { public enum Cycle { UNDERBODY ( void action () print (" Spraying the underbodytl); } }, WHEELWASH ( void action () { print ("Washing the wheels"); } { print ("Loosening the dirt ti); } }, PREWASH ( void action () }, BASIC ( void action () { print ("The basic wash"); } }, HOTWAX void action () { print ( "Applying hot wax"); } }, RINSE ( void action() { print("Rinsing"); } }, BLQWDRY ( void action () { print ("Blowing dry"); } }; abstract void action() i EnumSet cycles = EnumSet.of(Cycle.BASIC, Cycle.RINSE) i public void add(Cycle cycle) { cycles.add(cycle); public void washCar () { for(Cycle e : cycles) c. action () ; public String toString () { return cycles . toString () ; public static void main{String[] args) { CarWash wash = new CarWash(); print (wash) i wash.washCar() ; II El orden de adición no es importante: wash.add{Cycle.BLOWDRY) ; wash.add{Cycle.BLOWDRY ) i II Se ignoran los duplicadas wash.add(Cycle.RINSE1; wash.add{Cycle.HOTWAX) i print{wash) ; wash.washCar() ; 1* Output: [BASIC, RINSE] The basic wash Rinsing 676 Piensa en Java [BASIC, HOTWAX, RINSE, BLOWDRY] The basic wash Applying hot wax Rinsing Blowing dry *///,La sintaxis para definir un método específico constante es, en la práctica. la de una clase interna anónima, pero más sucinta. Este ejemplo muestra también otras características adicionales de los conjuntos EnumSet. Puesto que se trata de un conjunto, sólo permitirá almacenar un ejemplar de cada elemento, así que las llamadas duplicadas con add( ) con el mismo argu- mento serán ignoradas (esto tiene bastante sentido, ya que un bit sólo se puede "activar" una vez). Asimismo, el orden en el que añadamos las instancias enum no tiene importancia: el orden de salida está detenninado por el orden de declaración dentro de la enumeración. ¿Es posible sustituir los métodos específicos de constante, en lugar de implementar un método abstracto? Sí que es posible, como podemos ver aquí: 11: enumerated/OverrideConstantSpecific.java import static net.mindview.util.Print.*¡ public enum Overr ideConstantSpecific NU'I' I BOLT, WASHER { void f() { print(IIOverridden method"); }; void f () { print ( ndefault behavior" ) ; } public statie void main(String[] args) { for(OverrideConstantSpeeific Des: values ()) printnb (Des + ": 11); ocs . f () ; 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 experimentar con ellas como si tratara de clases normales. Cadena de responsabilidad en las enumeraciones En el patrón de dise~o Cadena de responsabilidad, creamos una serie de diferentes formas de resolver un problema y las encadenamos. Cuando tiene lugar una so}jcitud se la pasa a través de la cadena basta que se encuentra una de las soluciones que pueda gestionarla. Podemos implementar fácilmente una Cadena de responsabilidad simple utilizando métodos específicos de constante. Considere un modelo de una oficina de correos, que trate de gestionar cada correo de la forma más general posible, y que tiene que continuar intentando gestionar cada envío postal hasta conseguirlo o hasta llegar a la conclusión de que no es posible entregarlo. Cada uno de los intentos puede considerarse como un tipo de Estrategia (otro patrón de dise~o) y la lista completa forma una Cadena de responsabilidad. Comenzaremos describiendo lo que es un envío postal. Todas las diferentes características de interés pueden expresarse utilizando enumeraciones. Puesto que los objetos Mail (que representan los envíos postales) se generarán aleatoriamente, la fonna más fácil de reducir la probabilidad de que, por ejemplo, a un envío postal le corresponda un valor VES para GeneralDelivery (entrega de carácter general) consiste en entregar más instancias que no correspondan con el valor YES, así que las definiciones enum parecen un poco extrañas al principio. 19 Tipos enu me rados 677 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 enví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¡ public static Iterable generator{final int count) return new Iterable () { int n = count ¡ public Iterator iterator() return new Iterator () { public boolean hasNext () { return n- - > O; } public Mail next () { return randomMail () i } public void remove{) { /1 No implementado throw new UnsupportedOperationExcep tion() ¡ } }; } }; 678 Piensa en Java public class PostOffice enum MailHandler { GENERAL_DELIVERY { boolean handle (Mail m) { switch (m. generalDelivery) case YES: print ("Using general delivery for return true: default: return false: ti + 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 + automatically") ¡ return true; 11 } }, 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}; static void handle(Mail m} for(MailHan dler handler : MailHandler.values()) if (handler.handle (ml ) return; print (m + " is a dead letterO!); public static void main(String[] args) 19 lipos enumerados 679 for (Mail mail : Mail.generator ( 1 0}) print (mail.details ()) i hand l e (mail ) ; print ( "**·**" ) ; / * Output: Mail O, General Delivery : N02, Address UNSCANNABLE, Address Readability: YES3, Return address: OK1 Delivering Mail O normally *.*.* Mail 1, General Delivery : N05, Address Address Readability: ILLEGIBLE, Address Return address: OK1 Delivering Mail 1 automatically Scanability: Address Address: OK1, Scanability: VES), Address: OK5, . *• • • Mail 2, General Delivery: VES, Address Scanability: YES3, Address Readability: YES1, Address Address: OK1, Return address: OK5 Using general delivery for Mail 2 .*... Mail 3, General Delivery: N04, Address Scanability: YES3, Address Readability: YES1, Address Address : INCORRECT, Return address: OK4 Returning Mail 3 to sender * •• *. Mail 4, General Delivery: N04, Address Scanability: UNSCANNABLE, Address Readability: YES1, Address Address: Return address: OK2 Returning Mail 4 to sender INCORRECT, * •• *. Mail 5, General Delivery: N03, Address Address Readability: ILLEGIBLE, Address Return address: OK2 Delivering Mail 5 automatically •• *. * Mail 6, General Delivery: VES, Address Address Readability: ILLEGIBLE, Address Return address: OK4 Using general delivery for Mail 6 Scanability: YES1, Address: OK4, Scanability: YES4, Address: OK4, •• * • • Mail 7, General Delivery: VES, Address Scanability : YES3, Address Readability: YES4, Address Address: OK2, Return address: MISSING Using general delivery for Mai l 7 ** *. * Mail 8, General Delivery: NO), Address Scanability: YES1, Address Readability: YES3, Address Address: INCORRECT, Return address: MISSING Mail B is a dead letter .* •• * Mail 9, General Delivery: N01, Address Scanability: UNSCANNABLE, Address Readability : YES2, Address Address: OK1, Return address: OK4 Delivering Mail 9 normally .* *.* 680 Piensa en Java La Cadena de responsabilidad se expresa en la enumeración enum MailHandler, y el orden de las definiciones enurn determina 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. Máquinas de estado con enumeraciones Los tipos enumerados pueden ser ideales para crear máq/linas de estado. Una máquina de estado puede encontrarse en un número finito de estados específicos. Nonnalmente, la máquina pasa de un estado al siguiente basándose en una entrada, pero también existen estados transitorios: la máquina saJe de estos estados en cuanto se ha realizado la correspondiente tarea. Existen ciertas entradas pennitidas para cada estado y las diferentes entradas cambian el estado de la máquina a diferentes nuevos estados. Puesto que las enumeraciones reducen el conj unt o de posibles casos, resultan muy útiles para enumerar los diferentes estados y entradas. Cada estado tiene también normalmente algún tipo de salida asociada. Una máquina expendedora es un buen ejemplo de máquina de es tados. En primer lugar, definimos las entradas dentro de una enumeración: 11: enumerated/Input.java package enumerated; import java.util.*; public enum Input { NICKEL ( 5 ) . DIME (lO ) , QUARTER (25 ) , DOLLAR ( lOO ) . TOOTHPASTE (20 0) , CHIPS(75 ) , SODA ( lOO) , SOAP(50 ) , ABORT_TRANSACTION { public int amount () { 11 No permitir throw new RuntimeException ( "ABORT. amount () JI ) ; } . 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 representar 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 enumeració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 O) { print ("Your change: " + amount); amount = Oi state = REST I NG¡ }, 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 () "StateDuration.TRANSIENT states") i void output{) for { print{amount}; static void run{Generator gen) while(state != State.TERMINAL) ( state.next(gen.next()) ; while(state.isTransientl state.next() ; state.output() ; public static void main(String[] argsl { Generator gen = new RandomlnputGenerator() i if(args.length == 1) n + 19 Tipos enumerados 683 gen = new FilelnputGenerator{args[O]); run (gen) ; /1 Comprobación básica de que todo está en orden: class RandomlnputGenerator implements Generator { public Input next () { return Input. randomSelection () ; JI Crear objetos Input a partir de un archivo de cadenas separadas por 'j': class FilelnputGenerator implements Generator { private Iterator input; public FilelnputGenerator (String fileName) { input = new TextFile(fileName, ni") .iterator(); public Input next (1 { if(!input.hasNext()) return null i return Enum valueOf (Input. class, input. next () . trim () ) 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 esfuerzo 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 comprobar 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 complejos. 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 resuelve 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 despacho 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 produzca 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 ) ///,jj: enumerated jRoShamBol.java jj Ilustración del mecanismo de despacho múltiple. package enumerated¡ import java.util. * ¡ import static enumerated.Outcome.*¡ interface Outcome Outcome Outcome Outcome Item { compete(Item it); eval (Paper p) ¡ eval(Scissors s) ¡ eval(Rack r)¡ class Paper implements Item { public Outcome compete(Item it) { return it.eval(this) ¡ public Outcome eval (Paper p) { return DRAW; } public Out come eval (Scissors s) { return WIN¡ public Outcome eval (Rack r) { return LOSE¡ } public String toString () { return "Paper"; } class Scissors implements Item { public Out come compete (Item it) { return i t. eval (t his ) i public Out come eval (Paper p) { return LOSE¡ } public Outcome eval (Scissors s) { return DRAW i } public Outcome eval (Rock r) { return WIN ¡ } public String toString() { return "Scissors ll ¡ } class Rock implements Item { public Outcome compete(Item it) { return it.eval(this) ¡ public Out come eval (Paper p) { return WIN i } public Out come eval (Scissors s) { return LOSE; } public Out come eval (Rock r) { return DRAW; } public String toString() { return "Rock"¡ } public class RoShamBol { static final int SIZE = 20¡ private static Randam rand = new Random(47)¡ public static Item newItem() switch(rand.nextInt(3)} { default: case O: return new Scissors()¡ " 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 recer, sin citar la fuente en un libro escrito por otros autores. 4 686 Piensa en Java case 1: return new Paper(); case 2: return new Rack() i public static void match(Item a, System.out.println( a + n vs. 11 + b + ": 11 + Item b) { a. compete (b) ) ; public static void main(String[) args) for(int i = Di i < SIZE¡ i++} match(newltem(), newltem(»; { /* 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 VS. Rack VE. Paper VS. Paper VS. Rack VS. Rack vs. Paper VS. Paper: LOSE Paper: DRAW Paper: LOSE Scissors: LOSE Scissors: LOSE Scissors: WIN Paper: LOSE 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() realiza 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 obtiene es la elegancia sintáctica que se consigue al realizar la Harnada: en lugar de escribir un código muy complicado para determinar 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 importante para usted antes de embarcarse en programas que impliquen un despacho múltiple. Cómo despachar con enumeraciones Realizar una traducción directa de RoS ha mBol.java a una solución basada en enumeraciones resulta problemático, porque las instancias enu m no son tipos, así que los métodos eval() sobrecargados no funcionan: no se pueden utilizar instancias en um como argumentos de tipo. Sin embargo, existen distintas técnicas para implementar técnicas de despacho mú ltiple que penniten sacar provecho de las enumeraciones. Una de sus técnicas utiliza un constmctor para inicializar una instancia enum con una "fila" de resultados; contemplado en su conjunto esto produce una especie de tabla de búsqueda: JJ: enumerated/RoShamBo2.java 19 TIpos enumerados 687 // Conmutación de una enumeración basándose en otra. package enumerated¡ import static enumerated.Outcome.*¡ public enum RoShamBo2 implements Competitor PAPER(DRAW, LOSE, WIN), SCISSORS(WIN, DRAW, LOSE), ROCK(LOSE, WIN, DRAW) ; private Outcome vPAPER, vSCISSORS, vROCK¡ RoShamBo2(Outcome paper,Outcome scissors,Outcome rack) this . vPAPER = paper; this.vSCISSORS = scissorsi this.vROCK = rack; { public Outcome compete (RoShamBo2 it) { switch (it) { default: case PAPER: return vPAPER¡ case SCISSORS: return vSCISSORS¡ case ROCK: return vROCK; public static void main(String[] args) RoShamBo.play(RoShamBo2.class, 20); /* 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étodo 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 interfaz 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; public interface Competitor A continuación, definimos dos métodos estáticos (se hacen estáticos para evitar tener que especificar el tipo del parámetro explicitamente). En primer lugar, match() invoca a competc() para uno de los objetos Competitor comparándolo con otro, y podemos ver que en este caso el parámetro de tipo sólo necesita ser Competitor. Pero en play(), el parámetro de tipo tiene que ser tanto Enum porque se lo utiliza en Enums.random() como Compctitor porque se le pasa a match( ): JI: enumeratedjRoShamBo.java JI Herramientas comunes para los ejemplos RoShamBo. package enumerated¡ import net.mindview.util.*¡ public class RoShamBo { public static & Competitor rsbClass, int sizel { for(int i = O; i < size; i'H} match ( Enums. random (rsbClass l ,Enums .random(rsbClass}); } 111,El método play() no tiene un valor de retomo que implique el parámetro de tipo T, por lo que parece que podríamos utilizar comodines dentro del tipo Class en lugar de emplear la descripción proporcionada en el ejemplo. Sin embargo, los comodines no pueden abarcar más de un tipo base, asi que estamos obligados a usar la expresión anterior. Utilización de métodos específicos de constante Puesto que los métodos específicos de constante nos pem1iten proporcionar diferentes implementaciones de método para cada instancia ellum, parece una solución perfecta para configurar un sistema de despacho múltiple. Pero aunque se las puede proporcionar de este modo diferentes comportamientos, las instancias enum no son tipos, por lo que no se las puede emplear como argumentos de tipo en las signaturas de los métodos. Lo mejor que podemos hacer en este caso es definir una instmcción switch: JJ: enumeratedJRoShamBo3.java JJ Utilización de métodos específicos de constante. package enumerated; import static enumerated.Outcome.*¡ public enum RoShamBo3 implements Competitor PAPER { public Out come compete(RoShamBo3 it} { switch(it) { default: JI Para aplacar al compilador case PAPER: return ORAW; case SCISSQRS: return LOSE; case ROCK: return WIN; 19 Tipos enumerados 689 ), 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); / * Misma salida que RoShamBo2.java * /// :Aunque este programa funciona y no resulta irrazonable, la solución de RoShamBo2.java parece requerir menos código a la hora de aiiadir un nuevo tipo, por lo que resulta más sencilla. Sin embargo, RoShamBo3.java puede simplificarse y co mprimirse: JI : enumerated/RoShamBo4.java package enumerated¡ public enum RoShamBo4 implements Competitor ROCK ( public Outcome compe te {RoShamBo4 opponent) return compete (SCISSORS, opponent) i ). 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 public static void main(String[] args) RoShamBo.play{RoShamBo4.class, 20) i /* Misma salida que RoShamBo2.java * ///:- 690 Piensa en Java Aquí, el segundo despacho es realizado por la versión de dos argumentos de compete( ), que realiza una secuencia de comparaciones 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. Cómo despachar con mapas EnumMap Es posible realizar un "verdadero" despacho doble utilizando la clase EnumMap, que está diseñada específicamente para trabajar con las enumeraciones. Puesto que el objetivo es conmutar entre dos tipos desconocidos, podemos usar un mapa EnumMap de mapas EnumMap para realizar el doble despacho: //: enumerated/RoShamBo5.java // Despacho múltiple usando un mapa EnurnMap de mapas EnumMaps. package enumerated¡ import java.util.*¡ import static enumerated.Outcome.*¡ enum RoShamBo5 implements Competitor PAPER, SCISSORS, ROCK¡ static EnurnMap(RoShamBo5.class)) ¡ initRow(PAPER, DRAW, LOSE, WIN); initRow(SCISSORS, WIN, DRAW, LOSE) i initRow(ROCK, LOSE, WIN, DRAW) i static void initRow(RoShamBo5 it, Outcome vPAPER, Out come vSCISSORS, Out come vROCK) EnurnMap row = RoShamBo5.table.get(it) ¡ row.put (RoShamBo5. PAPER, vPAPER); row.put{RoShamBo5.SCISSORS, vSCISSORS) ¡ row.put(RoShamBo5.ROCK, vROCK) i { public Out come compete(RoShamBo5 it) return table.get(this) .get(it); public static void main(String[] RoShamBo.play(RoShamBo5.class, args) 20) ¡ /* Misma salida que RoShamBo2.java *///:- 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. Utilización de una matriz 2·0 Podemos simplificar la solución aún más dándonos cuenta de que cada instancia enum tiene un valor fijo (basado en su orden de declaración) y que el método ordinal() genera este valor. Una matriz bidimensional que asigne los competidores a los distintos resultados, permite obtener la solución más pequeña y directa (y también posiblemente la más rápida aunque recuerde que EnumMap utiliza una matriz interna): //: enumerated/RoShamBo6.java / / Enumeraciones utilizando ntablas" en lugar de despacho múltiple. package enumerated¡ 19 Tipos enumerados 691 import static enumerated.Outcome.*¡ enum RoShamBo6 implements Competitor PAPER, SCISSORS, ROCK; private static Outcome [] [J table ::: { DRAW, LOSE, WIN }, II PAPER { WIN, DRAW, LOSE }, II SCISSORS { LOSE, WIN, DRAW }, II ROCK }; public Outcome compete (RoShamBo6 other) { return table [this. ordinal ()] (other. ordinal ()] public static void main{String[] RoShamBo.play(RoShamBo6.class, i args) 20); 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 parece 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 constantes. Sin embargo, no hay nada que nos impida tener una tabla que genera un objeto de función. Para ciertos tipos de problemas, 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 versas ocasiones a lo largo del libro, la elegancia es importante y la claridad puede marcar la diferencia entre una solución adecuada 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, consistente 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 directa, por lo que sigue estando presente en el código antiguo (¡ya veces en el código nuevo!), en la biblioteca y en la documentació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 anotaciones 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 anotaciones 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 responsabilidad completamente nuestra. La sintaxis de las anotaciones es razonablemente simple y consiste principalmente en la adición del símbolo @ al lenguaje. 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 anotación genera un error de compilación si se escribe mal accidentalmente el nombre del método o si se proporciona 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á pennitida, 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 anotaciones 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 extremadamente 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, estructuradas, 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 anotaciones, 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 compilador 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.*¡ public class Testable { public void execute() Syst.em.out.println(!1Executing .. ") ¡ @Test void testExecute() { execute() i ///> Los métodos anotados no difieren de los demás métodos. La anotación @Test de este ejemplo puede utilizarse en combinació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 archivos 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 herramienta 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 conjunto 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 sistema. 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 compilador, 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.*; public class PasswordUtils @UseCase(id = 47, description Passwords must contain at least one numeric") public boolean validatePassword(String password) return (password.matches("\\w*\\d\\w*tI)) i 11 @UseCase(id = 48} public String encryptPassword(String passwordl { return new StringBuilder (password) . reverse () . toString () ; @UseCase(id = 49, description = "New passwords can I t equal previously used ones ll ) public boolean checkForNewPassword( List prevPasswords, String password) return IprevPasswords.contains(password); 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 description, por lo que cuando se procese la clase con un procesador de anotac iones aparecerá el va lor predeterminado defmido en @i nterface UseCase. Podemos imaginarnos fácilmente cómo podría emplearse un sistema nar la funciona lidad a medida que vamos completando el diseno. C0l110 éste para "esbozar" un programa y luego relle- Meta-anotaciones Actualmente sólo hay tres anotaciones estándar (descritas anterionnente) y cuatro meta-anotaciones definidas en el lenguaje Java. Las meta-anotaciones se utilizan para anotar anotaciones: « useCases, Class el ) for (Method ro : el. getDeclaredMethods ()) { UseCase uc = m.getAnnotation (UseCase.class ) ; i f {uc ! = nulll { System.out.println ( "Found Use Case:" + uc .id () + 11 11 + uc. description () ) ; useCases.remove(new Integer (uc.id ())) i for (int i : useCases ) { System. out. println ( "Warning: Missing use case- 11 + i ) i public static void main (String[J args ) { List useCases = new ArrayList() Collections.addAll (useCases, 47, 48, 49, 50 ) i trackUseCases(useCases, PasswordUtils.class ) i i 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 anotació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 verdadera limitación. También podemos tener elementos que sean ellos mismos anotaciones. Como veremos un poco más adelante, las anotaciones anidadas pueden resultar muy útiles. Restricciones de valor predeterminado El compilador es bastante quisquilloso acerca de los valores predeterminados de los elementos. Ningún elemento puede tener un valor no especificado. Esto quiere decir que los elementos deben tener valores predeterminados o valores proporcionados por la clase que utilice la anotación. Existe otra restricción y es que ninguno de los elementos de tipo no primitivo pueden tener null corno valor, ni a la hora de declararlos en el código fuente ni cuando se los defina como valor predetem1inado dentro de la interfaz de anotación. Esto hace que resulte dificil escribir un procesador que actúe de manera distinta dependiendo de la presencia o ausencia de un elemento, porque todos los elementos están presentes en la práctica en todas las declaraciones de anotaciones. Podemos obviar este problema tratando de comprobar si existen valores específicos, como por ejemplo cadenas de caracteres vacías o valores negativos: jj: annotationsjSimulatingNull.java import java.lang.annotation.*¡ @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. Generación de archivos externos Las anotaciones son especialmente útiles a la hora de trabajar con sistemas que requieran algún tipo de infonnación adicional como acompañamiento del código fuente. Tecnologías como Enterprise JavaBeans (en las versiones anteriores a EJB3) requieren numerosas interfaces y descriptores de implantación que fonnan una especie de "plantilla" de código, definida de 698 Piensa en Java la misma fomla para cada componente bean. Los servicios web. las bibliotecas de marcadores personalizados y las herramientas 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 informaciones 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 normalmente 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. * ¡ ~Ta rget (ElementTyp e.TYPE ) 11 Sólo se aplica a clases ~Retention{RetentionPolicy.RUNTIME) public @interface DBTable { public String name () default HH ¡ } 1//, Cada tipo de elemento ElementType que especifiquemos en la anotación @;Target es una restricción que le dice al compilador que nuestra anotación sólo se puede aplicar a ese tipo concreto. Podemos especificar un sólo valor de la enumeración enum ElernentType, o bien podemos especificar una lista f0n11ada por cualquier combinación de valores separados por comas. Si queremos aplicar la anotación a cualquier tipo de elemento Elemen tType. podemos omitir la anotación @'Ta rget, aunque esta solución es bastante poco común. Observe que @DOTable tiene un elemento narne( ). de modo que la anotación pueda suministrar un nombre para la tabla de la base de datos que el procesador tiene que crear. He aquí las anotaciones para los campos del componente JavaBean: /1: annotations/database/Constraints.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 representa 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 adecuados. 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 definir 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 columna. Observe que el va lor predetenninado para el elemento contraints() es @Constraints. Puesto que no hay valores de elementos 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¡ public @interface Uniqueness { Constraints constraints() default @Constraints(unique~true) i 111,He aquí un componente bean simple que utiliza estas anotaciones: JJ: annotationsJdatabaseJMember.java package annotations.database¡ @DBTable (name ~ "MEMBER") public class Member { @SQLString(30) String firstName¡ @SQLString(50) String lastName; @SQLlnteger Integer age; @SQLString(value : 30, constraints : @Constraints(primaryKey true) ) String handle i static int memberCount¡ public String getHandle () { return handle i } public String getFirstName () { return firstName i public String getLastName () { return lastName; publ ic String toString () { return handle ¡ } public Integer getAge () { return age i } 1//,A la anotación de clase @DBTable se le da el valor "MEMBER", que se utilizará como nombre de tabla. Las propiedades de bean, firstName y lastName, están ambas anotadas con @SQLString y sus valores de elementos son 30 y 50, respecti- 700 Piensa en Java vamente. Estas anotaciones son interesantes por dos razones: en primer lugar, utilizan el valor predetenninado en la anotación @Constraints anidada. y en segundo lugar emplean una característica especia l de abreviatura. Si definimos un elemento 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ánticamente 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 compleja. 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, bastante larga, basada en una pareja de nombre-valor para esta anotación anidada, volviendo a especificar el nombre de elemento 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 precisamente 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 cualificar 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, "VARCHAR(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 usando 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 relevante (por ejemplo, @SQLInteger), para anotar el campo deseado. Esto resulta ligeramente complicado, pero el compilador 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. Las anotaciones no soportan la herencia No podemos utilizar la palabra clave extends con @interfaces . Es una pena, porque una solución elegante seria oefinir una anotación @TableColumn, como hemos sugerído anteriormente, con una anotación anidada de tipo @SQLType. De esa forma, podriamos heredar todos nuestros tipos SQL, como @SQLInteger y @SQLString de @:SQLType. Esto reduciría la cantidad de texto que hay que escribir y haría más elegante la sintaxis. No parece que exista ninguna intención de que las 3l1otaciones soporten el mecanismo de herencia en versiones futuras del lenguaje, por lo que los ejemplos anteriores parecen ser lo máximo que podemos hacer teniendo en cuenta las circunstancias actuales. Implementación del procesador He aquí un ejemplo de procesador de anotaciones que lee un archivo de clases, localiza sus anotaciones de base de datos y genera el comando SQL para construir la base de datos: //: annotations/database/TableCreator.java /1 Procesador de anotaciones basado en el mecanismo de reflexión. 11 {Args: annotations.database.Member} package annotations.database; import java.lang.annotation.*; 20 Anotaciones 701 import java.lang.reflect .*¡ import java.util.*¡ public class TableCreator public static void main(String(] args) throws Exception if(args.1ength < 1) { System. out .print ln ("arguments: annotated classes tl ) ; System.exit (O) ¡ for(String className : args) Class cl = Class. forName (clas sName ) ; DBTable dbTable = cI.getAnnotation(DBTable.class) ¡ if (dbTab1e == null) { System.out.println{ "No DBTable annotations in class " + className); continue; String tableName = dbTable.name() i // Si el nombre está vacío, utilizar el nombre de la clase: if (tableName. length () < 1) tableName = cl . getName() . toUpperCase( ) ; List columnDefs = new ArrayList() i for(Field field : cl.getDeclaredFields(») ( String columnName = null; Annotation[) anns = field.getDeclaredAnnotations() i if(anns.length < 1) continue¡ // No es una columna de la tabla de base de datos if (anns [O) instanceof SQLlnteger) SQLlnteger sInt = (SQLlnteger) anns[O); // Utilizar el nombre de campo si no se especifica un nombre . if(sInt.name() .length() < 1) columnName field.getName( ) . toUpperCase() ; else sInt.name() ; columnName columnDefs.add(columnName + " INT" + getConstraints(sInt.constraints())) ; if (anns (O] instanceof SQLString) SQLString sString = (SQLString) anns[O); // Utilizar el nombre de campo si no se especifica un nombre. if(sString.name() .length() < 1) columnName field.getName() . toUpperCase() i el se columnName sString.name()¡ columnDefs . add(columnName + 11 VARCHAR(" + sString. value () + ")" + getConstraints(sString.constraints(») ; StringBuilder createCommand = new StringBuilder( "CREATE TABLE " + tableName + ,, ( It) ¡ for (String columnDef : columnDefs) createCommand. append (" \n "+ columnDef + "," ); // Eliminar coma final String tableCreate = createCommand.substring( 0, createCornmand.length() - 1 ) + ") ¡"; System. out .println ("Table Creation SQL for " + className + " is : \n" + tableCreate) ¡ 702 Piensa en Java privace static String getConstraints(Constraints con) String constraints = "ti; if(!con.allowNull(» constraints += " NOT NULL"; if(con.primaryKey() ) constraints +: PRlMARY KEY"; { if(con.unique(» constraints += UNIQUE" ; return constraints; /* Output: Table Creation SQL for annocations . database.Member CREATE TABLE MEMBER( FIRSTNAME VARCHAR(30)); Table Creation SQL for annotations.database . Member CREATE TABLE MEMBER( FIRSTNAME VARCHAR(30) , LASTNAME VARCHAR(50)); Table Creation SQL for annotations.database.Member CREATE TABLE MEMBER( FIRSTNAME VARCHAR(30) , LASTNAME VARCHAR(50) , AGE INT); Table Creation SQL for annotations.database.Member CREATE TABLE MEMBER( FIRSTNAME VARCHAR(30) , LASTNAME VARCHAR (50) , AGE INT, HANDLE VARCHAR(30) PRlMARY KEY); is is is : is : * /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 nombre de la columna de la tabla. Observe que, como 110 existe posibilidad de herencia en las interfaces de anotación, la utilizació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 contiene 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 obliga a recompilar el código Java cada vez que queramos cambiar el nombre de la tabla, lo que puede resultar no muy conveniente. 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 escribi 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 Utilización de apt para procesar anotaciones La herramienta de procesamiento de anotaciones apt es la primera versión de Sun de este tipo de herramienta. Puesto que se trala de una versión relativamente joven, la herramienta sigue siendo un poco primitiva. pero dispone de una serie de características que pueden faci litamos la tarea. Como j avac, ap t está diseñada para ejecutarse con archivos fuen te Java en lugar de con clases compiladas. De manera predetenninada, a pt compila los archivos fuente una vez que ha tenninado de procesarlos. Esto es útil si estamos creando automáticamente nuevos archivos fuen te como parte del proceso de construcción de la aplicac ión. Oc hecho, ap t comprueba si existen anotaciones en los archivos fuente recién creados y los compila, todo ello en una pasada. Cuando el procesador de anotaciones crea un nuevo archivo fuente, dicho archivo es comprobado a su vez en busca de anotaciones, en lo que constituye una nueva ronda (como se denomina en la documentación) de procesamiento. La herramienta continuará efectuando ronda tras ronda de procesamiento hasta que no se cree ningún archivo fueme. Entonces, compilará todos los archivos fuente existentes. Cada anotación que escribamos necesitará su propio procesador, pero la herramienta apt pennite agrupar fácilmente varios procesadores de anotación. La herramienta nos pennite especi ticar múltiples clases que haya que procesar, lo cual resulta más fácil que tener que iterar manualmente a través de una serie de clases File. También podemos agregar lo que se denominan procesos escucha para recibir una notificación cuando se complete una ronda de procesamiento de anotaciones. En el momento de escribir estas líneas, apt no está dispo nible como tarea Ant (consulte el suplemento en htlp://A1indVieH'. net/Books/BetlerJavo), pero mientras tanto se puede, obviamente, ejecutar la herrami enta como tarea externa desde Ant. Para compilar los procesadores de anotaciones descritos en esta sección, es necesario tener tools.jar en la ruta de clases; es ta biblioteca también contiene las interfaces com.sun .rnirror.*. apt funcio na util izando una factoría de procesadores de anotaciones (AnnotationProcessorFactory) para crear el tipo apropiado de procesador para cada anotación que encuentre. Cuando se ejecuta apt, hay que especificar una clase factoría o una ruta de clases en la que la herramienta pueda localizar las factorías que necesite. Si no hacemos esto, a pt se embarcará en un arcano proceso de descubrimiento, cuyos detalles pueden encontrarse en la sección Deve/oping 011 Anl1orolion Processor de la documentación de SUD. Cuando creamos un procesador de anotaciones para utilizarlo con a pt, no podemos emplear los mecanismos de reflexión de Java porque estamos trabajando con código fuente, no con clases compiladas. 4 La AP I mirror 5 resuelve este problema permitiéndonos visualizar los métodos, campos y tipos en el código fuente no compilado. He aquí una anotación que puede utilizarse para extraer los métodos públicos de una clase y convertirlos en una interfaz: 11: annotations / Extractlnterface.java II Procesamiento de anotaciones basado en APT. package annotationsi import java.lang.annotation.*¡ @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 @Extractlnterface (" IMul tiplier") public class Multiplier { public int multiply(int x, int total = O; for(int i = O; i < x; int y) { i++l total = addltotal, y); return total; private int add(int x, int y) { return x + y; } public static void main(String(] argsl Multiplier m = new Multiplier(); System.out.println("ll*16 = 11 + m. multiply(ll, 16)); /* 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. * ; public class InterfaceExtractorProcessor implements AnnotationProcessor { private final AnnotationProcessorEnvironment env¡ prívate ArrayList interfaceMethods new ArrayList{); public InterfaceExtractorProcessor( AnnotationProcessorEnvironment env) { this.env env¡ } public void process{) for(TypeDeclaration typeDecl : env.getSpecifiedTypeDeclarations{)) Extractlnterface annot = typeDecl.getAnnotation(Extractlnterface.class) ; if(annot == null) break; for(MethodDeclaration m : typeDecl.getMethods()) if (m.getModifiers () .contains(Modifier.PUBLIC) && ! (m.getModifiers{) . contains(Modifier.STATIC))) interfaceMethods.add(m) ; if (interfaceMethods.size() > O) { try { PrintWriter writer = env.getFiler() .createSourceFile(annot.value()); writer. println ("package " + typeDecl.getPackage() .getQualifiedName() +,,;,,); writer .println ("public interface " + annot. value () + " {"); for(MethodDeclaration m : interfaceMethods) 20 Anotaciones 705 writer.print(tt public ") i writer.print (m.getReturnType () wri ter. print (m . getSimpleName () int i "" O; + + 11) i (") i for(ParameterDeclaration parm ro. getParameters (}) { writer.print (parm.getType() + !I " + parm.getSimpleName()) ; if(++i < m.getParameters(} .size(}) writer.print ( " , 11 ); 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é procesando. 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 cualquier error que pueda haberse producido en el procesamiento, junto con el lugar del código fuente donde se haya producido. 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 nuestra 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 procesador 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 atds, AnnotationProcessorEnvironment env) { return new InterfaceExtractorProcessor(env); public Collection supportedAnnotationTypes{) return COllections . singleton("annotations.Extractlnterface lt ) ; 706 Piensa en Java public COllection supportedOptions() return Collections.emptySet( ) ; } 111 ,Sólo hay tres métodos en la interfaz An notation ProcessorFactory. Como puede ver, el que proporciona el procesador es getProccssorFor(), que toma un conjunto Set de declaraciones de tipo (las clases Java para las que se está ejecutando la herramienta apt), y el objeto An no tationProcesso r Enviro nm ent. que ya hemos visto cómo se pasaba al procesador. Los otros dos métodos, suppo rtedAnn otationTypes( ) y supportedOptions( ), sirven para poder comprobar que disponemos de procesadores para todas las anotaciones encontradas por apt y que soportamos todas las anotaciones especificadas en la línea de comandos. El método ge t Processo r For( ) es paniculamlente importante. porque si no devolvemos el nombre de clase completo de nuestro lipo de anotación dentro de la colección Strillg, a pt emitirá una advertencia infonnando de que no existe el procesador correspondiente y tenninará su ejecución sin hacer nada. El procesador y la factoría se encuentran en el paquete annotations, así que, para la estructura de directorios anterior. la línea de comandos está incrustada en el marcador de comentarios 'Exec' al principio de InterfaceExtractorProcessor. java. Esto le dice a a pt que tiene que utilizar la clase factoría definida anterionnente y procesar el archivo M ultiplie r.java. La opción -s especifica que los nuevos archivos deben crearse en el directorio annotatio Ds. El archivo IM ultiplicr.j ava generado, como podemos adivinar examinando las instrucciones prin tl n() en el procesador anterior, tiene el aspecto siguiente: pac kage annotations¡ public interface IMultiplier public int multiply (int x, int y) ¡ } Este archivo también será compilado por apt, así que verá que el archivo IMultiplier.class aparece en el mismo directorio. Ejercicio 2 : (3) Añada al extractor de interfaces el soporte para la operación de división. Utilización del patrón de diseño Visitante con apt El procesamiento de anotaciones puede resultar bastante complejo. El ejemplo anterior es un procesador de anotaciones relativamente simple que sólo interpreta una anotación, a pesar de lo cual requiere de una cierta complejidad para poder llevar a cabo su tarea. Para evitar que la complejidad crezca desmesuradamente cuando tengamos más anotaciones y más procesadores, la API m irror proporciona clases para dar soporte al patrón de diseño Visitante. Este patrón de diseño es uno de los patrones clásicos del libro Design Pallerns de Gamma et al., y también puede encontrar una explicación más detallada en Thinking in Patlerns. Un Visitante recorre una estructura de datos o colección de objetos, realizando una operación con cada uno. La estructura de datos no necesita estar ordenada, y la operación que se realice con cada objeto será específica del tipo de éste. Esto hace que se desacoplen las operaciones con respecto a los propios objetos, lo que quiere decir que podemos añadir nuevas operaciones sin necesidad de añadir métodos a las definiciones de clase. Esto hace que este patrón de diseño sea muy útil para el procesamiento de anotaciones, porque una clase Java puede considerarse como una colección de objetos TypeDecla r atio n, Field DeclaratioD, Met hodDeclaration, etc. Cuando utilizamos la herramienta ap t con el patrón Visitante, proporcionamos una clase Visito r que tiene un método para gestionar cada tipo de declaración que visitemos. De este modo, podemos implementar el comportamiento apropiado para las anotaciones asociadas con métodos, clases, campos, etc. He aquí de nuevo el generador de tab las SQL, pero esta vez con una factoría y un procesador que hace uso de l patrón de diseño Visitante: 11 : annotations / database / TableCreationProcessorFactory.java II II II II El ejemplo de la base de datos usando el patrón de diseño Visitante. {Exec: apt -factory annotations.database.TableCreationProcessorFactory database/Member.java -s database} package annotations.database¡ 20 Anotaciones 707 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.*¡ public class TableCreationProcessorFactory implements AnnotationProcessorFactory { public AnnotationProcessor getProcessorFor{ Set atds, AnnotationProcessorEnvironment env) { recurn new TableCreationProcessor(env); public Collection supportedAnnotationTypes() recurn Arrays.asLisc( lIannotations.database.DBTable", lIannotations.dacabase.Constraints ll , "annocations.database.SQLString", "annocacions.database.SQLlnceger") ; public Collection supportedOptions{) return Collections.emptySet(); private static class TableCreationProcessor implements AnnotationProcessor { private final AnnotationProcessorEnvironment env¡ pri vate String sql = 1111; public TableCreationProcessor( AnnotationProcessorEnvironment env) { this. env = env; public void process() for {TypeDeclaration typeDecl env.getSpecifiedTypeDeclarations ()) typeDecl.accept(getDeclarationScanner( new TableCreationVisitor(), NO_OP)); sql = sql.substring(O, sql.lengthO - 1) + ") ;"; System.out.println(lIcreation SQL is :\nll + sql); sql = ""; private class TableCreationVisitor extends SimpleDeclarationVisitor public void visitClassDeclaration{ ClassDeclaration d) { DBTable dbTable = d.getAnnotation(DBTable.class); if(dbTable != null) ( sql += IICREATE TABLE "; sql += (dbTable.name() .length() < 1) ? d.getSimpleName() .toUpperCase() dbTable.name() ; sql += 11 ( 11; public void visitFieldDeclaration( FieldDeclaration d) String columnName = ""; if {d. getAnnotation (SQLlnteger. classl != nulll { 708 Piensa en Java SQLlnteger sInt = d.getAnnotation( SQLlnteger.class) ; JI Utilizar el nombre del campo si no se especifica un nombre. i f (s lnt . name () . length ( ) < 1) columnName el se columnName sql += "\n d.getSimpleName() . toUpperCase() i sInt.name{) i columnName + " INT" + getConstraints (sInt. constraints (» + 11; "+ 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() columnName else columnName sql += "\n < 1) d.getSimpleName() ,toUpperCase(}¡ sString.name() i + columnName + " VARCHAR(" + sString. value () + ")" + 11 getConstraints (sString . constraints () ) + private String getConstraints(Constraints con) String constraints = 1111; if(!con.allowNull() ) constraints += 11 NOT NULLII; if(con.primaryKey()) constraints += ti PRlMARY KEY"; if(con.unique()) constraints += UNIQUE"; return constraints; 11,"; { La salida es idéntica a la del ejemplo DBTable anterior. El procesador y el visitante son clases internas en este ejemplo. Observe que el método process( ) sólo añade la clase visitante e inicializa la cadena SQL. Los dos parámetros de getDeclarationScanner( ) son visitantes; el primero se utiliza antes de visitar cada declaración y el segundo después. Este procesador sólo necesita el visitante previo visita, así que se proporciona NO _ OP como segundo parámetro. Éste es un campo de tipo estático de la interfaz DeclarationVisitor, que indica un objeto DeclarationVisitor que no lleva a cabo ninguna tarea. TableCreationVisitor am plia SimpleDeciarationVisitor, sustituyendo los dos métodos visitClassDeciaration() y visitFieldDeciaration(). SimpleDeciarationVisitor es un adaptador que implementa todos los métodos de la interfaz DeclarationVisitor, por lo que podemos concentrarnos en aquellos que necesitemos. En visitClassDeclaration(), se comprueba el objeto ClassDeclaration en busca de la anotación DBTable, y en caso de encontrarla, se inicializa la primera parte del objeto String de creación de la cadena SQL. En visitFieldDcciaration( ), se consulta la declaración de campo para ver las anotaciones existentes y la información se extrae de forma bastante similar a como se hacia en el ejemplo original que hemos presentado anterionnente en el capítulo. Podría parecer que esta fonna de hacer las cosas resulta más complicada, pero pennite obtener una solución más escalable. Si la complejidad del procesador de anotaciones se incrementa, escribir nuestro propio procesador autónomo, como en el ejemplo anterior, podría llegar pronto a resultar bastante complicado. Ejercicio 3: (2) Añada a TableCreationProcessorFactory.java soporte para más tipos SQL. 20 Anotaciones 709 Pruebas unitarias basadas en anotaciones Las pruebas lInilarias son la práctica de crear una o más pmebas para cada método de una clase. con el fin de comprobar de fonna melódica las distintas partes de una clase y verificar que su comportamiento es correcto. La herramientas más popular de pruebas unitarias en Java se denomina JUl1Í1; en el momento de escribir estas líneas, JUnit estaba a punto de ser actua lizada a su versión 4, para poder incorporar anotaciones 6 . Uno de los principales problemas de las versiones de JUnit que no incorporaba el soporte de anotac iones es la cantidad de "ceremonia" necesaria para preparar y ejecutar pruebas JUnit. Esta complejidad se redujo a lo largo del tiempo. pero las anotaciones permitirán simplificar todavía más el proceso de pruebas. Con las versiones de JUnit que no disponían de soporte de anotaciones, era necesario crear una clase separada para definir las pmebas unitarias. Con las anotaciones, podemos inc luir las pruebas unitarias dentro de la clase que hay probar, reduciendo así al míni mo el tiempo y la complejidad de las pruebas unitarias. Este técnica tiene la ventaja adicional de permitir comprobar tanto los métodos privados como los públicos. Puesto que este marco de trabajo para pmebas que utilizamos como ejemplo está basado en anotaciones, lo denominaremos @U nit. La forma más básica de pruebas, que es la que uti lizaremos la mayor parte del tiempo, sólo necesita la anotación @Test para indicar lo que hay que probar. Una opción es que los métodos de prueba no tornen ningú n argumento y devuelvan un valor boolean para indicar el éxito o el fa llo de la pmeba. Podemos utilizar cualquier nombre que queramos para los métodos de prueba. Asim ismo, los métodos de prueba @U nit pueden tener cualquier tipo de acceso que deseemos, incluyendo private. Para uti lizar@Unit, todo lo que hace fa lta es importar nCl.mindview.atunit,1 marcar los métodos y campos apropiados con marcadores de prueba @U nit (los cuales veremos en los siguientes eje mplos) y hacer que el sistema de construcción ejecute @U nit con la clase resu ltan te. He aqui un ejemplo simple: 11: annotations/AtUnitExamplel.java package annotations¡ import net.mindview.atunit.*¡ import net.mindview.util.*¡ public class AtUnitExamplel { publ ic String methodOne () { return "This is methodOne"¡ public int methodTwo() System. out .println ("This is methodTwo") return 2; i @Test boolean methodOneTest () { return methodOne () . equals ( "This is methodOne") ¡ @Test boolean m2 () { return methodTwo () == 2; @Test prívate boolean m3 () { return true; } II Muestra la salida en caso de fallo: @Test boolean failureTest () { return false; @Test boolean anotherDisappointment () { return falsej } public static void main(String[) args) throws Exception OSExecute .command ( "java net.mindview.atunit.AtUnit AtUnitExamplel" ) ¡ 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. 7 Esta bibliotcca es parte del código del libro. disponible en wW\\'.AfilldI7f!lulel. 710 Piensa en Java m2 This is methodTwo m3 failureTest (failed) anotherDisappointment ( S tests) (failed) »> 2 FAILURES «< annotations.AtUnitEx amplel : failureTest annotations.AtUnitExamplel : anotherDisappointment *///,Las clases que hay que probar con @Unit deben encontrarse en paquetes. La anotación @Test que va antes de los métodos methodOneTest(), m2(), m3(), failureTest() y anotherDisappointJIlcnt() le dice a @Unit que ejecute estos métodos como pruebas unitarias. También garantiza que esos métodos no tornen ningún argumento y devuelvan un valor boolean o void. Nuestra única responsabilidad a la hora de escribir la prueba unitaria consiste en detenninar si la prueba tiene éxito o falla , y devuel ve true o false, respecti vamente (para los métodos que devuelvan un va lor boolean). Si está famili ari zado con JUnit, también se habrá fijado en que @Unitproporciona una salida más informativa: puede verse la prueba que se está ejecutando actualmente, lo que hace que la salida de dicha prueba sea más útil, y al final nos dice las clases y pruebas que han producido errores. No estamos obligados a incluir los métodos de prueba dentro de nuestras clases, si esa solución no nos sirve. La fonn a más fácil de crear pruebas no embebidas es mediante el mecanismo de prueba: //: annotations/AtUnitEx ternalTest.java // Creación de pruebas no embebidas. package annotations; import net.mindview .atunit .* ; import net.mindview . util. * ¡ pUblic cIass AtUnitEx ternalTest extends AtUnitExamplel @Test boolean _methodOne() { return methodOne () . equals ("This is methodOne"); @Test boolean _methodTwo() { return methodTwo() == 2; } pubIic static void main(String(] args) throws Exception OSExecute.command( "java net . mindview.atunit.AtUnit AtUnitExternalTes t tl ) ; } / * 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 directamente 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 public class AtUnitComposition ( AtUnitExamplel testObject : new AtUnitExamplel() @Test boolean _methodOne{) { return i testObject . methodOne{) . equals("This 15 methodOne lt ) @Test boolean _methodTwo{) r eturn testObject . methodTwo() i == 2; public static void main(String[] args) throws Exception { OSExecute.command( "java net.mindview . atunit.AtUnit AtUnitComposition tl ) ; /* 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, podemos 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.*; public class AtUnitExample2 { public String methodOne () { return "This is methodOne " ; public int methodTwo () { System.out.println(ItThis is methodTwo U ) ; return 2; @Test void assertExample() assert methodOne() . equals( "This is methodOne l! ); @Test void assertFailureExample() assert 1 == 2 : "Wha t a surprise! ti ; @Test void exceptionExample() t hrows IOException { ne w FilelnputStream( "nofile . txt " ) ; 11 Genera excepción @Tes t boolean assert AndReturn() { 11 Aserción con mensaj e : assert me thodTwo () == 2: "methodTwo must egual 2"; return methodOne() .equals{ "This is methodOne lt ) ; 712 Piensa en Java public static void main(String[] argsl throws Exception OSExecute.command( "java net.mindview.atunit.AtUnit AtUnitExample2"J; / * 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 tests) (4 2 FAILURES «< annotations.AtUnitExample2: assertFailureExample annotations.AtUnitExample2: exceptionExample *jjj,»> 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.*¡ public class HashSetTest { HashSet testObject ~ new HashSet () ¡ @Test void initialization () { assert testObject.isEmpty(); @Test void _contains () { testObject.add{"one") i assert testObject.contains{llone"); @Test void _remove{) { testObject.add{ "one" ) ; testObject. remove ("one"); assert testObject.isEmpty{); public static void main(String[) args) throws Exception OSExecute.command( "java net.mindview.atunit.AtUnit HashSetTest ll ) ; /* Output: annotations.HashSetTest initialization remove contains OK (3 tests ) * jjj ,- La solución basada en la herencia parece más simple. en ausencia de otras restricciones. Ejercicio 4: (3) Verifique que se crea un nuevo objeto testObject antes de cada prueba. 20 Anotaciones 713 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.*¡ public class AtUnitExample3 private int n¡ public AtUni tExample3 (int n) { this. n public int getN () { return n ¡ public String methodOne () { return "This is methodOne" ¡ n¡ } public int methodTwo () { System.out.println(01This is methodTwo") ¡ return 2; @TestObjectCreate static AtUnitExample3 create () return new AtUnitExample3(47) ¡ { @Tes t boolean initializationO { return n == 47¡ @Test boolean methodOneTest() { return methodOne () . equals ("This is methodOne") ¡ @Test boolean m2 () { return methodTwo () == 2; public static void main(String(] args) throws Exception OSExecute.command( "java net.mindview.atunit.AtUnit AtUnitExample3") i 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 utilizando 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 import net.mindview util.*; import static net . mindview.util.Print.*; public class AtUnitExample4 ( static String theory = "All brontosauruses u + "are thín at ane end, much MUCH thicker in the + !I "middle, and then thin again at the far end."; private String word¡ private Random rand = new Random(); // Semilla basada en tiempo public AtUnitExample4 (String word) { this.word = word¡ public String getWord() { return word¡ } public String scrambleWord () ( List chars = new ArrayList() for(Character e : word.toCharArray()) chars.add(c) i Collections.shuffle(chars, rand) i StringBuilder result = new StringBuilder(); for(char eh : chars) result.append(ch) ; return result.toString() i } i @TestProperty sta tic List input Arrays. asList (theory. spli t (" " )); @Test Property static Iterator words = input.iterator(); @TestObjectCreate static AtUnitExample4 create () { if(words.hasNext()) return new AtUnitExample4(words.next()); else return null; @Test boolean words() print (" ,,, + getWord () + 11 1 " ) ; return getWord () . equals ( liare" ) ; @Test boolean scramblel () { // Cambiar a una semilla específica para obtener resultados verificables : rand = new Random(47); print ( 11 1 11 + getWord () + 11 1 11) i String scrambled = scrambleWord(); print (scrambl ed) ; return scrambled.equals(lIlAllI); @Test boolean scramble2() rand = new Random(74)¡ print (U I 11 + getWord () + U 1 U) ¡ String scrambled = scrambleWord(); print(scrambled) ; return scrambled .equals(Utsaeborornussu lt ) i public static void main {String[] args) throws Exception { System . out. println ( Ustarting U) ; OSExecute.command( lIjava net.mindview.atunit.AtUnit AtUnitExample4 1t / * 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. podemos 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.*; public class AtUnitExampleS private String text; public AtUnitExampleS (String text) { th is. text public String toString () { return text; ) @TestProperty static PrintWriter output¡ @TestProperty static int counter¡ @TestObjectCreate static AtUnitExampleS create() String id = Integer.toString(counter++); text; try ( output = new PrintWriter ("Test" + id + ". txt") ; catch (IOException e) { throw new RuntimeException (e ) ; return new AtUnitExampleS(id)¡ @TestObjectCleanup static void cleanup (AtUni tExampleS tobj) { System.out.println("Running cleanup") output. clase () ; i @Test boolean testl() output.print(l1testl") ¡ return true; @Test boolean test2() output. print (" test2") ¡ return true; @Test boo!ean test3() output.print("test3") ; return true; public static void main(String[] args ) throws Exception OSExecute.command{ "java net.mindview .atunit .AtUnit AtUni tExampleS " ) i / * 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. Utilización de @Unit con genéricos Los genéricos plantean un problema especial , porque no resulta posible "probar genéricamente". Debemos efectuar las pruebas para un parámetro de tipo específico o un conjunto de parámetros de tipo. La solución es simple: heredar una clase de prueba a partir de una ve rsión especificada de la clase ge nérica. He aquí una implementac ión simple de una pila : 11: annotations/StackL.java II Un pila construida sobre un contenedor linkedList. package annotations¡ import java.util.*¡ public class StackL private LinkedList list : new LinkedList(); public void push (T v) { list. addFirst (v); } public T top() { return list.getFirst(); public T pop () { return list. removeFirst () ¡ } ///,Para probar una versión String, hereda una clase de pmeba de StackL : 11 : annotations/StackLStringTest.java 11 Aplicación de @Unit a genéricos. package annotations; import net.mindview.atunit.*; import net.mindview.util.*¡ public class StackLStringTest extends StackL { @Test void yush () { push("one " ) ; assert tap() .equals("one ll ) push{"two") i assert top() .equals("two") i i @Test void yop () push(lIone"l ¡ push("two") ; assert pap () . equals (11 t wo ll ) i assert pop () . equals ("one") ; @Test void _ top() push("A"l i pUSh("S") ; assert top{).equals(IB"); assert top{).equals("B"); public static void main(String(] args) throws Exception { OSExecute.command( 20 Anotaciones 717 "java net.mindview.atunit.AtUnit StackLStringTest"l i } / * Output: annotations.StackLStringTest yush . J'op . _tap OK (3 tests) */ // ,- La única desventaja potencial de la herencia es que perdemos la capacidad de acceder a los métodos privados en la clase que se está probando. Si esto constituye un problema, podemos definir el método en cuestión como protected, o ailadir un método no privado @TestProperty que invoque al método privado (el método @TestProperty será luego eliminado del código de producción por la berramienta AlU nitRemover que se muestra más adelante en el capítulo). Ejercicio 8: (2) Cree una clase con un método privado y anada un método @TestProperty no privado como se ha descrito anterionnente. Invoque este método en su código de pruebas. Ejercicio 9: (2) Escriba pruebas @U nit básicas para HashMap. Ejercicio 10: (2) Seleccione un ejemplo de algún otro lugar del libro y añada pruebas @Unit. No hace falta ningún "agrupamiento" Una de las grandes ventajas de@UnitsobreJUnitesquenohacen falta "agrupamientos". En JUnit, necesitamos poder decir de alguna fanna a la herramienta de pruebas unitarias qué es lo que necesitamos probar, y esto requiere la introducción de "agrupamientos" de pmebas, para que JUnit pueda encontrarlos y ejecutar las pruebas. @Unit simplemente busca archivos de clase que contengan las anotaciones apropiadas, y ejecuta a continuación los métodos @Test. Uno de los principales objetivos que me planteé al desarrollar el sistema de pruebas @U nit es que fuera enormemente transparente, para que los desarrolladores pudieran comenzar a utilizarlo simplemente alladiendo métodos @Test, sin ningún otro código especial y sin ningún conocimiento adicional como los requeridos por JUnit y muchos otros sistemas de pruebas unitarias. Ya es suficientemente dificil escribir pruebas sin añadir nuevos errores, como para también perder el tiempo con complicaciones innecesarias, así que @U nit trata de hacer que la tarea de definir las tareas unitarias sea trivial. De esta fonna, resulta más probable que el diseñador se anime a escribir esas pruebas. 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 El marcador @Unit @TestProperty. package net.mindview . atunit; impore java.lang.annotation. * ; 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. *; public class AtUnit implements ProcessFiles.Strategy static Class testClass; static List failedTests= new ArrayList(}; static long testsRun = O; static long failures = O; public static void main(String[] args ) throws Exception { ClassLoader.getSystemClassLoader(} .setDefaultAssertionStatus(true ) ; II Habilitar aserciones new ProcessFiles(new AtUnit( ) , uclass" } .start(args); if(failures O) print ( "OK (" + testsRun + " tests)" ) ; else { print ( II(1I + testsRun + " tests)"); print("\n»> It + failures + It FAILURE u + (failures > 1 ? "Su: "") + " «<"); fer(String failed : failedTests) print (" "+ failed); public void process(File cFile) try { String cName = ClassNameFinder . thisClass( BinaryFile.read(cFile )) ; if (! cName. contains (" . ") ) return; // Ignorar clases no empaquetadas testClass = Class. forName (cName) ; ca tch (Except ion e ) { throw new RuntimeEx ception (e ) ; TestMethods testMethods = new TestMethods(); Method creator = null; Method cleanup = null; for(Method m : testC l ass.getDeclaredMethods()) testMethods . addlfTestMethod(m) ; if (creator == null} 20 Anotaciones 719 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 printnb ( " testMethods ) { !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 () ) ; print (success ? 11 ti : "(failed)"); testsRun++¡ if(!success) { failures++¡ failedTests.add(testClass.getName{) + ": " + m.getName()) ¡ if(cleanup != null} cleanup. ínvoke (testObject, testObject); catch(Exception e) { throw new RuntimeException{e) ; static class TestMethods extends ArrayList void addlfTestMethod(Method m) { if(m.getAnnotation(Test.class) == null) return; if(! (m .getReturnType {) . equals(boolean . class) 11 m.getReturnType{) .equals{void.class))) throw new RuntimeException ( "@Test method" + " must return boolean or void"); rn.setAccessible(true); II En caso de que sea privado, etc. add(m) ; 720 Piensa en Java private static Method checkForCreatorMethod(Method m) if(m.getAnnotation(TestObjectCreate . class) == null) return null¡ if (!m . getReturnType() . equals(testClass» throw new RuntimeException (U@TestObjectCreate 11 + "must return instance of Class to be tested") i if «m.getModifiers () & java .lang. reflect .Modifier. STATIC) < 1) throw new RuntimeException (II@TestObjectCreate " + "must be static."); m. setAccessible (true) ; return m; private static Methad checkForCleanupMethod(Method m) { if(m.getAnnotation(TestObjectCleanup.class) == null) return null; if(lm.getReturnType() .equals(void.class» throw new RuntimeException ("@TestObj ectCleanup " + "must return void") ; if { (m.getModifiers () & java.lang.reflect.Modifier.STATIC) < 1) throw new RuntimeException (u@TestObj ectCleanup " + "must be static."); if (m.getParameterTypes () .length == O I I m. getParameterTypes () [O l ! = testClass) throw new RuntimeException ( "@TestObj ectCleanup + "must take an argument of the tested type."); m.setAccessible{true) i return m; private static Object createTestObject(Method creator) if(creator != null) { { try { return creator.invoke(testClassl; catch (Exception el { throw new RuntimeException ("Couldn ' t "@TestObject (creator) method."); run " + else { II Utilizar el constructor predeterminado: try ( return testClass.newlnstance{); catch (Exception e) { throw new RuntimeException ( "Coulctn' t crea te a " + "test object. Try using a ®TestObject method.") i AlUnit.java utiliza la herramienta ProcessFiles de net.mindview.util. La clase AtUnit implementa ProcessFiles.Strategy, que contiene el método process(). De esta forma, se puede pasar una instancia de AlUnit al constructor ProcessFiles. El segundo argumento del constructor le dice a ProcessFiles que busque todos los archivos que tengan la extensión "c1ass". Si no proporcionamos un argumento de línea de comandos, el programa recorrerá el árbol de directorios actual. También podemos proporcionar múltiples argumentos que pueden ser archi vos de clase (con o sin la extensión .c1ass) o directorios. Puesto que @Unit encuentra automáticamente las clases y métodos que son susceptibles de prueba, no hace falta ningún mecanismo de "agrupamiento". 8 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 cualificado (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 cuando 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.*¡ public class ClassNameFinder { public static String thisClass(byte[] classBytes) Map offsetTable = new HashMap(); Map classNameTable new HashMap(); try ( DataInputStream data = new DataInputStream( new ByteArrayInputStream(classBytes)); int magic = data . readInt () ; // Oxcafebabe int minorVersion = data.readShort(); int majorVersion = data.readShort(); int constant-pool_count = data.readShort(); int[] constant-pool = new int[constant_pool_count]; for (int i = 1; i < constant-poo1_ count; i++) { int tag = data.read(); int tableSize¡ switchltag) ( case L II UTF int length = data.readShort(); char{] bytes = new char[length]; for(int k = O; k < bytes.length; k++) byteslkl = Ichar)data . readl); String className = new String(bytes); classNameTable.put(i, className ); break¡ case 5, II LONG case 6, II DOUBLE data.readLong(); // descartar 8 bytes i++¡ // Salto especial necesario break; case 7, II CLASS int offset = data.readShort(); offsetTable.put(i, offset); break; case 8, II STRING data.readShort(); // descartar 2 bytes break; case 3, II INTEGER case 4, II FLOAT case 9, II FIELD REF case 10, II METHOD REF case 11, II I NTERFACE METHOD- REF 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); short access_flags = data.readShort(); int this class = data.readShort(); int super_class = data.readShort()¡ return classNameTable.get( offsetTable. get (this_clas s ) ) . replace ( '/ ' , catch(Exception el { '. ,) i throw new RuntimeException{e); 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 realizada 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 constantes del programa y es, por tanto, de tamaño variable; el siguiente valor short nos dice cuál es el tamaño para poder asignar 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 simplemente recorrer ésta y extraer los fragmentos de interés. así que, como puede ver en el ejemplo, se descarta una gran cantidad 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 contiene ".', 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étodos @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 continuació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, razonablemente, 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 excepció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 genera 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 durante las pmebas. Eliminación del código de prueba Aunque en muchos proyectos no pasa nada si dejamos el código de prueba en el código final (especialmente si definimos todos los métodos de prueba como private, cosa que siempre podemos hacer), en algunos casos conviene eliminar el código de pmeba, para que el tamaiio del producto sea menor o para que ese código no esté al alcance de l cliente. Esto requiere prácticas de ingeniería de código intermedio demasiado sofisticadas como para realizar las manualmente. Sin embargo, la biblioteca de código abierto Javassist ll hace posib le la ingeniería de código intemledio. El siguiente programa adm ite un indicador -r opcional como primer argumento; si incluimos el indicador, eliminará las allmaciones @"Tes t, mientras que si no lo incluimos se limitará a mostrar esas anotaciones. También se emplea aquí ProcessFiles para recorrer los archivos y direclOrios que hayamos elegido: 11 : net/mindview/atunit/AtUnitRemover.java II Visualiza las anotaciones @Unit existentes en los archivos de II clase compilados. Si el primer argumento es " _r", se eliminan II las anotaciones @Unit. // (Argso .. ) II {Requires: javassist.bytecode.ClassFile¡ II Debe instalar la biblioteca Javassist disponible en // http, // sourceforge.net / projects / jboss/ ) package net.mindview.atunit¡ import javassist.*¡ import javassist.expr. * ¡ import javassist.bytecode.*¡ import javassist.bytecode.annotation.*¡ import java.io.*; import java.util.*; import net.mindview.util.*¡ import static net.mindview.util.Print.*¡ public class AtUnitRemover implements ProcessFiles.Strategy private static boolean remove = false; public static void main (String[] args ) throws Exception i f (args .length > O && args [01 . equals ( " -r" )) ( remove = true¡ String[ ] nargs new String{args.length - 1] ¡ System. arraycopy (args, 1, nargs, 0, nargs.length ) ¡ args = nargs; new ProcessFiles( new AtUnitRemover () , "class" ) .start (args } ; public void process(File cFile ) boolean modified = false¡ try { String cName = ClassNameFinder.thisClass( 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) if(attr == null) continue; for(Annotation ann : attr.getAnnotations()) if(ann.getTypeName() i . startsWi th (ltnet. mindview. atuni t") ) print (ctClass .getName () + 11 Methad: + mi.getName() + " " + ann); if (remove) { ctClass.removeMethod(method) modified = true; i ) /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 manipular 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 descubrimos que eliminar los campos @TestProperty resulta más complejo que eliminar los métodos. Dado que pueden existir operaciones 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 existen actualizaciones: es posible que en el futuro se añada la funcionalidad de eliminación de campos. Mientras tanto, observe 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 comprobació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 generados. 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 fabricante, 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 clases 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 proporcionar anotaciones como parte de sus herramientas. Como puede imaginarse al analizar el sistema @U nit, resulta bastante previsib 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 realmente de manera simultánea. La programación paralela puede mejorar enormemente la velocidad de ejecución de los programas, proporcionar un modelo 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 programació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 ejecutando 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 escribir 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 aparezcan 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 protecció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 yagresivos 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 independientemente y que los pocos casos en los que la concurrencia aparece durante las tareas cotidianas de programación (como por ejemplo, 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ódigo basado en hebras. Por ejemplo, los sistemas web constituyen una de las aplicaciones Java más comunes y la clase básica 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 apropiadamente. 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 segnridad 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 conscientes 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 comportamiento 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 comprender la programación orientada a objetos. Si nos aplicamos, podemos llegar a entender el mecanismo básico, pero generalmente 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 entender los conceptos y se puedan escribir programas multihebra de una complejidad razonable, pero tenga en cuenta que resulta fácil confiarse demasiado. En cuanto comience a desarrollar soluciones de una cierta complejidad, necesitará estudiar libros específicamente dedicados a esta materia. Las múltiples caras de la concurrencia Una de las razones principales por las que la programación concurrente puede resultar confusa es que hay más de un problema que resolver utilizando la concurrencia y más de una técnica para implementar la concurrencia, y no existe una clara correspondencia entre estos dos aspectos (e incluso, a menudo, las líneas de separación con completaruente difusas). Como resultado, estamos obligados a tratar de entender todos los problemas y los casos especiales para poder emplear la concurrencia de manera efectiva. Los problemas que se resuelven mediante la concurrencia pueden clasificarse, de manera un tanto burda, en dos categorías: "velocidad" y "gestionabilidad del diseño". Ejecución más rápida El tema de la velocidad parece simple a primera vista: si queremos que un programa se ejecute más rápidamente, lo descomponemos en fragmentos y ejecutamos cada uno de estos fragmentos en un procesador distinto. La concurrencia es una herramienta fundamental para la programación multiprocesador. Hoy día, como se está agotando la Ley de Moore (al menos para los chips convencionales), las mejoras de velocidad se producen en la forma de procesadores multinúcleo en lugar de mediante chips más rápidos. Para hacer que los programas se ejecuten más rápidamente es necesario aprender a aprovechar dichos procesadores adicionales, y ésa es una de las cosas que la concurrencia hace posible. Si disponemos de una máquina multiprocesador se pueden distribuir múltiples tareas entre los distintos procesadores, lo que permite incrementar enormemente el rendimiento. Esto es 10 que suele suceder con los potentes servidores web multiprocesador, que pueden clistribuir un gran nfunero de solicitudes de usuario entre las distintas CPU, dentro de un programa que asigne una hebra a cada solicitud. Sin embargo, la concurrencia puede también, a menudo, mejorar el rendimiento de programas que se estén ejecutando en un único procesador. Esto puede parecer poco intuitivo. Si pensamos en ello, un programa concurrente que se esté ejecutando en un único procesador debería tener un gasto de procesamiento administrativo mayor que si todas las partes del programa se ejecutaran secuencialmente, debido al coste añadido del cambio de contexto (cambio de una tarea a otra). A primera vista, parece que debería ser más rápido ejecutar todas las partes del programa como una única tarea, ahorrándose el coste asociado al cambio de contexto. 21 Concurrencia 729 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 operaciones 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 sucesos. 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 duració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 escribamos. Si lo hiciéramos, el código tendría un aspecto terrible, y además no existiría ninguna garantía de que un programador 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 operaciones 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 proceso 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últiples copias de seguridad redundantes del estado actual del libro. Hacía una copia en un directorio local, otra en un dispositivo 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 operativo 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 concurrente 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 proporciona 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 Mejora del diseño del código Un programa que use múltiples tareas en una máquina de un único procesador seguirá haciendo una única cosa cada vez, por lo que debería ser teóricamente posible escribir el mismo programa sin utilizar tareas. Sin embargo, la concurrencia proporciona un beneficio organizativo de gran importancia: el diseño del programa puede simplificarse enormemente. Algunos tipos de problemas, como la simulación, son difíciles de resolver si no se incorpora el soporte para la concurrencia. La mayona de las personas han tenido la oportunidad de ver algún tipo de simulación u otro, bien en forma de juego informático O bien como animaciones generadas por computadora en alguna película. Generalmente, las simulaciones involucran muchos elementos que interactúan entre sí, cada uno de los cuales tiene "su propio cerebro". Aunque es cierto que, en una máquina de un solo procesador, cada elemento de simulación está siendo controlado por ese único procesador, desde el punto de vista de la programación resulta mucho más fácil actuar como si cada elemento de simulación tuviera su propio procesador y fuera una tarea independiente. Una simulación de gran envergadura puede incluir tm gran número de tareas, lo que se corresponde con el hecho de que cada elemento de una simulación puede actuar de manera independiente; esto incluye no sólo los elfos y los brujos sino tam- bién las puertas o las piedras. Los sistemas multihebra tienen a menudo un límite relativamente pequeño en cuanto al número de hebras disponibles, estando dicho límite, en ocasiones, en el borde de las decenas o los centenares. Este número puede variar fuera del control del programa: puede depender de la plataforma, o en el caso de Java, de la versión de la máquina JVM. En Java, podemos asumir, por regla general, que no existirán suficientes hebras disponibles como para asignar una a cada elemento de una simulación de gran envergadura. Una técnica típica para resolver este problema consiste en utilizar lo que se denomina multihebra cooperativa. El mecanismo de hebras de Java es con desalojo, lo que significa que hay un mecanismo de planificación que proporciona franj as temporales para cada hebra, interrumpiendo periódicamente a una hebra y efectuando un cambio de contexto a otra hebra, de modo que a cada una se le asigne una cantidad de tiempo razonable como para poder realizar la tarea que tenga asignada. En un sistema cooperativo, cada tarea cede el control voluntariamente, lo que requiere que el programador inserte a propósito algún tipo de instrucción de cesión de control dentro de cada tarea. La ventaja de un sistema cooperativo es doble: el cambio de contexto es mucho menos costoso que, normalmente, en un sistema con desalojo, y además no existe ningún límite teórico al número de tareas independientes que pueden ejecutarse simultáneamente. Cuando estamos tratando con un gran número de elementos de simulación, ésta puede ser la solución ideaL Observe, sin embargo, que algunos sistemas cooperativos no están diseñados para distribuir las tareas entre los distintos procesadores, 10 que puede resultar muy restrictivo. En el otro extremo, la concurrencia representa un modelo muy útil (porque refleja muy bien lo que sucede) cuando estarnos trabajando con los modernos sistemas de mensajería, que involucran a muchas computadoras independientes distribuidas a 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 llegara 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 informació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 mensajeria, 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. Conceptos básicos sobre hebras La programación concurrente permite particionar un programa en una serie de tareas separadas y que se ejecutan de forma independiente. Usando un mecanismo multihebra, cada una de estas tareas independientes (también denominadas subtareas) se asigna a una hebra de ejecución. Una hebra es un único flujo de control secuencial dentro de un proceso. Un único proceso puede, por tanto, tener múltiples tareas que se ejecuten concurrentemente, pero a la hora de programar actuamos como si cada tarea dispusiera del procesador para ella sola. Un mecanismo subyacente se encarga de dividir el tiempo de procesador de manera transparente, sin que en general tengamos que prestar atención a este mecanismo. El modelo de hebras es una utilidad de programación que simplifica la tarea de realizar varias operaciones al mismo tiempo dentro de un mismo programa: el procesador irá saltando de una tarea a otra, asignando a cada una parte de su tiempo.4 Cada tarea piensa que tiene asignado el procesador de manera continua, pero lo cierto es que el tiempo del procesador se distribuye entre todas las tareas (excepto cuando el programa esté de hecho ejecutándose sobre múltiples procesadores l. Una de las mayores ventajas del mecanismo de hebras es que el programador puede abstraerse completamente de este nivel, de modo que el código no necesita saber si está ejecutándose sobre un único procesador o sobre varios. De esta manera, la utilización de hebras constituye una forma de crear programas que sean transparentemente escalables: si un programa se está ejecutando de forma demasiado lenta, podemos acelerarlo fácilmente añadiendo más procesadores a la computadora. Los mecanismos multitarea y multihebra tienden a ser las formas más razonables de utilizar los sistemas multiprocesador. Definición de las tareas Una hebra se encarga de dirigir una cierta tarea, por lo que necesitamos una forma de describir dicha tarea. Para ello, se emplea la interfaz Runnable. Para definir una tarea, basta con implementar Runnable y escribir un método run( l para hacer que la tarea realice su correspondiente trabajo. Por ejemplo, la siguiente tarea LiftOff se encarga de mostrar una cuenta atrás antes de un lanzamiento: 11: concurrency/LiftOff.java 11 Ilustración de la interfaz Runnable. public class LiftOff implements Runnable protected int countDown = la; 11 Predeterminado private static int taskCount = O; private final int id = taskCount++; public LiftOff () {} public LiftOff(int countDown) this.countDown = countDoWTI; public String status () { return 11#11 + id + 11 (11 + 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 (countDown > o ? coun tDown I1Liftoff! 10) + ") , 11 i publi c void run () while (countDown - - > O) { Sys tem. o u t . pr i nt (status () ) ; Thr ead. yie ld() ; 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 necesaria, 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 public c lass MainThread { public sta ti c vo i d mai n( String [] args ) LiftOff launch = new Lif tO ff {) i launch .run( ) j * OUtpu t , #0(9), #0(8), i #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 uso más básico de la clase Thread . /1 El public cIass BasicThreads { publi c s tati c void main (String [J args) { Thread t = new Thread( new Li ftOf f () ) ¡ t . ~tar t (); Syst em .out .println ("Wa iting f or LiftOff") / * Ou t p u t: ( 90% match) Wait i ng for Lift Off #0 (9), #0(8), #0(7), # 0(6) , *jjj,- #0 (5) , #0 (4 ) , i #0(3 ), #0(2) , #0(1), #O(Lif to ffl), 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 mensaje "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á siendo 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 ejecutan 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) , #3(8), # 4 (8 ) , #1 ( 6 ) , #2(6 ) , #4(5 ) , #0(4 ) , #2(3) , #3 ( 3), #0 (1), #1 (1), #l(Liftoff!), #2(9 ) . #3 ( 9), #4 (9 ) . #0(8), #1(8 ) . #2 ( 8), #0 (7 ), #1 (7), #2 ( 7), #3(7 ), #4(7 ) , # 0( 6), #3 ( 6) , #4 ( 6), #0(5), #1 (5), #2(5), #3 ( 5 ) , # 1( 4), #2(4 ) , # 3(4). #4(4 ) , #0(3 ), #1 (3 ) , #4 (3), #0(2), #1(2 ) , #2(2), #3 (2), # 4(2) , #2(1), #3 (1), #4(1), #0 (Lifto ff ! ), # 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áquina, 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 determinístico. De hecho, podemos ver enormes diferencias en la salida de este programa en una versión del JDK y en la siguiente. 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 depurador 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 posible que más de una hebra LIftOff tenga el mismo valor id . Posteriormente en el capítulo veremos cuál es la razón. 6 Esto no era así en algunas de las versiones anteriores de Java. 734 Piensa en Java la salida, que todas las tareas se ejecutan efectivamente hasta su conclusión, por lo que una hebra crea una hebra de ejecució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úmeros 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 permiten 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étodo para ser ejecutado. Un objeto ExecutorService (un objeto Exeeulor con un ciclo de vida de servicio, por ejemplo, apagar) 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.*¡ publ i c c lass CachedThre adPool { public stati c void main (String[] args ) { ExecutorService exec : Executors.newCachedThreadPool{); fo r( int i = Oi i < 5; i++} exec.execute(new LiftOff ()}¡ exec.shutdown() i 1* Outpu t, ( Sample) (9), #0 (8) , #1 (9) , #2 (9 ) , #3 (9) , # 4 (9 ) , #0 (7), #1 (8) , (8), #3 (8) , #4 (8) , #0 (6) , #1 (7) , #2 (7), #3 (7), #4 (7) , (5) , #1 (6) , # 2 (6), #3 (6) , #4 (6), #0 (4) , #1 (5), #2 (5) , (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) , #0 #2 #0 #3 *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 objeto FixedTbreadPool utiliza un conjunto limitado de hebras para ejecutar las tareas indicadas: 1/: concurrency/FixedTh re adPool .java import java.util.concurrent.*¡ public c lass Fixe dThreadPool { 21 Concurrencia 735 public static void main(String [] a rgs ) { JI El argumento del constructor es e l número de tareas: Execu torService exec = Executors.newFixedThreadPool (5); for (int i = O; i < Si i+ +} exec.execute(new LiftOff()); exec.shutdown() i } /* Output, #0 #2 #0 #3 #1 #4 #1 (9) , #0 (8) , (8) , #3 (8) , (5) , #1 (6), (5) , #4 (5), ( 3), #2(3 ) , (2 ), #0 ILif t ILifto ff! ) , (Samp1e) #1 (9) , #2 (9) , #3 (9) , #4 (9) , #0 (7), #1 (8), #4 (8) f #0 (6) , #1 (7) , #2 (7) , # 3 (7), #4 (7), #2 (6) , #3 (6) , #4 (6), #0 (4 ) , #1 (5), #2 (5) , #0 (3 ) , #1 (4), #2 (4), #3 (4 ) , #4 ( 4), #0 (2), #3 (3 ) , #4 (3), #0 (1 ) , #1 (2 ) , #2 (2 ) , #3 (2 ) , off ! ) , #1 ( 1 ) , #2 ( 1), #3 ( 1) , #4 (1) , #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 creació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 conjunto preasignado. Con esta solución, no podemos agotar los recursos disponibles porque el objeto FixcdThreadPool utiliza 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 durante 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 necesitaremos 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 ejemplo 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 ejecutará hasta completarse antes de qne se inicie la signiente tarea, utilizando todas ellas la misma hebra. En el siguiente ejemplo, 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 pendientes. / 1: concurrency/SingleThreadExecutor.java i mport java.util.concurrent.*; public class SingleThreadExecutor p u blic static void main (String {] args l { Execu torService exec = Executors.newSingleThreadExecutor() ; for(int i = O; i < 5; i ++} exec.execute(new LiftOff( » i exec . shutdown () ; 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 #0 #0 #1 #2 #2 #3 #4 (9) , #0 (8) , #0 (7) , #0 (6) , #0 ( 5) , #0 (4), (1 ) , #0 (Liftoff !) , #1 (9) , #1 (8), #1 (7), (4), #1 (3) , #1 ( 2) , #1 (1), #1 (Lif toff! ) , (7) , #2 (6) , #2 (5) , #2 (4) , #2 (3) , #2 (2), (Lifto ff! ) , #3 (9) , #3 (8) , #3 (7), #3 (6), (3) , #3 (2) , #3 (1), #3( Liftoff!) I #4 (9), (6) , #4 (5) , #4 (4) , #4 (3) , #4 (2 ), #4 (1), #0 #1 #2 #2 #3 #4 #4 (3) , #0 (2) , (6) , #1 (5), ( 9) , #2 (8) , (1) , ( 5 ), #3 (4), ( 8), #4 (7) , (Lift off ! ) , *///,Veamos otro ejemplo. Suponga que tenemos una serie de hebras que están controlando tareas que utilizan el sistema de archivos. Podemos ejecutar estas tareas con SingleThreadExecutor para garantizar que en cada momento sólo haya una tarea ejecutándose en cualquier hebra. De esta forma, no tenemos que preocuparnos de la sincronización en lo que respecta al recurso compartido (y además no colapsaremos el sistema de archivos). En ocasiones, una mejor solución consiste en sincronizarse con el recurso (de lo que hablaremos más adelante en el capítulo). Pero SingleThreadExecutor nos permite obviar los problemas de coordinación a la hora, por ejemplo, de construir el prototipo de un sistema. Serial izando las tareas, podemos eliminar la necesidad de serializar los objetos. Ejercicio 3: (1) Repita el Ejercicio 1 utilizando los diferentes tipos de ejecutores mostrados en esta sección. Ejercicio 4: (1) Repita el Ejercicio 2 utilizando los diferentes tipos de ejecutores mostrados en esta sección. Producción de valores de retorno de las tareas Un objeto Runnable es una tarea independiente que realiza un cierto trabajo, pero que no devuelve un valor. Si queremos que la tarea devuelva un valor cuando finalice, podemos implementar la interfaz Callable en lugar de la interfaz Run\1able. Callable, introducida en Java SES, es un genérico con un parámetro de tipo que represeota el valor de retorno del método call( ) (en lugar de run( y debe invocarse utilizando un método submit( ) de ExecutorService. He aquí un ejemplo simple: », JI: con cu rrency/CallableDe~o .java import java.util.concurrent.*; import java.util.*; class TaskW i thResu lt i mplements Callable private int id ¡ public TaskWithResult(int i d) this.id = id ; public String ca11 {) return "result of TaskWithResul t " + id; p ublic class CallableDemo { public static voi d main(String(] arg sl { ExecutorService exec = Executors.newCachedThreadPool(); ArrayList fs : results) try ( II get() se bloquea hast a comp l etarse: System.out.println(fs.get(» ¡ catch(InterruptedException e) { System.out.printl n(e) ; re turnj catch (ExecutionException e) { System.out.printl n(e) ; finally { 21 Concurrencia 737 exec.shutdown() i 1* Output: result of TaskWi thResul t result of TaskWithResult result of TaskWi thResul t result of TaskWithResult result of TaskWithResult result of TaskWithResult result of TaskWithResult result of TaskWithResult resu lt of TaskWithResu lt resu l t of TaskWithResu l t o 1 2 3 4 5 6 7 8 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 completado 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. Cómo dormir una tarea Una forma simple de modificar el comportamiento de las tareas consiste en invocar sleep( ) para detener (bloquear) la ejecución de dicha tarea durante un cierto tiempo. En la clase LiftOff, si sustituimos la llamada a yield( ) por una llamada a sleep( ), obtenemos lo signiente: JI: concurrencyjSleep i ngTask.java // Llamada a sleep() para detenerse durante un tiempo. import java.util. concurrent.*¡ public class SleepingTask extends LiftOff { public void run() { try ( while(count Down-- > O) { System.out.prin t(status( »¡ II A la antigua usanza: II Thread.s l eep(lOO); II Al estilo Java SEs /6 : TimeUni t.MILL ISECONDS. sleep(lOO) i catch(InterruptedException el { System.err.println(IlInterrupted rl ) ; public static void main (Stri ng[ } args) { Executorservice exec ~ Executors.newCachedThreadPool()¡ for(int i = O¡ i < Si i++) exec. execute(new SleepingTask()) i exec.shutdown () ¡ 1* Output : 738 Piensa en Java #0 #3 #1 #4 #2 #0 #1 # 1 (9), # 4 (8) , #2 (6 ) , # 0 (4) , (3) , # 3 (3) , (1 ) , #1 (1 ) , (Lifto f f!) , (9) , ( 8) , (6 ) , (5), #2 #0 #3 #1 #4 #2 #2 #3 (9) , #4 (9) , #0 (8) , #1 (8) , #2 (8) , #1 (7) , #2 (7) , #3(7) , #4 (7 ) , #0 ( 6) , # 4 ( 6), #0 (5 ) , #1 (5 ) , # 2 ( 5 ) , #3 (5 ) , #2 (4), #3 (4), #4 (4) , #0 (3 ) , #1 (3) , (3) , #0 (2) , #1 (2), #2 (2) , #3 (2), #4 ( 2), (1) , # 3 ( 1 ), #4 (1 ) , # 0 (Lif toff! ) , (Liftoff! ) , #3 (Liftoff!) , #4 (Liftoff! ) , (9) , (7) , ( 6) , (4), * // /,La llamada a sleep( ) puede generar una excepción InterruptedException, y como podemos ver, esta excepción se atrapa en run( ). Puesto que las excepciones no se propagan entre unas hebras y otras para volver a maine ), es necesario gestionar de manera local dentro de cada tarea todas las excepciones que puedan generarse. Java SES ha introducido la versión más explicita de sleep() como parte de la clase TimeUnit, como se muestra en el ejemplo anterior. Esto proporciona una mayor legibilidad, al permitimos especificar las unidades del retardo asociado con sleep(). TimeUnit también puede usarse para realizar conversiones, como veremos más adelante en el capítulo. Dependiendo de la plataforma, podríamos observar que las tareas se ejecutan en orden "perfectamente distribuido": cero a cuatro y luego vuelta de nuevo a cero. Esto tiene bastante sentido, porque después de cada instrucción de impresión cada tarea pasa a dormir (se bloquea), 10 que permite al planificador de hebras conmutar a otra hebra distinta, que se encarga de dirigir otra tarea. Sin embargo, el comportamiento secuencial descansa sobre el mecanismo subyacente de hebras, que es diferente entre un sistema y otro, así que no podemos confiar en que las cosas sean siempre así. Si necesitamos controlar el orden de ejecución de las tareas, 10 mejor que podemos hacer es emplear controles de sincronización (descritos más adelante) 0, en algunos casos, no utilizar hebras en absoluto, sino en su lugar escribir nuestras propias rutinas cooperativas que se entreguen unas a otras el control, en un orden especiflcado. EjercIcio 6: (2) Cree una tarea que duerma durante una cantidad aleatoria de tiempo comprendida entre 1 y 10 segundos, y que luego muestre el tiempo durante el que ha estado dormida y salga. Cree y ejecute un cierto número (indicado en la línea de comandos) de estas tareas. Prioridad La prioridad de una hebra le indica al planificador la importancia de esa hebra. Aunque el orden en que el procesador ejecuta 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 manipular 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 .*; public cla ss SimplePr ioritie s implements Runnable p r ivate i n t countDown = 5; private volatile double d í II Sin optimización priva te int prioritYi public SimplePrior it i e s( i nt priority ) { this . p riority = prioritYi p ubli c B t r ing t oString () { return Thre ad. c urr entThr ead () + n: I! + countDown; 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) / if(i % 1000 00 O) Thread.yield() ; { (double)i; System.out.println(this) i if{--countDown ==; O) return; public static void main (String [] args) { ExecutorService exec = Executors.newCachedThreadPool(); for(int i = O; i < 5; i++) exec.execute( new SimplePriorities(Thread.MIN_PRIORITY)); exec.execute( new SimplePriorities (Thread.MAX_PRIORI'rY)) i exec.shutdown() i 1* Output: (70% match) Thread [pool-l-thread-6 f 10 f mainJ: Thread[pool-l-thread-6,lO,main]: Thread [pool-l- thread - 6 I 10 I mainJ! Thread [pool-l-thread-6, 10 I mainJ: Thread[pool-l-thread-6,lO,mainJ: 5 4 3 2 1 Thread [pool-l-thread-3, l,main] : 5 Thread [pool-l-thread-2, 1,mainJ : Thread [pool-l-thread-l, 1, mainJ: Thread [pool-l- thread - 5, 1, mainJ: Thread [pool-l-thread-4, l,main] : 5 5 5 5 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 objeto 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 mediante 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 comportamiento 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 interrumpida (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 prioridad 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 utilizados 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. Cesión del control Si sabemos que ya hemos llevado a cabo lo que queríamos hacer durante la pasada de un bucle en nuestro método r un( ), podemos proporcionar una indicación al mecanismo de planificación de hebras, en el sentido de que ya hemos realizado una tarea suficiente y que puede permitirse a otra tarea disponer del procesador. Esta indicación (y es una indicación: no hay ninguna garantía de que una implementación concreta respete esa indicación) toma la forma de una llamada al método yield(). Cuando invocamos yield( ), estamos sugiriendo que se pueden ejecutar otras hebras de la misma prioridad. LiftOIT.java utiliza yield( ) para distribuir de manera adecuada el procesamiento entre las diversas tareas LiftOIT. Pruebe a desactivar mediante comentarios la llamada a T bread.yield( ) en LiftOff.run( ) para ver la diferencia. Sin embargo, en general, no podemos confiar en yield( ) para ninguna tarea seria de controlo de optimización de la aplicación. De hecbo, yield( ) se emplea muy a menudo de manera incorrecta. 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 termina, 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étodo mai n(). /1 : concurrencyj SimpleDaemons. j ava // La s h ebras demonio no i mpi den que el progra ma t e r mine. i mpor t java . u ti l. concurrent .*¡ impe rt stat i c ne t . mindview.uti l.Print.* ¡ public c l ass Simp l eDaemon s i mpleme nts Runnable p ubl ic vo i d r un() { try { while (true) ( TimeUnit. MILLISECONDS.sl ee p( l OO) ; print (Thr ead. curren t Thread () + I! n + thi s } ; catch( Int erruptedExcep tion e ) print ( "sl eep ( ) interrupted" ); pub l ic s t ati c void main (Stri ng[] a rg s) thro ws Exception f or( i nt i = O; i < 1 0 ; i ++} { Th r ead daemon = new Thr ead{new SimpleDaemons(» ¡ d aemon .set Da emo n(tru e ) ; /1 Hay q u e invocar la antes de sta rt () d aemon . start () ; print (Jl Al l daemons s t a rted ll ) i TimeUn i t. MI LLISECONDS. s lee p (17 5 ) ; / * 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 Thread[Thread-S , 5 , ma i n ] Simp l eDaemons@16caf43 Thread[Thread -6 ,S,main ] SimpleDaemons@6 68 4 8c Thread [Thread-7,5,mainl Simp l eDaemons@S8 13f2 Thread [Thread-8, 5,main] SimpleDaemons@ ld58aae Thread[Thread-9 ,S,mainl SimpleDaemons@83cc6 7 *///, - 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ándose son hebras demonio. Para poder ver el resultado de iniciar todas las hebras demonio, la hebra main( ) se pone brevemente 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 public class DaemonThreadFactory implements ThreadFactory { public Thread newThread{Runnable r) { Thr ead t = new Thread(r) ; t. s etDaemon (true) ; retur n t; 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.*¡ public class DaemonFromFactory implements Runnable { public void run() { try { while (true) { TimeUnit .MILLISECONDS.sleep (100) : print (Thread. c urren tThre ad () T 11 11 + this ); catch ( Interrupt edExcepti on e ) { pri n t { II I n terr upted " ) : public static void main{String [ ] args) thr ows Exception ExecutorSe rvice exec = Executors. newCachedThreadPool{ new DaemonThreadFactory( ) ); f o r(int i = O: i < 1 0; i++) exec.execute(new DaemonFromFactory()); print (IIAl1 daemons started ll ) ; TimeUnit.MILLISECONDS.sleep (500); II Ejecutar durante un tiempo 1* (Ej ecutar para ver la salida) *11 1: - 742 Piensa en Java 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.*¡ public cla ss DaemonThreadPoolExecutor extends ThreadPoolExecutor { pub l ic Da emonThreadPoolExecuto r () { s uper ( O, Integer.MAX_VALUE, 60L, TimeUni t . SECONDS, new SynchronousQueue () , new DaemonThreadFact ory ()) ; } /// , Para obtener los valores para llamada al constructor de la clase base, simplemente hemos echado un vistazo al código fuente Executors.java. Podemos averiguar si una hebra es de tipo demonio invocando isDaemon(). Si una hebra es un demonio, entonces todas las hebras que cree serán también demonios automáticamente, como demuestra el siguiente ej emplo: JI: concurrencylDaemons.java 1/ La s hebras demonio crean otras hebras demonio. i mport j a va.ut il. concurrent.* ; i mport stat ic n e t .mindv iew. u ti l . Pr i n t.*; clas9 Da emon i mplements Runnable { prí vate Thr ead{ ] t = new Thread{ l O] ¡ p u blic void run() { for (i n t i = O; i < t . leng t h; i++ ) tr i ] = new Thread(new DaemonSpawn ()) ; t [i l . s ta rt () ¡ printnb ( nDaemonSpawn 11 + i + 1I start ed, f or (int i = O; i < t. l eng th¡ i ++ ) printnb ( " t [ 11 + i + "] . i sDaemon ( ) t [i ) .isDaemon() + ", " ) ; wh ile (true) Thread . y i eld () 11 11 ) ; + ¡ c lass Da emonSpawn i mplements Runnab l e { publi c void run () { whil e{ true ) Thr ead. yield () ¡ 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 Permi tir que l as hebras demonio finalicen II sus procesos de arranque: TimeUnit.SECONDS.sl eep(l ) ; II}; 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 definen explícitamente como demonios), para demostrar de todos modos que esas hebras serán de tipo demonio. A continuació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.*¡ class ADaemon implements Runnable { public vpid run() { try { print (It Starting ADaemon It) ; TimeUnit.SECONDS.sleep(l) ¡ catch(InterruptedException e) { print (IIExiting via InterruptedException") i finally { print(ItThis should always run?lI); public class DaemonsDontRunFinally { public static void main(String[] args) throws Exception { Thread t = new Thread(new ADaemon()) i t. setDaemon (true) ; t. start () ; 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 cuanto 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 termine 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 . publ ic cIass SimpleThread extends Thread prívate int countDown = 5i private static i nt threadCount = Oí publ i c SimpleThread( ) { // Almacenar el nombr e de l a hebra: super{Integer.toString(++threadCount)) ; start () ; public String toString() return "#11 + getName() + public void run() while (true) { 11 (JI + countDown + 11) 1 u i { System.out.print{this) if{--countDown == O) i return; p ubl ic static vo id rna i n (S tri ng [] f or(int i = O; i < 5; i ++ } args ) { n ew Simpl eThread () ; 1* Output: #1(5), # 2(2), #4(4), #5 (1), #1(4), # 2(1) , #4(3), #1(3), #3(5), #4(2), #1(2), #3(4), #4(1), #1(1 ), #3(3), #5(5), #2 ( 5), #3(2), # 5(4), #2(4 ) , # 3 (1), #5(3), #2(3), #4(5), #5(2), */// :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 public void run () whil e (true) { "; 21 Concurrencia 745 System.out.print(th is) ; if (- -countDown ~= O) ret urn ; public static void main(St ring[] args) for(int i = o; i < 5i i++ } n ew SelfManaged(); 1* Output, Thread- O(5) , Thread - l (S) , Thread-2 (S) , Thread- 3(S) , Thread -4 (5) , Thread-O (4), Thread- Q(3) , Thread-l (4), Thread-l (3) , Thread-2 (4) Th read-2 (3), Thread-3 (4) , Th read - 3(3) , Thread -4 (4) , Thread- 4 (3), I { Thread - O(2) , Thread- l(2) , Threa d-2 (2) , Thread - 3 (2), Thread- 4 (2 ) , Thread-O (1 ) , Thread-l (l ) , Thread-2 (1) , Thread-3 (1 ) , 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, porque 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 .*¡ /1 Ut i lización de una clase i nterna nominada: class InnerThreadl { prívate int countDown = 5¡ private Inner inner ¡ private class Inner extends Thread { I nner (Stri ng name) { super (name) ¡ start () ; public void run() try { while (true) { print(this)¡ i f(--countDown s l eep(lO ) ; O) re turn¡ catch( InterruptedException e ) { print (11 i n terrupted" ) ; public String toString() return getName () + 11: 11 + countDown ¡ public I nnerThreadl(String name) i nner = new Inner(name)¡ { 746 Piensa en Java II utilización de una clase interna anónima: class InnerThread2 { private int countDown = 5; private Thread t; public InnerThread2(String name) { t = new Thread(name) { public void run() { try { while (true) { print(this) ; if(--countDown O) return; sleep (10); catch(InterruptedException el print (I! sleep () interrupted 11) ; public String toString() return getName () + 11. I! + countDown; } }; t. start () ; II Utilización de una implementación Runnable nominada: class InnerRunnablel { private int countDown = 5; private Inner inner; private class Inner implements Runnable { Thread t; Inner (String name) { t = new Thread(this, name); t. start () ; public void run () try { while(true) { print (this) ; if(--countDown == O) return; TimeUnit.MILLISECONDS.sleep(lO) ; catch(InterruptedException e) print (11 sleep () interrupted 11) ; public String toString () { return t.getName() + n: + countDown; public InnerRunnablel(String name) inner = new Inner(name); { II Utilización de una implementación Runnable anónima: class InnerRunnable2 { private int countDown = 5; 21 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 public String toString () { return Thread. currentThread () .getName() + 11. " + countDowll; } }, name); t. start () i // Un método separado para ejecutar un cierto código como una tarea: class ThreadMethod { private int countDown = 5; private Thread t; private String name; name; } public ThreadMethod(String name) { this.name public void runTask() { i f It ~~ null) { t = new Thread(name) { public void run() { try { while (true) { print(this) ; if(--countDown O) return¡ sleep(lO) ; catch(InterruptedException e) print (I! sleep () interrupted ") ¡ public String toString() return getName () + ". " + countDown¡ } }; t. start () ¡ public class ThreadVariations { public static void main(String[] args) new InnerThreadl ( n InnerThreadl ll ) ¡ new InnerThread2 ( "InnerThread2 11) ; new InnerRunnablel (11 InnerRunnablel ll ) ; new InnerRunnable2 (11 InnerRunnable2 11) ; new ThreadMethod("ThreadMethod") .runTask(); / * (Ej ecutar para ver la salida) * / / /:- Concurrencia 747 748 Piensa en Java 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 referencia 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 emplean 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 estemos 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 describir 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 nosotros). 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 deberí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 gestionarlas 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á ocurriendo. Parte de esta naturaleza de bajo nivel ha terminado deslizándose en la implementación Java, por 10 que para permanecer 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. Absorción de una hebra Una hebra puede invocar join( ) sobre otra hebra para esperar que esa segunda hebra se complete antes de que la primera continúe con su trabajo. Si una hebra invoca t.join( ) sobre otra hebra t, entonces la hebra invocante se suspende hasta que la hebra objetivo t finalice (cuando t.isAlive() es false). 21 Concurrencia 749 También podemos invocar join( ) con un argumento de fin de temporización (en milisegundos o en milisegundos y nanosegundos), 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.*¡ cIass Sleeper extends Thread { prívate i n t duration¡ public Sleeper (Stri ng name , int sleepTime) { super (name) ; duration = sleepTime; start () ; public void run() { try ( sleep(duration) i cat ch(InterruptedExcepti on e) { print(getName() + u was interrupted. 11 + 11 islnterrupted (): 11 + islnterr upted () ) ; returnj print (getName () + 11 has awakened") i class Joi ner extends Thread { private S l eeper s l eeper¡ public Joiner(String name, Sleeper s l eeper) super (name) i th i s.sleeper = sleeper¡ start () ; public void run() { try ( sleeper .join() i catch(InterruptedExcept ion el print (Ulnterrupted!1 ); print (getName () + u join completed ll ) i public class Joining pub lic s t atic void main(String [J args) ( Sleeper new Sleeper (US leepyl!, 1500 1 , s l eepy new Sleeper (IIGrumpyl!, 1500 1 ; grumpy Joiner dopey "" new Joiner (lIDopey l l , sleepy), doc = new Joiner(!1Doc U , grumpy) i grumpy.inte r r upt() ¡ 1* Output , { 750 Piensa en Java Grumpy was interrupted. islnterrupted(): false Doc join completed Sleepy has awakened Dopey join completed *111,Un objeto Sleeper es una hebra que pasa a dormir durante un tiempo especificado en su constructor. En run( l, la llamada a sleep( l puede terminar cuando finaliza el tiempo especificado, pero también puede ser interrumpida. Dentro de la cláusula cateh, se informa de la interrupción, junto con el valor de islnterrupted( l. Cuando otra hebra invoca interrupt( l sobre esta hebra, se activa un indicador para mostrar que la hebra ha sido interrumpida. Sin embargo, este indicador se borra en el momento de tratar la excepción, por lo que el resultado será siempre false dentro de la cláusula eateh. El indicador se utiliza para otras situaciones en las que una hebra puede examinar su estado de interrupción, de forma independiente de la excepción. Un objeto Joiner es una tarea para que un objeto Sleeper se despierte invocando join( l sobre ese objeto Sleeper. En main( l, cada objeto Sleeper tiene un objeto Joiner y podemos ver a la salida que si el objeto Sleeper es interrumpido o finaliza normalmente, el objeto Joiner completa su tarea en conjunción con el objeto Sleeper. Observe que las bibliotecas java.util.eoneurrent de Java SES contienen herramientas tales como CyelieBarrier (que se ilustra más adelante en este capítulol que pueden ser más apropiadas que join( l, que formaba parte de la biblioteca de hebras original. Creación de interfaces de usuario de respuesta rápida Como hemos indicado anteriormente, uno de los motivos para la utilización de hebras consiste en crear una interfaz de usuario de rápida respuesta. Aunque no vamos a sumergimos en las interfaces gráficas hasta el Capítulo 22, Inteifaces gráficas de usuario, el siguiente ejemplo es un simple prototipo de una interfaz de usuario basada en consola. El ejemplo tiene dos versiones: una que se queda bloqueada en un cálculo y nunca puede leer la entrada de la consola y una segunda que inserta el cálculo dentro de una tarea y puede, por tanto, estar realizando a la vez el cálculo y escuchando la entrada de la consola. 11: concurrency/ResponsiveUI.java II Capacidad de respuesta de la interfaz de usuario II {RunByHand} 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í public class ResponsiveUI extends Thread private static volatile double d = 1¡ public Responsi veUI () { setDaemon(true) ; start () ; public void run() { while (true) { d = d + (Math.PI + Math.E) I d; public static void main(String[] args) throws Exception { II! new UnresponsiveUI()¡ II Hay que matar este proceso new ResponsiveUI()¡ System.in.read{) ¡ 21 System.out.println(d); Concurrencia 751 jI Mostrar el progreso 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 utilizando el while condicional). Si desactivamos mediante un comentario la línea que crea una interfaz UnresponsiveUI, tendremos 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 palabras 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 simplemente 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 laureado 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 especiales 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 necesario 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.*; public cIass ExceptionThread implements RunnabIe { pubIic void run() { throw new RuntimeException()¡ pubIic static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool()¡ exec.execute(new ExceptionThread()) ¡ 8 Effective JavaTM Programming Language Guide, de Joshua Bloch (Addison-Wesley,2001), p. 21t. 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 consultoría en bastantes proyectos en los que esta filosofía era aplicable. 752 Piensa en Java La salida es (después de quitar algunos cualificadores para que quepa en la página): j ava .l ang . Runt imeException at ExceptionThread.run(ExceptionThread.java:7) at ThreadPoolExecutor$Worker.runTask(Unknown Souree) at ThreadPoolExecutor$ Worker.run(Unknown Souree ) at java.lang.Thread.run(Unknown Sauree) No se pierde nada al encerrar el cuerpo del método principal dentro un bloque try-catch: JI: concurrency/NaiveExceptionHandling.java // {ThrowSException} import java . ut il.concurrent.*; public class NaiveExcept ionHandli ng publi c stat i c void ma i n(Stri ng [J args) t ry { Execu t orService exec = Execu tors .newCach edThreadPool (}; exec.execut e(new ExceptionThread( ») ; catch(RuntimeExce ption ue) { /1 ¡Esta i n s t rucc ión NO se ejecutará ! System.out .println(lIException has be e n handled! ") i 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.UncaugbtExceptionHaodler 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 .*¡ class ExceptionThread2 implements Runnabl e pub lic void run () { Thread t = Thread.currentThread() ; Sys t em.out .println(llrun() by 11 + t) ¡ System . out .println( lIeh = Il + t .getUncaughtExcept ionHandl er()) t hrow new RuntimeException()¡ i cIass MyUn caughtExceptionHandler implements Thread.UncaughtExcep t i onHandler { public void uncaugh tExcept ion (Thread t, Throwab l e e l { System . out .pr i ntl n ( "caught •• + e l; c l ass HandlerThreadFactory implements ThreadFactory { publi c Thread newThread(Runnable r) { System. out.println(this + 11 creating new Thread") Thread t = new Thread(r)¡ System.out.println(lIcreated 11 + t); i 21 Concurrencia 753 t .setUncaughtExceptionHandl er { new MyUncaugh tExceptionHandler {» i System.ou t.println ( fleh = n + t . getUncaughtExceptionHandle r ( }) ; re turn t i public cIass CaptureUncaughtException { publi c s tatic void main(St ring[J a rg s) ExecutorService exec = Executors .newCachedThreadPool( new HandlerThreadFact ory(} ) j exec .execute(new ExceptionTh read2 (» i / * Output, (90% match ) Ha nd lerThreadFac tory@de6ced creat i ng new Thread creat ed Thread [Thread - O,5,main1 eh = MyUncaugh tExceptionHand l ey@l f b8ee3 r un() by Thr ead [Thread - O,5 ,main1 eh = MyUnc aughtExceptionHandler@l f b8ee3 caught java . lang.RuntimeException *///, Hemos ailadido facilidades de traza adicionales para verificar que las hebras creadas por la factorfa reciben el nuevo objeto UncaughtExceptionHandler. Podemos ver que las excepciones no capturadas están siendo ahora capturadas uncaughtException. El ejemplo anterior nos pelTIlite configurar la rutina de tratamiento caso por caso. Si sabemos que vamos a utilizar la misma rutina de tratamiento de excepciones en todas partes, una técnica todavía más simple consiste en defInir la rutina predeterminada de tratamiento de excepciones no capturadas, que configura un campo estático dentro de la clase Thread : 1/ : concurrencyl Set ti ngDefaul tHand l er. j a va import java .uti l . concurr e nt .* ¡ public c l ass SettingDe f aultHand ler { pub lic s ta ti c vo i d main (St r i ng[ ] args ) Thread.setDefaul t Uncaugh t ExceptionHandler( new MyUn caughtExcept ionHandler( )) ; Execut orSerV1ce exec = Executors .newCachedThreadPool () ; exec.execute(new Excep tionThread())¡ /* 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 especializa su método uncaughtException(); en caso contrario, invoca la rutina predeterminada defaultUncaughtExceptionHandler . 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 espacio de problema y que hace una sola cosa cada vez. Puesto que s6lo hay una entidad, no tenemos que pensar en el problema 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 Acceso inapropiado a los recursos Consideremos el siguiente ejemplo en el que una tarea genera números pares y otras tareas consumen dichos números. Aquí, el único trabajo de las tareas consumidoras consiste en comprobar la validez de los números pares. En primer lugar, definimos EvenChecker, la tarea consumidora, ya que la vamos a reutilizar en todos los ejemplos subsiguientes. Para desacoplar EvenChecker de los varios tipos de generadores con los que vamos a estar experimentando, crearemos W1a clase abstracta denominada IntGenerator, que contiene el mínimo número de métodos necesarios que EvenCbccker debe utilizar: dispone de un método next( ) y el generador puede ser cancelado. Esta clase no implementa la interfaz Generator, porque debe generar un valor int, y los genéricos no soportan parámetros primitivos. 11: concurre ncy/ lntGenerator,java publi c abstract cIass IntGenerator f alse¡ pr ivate volati l e boolean canceled public abs t ract i nt next () ¡ II Pe rmit i r que cancel arlo: public void can cel () { cancel ed = true¡ } public boolean isCanceled() { return cance led¡ 11 /,IntGenerator tiene un método cancel( ) para cambiar el estado de un indicador booleano canceled, así como un método isCaneeled( ) para ver si el objeto ha sido cancelado. Puesto que el indicador canceled es de tipo boolean, es atómico, lo que significa que las operaciones simples como la asignación y la devolución de un valor, tienen lugar sin que se puedan producir interrupciones, así que es posible ver ese campo en un estado intermedio durante la realización de esas operaciones simples. El indicador caneeled también es de tipo volatile, con el fin de asegurar la visibilidad. Hablaremos más en detalle de la atomicidad y la visibilidad posteriormente en el capítulo. Cualquier objeto IntGenerator puede ser probado con la siguiente clase EvenCheeker: 11: concurrency/EvenChecker.java import java.util.concurrent.*¡ pUblic c l ass EvenChecker implement s Runnable { pri vate IntGenerator generator; private final int id: public EvenChecker (IntGenerator g, in t identl generator = 9: id = ident; 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 II Probar cualquier tipo de IntGenerator: pUblic stati c void test (I nt Genera t or gp, int c ount ) { System . out .println ( " pr ess Control - C t o exit n ) ; ExecutorServ i c e exec : Executors.newCachedThreadPool(}; for(int i = O: i < count¡ i++) exec.execute(new EvenChecker(gp, i )); exec .shutdown () ¡ II Valo r prede t e r minado de recuento : publ ic static void tes t (IntGenerator gp) t est (gp, 10); { 21 Concurrencia 755 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 necesario 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 volverá. 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 posteriores, 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: 11 : con curr ency/EvenGen e r a tor.jav a II Cuando la s hebras col isi o nan . p ub li c cl a ss Ev enGe nerat o r exten ds Int Gene r ator p r i vat e int c u rre n t Even Value = O; pub lic int next () { ++currentEv enVa l ue¡ II ¡Punto de p eligro ! ++currentEvenValue¡ r eturn curr e ntEvenValue ; p u b li c static v o i d main (St ring [) arg s ) { EvenChecker.test(new EvenGenera t or())¡ / * Output, Press Con tro l 894 7 6 9 93 not 89476993 not (S amp l e ) - C to exi t even ! 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 MutexEvenGenerator añade un mutex denominado lock que utiliza los métodos lock( ) y unlock() para crear una sección crítica dentro de next( ). Cuando utilizamos objetos Lock, es importante intemalizar la estructura sintáctica que aquí se muestra: justo después de la llamada a lock( ), hay que insertar una instrucción try-finaUy con unlock( ) en la cláusula finally, ésta es la única forma de garantizar que el bloqueo se libere siempre. Observe que la instrucción return debe estar dentro de la cláusula try para garantizar que el método unlock( ) no se ejecute demasiado pronto, exponiendo los datos a la posible manipulación por parte de otra tarea. Aunque la cláusula try-finaUy requiere más código que utilizar la palabra clave synchronized, también representa una de las ventajas de los objetos Lock explícitos. Si algo falla empleando la palabra clave synchronized, se genera una excepción, pero no tenemos la posibilidad de realizar ninguna tarea de limpieza para poder mantener el sistema en un estado correcto. Con los objetos Lock explícitos, podemos mantener el estado correcto del sistema empleando la cláusula finally. En general, cuando utilizamos synchronized, necesitaremos escribir menos código y la oportunidad de que se produzcan errores se reduce enormemente, por lo que sólo utilizaremos normalmente los objetos Lock explícitos cuando estemos resolviendo problemas especiales. Por ejemplo, con la palabra clave synchronized, no podemos realizar intentos fallidos de adquirir un bloqueo, ni tampoco tratar de adquirir un bloqueo durante un cierto espacio de tiempo y luego liberarlo; para hacer estas cosas, es necesario emplear la biblioteca concurrent: 11 : concurrency /Att emptLocking.j ava II Los bloqueos de la biblioteca concurrent nos permiten II desistir a l intentar a dquir i r un bloqueo. import java.util.concurrent.*¡ import java.util.concurrent.locks.*¡ 21 Concurrencia 759 pub lic c l ase At temptLock i ng { p rívate ReentrantLoc k l ock = new Reen t rantLock()¡ pub l ic void un timed () { b ool ean c aptured = lock.tryLoc k{) ; try ( System.ou t .print l n ( tltryLoc k () : + cap tured} ; fina lly ( if (captured ) l ock. unlock() i public void timed () { b ool ean c ap tured = fal s€¡ try { captured = l oc k . t r yLock (2, TimeUni t.SECONDSJ; catch(InterruptedExcepti on e l { throw new Runt i meException (e) ; ) t ry ( Sy stem. o u t . println ( fl tryLock (2 , Ti meUni t . s ECONDS ) : " + c aptu redJ¡ fina lly ( if (captured) l o ck. unlock () i public sta tie v oid maln (String [) a r gs) { fin a l Att emptLocking al = new Att emptLocking()¡ al. un t irned () ; /1 True -- el bloqueo est á d isponible a l .timed() ¡ // True -- e l bloqueo e s tá di sponible JI Ahora c rear una t area separada para e s t able cer e l bloque o: new Thread () { ( setDa e mon (true ) ; } public v oid run () { a l.lock . l ock () ; Sy s tem. ou t. print ln ( n acquired " ) ; ) ) ,st a r t () ; Thre ad .yield () ; II Da r una oportunidad a l a segunda tarea al.unt imed (}¡ II Fa1se b l oque o a dquirido por l a tarea al.timed(); II False -- bloqueo adquirido p or la tare a 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 bloqueo, 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 especializadas, 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 operaciones atómicas no necesitan sincronizarse". Una operación atómica es aquella que no puede ser interrumpida por el planificador 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 atomicidad 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 tentació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 memoria 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 resultados incorrectos (esto se denomina en ocasiones desgajamiento de palabra. porque existe la posibilidad de ver el valor despué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 fuertes, 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 expertos 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 procesadores 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 procesador local, por ejemplo), de modo que diferentes tareas tendrán una visión distinta del estado de la aplicación. El mecanismo 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 cambio. Esto es cierto incluso si se utilizan cachés locales: los campos volátiles se escriben inmediatamente en la memoria principal 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 basado 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 responsabilidades 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 normalmente atómicas. Sin embargo, en e++ incluso las siguientes instrucciones podrían ser atómicas: i++¡ i += 2 ¡ JI Podría ser atómica en c++ /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, 2, 5, 6, 7, 10 , aload O dup getfield iconst iadd #2; IICampo i,r #2; II Campo i,r 1 putfield re t urn voi d f2 () ; Cede: °, 1, 2, 5, 6, 7, 10, a load O dup getfield #2; II Campo i,r iconst 3 i add putfield #2; IICampo i,r 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 instante de obtener el valor y el instante de almacenar el valor corregido, otra tarea podría modificar el campo, así que las operaciones no son atómicas. Si aplicamos ciegamente la idea de le atomicidad, podemos ver que getValue( ) eu el siguieute programa se ajusta a la descripció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 {) ¡ public stat ic void main {String[] a rgs) { ExecutorServi ce exec = Executor s.newCachedThreadPool() ¡ AtomicityTe st at = new AtomicityTest(); exec.execute(at ) ; while (true ) { i nt va l = at.getValue() if(val % 2 1= O) i { Syst em . out .print ln (val) ¡ System.exi t (O); /* Output, 1 9158 376 7 (Samp1e) * /// , 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 algunas 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 almacenados 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 valores sobreescritos será mínima. Los métodos add() y contains() están sincronizados para impedir las colisiones entre hebras: 11: concurrency/SerialNumberChecker.java II II Determinadas operaciones que pueden parecer seguras no lo son cuando hay hebras presentes. II {Args, 4} import java.util.concurrent.*; II Reutiliza el almacenamiento para no agotar la memoria: class CircularSet { private int[] array; private int len; private int index = Di public CircularSet(int size) array = new int[size]; len = size; II Inicializar con un valor no producido por II el generador SerialNumberGenerator: for(int i = O; i < sizei i++) array [i] = ~l; public synchronized void add(int i) { array[index] = i· II Implementar circularmente el index y reescribir los elementos II antiguos: index = ++index % len; public synchronized boolean contains(int val) for(int i = O; i < len; i++) if(array[i] == val) return true; return false¡ { public class SerialNumberChecker { private static final int SIZE = 10; private static CircularSet serial s = new CircularSet(lOOO); private static ExecutorService exec = Executors.newCachedThreadPool() ¡ sta tic class SerialChecker implements Runnable { public void run() while (true) { 764 Piensa en Java int se rial Ser ialNumberGenerato r .next8erialNumber() ; if (serials . contains (s erial» { System . out.println(uDuplicate: n + serial) System.exit(O) ; serials .add (serial l i i 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 producido 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 synchronized 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 peligroso. 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 optimizació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 priva te At omiclnteger i = new Atomiclnteger(Q) pub li c int getValue() { re turn i.get() ; } priva t e voi d evenlncremen t () { i .addAndGe t (2) i public void run() i } ( whi l e ( true) evenlncrement() i public static vo id main(S tr i ng[] args) new Ti mer() . schedule (new TimerTask () publ ic void run() { System. err .prin t ln ( uAbo r ting" ) ; System.exi t (Q) i }, 5000); /1 Terminar después de 5 segundos Exe cutorService exec = Executors .newCachedThreadPool(); Atomi c lntegerTe st ait = new AtomiclntegerTest(); exec. execute(ait) i while (true) ( i nt val = ait.getValue( ) ; if (val % 2 != O) ( System.out.pr i n t ln(val ) i System.exit(O) i 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); public static voi d main (Stri ng [J args) { EvenCh ecker.test( new AtomicEvenGenerator()); } 111 > De nuevo, todas las demás formas de sincronización se han eliminado empleando Atomíclnleger. Es necesario recalcar que las clases Atomic fueron diseñadas para construír las clases de java.util.concurrent, y que sólo debemos utilizarlas en nuestro propio código en circunstancias especiales, e incluso en ese caso únicamente cuando podamos garantizar que no van a surgir otros posibles problemas. Generalmente, es más seguro utilizar los bloqueos (bien la palabra clave synchronlzed o bien objetos Lock explícitos). Ejercicio 14: (4) Demuestre que java.util.Tltner se puede escalar para ntilizar números de gran magnitud creando lffi programa que genere muchos objetos Timer que realicen alguna tarea simple cuando se produzca el fin de temporización. 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á empleando 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 tareas 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.*¡ class Pair { II No protegida frente a hebras private int x , y¡ publ i c Pair( i nt x, int y ) { this.x Xi this.y = y; public Pair () public publ i c pUblic public { th is (O, O) ; } int getX () { ret urn X' int getY() { return y¡ void incrementX() { x++¡ void incrementY{) { y++¡ p u blic String t o String () return 'IX: u + X + lO, { y: " + y; pubIic cIass PairValuesNotEqualException extends RuntimeException { public PairValuesNotEqualException () super ("Pair values not egual: 11 + Pair. thi s ) ; II Inva riante arbit rari o publ ic void checkState{) ambas vari ables deben ser iguales: if(x ! = y) throw new PairVal uesNotEqualExc ept i on ( ) ; II Proteger un objeto pair dentro de una clase protegida f rente a hebras: abstract class PairManage r { Atomiclnteger c heckCounter = new AtomicInt eger (O); protected Pair p = new pair()¡ private List storage = Col lections.synchroniz edList(new ArrayLi st ()); publ ic synchronized Pair getPair () { 21 Concurrencia 767 /1 Hacer una copia para que e l ori g inal esté seguro: return new Pair (p . getX (), p.gety(»; JI Asumimos que ésta es una operac~on de la rga durac ión protected void store(Pair p) { storage . add (p) ; t ry { TimeUni t .MILLI SECONDS.sleep(SO) ; } catch (InterruptedException igno re) {} public abstract void increment()¡ JI Sincronizar e l método completo: c I ass PairManage r l extends PairMan ager { public s ynchronized void incr ement() { p. incremen tX () ; p. i nc r ement y () ; store(getPair (» ¡ JI Uti li zar una sección crítica: cIass PairManager2 extends PairManager publi c vo i d increment() { pair temp¡ synchronized(this) p. incrementX () ; p.incrementy( } ; temp = getPair()¡ s t ore (temp ) ; cIass PairMan ipu lator ímplements Runnab le { prí vate PairManager pm; publi c PairManipulator(PairManager pm) this .pm = pm, publi c void run () whi le (true) pm. increment () ; public String toString() return "Pair: I! + pm.getPair() + 1I c hec kCoun t er = I! + pm. c heckCounter . get () ; } c l ass pairChecker i mplemen t s Runnable { private PairManager pm; public PairChecker(PairManager pm) { this.pm = pm; public vo i d run () while (true ) { pm.checkCounter.incrementAndGet() ; pm.getPair() .checkState() j 768 Piensa en Java publ ic clasa CriticalSection { II Probar las dos técnicas: s t atic void testApproaches(PairManager pmanl, PairManager pman2) { ExecutorService exec ~ Executors .newCachedThreadPool (); Pai r Manipulator pm1 = new PairManipulator(pmanl), pm2 ~ new PairManipulator (pman2 l ; pairChecker pcheckl ~ new Pa irChecker(pmanl ), pcheck2 ~ new PairChecker {pman2)¡ exec.execut e (pml ) ; exec.execuce(pm2) ; exec.execute(pcheck1 ) ; exec.execute(pcheck2) ; t ry ( TimeUn i t .MILLISECONDS.sleep (500) i catch(InterruptedException el { System.out.println( nSl eep interrupted n ) i System. out. println (npm1: System.exit(O) i 11 .,. pro1 .,. n\npm2 : n .,. pm2); public static void main (String [] args) PairManager pman1 ~ new PairManager1() , pman2 ~ new PaírManager2 () ; te s t Approaches(pmanl, pman2); 1* Out put: (Sample) prol: paír: x: 15, y: 15 checkCounter pm2 : pair: x: 16, y: 16 checkCounter *jjj, - { 272565 3956974 Como se indica, Pair no es seguro de cara a las hebras, porque su invariante (o porque su invariante es arbitrario) requiere que ambas variables mantengan los mismos valores. Además, como hemos visto anteriOlTI1ente en el capítulo, las operaciones de incremento no son seguras respecto a las hebras, y como ninguno de los métodos está sincronizado, no podemos confiar en que un objeto Pair pennanezca sin corromperse dentro de un programa con hebras. Imagine que alguien nos entrega la clase Pair no protegida frente a hebras, y que necesitamos utilizarla en un entorno de hebras. Para hacer esto, creamos la clase PairManager, que almacena un objeto Pair y controla todo el acceso al mismo. Observe que los únicos métodos públicos son getPair( l, que está sincronizado y el método abstracto increment( l. La sincronización de increment( l se gestionará cuando se lo implemente. La estructura de PairManager, en la que la funcionalidad implementada en la clase base utiliza uno o más métodos abstractos definidos en las clases derivadas se denomina Método de plantillas en la jerga de los Patrones de diseño. 15 Los patrones de diseño nos penniten encapsular los cambios que nuestro código pueda sufrir; aquí, la parte que cambia es el método illcrement( l. En PairManagerl todo el método increment( l está sincronizado, mientras que en PairManager2 sólo se sincroniza parte del método increment( l utilizando un bloque syncbronized. Observe que la palabra clave syncbronized no fonna parte de la signatura del método y que, por tanto, puede ser añadida al sustituirlo en una clase derivada. El método store( l añade un objeto Pair a un contenedor ArrayList sincronizado, por lo que esta operación es segura con respecto a las hebras. Por tanto, no es necesario protegerla, y se la coloca fuera del bloque syncbronized en PairManager2. 15 Veáse Design Pattems, por Gamma el al. (Addison-Wesley, 1995). 21 Concurrencia 769 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 PairManipulator 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 PairManager2.increment( ), que tiene el bloque synchronized y goza, por taoto, de un mayor tiempo con los bloqueos liberados. É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 ()) Einally ( lock.un l ock () ; i // Usar una sección crítica: cIas s Exp Iicit PairManager2 exte nds PairMa nager pri va t e Lock Iock = new Reentran tLock(l¡ pub I ic void i nc rement () { pair t empi lock .loc k () ; try ( p. incrementX () i p. incrementY () ; temp = getPair() i finally ( l ock . unlock( ) ; store (temp ) ¡ pub lic cIas s Explic itCritica lSection { publ i c stat ic void main( String [] args ) throws Exception { PairManager pmanl = new Explic itPairManagerl () , pman2 = new Explicit Pa i rManager2 () ¡ Critic alSec t ion.testApproaches (pmanl, pman2 ) ; /* Output, (Sampl e) pm1: pai r : x: 15, y : 15 checkCounter pm2: pair: x : 16, y: 16 checkCounter *///,- 1740 35 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. Sincronización sobre otros objetos A un bloque synchronized hay que proporcionarle un objeto con el que sincronizarse, y normalmente el objeto más apropiado para este propósito es el objeto actual para el que esté siendo invocado el método: synchronized(this), que es la técnica usada en PairManager2. De esta fonna, cuando se adquiere el bloqueo para el bloque synchronized, no se pueden invocar otros métodos synchronized y secciones críticas del objeto. Por tanto, el efecto de la sección crítica, al sincronizarse sobre this, consiste simplemente en reducir el ámbito de sincronización. En ocasiones, es necesario sincronizarse con otro objeto, pero si hacemos esto debemos garantizar que todas las tareas relevantes se sincronicen con el mismo objeto. El siguiente ejemplo demuestra que dos tareas pueden entrar en un objeto si los métodos de dicho objeto se sincronizan con bloqueos diferentes: JI : concurrency/SyncObject.java II Sincronización con otro objeto. i mport static net .mindview.ut i l.Print .*¡ cIass DualSynch { private Ob ject syncObject = new Ob ject (); pubIi c synchronized void f () { for(int i = O; i < 5; i+ + ) { print (n f () ") ; Thread. yield () i public void 9 () { synchronized (syncObject ) for(int i = O; i < S; i++ ) print (119 () 11) i Thread. yield 1) ; public class SyncObject { public stat i c void main(String[] args) { final DualSynch ds = new DualSynch() i new Threadl) { public void run() { dS.f() ; ) ) . start 1) ; ds.g l) ; ) /* Output, gl) fl) g il fl) g () f () gl ) fl) g() f() *///,- ISample) 21 Concurrencia 771 DuaISync.f() se sincroniza con Ihis (sincronizando el método completo), y g() tiene un bloque synchronized que se sincroniza 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. Almacenamiento local de las hebras Una segunda forma de evitar que las tareas colisionen o accedan a recursos compartidos consiste en eliminar la compartición de variables. El almacenamiento local de las hebras es un mecanismo que crea automáticamente un espacio de almacenamiento distinto para la misma varíable, para cada hebra diferente que esté utilizando un objeto. Así, si tenemos cinco hebras utilizando un objeto con una varíable x, el almacenamiento local de las hebras genera cinco espacios diferentes de almacenamiento para x. Básicamente, este mecanismo nos pennite asociar un cierto estado con cada hebra. La creación y gestión del almacenamiento local de la hebra son realizados por la clase java.Iang.ThreadLocal, como puede verse aquí: jI : concurrency/ThreadLocalVariableHolder.java II Asignación automática de su propio espacio JI de almacenamiento a cada hebra. i mport java. u t i l.concu rrent.*i i mport java . u til.*i c l ass Accessor implements Runnable { private final int id¡ public Acces so r (int idn) { id "" idn¡ public void run() { while(IThread.currentThread () .islnterrupt ed(» ThreadLoca I Variabl eHolder.increment () ¡ System.ou t.println(this) i Thread. yield () ; public String toString () { return n#" """ id + 11: n + ThreadLocaIVariableHolder.get() ¡ public class ThreadLocalVariableHolder { private static ThreadLocal value new ThreadLocal{) { private Random rand = new Random(47); protec ted synchronize d I nteger initialVa l ue() return rand . n extlnt (l OOOO) ¡ } }; public static void i ncrement () value. s et {val ue.getO + 1 ) i public static int get() return value.get(); } public stat i c void main{S tring[J args) throws Exception Executo rService exec = Executors.newCachedThreadPool()¡ 772 Piensa en Java for(int i = O; i < 5; i++} exec.execute(new Ac cesso r(i}); TimeUnit.SECONDS.sleep(3) ¡ II Ejecutar durante un tiempo exec.shutdownNow() ¡ II Todos los objetos AcceS$ors terminarán / * Output: #0, #1, #2, #3, #4, #0, #1, #2, #3, #4, (Samp le) 9259 556 6694 1862 962 9260 557 6695 1863 963 *///, 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 asociado 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 TbreadLocalVariableHolder. Observe que incremcnt( ) y get( ) no están sincronizados, porque ThreadLocal garantiza que no se produzca 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 bastante razonable. Sin embargo. en algunas situaciones, la tarea debe terminarse de manera más abrupta. En esta sección, veremos 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 adicional 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 distintas 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 return (count = ++temp)¡ public synchronized int value() { return count¡ } class Entrance implements Runnable { prívate static Count count = new Count(); prívate static List entrances = new ArrayList() i prívate int number = O; ji No necesita sincronización para leer: prívate final int id; prívate static volatile boolean canceled = false; // Operación atómica sobre un campo volátil: public static void cancel () public Entrance(int id) { { canceled = true; } this.id = id: // Mantener esta tarea en una lista. También impide jI que se aplique la depuración de memoria a las tareas muertas: entrances.add(this) i public void run() { while ( ! canceled) { synchronized(this) ++number; print (this + !I Total: 11 + count. increment () ) ; try { TimeUnit.MILLISECONDS.sleep(lOO) ; catch(InterruptedException e) { print (" sleep interrupted!l); print("Stopping + this); public synchronized int getValue() { return number; public String toString() return "Entrance " + id + ! I : !I + getValue () ; public static int getTotalCount() return count.value(); public static int sumEntrances{) int sum = O; for(Entrance entrance : entrances) sum += entrance.getValue{); return sum; public class OrnamentalGarden { public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); for{int i = O; i < 5; i++) exec.execute(new Entrance(i)); /1 Ejecutar durante un tiempo, luego detenerse y recopilar los datos: TimeUnit.SECONDS.sleep(3) ; Entrance.cancel() i exec.shutdown{) i Concurrencia 773 774 Piensa en Java if(! exec.awaitTermina tion(250, TimeUnit.MILLISECONDS)) pr i nt (1' Sorne tasks were not terminated! n) ; print ("Tota l : 11 + Entrance .getTotal Count ( ) ) i print("Sum of Entrances: 11 + Entran ce . s umEn trances(» ) ; / * Output: Entrance O, 1 Entrance 2, 1 Entrance 1, 1 Entrance 4, 1 Entrance 3, 1 Bn tra nce 2, 2 Entrance 4, 2 Entrance O, 2 (Sample) To ta l: To tal: To tal: Total : Tota l : Total: To t a l : To ta l: 1 3 2 5 4 6 7 8 Entrance 3, 29 Total : 143 Entrance O, 29 Total: 144 Entrance 4, 29 Tot al: 145 Entrance 2, 30 Total: 147 Entrance 1, 30 Tota l : 1 46 Entrance O, 30 Tot a l: 149 Entrance 3, 30 To t al : 148 Entra nce 4, 30 Total : 1 5 0 Stopping Entrance 2, 30 Stopping Entrance 1, 30 Stopping Entrance O, 30 Stoppi ng Entrance 3, 3 0 Stoppin g Entrance 4, 30 Total: 1 5 0 Sum oE Entrances: 150 * /// ,- Un único objeto Couot mantiene la cuenta maestra de los visitantes del jardín y se almacena como campo estático en la clase Entrance. Count.increment( ) y Couot. value( ) están sincrooizados para controlar el acceso al campo count. El método incremente ) utiliza un objeto Random para ejecutar el método yield( ) aproximadanaente la mitad de las veces, entre la operacióo de extraer count para almacenarlo en temp y la de incrementar y almacenar de nuevo temp en conn!. Si desactivamos mediante un comentario la palabra clave syochronized en increment(), el programa deja de funcionar porque habrá múltiples tareas accediendo a couot modificándolo simultáneamente (el método yield( ) hace que el problema se produzca más rápidamente). Cada tarea Entrance (que representa una de las entradas del jardín) mantiene un valor localnumber que contiene el número de visitantes que han pasado a través de esa entrada concreta. Esto proporciona una forma de verificar el objeto count, para comprobar que está registrando el número correcto de visitantes. Entrance.run() simplemente incrementa number y el objeto count y pasa a dormir durante 100 milisegundos. Puesto que Entranee.caneelOO es un indicador booleano volátil, que sólo se lee y se configura (y que nunca es leído en combinación con otros campos), seria posible no sincronizar el acceso al mismo. Pero, siempre que tenga alguna duda acerca de si sincronizar algo o DO, lo mejor es utilizar synchronized. Este programa hace un esfuerzo especial para [malizar todas las cosas de una manera estable. En parte, la razón de hacer esto es ilustrar lo cuidadoso que hay que ser a la hora de terminar un programa multibebra; por otro lado, pretendemos con eUo ilustrar el valor de interrupt( ), del que hablaremos en breve. Después de 3 segundos, maine ) envía el mensaje estático cancele ) a Entranee, de modo que Uama a shutdown( ) para el objeto exec, y luego invoca a awaiffermioation( ) sobre exec. ExecutorServíce.awaitTermination( ) espera a que cada tarea se complete y si todo se completa antes de que finalice la temporización devuelve true, en caso contrario, devuelve false para indicar que no se han completado todas las tareas. Aunque esto hace que cada tarea salga de su método run( ) y terminen por tanto como tarea, los objetos Entrance seguirán siendo válidos, porque en el constructor cada objeto Entrance está almacenado en un contenedor estático List denom..inado entran ces. Por tanto, sumEntrances( ) seguirá funcionando con objetos Entranee válidos. 21 Concurrencia 775 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.increment( ), 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. Terminación durante el bloqueo El método Entrance.run( ) del ejemplo anterior iocluye una llamada a sleep( ) dentro de su bucle. Sabemos que sleep( ) termina por despertarse y que la tarea alcanzará la parte superior del bucle donde tendrá la oportuoidad de salir del mismo, comprobando el indicador cancelled. Sin embargo, sleep( ) es simplemente una de las situaciones en las que la ejecución de una tarea puede quedar bloqueada, y existen ocasiones en las que es necesario terminar una tarea bloqueada. Estados de las hebras Una hebra puede estar en uno de cuatro estados: 1. Nueva: una hebra permanece en este estado sólo momentáneamente, mientras se la está creando, se asignan los recursos del sistema necesarios y se realiza la inicialización. Después de esto, la hebra pasa a ser elegible para recibir tiempo de procesador. El planificador hará entonces que la hebra efectúe una transición a los estados ejecutable o bloqueado. 2. Ejecutable: significa que una hebra puede ejecutarse cuando el mecanismo de gestión de franjas temporales tenga ciclos de procesador para esa hebra. Así, la hebra puede o no estarse ejecutando en cualquier momento dado, pero no hay nada que la impida ejecutarse si el planificador así 10 decide. Por tanto, la hebra no está ni muerta ni bloqueada. 3. Bloqueada: la hebra puede ejecutarse pero hay algo que 10 impide. Mientras que una hebra se encuentra en el estado bloqueado, el planificador simplemente la ignorará y no la asignará tiempo de procesador, Hasta que una hebra no vuelva a entrar en el estado ejecutable, no realizará ninguna operación. 4. Muerta: una hebra en el estado muerto o terminado ya no es tenida en cuenta por el planificador y no puede recibir tiempo de procesador. Su tarea se ha completado y la hebra ya no es ejecutable. Una forma de que una tarea muera es volviendo de su método run( ), pero la hebra de una tarea también puede interrumpirse como veremos en breve. 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 herramientas 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 porque 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 desbloquear 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 bloqueos que la hebra haya adquirido, y si los objetos están en un estado incoherente ("dañados"), otras tareas podrían consultarlos 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 comprobar 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 programador 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 excepciones, porque quiere decir que a menudo se las emplea para control del flujo del ejecución). Para volver a un estado aceptable 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 interrupción InterruptedException si ya está bloqueada o si se trata de realizar una operación de bloqueo. El indicador de estado 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 tareas 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 norrnahnente 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 llamando 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.*; class SleepBlocked implements Runnable { public void run (l { try { TimeUnit.SECONDS.sleep(lOQl i catch(InterruptedException el print (n InterruptedException]l 1 i 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 pri nt ("Exiting SleepBlocked.run()11) i class I OBlocked implements Runnable { p riva te InputS tream in; pub li c I OB l ocked (InputStream is) { in is; } publi c vo id run() { try { print ( IIWaiting for read {) : 11 ) ; in.readO; catch( I OException e l { if (Thread.currentThread () .islnterrupted (» print (" I nterrupted frem b l ocked l / O" ) ; el se { t h row new Run timeException{ e ) ; print{"Exiting I OBl ocked.runO 11) i c l ase SynchronizedBlocked implements Runnable public synchronized void f () { whi l e{true) // Nunca libera el bloqueo Thread .yield() ; publi c SynchronizedBlocked() new Thread () { public void run () f(); JI Bl oqueo adquirido por esta hebra ) ) . start () ; public void run () pr i nt ( "Trying ta call f () 11) i f () ; print ( !lExiting SynchronizedBlocked. run () ") ; publi c c l ass Interrupting { private static ExecutorService exec Executors.newCachedThreadPool{) i s tati c vo id test{Runnable r) throws Inte rruptedException{ Fut ure f = exec . submit (r); Ti meUnit.M ILLI SBCONDS.sleep (lOO) ; print ( II Interrupting " + r.getClass () .getName()); f .canc el(true) ¡ II Interrumpe si se está ejecu tando print ( " Interrupt sent to " + r. getCl ass () . get Name () ) ; public static void main (String [} args) throws Exception test(new SleepBlocke d()); test(new IOBlocked (System.in)) i t e st(new SynchronizedBlocked()); TimeUnit .SECONDS.sleep(3) ; print( IIAbort ing with System. exit(Ol 11) i System.exit(O) i II ... puesto que l as 2 últimas interrupciones fallaron Concurrencia 777 778 Piensa en Java 1* Ou tpu t: (95 % matc h) Inte rrupting SleepB l ocked Inte rruptedException Exiting SleepBlocked.run() Int errupt sent to SleepBlocked Waiting for read() : Interrupting IOBlocked Interrupt sent ta IOBlocked Trying to call fl) Interrupting SynchronizedBlocked Interrupt sent ta SynchronizedBlocked Abor t ing wit h System.exit(O ) * /// , . Cada tarea representa un tipo diferente de bloqueo. SleepBlock es un ejemplo de bloqueo intel11lDlpible, mientras que IOBlocked y SynchronizedBlocked son bloqueos no interrumpibles. 17 El programa demuestra que las operaciones de E/S y de espera sobre un bloqueo synchronized no sean interrumpibles, cosa que también puede deducirse examinando el código: no se requiere ninguna rutina de tratamiento de InterruptedException ni para la E/S ni para los intentos de invocar un método sincronizado. Las dos primeras clases son bastante simples: el método run( ) llama a sleep( ) en la primera clase y a read( ) en la segunda. Sin embargo, para ilustrar SynchronizedBlocked, debemos primero adquirir el bloqueo. Esto se lleva a cabo en el constructor creando una instancia de una clase Thread anónima que adquiere el bloqueo del objeto invocando f() (la hebra debe ser diferente de aquella que está dirigiendo run( ) para SynchronizedBlock, porque una misma hebra si que puede adquirir múltiples veces un bloqueo sobre un objeto). Dado que fO nunca vuelve, ese bloqueo nunca es liberado. SynchronizedBlock.TUn( ) trata de invocar f( ) Y se queda bloqueado esperando a que el bloqueo se libere. Podemos ver, analizando la salida, que se puede interrumpir una llamada a sleep( ) (o cualquier llamada que requiera que capturemos InterruptedException). Sin embargo, no se puede interrumpir una tarea que esté tratando de adquirir un bloqueo sincronizado o que esté tratando de efectuar una operación de E/S. Esto es un poco desconcertante, especiahnente si estamos creando una tarea que realice operaciones de E/S, porque significa que la E/S tiene el potencial de bloquear el programa multihebra. Obviamente esto constituiría un auténtico problema, especialmente para programas basados en la Web. Una solución un tanto drástica pero en ocasiones bastante efectiva para este problema, consiste en cerrar el recurso subyacente que hace que la tarea esté bloqueada: JI : concurrency j CloseResource. j ava // I nterrupc i ón de una tarea bloqueada / / cerrando el recurso subyacente. / / {RunByHand} import java.net.*¡ import java. util.concurrent.*; impo rt java.io.*; import static net .mindview.ut il.print. * ¡ public c l ass CloseResource { publi c static void main (String [J args ) throws Exception { ExecutorServi ce exec = Executors.newCachedThreadPool () ; ServerSocke t server = new ServerSoc ket(808 0) ¡ I nput Stream socke t lnput = new Socket ( lIlocalhost ll , 8 08 0) . get Inpu tSt r eam() ; exec .exec ute {new IOBlocked (s ocke t l npu t )) ¡ exec.execu te {new IOBlocked (System.in); TimeUnit.MILLISECONDS.sleep{lOO ) i print ( IIShutting down all threads!!) i exec.shutdownNow() ; 17 A lgunas versiones del JDK también proporcionaban soporte para la instrucción InterruptedIOException. Sin embargo, este soporte s610 estaba parcialmente 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 TimeUni t.SBCONDS .sl eep ( l )¡ print ("C l os ing 11 + socke t l nput . getClass () . getName () ) i socketlnput.close() ; 1/ Libera la heb r a b loqueada TimeUnit .SECONDS.sleep (l) ; print ( "Closing 11 + System. in.getClass () .getName () ) i System . in.close(); /1 Libera la hebra b l oqueada 1* Output , (85% match) Waiting for re ad() : Waiting for read() : Shutting down all thre a ds Cl os1 ng java.net.SocketlnputStream Interrupted fram blocked l /O Exi ting IOBlocked.run () Closing j ava. 10.Buf feredlnput Stream Exiting IOBl ocked .run {} * /11 , Después de invocar shutdownNow(), los retardos utilizados antes de llamar a close() para los dos flujos de datos de entrada permiten resaltar que las tareas se desbloquean una vez que el recurso subyacente se ha cerrado. Resulta interesante observar que la interrupción aparece cuando estarnos cerrando un objeto Socket pero no al cerrar System.in. Afortunadamente, las clases nio presentadas en el Capítulo 18, Entrada/salida, permiten una interrupción más civilizada de las interrupciones de E/S. Los canales nio bloqueados responden automáticamente a las interrupciones: 11: cencurrency/NIOInterruption. java II Interrupc ión de un canal NIO bloqueado. impert impert i mpert impert impert i mpert java.net.*; java . nio. *; java.nio.channel s.*; java.util.eoneurrent.*; java . io .* ¡ stati c net. mindview. u t il.Pr i nt.*¡ class NI OBl ocked implements Runnabl e { private fina l SocketChannel sc; pub li e NIOBlocked (SocketChannel se) { this. se public void run() { try { print (IIWaiting for read () in " + this ) ; sc .read(ByteBuffer.allocate(l) i catch (ClosedByInterruptException el { print ( !I ClosedByInterruptExceptien") ; catch{AsynchronousCloseExcept i on el { print ( lIAsynchronousCloseException 11 l ; catch(IOExcept i on e ) { throw new Runtime Excep t i on (e) i se; ) ) p r int ( "Exiti ng NIOB l ocked.run() 11 + t hi s); public class NIOlnterruption { public static void main(String[] args) throws Exception { ExecutorService exec = Exeeutors.newCachedThreadPeol(); ServerSocket server = new ServerSocket(80BO) ¡ InetSocketAddress isa = new InetSocketAddress(lI localhost", 8080) ¡ SecketChannel sel SocketChannel .op en(isal ¡ SocketChannel sc2 = SocketChannel.open( i sal¡ 780 Piensa en Java Future t = exec.submit(new NIOBlocked(scl)); exec.execute(new NIOBlocked(sc2)) ¡ exec.shutdown() ¡ TimeUnit.SECONDS.sleep(l) ¡ II Producir una interrupción mediante cancel: f. cancel (true) ; TimeUnit.SECONDS.sleep(l) ¡ II Liberar el bloqueo cerrando un canal: sc2. close () ¡ 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 necesario 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. Tareas bloqueadas por un mutex Como vimos en Interrupting.java, si tratamos de llamar a un método sincronizado sobre un objeto cuyo bloqueo ya haya sido adquirido, la tarea llamante será suspendida (bloqueada) hasta que el objeto esté disponible. El siguiente ejemplo muestra cÓmo puede una misma tarea adquirir múltiples veces el mismo mutex: jj: concurrencyjMultiLock.java jj Una hebra puede volver a adquirir el mismo bloqueo. import static net.mindview.util.Print.*¡ public class MultiLock { public synchronized void fl(int count) i f (count-- > O) { print (nfl () calling f2 () with count f2 (count) ; public synchronized void f2(int count) { if (count-- > O) { print(l1f2() calling fl() with count fl (count) ; 11 + count); 11 + count); public static void main(String[] args) throws Exception final MultiLock multiLock = new MultiLock() ¡ new Thread () { 18 Ervin Varga me ayudó en la investigaciones relacionadas con esta sección. 21 Concurrencia 781 public vo i d run () multiLock ,fl(lO) ( j } }.start (); 1* Output : n () f2 () n() f2 () n I) f2 O n () f21) n() f2 () cal ling cal ling call ing call i ng cal ling calling cal ling call i ng call ing c a lling f2 () n () f2 with count 9 wi th count 8 O with count 7 no wi th count 6 f 2 1) wi th count 5 n () f2 with count 4 O with count no 3 with count 2 f2 () with count 1 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ísticas 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 class Blocked2 i mplemen ts Runnable { BlockedMutex blocked = new BlockedMut ex() ; public void run() ( print ( "Wa i ting for f ( ) in Bl ockedMutex " ) i blocked. f () i print (II Broken out of blocked call 1l ) ; 782 Piensa en Java public class Interrupting2 public static void main(string [] args) throws Exception { Thread t = new Thread (n ew Blocked2 (» ¡ t.startO ; TimeUni t .SECONDS.sleep(l) j System.out.println(I!Issuing t.int errupt() n); t.in terrupt{}; /* 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( ) permite salir de una llamada que esté bloqueada por un mutex. 19 Comprobación de la existencia de una interrupción Observe que cuando invocamos interrupt( ) para una hebra, el único instante en que se produce la interrupción es cuando la tarea entra, o se encuentra ya dentro, de una operación bloqueante (excepto, como hemos visto, en el caso de los métodos sincronizados bloqueados O la E/S ininterrumpibles, en cuyo caso no hay nada que podamos hacer). Pero ¿qué sucede si hemos escrito.código que pueda o no hacer esa llamada bloqueante dependiendo de las condiciones en las que se la ejecute? Si sólo podemos salir generando una excepción en una llamada bloqueante, no siempre seremos capaces de abandonar el bucle run( ). Por tanto, si invocamos interrupt( ) para detener una tarea, la tarea necesita una segunda forma de salir en caso de que el bucle ruo( ) no esté realizando ninguna llamada bloqueante. Esta oportunidad se presenta gracias al estado interrnmpido, que es fijado por la llamada a interrupt( ). Comprobamos si la tarea está en estado interrumpido llamando a interrupted( ). Esto no sólo nos dice si se ha llamado a interrupt( ), sino que también borra el estado interrumpido. Borrar el estado interrumpido garantiza que el sistema no nos notifique dos veces que se ha interrumpido una tarea. La notificación la recibiremos a través de una única excepción Interrupted-Exception o una única comprobación con éxito del método Tbread.lnterrupted( ). Si queremos comprobar de nuevo si hemos sido interrumpidos, podemos ahnacenar el resultado al invocar Tbread.interrupted( ). El siguiente ejemplo muestra la sintaxis típica que se usaría en el método run( ) para gestionar ambas posibilidades (bloqueada y no bloqueada) cuando está activado el estado interrumpido: /1 : concurrency/lnte rruptingldiom.java /1 Sintaxis general para interrumpir una tarea. II {Args, l1oo} import j ava.uti l.concurrent .* ¡ import static net.mindview.ut i l.Print.*¡ c lass NeedsCleanup { prívate final int id; public NeedsCleanup(int ident ) id = ident¡ print ("NeedsCleanup + id) i public void cleanup() pri n t ( 11 Cleaning up " + id ) i 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 class Blocked3 implements Runnable private volatile double d = 0.0; public void run() { try { while(!Thread.interrupted()) // punto1 NeedsCleanup nl = new NeedsCleanup(I); II Iniciar try-finally inmediatamente después de la definición II de nI, para garantizar una limpieza apropiada de nI: try { print (nSleeping") i TimeUnit.SECONDS.sleep(l) ; / / Punto2 NeedsCleanup n2 = new NeedsCleanup(2) i II Garantizar una limpieza apropiada de n2: try { print ("Calculating ll ) i II Una operación no bloqueante de larga duración: for(int i = 1; i < 2500000; i++) d = d + (Math.PI + Math.E) / d; print (11 Finished time-consuming operation") j finally { n2. cleanup () ; } finally { nl.cleanup() i print ("Exiting via while () test 11) ; catch(InterruptedException e) { print (IIExiting via InterruptedException"); public class Interruptingldiom { public static void main(String[] args) throws Exception i f (args .length ! = 1) { print (nusage: java InterruptingIdiom delay-in-mS") ¡ System.exit (1) i Thread t = new Thread(new Blocked3()}¡ t. start () i TimeUnit.MILLISECONDS.sleep(new Integer(args[O])) t. interrupt () ¡ 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 *///,- i Concurrencia 783 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 invoque interrupt(). Utilizando diferentes retardos, podemos salir de Blocked3.run( ) en diferentes puntos del bucle: en la Hamada 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 bloqueante. En ese caso, sólo se limpian los objetos NeedsCleanup que hayan sido creados hasta el punto en que se genera la excepció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 seguida 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 destructores en Java, depende de que el programador de clientes describa las cláusulas try-finaUy apropiadas. Cooperación entre tareas Como hemos visto, cuando se utilizan hebras para ejecutar más de lUla tarea simultáneamente, podemos evitar que unas tareas interfieran con tos recursos de otras utilizando un bloqueo (mutex) para sincronizar el comportamiento de las dos tareas. En otras palabras, si dos tareas está interfiriendo en lo que respecta a un recurso compartido (normalmente la memoría), utilizamos un mutex para permitir que sólo una tarea acceda en cada momento a dicho recurso. Con ese problema resuelto, el siguiente paso consiste en aprender lo que hay que hacer para que las tareas puedan cooperar entre sí, de modo que múltiples tareas puedan trabajar juntas para resolver un cierto problema. Ahora, la cuestión no es qué interferencias se producen entre unas tareas y otras, sino cómo trabajar al unísono, ya que partes de un cierto problema debe~ rán ser resueltas antes de que se puedan resolver otras partes. Esto se parece bastante a la planificación de proyectos: primero hay que hacer los cimientos de la casa, pero mientras, se pueden ir haciendo los perfiles de aluminio o fabricando los ladrillos, y estas dos tareas tienen que estar finalizadas antes de que el resto de la casa pueda completarse. Asimismo, la fontanería deberá estar terminada antes de hacer las paredes, las paredes deberán haber sido acabadas antes de poder finalizar los interiores, etc. Algunas de estas tareas pueden hacerse en paralelo, pero ciertos pasos requieren que determinadas tareas previas se compieten antes de poder continuar. La cuestión clave cuando hay una serie de tareas cooperando es la negociación que se produce entre dichas tareas. Para llevar a cabo esa negociación, utilizamos la misma base: el mutex, que en este caso garantiza que sólo haya una tarea que pueda responder a una señal. Esto elimina cualquier posible condición de carrera. Además del mutex, tenemos que añadir una forma de que una tarea suspenda su ejecución hasta que un cierto estado externo cambie (por ejemplo, "la fontanería ha sido acabada"), lo que indicará que será el momento de que dicha tarea continúe. En esta sección, vamos a examinar el tema de la negociación entre tareas, que se puede implementar utilizando los métodos wait() y notifyAII() de Object. La biblioteca de concurrencia de Java SES también proporciona objetos Condition con métodos await() y signal(). Veremos los problemas que pueden surgir, junto con sus correspondientes soluciones. wait() y notify AUO wait( ) nos permite esperar a que se produzca un cambio en cierta condición que está fuera del control del método actual. A menudo, esta condición será modificada por otra tarea. Lo que no queremos es permanecer inactivos dentro de un bucle mientras comprobamos la condición de la tarea; este tipo de espera se denomina espera activa, y representa usualmente un mal uso de los ciclos de procesador. Por ello, el método wait( ) suspende la tarea mientras espera a que el mundo exterior cambie, y sólo cuando tiene lugar una llamada a notify() o notifyAll( ) (que sugieren que puede haber ocurrido un cierto suceso de interés) se despertará la tarea y comprobará si se han producido cambios. De este modo, wait() proporciona una fonna de sincronizar las actividades entre las tareas. 21 Concurrencia 785 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 adquirido por otra tarea, por lo que durante mla espera con wait() podrán iovocarse otros métodos sincronizados en el (abara desbloqueado) 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 puedan 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 permitir 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, independientemente 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étodos 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 IIIegalMonitorStateException con el poco intuitivo mensaje de "current thread not owner" Oa hebra actual no es la propietaria). Este mensaje 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 dentro 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 (representado 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 () ; public synchronized void buffed() { waxOn = false; /1 Listo para otra capa de cera 786 Piensa en Java notifyAll () ; publi c synchronized voi d waitForWaxing() throws InterruptedException { while(waxOn == fa l se) wait () ; publ i c synchronized void waitForBuffing ( ) throws I nterruptedException { whil e (waxOn == true) wait () ; class WaxOn impl ement s Runnable { private Car car¡ publ ic WaxOn(Car e l { car = c; public void run() { try { wh i l e ( !Th r ead .int e rrupted () printnb (IrWax On! Ir); TimeUni t .MILLI SECONDS.s leep(2 00l car. waxed ( ) ; car.wai tFo r Bu ff i ng () ; i catch(Int erruptedExc epti on el { prin t ( IIExiting via interrupt " ) ; print (IIEnding Wax On task" ) ; class waxOff implements Runnabl e { pr i v ate Car car; pub lic WaxOf f (Car e } { car = c¡ publi c void run () try { { while (-! Thread . interr upted () ) car . wa i tForWaxing() ; printnb ( "Wax Off ! 11); TimeUnit.MILLISECONDS.sleep(200) ; c ar . b u ff ed () ; catch(InterruptedException el { print(IIExiting via interr upt ll l ; print (!l Ending Wax Of f task" ) i public cIass WaxOMatic { publ ic static void main(String[] args) throws Exeeption { Car car = new Ca r ( ) ¡ ExecutorService exec = Executors.newCachedThreadPool( ) ; exec.execute(new WaxO ff(ca r)) i exec.execute (new WaxOn (car »); TimeUn i t . SECONDS.sleep (S)¡ II Ejecu tar duran t e cierto tiempo ... exec.shutdownNow(); II Int errumpir todas las tare as 21 Concurrencia 787 } 1* Output, (95% match) Wax On! Wax Off! Wax Gn! Wax Off! Wax Off! Wax On! Wax Off! Wax On! Wax On! Wax Off! Wax Gn! Wax Off! Wax Off! Wax On! Wax Off! Wax Gn! End ing Wax On task Wax On! Wax Off! Wax On ! Wax Off! Wax Gn! Wax Off! Wax On! Wax Off! Wax On! Exiting via inte rrupt Exiting via interrupt Ending Wax Off task *//1, Aquí, Car tiene un único campo booleano waxOn, que indica el estado del proceso de aplicación-pulido. En waitForWaxing( ), se comprueba el indicador waxOn y, si es false, se suspende la tarea lIamante invocando wait( ). Es importante que esto tenga lugar dentro un método sincronizado, en el que la tarea haya adquirido el bloqueo. Cuando invocamos wait( ), la hebra se suspende y el bloqueo se libera. Resulta esencial que se libere el bloqueo, porque para cambiar con seguridad el estado del objeto (por ejemplo, para cambiar waxOn a true, que es algo que tiene que ocurrir para que l. tarea suspendida pueda continuar), dicho bloqueo debe estar disponible para que lo adquiera alguna otra tarea. En este ejemplo, cuando otra tarea invoca waxed() para indicar que es el momento de hacer algo, hay que adquirir el bloqueo para poder cambiar waxOn a true. Después, waxed( ) invoca a notify AU( ), que despierta a la tarea que había sido suspendida en la llamada a wait(). Para que la tarea pueda despertarse de una llamada a wait( ), deberá primero readquirir el bloqueo que liberó en el momento de entrara en wait( ). La tarea no se despertará hasta que dicho bloqueo esté disponible 2o WaxOn.run( ) representa el primer paso dentro de un proceso de aplicación de la cera al coche, así que lleva a cabo su operación: una llamada a sleep() para simular el tiempo necesario para la aplicación de la cera. A continuación, le dice al coche que la aplicación de la cera se ha completado y llama a waitForBuffing(), que suspende esta tarea con una llamada a wait( ) hasta que la tarea WaxOff llama a buffed() para el coche, cambiando el estado y llamando a notifyAll(). WaxOff.run(), por otro lado, entra inmediatamente en waitForWaxing( ) y se suspende, por tanto, hasta que la cera haya sido aplicada por WaxOn y se invoque a waxed(). Cuando se ejecuta este programa, podemos ver cómo este proceso en dos pasos se repite continuamente a medida que las dos tareas se ceden la una a la otra el control. Después de cinco segundos, interrupt( ) detiene ambas hebras; cuando se invoca shutdownNow() para un objeto ExecutorService, éste invoca a interrupt() para todas las tareas que esté controlando. El ejemplo anterior resalta el hecho de que hay que rodear una llamada a wait( ) con un bucle while que compruebe la condición o condiciones de interés. Esto es importante porque: • Puede que tengamos múltiples tareas que estén esperando a un determinado bloqueo por la misma razón, y la primera tarea que se despierte puede cambiar la situación (incluso si no hacemos esto, alguien podría heredar de nuestra clase y hacerlo). En este caso, dicha tarea debería ser suspendida de nuevo hasta que su condición de interés cambiara. • En el momento en que esta tarea se despierte de su llamada a wait( ), es posible que alguna otra tarea haya cambiado las cosas de modo que esta tarea sea incapaz de realizar su operación en este momento, o no le interesa realizarla. De nuevo, deberla volver a ser suspendida invocando de nuevo a wait( ). • También es posible que las tareas estuvieran esperando el bloqueo del objeto por razones distintas (en cuyo caso, es necesario utilizar notifyAll( En este caso, necesitarnos comprobar si se nos ha despertado por la razón correcta y, en caso contrario, volver a invocar wait( ). ». Por !auto, resulta esencial que comprobemos nuestra condición de interés concreta y que volvamos a wait( ) si dicha condición no se cumple. La estructura sintáctica para hacer esto es un bucle while. Ejercicio 21: (2) Cree dos clases Runnable, una con un método run() que se inicie e invoque wait( ). La segunda clase debe capturar las referencias del objeto Runoable. Su método run( ) debería invocar notify AllO para la 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 informar del cambio a través de la consola. Observe cuánto tiempo desperdicia el programa dentro de la espera 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 ) { sharedMoni tor .no tify() ; T2, while(someCondition) 11 Punto 1 synchroniz ed(sha redMonito r) sharedMonitor .wait() ; La 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á ciegamente en wait(). El mensaje notify() se perderá y T2 esperará indefinidamente a recibir una señal que ya había sido enviada, 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 correcta 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 entrará 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 synch ronized void prod () { notify()¡ } synchronized void prodAll () { notifyAll () ; clas s Task implements Runnable { static Blocker blocker = new Blocker(); public vo id run( ) { blocker.waitingCal l() ¡ class Task2 implements Runnable { II Un ob jeto Blocker separado: static Blocker b l ocker = new Blocker( ) ¡ public void run() { blocker.waitingCall()¡ publ ic class No tifyVsNotifyAll { public static void main(String[] args) t hrows Exception { Exec utorService exec = Executors.newCachedThreadPool( ); for(int i = O; i < Si i++) exec.execute(new Task(»¡ exec.execu te {new Task2{»¡ Timer timer = n ew Timer()¡ timer.scheduleAtFixedRate(new Time rTask() { bool ean prod = true ; pub lic vo id run () { i f (prod ) { System.out .print(lI\nnotifyO Task.block er. prod() i prod = fals e¡ u) i el se { System.out.print(" \ nnotifyAll ( ) Task .blocker.prodAl l{) i prod = true; n); } }, 400, 400}¡ II Ejecutar cada 4 segundos 790 Piensa en Java TimeUnit.SECONDS.sleep(S) i /1 Ejecutar durante un tiempo ... timer.cancel() ; System.out .println ("\nTimer canceled"); TimeUnit.MILLISECONDS.sleep(500) ; System.out.print (UTask2.blocker.prodAll () Task2.blocker.prodAll() 11); j 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-lthread-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-lthread-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-lthread-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-lthread-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-lthread-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-lthread-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, ninguna 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 quiere 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 concreto. 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 camarero 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¡ orderNum¡ } public Meal(int orderNum) { this.orderNum public String toString () { return uMeal 11 + orderNum¡ } class WaitPerson implements Runnable { private Restaurant restaurant¡ public WaitPerson(Restaurant r) {restaurant r¡} public void run() { try ( while(!Thread.interrupted()) synchronized(this) { while(restaurant.meal == null) wait()¡ II ... para que el cocinero prepare un plato. print (UWai tperson got 11 + restaurant. meal) ¡ synchronized (restaurant. chef) { restaurant.meal = null¡ restaurant.chef.notifyAII() ¡ II Listo para otro catch(InterruptedException e) { print ("Wai tPerson interrupted"); class Chef implements Runnable { private Restaurant restaurant; private int count = O¡ public Chef(Restaurant r) {restaurant r¡} public void run() { try ( while(!Thread.interrupted()) synchronized (this) { while(restaurant.meal != null) wait()¡ II ... para que se lleven el plato 792 Piensa en Java if (++count == 10) { print ("Out of food, closing") ¡ restaurant.exec.shutdownNow() ; printnb ( 11 Order up! "); synchronized(restaurant.waitPerson) restaurant.meal = new Meal(count)¡ restaurant.waitPerson.notifyAll() ; TimeUnit.MILLISECONDS.sleep(lOO) ; catch(InterruptedException e) print("Chef interrupted!1} ¡ public cIass Restaurant { Meal meal; ExecutorService exec = Executors.newCachedThreadPool(); WaitPerson waitPerson = new WaitPerson(this} ; Chef chef = new Chef(this)¡ public Restaurant() { exec.execute(chef)¡ exec.execute(waitPerson) ; pubIic static void main(String[] new Restaurant(}¡ /* Output: Order upl Waitperson got Meal Order up! Waitperson got Meal Order up! Waitperson got Meal Order up! Waitperson got Meal Order up! Waitperson got Meal Order up! Waitperson got Meal Order up! Waitperson got Meal Order up! Waitperson got Meal Order up! Waitperson got Meal Out of food, closing WaitPerson interrupted Order up! Chef interrupted args) { 1 2 3 4 5 6 7 8 9 *///,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 situaciones 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 despertamos, 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() (empleando, 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 invocar 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( ) completo 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 bloqueante (interrumpible). Por tanto, primero veremos que se muestra el mensaje "Order up!" y luego se genera InterruptedException 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 posteriormente 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ápido que el consumidor. Si el consumidor es más rápido que el productor, entonces no deberá leer los mismos 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. Utilización de objetos Lock y Condition explicitos Existen berranlientas adicionales ex.plícitas dentro de la biblioteca java.ntil.coneurrellt de Java SES que puede utilizarse para reescribir WaxOMatic.java. La clase básica que utiliza un mutex y permite la suspensión de tareas es Condition, y puede suspender una tarea llamando a await() sobre un objeto Condition. Cuando tiene lugar un cambio de estado externo que pudiera implicar que una tarea puede continuar con su procesamiento, enviamos una notificación a la tarea invocando el método sigllal( ), para despertar a una sola tarea, o signalAlI( ), para despertar a todas las tareas que se hayan suspendido a sí mismas para esperar por ese objeto Condition (al igual que sucede con notifyAII(), signalAJJ() es la técnica más segura). He aquí un programa WaxOMatic.java reescrito para incluir un objeto Condition que se utiliza para suspender una tarea dentro de waltForWaxing( ) o waitForBuffing( ): 794 Piensa en Java ji: concurrency/waxoma t i c2;WaxOMat i c2 . java // Ut i lizac i ón de objetos Lock y Condit i on. package concurrency.waxomatic2¡ i mport java.uti l .con c urrent.*; import j ava.util.con current.locks.*¡ i mport stat i c net . mindview.util . Pri nt.*¡ 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() ; pub li c vo id waitFo r Waxing () t h rows I n terruptedException { lock . l ock() ; t ry ( while(waxOn == falsel condi tion.await() ¡ finally ( lock .unl ock() ; public vo i d waitForBuffing() lock .l ock () ; try ( while(waxOn ~~ true) condi t ion.awai t () ¡ finally ( l ock. un l ock( ) ; throws InterruptedExcep tio n{ class WaxOn i mpleme nt s Runnable { priva te Car ear i public WaxOn (Car e) { car = c¡ p ub lic voi d run () { try ( while( !Thread .interrupted{)) pri ntnb ( nWax On ! U); TimeUni t .MILLISECONDS.s l eep(200) ear.waxed{) ; i 21 Concurrencia 795 car.waitForBuffing( ) ; catch{InterruptedException e ) { print (" Exi ting vía interrupt II) i print ( 11 Ending Wax On task 11 ) i class WaxOff implements Runnable { private Car car¡ public WaxOff(Car el { car = C¡ public voi d run ( ) { try { while { !Thread.interrupted (» car.waitForWaxing()¡ pri ntnb ( "Wax Off! ") ; TimeUnit.MILLISECONDS.sleep( 2 00 ) ; car.buffed () ; catch(InterruptedException e ) { print (11 Exi t ing via interrupt n) i p rin t (IIEnding Wax Of f task " ) i public clase WaxOMatic2 { public static void main(String (} args) throws Exception { Car car = new Car ( ) ¡ Execut orService exec = Executo rs.newCachedThreadPool () ¡ exec.execu te (new WaxOff (car » ¡ exec.execu te (new WaxOn (car»¡ TimeUn it,SECONDS .sleep (5 ) i exec.shutdownNow() i / * Output, (90% match ) Wax On! Wax Off ! Wax On ! Wax Off! Wax Of f! Wax On! Wax Of f! Wax On ! Wax On! Wax Off ! Wax On ! Wax Off! Wax Off! Wax On! Wax Off! Wax On! Ending Wax Off task Exi t ing via interrupt Ending Wax On task Wax On! Wax Off ! Wax On ! Wax Off ! Wax Onl Wax Off! Wax On! Wax Off! Wax On! Ex i ting via interrup t * / //,En el constructor de Car, un único objeto Lock produce un objeto Condition que se utiliza para gestionar la comunicación inter-tareas. Sin embargo, el objeto Condition no contiene ninguna información acerca del estado del proceso, así que es necesario gestionar información adicional que indique este estado; esa información es el campo waxOn de tipo booleano Cada llamada a lock() debe ir seguida inmediatamente de una cláusula try-finally para garantizar que el desbloqueo se produzca en todos los casos. Al igual que sucede con las versiones integradas, una tarea debe poseer el bloqueo antes de poder invocar a await( ), sigoal( ) o signalAlI( ). Observe que esta solución es más compleja que la antenor y que esa complejidad no nos proporciona ninguna ventaja adicional en este caso. Los objetos Lock y Condition sólo son necesarios para otros problemas de gestión de hebras más complicados. Ejercicio 27: (2) Modifique Restaurant.java para utilizar objetos Lock y Condition explícitos. 796 Piensa en Java 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 elimine 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 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 + ,,) t.inter r upt() ; prin t ("F i nished " + msg + " test"); n); public s tatic void main(String(] args) test ("LinkedBlockingQueue", / / Tamaño i l imi t ado new LinkedBlockingQueue()} i test ("ArrayBlockingQueue ll , JI Tamaño fijo new ArrayBlockingQueue (3)); test ( 11 Synch:tonousQueue ", / / Tamaño igua l a 1 new SynchronousQueue()) i 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( ). Ejemplo de uso de BlockingQueue Como ejemplo de utilización de las colas de tipo BlockingQueue, considere una máquina que tiene tres tareas: una para hacer una tostada, otra para untarla de mantequilla y otra para poner mennelada sobre la tostada ya untada con mantequilla. Podemos ir haciendo pasar la tostada por las distintas colas BlockingQueue que sirven de comunicación entre los procesos: /1: concurrency/ToastOMatic.java /1 Una i mport import import tostadora basada en colas. java.util .concurrent.*; java.uti l .*; static net.mindv iew. util.Pri n t.*: 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 class Toas tQueue extends LinkedBlockingQueue {} class Toaste r i mplements Runnable prívate ToastQueue toastQueue ¡ prívate int count = O; private Random rand = new Random( 47) i public Toaster (ToastQueue tq} { toastQueue tq; ) publ ic void run( ) { try ( while(!Thread.interrupted(» TimeUnit.MILLISECONDS.sleep( 100 ~ rand.next lnt(500» i /1 Hacer t o stada Toast t = new Toast(countTT) print (ti; /1 Insertar en la cola toas tQueue.put(tl¡ i catch(InterruptedException e) print (nToaster int errupted 11) i print ( flTo aster o ff n ) ; /1 Untar de mantequilla la tostada: c lass Bu t terer implements Runnab l e { prívate ToastQueue dryQueue, butteredQueue¡ public Butterer(ToastQueue dry, Toas tQueue but tered ) dryQueue = dry; bu t teredQueue = but tered¡ public void run () try ( while(!Thread.interrupted{)) II Se b l oquea hasta que haya otra tost ada dispon ible: Toast t = dryQueue.take{); t .butter () ¡ print (ti; butteredQueue.put(t l ; c atch(Int erruptedException el print("Butterer interrupted ll ) { i print ( lIButterer off" ) ; II Pone r mermelada sobre la tostada untada de mantequilla: c l ass Jammer implements Runnable { prívate Toas tQueue butteredQueue, f inishedQueue; public Jammer(ToastQueue buttered, ToastQueue finishedl butteredQueue butt ered¡ finis h edQueue = finished; public void run () try ( while( !Thread .interrupted()) { II Se bloquea hasta que haya ot ra t ostada disponible: 21 Concurrencia 799 Toas t t : butteredQueue.take( } i t. jam !) ; print!t) ; f i nishedQueue . pu t( t l ; ca tch (I nterruptedExc eption el print (" Jammer inte rrupted IT ) i 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¡ pub l ic void run () try { whi le ( !Th read .in t errupted ()} // Se b l oquea hasta que haya otra to s tada disponible: Toas t t = fi nishedQueue .take (); /1 Verif i car que l a tostada se ha p rep arado co rre ctamente ji y que todas l as t os tadas l l evan mermelada: if(t.getld ll ! " counter++ 1 1 t.getStatus() ! = Toast . Status.JAMMED) print(II:»» Error: 11 + tl i System.exi t (1 ); el se print("Chomp! n + t ) ; catch(Inte rruptedExcep tion e ) print (n Eater interrupted "); print (IIEate r o f f" ) ¡ publi c c lass ToastOMatic { public stat ic void main(String[] a rgs) throws Exception ToastQueue dryQueue = new ToastQueue( ) , bu t teredQueue = new ToastQue ue( ), finishedQueue = new ToastQueue()¡ ExecutorS ervice exec : Executors.newCachedThreadPool() i exec. execute(new Toaster(dryQueue)); exec.execute (new But terer(dryQueue, butteredQueue) ) ; exec.execu te( new Jarnmer(butteredQueue , f inishedQueue}) ¡ exec.execute(new Eater(finishedQueue » i TimeUnit . SECONDS .sleep(S) i exec. shutdownNow () i / * (Ejecu tar para ver la salida ) */// : - Toas! es un excelente ejemplo de la ventaja de emplear valores enum. Observe que no hay ninguna sincronización explícita (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 existirí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, utilizando dos líneas de fabricación separadas (una para el pan con mantequilla, otra para el pan con mermelada y luego mezclando las dos líneas). Utilización de canalizaciones para la E/S entre tareas A menudo, resulta útil que las tareas se comuniquen entre sí utilizando mecanismos de E/S. Las bibliotecas de gestión de hebras pueden proporcionar soporte para la E/S inter-tareas en la forma de canalizaciones (pipes). Estas canalizaciones existen en la biblioteca E/S de Java en forma de las clases PipedWriter (que permite a una tarea escribir en una canalización) y PipedReader (que permite a otra tarea distinta leer de la misma canalización). Podríamos considerar esto como una variante del problema productor-consumidor, siendo la canalización una solución prediseñada. La canalización es básicamente una cola bloqueante, y existía ya como solución en las versiones de Java anteriores a la introducción de BlockingQueue. He aquí un ejemplo simple en el que dos tareas utilizan una canalización para comunicarse: /1: eoncurrency/PipedIO.java /1 Utilización de canalizaciones para la E/S inter-tareas import java.util.concurrent.*¡ import java.io.*; import java.util.*¡ import static net.mindview.util.Print.*¡ class Sender implements Runnable { private Random rand = new Random(47) ¡ private PipedWriter out = new PipedWriter(); public PipedWri ter getPipedWriter () { return out ¡ } public void run() { try { while (true) for(char c = 'A' ¡ c <= 'z' ¡ c++) { out. write (e) ; TimeUnit.MILLISECONDS.sleep(rand.nextlnt(500)) ¡ catch(IOException e) { print (e + " Sender write exception") i eatch(InterruptedException e) { print (e + " Sender sleep interrupted"); class Receiver implements Runnable { private PipedReader in; public Receiver(Sender sender) throws IOException in = new PipedReader(sender.getPipedWriter()) ¡ public void run () try { while(true) { 1/ Se bloquea hasta que haya caracteres: printnb(rrRead: rr + (char)in.read() + n, 11); catch(IOException e) print(e + rr Receiver read exceptionrr); 21 Concurrencia 801 public c l ass PipedIO { public stat ic void main(String[] args) throws Exception { Sender sender = new Sender()¡ Receiver receiver = new Rece iver (sende r )¡ Execu t orService exec = Executors.newCachedThreadPool(}¡ exec.execute( sender) ; exec. execute(receiver) ; TimeUnit .SECONDS,s leep (4 ) ; exec.shutdownNow() ; /* Output, ( 65\ match ) Read: A, Read: B, Read : e, Read : DI Read: E, Read: F, Read: G, Read: H, Read: I, Read: J, Read: K, Read: L, Read: M, java.lang.lnterruptedException: sleep interrupted Sender sleep i n terrupted java.io.lnterruptedIOException Rece iver r ead exception * /// , Sender y Receiver representan tareas que necesitan comunicarse entre sí. Sender crea tUl objeto escritor PipedWriter, que es un objeto autónomo, pero dentro de Reeeiver la creación del objeto lector PlpedReader debe asociarse con un objeto PipedWriter dentro del constructor. Sender pone datos sobre el objeto Writer y duerme una cantidad aleatoria de tiempo. Sin embargo, Receiver no tiene ningún método sleep( ) o wait(). Sin embargo, cuando invoca el método de lectura read( ), la canalización se bloquea automáticamente si no existen más datos. Observe que los objetos sender y recelver se inician en maln(), después de que los objetos hayan sido completamente construidos. Si no comenzamos con objetos completamente construidos, la canalización puede presentar un comportamiento incoherente en las distintas plataformas (observe que las colas de tipo BIockingQueue son más robustas y fáciles de usar). Una diferencia importante entre un objeto PipedReader y la E/S normal es la que podemos ver cuando se invoca sbutdownNow(): el lector PipedReader es interrumpible, mientras que si cambiáramos, por ejemplo, la llamada in.read( ) por System.ln.read( ), la interrupción interrupt( ) no conseguiría salir de la llamada a read( ). Ejercicio 30: (1) Modifique PipedIO.java para utilizar una cola BlockingQueue en lugar de una canalización. 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 interbloqueo (dead/ock).21 Si tratamos de ejecutar un programa y se produce directamente un interbloqueo, podemos intentar localizar inmediatamente 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 ejecute (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 descripció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 bloqueadas), 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 problema, 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 comprar cinco palillos (o más generalmente, el mismo número de palillos que de filósofos). Esos palillos están espaciados alrededor 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 Palillos para la cena de los filósofos. JI public class Chopstick { private bool ean taken = false¡ public s ynchron ized void take() t h rows Interrup tedException whi l e (taken) wait () taken = i true¡ public synchronized voi d drop() taken = false; { notifyAll () ; } /// , No puede haber dos filósofos (objeto Philosopher) que tomen (con el método take()) el mismo palillo (objeto Chopstick) al mismo tiempo. Además, si el objeto Chopstick ya ha sido tomado por un objeto Phllosopher, otro filósofo podrá esperar (wait()) hasta que el objeto Chopstick quede disponible cuando su propietario actual invoque drop() (soltar palillo). Cuando una tarea Philosopher invoca take( ), esa tarea espera que el indicador taken (ocupado) valga false (es decir, hasta que el objeto Pbllosopher que actualmente posee el objeto Chopstick lo libere). Entonces, la tarea asigna al indicador taken el valor true para indicar que el nuevo objeto Philosopher posee ahora el objeto Cbopstick Cuando este objeto Pbilosopher haya tenninado de usar el objeto Chopstick, invocará drope ) para cambiar el indicador y llamará a notify AIl( } para notificar a todos los demás objetos Philosopher que puedan estar esperando a utilizar el objeto Chopstick ji : concurrency jPhilosopher. j ava /1 Un fil ósof o comensal import java.util.concurrent.*¡ import java.util.*; import static net.mindview.util,Print.*¡ public class Philosopher i mplement s Runnable private Chopstick 1ef t; private Chopstick right¡ private final int id; privat e fina l int ponde rFactorj private Random rand = new Random(47) ¡ pri vate void pause() throW9 InterruptedExcept ion if (ponderFactor == O) retu rn; Time Unit.MILLISECONDS.s leep ( rand.nextlnt (ponderFactor * 250 )) ; publi c Philosopher(Chopstick 1eft, Chopstick right, int iden t , int ponder ) { this.left : left¡ this.right = right; 21 Concurrencia 803 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 + left. take () ; print (this + pause () i right.drop() i left . drop () ; + "grabbing 1eft") i + Ueating H) i catch(InterruptedException el { print (this + JI n + lIexiting via interrupt ll ) i public String toString () { return nphilosopher 11 + id; } 111,En Philosopher.run( l, cada objeto Philosopher simplemente piensa y come de manera continua. El método pause( l efectúa una llamada a sleeps( l durante un período aleatorio si el factor ponderFactor es distinto de cero. Utilizando esto, vemos que el filósofo está pensando durante un intervalo de tiempo aleatorio y que luego intenta tomar los palillos izquierdo y derecho, comiendo durante un intervalo de tiempo aleatorio, y luego volviendo a repetir el ciclo. Ahora podemos escribir una versión del programa, versión que estará sometida al problema del interbloqueo: JI: concurrency/DeadlockingDiningPhilosophers.java /1 Ilustra cómo puede haber interbloqueos ocultos en un programa. II {Args, O 5 timeout} import java.util.concurrent.*; public class DeadlockingDiningPhilosophers public static void main(String[] args) throws Exception { int ponder = 5; if(args.length > O) ponder = Integer.parselnt (args [O] ); int size = 5; if(args.length > 1) size = Integer.parselnt (args [1] ); ExecutorService exec = Executors.newCachedThreadPool(); Chopstick[] sticks = new Chopstick[size]; for(int i = O; i < size; i++) sticks[iJ = new Chopstick() i for(int i = O; i < size; i++) exec.execute(new Philosopher( sticks [i] sticks [(i+1) % size] i ponder)); if (args .length == 3 && args [2] . equals ("timeout l1 ) } TimeUnit.SECONDS.sleep(5) ; el se ( System.out.println("Press 1 Enter 1 to quit l1 ) ; System.in.read() ; I J exec.shutdownNow() ; /* (Ej ecutar para ver la salida) * / / /:- J 804 Piensa en Java 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 pensando, 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 objetos Chopstick. Al último objeto Philosopher se le asigna el objeto Chopstick situado en la posición cero como palillo derecho, 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, quedando 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 cuatro 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 propiedad 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 producirse como un suceso normal. En otras palabras, nuestros filósofos son muy educados y no arrebatan los palillos 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 tareas 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 consiste en impedir que se dé la cuarta condición. Esta condición sucede porque cada filósofo trata de tomar los correspondientes 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 izquierdo, 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 inmediatamente a su derecha tome los dos palillos que necesita. En este caso, se impide la espera circular. Ésta sólo es una de las posibles 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.*¡ public class FixedDiningPhilosophers 21 Concurrencia 805 public s ta tic void ma i n (String [] a r gs ) t hrows Exception { i n t ponder = 5; if (a rgs .length > O) ponder = Integer.parse lnt (args (O ] ) i int s i ze = Si if(args. l ength > 1) size = Integer.parselnt (args [1 ] ) i ExecutorService exec = Executors.newCachedThreadPool() i Chopst ick [] sticks = n ew Chopstick [51ze] ; fo r(in t i = O; i < size; i ++) st i cks[i ] = new Chopstick (} i for (i n t i = O; i < siz e¡ i+T } if (i < (size- 1 ) exec.execute (new Phi losopher ( s ti c ks [i ] , sticks [i +1], i, ponder )) ; eI se exec . execute( new Ph i l osophe r ( st i cks [ O] sticks [i ] , i , p o nder ») ; if (a r gs.length == 3 && args[ 2] . e qua l s ( "t i me out ") ) TimeUnit.SECONDS . sleep(S ) ; f else { System . out . println (n Press System.in . re ad() i exec.shutdownNow() r Enter r to gui t!l) i i /* (Ej ecutar para ver la salida) * / / /:Garantizando que el último filósofo tome y dej e el palillo izquierdo antes que el derecho, eliminamos el interbloqueo y el programa funcionará sin problemas. No hay ningún soporte del lenguaje para ayudarnos a prevenir el interbloqueo; es un problema que deberemos resolver nosotros mismos realizando un diseño cuidadoso. Ya sé que estas palabras no sirven de mucho consuelo a aquellas personas que estén tratando de depurar un programa sometido al interbloqueo. Ejercicio 31: (8) Cambie DeadJockingDiningPhilosophers.java de modo que, cuando un filósofo haya tenuinado de emplear sus palillos, los deje en una bandeja. Cuando un filósofo quiera comer, tomará los dos palillos siguientes que estén disponibles en la bandeja. ¿Elimina esto la posibilidad del interbloqueo? ¿Podemos reintroducir el interbloqueo simplemente reduciendo el número de palillos disponibles? Nuevos componentes de biblioteca La biblioteca java.util.concurrent de Java SES introduce un número significativo de nuevas clases diseñadas para resolver los problemas de concurrencia. Aprender a emplear estas clases puede ayudamos a escribir programas concurrentes más simples y robustos. Esta sección incluye un conjunto representativo de ejemplos de diversos componentes, aunque algunos de los componentes no los analizaremos aquí (aquellos que es menos probable que el lector se encuentre a la hora de analizar programas o utilice a la hora de escribirlos). Puesto que estos componentes permiten resolver varios problemas, no existe una forma clara de organizarlos, así que trataré de comenzar con ejemplos sencillos y continuar con una serie de ejemplos de creciente complejidad. 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 recuenlo 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.*¡ // Realiza cierta parte de una tarea : cIass TaskPortlon i mplements Runnab le prívate static i nt counter = O; príva te f i nal int id ~ counte r++¡ prívate static Random rand = new Random(47)¡ private fi na l CountDownLatch latch¡ TaskPortion(countDownLat ch l a tch) { this.latch = latch¡ public void run () try { doWork() ; la tch .countDown()j catch (I nterruptedException ex) II Forma aceptable de salir public vo i d doWork () throws InterruptedException { TimeUni t. MILLISECONDS. s l eep(rand . nex tl n t(2 000») ; print (t h is + "compl eted ll ) ; public String toString () { return St ring . format (II %1 $ -3d id) i II Espera sobre CountDownLatch: class Wa i t ingTask implemen t s Runnable prívate stati c i n t count er = O; prívate fi nal int id = count er++¡ private final CountDownLatch l atch¡ Wait ingTask(CountDownLat ch l atch) { thi s. latch = latch¡ } public void run() { try { latch.awai t() ; pri nt ( "Latch b arr i er passed f or " + th i s); catch{Interr uptedExcept ion ex) print(this + !1 ínterrupted n ) i public Str ing toS tr ing () { 21 Concurrencia 807 return String . format (IIWa iti ngTask %1$-3d id ) ; public class CountDawnLatchDemo { s tatic final i n t SI ZE = 100; public s tatic void main(String[] argsl throws Exc e ption { ExecutorService exec = Executors.newCachedThreadPool( ) ; / / Todos deb e n compartir un único objeto CountDo wnLatch: CountDownLatch latch = new CountDownLatch(SIZEl¡ for(int i = O; i < 10; i+-1''' ) exec.execute(new WaitingTask(latch} ) i for(int i = O; i < S IZE¡ i + + ) exec.execute(new TaskPortion(latch»; print ( IILaunched a 11 tasks" ) i exec. shutdown{); JI Sal i r cuando todas las tareas se hayan c ompletado / * (E jec u tar para ver la sa l ida ) */// : TaskPortion duenne durante uu período aleatorio para simular la tenninación de parte del proceso, y WaitingTask indica que una parte del sistema tiene que esperar hasta que el problema inicial se haya completado. Todas las tareas funcionan con el mismo contador CountDownLatch, que se define en main( ). Ejercicio 32: (7) Utilice un objeto CountDownLatch para resolver el problema de correlación de los resultados de las distintas entradas de OrnamentalGarden.java. Elimine el código innecesario en la nueva versión del ejemplo. Seguridad de las hebras en la biblioteca Observe que TaskPortion contiene un objeto estático Random, lo que significa que puede haber múltiples tareas invocando Random.nextInt( ) al mismo tiempo. ¿Es esto seguro? Si existe un problema, puede resolverse en este caso asignando dos TaskPortion en su propio objeto Random, es decir, eliminando el especificado static. Pero esa misma cuestión seguirá siendo pertinente, con carácter general, para todos los métodos de la biblioteca estándar de Java: ¿cuáles de esos métodos son seguros de cara a las hebras y cuáles no lo son? Lamentablemente, la documentación del JDK no es muy explicativa en este aspecto. Resulta que Random.nextInt( ) sí que es seguro respecto a las hebras, pero es necesario descubrir en cada caso si un cierto métodolo es, efectuando una búsqueda en la Web o inspeccionando el código de la biblioteca Java. Evidentemente, esta situación no resulta particularmente atractiva para un lenguaje de programación que está diseñado, al menos en teoría, para soportar la concurrencia. 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 caballos escrito en BASIC y denominado (debido a las limitaciones de los nombres de archivos) HOSRAC.BAS. He aqulla versión orientada a objetos y con hebras de dicho programa, utilizando una clase CyclicBarrier: 11: concurrency/HorseRace.java 1I Utilización de Cycl icBarrier. 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 mediante el que se accedía a una computadora HP-lOOO. 22 808 Piensa en Java impor t java.util.concurrent.*¡ import java.util.*¡ i mport static net . mi ndview.util.Print.*¡ c I ass Harse implements Runnable { prívat e sta tic int counter = O; p rivat e final i nt id = counter++¡ private int strides = O: prívate static Random rand = new Random(47 ) ; private static CyclicBarrier barrier¡ public Ho rse (CyclicBarrier b) { bar rier = b; public synchroni zed int getSt ri des () { return strides; publ ic void r un( ) ( try ( while ( !Th read. i n terrupted() ) synchronized (this ) { s t rides += rand.nextInt(3)¡ barrier.await() JI Produce O, 102 i catch(InterruptedException el { /1 Una forma legí tima de sal ir catch(BrokenBarrierException el /1 Ésta queremos probarla throw new RuntimeException(e); publ ic String taString () { return IIHorse public String t racks () { StringBuilder s = new Str ingBuilder(); for( i nt i = O; i < getS tr i des() ; i++) s.append ( II *") i s . append (id) ; return s.toString () ¡ ti + id + " publ i c class Ho r seRace { static final i n t FINISH_ LINE = 7 5; private List harses = new ArrayLi s t() ; privat e ExecutorServi ce exec Executars.newCachedThreadPool() ; private CyclicBarrier b arrier; publi c HorseRace(int nHorses, final int pause) { barrier = new CyclicBarrier(nHorses, new Runnable () public void run() ( StringBuilder s = new StringBuilder(); for( i n t i = O; i < FINISH_LINE¡ i ++ ) s . append ( "=" ); / / La va l la en el hipódromo print (s ) ; f or(Horse harse : harses) pri nt (horse.tracks (») ; for(Harse h9rse : horse s) if (ho rse.getStri des () >= FINISH_LINE l print (harse + "won !" ); exec .shutdownNow() ; return; try TimeUn i t.MILLISECONDS .sleep(pause ) i 11 ¡ 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)¡ public static void main (String [] args) { int nHorses = 7 i int pause:;:: 200; if (args .length > O) { / / Argumento opcional int TI = new Integer(args[O]) i nRorses :;:: TI > O ? TI : nHorses¡ if (args .1ength > 1) { / / Argumento opcional int p new Integer(args[l]) j pause :;:: p > -1 ? P : pause¡ new HorseRace(nHorses, pause)¡ /* (Ejecutar para ver la salida) *///:Un objeto CyclicBarrier puede proporcionar una "acción de barrera", que es un objeto Runnable que se ejecuta automáticamente cuando el contador alcanza el valor cero; ésta es otra distinción entre Cyc1icBarrier y CountdownLatch. Aquí, la acción de barrera se defme mediante una clase anónima que se entrega al constructor de CyclicBarrier. Intenté que cada caballo pudiera imprimirse a sí mismo, pero el orden de visualización dependía del gestor de tareas. CyclicBarrier permite que cada caballo haga lo que necesita hacer para poder continuar adelante, y luego tiene que esperar en la barrera hasta que todos los demás caballos hayan avanzado. Cuando todos los caballos se han desplazado, CyclicBarrier llama automáticamente a su objeto Runnable que define la acción de barrera para mostrar los caballos por orden junto con la valla. Una vez que todas las tareas han pasado la barrera, ésta estará automáticamente lista para la siguiente ronda. Para obtener el efecto de una animación muy simple, reduzca el tamaño de la consola para que sólo se muestren los caballos. 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 situado 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 private final int de l t a; private f i na l long trigger; protected static Lis t< DelayedTask> sequence new ArrayList{) i public DelayedTask(int delayl nMilliseconds) delta = delaylnMilliseconds¡ trigger = System.nanoTime{) + NANOSECONDS.convert(delta, MILLI SECONDS); sequence.add{this) ; public l ong getDelay(TimeUnit unit) { return un it.convert( trigger - System.nanoTime () , NANOSECONDS ) ¡ public int compareTo (Delayed arg) { DelayedTask that = (DelayedTask) arg¡ if(trigger < that.trigger) return - li if (tr igger > that.trigger) return 1 ; return O; publ ic void run () { printnb (this + 11 11); public String toString () { return String.format{1I [%1$ - 4d] ", de lta) + " Task IJ + id¡ public String summary() return IJ (IJ + id + I I : ! I + delta + !I) 11 i publ ic static cIass EndSentinel extends DelayedTask { private ExecutorService exec¡ public EndSentinel(int delay, Execu t or Service e l { super (delay) j exec = e¡ public void run () ( for(DelayedTask pt : sequence) printnb (pt . summary () + 11 !I ) ¡ print () ; print (this + 11 Calling shutdownNow () exec.shutdownNow() ¡ II} ; c lass DelayedTaskConsumer implemente Runnable { private DelayQueue q¡ public DelayedTaskConsumer {DelayQueue q ) { this . q = q; public void run () try ( whi l e(!Thread.interrupted( )) q.take{) .run () ; // Ejecu tar tarea con la hebra actual catch(InterruptedException e) // Forma aceptable de salir print ("Finished DelayedTaskConsumer") j 21 Concurrencia 811 public class DelayQueueDemo { public static void main(String[] args) { Random rand ~ new Random(47); ExecutorService exec = Executors.newCachedThreadPool(); DelayQueue queue = new DelayQueue() i JI Rellenar con tareas que tengan retardos aleatorios: for{int i = O; i < 20; i++) queue.put(new DelayedTask(rand.nextlnt(500D))) i /1 Establecer el punto de detención queue.add(new DelayedTask.EndSentinel(SOOO, exec)} i exec.execute(new DelayedTaskConsumer(queue)) i /* 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 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 salida 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 ejecució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 prioridad 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.* ¡ class PrioritizedTask implements Runnable, Comparable pri vate Random rand = new Random(4 7 )¡ private stat ic int counte r = O¡ pri vate final int id = counter++i private fi nal int prioritYi protected static List sequence new ArrayList() ¡ pUblic Prior i tizedTask (i n t priority ) t h i s.priority = prioritYi sequenee.add (thi s) ; public int compareTo ( PrioritizedTasK arg ) return priority < arg.priority ? 1 : (priority > arg.priori ty ? - 1 : O) i publ i e void run ( ) try ( TimeUnit.MILLISECONDS. sleep(rand.next Int (250) ; catch(InterruptedExcep t i on el { II Forma aceptabl e de salir print (this) ; publ ic St r ing toStri ng () return String .format (II [% 1$ - 3dl " , priori t y ) + 11 Task 11 + id; public String summary () return " ( II + id + ":" + pri or ity + "l"; public static c l ass EndSent inel extends Prioriti zedTask private ExecutorService exec¡ public EndSentinel(ExecutorServi ce el super(-l); II La menor prioridad en este programa exec = e; publi c void run () int count = Oi for (PrioritizedTask pt : sequence ) { pri n tnb (pt .summary ( »¡ if( ++count % 5 == O) pri n t () ; print () ; print (t his + " Calling s hu tdownNow() " ) ; exec.shu t downNow () ; c l ass Priorit izedTaskProducer implements Runnable { private Random rand = new Random(47)¡ private Queue queue; 21 prívate Executo rService exec ¡ pub lic Pr i or i tizedTaskProduc er( Queue q, ExecutorServi ce e ) { queue :: q¡ exec = e¡ JI Utilizado para BndSentinel public void run() { /1 Cola no limitada; nunca se bloquea. JI Rellenarla rápidamente con prioridades aleatorias: for (in t i :: O; i < 20; i++ ) { queue . add (new PrioritizedTask(rand.nextlnt(lO») ¡ Thread. yie ld () ; /1 Introducir tareas de prior idad máxima: try { for(int i O; i < 10; i++ l TimeUni t .MILLISECONDS.sleep(2S0) i queue.add(new Prio rit izedTask( l O}); /1 Añadir tareas, primero l as de menor prioridad: for[ i n t i = O; i < 1 0 ; i+. ) queue . add(new PrioritizedTask(i)); JI Un centinela para detener todas las tareas : queue.add(new Priori tizedTas k.EndSenti ne l (exec)) ¡ catch( I nterruptedException e) { // Fo r ma acept ab l e de salir print ( " Fini s hed PrioritizedTaskProducer ll ) i class PrioritizedTaskConsumer implements Runnable private PriorityBlockingQueue q ¡ public PrioritizedTaskConsumer{ PriorityB l ockingQueue q ) { this.q = q¡ public void run () try { while(!Thread .interrupted()) // Utilizar la hebra actual para ejecutar la tarea: q. take () . run () ; catch ( Inter r uptedExc eption e) // Forma aceptable de sali r print(IIFinished PrioritizedTaskConsumer" ) ¡ public class PriorityBlockingQueueDemo { public static vo id main{String (] args) throws Exception { Random rand = new Random(47 )¡ Execut orServi ce exec = Executors . newCachedThreadPool( } ¡ Pri orityBlock ingQueue queu e = new Pri orityBlockingQueue ( ) ¡ exec.execute(new PrioritizedTaskProducer(queue, exec)); exec. execute(new PrioritizedTas kConsume r(queue)) ¡ / * (Ejecutar para ver la salida) * /// :- Concurrencia 813 814 Piensa en Java 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 sincronizació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. El controlador de invernadero implementado con ScheduledExecutor En el Capítulo 10, Clases internas, introdujimos el ejemplo de un sistema de control aplicado a un invernadero hipotético, donde se encendian y apagaban diversos elemenlos o se los ajustaba de alguna manera. Éste puede verse como un tipo de problema de concurrencia, siendo cada suceso deseado en el invernadero una tarea que se ejecuta en un instante predefinido. La cIase ScheduledThreadPooIExecutor proporciona el servicio necesario para resolver el problema. Utilizando schedule() (para ejecutar una tarea una vez) o scheduleAtFixedRate() (para repetir una tarea a intervalos regulares), configuramos objetos Runnable que haya que ejecutar en un cierto instante futuro. Compare el código siguiente con la técnica utilizada en el Capítulo 10, Clases internas, para observar cómo se simplifican las cosas cuando podemos emplear una herramienta predefinida como ScheduledThreadPooIExecutor: /1: concurrency/GreenhouseScheduler. java // Reescri tura de innerclasses/GreenhouseController.java // para usar la clase ScheduledThreadPoolExecutor. II {Args, SOOO} i mport java . u ti l .concurrent.* i import java.util.*; public class GreenhouseSchedul er private volatile boolean light false; private volatile boolean water false¡ pri vate String thermostat = "Day ll; public synchronized String getThermostat() return thermostat¡ public synchroni zed void setThermos tat(St ring value) { thermostat = value¡ ScheduledThreadPoo lExecutor scheduler = new ScheduledThreadPoolExecutor(lO) i public void schedule(Runnable event, long delay} { scheduler.schedule(event ,delay,TimeUnit.MILLISECONDS) i public void repeat(Runnabl e even t , long initialDelay , l ong periad) { schedu ler.scheduleAtFixedRate { event¡ initialDelay, period, TimeUnit.MILLISECONDS); class LightOn implements Runnable { public void run() { II Inclui r aquí e l código de control de l hardware II pa ra encender fís i camente la il umi nación. System.out.println ( II Turning o n l igh ts " ); light = true; class LightOff implements Runnable { publi c void run() { 21 Concurrencia 815 II II Incl u ir aquí el código de control del hardware para apagar físicamente l a i luminación. Sys t em. out. pri n tl n (IITurning off l ights") j light = false; class WaterOn implements Runnable ( public void run() { II Incluir aquí el código de control del hardware . System . out .println (IITurning greenhou se water onU) j water = true; class WaterOff implements Runnable { public void runll { II Incluir aquí el código de control del hardware . System. out .println ("Turning greenhous e water off U) ; water = false; class ThermostatNight impl emen ts Runnable public void run() { II Incluir aquí el código de cont rol del hardware . Syst em.out .println (IIThermostat to night setting Jl ) ; setThermostat ( IlNight n ) ; class ThermostatDay impl ernents Runnable ( public v o id run() ( II Incl u ir aquí el código de control del hardware. System.out.println(IIThermostat to day setting ll ) ; setThermostat (nDayll) ; class Bell i mplemen ts Runnable ( publi c void run() ( System.out.print l n {"Bing !" ); c l ass Termi nate implements Runnab l e ( public void run() ( Syste m. out .println (IITerminating lt ) ; schedul er.shutdownNow () j II Hay que iniciar una tarea separada para hacer este trabajo, II y a que el planificador ha sido terminado: new Thread Il ( public void run( ) ( for(DataPoint d : data ) System.out.prin tln {d ) i } } .startll; II Nu eva ca racter í stica: recopi l a ción de datos sta tic clas s DataPoin t ( final Calendar t ime; final float temperature; final float humiditYi public DataPoint{Calendar d, float temp, float hum) time '" di temperature = tempi { 816 Piensa en Java humidi t y '" h u m; public String toString( ) r eturn time .getTime() + String . format ( u t emperature : %1$ . lf humidity: temperatur e, h umidity) i %2$ .2f u , } prívate Calendar lastTime '" Calendar.getlnsta nc e() { 11 Ajustar la hora con medias h oras lastTime.se t (Calendar.MlNUTE, 30) i lastTime.set(Ca l endar . SECOND, 00) i j private floa t l astTemp = 65.0f¡ priv ate int tempDi r ection = +1; private float l as t Humidity = 5 0 .0f; private int h umidityDirect ion = +1 ; private Random rand = new Random(4 7); List data = Co11ections.synch ronizedL i st( new ArrayLi s t(}) ¡ c l as s Co ll e c t Data implements Runnable { public v oid run ( ) { System .out.print1n( UCollecting data U) ; syn c hronized(GreenhouseScheduler.this) 11 Si mular q ue el interva l o es mayor de lo que e s : last Ti me.se t(Calendar.M l NUTE, l as tT ime. get(Calendar.M I NUTE ) + 30); 1/ Una oportunidad entre 5 de invertir la d i r e cción: if ( rand.next lnt (5) == 4) tempD i rection = - tempDirectioD ; 1/ Almacenar va l o r ant erior: las t Temp = l astTemp + tempDi r ection * (l . Of + ran d. n ex t Float()) ; if (ran d .nextlnt (5 ) == 4 ) h umi dityDirec tion = -hum i d i tyDirection; l as t Humidity = lastHumidity + humi dityD irection * rand.nextFloat(); 1/ Es preci s o clonar Ca l endar, ya que en caso contrario todos 11 los puntos d e datos a l macenarían re ferencias al mismo v a l or 1/ l astTime. Para un objeto bás i co c omo Calendar , clone(} es OK. data .add(new Da t aPoint( (Ca l endar) las tTime.clone ( ) , l as tTemp, lastHumidity) i public s t atic void main(String[ ] args ) { Green houseSchedu l er g h = new GreenhouseSch eduler (); g h .schedul e(gh.new Terminate() , 5 000); // La ant er i or clase " Restart ll no es necesaria: gh.repeat(gh .new Be l l( ) , O, 1 0 00 ); gh.repeat(gh.new Th ermos tatNigh t() , O, 2000); g h .re peat(gh . new LightOn(), O, 200); gh.repeat(gh . new LightOff (}, O, 4 00) ¡ gh.repeat( g h .new WaterOn(), O, 600)¡ gh.repeat( gh.new WaterOff(}, O, SOO}; gh.repeat(gh.new ThermostatDay(), O, 14 0 0) ¡ gh.rep eat(gh.new Col l ectData(), 500, 500) i 1* (Ejecu tar para v e r la sali da) */11:_ 21 Concurrencia 817 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 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 pensar 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 terminado con ellos. Esta funcionalidad puede encapsularse en una clase genérica: ji: concurrency/Pool .java JI JI Utilización de un semáforo dentro de conjunto compart ido, 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.*¡ pubIi c c Ias s Poo l< T > private int size; prívate List i tems ~ new ArrayLíst() ; prívate volatíl e bool ean [J che ckedOut; private Semapho re a vailable; pub li c Pool{Class c lassOb ject, int size) this.size = size¡ checkedOut ~ new boolean[sizeJ i avail ab le ~ new Semaphore(si ze, tru e); II Cargar conj unt o con obje tos que p uedan ext ra erse: for {int i ~ O; i < s i ze¡ ++ i ) t ry { II Presupo n e un constructor predet erminado: items.add(classObject.newInsta nce()) i catch (Exc ept ion e) { throw new RuntimeExc eption(e ); publi c T c heckOut() throws Int erruptedExcepti o n { a vai l abl e.acquire() i return ge tl tem (); public vo id che ck ln(T xl if(rel easeltem (x) ) ava i lable.release()¡ private synchron ized T getlt e m() for (int i = O; i < size¡ ++ i) i f ( ! checkedOut [ill { checkedOut[i ] = t r ue; 818 Piensa en Java return i t ems .get (i) i } r eturn nul l ; JI El semáforo i mp i de l l ega r aquí private synchroniz ed bool ean releaseltem (T item) int index = items.indexOf (item); if(index == -l} return fal se; JI No está e n la lista if IcheckedOut [index] ) { checkedOut [indexJ = fal s e; return true; ret ur n f a l se; /1 No ha s ido ext r a ído En esta fonna simplificada, el constructor utiliza newInstance() para cargar de objetos el conjunto compartido. Si necesitarnos 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( ), available 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 bastante tiempo en ejecutarse: JI : concurren cy/ Fat. java JI Objetos que son cos t osos de crear. public cIass Fat { private vo l ati l e dauble di /1 Impedir optimización private stat i c int counter = O; priva te final i n t id = coun t er++¡ public Fat I l { II Operación costosa e interrumpible: for (i n t i = 1 ; i < 10 000; i++ } { d + " IMath. PI + Math .El / Idouh l e l i; public vo i d ope rationO { System .out.print l n(this) ¡ } public String toString () { return UFat id: u + id; } /// ,Agruparemos estos objetos en un conjunto compartido para limitar el impacto de su constructor. Podemos probar la clase Pool creando una tarea que extraiga objetos Fat, los conserve durante un cierto período de tiempo y luego los devuelva: 11 : concurrency/ SemaphoreDemo.java II Pr ueb a de la clase Pool import java.util.concurrent.*¡ import java.ut il.*¡ import static net .mindview.ut il .Print.*; II Una tarea para extraer un recurso de un conj unto compar tido: c lass CheckoutTas k impl ements Runnable privat e stat ic i nt counte r = O; pri vate final int id = counter++¡ private Pool< T> pool; public CheckoutTask(Pool pool) this. pool = pool; 21 Concurrencia 819 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 public String toStr ing () return "CheckoutTask 11 + id + 11 "; publi c c l ass SemaphoreDemo { fina l sta ti c i n t SIZE = 25; public static void main (String[] args) throws Exception { final Pool pool = new Pool (Fat.class, SIZE); ExecutorService exec = Executo rs .newCachedThre adPool()¡ fo r(int i = O; i < SIZE¡ i++} exec . execute (new CheckoutTask< Fat>{pool))¡ print ( Il A l l CheckoutTasks created ll ) i List list = new ArrayList(); f o r (int i = O; i < SIZE; i ++) { Fat f = pool.checkOut(); printnb{i + ": ma in () thread checked out 11 ) ; f . ope r at i on{); list. add(f) ; Future b l ocked exec . submit(new Runnable () { public void run () try { // El semáforo i mpide extracciones adicionales, // por lo que se bloquea l a llamada: pool.checkOut() ; catch(InterruptedException e) { print ("checkOut () Interrupted"); } }) ; 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(); /* (Ej ecutar para ve r la salida) * ///:En maio(), se crea un objeto Pool para almacenar los objetos Fa! y un conjunto de tareas CheckoutTask comienza a gestionar el conjunto compartido. Entonces, la hebra main() comienza a extraer objetos Fa! sin devolverlos. Una vez que ha extraído todos los objetos del conjunto compartido, el semáforo no pennitirá ninguna extracción adicional. El método run() de blocked se ve así bloqueado, y después de dos segundos se invoca el método cancel() para salir de Future. Observe que las extracciones redundantes son ignoradas por el objeto Pool. 820 Piensa en Java 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 conjuntos 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 ExchangerProducer y ExchangerConsumer utilizan una lista List como el objeto que hay que intercambiar; cada uno contiene un objeto Exchanger para este contenedor List. 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 contenedores List habrán sido intercambiados: /1 : concurrencyjExchangerDemo.java impo rt java.util.concurrent.*¡ import java.util .*¡ import net.mindview.util.*¡ class ExchangerProducer implement s Runnab le private Generator generator; pri vate Exchanger ho l der; ExchangerP roducer( Exchanger gen, List ho lde r) { exchanger = exchg; generator = gen ; this.ho lder = ho lder; p ublic void r un() { try { while(!Thread .interrupted()) for(int i = O¡ i < ExchangerDemo.size; i++l holder .add(generator.next()) i II Intercambiar el contenedor yacio por el lleno: holder = exchanger.exchange(holder); catch( I nterruptedExcept i o n el II OK terminar de esta forma . c l ass Exc hange rCon s umer implemen t s Runnable p r ivate Exchanger holder; private volatile T value¡ Exchange r Consumer(Exchanger
  • holde r ) { exchanger = ex; this.holder = holder; 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 . System.out.println("Final value: ..¡. I! value) j public c!ass ExchangerDemo static i nt size = 1 0; s tatic int delay = 5; // Segundos public static void main(String[] argsl throws Exception { if(args.length > O) size = new I n teger (args[ Q) ; if(args .length > 1) delay = n ew Integer(args[l]); ExecutorSe rvice exec = Executors.newCachedThreadPool () ¡ Exchanger producerLi st = new CopyOnWriteArrayList{), consumerLi st = new CopyOnWr iteAr rayList()¡ exec.execute (new ExchangerProducer{ xc, BasicGenerator.create{Fat.class) producerList)) exec.execute{ new ExchangerConsumer( xc,consumerList » ; TimeUnit.SECONDS.sleep(delay) i exec.shutdownNow{) ; J j / * 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 CopyOnWriteArrayList 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 programar. 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á diferente 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.*; JI Los objetos de sólo lectura no requieren sincroni zación : cIass Custorner { private final int serviceTime: public Customer (int tm) { serviceTime tm: } public int getServiceTime{) { return servi ceTime¡ publi c string toString () { return 11 (Ir + serviceTime + 11] n i ::=; ) /1 Mostrar a la fila de clientes cómo visualizarse: cIas s CustomerLine extends ArrayBlockingQueue public CustomerLine(int maxLineSize){ super (maxLineSize) i public Stri ng toString () if (t his. s i ze() == O) return 11 [Empty] 11 ; StringBuilder result = new StringBuilder {); for(Custome r customer : this) result.append{customer) i return result.toString() i II Añadir c lie ntes aleatori amen te a una col a: c lass CustomerGenerator implements Runnable ( private Cus tomerLíne customersj prívate statíc Random rand = new Random(47) public CustomerGenerator( CustomerLíne cq) ( customers = cq¡ i public void run () try { while ( !Thread.int errupted (» TimeUnít,MILLISECONDS.sleep(rand.nextlnt(300) ¡ customers.put(new Customer(rand.nextlnt(lOOO))) i catch(InterruptedException el { System, out .println ("CUstomerGenerator in terrupted I!) System. ou t . println ( "Cus t omerGenerator terminating l l ) class Teller implements Runnabl e, Comparable private static int counter = O; private final int id ~ counter++¡ i i 21 Concurrencia 823 1/ Clientes servidos durante este intervalo: private int customersServed = o; private CustomerLine customersj prívate boolean servingCustomerLine = true¡ public Teller(CustomerLine cq} { customers = cq¡ } public void run() { try { while(!Thread.interrupted()) Customer customer = customers.take() ¡ TimeUnit.MILLISECONDS.sleep( customer.getServiceTime()) j synchronized {this } { customersServed++¡ while(!servingCustomerLine) wa it () ; catchllnterr uptedException e) { System.out.println{this + lIinterrupted"}; System.out.println(this + Uterminating U); public synchronized void doSomethingElse() customersServed = O; servingCustomerLine = false¡ public synchronized void serveCustomerLine () assert ! servingCustomerLine: "already serving: " + this j servingCustomerLine = true; notifyAll () ; 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)¡ clasa TellerManager implements Runnable private ExecutorService exec; private CustomerLine customers¡ private PriorityQueue workingTellers new PriorityQueue (); prívate Queue tellersDoingOthe r Things new LinkedList() ¡ private int adjustmentPer iod ¡ privat e static Random rand = new Random(47 )¡ publ i c TellerManager(ExecutorService e, CustomerLine customers, int adj u stmentPeriod) exec = e¡ this.customers = customers¡ this.adjustmentPeriod = adjustmentPeríod¡ II Comenzar con un único cajero: Teller teller = new Teller(customers) ¡ exec.execute(teller)¡ workingTellers. add (teller) ¡ 824 Piensa en Java public void adjustTellerNumber() { II Esto es realmente un sistema de control. Ajustando II los números, podemos descubrir problemas de estabilidad II en el mecanismo de control. II Si la fila es demasiado larga, añadir otro cajero: if (customers.size () I workingTellers.size() > 2) { II Si los cajeros están descansando o haciendo II otra tarea, decir a uno que venga: if(tellersDoingOtherThings.size() > O) Teller teller = tellersDoingOtherThings.remove() ¡ teller.serveCustomerLine() ¡ workingTellers.offer(teller) ; return¡ II en caso contrario, crear (contratar) un nuevo cajero Teller teller = new Teller(customers) ¡ exec.execute(teller) i workingTellers.add(teller) i return; II Si la fila es lo suficientemente corta, eliminar un cajero: if(workingTellers.size() > 1 && customers. size () / workingTellers. size () < 2) reassignOneTeller(} ; II Si no hay una fila, sólo hace falta un cajero: if(customers.size() == O) while (workingTellers.size () > 1) reassignOneTeller() ; } II Dar a un cajero un trabajo diferente o un descanso: pri vate void reassignOneTeller () { Teller teller = workingTellers.poll() ¡ teller.doSomethingElse() ; tellersDoingOtherThings.offer(teller) ¡ public void run() { try { while(!Thread.interrupted() ) TimeUnit.MILLISECONDS.sleep(adjustmentPeriod) i adjustTellerNurnber() i System.out .print (customers + 11 { ") i for(Teller teller : workingTellers) System.out .print (teller. shortString () + 11 11); System. out .println ("} n) ¡ catch(InterruptedException e) { System.out.println(this + lIinterruptedn) i System.out.println(this + "terminating ll ) ; public String toString () { return lITellerManager JI i public class BankTellerSimulation { static final int MAX_LINE_SIZE = 50j static final int ADJUSTMENT PERlOD = 1000; public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); II Si la fila es muy larga, los clientes se irán: CustomerLine customers = 21 Concurrencia 825 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() } /* Ou t put: [429 ] [861] [575] [984] [2 00] [258] [342] [8 10 ] i (Sample) [2 07] { T O T1 } [140] [322] { TO T1 } [8 04] [826] [8 96 ] [ 984 ] { TO T 1 T2 } [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 completado, 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 cajero 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 agregar 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 solicitudes 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 enumerados. 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 funciona 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.*; /1 Se entrega al camarero, que a su vez se lo da al: class Order { 11 (un objeto de transferencia de datos) private s tatic int counter = Di private final int id = counter++¡ private final Customer customer¡ private final WaitPerson wai tPerson¡ private fina l Food foad; publie Order{Customer elist, WaitPerso n wp, Food f ) { customer = cust; waítPersan = wp¡ food = fi public Food item () { ret urn faod; } publie Custamer getCustomer() { return eustomer¡ } public WaitPerson getWait Person() { return waitPerson; public String taString () { return II Order : 11 + id + 11 item: 11 + food + for: " + custamer + 11 se rved by: n + waitPersan j 11 Esto es l o q ue el chef devue l ve: c l ass Pl ate { private final Order order; prívate final Food food; public Pl ate(Order ord, Food f ) { a rder::: o rd¡ food = f ¡ publ ic Order getOrder() { return arder; } public Foad getFood() { return food; } public String toString() { return food.toString(); class Cus tomer implements Runnabl e { 21 private static int counter = o: private final int id = counterT+¡ private final Wa i tPerson waitPerson; II Sólo se puede recibir un p l ato cada vez: private SynchronousQueue placeSetting new SynchronousQueue ()¡ public Customer(WaitPerson w) { waitPerson = W¡ public void deliver(Plate p ) throws InterruptedExcept ion II S610 se bloquea si el cliente está todavia II comiendo el plato anterior: placeSe t ting.pu t (p)¡ public void run() { for (Cour se caurse : Caurse. values ()) { Faod toad = course.randomSelection() i try ( waitPerson.placeOrder(this, foad); II Se bloquea hasta que se entregue e l plato : pr i nt (this + "eating 11 T placeSetting. take () ; catch (InterruptedException e) { print (this + Uwait ing far " + caurse + 11 interrupted " ) ; break: print (this + uf i nished meal, public S t ring toString() return "Custarner 11 + id + !I l eav i ng"); n¡ c lass WaitPerson implements Runnable private sta t ic int counter = O; private fina l int id = counter++¡ priva te fina l Res t aurant rest aurant; BlockingQueue filledOrders = new LinkedBlockingQueue() ¡ public Wait Person(Restaurant rest) { restaurant = rest¡ public void placeOrder(Custamer eust , Food faad) { try ( II No debería bloquearse en la práctica, porque es una cola II Li nkedBlocki ngQueue sin límite de tamaño: restaurant.orde rs. put(new Order(cus t, this, food» ¡ catch {InterruptedExcept i on e) { print (this + " placeOrder interrupted"); public void run() { try { while ( ! Thread. interrupted ( ) ) II Se bloquea hasta que está listo un plato Plate plate = filledOrders. t ake( )¡ print (this + nreceived I! + plate + 11 delivering to 11 + plate.getOrder() .getCustomer(»); plate.getOrder() .getCustomer() . delive r(plate) ¡ Concurrencia 827 828 Piensa en Java c atc h(Interrupte dExcep t ion e l print (thi s + pri nt(th i s + i n ter rupted"); 11 o f f duty") i publ ic String t oSt ring() return "Wai tPerson " + id + " 11 i class Chef implements Runnable { private static int c ounter = O; prívat e final int id = count er++¡ private fina l Res taurant r estaurant; p rívat e static Random r and = n ew Random( 47 ) ; public Chef(Restaur ant r est) { restaurant = rest; public void run() { try { whi l e ( ! Thre ad. interrupted () ) // Se bloquea hasta que apa r ece un pedido: Order arder = restaurant.orde rs.take (); Food reque s tedltem = arder .item (); JI El tiempo para preparar e l pedido: TimeUnit.MILLISECONDS.sleep(rand.nextlnt(SOO)) j PI ate plate = new PI ate (order , requestedltem) ¡ order.getWai t Person() .f illedOrders . put (plate) i catch(InterruptedExcept i on el pri nt (thi s + 11 inte rrupted" ) i print (thi s + off duty" ) i public St r ing toS tri ng () { return I1Chef 11 + id + u u i class Restaurant implements Runnable { p r í v at e List wait Persons new ArrayList() i private Lis t chef s = new ArrayList () j p r í vate ExecutorServ ice exec; private static Random r and = n ew Random(47); Bl oc kingQueue orde rs = new Linke dBloc kingQueue() ; public Restaurant{ Execut orService e, i nt nWait Persons, i n t nChefs) { exec = e; for(in t i = O; í < nWai tPersons; i ++) { Wai tPerson wai tPers on = new WaitPerson(this)¡ wai tPersons. add (waitPe r son) ; exec. execute (waitPers onl i for(int i = O; i < nChefs; i ++ ) Chef chef = new Che f(this); chefs .add(chef) ; exec .execute (chef ) ; publi c vo id run() { try { whi le (!Thr ead. interrupted() ) 21 Concurrencia 829 // Ll ega un nuevo client e; asignar un camarero: WaitPerson wp = waitPersons.get( rand.nextlnt(waitPersons.size() ) i Customer e = new Customer(wp) i exec .execute (c) ; TimeUnit.MILLISECONDS.sleep (l OQ ) i catch(InterruptedException el print ("Res taurant { i n terrupted " ) ; print ("Restaurant closing") i } public class RestaurantWithQueues { public static void main (String[] args ) throws Exception { Executo rS ervice exec = Executors .newCachedThreadPool (); Restauran t r estaurant = new Restaurant (exec, 5, 2); exec.execute(restaurant) ; if {args ,lengt h > O) ji Argumento opcional Ti meUnit.SECONDS.sleep(new Integer(args[O ) )¡ else ( print(IIP res s 'Ent er' to quitn) i System.in.read() ; 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 tareas. 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 tiona 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 (Customer) 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 hipotética línea de montaje robotizada para automóviles. Cada objeto automóvil (Ca r) será construido en varias etapas, comenzando 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 JJ : concurrencyJ CarBuil der. j ava II Un ejemplo complej o de ta reas que funcionan conj unt amente. impert java.util.concurrent.*¡ impert java.util.*¡ import static net.mindvi ew.util.Print.*¡ cl ass Car priva te fi nal int id; private boolean engi ne ~ false, driveTrain = fal se , wheels publi c Car (int idn) IJ {id = idn; fa lse ¡ } Objeto Car yac i o: public Car () {id = -1; } publi c synchronized int getId () { re turn id¡ } public synchron ized vo id addEngine() { engine publ ic synchronized void addDriveTra i n () { d riveTrain = tru e; true¡} public synchronized vo id addWheels (} { wheels = true; publ i c synch ron ized String toString () { return IICa r 11 + id + H [11 + 11 engine: 11 + engine + drive Train: 11 + drive Train + 11 wheels: 11 + wheels + 11 ] 11 ; cIass CarQueu e extends Li nkedBlockingQu eue {} cIass ChassisBuilder implement s Runnable { private CarQueue carQueuej private i nt coun ter = O; publ ic ChassisBuilder(CarQueue cq) { carQueue publi c void run() { cq; } t ry ( while(!Thread.interrupted (» TimeUnit.MILLISECONDS . s leep( SOO) ¡ II Hacer chas i s : Car c = new Car (counter++)¡ print ("ChassisBuilder created " + e) II Inse rtar en la cola carQueue.put(c) ; j catch(InterruptedExc ep tion e } { print (11 I nte rrupt ed : Chass isBuilder") ¡ print ( "ChassisBuilder off " ) ¡ cIass Assembler impl ement s Runnable { private CarQueue chass isQueue, finis h ingQueue ¡ private Car car¡ priva te CyclicBarri er barri er = new CyclicBar rier (4)¡ p r ivate RobotPool robotPool¡ public Assembler(Car Queue cg, CarQueue fg, RobotPool rp){ chassisQueue = cq; fi nishingQueue = fq; robot Pool = rp; 21 public Car car () { return car; } public CyclicBarrier barrier ( ) { return barrier i } public void run() { try { while(!Thread.interrupted()) // Se bloque hasta que esté disponible un chasis: car = chassisQueue.take() ¡ // Comprar robots para realizar el trabajo: rObotPoo l.hire(EngineRobot.class, this); robotPool.hire(DriveTrainRobot.class, this); robotpool.hire(WheelRobot.class, this); barrier.await() i / / Until the robots finish // Insertar coche en la cola de acabado (fi n ishingQueue ) // para trabajos adicionales finis hingQueue.put(car}¡ 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); print ("Assembler off") j class Reporter implements Runnable { private CarQueue carQueue; p ublic Reporter(CarQueue cq ) { carQueue c q; ) public void run() { try { while ( !Th read. i n terrupted( }) print(carQueue.take (» ; catch ( InterruptedExcept i o n e l { print (UExiting Reporter via i nterrup t ll ); pr i nt (UReporter off n) ¡ abstract c lass Robot implements Runnabl e { prívate RObotPool pool; public Robot(RobotPool p) { pool. p; ) p r otected Assembler assembler¡ public Robot assignAssembler(Assembler assembler) this.assembler = assemb l er¡ ret urn this¡ } p r ívate boolean engage = fals e¡ publ ic sync hronízed void engage () engage = tru e ; notifyAll () ; // La parte de run() que es diferente pa r a cada robot: abstract protected void performServi c e(); public void run() { try { powerDown ()¡ // Esperar hasta que haga falta Concurrencia 831 832 Piensa en Java 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 ) ¡ pri nt(this + 11 off"); private synch ronized void powerDown() throws InterruptedException engage = fa l se; assembler = nuIl; II Desconectar del ensambl ador (Assembler) II Volver a ponernos en la cola de disponibles: pool . re l ease( this) ; whi le(engage == f alse) 11 Desconectar alimentación wait () ; public String toS t ring () { return getClass() . getName( ) ; } class EngineRobot ex tends Robot { public EngineRobot(RobotPool pool) { super (pool) ¡ protected void performService () { print (thi s + " installing engine"); assembler.car() .addEngine ()¡ class DriveTrainRobot extends Robot { public DriveTra i nRobot(RobotPool pool ) { super (pool ) protected void performService() { print (this + !I i ns t alling DriveTrain ll ) ; assembler.car() . addDr iveTra i n () ¡ j class WheelRobot ext ends Robot { pUblic WheelRobot (Robot Pool poo l ) { super(pool)¡ protected void p e rformService() { print (this + n ins t a ll ing Wheels lJ ) ; assembler.car( ) .addWh eels (); class RobotPool ( II Impide sileciosamente que e x istan entrada idén ticas : private Set pool = new HashSe t () ; public synchroni zed void add(Robot r) { pool.add(r) i not if yAll () ; pUblic sync hron ized void h ire(Class robot~ypeJ Assemb l er d) 21 Concurrencia 833 thr ows I nterruptedExcept i on { f or(Robot r : pool ) i f (r.getClass (1 . equals (robotTypel I pool.remove(r) i r.assignAssembler(dl i r.engage()¡ /1 Encenderlo para reali zar l a tarea ret urn; wait( ) i 1/ Ni nguno dispon ible hire( robot Type, d); // Intentar de nuevo re cursivamente public synchroni zed void release(Robo t r) { add(r); } publi c c lass CarBui lder { args) throws Exception { CarQueue c hassisQueue = new CarQueue(), finishingQueue = new CarQueue() ¡ Execut orServi ce exec = Executors.newCachedThreadPool () ; RobotPool robotPool = new RobotPool( ) ¡ exec.execute (new EngineRobo t(robotPoo l )} ; exec. execut e(new DriveTrainRobot(robotPool)); exec.execute(new WheelRobot(robot Poo l))j exec. execut e(new Assembler( chass i sQueue, f i nishi ngQue ue, robotPool )) i exec.execu t e(new Reporter( f inishingQueue ) ) ; // Comenzar a funci onar produci endo un chasi s: exec. execute(new ChassisBuilder(chas sisQueue}}; TimeUnit .SECONDS.sleep(7) i exec. shutdownNow() i public stat ic void main (String [J / * (Ejecuta r para ver l a salida) *///:Los objetos Car se transportan de un lugar a otro mediante una cola CarQueue, que es un tipo de LinkedBlockingQueue. Un objeto ChassisBuilder crea un chasis de coche y lo coloca en una cola CarQueue. El objeto Assembler extrae los objetos Car de una cola CarQueue y adquiere objetos Robot para trabajar con ellos. Una barrera CyclicBarrier permite que Assembler espere hasta que todos los objetos Robot hayan terminado, en cuyo momento coloca el objeto Car en la cola CarQueue de salida para transportarlo a la siguiente operación. El consumidor de la cola CarQueue final es un objeto Reporter, que simplemente imprime los datos del objeto Car para demostrar que las tareas se han completado apropiadamente. Los objetos Robot se gestionan mediante un conjunto compartido y cuando hace falta realizar un trabajo, se extrae el objeto Robot apropiado de ese conjunto. Después de completar el trabajo, el objeto Robot se devuelve al conjunto compartido. En main( ), se crean todos los objetos necesarios y se inicializan las tareas, inciando ChassisBuilder en último lugar para co menzar con el proceso (sin embargo, debido al comportamiento de la cola LinkedBlockingQueue, no importaria si iniciáramos la tarea de construcción del chasis en primer lugar). Observe que este programa se ajusta a todas las directrices relativas al tiempo de vida de los objetos presentadas en este capítulo, de manera que el proceso de terminación resulta seguro. Observará que Car tiene defmidos todos sus métodos como synchronized. En realidad, en este ejemplo, esto es redundante, porque dentro de la fábrica, los objetos Car se desplazan a través de colas y sólo una tarea puede estar trabajando en un cierto coche en un deteITI1inado momento. Básicamente, las colas fuerzan a realizar un acceso serializado a los objetos Car, Pero ésta es exactamente el tipo de trampa en la que podemos caer; podemos decir "Tratemos de optimizar el programa no sincronizando la clase Car, porque parece que no es necesario". Pero posteriormente, al conectar este sistema a otro que sí que necesite que el objeto Car esté sincronizado, el sistema no funcionará. Brian Goetz comenta: Resulta mucho más fácil decir: "Car podría ser usado en múltiples hebras, así que hagamos que sea seguro de cara a las hebras de la forma más evidente". La manera en que podemos caracterizar este 834 Piensa en Java 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. Optimización del rendimiento Muchas de las clases de la biblioteca java.util.coucurrent de Java SE5 tienen el propósito de mejorar el rendimiento. Cuando examinamos de manera somera la biblioteca concurrent, puede resultar difícil discernir qué clases están pensadas para una utilización normal (como por ejemplo BlockingQneue) y cuáles otras se usan exclusivamente para mejorar el rendimiento. En esta sección, vamos a examinar algunos de los problemas y de las clases relativos a las técnicas de optimización del rendimiento. Comparación de las tecnologías mutex Ahora que Java incluye la antigua palabra clave synchronized junto con las nuevas clases Lock y Atomic de Java SE5, resulta interesante comparar las diferentes técnicas para poder comprender mejor las ventajas de cada una y saber cuándo emplearlas. La técnica más simple consiste en intentar una prueba sencilla de cada técnica, como la siguiente: //: concurrency/SimpleMicroBenchmark.java // Los peligros de las micropruebas. import java.util.concurrent.locks.*i abstract class Incrementable { protected long counter = Di public abstract void increment()¡ class SynchronizingTest extends Incrementable { public synchronized void increment() { ++counter; class LockingTest extends Incrementable { private Lock lock = new ReentrantLock() ¡ public void increment() { lock.lock() ; try { ++counter¡ finally ( lock.unlock() i public class SimpleMicroBenchmark { static long test(Incrementable incr) long start System.nanoTime()i for(long i = Di i < lOOOOOOOL¡ i++) 21 Concurrencia 835 incr,increment() i return System.nanoTime() - start; public static void main(String[] args) long synchTime = test(new SynchronizingTest())¡ long lockTime = test (new LockingTest())¡ System.out.printf (Usynchronized: %1$10d\n", System. out .printf (tlLock: %1$10d\n ll synchTime) 1 j lockTime); System.out.printf(!1Lockjsynchronized = %1$.3fll, (double) lockTimej (doublel synchTime) ; /* Output: synchronized: Lock: (75% match) 244919117 939098964 Lock/synchronized = 3.834 *///,Podemos ver, analizando la salida, que las llamadas al método synchronized parecen ser más rápidas que la utilización de un bloque ReentrantLock. ¿Qué es lo que está sucediendo? Este ejemplo ilustra los peligros de las denominadas "micropruebas de rendimiento".23 Generalmente, este ténnino se refiere a la realización de pruebas de rendimiento de una característica aislada, fuera de contexto. Por supuesto, sigue siendo necesario diseñar pruebas para verificar enunciados como "Lock es mucho más rápido que synchronized". Pero tenemos que ser conscientes de lo que está sucediendo realmente durante la compilación y en tiempo de ejecución a la hora de escribir estos tipos de pruebas. Existen diversos problemas en el ejemplo anterior. En primer lugar, sólo podremos ver la verdadera diferencia derendimiento si los mutex están contendiendo, así que tiene que haber múltiples tareas intentando acceder a las secciones de código protegidas por el mutex. En el ejemplo anterior, cada mutex se comprueba mediante la única hebra main( ) aislada. En segundo lugar, es posible que el compilador realice optimizaciones especiales al ver la palabra clave synchronized, y que incluso se percate de que este programa tiene una sola hebra. El compilador podría incluso identificar que el contador counter simplemente se está incrementando un número fijo de veces, y limitarse a precalcular el resultado. Existen muchas variaciones entre los distintos compiladores y sistemas de ejecución, así que resulta dificil saber exactamente qué es lo que sucederá, pero necesitamos impedir que el compilador pueda llegar a predecir el resultado de los cálculos. Para diseñar una prueba válida, debemos hacer el programa más complejo. En primer lugar, necesitamos múltiples tareas, y no sólo tareas que modifiquen valores internos, sino también tareas que lean esos valores (en caso contrario, el optimizador podría darse cuenta de que los valores no están siendo utilizados nunca). Además, el cálculo debe ser complejo y lo suficientemente impredecible como para que el compilador no tenga la posibilidad de realizar optimizaciones agresivas. Conseguiremos esto precargando una matriz de gran tamaño con valores enteros aleatorios (la precarga reduce el impacto de las llamadas a Random.nextInt() en los bucles principales) y utilizando esos valores en un sumatorio: 11: concurrency/SynchronizationComparisons.java II Comparación del rendimiento de objetos Lock y Atomic II explícitos y la palabra clave synchronized. import import import import import java.util.concurrent.*¡ java.util.concurrent.atomic.*¡ java.util.concurrent.locks.*¡ java.util.*¡ static net.mindview.util.Print.*¡ abstract class Accumulator { public static long cycles = 50000L¡ II Numero de modificadores y lectores durante cada prueba: private static final int N = 4¡ public static ExecutorService exec = 23 Brian Goetz me ayudó mucho, explicándome estos problemas. Consulte su artículo en -www128.ibm.com/developerworks/library(j-jtp12214paraconocer 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( } ¡ publi c abs trac t void accumulate() i publi c abstract long read(); prívate c lass Modif ier implements Runnable pub l ic void r un() ( for( l ong i = Di i < cycles; i+ +) accumula t e () ; try ( barrier.await() ; catch(Exception el t hrow new RuntimeException(e) ; private c l ass Reader impl ements Runnable { private vola ti le l ong va l ue¡ public vo i d run () ( for(long i = O; i < cyc le s ¡ i++) va l ue = read () ; try ( barrier.awai t() ; catch(Exception e) t h row new RuntimeException (e ) ; 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) d uration = System. nanoTime( ) ¡ - start¡ printf ( II %-13s: %13 d \ n " , id , d uration ) ; 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 cIass BaseLine extends Accumulator { id = lIBaseLine"; } public void accumulate(} { value += preLoaded[index++] i if(index >= SIZE) index public long read() = O; { return value¡ cIass SynchronizedTest extends Accumulator { id = Usynchronized ll ; } public synchronized void accumulate() value += preLoaded[index++J i if(index >= SIZE) index = O; public synchronized long read() return value¡ class LockTest extends Accumulator { { id = "Lock"; } private Lock lock = new ReentrantLock() public void accumulate() { lock.lock() ; i try { value += preLoaded[index++l; if(index >= SIZE} index = Oi finally { lock. unlock () i public long read() lock .lock () ; try { return va!ue¡ finally { lock.unlock() ; cIass AtomicTest extends Accumulator { { id = I1Atomic" ¡ } private Atomiclnteger index = new Atomiclnteger(O) ¡ private AtomicLong value = new AtomicLong(O)¡ public void accumulate() { II ¡Vaya! Depender de más de un objeto Atomic a la vez II no funciona. Pero sigue dándonos un indicador de II rendimiento: int i = index.getAndlncrement()¡ value.getAndAdd(preLoaded[i]) ¡ if (++i >= SIZE) indexo set (O); public long read() { return value.get()¡ public class SynchronizationComparisons { static BaseLine baseLine = new BaseLine(); static SynchronizedTest synch = new SynchronizedTest()¡ Concurrencia 837 838 Piensa en Java static LockTest lock = new LockTest{ ) ¡ static AtomicTest atomi c = n ew AtomicTest(); static void test () print(" == =============== ===========") i printf (11%-125 : %13d\n", "Cycles ll , Accumulator.cycles) baseLine.timedTest() ; synch.timedTest() i lock.timedTest{) ; atomic.timedTest() i Accumulator.report(synch, baseLine); Accumulator.report(lock, baseLine); Accumulato r.report(atomic, baseLine) i Accumulato r.report(synch, lock) i Accumulator.report{synch, atomic); Accumulator.report {lock, atomic); i public static vo id main(String( ] args ) int iterations 5; II Predeterminado if(args. l ength > O) II Cambiar opc i onalmente las iteraciones iterations = new Intege r( args[O ]}¡ JI La primera vez r e l lena el conjunto compartido de hebras: print ( nWarmup" ) ; baseLine.timedTest( ) ; II Ahora la prueba i nicial no incluye el coste de II iniciar las hebras por primera vez. II Generar múltiples puntos de datos: for(int i = O; i < iterati onsi i++) test () ; Accumulator.cyc les *= 2; Accumul ator.exec .shutdown() ; /* Output, Warmup BaseLine (Sample) 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 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 *///,- Concurrencia 839 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 clases 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 principio 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ásicamente, si hay más de un objeto Atomic implicado, probablemente nos tengamos que dar por vencidos y utilizar mutex más convencionales (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 eficiente 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 optimizaciones 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 tambié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 primer 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áctica, las secciones protegidas con mutex pueden tener un tamaño mayor que en el ejemplo anterior, por 10 que el porcentaje 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ódigo 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 24 Véase Thinking in Patterns en www.MindView.nel. 21 Concurrencia 841 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 sentido comenzar utilizando la palabra clave synchronized y cambiar únicamente a objetos Lock si la optimización del rendimiento 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 independiente de todos los demás objetos. Resulta más seguro comenzar con otras técnicas más tradicionales de mutex y sólo tratar de cambiar a Atomic posteriormente, si así lo exige la optimización del rendimiento. Contenedores libres de bloqueos Como hemos indicado en el Capítulo ll, Almacenamiento de objetos, los contenedores son una herramienta fundamental en el campo de la programación y esto incluye, por supuesto, la programación concurrente. Por esta razón, contenedores primitivos como Vector y Hashtable tenían muchos métodos synchronized, que hacían que se incurriera en un coste inaceptable cuando no se los estaba utilizando en aplicaciones multihebra. En Java 1.2, la nueva biblioteca de contenedores no estaba sincronizada, y a la clase Collections se le añadieron varios métodos de decoración estáticos "sincronizados" para sincronizar los distintos tipos de contenedores. Aunque esto representaba una mejora, porque nos daba la posibilidad de usar o no la sincronización dentro de nuestros contenedores, el coste asociado sigue estando basado en los bloqueos de tipo synchronized. Java SE5 ha añadido nuevos contenedores específicamente para incrementar el rendimiento en aplicaciones que sean seguras con respecto a las hebras, utilizando inteligentes técnicas para eliminar el bloqueo. La estrategia general que subyace a estos contenedores libres de bloqueo es la siguiente: las modificaciones de los contenedores pueden tener lugar al mismo tiempo que las leclmas, siempre y cuando los lectores sólo puedan ver el resultado de las modificaciones completadas. Cada modificación se realiza en una copia separada de la estruclma de datos (o en ocasiones en una copia separada de toda la estructura) y esta copia es invisible durante el proceso de modificación. Sólo cuando la modificación se haya completado se intercambia atómicamente la estructura modificada por la estruclma de datos "principal", después de lo cual los lectores podrán ver la modificación. En CopyOnWriteArrayList, una escritura hará que se cree una copia de toda la matriz subyacente. La matriz original sigue existiendo para que puedan realizarse lecturas seguras mientras se está modificando la matriz copiada. Una vez que se ha completado la modificación, una operación atómica intercambia la nueva matriz por la antigua de modo que las nuevas lecturas podrán ver la información. Una de las ventajas de CopyOn WriteArrayList es que no genera excepciones ConcurrentModificationException cuando hay múltiples iteradores recorriendo y modificando la lista, as! que no hace falta escribir código especial para protegerse frente a tales excepciones, a diferencia de lo que ocurria en el pasado. CopyOnWriteArraySet utiliza CopyOn WriteArrayList para conseguir un comportamiento libre de bloqueos. ConcurrentHasbMap y ConcurrentLinkedQuene emplean técnicas similares para permitir leclmas y escrituras concurrentes, pero sólo se modifican partes del contenedor en lugar del contenedor completo. Sin embargo, los lectores seguirán sin ver ninguna modificación antes de que éstas estén completadas. ConcurrentHasbMap no genera la excepción ConcurrentModificationException. 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 public abstract clas s Test er static int testReps = 10; static int testCycles = 1000; static int containerSize = 100 0 ; abstract e c ontainer l nitializer(); abstract void startReadersAndWriters()¡ C tes t Container; String test l d; int nReaders; int nWriters ¡ volati le l ong readResult = O; volatil e long readTime = O; volatile long writeTime = O; CountDownLatch endLatch; static ExecutorService exec = Executors.newCachedThreadPool () ; Integer[] writeData ¡ Teste r(String testld, int nReaders, int nWrit ers) this. testld = testld + " " + nRe aders + "r 11 + nWriters + Ilw ll i this.nReaders = nReaders; this.nWriters = nWritersj write Data = Generated.array (Integer .class, new RandomGenerator. Integer (). , c ontainerSize ); for(int i = O; i < testReps; i+ +) { rllnTest () ; readTime = O; writeTime = O; 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"} ¡ System. out .printf("%-27 s %14d %14d\n", test l d, readTime, writeTime) ¡ if(readTime != O && writeT i me != O) System.out .printf ( "%- 275 %14d\ n", "readTime + writeTime =", readTime + wr i teTime ) ¡ abst ract c lass TestTask implements Runnable abstract vo id test(} ¡ abst ract void putResult s(); long duration ¡ public void run () l ong startTime = System.nanoTime (} ¡ test () ; duration = System .nanoTi me () - startTime¡ synchron ized (Tester. this ) { putResults(} ¡ endLatch.countDown() ; 21 Concurrencia 843 public static void ini tMain{String (] args) { if(args.length > O) testReps = new Integer(args [O] ) ; if(args.length > 1 ) testCycles = new I n teger (args[l J); if {args.length > 2) containerSi ze = new Integer{args(2]); System.aut.print f(I!%- 27s %14s %14s \n ll , uType 11 , I!Read timen J IIWrite timen); } /// ,El método abstracto containerlnitializer( ) devuelve el contenedor inicializado que hay que probar, que se almacena en el campo testContainer. El otro método abstracto startReadersAndWriters( ), inicia las tareas lectora y escritora que leerán y modificarán el contenedor que estemos probando. Se ejecutan diferentes pruebas con un número diferente de lectores y escritores para ver los efectos de la contienda de bloqueo (para los contenedores basados en synchronized) y de las escrituras (para los contenedores libres de bloqueos). Al constructor se le proporciona diversa información acerca de la prueba (los identificadores del argumento deben ser autoexplicativos), después de lo cual invoca el método runTest() un número de veces igual al valor repetitions. runTest() crea un contador CountDownLatch (para que la prueba pueda saber cuándo se han completado todas las tareas), inicializa el contenedor, llama a startReadersAndWritcrs( ) y espera hasta que todas las tareas se hayan completado. Cada clase lectora o escritora está basada en TestTask, que mide la duración de su método abstracto teste ), y luego llama a putResults( ) dentro de un bloque de tipo synchronized para ahuacenar los resultados. Para usar este sistema (en el que podemos reconocer el patrón de diseilo basado en el método de plantillas), debemos heredar de Tester para el tipo de contenedor concreto que queramos probar, y proporcionar las apropiadas clases Reader y Writer: / /: concurrency/ ListComparis,o ns . j ava ~o} (Prueba ráp i da de verificación durante la construcción) / / Compa rac i 6naproximada del rendimi ento de listas compatibles con hebras. import java.util.concurrent.*¡ i mport java. ut i l.*; impert net.mindview.uti l .*¡ 1/ {Args: 1 10 abstract class ListTest extends Tester
  • containerlnitializer ( ) { return Collections.synchronizedList( new ArrayList ( new CountinglntegerList (cont ainerSize» ); SynchronizedArrayListTes t( int nReaders, int n Wri ters ) super ( "Synched ArrayList", nReaders, nWri t ers); class CopyOnWri teArrayListTest extends ListTes t List containerlnitializer () { return new CopyOnWriteArrayList( ne w Counting l ntegerL ist(containerS ize) i CopyOnWriteArrayLi s tTest(int nReaders, int nWri ters ) super ( "CopyOnWri t eArrayList ", nReaders , nWriters) i public c l ass Lis t Comparisons { public static vo id main(String[] args) Tes ter. initMain (args) { i new Synchroniz edArrayListTest(10, O) new SynchronizedArrayListTest(9, 1); new Synch ronizedArrayListTest(5, 5 ); new CopyOnWri teArrayListTe st(10, O) i new CopyOnWriteArrayListTest(9, 1) ¡ new CopyOnWriteArrayListTest(5, 5); Tester.exec.shutdown{) ; 1* Output : ISample) Type Synched ArrayList 1 0r Ow Synched ArrayLis t 9r lw readTime + writeTime Synched ArrayLis t 5r 5w readTime + writeTime CopyOnWri t eArrayList 10 r Ow CopyQnWriteArrayList 9r 1w readTime + writeTime CopyOnWriteArrayLi s t 5r 5w readTime + writeTime * jjj ,- i Read time 232158294700 1 98947618203 223866231602 117367305062 249543918570 758386889 741305671 877450908 21 27630 75 68180227375 Wr ite time O 24918613399 132176613508 O 136145237 6796 7464 300 En ListTest, las clases Reader y Writer realizan las acciones específicas para un contenedor List. 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 inicializar 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 independientemente 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 escritores y sigue siendo significativamente más rápido cuando hay cinco escritores. Parece que podemos utilizar de manera bastante 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. Comparación de las implementaciones de mapas Podemos utilizar el mismo sistema para obtener una idea aproximada del rendimiento de un HashMap de tipo syncbroniud comparado con un ConcnrrentHasbMap: // : concurrenc y / MapComparisons .java / I {Args: 1 la lO} (Prueba rápida de verificac ión durante la construcción) // Comparación ap roximada del rendimiento de los mapas // compatibles con hebras. import java.util.concurrent.*i import j ava.ut i l.*; i mport net.mindview.util.*¡ abst rac t class MapTest extends Tester containerlnitializer() return Collections.synchronizedMap( new HashMap ( MapData.map( new CountingGenerator. Intéger () , n€w CountingGenerator,Integer(), containerSize))) i SynchronizedHashMapTest(int nReaders, int nWritersl super ( nSynched HashMap n nReaders, nWri ters) ; I class ConcurrentHashMapTest extends MapTest { Map containerlnitializer{) return new ConcurrentHashMap( MapData.. map ( new CountingGenerator.lnteger(), new CountingGenerator.lnteger{), containerSize)); ConcurrentHashMapTest(int nReaders, int nWriters) super (IICóncurrentHashMapll, nReaders, nWriters); public class MapComparisons { public static void main(String[] args) Tester. initMain (args) i new SynchronizedHashMapTest(10, O); new synchronizedHashMapTest(9, 1) i neW SynchronizedHashMapTest(5, 5); new ConcurrentHashMapTest(lO, O) i new ConcurrentHashMapTest(9, 1); new CortcUrrentHashMapTest(5, 5); Tester.exec.shutdown() i 1* Output: (Sample) Type Synched HashMap 10r Ow Synched HashMap 9r lw readTime + writeTime = Synched HashMap Sr 5w readTime + writeTime = Read time 306052025049 428319156207 476016503775 243956877760 487968880962 Write time O 47697347568 244012003202 21 Concurrencia 847 ConcurrentHashMap l Or Ow Concurren tHashMap 9r lw readTime + writeTime ConcurrentHashMap Sr 5w readTime + writeTime 23352654318 18833089400 20374942624 120 37625732 23888114831 o 1541853224 11850489099 * /// , . El impacto de añadir escritores a un mapa ConcurrentHashMap es todavia menos evidente que para CopyOn WriteArrayList, 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 significa 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écnica a aquellos problemas que puedan adaptarse a una serie de requisitos. Si fallara compareAndSet( ), tenemos que decidir 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álculos 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, utilizando 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 tendrí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 . *¡ public class FastSimulation { static final int N ELEMENTS = 100000; static f i nal int N_GENES = 30; static final int N_EVOLVERS = SO; static final Atomiclnteger [] (] GRID "" new Atomiclnt eger[ N_ELEMENTS ] [N_ GENES]; stat ic Random rand = new Random ( 47)¡ a tati e class Evol ver implements Runnable public void run () { while(!Thread.i nterrupted()) { 848 Piensa en Java 11 Selecc i onar aleatoriamente un e lement o con el que trabajar: int element = r and.nextlnt(N_ ELEMENTS) ¡ for(int i = O¡ i < N_GENES¡ i++} { i n t previous = element - 1; i f (pr evious < O) previous = N ELEMENTS - 1; int next = e l ement + 1 ; if(next >= N_ELEMENTS) next = Oi int oldvalue = GRID[e lemen t] [i] .get() i 11 Real izar al gún tipo de cálculo de modelado: i n t newval ue = o ldval ue + GRID [pre vious l [ i l . get() newvalue 1= 3; 11 + GRID [next l [il .get () ; Promediar los tres valores i f (! GRID [elementl [il . compareAndSet(oldvalue, newva lue)) { 11 Aqu í una po lí tica para tratar l os fallos . En este caso, 1/ nos limita remos a i n formar de l f a llo y a igno ra r lo; 1/ nue stro model o se e ncargará de trat a r con él, print ("Old va l ue change d f rom !I + oldvalue) i public stat ic vo id main (String[] argsl throws Excepti on { ExecutorService exec = Executars.newCachedThreadPaol()¡ f or( int i = O; i < N_ELEMENTS¡ i+ +) fo r (i n t j = O; j < N_GENES; j++ ) GRID (i] ( jl = new Atomiclnteger (rand .next l nt (lO OO)); f a r(int i = O i i < N_ EVOLVERS¡ i+ +) exec.execute(new Evo l ve r()) i TimeUnit , SECONDS.sleep (5 ) j exec. shu tdownNow( ) ¡ / * (Ejecut ar para ver la salida ) *11/:- Todos los elementos se insertan en una matriz, en la suposición de que esto ayudará a incrementar la velocidad (esta suposición será comprobada en un ejercicio). Cada objeto Evolver promedia su valor con los valores contenidos antes y después suyo, y si se detecta tul fallo cuando se intenta hacer la actualización, simplemente se imprime el valor y se continúa. Observe que no aparece ningún mutex en el programa. Ejercicio 39: (6) ¿Son razonables las suposiciones realizadas en FastSimnlation.java? Pruebe a cambiar la matriz, sustituyendo los valores AtomicInteger por valores int ordinarios y utilizando mutex de tipo Lock. Compare el rendimiento de las dos versiones del programa. 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áquina multiprocesador o no. En último término, la única forma de saber si podemos obtener alguna ventaja con ReadWriteLock 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.*¡ public c l ass ReaderWriterList { private ArrayList lockedList¡ JI Realizar una ordenac ión equitativa: prívate ReentrantReadWriteLock lock new ReentrantReadWriteLock(t rue ) ; pub lic ReaderWriterList(in t size, T i nitialVa l ue ) l ockedList = n ew ArrayList( COllections.nCopies (size, i n it ialValue}); p u blic T set(in t i ndex, T e lement) Lock wlock = lock .wr i teLock(}; wlock . l ock () ; try ( r e turn l ockedList. s e t(index. elementl; fi n a lly ( wlock.unlock()¡ p ub lic T get (int index I ( Lock rlock = l ock . readLoc k ()¡ r l ock .lock 11 ; try ( // Mostra r que mú l t ip les lector es pueden // adqu irir e l bloqueo de l ec t ura : if (lock.getReadLockCount () > 1 ) print (lock .getReadLockCount ( » ; return l ockedList.get (index); finally ( rl ock . un lock () ; public static void main(St r ing[ ] args) thr ows Exception { new ReaderWri t erLi stTes t(30 , 1 ); 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 (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) ; catch(Inte r r upt edException e l // Forma aceptable de sa li r print(I1Writer finished, shutting down " ) ; 850 Piensa en Java exec.shutdownNow () ; privat e c l ass Reader i mplements Runnabl e public void r un () { { try { while(!Thread.interr upted() f or(int i = Oi i < SIZE¡ i++l list.get(il i TimeUnit.MILLISE CONDS ,sleep( l} ¡ cat ch(In terruptedExcep t ion el // Forma aceptable de salir public ReaderWr i terListTe st (i nt readers , int writers) for (int i = D i i < reade r s¡ i++} exec.execute (new Reader(»¡ for (int i = O; i < writers¡ i++} exec.execute(new Wri t er (» i { 1* (Ejecutar para ver la salida) *///:Una lista ReaderWriterList puede almacenar un número fijo de objetos de cualquier tipo. Debemos indicar al constructor el tamaño deseado de la lista y un objeto inicial con el que rellenar la lista. El método set( ) adquiere el bloqueo de escritura para poder invocar el método ArrayList.set( ) subyacente, mientras que el método get() adquiere el bloqueo de lectura para poder invocar ArrayList.get( ). Además, get( ) comprueba si hay más de un lector que haya adquirido el bloqueo de lectura y, en caso afinnativo, muestra dicho número para demostrar que puede haber múltiples lectores que adquieran el bloqueo de lectura. Para probar la lista ReaderWriterList, ReaderWriterListTest crea tareas tanto lectoras como escritoras para una lista ReaderWriterList. Observe que hay muchas menos escrituras que lecturas. Si examina la documentación del JDK para ReentrantReadWrlteLock, verá que hay varios otros métodos disponibles, así como cuestiones relativas a la equidad y a las '"decisiones de polítican • Se trata de una herramienta -bastante sofisticada, y que sólo debemos usar cuando estemos buscando formas de aumentar las prestaciones. El primer prototipo de un programa debería emplear una sincronización directa, debiéndose introducir ReadWriteLock s6lo si es necesario. Ejercicio 40: (6) Siguiendo el ejemplo de ReaderWrlterList.java, cree un mapa R eaderWrlterMap utilizando un mapa HashMap. Investigue su rendimiento modificando MapComparisons.java. ¿Cómo se compara con un contenedor HashMap sincronizado y con Wl contenedorConcur:r:entHashMap? Objetos activos Después de estudiar este capítulo, habrá observado que el mecanismo de gestión de hebras en Java parece bastante complejo 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 encaje 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 denominen "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 objeto, 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 II II Sólo se pueden pasar constantes, imrnutables, "obj etos desconectados" , u otros objetos ac tivos como argumento s 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 .*¡ publíc class Ac t iveObjectDemo { private Execut orService ex ~ Executors.newSingl eThreadExecutor( ) i prívate Random rand ~ new Random (4 7)¡ II Insertar un retardo aleatorio para producir e l efecto II de un tiempo de cálculo: prívate void pause (int fact or) { try { TimeUn it.MILLISECONDS.sleep ( 1 00 + rand .nextlnt( fa ctor)}¡ catch( I nterr upt edException e) { print ( "s l eep() interrupted 1t ) ; publ i c Fut u re calculatelnt (f i nal i n t x, fina l int y) { ret u r n ex.submi t (ne w Cal l able() public Integer cal l () { print ( lIs tarting 1t + x + 1t + 11 + y) i pause(500) ; return x + Yi } }) ; publi c Future calcul a t e Float{ final float x, f i nal float y) r etur n ex.submit( new Callable() { public Float call () { print ( "start i ng " + x + " + n + y ) ¡ pau se (2 000); return x + y; } }) ; public vo i d shutdown () { ex.shutdown () ; } publ i c stat ic void main {String[] args ) { Ac t iveOb j ectDemo dl ~ new ActiveObjectDemo() ; II Evita ConcurrentModificationException: Lis t O) { for(Future f : results) if(f.isDone()) { try { print (f .get ()); catch(Exception e) { throw new RuntimeException(e) ; results.remove(f) ; dI . shutdown () ; /* Output: (85% match) All asynch calls made starting 0.0 + 0.0 starting 0.2 + 0.2 O. O starting 0.4 starting 0.8 starting 1.2 starting 1.6 starting 0.4 + 0.4 0.6 + 0.6 0.8 + 0.8 O + O 1 + 1 O starting 2 + 2 2 starting 3 + 3 4 starting 4 + 4 6 8 *///,El "ejecutor monohebra" producido por la llamada a Executors.newSingleThreadExecutor() mantiene su propia cola bloqueante no limitada, y tiene una única hebra que extrae tareas de la cola y las ejecuta hasta completarlas. Todo lo que necesitamos hacer en calculateInt( ) y calculateFloat( ) es enviar con submit( ) un nuevo objeto CaUable en respuesta a una llamada a método, transformando así las llamadas a métodos en mensajes. El cuerpo del método está contenido dentro del método call( ) en la clase interna anónima. Observe que el valor de retomo de cada método de objeto activo es un objeto Future con un parámetro genérico que es el tipo de retomo real del método. De esta forma, la llamada a método vuelve casi inmediatamente, y elllamante utiliza el objeto Future para descubrir cuándo se completa la tarea y para extraer el valor de retomo real. Esto permite gestionar el caso más complejo, pero si la llamada no tiene valor de retomo el proceso se simplifica. En main(), se crea una lista List m = new HashMap( ) i public TextArea () { /1 Uti li zar todos los datos: m.putAll (Countries.capitals ()) i b.addAct ionList ener (new ActionListener() public void a c tionPerformed(ActionEvent el fo r{Map .Entry me : m.entrySet() ) t.append(me.getKey{) + " : 11+ me.getVa l ue(}+"\n")¡ f } }) ; c.addAct ionListener(new ActionList ener() { p ublic vo id a ction Performed(ActionEvent e) t.setText(!1U ) i } }) ; setLayout( new FlowLayout (» add (new JScrollPane(t»¡ i add(b ) ; add(c) i pu blic stat ic void main {String [] args) { run(new TextArea ( ) , 475, 425); En el constructor, el mapa se rellena con todos los países y sus capitales. Observe que, para ambos botones, se crea el objeto ActionListener y se añade sin definir una variable intermedia, dado que no necesitamos volver a referimos a dicho objeto 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 desplazamiento 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 entornos de programación Gill, he de confesar que me impresionan bastante la simplicidad y el adecuado diseño de componentes 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 componentes. 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 componentes, 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, existe 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 predeterminado. 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 BorderLayout.CENTER Rellenar la parte central, hasta alcanzar a otros componentes o hasta alcanzar los bordes. Si no especificamos un área en la que colocar el objeto, el área predeterminada será CENTER. En este ejemplo, utilizamos el gestor de disposición predeterminado, ya que JFrame toma como opción predeterminada BorderLayont: 11: guilBorderLayoutl .j ava II Ejemplo de BorderLayout. import javax. swing.*¡ import java . awt.*¡ i mport stati c net.mindview.util.SwingConsole.*; p ublic class BorderLayoutl extends JFrame { public BorderLayoutl( ) { add(Bor derLayout . NORTH, new JButton ("North ll » ; add (BorderLayout . SOUTH, new JButt on (JlSouth ") ) ; add(BorderLayout .EAST, new JButton(nEast") ); add (Bor derLayout. WEST, new JButton ( II West 11 ) ) ; add (BorderLayout . CENTER, new JButton (If Center ") ) ; public s t atic void main( String(] args ) run(new Borde r Layoutl{), 300, 250 ); ) /// , Para todas las colocaciones salvo CENTER, el elemento que añadamos se comprime para que quepa en la cantidad de espacio más pequeña posible a lo largo de una dimensión, mientras que se estira para ocupar el máximo espacio sobre la otra dimensión. Sin embargo, CENTER se estira según ambas dimensiones para ocupar toda la parte central. 22 Interfaces gráficas de usuario 867 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 fonnulario. 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. *¡ public c lass Fl owLayoutl ex tends JFrame { public FlowLayoutl () { setLayout{new Fl owLayout ()) i for(int i = O; i < 20; i'¡- T ) add (new JBut ton ( lfBu tton 11 + i)); publ i c static vo i d ma i n(Str i ng [] args) run (new FlowLayoutl() 300, 300) ; I 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 tendrá el tamaño de su cadena de caracteres asociada, si tratamos·de justificar a la derecha su texto, la visualización no se modificará. 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 columnas 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.* ¡ public cIass Gri dLayou tl extends JFrame public GridLayoutl () { setLayout(new GridLayout( 7 ,3)} ; for(in t i = O; i < 20; i ++} add (n ew JButton (!1Button n +- i )) i pubI i c s t atic voi d main(string [} a rgs ) r un(new Gr i dLayoutl() , 30 0 , 300); 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 automá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 descargarse de http://java.sun.com. Este componente está apilado sobre GridBagLayout y oculta la mayor parte de su complej 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 tambié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 componentes 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. ¿Cuál es la mejor solución? Swing es bastante potente; pueden hacerse una gran cantidad de cosas con sólo unas líneas de código. Los ejemplos mostrados son bastante simples y, de cara a aprender a utilizar la biblioteca tiene sentido escribirlos manualmente. De hecho, podemos conseguir una gran cantidad de funcionalidad combínando gestores de disposición simples. Llegados a un punto, sin embargo, deja de tener sentido escribir de forma manual los formularios GUI; la tarea se hace demasiado complicada y no es una buena forma de invertir nuestro tiempo de programación. Los diseñadores de Java y de Swing orientaron el lenguaje y las bibliotecas para soportar las herramientas de construcción de interfaces GUI, que han sido creadas con el expreso propósito de facilitar la experiencia de programación. Mientras que comprendamos el tema de los gestores de disposición y sepa..1JlOS cómo tratar los sucesos (tal como se describe a continuación) no resulta particulannente importante conocer los detalles acerca de cómo disponer los componentes de forma manual; dej e que la herramienta apropiada se encargue de hacer su tarea por usted (Java está diseñado, después de todo, para incrementar la productividad de los programadores). El modelo de sucesos de Swing En el modelo de sucesos de Swing, un componente puede iniciar ("disparar") un suceso. Cada tipo de sucesos está representado por una clase diferente. Cuando se dispara un suceso, es recibido por uno o más "escuchas" que actúan de acuerdo 22 Interfaces gráficas de usuario 869 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á disparando el suceso. Este registro se realiza invocando el método addXXXListener( ) en el componente encargado de disparar 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. Tipos de sucesos y de escuchas Todos los componentes Swing incluyen métodos addXXXListener( ) y removeXXXListener( ) de tal forma que se pueden agregar y eliminar los tipos apropiados de escuchas para cada componente. Observará que "XXX" en cada caso también representa el argumento del método, como por ejemplo en addMyListener(MyListener m). La siguiente tabla presenta los sucesos, escuchas y métodos básicos asociados, junto con los componentes básicos que soportan esos sucesos concretos, proporcionando los métodos addXXXListener( ) y removeXXXListener( ). Recuerde que el modelo de sucesos está diseftado para ser ampliable, así que puede que se encuentre con otros tipos de sucesos y de escuchas que no están incluidos en esta tabla. ActionEvent ActionListener addActionListener( ) removeActionListener( ) JButton, JList, JTextField, JMenuItem y sus derivados, incluyendo JCheckBoxMenuItem, JMenu y JPopupMenu AdjustmentEvent AdjustmentListener addAdjustmentListener( ) removeAdjustmentListener( ) Jscrollbar y cualquier cosa que creemos que implemente la interfaz Adjustable ComponentEvent ComponentListener addComponentListener( ) removeComponentListener( ) *Component y sus derivados, incluyendo JButton, JCbeckBox, JComboBox, Container, JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar, JTextArea y JTextField ContainerEvent ContainerListener addContainerListener( ) removeContainerListener( ) Container y sus derivados incluyendo JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog y JFrame 870 Piensa en Java FocusEvent FocusListener addFocusListener( ) removeFocusListener( ) Component y sus derivados* KeyEvent KeyListener addKeyListener( ) removeKeyListener( ) Component y sus derivados* MouseEvent (tanto para los ches como para el movimiento del ratón) MouseListener addMouseListener( ) removeMouseListener( ) Component y sus derivados* MouseEvent6 (tanto para los clics como para el movimiento del ratón) MouseMotionListener addMouseMotionListener( ) removeMouseMotionListener( ) Component y sus derivados* WindowEvent WindowListener addWindowListener( ) removeWindowListener( ) Window y sus derivados, incluyendo JDialog, JFileDialog y JFrame ItemEvent ItemListener addIternListener( ) removeItemListener( ) JCheckBox, JCheckBoxMenuItem, JComboBox, JList y cualquier cosa que implemente la interfaz ItemSelectable TextEvent TextListener addTextListener( ) removeTextListener( ) Cualquier cosa derivada de JTextComponent, incluyendo JTextArea y JTextField Puede ver que cada tipo de componente soporta sólo ciertos tipos de sucesos. Resulta bastante tedioso buscar todos los sucesos 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 buscar los métodos de una clase concreta, bien la lista completa de todos o un subconjunto de los nombres que se correspondan 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 documentación del JDK. He aquí la versión GUI más útil de ShowMethods.java, especializada para buscar los métodos "addListener" de los componentes 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.*¡ public class ShowAddLi steners extends JFrame { pri v ate JTextField name = new J Text Fie ld (25)¡ pr ivate JTextArea res u l ts = new JTe x t Area( 40, p rivate static Pat tern addL i stener = 65); Pattern.compile(" (add\\ w+? Listene r\\( .*? \\» " ) ; pri vate s tatic Pat t ern qual ifier = Pattern. compi le (11 \ \ w+ \ \ ." ) ; c lass NameL i mplemen ts ActionListener public void act i on Pe rf or med (Ac tionEvent e) { Stri ng nm = name.getText(} . t rim()¡ i f (nrn . leng t h () ~~ O) { re s ul ts. setText ("No mat ch " ) ; return ¡ Class kind ¡ try { kind = CIass. f orName (" j avax. s wing . 11 + nm); catch(ClassNotFoundExc ept ion ex) { r esult s.set Text ( !lNo matc h ll ) ¡ r eturn; Method[] rnethods ~ kind.getMet hods( ); resul ts. se tText ( 11 11 ) ; for(Meth od m : methods ) { Ma t cher matche r = addListener . ma t cher(m . toString(»¡ i f(ma tcher.find ( )} resuIts .append(qualifi er.matcher( matcher.group(l)} . r epl ac eAll(lI!1) + lI\nl1) i public ShowAddL ist eners () { NameL nameLi s t ener = new NameL() i n ame. addAct i onLis tener (nameListener) i JPanel top = new JPane l ()¡ t op . add (new J Labe l ( "Swing class name (p r e ss En ter) : "» top.add(narne) ; a dd( BorderLayout. NORTH, top ) ; a dd( new JS c rol l Pane (resul ts»; II Datos i ni c iales y p rueba: name. setText ( IJTextArea") ¡ n ame Listener.actionPerformed ( new Ac tionEvent ( "" I O 1"" » i p ublic static void main(String[] args) { run( n ew ShowAddListeners() 500, 400) i I ; 872 Piensa en Java 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 captura 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 combinación de caracteres alfabéticos y seguida de "Listener" y de la lista de argumentos entre paréntesis. Observe que esta expresió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 correspondencia 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 ejemplo, 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 ComponentAdapter componentHidden(ComponentEvent) componentShown(ComponentEvent) componentMoved(ComponentEvent) componentResized(ComponentEvent) ContainerListener ContalnerAdapter componentAdded(ContainerEvent) componentRemoved(ContainerEvent) FocusListener FocusAdapter focusGained(FocusEvent) focusLost(FocusEvent) KeyListener KeyAdapter keyPressed(KeyEvent) keyReleased(KeyEvent) 22 Interfaces gráficas de usuario 873 MouseListener MouseAdapter mouseClicked(MouseEvent) mouseEntered(MouseEvent) mouseExited(MouseEvent) mousePressed(MouseEvent) mouseReleased(MouseEvent) MouseMotionListener mouseDragged(MouseEvent) WindowAdapter windowOpened(\VindowEvent) 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 junto 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. Utilización de adaptadores de escucha por simplicidad En la tabla anterior, podemos ver que algunas interfaces escucha sólo tienen un método. Implementar estas interfaces es trivial. Sin embargo, las interfaces escucha que tienen múltiples métodos pueden ser más cómodas de emplear. Por ejemplo, si queremos capturar un clic de ratón (que no haya sido ya capturado por nosotros, por ejemplo, mediante un botón), necesitaremos escribir un método para mouseClicked( ). Pero como MouseListener es llila interfaz, hay que implementar todos los demás métodos, incluso aunque no hagan nada. Esto puede resultar bastante tedioso. Para resolver el problema, algunas (pero no todas) de las interfaces escucha que disponen de más de un método se proporcionan con adaptadores, cuyos nombres podemos ver en la tabla anterior. Cada adaptador proporciona métodos predeterminados vaCÍos para cada uno de los métodos de la jnterfaz. Cuando heredamos del adaptador, basta con sustituir sólo los métodos que tengamos que modificar. Por ejemplo, el escucha MouseListener típico que utilizaremos sería similar al siguiente: class MyMouse Listener extends MouseAdapter public v oid mouseClicked(MouseEvent e) { // Responder al clic del ratón ... El objetivo de los adaptadores es facilitar la creación de clases escucha. Sin embargo, los adaptadores presentan una desventaja. Suponga que escribimos un adaptador MouseAdapter como el anterior: class MyMouseListener extends MouseAdapter public void MouseClicked(MouseEvent e) { // Responder al clic del ratón ... Esto funciona, pero el programador puede volverse loco tratando de ver por qué, ya que todo se compilará y ejecutará correctamente, 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úsculas 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. Control de múltiples sucesos Para demostrar que estos sucesos se están realmente disparando, merece la pena crear un programa que controle el comportamiento de un botón JButton, y que no se limite a ver si ha sido pulsado. En este ejemplo también nos muestra cómo here· dar nuestro propio balón de JBntton 7 En el código que se muestra a continuación, la clase MyBntton es una clase interna de TrackEvent, por lo que MyButton puede acceder a la ventana padre y manipular sus campos de texto, lo cual es necesario para poder escribir la información de estado en los campos de la ventana padre. Por supuesto, se trata de tilla solución limitada, ya que MyButton sólo puede emplearse en conjunción con TrackEvent. Este tipo de código se denomina en ocasiones Hcódigo altamente acoplado": jj : gui jTra ckEven t . j ava jj Mostrar l os sucesos a medida que tienen l ugar. import javax.swing.*i import java. awt.*; import java.awt .event.*; import java . uti l .*i import static net .mindview .ut il. SwingConsol e .* ¡ publi c class TrackEvent extends JFrame private HashMap h new HashMap (); private String[J event = { "focusGained n , nfocusLost", "keyPressed ll , "keyReleased", !1keyTyped", "mouseClicked ll , "mouse Entered ", "mouseExited n , II mousePressed ll , "mouseRe l eased", IImouseDragged", "mouseMoved" }; private MyButton bl = new MyBut ton (Col or .BLUE, " testl" ), b2 = new MyButton(Color.RED, IItest2 11 ) i class MyButton ext ends JButton { void report(String field, String msg) { h.ge t( field) .setText(msg); FocusListener fI = new FocusListener {) public v o id focusGained(FocusEvent e ) { report( UfocusGained ll , e.paramString(» ) i publi c v o id focusLost (FocusEvent e) { report("focusLost n , e .paramString() l } i }; KeyLi ste ner kl = new KeyListener() { p ub lic void key Pressed(KeyBven t e) report ( "keyPressed ll , e.paramString( )) ; public void keyReleased( KeyEvent el 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 report ( "keyReleased ll , e.paramString(»; public void keyTyped(KeyEvent e l { r epart ("keyTypedl! , e . paramString ( ) ) ; } }; MouseListener mI = new MouseL i stener () { public void mouseClicked(Mouse Event el report{ lI rnou seClicked", e.paramString()) i p ublic void mouseEntered (MouseEvent e l repa r t ( IImouseEntered", e .paramStrin g () ) ; pub lic void mouseEx i ted {Mouse Event el { repar t (IImou seExi t ed " , e .paramStri ng () ) ; public v o i d mous ePressed (Mou seEvent e) { repart ("mousePressed ll , e . paramSt ri ng () ) ; public void mous eReleased (MouseEvent e) { repart ( "mollseReleased", e. paramString () ) i } }; MouseMotionLi stener mml ~ new MouseMot i onLi stene r() public void mouseDragged(MouseEvent e) { repart ( "mouseDragged ", e . paramStri ng () ) ¡ public void mous e Moved(MouseEvent e ) { repor t ("mouseMoved ll , e.paramString {)) ¡ } }; publ i c MyBu tton {Col or color, String labe l ) { super (label ) i setBackground {color ) ¡ addFocusL istener(fl ) ¡ addKeyLis tener(kl)¡ addMouseL istener(ml l ¡ addMouseMot ionListene r(mml) j public TrackEvent () { set Layout( n ew GridLayout(event. l ength 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, T 1, 2» i tl; add(bl) ; add (b2 ) ; public static void main{String [] argsl run( new TrackEvent(), 700, 500); 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 suceso 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 programa. 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 aparezcan 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. Una selección de componentes Swing Ahora que comprendemos los gestores de disposición y el modelo de sucesos, estamos preparados para ver cómO pueden utilizarse los componentes Swing. Esta sección es un recorrido no exhaustivo de los componentes Swing y de las características que probablemente vaya a utilizar la mayor parte de las veces. Hemos intentado que cada ejemplo sea razonablemente pequeño, para poder tomar fácilmente ese código y aplicarlo en otros programas. Tenga en cuenta que: 1. Podemos ver fácilmente qué aspecto tendría la ejecución de estos ejemplos compilando y ejecutando el código fuente descargable correspondiente a este capítulo (www.MindView.net). 20 La documentación del JDK disponible en http://java.sun.com contiene todas las clases y los métodos de Swing (aquí sólo mostramos algunos). 3. Debido al convenio de denominación empleado a los sucesos Swing, resulta bastante fácil adivinar cómo escribir e instalar una nueva rutina de tratamiento para lID tipo de suceso concreto. Utilice el programa de búsqueda Show AddListeners.java, presentado anterionnente en el capítulo, como ayuda a la hora de in vestigar un componente concreto. 4. Cuando las cosas comiencen a complicarse demasiado. piense en utilizar un constructor de interfaces GUI. Botones Swing incluye distintos tipos de botones. Todos los botones, casillas de verificación, botones de opción e incluso elementos de menú heredan de AbstractButlon (que en realidad, ya que están incluidos los elementos de menú, debería, probablemente, 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 import import import javax.swing.*¡ javax. s wing.border.*¡ j avax. sw ing.plaf. bas ic.* ¡ java.awt.*¡ 22 Interfaces gráficas de usuario 877 import static n et. mindview .util . SwingConsole.*¡ p ub lic c lass Buttons extends J Frame { pr i vate JBut t on jb "" n ew JButton ( IIJButton" )¡ pr i vate BasicArro wButt on up = new Basi cArrowBut ton(B asi cAr rowButton.NORTH ) , down = new BasicAr rowBu tton( BasicAr rowBu tton . SOUTH), right = new Basi cArrowBut ton (Basi cArrowBu tton. EAS T) I le ft = n ew Basi c ArrowBu t ton( Bas i cArrowBu tton . WEST ) ; public Bu t tons () ( se t Layout(new FlowL ayout()); add Ij b) ; add(new JToggl eBut ton (uJToggleBut ton ll ) ) ; add (new JCheckBox( uJChe c kBox ll ) ) i a d d (new JRad i oBut t on ( II JRad ioBu tton 11 ) ) i Jpane l j p = n ew JPanel (); jp.setBorder (new TitledBorde r ( "Di rections lI ) ) ; jp. addlup) ; jp. add (down ) ; jp . addlleft) ; jp. addlright) ; add (jp) ; p ubli c static vo id ma in (String [J r u n (new Bu t tons(}, 350, 2 0 0}¡ a r gs) { 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, pulsado o no pulsado. Pero las casillas de veriftcación y los botones de opción se comportan de forma idéntica, pennitiendo simplemente 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.*; publ ic class Bu ttonGroup s ext ends JFrame { private sta tic Strí ng [) íds = { 11 J une 11 , uWard " , uBeaver", "Wa llyll , " Eddie", "Lumpy" }; st ati c Jpane l makeBPane l ( Class< ? ext ends Abstr ac t Button> k ind, But tonGroup bg = new Butt onGroup (}¡ S tr ing [] i ds) { 878 Piensa en Java Jpanel jp = new Jpanel()¡ String title = kind.getName() ¡ title = title.substring(title.lastlndexOf('. 1) + 1) i jp.setBorder(new TitledBorder(title)) i for (String id : ids) { AbstractButton ab = new JButton(l!failed") i try ( JJ Obtener el método constructor dinámico JJ que toma un argumento de tipo String: Constructor ctor = kind.getConstructor(String.class) ¡ JJ Crear un nuevo objeto: ab = (AbstractButton) ctor.new l nstance (id) ¡ catch (Exception ex) { System.err.println("can1t create TI + kind) ¡ 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)) ¡ public static void main(String[] args) run(new ButtonGroups(), 500, 350) ¡ } ///,El título del borde está tomado del nombre de la clase, una vez eliminada la información de ruta. El botón AbstractButton se inicializa con un control JBution que tiene la etiqueta "Failed", de modo que si ignoramos el mensaje de excepción, seguiremos viendo el botón en la pantalla. El método getConstructor() produce un objeto Constructor que toma la matriz de argumentos de los típos contenidos en la lista de objetos Class que se pasan a getConstructor( ). Entonces todo lo que tenemos que hacer es invocar newInstance( ), pasándole la lista de argumentos, que en este caso es simplemente el objeto String de la matriz ids. Para conseguir un comportamiento de tipo "or exclusivo" con los botones, creamos un grupo de botones y añadimos cada uno de los botones deseados al grupo. Al ejecutar el programa, veremos que todos los botones excepto JButton exhiben este comportamiento de tipo "or exclusivo". Iconos Podemos utilizar un objeto Icon dentro de un control JLabel o de cualquier control que herede de AbstractButton (incluyendo JButton, JCheckBox, JRadioButton y los diferentes tipos de JMenuItem). Utilizar Icon con JLabel resulta bastante 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 conl: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 import j ava.awt.event .*¡ import sta tic net . rni ndview . util.SwingConsole.*¡ public class Fac es extends J Frame { private stat i c Icon [] f ace s; pr i vate JButton j b, j b 2 = new JButton {U Disab l e " ) i prí vate boolean mad = fals e ; public Faces () { fa c es = new Icon [] { new Image lcon (ge tC l as s () . getResource ( "FaceO . gi f n ) ) , n ew I mag e l con (getCl ass () .ge t Resource ( "Facel . g i f!!) ) , n e w I magelcon (getClass( } . getResource {"Face2 . gi f U » , ne w Image l con (ge tClass() .getResource( nFace3 . g if" » , n ew l mage l con (getC l ass () . getRes o urce ( II Face4 . g i f l1 ) ) f }; 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 mad :: true ¡ [ O] ) ¡ jb.setVer t i calAl ignmen t (JBu t ton. TOP ) ; jb.setHorizonta lAlignment{ J Bu tton.LEFT) ; } )) ; jb .setRol loverEnab led (true )¡ jb . setRo l l overlcon (faces [l )} ¡ jb.setPressedl con (faces [2 ] ) ¡ j b . setDisabledlcon (faces[4 ) ) i jb . setToolTipText ( "YoW ! " ) ; add (jb) ; j b2.addActionLi s tener (new ActionLis tene r() public vo i d ac t i onPe r f ormed(ActionEvent el if (jb. i s En abl ed ()) { jb . se tEnabl e d(f a lse) ¡ jb2.se tText( lI En a ble " ) ¡ else { j b.se t Enabl ed (true ) ; j b 2 . setText ("Disable " ) ¡ } }J ; add (jb2) ; public static void main (String [] args ) { run (new Faces {) , 250, 12 5 ) ¡ } ///:Podemos emplear un objeto leon como argumento para muchos constructores diferentes de componentes Swing, pero también podemos usar se!leon( ) para aíladir o modificar un objeto Ieon . Este ejemplo también muestra cómo un control JButton (o cualquier control AbstractButton) puede fijar los distintos tipos de iconos que aparecen cuando suceden cosas con un botón: cuando se pulsa, cuando se desactiva o cuando se pasa el ratón por encima del botón sin hacer clic. Podrá comprobar que estos efectos proporcionan a los botones un aspecto bastante animado. 880 Piensa en Java Sugerencias En el ejemplo anterior hemos añadido una "sugerencia" a un botón. Casi todas las clases que utilizaremos para crear nuestras 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.*; public c l ass TextFields extends JFrame prívate JBut ton bl "" new JBut ton( 1IGet Tex t 1l ) b2 = new JBut ton("Set Textil) i prívate JTextFi e ld tl new JTextField (30 ) , t2 = new JTex t Field (30) , t3 = new JTex t Field (30) ; priva te Str ing s = 1111; private UpperCaseDocument u cd ne\\I UpperCaseDocument () J public TextFields 1) { tl.setDocument (ucd ) ; ucd.addDocumentListener(new TI()) bl . addAc tionListener(new Bl()); b2 . addAct i onListener(new B2()); tl.addActionListener(new TIA() i setLayout( new FlowLayout(»); i addlb l ) ; add (b2); add 101) ; addlt2); addlt3) ; c lass Tl implernents DocumentListener { public void changedUpdate(DocumentEvent e ) {} publ i c void insert Update(Documen t Even t el { t2 .setTex t(tl . getText ()) i t3.setTex t("Text: "+ tl. getText()) i public void removeUpdate(Document Event el t2 .setText (t 1.getText ()); class TlA imp lements ActionLi s tener private int count = O; { i 22 Interfaces graficas de usuario 881 publi c void act ionPerformed (Ac tionEven t e) { t3.setText ( " tl Action Even t .. + count++ ) ; c la ss 8 1 i mpl ements ActionLi stener { public vo id actionPerformed(ActionEvent el if(tl.getSelectedText() == null) 5= tl.getText ()¡ { else s = tl.getSelectedText() i t l .setEdi table(true ) ; class 82 implements ActionListener { publ i c void actionPerf ormed(ActionEvent e) { ucd.setUpperCase (false ) i tl . setText ( " Inserted by Butte n 2 : " + sl i ucd . setUpperCase(true ) ; tl. setEditable (false l ; publi c s tatic void main(String[] args) r un(new Text Fields(), 3 75 , 20 0); class Upp erCaseDocument extends PlainDocument prívate boolean upperCase = true; public vo i d setUpperCase(boolean flag) { upperCase = flag¡ 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 responde a cualquier cambio que se produzca en el "documento" (el contenido del control JTextField, en este caso). Ese escucha 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 cursor 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úsculas 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 denominado showBorder() que crea un control JPanel y agrega en cada caso el borde correspondiente. Asimismo, el ejemplo utiliza 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 jj: guijBorders. j ava jj Diferentes bordes Swing. import javax.s wing.*; i mpor t javax. swing.border.*; import j ava.awt.*i i mport static net. mi ndv iew.util.SwingConsole .*¡ publi c cIass Borders extends JFrame { static JPane l showBo rder (Border b) { JPane l jp = new JPanel()¡ jp.setLayout (new BorderLayout(»; String nro = b.getClas s() .toString (); nm = nm.subs tring(nm.las t l ndexOf (' .1) + 1) jp.add(new JLabel(nm, JLabel .CENTER), BorderLayout.CENTER) ; j p. set Border(b)¡ return jPi i 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»» ¡ publ i c stat ic void main{String(] args ) run(new Borders(), 500, 30 0) ¡ } jjj,- También podemos crear nuestros propios bordes y colocarlos en botones, etiquetas, etc., es decir, en cualquier cosa derivada 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.*¡ public c l ass TextPa ne extends JFrame { private JBut ton b := new JButton(IfAdd Textil ) i private JText Pane tp := new JTextPane()¡ private static Gene rato r 99 = new RandomGe nerator.S t ring(7) i publ ic TextPane () { 22 Interfaces gráficas de usuario 883 b.addAct ionListener (new ActionLi s tener() { public void actionPerformed (ActionEvent e l for(int i == 1; i < 10; i ++) tp.setText(tp.getTextO .... sg.next() + " \n") ; } }) ; add(new JScrollPane(tp) ) i add(BorderLayout.SOUTH, b); public static void main (String [] args) run(new TextPane() 475, 425) j { I 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.*¡ public class Ch eckBoxes extends JFrame { private JText Area t = new JText Area(6, 15 ) ; pri vate ·.JChec kBox e bl new JCheckBox ("Check Box 1" ) , cb2 = new JCheckBox ( I! Check Box 2 " ) , eb3 = new JCheckBox ( n Check Box 3 11 l ; public CheckBoxes() { cbl.addAc tionListener(new ActionListener() pUblic void actionPerformed(Ac t i onEvent e) trace(l!l ", cbl) ¡ } }) ; 884 Piensa en Java cb2.addActionListen er(new Ac t ionLi stener() public vo id act ionPerfo rme d( Act ionEvent el trac e( '12", cb 2 ); } }) ; cb3.addActionListener(new ActionL i stener () publ ic void ac tion Performed(ActionEvent e) trace(1l3", eb3); } }) ; set Layout(new F lowLayou t() } i add(new JS crollPane(t» ¡ add lebl) ; a dd(eb2) ; add (eb3) ; prí vate void trace (Stri ng b, JCheckBox eb) { if( cb .is Sele ct ed( ) ) t . append ( 11 Box + b + Set \n ll) ; t . a ppend ( 11 Box + b + Cleared\nn) ; else pub l ic static void main(String[] args) run(new CheckB o xes(), 200, 300) i 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 elecció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 . *¡ public class Rad i oBut t ons extends J Frame { pr ivate JTextFie l d t = new J TextFi e ld( 15); p rivate Butt onGroup 9 = new Butto nGroup(); pr i vate JRadioButton 22 Interfaces gráficas de usuario 885 rbl new JRadioButton (lIone lT , falsel rb2 new JRadioButton ("two", fa!se) rb3 new JRadioButton("three lT , false) i private ActionListener al = new ActionListener() public void actionPerformed(ActionEvent el { t. setText (IIRadio button 11 + I I ( (JRadioButton) e .getSource () ) .getText () ) i } }; public RadioButtons () { rbl.addActionListener(al) i rb2.addActionListener(al) i rb3.addActionListener(al) i g.add(rbl); g.add(rb2); g.add(rb3); t. setEditable (false) i setLayout(new FlowLayout()) i add(t) ; add(rbl) ; add(rb2) ; add(rb3) ; public static void main(String[] args) run(new RadioButtons(), 200, 125); } ///,Para mostrar el estado, se emplea un campo de texto. Este campo se define como no editable, ya que sólo se utiliza para mostrar datos, no para aceptarlos como entrada. Así, se trata de una alternativa a JLabel. Cuadros combinados (listas desplegables) Al igual que los grupos de botones de opción, una lista desplegable es una forma de obligar al usuario a seleccionar un elemento de entre un grupo de posibilidades. Sin embargo, es una forma más compacta de conseguir este efecto y una fonna más fácil de cambiar los elementos de la lista sin sorprender al usuario (también podríamos cambiar los botones de opción dinámicamente, pero el efecto visual resulta bastante molesto). De manera predetenninada, el recuadro JComboBox no se parece al cuadro combinado de Windows, que permite seleccionar un elemento de una lista o escribir nuestra propia selección. Para conseguir este comportamiento debemos invocar el método setEditable( l. Con un recuadro JComboBox, se selecciona un único elemento de entre una lista. En el siguiente ejemplo, el recuadro JComboBox comienza con un cierto número de entradas y luego se añaden nuevas entradas al recuadro cuando se pulsa un botón. 11: gui/comboBoxes.java II Utilización de listas desplegables. import import import import javax.swing.*¡ java.awt.*¡ java.awt.event.*; static net.mindview.util.SwingConsole.*¡ public class ComboBoxes extends JFrame { private String[] description = { "Ebullient lT ITObtuse", "Recalcitrant ll , IlBrilliant lT ITSomnescent", !1Timorous TT , "Florid ll , "Putrescent" I }; private JTextField t = new JTextField(15); private JComboBox c = new JComboBox(); private JButton b = new JButton(TTAdd items lT )¡ private int count = O; public ComboBoxes () { for(int i = O; i < 4¡ i++) , 886 Piensa en Java c.addl t e m(description [count+ +}) ; t.setEditable(false l ; b.addActionListener {new Ac t ionListener () public void actionPerfo rmed(ActionEvent e l i f(c ount < description.length) c.addl tem(description[ count ++] ) ¡ } }) ; c . addActionListener (new ActionListene r () { public void actionPerformed(Ac ti onEvent el t . setText (JI index: JI + c. getSelectedlndex () + ((JComboBox)e.getSource ()) .getSeleet ed I tem()); } }J; setLayou t (new F1owLayout(» ¡ add(t) ; add (e) ; add(b ) ; JI + public stat ic vo id ma i n(String[) argsl run(new ComboBoxes{) 20 0, 175); 1 El control JTextField muestra el "índice seleccionado", que es el número de secuencia del elemento actualmente seleccionado, 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 invocar 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 resaltado 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 seleccionados. 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 .*¡ public c lass List ext ends JFrame { priva te String[J f 1avo rs ~ { "Chocol ate ", "Strawberry " , "Vani lla Fudge Swi r 1 n, IlMint Chip", "Mocha A1mond Fudge", tlRum Ra i sin " , uPra1i n e Cream", IIMud Pie" }; private DefaultListModel 1I tems = new DefaultListModel(); private JList 1st = new JList (l I temsl ¡ priva t e JTextArea t = new JTextArea{flavors.1ength, 20); private JButton b = new JButton("Add I tem U ) ; 22 Interfaces gráficas de usua rio 887 pri vate ActionListene r bl = ne w ActionL i stener () public vo i d act i onPerformed (ActionEvent e} { i f (count < f l avors . length ) { l I tems.add (O , f lavors[count+ +}) i el se ( /1 Desactivar , ya que no quedan más JI sabores por a ñadi r a la l ista. b . setEnabled (false ) ; } }; 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 p u b li c List () { = O; ( 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 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 ) ; 1 add( t) ; add(lst) ; add(b ) ; 11 Regi s tr ar escuchas de sucesos l st.addListSe l ect i o nListener{ l l) b . addActionListener (b l) ; i pub lic stati c void ma in(String [] args ) { run( new Lis t () , 250, 375) i Como podrá observar también se han añadido bordes a las listas. Si simplemente queremos incluir una matriz de objetos String en un control JList, existe una solución mucho más sencilla: basta con pasar la matriz al constructor JList, y éste construye la lista automáticamente. La única razón para usar el "modelo de lista" en el ejemplo precedente es para poder manipular la lista durante la ejecución del programa. JList no proporciona un soporte directo automático para el desplazamiento de pantalla. Por supuesto, nos bastana con insertar el control JList en un panel JScrollPane, que se encargaría de gestionar todos los deta lles por nosotros. Ejercicio 16: (5) Simplifique List.java pasando la matriz al constructor y eliminando la posibilidad de adición dinámica de elementos a la lista. Tableros con fichas El control JTabbedPalle permite crear un "cuadro de diálogo con fichas", que tiene una serie de pestañas de archivador a lo largo de uno de sus bordes. Cuando se pulsa en una de las fichas, aparece un cuadro de diálogo diferente. 888 Piensa en Java 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.*¡ public class Tabbedpanel extends JFrame private String [J flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swi rl", "Mint Chip", IIMocha Almond Fudge" "Rum Rai s in", npraline Cr eam" , II Mud Pie" }; private JTabbedPane tabs = new JTabbedPane( ) ; private JT extField txt new JTextField(20) ¡ publ ic TabbedPanel() ( int i = Oi for(String f l avor : flavors) tabs.addTab (f l avors(i ] , new JButton{"Tabbed pane " + i ++» i tabs.addChangeListener (new ChangeListener () public vo id stateChanged(ChangeEvent e) { txt. setText ("Tab selected: 11 + tabs.getSel ectedlndex(») ; } }) ; add (BorderLayout . SOUTH, txt ) ; add (tab s ) ; f publ ic stati c vo id ma i n (String [] args ) run(new TabbedPanel ( ), 400, 250); } ///,Al ejecutar el programa, vemos que el control J TabbedPane apila automáticamente las fichas si hay demasiadas como para que todas quepan en una misma fila. Podemos comprobar que esta funcionalidad cambia el tamaño de la ventana al ejecutar el programa desde la línea de comandos de la consola. Recuadros de mensaje Los entornos de ventanas suelen contener un conjunto estándar de recuadros de mensaje que pennite presentar rápidamente 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.*¡ public c l ass MessageBoxes extends JFrame { private JButton[] b = { new JButton( ITAlert lT ) , new JButton( IIYe sjNoll), new JButton( IT Color"), new JButton ("Input " ), new JButton("3 Vals") 22 Inlerfaces gráficas de usuario 889 ); 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 JOp t ionPane.ERROR_MESSAGE ) ; 11 I I 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) ; pub l ic s t a ti c vo id main (St ring [] a r gs ) r un (new MessageBoxes () , 200 , 20 0); ) /// :Para escribir un único escucha ActionListener, hemos adoptado en el ejemplo la solución, algo arriesgada, de comprobar las etiquetas de tipo String de los botones. El problema con este enfoque es que resulta fáci l obtener la etiqueta de manera errónea, nonnalmente en lo que respecta al uso de mayúsculas y minúsculas, y estos errores pueden ser dificHes de detectar. Observe que showOptionDialog() y showInputDialog( ) proporcionan objetos que contienen el valor introducido por el usuario. Ejercicio 17: (5) Cree una aplicación utilizando SwingConsole. En la documentación del JDK disponible en http://java.sun.com, localice el control JPasswordField y añádalo al programa. Si el usuario escribe la contraseña correcta, utilice JOptiollPane para proporcionar un mensaje de confinnación al usuario. · 890 Piensa en Java 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 componente 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 escucha 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.*¡ public c l ass SimpleMenus extends J Frame { private JText Field t = n ew JTextField(15 ) i privat e ActionLis tener al = new ActionLis tener() public void actionPerformed(ActionEvent el t.setTex t( (JMenu l tem)e.getSource (» .getText{»; } }; private J Me n u (] menus new JMenu("W i nken tr ) new JMenu ("Nod 11) = { n ew JMenu(ItBlinken lt f ) f }; private JMe n u I tem[] items = new JMenultem(llFee ll ) new JMenultem{I1Fi l1 ) n e w JMenultem{n Fon ) new J Menu l t em{nZipll ) , new JNen u ltem ("Zap " ) , n ew J Menu ltem ( IZot " ), new JMe nultem ( "Oll yll ) , n ew JMenu l tem ( " 0 x e n " ), new JMe nu ltem ( "Free n ) f f f }; public SimpleMenus() for( int i == o; i < items .length¡ i++) items [i] . addActionListener (al) ¡ menus [ i % 3J .addli tems [ iJ ) ; { JMenuBar mb == new J MenuBar ( )j f or(JMenu jm : men u s ) mb.add ljm) ; setJMenuBar(mb) ; setLayout(new FlowLayout(»); add lt) ; pub lic static vo id main (Stri ng[ ] args ) r un{new Simp l e Me n us () , 200, 150) j } ((( , - 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 también ilustra la definición de menús en cascada, de atajos de teclado, de elementos JCheckBoxMenultem, y también muestra 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.*¡ pub l ic cIass Me nus extends JFrame { private String [J f l avors = { "Chocol a te ll , nStrawbe rry", ITVanilla Fudge Swirl" , Il Mi nt Chip 11 , I1Mocha Almond Fudge ll , "Rum Rai sint!, " Pralí ne Cream ll , "Mud Pie" }; n ew JTextFiel d ( liNo flavo r" , prívat e JTextFi eld t new J MenuBar () ; private JMenuBar mbl prívate JMenu f new J Menu( "Fil e " ) , ro = new JMenu ( 11 Flavors 11 ) , S = new JMenu ( "Safe ty " ) ¡ 11 Solución al ternativa: prí vate J CheckBoxMenul tern{ ] safet y n ew JCheckBoxMenultem( "Guard" ) , new J CheckBoxMenuI tem ( "Hide IT ) 30) ¡ }; pri vate J Menu l tem[) file = { new JMenu ltem ( II 0pen }¡ 11 Una segunda barra de menú para efectuar un interc ambi o : private JMenuBa r mb2 = new JMe nuBar( )¡ pri vate JMenu fooBar = new J Menu ( 11 f ooBar ll ) ; private JMenul tem[ ] other = { 11 Adición de un a ta jo de tec lado e s muy simpl e, // pero sólo pueden incluirse l os a ta jos en los cons truc t o res /1 en el caso de l os elementos J Menultem: new JMenultem{IIFoo", KeyEven t .vK_ F), new J Menu l tem{IIBar ll , KeyEvent .VK_Al, // Sin a ta jo de teclado: new JMenuI tem ( 11 Baz 11 ) , }; private JBu t ton b = new JBu t ton ( IISwap Menus ll ) ; clas s BL implements Ac t ionLi stener { publ ic void ac tionPerformed( ActionEvent el { J MenuBar m = getJMenuBar () ; setJMenuBar (m == mb l ? mb2 : mbl ) ¡ va lidate()¡ // Refrescar el marco lt c lass ML implemen ts Ac tionLi stener { publi c vo id act ionPerforme d (ActionEvent e) { JMenult em target = (JMenu l tem) e.ge tSource () ¡ ) 892 Piensa en Java String actionCommand = target.getActionCommand() if (actionCommand. equals (IIOpen")) { String s = t.getText() i boolean chosen = false; for(String flavor : flavors) if(s.equals(flavor)) chosen = true; if ( ! chosen) t. setText (nChoose a flavor first! 11) ¡ else t.setText(ITOpening 11 + S + TI. Mmm, mm!IT); i class FL implements ActionListener { public void actionPerformed(ActionEvent e) JMenultem target = (JMenultem)e.getSource()¡ t.setText(target.getText()) ¡ } 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 ) ¡ class BarL implements ActionListener { public void actionPerformed(ActionEvent e) t. setText ("Bar selected ll ) ¡ • class BazL implements ActionListener { public void actionPerformed(ActionEvent e) t. setText (ITBaz selected 11) ; class CMIL implements ItemListener { public void itemStateChanged(ItemEvent e) JCheckBoxMenultem target = (JCheckBoxMenultem)e.getSource() j String actionCommand = target.getActionCornmand(); if (actionCommand.equals (!1Guard ll ) ) t. setText (IIGuard the Ice Cream! 11 + "Guarding is JI + target.getState ()); el se if (actionCommand. equals (IIHide") ) t. setText (IIHide the Ice Cream! 11 + "Is it hidden? IT + target.getState()) ¡ 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 safe ty[l] . addlternListener( cmil ) ; other [ Ol .addActionListener(new FooL(»¡ other [l] .addAct ionListe ner(new BarL(» i other[2] .addActionListener(new BazL(); FL fl = new FL( ) ; int n = O; for (String fla v o r : flavors) { JMe nultem mi = new JMenultem( flavor ); mi.addActionLis t ener(fl) ; m. add(mi ) ; 1/ Añadi r separadores a int erva los: ifl(n+ + + 1 ) % 3 == O) m. addSepara t or ( ) ; fo r(JCheckBoxMenultem sfty : safety) s.addl sfty ) ; s. setMnemonic (KeyEvent .VK_ A) ; f. a dd (s) ; f.setMn emoni c(KeyEvent.VK_Fl i for( int i = Oi i < fi l e.l ength¡ i+ +) f i l e [ i ] . addActionL i stener (f I ) i f.add(file[i l) ; { 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) ; p ub li c s ta tic void ma in (Str ing[] r un(new Me n us (} , 300, 200); args) { 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 activa 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 programa actual mediante el método setJMenuBar() . Observe que, cuando se pulsa el botón se comprueba qué menú está instalado 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 correspondencia con "Open". Este tipo de comparación entre cadenas de caracteres constituye una fuente de errores de programación. La activación y desactivación de los elementos de menú se gestiona automáticamente. El código que gestiona los elementos 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 estado 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, asociaremos 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 (botones, 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 modificar. Todo el código funciona con el "comando de acción", así no se ve afectado por los cambios que efectuemos en las etiquetas 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ándolo sobre JMenuItem, después de lo cual se extrae el comando de acción para pasarlo a través de una cascada de instrucciones 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 innecesario 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 errores. 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 comportamiento 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.*; pub li c c l ass Popup extends JFrame { private JPopupMenu popup = new JPopupMenu()¡ private J~extF ield t = new JTe xtField(lO)¡ public Popup () { setLayout(new FlowLayout()); add (t) ; ActionLi stener al = new ActionListener() public void act i onPerformed(ActionEvent e) t.setText«(JMenultem)e.getSource () ) .getText() ) ; } }; JMenu ltem m = new JMenu l t em(" Hi ther ll ) ; m. addActionListener (a l ) ; popup.add(m) ; m = new JMenu l tem ("Yon " ) ; m.addAct i onL istener(al ) ; popup.add(m) ; new JMenultem( "Afar")¡ rn.addActionListener(all; ID = p opup. add (m) ; popup.addSeparator() i m = new JMenultem (11 Stay Here ") i m.addActionListener{al) ; popup. add (m ) ; PopupLi stener p I = new PopupListener () ; addMouseListener (pl ) ; t.addMouseL istener(p l ) ; class POpupLi s tener extends MouseAdapter { public void mousePressed(MouseEven t e ) { maybeShowPopup (e) ; publ ic void mouseReleased(MouseEvent el maybeShowpopup( e) ; { private vo id maybeShowPopup(MouseEvent e) if(e.isPopupTrigger{)) popup.show(e .getComponent()} e.getX() pub lic static void main(String (] args) r un( new Popup () , 300, 200); { 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 etiqueta 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 suelen 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 proporcionados 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 superficie 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 preocuparse 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() proporciona 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,*¡ cIass SineDraw extends Jp anel { priva te static final int SCALEFACTOR 200; private in t cyc l es¡ pr i vate int p o ints; pri vate double [l sin e -S i priva t e in t [] ptS¡ pub l ic SineDraw(} { setCy c l es (5) ; p ubl ic void paintComponent(Graphics g) { super.paintComponent(g ) ; int maxWidth = getWidth() i double hstep = (doub le)ma xWidth j (doub l e)po i nts¡ int maxHeight = getHeight () ; pts = new i nt [po i n t s } ; for{ i n t i = O; i < p o i n ts; i + + ) pts l i J = (int ) (sines [ i] * maxHei g h tj2 * . 9S + maxHe i ghtj2); g.s etColor(Color .RED) i for{i nt i = 1¡ i < points¡ i++) { int xl (int) ((i - 1) * hstep) ; int x 2 ( int) ( i * hstep ) ¡ i nt y1 p t s (i- 1 ] ; int y2 p t s [i J ; g.dr awL ine (xl, yl, x2, y2 ) ¡ public void setCycles(int newCycles) cycles = newCycles; points = SCALE FACTOR * c yc l es * 2' sines = new double (poin t s l ; for( i nt i = O; i < po in ts; i ++ ) doubl e radian s = (Ma t h,P I / SCALEFACTOR ) si n e s[ i) = Math . sin (ra dians ) ; repa int () i * i ¡ 22 Interfaces gráficas de usuario 897 public class SineWave extends JFrame { private SineDraw sines = new SineDraw(); private JSlider adjustCycles = new JSlider(l, 30, 5); public SineWave () { add (sines) ; adjustCycles.addChangeListener(new ChangeListener() public void stateChanged(ChangeEvent e) { sines.setCycles( ((JSlider)e.getSource()) .getValue()); } }) ; add (BorderLayout. SOUTH, adjustCycles); public static void main (String [] args) run(new SineWave(), 700,400); { 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 valores. 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 propio 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 programación intentando hacer cosas como ésta en otros lenguajes hacía que fuera un poco renuente a realizar esto, pero resultó 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 disparaba 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 proceso similar, y además verá que es un proceso bastante simple, incluso si no ha utilizado un componente concreto anteriormente. 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 informarse 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étodos "getter" y "setter". Ejercicio 22: (7) Cree una aplicación utilizando SwingConsole. La aplicación debe tener tres deslizadores, que permitan 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 rotatorio en la pantalla. Un deslizador debe controlar la velocidad de rotación y un segundo deslizador controlará 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, utilizando 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 SwingConsole) 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 entornos 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 import impert impert javax.swing.*¡ java.awt.*; java.awt.event.*¡ static net.mindview.util.SwingConsole.*¡ c l ass MyDia l og extends JDialog { public MyDialog(JFrame parent) super (parent, "My dialog", true ) i se tLayout (new FlowLayout()) i add (new JLabel ( IIHere is my dialog ll ) ) i JButton ok = new JBu tton ( II OKII) i ok .addActionListener (new ActionListener() public void ac tionPerformed(ActionEvent el dispose ()¡ II Cierra el cuadro de d iálogo } }) ; add lok) ; setSize(150, 125 ) ; public class Dialogs extends JFrame { private JButton bl = new JButton ("Dialog Box"); 22 Interfaces gráficas de usuario 899 priva te MyDialog dlg = new MyDialog(null) i public Dialogs () { bl.addActionListener(new ActionListener() public void actionPerformed(ActionEvent e) dlg.setVisible(true) ; } }) ; addlbl) ; public static void main (String [] run(new Dialogs() 125, 75) i args) { I 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 blanco 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.*; public class TicTacToe extends JFrame private JTextField rows = new JTextField (113 11 ) , col s = new JTextField(!l3!1) i private enum State { BLANK, XX, 00 } static class ToeDialog extends JDialog priva te State turn = State.XX; // Comenzar con el turno de x ToeDialog (int cellsWide, int cellsHigh) { setTitle("The game itselff!) i setLayout(new GridLayout(cellsWide, cellsHigh)) ¡ for(int i = O¡ i < cellsWide * cellsHigh¡ i++) add(new ToeButton()) ¡ setSize(cellsWide * 50, cellsHigh * 50) i setDefaultCloseOperation(DISPOSE_ON_CLOSE) i class ToeButton extends JPanel { private State state = State.BLANK¡ public ToeButton() { addMouseListener(new ML()); } public void paintComponent(Graphics g) { super.paintComponent(g) i int xIO,yl=O, x2 getSize () . width - 1, y2 getSize() .height - 1; g.drawRect(xl, yl, x2, y2); xl = x2/4¡ yl = y2/4; int wide = x2/2, high y2/2; 900 Piensa en Java if (st at e State. XX) =:= { g.drawLine(xl, yl, xl + wide, yl + high ) ; g .drawLine (xl , yl + high, xl + wide, yl ); íf(state == State.OQ) g.drawOval(xl, yl, xl + wide/ 2, yl + h igh/2l ; clas s ML extends Mous eAdap ter { public vold mousePressed(MouseEvent el i f (st ate = = St ate .BLANK) state { { turn¡ := turn := (turn == State.XX ? S tate.OO State.XX) ; eIs e state (st at e repa i nt () State. XX ? S tat e .OO : State .XX ) ; i class BL implemen ts ActionListener { publ ic vo id ac ti onPerformed(ActionEvent el { JDi alog d := new ToeDia l og ( new Integer(rows.getText()), new Integer(cols.getText())) d .setVi s ible(true) i 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 = new JBu t ton {ligo" } j b. addActionListener (new BL ()} j add(b, BorderLayout.SOUTH} ; JButton b public stat i c vo i d main{String(] args} run(new TicTacToe{} , 200 , 200); 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. Cuadros de diálogo de archivos Algunos sistemas operativos tienen una serie de cuadros de diálogo predefinidos especiales para gestionar la selección de cosas tales como tipos de fuentes, colores, impresoras, etc. Casi todos los sistemas operativos gráficos penniten abrir y guardar archivos, asi que un componente de Java, JfileChooser, encapsula estas operaciones para facilitar su realización. La siguiente aplicación ilustra dos formas de cuadros de diálogo JFileChooser, una para abrir un archivo y otra para guardarlo. La mayor parte del código debería resultar familiar al lector, y todas las actividades de interés tienen lugar dentro de los escuchas de acción asociados con los dos clies de ratón distintos: /1 : gui / Fi l eChooserTest .j ava JI Ejemplo de cuadros de diálogo de archivos. import javax.swing.*¡ import java . awt .*i import java.awt.event.*i import static net.mindview.ut il.SwingConsole. *; publi c c lass FileChooserTes t extends JFrame { privat e JTextField fileName = new JTextField () , dir = new J TextField(); priva t e JButton open :::: new JBu t t on( "Open" ), s ave "" new JBu t t on( "Save" ) ; public Fi l eChooserTes t () { Jpane l p = new JPane l {); open.addActionL istene r {new OpenL( » ; p.add(open) ; save.addActionListener (new SaveL( )) i p.add{save) ; add(p , BorderLayout.SOUTH); dir . setEditable (false) i fil eName.setEditable(fals e) ; p = new JPanel(); p.setLayout(new GridLayout(2,1»; p.add(fi l eName) ; p.add(dir) ; add(p, BorderLayout . NORTH) i class OpenL impl emen ts Action Li stener public vo id actionPerforrned (Ac tionEvent e l { JFileChoose r e = new JF i leChooser( } ¡ / / Ejempl o de c uad r o de d iálogo IIOpen " (abrir ) : int rVa l = c.showOpenDia l og( Fi l e ChooserTest.thi s); if(rVal == JFileChooser.APPROVE_ OPTI ON ) { fi l eName . setText (c. getSel ectedFile ( ) . getName () ) ; dir . setText (c.getCurrentDirectory() .toString( )) ; if(rVal == JFileChoose r . CANCEL_ OPTIONl file Name . setText ("You pressed can cel" ); dir.setText(nu) ; 902 Piensa en Java class SaveL implements ActionListener { public void actionPerformed(ActionEvent e} { JFileChooser c = new JFileChooser(); II Ej emplo de cuadro de diálogo "Save" (guardar): int rVal = c.showSaveDialog(FileChooserTest.this); if(rVal == JFileChooser.APPROVE_OPTION) { fileName.setText{c.getSelectedFile() .getName()); dir.setText(c.getCurrentDirectory() .toString()); if(rVal == JFileChooser.CANCEL_OPTION) { fileName.setText(nyou pressed cancel") i dir.setText("") ¡ public static void main(String[] args) { run(new FileChooserTest(), 250, 150) ¡ 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. HTML en los componentes Swing Cualquier componente que admita texto puede aceptar también texto HTML, que será refonuateado de acuerdo con las reglas del lenguaje HTML. Esto quiere decir que podemos añadir muy fácilmente texto fonuateado a un componente Swing. Por ejemplo: 11: gui/HTMLButton.java II Adición de texto HTML en componentes Swing. import import import import javax.swing.*¡ java.awt.*¡ java.awt.event.*¡ static net.mindview.util.SwingConsole.*; public class HTMLButton extends JFrame private JButton b = new JButton( "" + n
    Hello!
    Press me now! n) ; public HTMLButton() { b.addActionListener{new ActionListener() public void actionPerformed{ActionEvent e) add(new JLabel ("" + "Kapow! 11)) i II Forzar una redisposición para incluir una nueva etiqueta: validate() i 22 Interfaces graficas de usuario 903 )) ; se t Layout(new FlowLayou t ()} ¡ add{b) ; publ ic s tat ic void ma in (String[J args) run(new HTMLButton (), 200, 500) i Es necesario iniciar el texto con "", después de lo cual se pueden emplear marcadores HTML. Observe que no estamos 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 etiqueta 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árrafo anterior. Deslizadores y barras de progreso Un deslizador (que ya ha sido utilizado en SineWave.j ava) permite al usuario introducir datos moviendo un punto de un lado a otro, lo cual resulta bastante intuitivo de algunas situaciones (como por ejemplo, en los controles de volumen). Una barra de progreso muestra los datos en una forma relativa, entre lUla posición "vacía" y una posición ¡'llena" para que el usuario pueda hacerse una idea del estado en el que se encuentra el proceso. Mi ejemplo favo rito consiste en asociar el deslizador con la barra de progreso, de modo que cuando se mueve el deslizador, la barra de progreso modifica su aspecto correspondientemente. El siguiente ejemplo también ilustra la clase Progr essM onitor, que es un cuadro de diálogo emergente con una funcionalidad más rica: /1 : gu i /Pr ogr ess . java Utilización de deslizadores, barras de progreso y monitores de prog reso. import j avax.swing.*¡ import javax.swing .border.*; i mport javax.swing.event . *¡ i mpor t java.awt.*¡ import static net.mindview.util.SwingConsole.*¡ JI public class Progress extends JFrame { prívate JProgressBar pb = new JProgressBar()¡ private ProgressMonitor pm = new ProgressMonitor( this, nMonitoring Progress", "Test n , 0 , lO O}; private JSlider sb = new JSlider (JSlider . HOR IZONTAL, p u blic Progress () { O, 100, 60); setLayout (new GridLayou t(2,1) ) ; add {pb ) ; pm . setProgress(O) ¡ pm. setMill isToPopup (1 000) ; sb. setValue (O ) ¡ sb.setPa i ntTicks(true}; sb.setMaj o rTickSpacing (20 ) ; sb. setMinorTickSpacing (5) i sb. setBorder (new Tit l edBorde r ( nSlide Me!! )) pb.setModel{sb . getModel{); add (sb) ; i II Modelo compartido sb.addChangeLi stener( n ew ChangeListener() { publ ic void stateChanged{ChangeEvent e) { pm.setProgress(sb.getValue(» j 904 Piensa en Java } }) ; public stati c void ma i n (Strin g [1 args) run( n ew Progre ss() , 300, 200)¡ { 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 algunas situaciones sencillas. El control P rogressMonitor no tiene un modelo, por lo que es obligatorio utilizar el método basado 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 ejemplo 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 conectar el deslizador y la barra de progreso. Selección del aspecto y del estilo El concepto "aspecto y estilo seleccionables" permite al programa emular el aspecto y el estilo de diversos sistemas operativos. Podemos incluso cambiar dinámicamente el aspecto y el estilo mientras el programa se está ejecutando. Sin embargo, generalmente haremos una de las dos cosas: o bien seleccionar el "aspecto y estilo interplataforma" (que es el diseño "metal" de Swing), o seleccionar el aspecto y estilo del sistema en el que nos encontramos acttIalmente, de modo que el programa Java parezca haber sido creado específicamente para dicho sistema (está es casi siempre la mejor elección en la mayoría de los casos, para evitar confundir al usuario). El código para seleccionar uno de estos dos comportamientos es bastante simple, pero es preciso ejecutarlo antes de crear ningún componente visual, porque los componentes se construirán basándose en el aspecto y estilo actuales, y no se modificarán simplemente porque cambiemos el aspecto y el estilo a mitad de programa (dicho proceso es más complicado y resulta menos común, por lo que remitimos al lector a los libros dedicados específicamente a Swing). De hecho, si queremos usar el aspecto y estilo interplataforma ("metal"), que es característico de los programas Swing, no tenemos que hacer nada, ya que se trata de la opción predeterminada. Pero si queremos en su lugar emplear el aspecto y estilo actuales del sistema operativ0 8, basta con insertar el siguiente código, normalmente al principio de main(), pero al menos antes de añadir ningún componente: try { UIManager.setLookAndFee l ( UIManager.getSysternLookAndFeelCl assName())¡ catch(Exception e ) { throw new RuntimeBxception (e) i 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.*¡ public cIass LookAndFeel extends JFrame { prívate String[] choices = I1Eeny Meeny Minnie Mickey Moe Larry Curly",split(U prívate Component[] samples new JButton(lIJButton") new new new new u)¡ = { 1 JTextField (lIJTextField") JLabel (lIJLabel n) I JCheckBox ( 11 JCheckBox 11) I JRadioButton (lIRadio") I I new JComboBox{choices), new JList(choices), }; public LookAndFeel() { super ("Look And Feel"); setLayout(new FlowLayout()); for(Component component : samples) add (component) ; private static void usageError() { System.out.println( "Usage : LookAndFeel [cross I systeml motif] !l) ; System. exit (1) ; public static void main(String[] args) if(args.length == O) usageError(); if(args[O] .equals(!lcross")) { try { UIManager.setLookAndFeel(UIManager. getCrossPlatforrnLookAndFeelClassName()) catch(Exception e) { e.printStackTrace() j j el se if(args[O] .equals("systern ll ) ) try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()) ; catch (Exception e) { e.printStackTrace() ; else if (args [O] .equals("motif")) try { UIManager. setLookAndFeel (11 com. sun. java. !I + "swing.plaf.motif.MotifLookAndFeel") ; catch (Exception e) { e.printStackTrace() ; } el se usageError() j II Observe que el aspecto y estilo deben configurarse 11 antes de crear cualquier componente. 906 Piensa en Java r un(new LookAndFeel(), 300, 300) i 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 invocando 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). Árboles, tablas y portapapeles Podrá encontrar una breve introducción y diversos ejemplos sobre estos temas en el suplemento en linea para este capítulo (en inglés) disponible en www. MindView.net. JNLP Y Java Web Start Resulta posible .firmar un applet por razoneS de seguridad. Esto se muestra en el suplemento en línea de este capítulo (en inglés) disponible en www.MindView.nel. Los applets firmados son potentes y pueden tomar el lugar de una aplicación, pero deben ejecutarse dentro de un explorador web. Esto requiere el sobrecoste adicional que el explorador se esté ejecutando en la máquina cliente, y significa también que la interfaz de usuario del applet está limitada y es, a menudo, visualmente confusa. El·explorador web tiene su propio conjunto de menús y barras de herramientas, que aparecerán por encima del applet 9 El protocolo lNLP (Java Network Lmmch Protocol, protocolo Java de inicio a través de red) resuelve el problema sin sacrificar las ventajas de los applets. Con mla aplicación lNLP, podemos tratar de descargar e instalar una aplicación Java autónoma en la máquina del cliente. Esta aplicación puede ejecutarse desde la línea de comandos, desde un icono de escritorio o a través del gestor de aplicaciones instalado con la implementación JNLP. La aplicación puede incluso ejecutarse desde el sitio web desde el que fue inicialmente descargada. Una aplicación lNLP puede descargar dinámicamente recursos de Internet en tiempo de ejecución y se puede comprobar automáticamente la versión si el usuario está conectado a Internet. Esto significa que proporciona todas las ventajas de un applet junto con las ventajas de las aplicaciones autónomas. Al igual que los applets, las aplicaciones JNLP necesitan tratarse con cierta precaución por parte del sistema cliente. Debido a esto, las aplicaciones JNLP están sujetas a las mismas restricciones de seguridad que los applets. Al igual que los applets, pueden implantarse mediante archivos JAR firmados, dando así al usuario la opción de confiar en quien firma. A diferencia de los applets, si se implantan mediante un archivo JAR no firmado, pueden seguir teniendo acceso a ciertos recursos del sistema cliente por medio de diversos servicios de la API JNLP. El usuario deberá aprobar estas solicitudes durante la ejecución del programa. lNLP describe un protocolo, no una implementación, así que hace falta una implementación para poder usarlo. Java Web Star!, o JAWS, es la implementación de referencia oficial de Sun, disponible de forma gratuita y distribuida como parte de Java SES. Si la está utilizando para desarrollo, deberá asegurarse de que el archivo JAR (javaws.jar) se encuentre en su ruta de clases; la solución más cómoda es añadir javaws.jar a la ruta de clases a partir de su ruta de instalación normal en jre/lib. Si está implantado la aplicación JNLP desde 1m servidor web, deberá comprobar que el servidor recononozca el tipo MIME applicationlx-java-jnlp-fIle. Si está empleando una versión reciente del servidor Torneat (http://jakarta.apache.org/tomcat) este tipo MIME ya estará configurado. Consulte la guía de usuario de su servidor concreto. Crear una aplicación lNLP no es dificil. Lo que hacemos es crear una aplicación estándar que se archíva en un archivo lAR, y luego proporcionar un archivo de arranque, que es XML sinlple que proporciona al sistema cliente toda la información necesaria para descargar e instalar la aplicación. Si decide no firmar el archivo JAR, entonces deberá emplear los servicios suministrados por la API JNLP para cada tipo de recurso de la máquina del usuario al que quiera acceder. 9 Jererny Meyer ha desarrollado esta sección. 22 Interfaces gráficas de usuario 907 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 ¡ class OpenL implement s ActionListener public void act i onPerformed{ActionEvent el { Fi leOpenService fs = null¡ try { fs = (Fi l eOpenService ) ServiceManager.lookup( " javax. jnl p. FileOpenService " ) i catch(Unavail abl eServiceExcept i on use) { throw new RuntimeException(use)¡ 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 catch {Exception exc) { t hrow new RuntimeException(exc); save. setEnab l ed (truel ; class SaveL implements ActionListen er { public vo id actionPerformed(ActionEv e n t el FileSaveService fs = null¡ { try { = (FileSaveService ) Servi ceManager.lookup( fs j avax . j n l p . Fi leSaveServ i ce ti) i ca t c h (Unavai l ableServi ceExc eption use ) { throw new RuntimeExcept i on( u s e) i 11 } i f (fs 1= null ) { t ry { f i l e Contents = fs. saveFi leDialog ( u.lI, new String [] { "txt " } , new ByteArraylnputStream( ep . getText () . getBytes()), fileContents.getName(» ; if(fi leContents == null ) return¡ fileName.setText(fi leContents . getName(» catch(Except ion exc) { thr ow new Runt i meException(exc)¡ pu b l i c stat i c void ma i n (Str i ng (] args ) i { JnlpF i l eChooser fe = new J n l pF i leChoose r () fe. setSize (400, 300); fc.setVisible{true ) i i 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 empleando el método ServiceManager.lookup(), y s6lo se podrá acceder a los recursos del sistema cliente a través de los objetos 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 utilizando, 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 restringido 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 FileChooser demo application Mindview Inc. 11 22 Interfaces gráficas de usuario 909 Jn l p File choose r Applicat i on I l u s tra l a apertura, lectura y escri t ur a de un arch ivo de tex t o <1con href=umindview.gif ll / > < j2 se vers ion::: t1 1 .3 + 1I href='' ht tp: // j ava.sun.com/ products / a ut odl / j 2se Ol / > 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 Follow the i nstru c tions in JnlpFil e Chooser.j ava t e build jnlpfilechooser.jar, t h e n:
    fu ture ¡ public final C task¡ public TaskItem(Future future. e task) { this . future ~ f uture¡ this . task ~ t ask¡ { En la biblioteca java.lltil.collcurrent, la tarea no está disponible a través del objeto Future de manera predeterminada, porque 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.*¡ public c Iasa TaskManager {exec.submit(task),task»); pubIic Li s t getResuIts () { Iterator results = new ArrayList ()¡ whi le {i tems . hasNext ( » { TaskItem item = items.next( )¡ if (item . future. i sDone () { 22 Interfaces gráficas de usuario 913 try { results.add(item.future.get()) i catch (Exception el { throw new RuntimeException(e); items.remove() i return resul ts i public List purge () Iterator results = { items = iterator(} i new ArrayList() i while(items.hasNext()) { Taskltem ítem = items.next()¡ ji Dejar las tareas completadas para insertar los resultados: if(!item.future.isDone()) { results.add("Cancelling !1 + item.task); item.future.cancel(true)¡ items.remove() i JI Puede interrumpir 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 import import import import import import javax.swing.*¡ java.awt.*; java.awt.event.*; java.util.concurrent.*¡ net.mindview.util.*; static net.mindview .util.SwingConsole.*; cIass CallableTask extends Task implements Callable publie Stríng eaH () { run () ; return "Return value of 11 + this¡ 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 manager new TaskManager() i public InterruptableLongRunningCallable() { bl.addActionListener(new ActionListener() de larga duración. 914 Piensa en Java p u bli c voi d actionPer f ormed(ActionEvent el Callabl e Task task ~ n ew CallableTask(}; manager.add(task ); system.out .pri n t ln (t ask + ., a dded to t h e queu e n ) ; } }) ; 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 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 ) ; public static void ma i n (String[) args ) { run(new I n terruptab l eLongRunningCallable( ) , 200, 1 5 0)¡ } /// ;- Como podemos ver, CaUableTask hace exactamente lo mismo que Task excepto en que devuelve un resultado (en este caso, un objeto String que identifica la tarea). Se han creado utilidades no Swing (que no fonnan parte de la distribución estándar de Java) denominadas SwingWorker (disponibles en el sitio web de Sun) y Foxlrol (disponibles en http://foxtrol.sourceforge.net) para resolver un problema similar, pero en el momento de escribir estas líneas, dichas utilidades no han sido modificadas para aprovechar el mecanismo CaUable/Future de Java SES. A menudo, es importante proporcionar al usuario [mal algón tipo de indicación visual de que una tarea se está ejecutando, y de su progreso. Nonnalmente, esto se hace mediante una barra de progreso JProgressBar o un monitor de progreso ProgressMonitor. Este ejemplo utiliza un monitor de progreso ProgressMonitor: 11 : guiJMoni toredLongRu nni ngCa ll a bl e .java II Visualización d e l progreso d e la t area con Pr ogressMonitors. import j avax . s wi ng.* ; i mpo r t java. a wt. *i i mport java . awt . event .*; import java.ut il.concurrent.*; import net.mindv iew.util.*; import s tat i c net.mindview.ut il.SwingConso l e.*¡ class Moni toredCal l ab le i mplements Callab le private s t at i c int coun ter = O; pr í vate fi n a l i n t id ~ count er ++ ¡ private fina l Pr ogressMonitor mon i tor; prívate final static int MAX = 8; public Monit oredCallable(progre ssMonitor monit o r) { this.mon i tor ~ monitor¡ monitor . setNote(toString ()} i 22 Interfaces gráficas de usuario 915 monitor . setMaximum(MAX - 1); monitor.set Mi l lisToPopup (SOO); public Stri ng call () { System.out.println{th is T started U ) 1I i try { for(int i = O; i < MAX; i ++) { TimeUni t .MILLISECONDS.s l eep (SOO) ; if (monitor .isCance led()} Thread. curr entThread () .interrupt (} ; f i nar int progr e s s = i; Swi ngUtili ti es . i n vokeLater( new Runnab l e () { public void run ( ) { monitor.setProgress( progress) ; } ) ; cat ch (I nte rruptedException e ) { monitor.close ( ) ; System.out.print l n(this + inte rruptedll); return "Re s u l t: 11 + this + 11 interrupted" i It System. out .println(this + re turn "Result: 11 + this + 11 11 campl eted"); completed"; publ i c String t oS tring () { return II Task 11 + id; } }; publ ic c l ass MonitoredLongRunningCallable extends JFrame { private JButton bI n ew JButt on ("Start Long Running Task ll ) , b2 = new JBut ton ( 11 End Long Running Task 11) , b 3 = new JBut ton("Get results n ) ; p rivate TaskManager manager n ew TaskManager () ; public Moni toredLongRunni ngCal l ab l e() { bl.addActionListener(new ActíonLíste ner() { public vaíd a ctionPerformed(Act ionEvent e) MonitoredCallable task = new MonitaredCal lable( new ProgressMonitor( MonitoredLongRunningCallabl e .thi s, "Long-RUlUling Task", "", O, O) ) ; manager . add (task) i Syst em.out.println(task + 11 added to the queue"); } }) ; b2.addAct i onLi stener(new Act ionListener() public void act ionPerformed( ActionEvent e) for(String result : manager.purge (» System. out . pri n t ln (resu l t ) ; } }) ; b 3 . addActionListene r (new ActianLis tener () { publi c void actionPerformed(ActionEvent e) f or(String result : manager .getResults()} System.out.println(result ) i 916 Piensa en Java } }); setLayout(new FlowLayout(})¡ add(b1); add(b2) ; add(b3) ; pub l ic static void main(St ring [J args) { run(new MonitoredLongRunningCallable{) I 200 , 500) j } ///: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 invocar 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 constructor MonitoredLongRunningCallable. Ejercicio 33: (6) Modifique InterrnptableLongRunningCallable.java para que se ejecuten todas las tareas en paralelo 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 descubrir 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.*¡ class CBox extends JPane l i mp l ements Runnable pr i vate int pause; private ·stat i c Random rand = new Random () ¡ private Col or color = n ew Color(O)¡ publ ic void paintCompon ent(Graphics g) { g.setColor(col or)¡ Dimension s = getSize()¡ g . fillRect(O, O, s.width, s.height}; p ub li c C8ox(int pause) { this.pause = pause; public void run( ) { try { whi l e(!Thread .interrupted() ) color = new Color( rand.nextlnt(OxFFFFFF» i 22 Interfaces gráficas de usuario 917 repaint(); JI Solicitar asíncronamente el redibujo TimeUnit.MILLISECONDS.sleep(pause) i catch(InterruptedException el JI Forma aceptable de salir public cIass ColorBoxes extends JFrame { private int grid = 12; private int pause = 50; private static ExecutorService exec Executors.newCachedThreadPool() i public ColorBoxes () { setLayout(new GridLayout(grid, grid)); for (int i = O i i < grid * grid¡ i++) { CBox eb = new CBox(pause) i add (eb) ; exec.execute(cb) i public static void main(String[] args) ColorBoxes boxes = new ColorBoxes(); if(args.length > O) boxes.grid = new Integer(args[O])¡ if(args.length > 1) boxes.pause = new Integer(args[l]) run(boxes, 500, 400); i ColorBoxes configura un objeto GridLayout de tal manera que tenga una serie de celdas grid en cada dimensión. A continuació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 proporcionando 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 mostrarlo. 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 realidad 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 problema 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 derivado, tenemos que acordamos de llamar a la versión de la clase base de paint( ), de modo que se sigan realizando las acciones 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 diferencias 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". Programación visual y componentes JavaBean Hasta ahora, hemos visto en el libro lo útil que resulta el lenguaje lava para la creación de fragmentos de código reutilizable. La unidad de código "más reutilizable" ha sido la clase, ya que comprende una unidad cohesionada de características (campos) y comportamientos (métodos) que pueden reutilizarse bien directamente, mediante composición, o bien mediante herencia. La herencia y el polimorfismo son partes esenciales de la programación orientada a objetos, pero en la mayoría de los casos, a la hora de construir una aplicación, lo que realmente queremos es componentes que hagan exactamente lo que necesitamos. Lo que nos gustaría es colocar estos componentes en nuestro diseño como si fueran los circuitos integrados que un ingeniero electrónico dispone sobre una placa de circuito impreso. Parece que debería haber alguna forma de acelerar este estilo de programación basado en la "construcción modular". La "programación visual" consiguió su primer éxito (no gran éxito) con Visual BASIC (VE) de Microsoft, seguido de un diseño de segunda generación representado por Delphi de Borland (que fue la principal inspiración para el diseño de lavaBeans). Con estas herramientas de programación, los componentes se representan visualmente, lo que tiene bastante sentido, ya que normalmente mostrará algún tipo de componente visual como nn botón o nn campo de texto. De hecho, la representación visual coincide a menudo con la fonna exacta que el componente tendrá cuando el programa se ejecuta. Por tanto, parte del proceso de programación visual implica arrastrar no componente desde una paleta y depositarlo sobre el formulario. El entorno integrado de desarrollo (IDE, Integra/ed Development Environment) Application Builder escribe automáticamente el código a medida que realizamos estas operaciones y dicho código hará que se cree el componente dentro del programa. Nonnalmente, para completar el programa no es suficiente con colocar el componente sobre un formulario. A menudo, es preciso cambiar las caracterfsticas de un componente, como por ejemplo el color, el texto que se muestra, la base de datos a la que está conectado, etc. Las características que se pueden cambiar en tiempo de diseño se denominan propiedades. Podemos manipular las propiedades de nuestro componente dentro del entorno IDE y, cuando se crea el programa, estos datos de configuración se guardan para poder ser recuperados cuando el programa se ejecute. El lector debería tener clara a estas alturas la idea de que nn objeto es más que nna serie de características: también es un conjunto de comportamientos. En tiempo de diseño, los comportamientos de un componente visual están parciahnente representados por sucesos, cada uno de los cuales constituye una declaración del tipo: "He aquí algo que puedes hacer a este componente". Normalmente, somos nosotros los que decidimos qué es lo que querernos que ocurra, asociando a ese suceso un cierto código. La parte crítica es la siguiente: el entorno IDE utiliza el mecanismo de reflexión para interrogar dinámicamente al componente y averiguar qué propiedades de sucesos soporta. Una vez que sabe cuáles son, puede mostrar las propiedades y permitirnos modificarlas (guardando el estado cuando construyamos el programa), y también muestra los sucesos. En general, lo que nosotros hacemos es no doble clic sobre un suceso y el entorno !DE crea un cuerpo de código y lo asocia con el suceso particular. Lo único que hace falta en dicho pnnto es escribir el código que tenga que ejecutarse cuando ocurra el suceso. Todo esto significa que el entorno IDE hace noa gran cantidad de trabajo por nosotros. Como resultado, podemos centrarnos en el aspecto del programa y en lo que se supone que el programa debe hacer, delegando en !DE la gestión de todos los detalles de conex.ión. La razón de que las herramientas de programación visual hayan tenido tanto éxito es que permiten acelerar enortnemente el proceso de construcción de una aplicación, por supuesto, la interfaz de usuario, pero a menudo también se acelera la construcción de otras partes de la aplicación. 22 Interfaces gráficas de usuario 919 ¿Qué es un componente JavaBean? Un componente es. en definitiva, simplemente un bloque de código que normalmente está encerrado dentro de una clase. La cuestión clave es la capacidad del !DE para descubrir las propiedades y sucesos de cada componente. Para crear un componente VB, el programador tenía originalmente que escribir un fragmento bastante complicado de código, que tenía que respetar ciertos convenios para exponer las propiedades y sucesos (que se hizo más fácil a medida que transcurrieron los años). Delphi era una herramienta de programación visual de segunda generación, y el lenguaje estaba diseilado específicamente centrándose en la programación visual, con lo que era mucho más fácil crear un componente visual. Sin embargo, Java ha llevado la creación de componentes visuales a su estado más avanzado con JavaBeans, ya que un componente Bean es simplemente una clase. No hace falta escribir ningún código adicional ni utilizar extensiones del lenguaje especiales para poder transfonnar algo en un componente Bean. Lo único necesario es, de hecho, modificar ligeramente la forma de denominar los métodos. Es el nombre del método lo que le dice al entorno ¡DE si se trata de una propiedad, de un suceso o de un método nanual. En la documentación del JDK, este convenio de denominación se llama, de manera confusa, "patrón de diseño". Resulta bastante desafortunado que esto sea así, ya que los patrones de diseño (consulte Thinking in Patterns en www.MindView.net) ya son lo suficientemente complejos sin necesidad de que se introduzca confusión adicional. El convenio de denominación no es un patrón de diseilo, y resulta bastante sencillo: 1. Para una propiedad denominada xxx, normalmente creamos dos métodos: getXxx( ) y setXxx(). La primera letra después de "get" o "ser' será puesta automáticamente en minúscula por las herramientas que examinen los métodos, con el fin de generar el nombre de la propiedad. El tipo producido por el mélodo "get" coincide con el tipo del argumento del método "sel". El nombre de la propiedad y el tipo de los métodos "get" y "set" no están relacionados. 2. Para una propiedad de tipo boolean, podemos emplear la técnica anterior basada en "get" y "set", pero también podemos usar "is" en lugar de "gel." 3. Los métodos nonnales del componente Bean no se adaptan al convenio de denominación anterior, pero son de tipo publico 4. Para los sucesos, se utiliza la solución Swing basada en escuchas. Es exactamente el mismo convenio que hemos visto anterionnente: addBounccLístener(BonnceListener) y removeBounceLlstener(BounceListener) para gestionar un suceso BounceEvent. La mayor parte de las veces, los sucesos y escuchas predefmidos satisfarán completamente nuestras necesidades, pero también podemos crear nuestros propios sucesos e interfaces escucha. Para crear un componente Bean simple, podemos emplear estas directrices: // : frogbean/Frog. java // Un componente JavaBean triv ial. package f rogbean¡ import java.awt .*¡ import java.awt.event.*; c lass Spo t s {} pub li c c lass Frog private int jumps; priva te, Color color ¡ prívate Spots spots; privat e b oo le an jmpr¡ public int getJump s () { return jumps; public vo id setJumps(i n t newJumps) { jumps = newJumps¡ public Color getColor () { ret urn color ¡ public v o id setCo lor(Color newColor) { color = newColor¡ public Spo t s getSpots () { return spotSi } 920 Piensa en Java pub1ic void setSpots(Spots newSpots) spots ~ newSpots¡ { pub 1 ic boolean isJumper() { return jmpr¡ } public void setJumper(boolean j) { jmpr = j¡ public void addActionListener(ActionListener 1) / / ... public void removeActionListener(ActionListener 1) { / / ... public void addKeyListener(KeyListener 1) { / / ... public void removeKeyListener(KeyListener 1) { / / ... / / Un método público "normal 11 : public void croak() { System.out.println(!lRibbet! 11) ¡ 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 propiedad). 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 variables 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étodos "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. Extracción de la información Beanlnfo con Introspector Una de las partes más críticas del esquema JavaBean tiene lugar cuando arrastramos una Bean de una paleta y la colocamos en un formulario. El entorno IDE debe ser capaz de crear la Bean (lo cual puede hacerse si existe un constructor predeterminado) y luego, sin tener ningúo acceso al código fuente de la Bean, extraer toda la información necesaria para crear la hoja de propiedades y las rutinas de tratamiento de sucesos. Parte de la solución resulta evidente a partir de lo comentado en el Capítulo 14, Información de tipos: el mecanismo de reflexión de Java permite descubrir todos los métodos de una clase desconocida. Esto resulta perfecto para resolver el problema del diseño JavaBean sin requerir palabras clave del lenguaje como las que existen en otros lenguajes de programación visual. De hecho, una de las principales razones de que se añadiera el mecanismo de reflexión de Java fue para soportar JavaBeans (aunque el mecanismo de reflexión también soporta la serialización de objetos y la invocación remota de métodos, RMI, que resulta útil para la programación normal). Por tanto, podriamos esperar que el creador del entorno IDE usara el mecanismo de reflexión con cada Bean y analizara sus métodos para encontrar las propiedades y sucesos correspondientes. Ciertamente, esto resulta posible, pero los diseñadores de Java querían proporcionar una herramienta estándar, no sólo para hacer que los componentes Bean sean simples de usar, sino también para proporcionar una pasarela estándar para la creación de otros componentes Bean más complejos. Esta herramienta es la clase Introspector, y el método más importante de esta clase es el método estático getBeanInfo( ). Basta con pasar una referencia Class a este método y el método se encargará de interrogar a fondo a dicha clase y de devolver un objeto BeanInfo que se puede diseccionar para determinar las propiedades, los métodos y los sucesos. Normalmente, nosotros no tenemos que preguntarnos nada acerca de esto; 10 más probable es que obtengamos la mayoría de nuestros componentes Bean ya diseñados, y no tenemos por qué conocer todos los detalles subyacentes. Basta con arrastrar los componentes sobre el formulario, configurar sus propiedades y escribir las rutinas de tratamiento para los sucesos 22 Interfaces gráficas de usuario 921 de interés. Sin embargo, la utilización de Introspector para mostrar infonnación acerca de una Bean constituye un ejercicio 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.*¡ public class BeanDumper extends JFrame { private JTextField query = new JTextField(20) prívate JTextArea results = new JTextArea(); public void print(String s) i { results.append(s + "\nll) i public void dump{Class bean) resul ts. setText (" ") i Beanlnfo bi = null; { try { bi = Introspector.getBeanlnfo(bean, Object.class); catch (IntrospectionException el { print (JI Couldn! t introspect 11 + bean. getName () ) ; return; 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 () ) ; Method readMethod = d.getReadMethod(); if(readMethod != null) print(JlRead method:\n u + readMethod); Method writeMethod = d.getWriteMethod(); if(writeMethod 1= null) print(JlWrite method:\n u + writeMethod); print("====================") ; 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("====================,!) ¡ class Dumper implements ActionListener { public void actionPerformed(ActionEvent el 922 Piensa en Java Str ing name = q uery . getTex t (); Class e = null¡ t ry { e = Cl ass . forName(name)¡ catch(Clas sNotFoundException ex) results. setText ( "Couldn' t find 11 + name ) ; return¡ 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 pUblic static void main(String[] args) run(new BeanDumper(}, 600, 50 0) ; ) !!! ; BeanDumper.dump( ) se encarga de realizar todo el trabajo. Primero trata de crear un objeto BeanInfo, y si tiene éxito, invoca los métodos de BeanInfo que generan la infonnación acerca de las propiedades, métodos y sucesos. En Introspeetor.getBeanlnfo(), podemos ver que hay un segundo argumento que le dice a Introspeetor dónde detenerse dentro de la jerarquía de berenci,,- En este ejemplo, se detiene antes de analizar todos los métodos de Object, ya que no estamos interesados en ellos. Para las propiedades, getPropertyDescriptors( ) devuelve una matriz de objetos PropertyDescriptor. Para cada objeto PropertyDescrlptor, podemos invocar getPropertyType( ) para encontrar la clase del objeto que se pasa a los métodos de propiedad o que estos métodos devuelven. A continuación, para cada propiedad, podemos obtener su seudónimo (extraído de los nombres de los métodos) con getName(), el método de lectura con getReadMetbod() y el método de escritura con getWriteMethod( ). Estos dos últimos métodos devuelven un objetio Method que puede de hecho utilizarse para invocar el método correspondiente sobre el objeto (esto es parte del mecanismo de reflexión). Para los métodos públicos (incluyendo los métodos de propiedad), getMetbodDescriptors() devuelve una matriz de objetos MethodDescriptors. Para cada un o, podemos obtener el objeto Method asociado e imprimir su nombre. Para los sucesos, getEventSetDescriptors( ) devuelve una matriz de objetos EventSetDescrlptor. Cada uno de estos objetos puede consultarse para averiguar la clase a la que pertenece el escucha, los métodos de dicho escucha y los métodos para agregar y eliminar escuchas. El programa BeanDumper visualiza toda esta infonnación. En el arranque, el programa fuerza la evaluación de frogbcan.Frog. La salida, después de eliminar los detalles innecesarios, es: Property type : Col or Propert y name: color Read method: pub l ic Col or getColor() Wr ite me t hod: public void se t Col or(Col o r ) == ~= = = = = = = = = = = c === == 22 Interfaces gráficas de usuario 923 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 publ ic public publ ic public pub l ic methods: void setSpots(frogbean . Sp ots) void set Col or(Col or ) vo i d setJumps(int l boolean isJumper() frogbean . Spots getSpots() public void c roak (l publ ic public publ i c public publi c pub l ic public void addActionL i stener(ActionListener) void addKeyListener(KeyListener) Color getColor() void setJumper(bool ean ) i nt getJumps() void removeAc t ionLi stener(ActionLis tener) 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 Remove Listen er Method: p ublic void removeKeyListener(KeyListener ) ~ ~========= = = =~===~= 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 partir 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 ejemplo 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 realizarnos 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 ofrecida por el mecanismo de reflexión). Una Bean más sofisticada El ejemplo siguieute es ligeramente más sofisticado aunque un poco frívolo. Se trata de un control JPanel que dibuja un círculo alrededor del ratón cada vez que éste se mueve. Cuando apretamos el botón del ratón, aparece la palabra "Bang!" en mitad de la pantalla y se dispara un escucha de acción. Las propiedades que podemos modificar son el tamaño del círculo y el color, el tamaño y el texto de la palabra que se muestra cuando se pulsa el botón del ratón. El componente BangBean también tiene sus propios métodos addActionListener( ) y removeActionLlstener( j, de modo que podemos asociar nuestro propio escucha que se disparará cuando el usuario haga clic sobre el compoDente BangBean. Resulta sencillo identificar eD el ejemplo el soporte y propiedades del suceso: JI : bangbean/BangBean.java Una Bean gráfica. package bangbean; import javax.swing .*¡ import java.awt.*¡ import j ava.awt.event.*i import java.io.*¡ impo rt java.util.*; JI 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¡ publ ic String getBangText () { return text; public void setBangText(Str ing newText) text = newText¡ publi c i n t getFon t S ize () { { { re t urn fontSize; publi c void setFontSize(int newSize) { fontSize = n ewS ize; public Color get TextColor() { return tColor ; public void setTextColor(Color newCo lor ) { teo I ar = newCol o r ; publ ic voi d paintCompon en t (Graphics g ) { super.paintComponent (g) ; g. setColor (Colo r.BLACK) ; g.drawOva l(xm - cSize/2, ym - cS ize/2, cSize, cS ize) 1/ Se trata de un escucha unidifusión , qu e es l a 1/ forma más s i mple de gestionar los es cuchas : publ ic void addActionL i stener(ActionLi stene r 1 ) t h rows TooManyLi s tenersException { if(actionListener ! = nu ll ) throw new TooManyListenersException(); actionL istener = 1; pub1ic void removeActionListener(ActionListener 1 ) actionListene r = null; class ML extends MouseAdap te r p ub lic void mousePressed (MouseEve nt el { Graphics 9 = getGraphi cs () ¡ g.se tColor(tCo lor) ¡ g.setFont( new Font( "TimesRoman", Font.BOLD, f ontSize» ¡ i nt widt h = g. getFontMetrics{) .stringWidth{text)¡ g.drawString(text , (getSizeO . widt h - width ) /2 , getSize () . height/2) ; g . d ispose () ; II I nvocar el método de l escuch a : if(actionListener != nul l) actionListener.actionPerformed( new ActionEvent(BangBean.this, Act i onEvent .ACTION_ PERFORMED, null» i class MML extends Mou seMotionAdapter { public vo id mouseMoved(MouseEvent e) xm = e . getX () ; ym = e.gety() ; repa int () i j 926 Piensa en Java publi c Dimensi on getPreferredSize() return new Dimension(200, 200); 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 TooManyListenersException. 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: 11: bangb ean/BangBeanT est .java II {Tímeout: s} Abortar despu és de 5 segundos duran te las pruebas package bangbean¡ import javax.swíng.*i i mport java . awt . *; import java.awt.event.*; i mpo rt java.util . *¡ import static net.mindview.uti l.SwingConsole.*¡ p u blic class BangBean Test extends JFrame { private JTextField txt = new JText Field(20)i II Durante las pruebas, infor mar de la s acciones: class BBL i mp l ements Ac tíonListener { private int count = Di publ ic void actionPer f ormed(ActionEven t e ) { txt.se t Text {IIBangBean act ion 11+ count++ ); 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 publi c static void main (String{ ] args) run( new BangBe anTest(}, 4 00, 500) 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 preparado para componentes Bean, observará que hay muchas más propiedades y acciones de las que el código precedente permite 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 disponibles 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 significativamente en las versiones recientes del JDK). Si esto es un problema, podemos dejar sin sincronizar los métodos 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 sincronizar 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 podemos 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 evitamos 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 sucesos: IJ: gui/BangBean2.java IJ Deería escribir sus componentes IJ poder ejecutarlos en un entorno import import import import import import Beans de esta forma para multihebra. javax.swing.*¡ java.awt.*¡ java.awt.event.*¡ java.io.*¡ java.util.*¡ static net.mindview.util.SwingConsole.*¡ public class BangBean2 extends JPanel implements Serializable { private int xm, ym¡ private int cSize ~ 20¡ JI Tamaño del círculo pri vate String text ~ "Bang!!l ¡ private int fontSize ~ 48¡ private Color teolor = Color.RED¡ private ArrayList actionListeners new ArrayList() ¡ public BangBean2() { addMouseListener(new ML()) ¡ 928 Piensa en Java addMouseMotionListener(new MM()); public synchronized int getCircleSize () { return cSize¡ public synchronized void setCircleSize(int newSize) { cSize = newSize¡ public synchronized String getBangText() { return text¡ public synchronized void setBangText(String newText) { text = newText; public synchronized int getFontSize{) { return fontSize; public synchronized void setFontSize(int newSize) { fontSize = newSize¡ public synchronized Color getTextColor() { return teoIar;} public synchronized void setTextColor(Color newColor) { teoIar = newColor¡ public void paintComponent(Graphics g) super.paintComponent(g) j g.setColor(Color.BLACK) i g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); II II Éste es un escucha de multidifusión, que se usa más a menudo que la solución unidifusión empleada en BangBean.java: public synchronized void addActionListener(ActionListener 1) actionListeners.add(l) ¡ public synchronized void removeActionListener(ActionListener 1) actionListeners.remove(l) i II Observe que esto no está sincronizado: public void notifyListeners() { ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null) i ArrayList Iv = null¡ II Hacer una copia somera de la lista por si alguien II añade un escucha mientras estamos II invocando los escuchas: synchronized(this) { Iv = new ArrayList (actionListeners) ¡ II Invocar todos los métodos escucha: for(ActionListener al : Iv) al. actionPerformed (a) ; class ML extends MouseAdapter public void mousePressed(MouseEvent e) Graphics 9 = getGraphics() i g.setColor(tColor) ; g.setFont( new Font ("TimesRoman", Font .BOLD, fontSize)) j int width = g.getFontMetrics() .stringWidth(text)¡ g.drawString(text, (getSize() .width - width) 12, getSize () .height/2) ¡ g.dispose() ; notifyListeners() ; 22 Interfaces gráficas de usuario 929 class MM public xm = ym = extends MouseMotionAdapter void mous eMoved(Mou seEvent e) e.getX()¡ e.gety(); repaint() i public static void main (S t ring [J args) { BangBean2 bb2 = new BangBean2(); bb2. addAc tionListener(new Act ionListener() public vo id actionPerformed(ActionEvent el System.ou t.println("ActionEvent " + e l i } }) ; bb2. addActionListener (new Acti onLi stener () { pub li c void actionPerformed (ActionEvent e ) System . out. println (IIBangBean2 action rr) ; } }) ; bb2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e l System. out .println ("More actionO!) j } }) ; 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 llamada 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 funcionar correctamente independientemente de si se sincroniza o no. Sin embargo, las cuestiones que hay que tener en cuenta 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 suceder 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 sincronizar 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 ejemplo, 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áticamente 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 necesita 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 capacidades de multidifusión de BangBean2 añadiendo escuchas adicionales. Empaquetado de una Bean Antes de poder incluir componentes JavaBean en un entorno IDE preparado para ese tipo de componentes, es necesario incluirlo en un contenedor Bean, que es un archivo JAR que incluye todas las clases Bean, junto con un archivo de "manifiesto" que dice: "Esto es una Bean". Un archivo de manifiesto es simplemente un archivo de texto que se ajusta a un formato concreto. Para el componente BangBean, el archivo de manifiesto tendría el aspecto siguiente: Manifest-Version: 1.0 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 denominado 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 paquete, 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 último argumento de la línea de comandos iar anterior es un subdirectorio bangbean. Cuando se da a iar el nombre de un subdirectorio, 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 archivo de manifiesto original y volver a invocar jar para crear un nuevo archivo JAR para nuestra Bean. También podemos añadir 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 entorno !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 subdirectorio 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 empaquetar la Bean, y luego cargue Bean Builder o alguna herramienta de construcción de programas basada en Bean para poder probar el componente. Soporte avanzado de componentes Bean Podemos ver lo fácil que resulta construir una Bean, pero en realidad no estamos limitados a las operaciones que se han descrito aquí. La arquitectura JavaBeans proporciona un punto de entrada muy simple para poder aprender los fundamentos, pero también peImite escalar las soluciones para adaptarlas para situaciones más complejas. Dichas situaciones quedan fuera del alcance de este libro, pero las vamos a presentar aquí de forma breve. Puede encontrar más información en http://java.swl.com/beans. Un aspecto en el que se puede añadir sofisticación es el relativo a las propiedades. Los ejemplos que hemos visto hasta ahora mostraban únicanlente propiedades simples, pero también es posible representar múltiples propiedades mediante una matriz. Esto es lo que se denomina propiedades indexadas. Simplemente basta con proporcionar los métodos apropiados (que de nuevo deberán ajustarse a un convenio de denominación para los nombres de métodos) e Introspector reconocerá las propiedades indexadas para que el entorno !DE pueda responder adecuadamente. Las propiedades puedan estar acopladas, lo que significa que enviarán notificaciones a otros objetos mediante un suceso PropertyCbangeEvent. Los otros objetos pueden entonces decidir modificarse a sí mismos basándose en el cambio sufrido por la Bean. Las propiedades pueden estar restringidas, lo que significa que otros objetos pueden vetar una cierla mo 111> Los archivos MXML son documentos XML, por lo que comienzan con una directiva XML de versión/codificación. El elemento MXML más externo es el elemento App lieation, que es el contenedor visual y lógico de mayor nivel para una interfaz de usuario Flex. Podemos declarar marcadores que presenten controles visuales, como por ejemplo la etiqueta Label del ejemplo anterior, dentro del elemento AppUeation. Los controles se incluyen siempre dentro de UD contenedor, y los contenedores encapsulan gestores de disposición. entre otros mecanismos para poder gestionar la disposición de los controles incluidos en ellos. En el caso más simple, como en el ejemplo anterior, AppUeation actúa como el contenedor. El gestor de dispositivo predeterminado de Applieation se limita a colocar los controles verticalmente en la interfaz en el orden que hayan sido declarados. ActionScript es una versión de ECMAScript, o JavaScript, que parece muy similar a Java y soporta clases y mecanismos fuertes de tipado, además de mecanismos de script dinámico. Añadiendo un script al ejemplo, podemos introducir un cierto comportamiento. Aquí, se utiliza el control MXML Seript para incluir código ActionScript directamente dentro del archivoMXML: II, ! gui /flex/hellofl ex2.mxml ll // /,El control DataGrid contiene marcadores anidados para su matriz de columnas. Cuando vemos un atributo O un elemento anidado de un control, sabemos que se corresponde con alguna propiedad, sucesos u objeto encapsulado dentro de la clase ActionScript subyacente. El control DataGrid tiene un atributo id con el valor songGrid, por lo que ActionScript y los marcadores MXML pueden hacer referencia a esa cuadrícula de datos mediante programa empleando songGrid como nombre de variable. La cuadricula de datos DataGrid expone muchas más propiedades que las que se muestran aquí; puede encontrar la API completa para los controles contenedores MXML en la dirección http://livedocs.macromedia.com/flex/15 /asdocs _ en/indexo html. El control DataGrid está seguido de un control VBox que contiene una imagen Image para mostrar la earátnla del álbum junto con la información acerca de la canción, y un control MediaPlayback que permite reproducir archivos MP3. En este ejemplo se descarga el flujo de contenido con el fin de reducir el tamaño del archivo SWF compilado. Cuando se incluyen imágenes o archivos de audio y vídeo en una aplicación Flex, en lugar de descargar el fluj o de datos correspondiente, los archivos pasan a formar parte del archivo SWF compilado y se suministran junto con la interfaz de usuario, en lugar de descargarse bajo demanda en tiempo de ejecución. El reproductor Flash Player contiene codees integrados para reproducir y descargar audio y vídeo en una diversidad de formatos. Flash y Flex soportan el uso de los formatos de imagen más comunes de la Web, y Flex tiene también la posibilidad de traducir archivos SVG (scalable vector graphics) a recursos SWF que pueden integrarse en los clientes Flex. 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 animaciones. 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ámicamente 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 verdaderamente 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 adaptará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 font-family: Arial, 11 sansl! i 11 sansl! i font-size: 16; font-weight: bold¡ .boldText font-family: Arial, 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 archivo 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 controles 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 asignado 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 referencia al objeto RemoteObject que se corresponde con el método Java. Conexión con Java El marcador RemoteObject situado al final del archivo MXML establece la coneXlOn con la clase Java externa gni.flex.songService. El cliente Flex utilizará el método getSongs( l de la clase Java para recuperar los datos con los que rellenar la cuadrícula DataGrid. Para hacer esto, es necesario que la clase externa aparezca corno un servicio, es decir, como un interlocutor con el que los clientes puedan intercambiar mensajes. El servicio definido en el marcador RemoteObject tiene un atributo source que indica la clase Java del objeto RemoteObject, y especifica una función de retrollamada ActionScript, onSongs( l, que hay que invocar cuando se vuelva del método Java. El marcador method anidado declara el método getSongs( l, que hace que ese método Java esté accesible para el resto de la aplicación Flex. Todas las invocaciones de servicios en Flex vuelven asíncronamente mediante sucesos disparados hacia esas funciones de retrollamada. El objeto RemoteObject hace que se muestre un control de cuadro de diálogo y alerta, en caso de que se produzca un error. Ahora podemos invocar el método getSongs( l desde Flash utilizando ActionScript: songService.getSongs() i 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.*¡ public cIass SongService private List songs = new ArrayList(}¡ public SongService () { fillTestData (); } public List getSongs () { return songs; } public void addSong(Song song) { songs.add(song); public void removeSong(Song song) { songs.remove(song); private void fillTestData() { addSong (new Song (nChocolate" "Snow Patrol" I I 938 Piensa en Java "Final St raw", "sp-f inal -straw. jpgU , "chocol ate.mp3 U)) ; addSong{new Song{lIConcerto No. 2 in El!, "Hi lary Hahn" "Bac h: Violin Concertos" "hahn. jpgl! , "ba chv iolin2.mp3 11 )} ; addSong (new Song (" ! Round Midnight Ir IIWes Montgomery", uThe Artistry of Wes MontgomeryU, "wesmontgomery. jpgll, flroundmidnigh t . mp3 11 ) ) j f f f Cada objeto Song es simplemente un contenedor de datos: 11, gui / flex/Song.java package gui.f l ex ¡ pub l ic class Song implements j a va.io .Ser ializable { private Str ing name; private Str ing artist¡ private String album ¡ private String albumlmageUrl; private String songMediaUrl¡ public Song () {} publi c Song(String name, String arti s t, String album, String a lbuml mageUrl, String songMediaUrl) { this.name ~ name; this . artist = artist; thi s .album = album¡ this.albumlmageUrl = albumlmage Ur l¡ th is.songMediaUr l = songMediaUrl¡ publ ic voi d setAlbum(String album) { this.a lbum = album¡} public String getAl bum() { return album; } publ ic void se t AlbumlmageUrl (Stri ng albumlmageUrl) this.albumlmageUrl = albumlmageUrl¡ public Stri ng getAl bumlmageUr l {) { return albumlmageUrl¡ } public vo id setArti st (String artist ) { this. a r tist = artist¡ p ubli c String getArtist () { return art i s t¡ } public v oid setName(String name) { this.name = name; public String getName() { return name¡ } publi c v o id setSongMediaUrl(String songMediaUrl) th is.songMediaUrl = songMediaUrl¡ publi c String getSongMediaUrl{) { re t urn songMediaUrl¡ } /! 1,Cuando se inicializa la aplicación o se pulsa un botón refreshSongsButton, se invoca getSongs( ) y, al volver del método, se llama al método onSongs(event.result) de ActionScript para rellenar la cuadrícula songGrid. He aqui el listado de código ActionScript, que está incluido dentro del control Script del archivo MXML: // : gui/f l ex/songSc r ipt.as function getSongs() { songService.getSongs() ¡ function selectSong(event) { var song = songGrid.getltemAt(event.iteml ndex)¡ showSonglnfo{song) ¡ 22 Interfaces gráficas de usuario 939 func t ion showSonglnfo(song ) { s ong lnfo.text = song.name + newline; songlnfo.text += song.artist + newline; song l nfo.text += song.album + newline¡ a lbumlmage.source = song.albumlmageUr l; songPlayer.contentPath = song.songMediaUrl; songPlayer.visible = true; 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. Modelos de datos y acoplamiento de datos Los controles pueden invocar directamente servicios, y las retro llamadas de suceso de ActionScript nos dan la posibilidad de actualizar mediante programa los controles visuales cada vez que los servicios devuelven datos. Aunque el script que actualiza los controles es bastante sencillo, puede resultar ser bastante largo y engorroso, y su funcionalidad es tan común que Flex gestiona el comportamiento automáticamente, con acoplamiento de datos. En su forma más simple, el acoplamiento de datos permite a los controles hacer referencia directa a los datos, en lugar de requerir código de conexión para copiar los datos en un control. Cuando se actualizan los datos, también se actualiza automáticamente que hace referencia a los mismos sin necesidad de intervención del programador. La infraestructura de Flex responde correctamente a los sucesos de cambio de Ios datos y actualiza todos los controles que estén acoplados a ellos. He aquí un ejemplo simple de sintaxis de acoplamiento de datos: en dicho archivo, y examine la sección incluida en ella y vea la siguiente nota: < !-For seeurity, the whitelist is locked down by default. Uncomment the souree element below lo enable access lo ail classes during deve/opment. We strongly recommend no! alIowing access fa al! saurce files in production, since this exposes Java and Flex system cJasses. * --> Elimine el comentario de esa entrada para pennitir el acceso, de modo que quede *. El significado de ésta y otras entradas se describe en los documentos de configuración de Flex. 22 Interfaces gráficas de usuario 941 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. Creación de aplicaciones SWT Como hemos indicado anteriormente, Swing adoptó el enfoque de construir todos los componentes de la interfaz de usuario píxel por pixel, con el fin de proporcionar todos los componentes deseados independientemente de si el sistema operativo subyacente disponía de ellos o no. SWT adopta una postura intermedia, utilizando componentes nativos si el sistema operativo los proporciona, y sintetizando los componentes si no lo hace. El resultado es una aplicación que para el usuario se asemeja a una aplicación nativa, y que a menudo tiene la velocidad bastante superior a la del programa Swing equivalente. Además, SWT tiende a ser un modelo de programación menos complejo que Swing, lo cual puede resultar deseable en un gran número de aplicaciones. 12 Puesto que SWT utiliza el sistema operativo nativo para realizar la mayor parte posible del trabajo, puede aprovechar automáticamente algunas de las características del sistema operativo que pueden no estar disponibles con Swing; por ejemplo, Windows tiene mecanismo de "representación subpíxel" que hace que las fuentes de caracteres parezcan más nítidas en las pantallas LCD. Resulta incluso posible crear applets utilizando SWT. Esta sección no pretende ser una introducción completa a SWT; simplemente se trata de proporcionar una panorámica de esta biblioteca y de comparar SWT con Swing. Descubrirá que existe una gran cantidad de widgets SWT y que todos ellos son bastante sencillos de utilizar. Puede analizar los detalles en la documentación completa y los muchos ejemplos que podrá encontrar en www.eclipse.org. También hay diversos libros de programación con SWT, y posiblemente aparezcan más en el futuro. 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 directorio 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 plataforma. 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 public static void main(String [] args) Display display = new Display() i Shell shell = new Shell(display) ¡ shell.setText(!!Hi there, SWT!I!)¡ II Barra de título shell.open() ¡ while(!shell.isDisposed()) if(!display.readAndDispatch() ) display.sleep() ; display.dispose() i 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 sistema 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ítulo 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 insertar 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 necesitando 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 significa 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 inmediatamente. Sin embargo, si no hay nada pendiente, invocamos el método sleep( ) del objeto Display para esperar durante un periodo 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 operativo 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 public class ShellsAreMainWindows static Shell[] shells = new Shell[lO]; public static void main(String [] args) Display display = new Display() i for(int i = O; i < shells.length; i++) shells[i] = new Shell(display) ¡ shells[i] . setText("Shell W' + i) i shells [i] . open () ; while(lshellsDisposed()) 22 Interfaces gráficas de usuario 943 if { !disp l ay .readAndDispatch()) display.s l eep() ; display .dispose( ) i s tatic boolean shellsDisposed( ) f ar( int i = o; i < shells. length; i ++) if(shells[il .isDisposed()) return true i return false¡ 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 import impor t import o rg.eclipse.swt.*i org.eclipse.swt.widgets.*¡ org.eclipse .swt.layout.*; java.io.*; public class DisplayProperties public static void main(String [J argsl Display display = new Display(); Shell shell = new Shell(display); shell.setText{IIDisplay Properties!l); she ll. setLayout(new FillLayout{ »); Text tex t ~ new Text(shell, SWT.WRAP I SWT.V_SCROLL) ¡ StringWriter props = new Stri ngWri ter()¡ System.getProperties() .lis t( n ew Prin t Writer (props » ¡ text.se t Text(props.toString(» ¡ shell.openO; while(!she l l.isDisposed {» if {!display.readAndDispatch (» display.sleepl) ; display.dispose{) ¡ 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 argumento. 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 dificiles o imposibles de cambiar, excepto a través del constructor. Compruebe siempre la documentación del constructor del widgel 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. Eliminación del código redundante Antes de continuar adelante, observe que hay que realizar ciertas cosas para cada aplicación SWT, de la misma forma que existían acciones duplicadas en los programas Swing. En SWT, siempre creamos un objeto Display, construimos un objeto 944 Piensa en Java 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 eliminar 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 . *¡ public in t erface SWTApplicati on voi d crea t eConten ts(Composite paren t ) ¡ } /// ,- A la aplicación se le entrega un objeto Composite (SheU es una subclase) y la aplicación debe usar este objeto para crear todos sus contenidos dentro de createContcnts( l. SWTConsole.run( ) invoca createContents( ) en el punto apropiado, establece el tamaño de la sheU de acuerdo con lo que el usuario le haya pasado a run( l, abre la shell y luego ejecuta el bucle de sucesos, eliminando finalmente la shell al salir del programa: jI : swtjutil/SWTConsole . java pac kage swt.util¡ impor t org.ec li pse.swt.widgets.*¡ public class SWTConsol e { public sta tic void run(SWTAppli cation swtApp, int width, int height) { Displ ay display = new Di splay( ) ; Shell s hell = new Shell(display); s he l l.setText (swtApp.getClass () . getSimpleName( ) ¡ swtApp.createContents (shel l } ¡ s hel l. se tS ize(width, he i ght) ¡ shell . ope n () ; whil e (! shell. isDisposed ()) { if(! display.re adAndDi spatch() ) di splay.sleep() ; display .di s pose () ; } /// , Esto establece también la barra de título, asignándole el nombre de la clase SWTApplication, y establece la anchura y altura de la SheU mediante los valores width y hcighl. Podemos crear una variante de DisplayProperties.java que muestre el entorno de la máquina, usando SWTConsole: ji : swt j DisplayEnvironment . java i mport swt .util.*¡ impore org . eclipse .swt. * ; i mport org.eclipse.swt . widgets .*¡ import org.eclipse.swt . l ayout. *¡ import java.util.*¡ public clase DisplayEnvironment implements SWTApplication public void c reateContents (Composi t e parent ) { parent.setLayou t( new FillLayout( )) ¡ Text text = new Text (p a r en t, SWT . WRAP I SWT . V_ SCROLL )¡ for {Map.Ent ry entry: System.getenv() .entrySet( )) { text.append(entry.ge t Key () + n: 11 + ent r y.ge t Value() + " \ n") i public stat i c void main(St r ing [) args) { 22 Interfaces gráficas de usuario 945 SWTConsole,run(new DisplayEnvironment() I 80 0, 600) i ///,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. *¡ public cI ass Menus implements SWTApplication { priva te static She ll shell¡ public void createContents(Composite parent ) s hel l ~ parent.get Shell () ; Menu bar = new Menu{shel l , SWT.BAR)¡ shell .setMenuBar( bar l i Set words = n ew TreeSet ( new TextFi l e (lIMenus. j ava U, u\ \W+ n)); Iterator i t = words.iterator () ; while (it. next () . matches ( 11 [O - 9 ] + " ) ) ; II De splazarse hasta pasados los números. Menultem[] mltem = new Menul tem[ 7] ; for(in t i = O: i < mltem.length¡ i++ ) { mltem[i] = new Menultem(bar, SWT.CASCADE) ¡ mltem [i ] . se tText (it . nex t () ) ; Menu submenu = new Menu(shell, SWT .DROP_DOWN ) ; mlt em [i] .setMenu(submenu) ¡ int i = O; while(it. hasNext()) addltem(bar, i t, mltem[i ] ) ; i ~ (i + 1 ) % mltem.length ¡ s tat i c Listene r listener = new Listener() pUbl i c void handleEvent(Event el { System.ou t.println{e . toString{)) ; } }; vo i d add l tem(Menu bar , I terator i t, Menu l tem mlte m) { Menultem i tem = new Menultem(mltem .getMenu (),SWT .PUSH)¡ i tem . addLi stener {SWT.Selection, l i stener) ¡ item. setText (it. next () ) ; publi c static void ma in (String [] args ) { 946 Piensa en Java sWTConsole .run(new Menus(), 600, 200) ¡ ) /// ,- Los objetos Menu deben colocarse dentro de una Shell, y Composite permite deteOllinar la shell mediante getShell( ). TextFile procede de net.mindview.util y ya lo hemos descrito antes en el libro; aquí, se rellena un conjunto TreeSet con palabras, de foOlla que aparezcan ordenadas. Los elementos inicíales son números, que se descartan. Utilizando el flujo de palabras, se asigna un nombre a los menús de nivel superior de la barra de menús y luego se crean los submenús y se rellenan con palabras hasta que no quedan más palabras. En respuesta a la selección de uno de los elementos de menú, Listener simplemente imprime el suceso para que podamos ver el tipo de información que contiene. Cuando se ejecute el programa, verá que parte de la infoOllación incluye la etiqueta del menú, de manera que podemos pasar la respuesta del memi en dicha infoOllación; o bien podemos proporcionar un escucha diferente para cada menú (que es un enfoque más seguro, de cara a la intemacionalización). Paneles con fichas, botones y sucesos events SWT dispone de un rico conjunto de controles, denominados widgets. Examine la documentación de org.eclipse.swt.widgets para ver los controles básicos y org.eclipse.swlcustom para ver otros más sofisticados. Para ilustrar algunos de los widgels básicos, este ejemplo coloca una serie de subejemplos dentro de paneles con fichas. También podrá ver cómo crear objeto Composite (aproximadamente equivalente a los paneles JPanel de Swing) para insertar elementos dentro de otros elementos: 11 : swt/TabbedPane.java II Colocación de componentes SWT en pane les con fi c has. i mport i mport import import import import import swt. u ti l .*¡ org.eclipse.swt.*; org.eclipse.swt.widgets.*¡ org.eclipse.swt.events.*; org.ec l ipse.swt.graphics .*¡ org. ec lipse.swt.layou t.*¡ org.eclipse.swt.browser . *¡ public clas5 TabbedPane implements SWTApplication { privat e static TabFolder folder¡ pri vate static Shell shell¡ publ i c void createCon tent s (Compo s i te parent) shell = parent .getShell(); parent.setLayout(new FilILayout() ) ¡ folder = new TabFolder(shell, SWT .BORDER) ¡ labelTab( ) ; directoryDialogTab() ¡ buttonTab() ; s liderTab () í scribbleTab() i browserTab () ¡ public static void label Tab () { Tabltem tab = new Tabltem( folder, SWT.CLOSE)¡ tab. setText ("A Label ") ¡ II Texto de la ficha tab. setTool TipText ( l/A simp le label " ) ; Label label = new Label( folder. SWT.CENTER ) ¡ l abel. setText ( "Label text 11 ) ; tab.setCon trol (label ) ; public static voíd direc to ryDialogTab() Tabltem tab = new Tabltem( folder , SWT.CLOSE) ¡ tab. setText ( I/Directory Dialog ") ¡ tab.se t ToolTipText ( lIS e l ec t a directoryU ) i 22 Interfaces gráficas de usuario 947 final Buttan b '" new Buttan{ f older, SWT.PUSH:) ; b . setText(IIS e l ec t a Directaryt<) ; b. addListener (SWT.MouseDawn, new Lis tener (} { public vaid handleEvent(Event el ( BirectaryDialog dd = new DirectoryDi a l og{shell); Stri ng path = dd.open(); if (path ! = null) b. set'Text (path ) i } }) ; tab.setCantrol (b).; public static va id buttonTab () Tabltem tab = new Tabl tem(folder, SWT.CLOSE ) i tab.se tText(IIButtans lr ) i tab. s etTaa lTipText (!!Different kinds of Buttons,u). i Composit'e campos ite = new Composite (folder, SWT .NONE) ; compos ite .setLayout (new GridLayoutf4, true) ); fo r fint d i r : new' i n t [) { SWT.UP, SWT.R ~GHT, SWT. LEFT, SWT.Do~m }) { Buttan b = new Button (camposite, SWT.ARROW ¡ dir); b. addL i stener (SWT.MouseDawn, lis tener ); newBut ton(compas ite, SWT.CHECK, nCheck buttan " ); newButton(compas ite, SWT.PUSH, IIPush buttan!!); newButton(composite, SWT . RADIO, IIRadi o buttonll) ¡ n ewBut t on(compos ite r SWT.TOGGLE , II Toggle button " ); newButton(composite, SWT. FLAT, "Fla t bu t ton " ); tab .se 't Contro l( composite) ; private static Listener li stener = new List ene r () { publ ic void handleEvent {Event e) { MessageBox m = new MessageBox(shell, SWT.OK) ¡ m.setMessage(e . toString()) ; m.openO; } }; private stat i c void newButton{Composite composite, int type, String labe! ) { Button b = new Button(compo site, type); b.setText( l abell ; b.addListener(SWT.MouseDown, l istener); public static void sliderTab {) Tabltem tab = new Tabl tem(folder, SWT . CLOSE ) ; tab . setText ( "S lide rs and Progress b ars ll ) ; tab. setToolTipText (IITied Slider to ProgressBar") i Compos it e composite ~ new Composite(folde r, SWT.NONE)¡ composi te.set Layout{new GridLayout(2, true »; fina l Slider slider = n ew Slider(composite, SWT . HORIZONTAL}¡ fi nal ProgressBar p r ogre ss = new ProgressBar{composite , SWT.HORIZONTAL) i slider.addSe lectionListener {new SelectionAdapter() publ i c void widgetSelected(SelectionEvent event} progress.setSelection(slider.getSelection(») ; } }l ; tab.setControl(composite ) i 948 Piensa en Java publ i c static void scribbleTab () { Tab l tem t ab = new TabItem( f o l der, SWT,CLOSE ) ¡ tab. setText( lIScribble ll ) i tab.setTool TipText(lISimple graphics: drawing"} i final Canvas canvas = new Canvas(folder, SWT.NONE ) ; ScribbleMouseListener sml = new ScribbleMouseListener(); canvas.addMouseListener(srnl) i canvas.addMouseMoveLi s t e ner (sml) ; tab.setControl(canvas) i pri va t e s t ati c class Scribbl eMouseListener extends MouseAdap ter i mplements MouseMoveListener private Po int p = new Point(O, O) j public vo id mouseMove(Mou seEvent el { iflle.stateMask & SWT.BUTTON1) == O) re turn¡ GC ge = n ew GC « Canvas)e , widg et ) ¡ gc.drawLine(p.x, p . y, e .x, e.y) i gc . d ispo se() i upda t e Point (e ) i publi c voi d mouseDown(MouseEvent el { updatePoint(el pri va t e void updatepoint(MouseEvent e l { p.X p. y i e.Xi = e. y; pub lic st ati c void b rowser Tab () { TabItem tab = new TabItem(folder , SWT.CLOSE) i tab.setTex t ( "A Br owser " ) i tab. setToolTipText("A Web browser ") ¡ Browser browser = null¡ try { browser = new Br owse r(fo lde r, SWT.NONE) i c atch( SWTError el { Label labe l = new Label (f olde r , SWT.BORDER) ¡ label.setText("Could n ot initialize browser"); t ab.s etControl(label) j if(browser 1= nu ll) { b rows er. setUrl ( "http://www.mindview.net ll ) tab .setControl (browser) ¡ i pUblic static void ma i n{ String [] args) { SWTConsole.run{new TabbedPane( ) , 800, 600); 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 completamente 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 import import i mport org. e clipse.swt .*i org.e c lipse .swt.widget s .*¡ org . eclipse .swt.event s.*; org.eclipse .swt.layout .*i cIass SineDraw extends Canvas { prívate static f ina l i n t SCALEFACTOR 200; prívate i nt cyc l es; private int points; private double[] sines; private int[] pts ; public SineDraw(Composit e parent, i nt s tyle) super (parent, s tyl e ) ; a ddPa i n tL i stener (new PaintLi stener () { public void paintControl (Pa i n CEvent e l i n t maxWidth "" -g etS i ze() . Xi double h step "" (doublelmaxWi d th / (double)points i int maxHeight "" get Siz e() .Yi pts = new int[points ]; for( i nt i = O; i < points¡ i++) pts [i ) = (i nt ) «(sines[i) * maxHeight / 2 * .95 ) + ImaxHeight / 2»; e.gc.setForeg round( e.di splay .getSys temColo r (SWT. COLOR_ RED )) ¡ for ( i nt i = l; i < points ¡ i ++ ) { int xl (int) « i - 1) * hstep); int x2 (int) (i * hstep); int yl pts[i - 11 ; i nt y2 pts [i] ; e.gc.dr awLine(xl , y1, x2, y2)¡ ) ) ; setCyc l es(S) j public vo id setCycl es(int newCycles) cycle s = newCycles¡ p oints = SCALEFACTOR * cycles * 2; sines = new double [poi nts)i for (i n t i = O; i < points¡ i ++) double radians = (Math . PI / SCALEFACTOR ) si nes( i] = Mat h . sin(radians ) i redraw() ; * i¡ 950 Piensa en Java public class SineWave implements SWTApplication { private SineDraw sines; private Slider slider¡ public void createContents(Composite parent) parent.setLayout(new GridLayout(l, true)) i sines ~ new SineDraw(parent, SWT.NONE); sines.setLayoutData( new GridData(SWT.FILL, SWT.FILL, true, true)) sines.setFocus() ; Blider = new Slider(parent, SWT.HORIZONTAL); slider.setValues(5, 1, 3D, i 1, 1,1) i slider.setLayoutData( new GridData{SWT.FILL, SWT.DEFAULT, true, false)); slider.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { sines.setCycles{slider.getSelection{)) ; ) l) ; 1 public static void main(String[] args) { SWTConsole.run(new SineWave(), 700, 400)¡ En lugar de JPanel, la superficie de dibujo básica en SWT es Canvas. Si comparamos esta versión del programa con la versión Swing, veremos que SineDraw es prácticamente idéntico. En SWT, obtenemos el contexto gráfico gc a partir del objeto suceso que se entrega al escucha PaintListener, y en Swing el objeto Graphics se entrega directamente al método paintComponent( l. Pero las actividades realizadas con el objeto gráfico son iguales y setCyc1es( l es idéntico. createContents( ) requiere algo más de código que la versión Swing, para disponer los elementos y configurar el deslizador y su correspondiente escucha, pero de nuevo, las actividades básicas son aproximadamente iguales. Concurrencia en SWT Aunque AWT/Swing es monohebra, resulta posible violar fácilmente esa caracteristica monohebra de manera que se obtenga 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 import import import import import import import import swt.util.*¡ org.eclipse.swt.*i org.eclipse.swt.widgets.*; org.eclipse.swt.events.*¡ org. eclipse. swt. graphics. * i org.eclipse.swt.layout.*; java.util.concurrent.*¡ java.util.*; 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 static Random rand = new Random()¡ private static RGB newCol or() { return new RGB (rand.nextlnt (25 5) , rand.nextlnt(2ss ), rand.nextlnt(2sS)); 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) ; catch {Int erruptedException e) II Forma acep table de salir catch(SWTExceptíon e) { II Forma aceptable de salir: nuestro padre II ha sido terminado desde debajo de nosotros. public class ColorBoxes implements SWTApplication { private int grid = 12; prívate int pause = S O; public void createContents(Co~posite parent) { GridLayout gridLay_o ut = new GridLayout (grid, true ) ; g r i dLayou t.horizontalSpacing = Oí gridLayout. vertical Spacing = O i parent.setLayout(gridLayout) ; ExecutorService exec = new DaemonThreadPool 'E xecutor() ¡ for(ínt i = O; i < (grid * grid)¡ i++) { final CBox cb ~ new CBox(parent, pause)¡ cb. setLayoutData (new GridData (GrídData. PIL,L_BOTH) ) i exec. execute (e_b ) ; } public static void main(string (] args) ColorBoxes ,b oxes = new ColorBoxes () ; if{args. length > O) boxes.grid = new Integer(args[Q ]); if(args.length > 1) 952 Piensa en Java box es. pa u s e = new Integer( args [ l ]) ; SWTCOnsole. r u n (boxes, 5 0 0, 4 0 0 } ; } // / , Como en el ejemplo anterior, el dibujo se controla creando un escucha PaintListener con un método paintControl( ) que se invoca cuando la hebra SWT está lista para pintar el componente. El escucha PaintListener se registra en el constructor de CBox. Lo que difiere notablemente en esta versión de CBox es el método run(), que no puede limitarse a invocar redraw() directamente, sino que tiene que enviar la llamada a redraw() al método asyncExec() del objeto Display, que equivale aproximadamente al método SwingUtillties.invokeLater( ). Si sustituimos estos por una llamada directa a redraw( ), comprobaremos que el programa simplemente se detiene. Al ejecutar el programa, veremos pequeños artefactos visuales: líneas horizontales que atraviesan ocasionalmente un recuadro. Esto es debido a que SWT no utiliza doble buffer de manera predetenninada, mientras que Swing si lo utiliza. Trate de ejecutar la versión Swing al lado de la versión SWT y podrá observar la diferencia más claramente. Podemos escribir código para utili zar un doble buffe r SWT; podrá encontrar ejemplos en el sitio web www.eclipse.org. Ejercicio 42: (4) Modifique swt!ColorBoxes.java para que comience distribuyendo una serie de puntos ("estrellas") por todo el lienzo y luego cambie aleatoriamente el color de esas "estrellas". ¿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 embargo, 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 aspecto 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 lenguaje. 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 preparado: 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 programació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 programació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 programadores. 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 usuario 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 muyadecuado 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 relevantes como para incluirlos en la versión impresa de la cuarta edición .dellibro. Thinking in C: fundamentos para Java En www.MindView.net. podrá descargarse gratuitamente el seminario Thinking in C. Esta presentación,creadaporChuck Allison y desarrollada por MindView, es un curso Flash multimedia que proporciona una introducción a la sintaxis, los operadores y las funciones de C en las que se basa la sintaxis de Java. Observe que es necesario instalar en nuestro sistema el reproductor Flash PIayer de www.Macromedia.com para poder ver la presentación Thinking in C. Seminario Thinking.in Java Mi.empresa, MindView, Inc., proporciona seminarios de formación públicos o privados de carácter práctico y de. cinco .días de duración basados en el material de este libro. Anteriormente denominado seminario lfands-On Java, se trata .de nuestro principal seminario introductorio, que proporciona la base para seminarios más avanzados. Una .serie de materiales seleCcionados de cada capítulo repr-esentan una lección, que va seguida por un período de ejercicios monitorizado, de ·modo qlle cada-estudiante recibe ,una atención -personalizada. Puede .encontrarinformación sobre horarios y lugares donde .se :imparte este seminario, junto con testimonios y otros detalles en www.MindView.net. 956 Piensa en Java Seminario en CD Hands-On Java El CD Hands-On Java contiene una versión ampliada del material del seminario Thinking in Java y está basado en este libro. Proporciona al menos una parte de la experiencia del seminario real, pero sin los gastos de viaje asociados. Existe lUla conferencia audio y una serie de presentaciones correspondientes a cada uno de los capítulos del libro. Yo he creado personalmente dicho seminario y me he encargado de narrar el material contenido en el CD. El material está en fonnato Flash, por lo que debería poder ejecutarse en cualquier platafonna que soporte Flash Playero El CD de Hands-On Java CD puede adquirirse a través de www.MindView.net. donde encontrará demostraciones de prueba del producto. Seminario Thinking in Objects Este seminario introduce las ideas de la programación orientada a objetos desde el punto de vista del diseñador. Explora el proceso de desarrollo y construcción de un sistema, centrándose principalmente en los denominados "Métodos ágiles" o "Metodologías ligeras", y en especial en XP (Extreme Programming). En el seminario introduje las metodologías en general, lUlas pequeñas herramientas como las técnicas de planificación mediante "tarjetas índices" descritas en Planning Extreme Programming por Beck y Fowler (Addison-Wesley, 2001), las tarjetas CRC para diseñar objetos, la programación por pares, la planificación de iteraciones, las pruebas unitarias, la construcción automatizada, el control de código fuente y temas similares. El curso incluye un proyecto XP que se desarrolla a lo largo de lUla semana. Si está iniciando un proyecto y desea comenzar a utilizar técnicas de diseño orientadas a objetos, podemos emplear su proyecto como ejemplo y disponer de un primer prototipo de diseño al final de la semana. Visite "WWw.MindView.net para obtener la infonnación sobre horarios y lugares donde se imparte este seminario, así como testimonios y otros detalles Thinking in Enterprise Java Este libro se emplea a partir de algunos de los capítulos más avanzados de las ediciones anteriores en Thinking in Java. Este libro no es un segundo volumen de Thinking in Java, sino que se centra más bien en cubrir los temas avanzados de la programación empresarial. Está disponible (aunque todavía en desarrollo) en forma de descarga gratuita en www.MindVíew.net. Pero como es un libro separado, debe expandirse para cubrir los temas necesarios. El objetivo, como el de Thinking in Java, es proporcionar una introducción comprensible a los fundamentos de las tecnologías de programación empresarial, para que el lector esté preparado para acometer un estudio basado en dichos temas. La lista de temas que incluye, entre otros, es la siguiente: • Introducción a la programación empresarial • Programación de red con Sockets y canales • Invocación remota de métodos (RMI) • Conexión a bases de datos • Servicios de denominación y directorio • Servlets • Java Server Pages • Marcadores, fragmentos JSP y lenguaje de expresión • Automatización de la creación de interfaces de usuario • Enterprise JavaBeans • • 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. Thinking in Patterns (con Java) Uno de los pasos adelante más importantes dentro del diseño orientado a objeto es el movimiento de los "patrones de diseño", que se tratan en Design Patterns, de Gannna, Helm, Johnson & Vlissides (Addison-Wesley, 1995), Dicho libro muestra 23 clases generales de problemas junto con sus soluciones, escritos principalmente en C++, El libro Design Patterns es una fuente autorizada de lo que ahora se ha convertido en un vocabulario esencial, si es que no obligatorio, para la programación orientada a objetos. Thinking in Patterns introduce los conceptos básicos de los patrones de diseño junto con una serie de ejemplos en Java. El libro no pretende ser una simple traducción de Design Patterns, sino más bien una nueva perspectiva desde el punto de vista de Java. No está limitado a los 23 patrones tradicionales, sino que incluye también otras ideas y técnicas de resolución de problemas. Este libro comenzó con el último capítulo de la primera edición de Thinking in Java, y a medida que las ideas continuaron desarrollándose, quedo claro que era necesario incluir ese material en su propio libro independiente. En el momento de escribir estas líneas, el libro se encuentra todavía en desarrollo, pero el material ya está muy avanzado y ha sido utilizado en numerosas presentaciones del seminario Objects & Patterns (que ahora se ha dividido en los seminarios Designing Objects & Syslems y Thinking in Patterns). Puede encontrar más información acerca de este libro en wyvw.MindView.net. Seminario Thinking in Patterns Este seminario ha evolucionado a partir del seminario Objecls & Pallems que Bil! Venners y yo mismo hemos impartido en los últimos años. Dicho seminario llegó a estar demasiado cargado de contenido, por 10 que lo dividimos en dos seminarios independientes: éste y el seminario Designing Objects & Systems, descrito anterionnente. El seminario se ajusta de manera bastante fiel al material y a la presentación del libro Thinking in Patterns, por lo que la mejor fonna de conocer los detalles sobre el seminario es consultar la información relativa al libro en wyvw.MindView.net. Buena parte de la presentación que se centra en el proceso de evolución del diseño, comenzando por una solución inicial y repasando la lógica y el proceso que subyace a la evolución de esa solución hacia diseños más apropiados. El último proyecto mostrado (una simulación de reciclado de residuos) ha ido evolucionando a lo largo del tiempo, y se puede examinar esa evolución como un prototipo de la manera en que un diseño real puede comenzar en fonna de una solución adecuada a un problema concreto, para tenninar evolucionando hasta convertirse en una solución flexible para toda una clase de problemas. Este seminario le ayudará a: • Aumentar enormemente la flexibilidad de sus diseños. • Incorporar a los diseños los conceptos de ampliabilidad y reusabilidad. • Crear mecanismos de comunicación más densos acerca de los diseños utilizando el lenguaje de patrones. • Después de cada presentación, hay un conjunto de ejercicios sobre patrones de diseño que los asistentes deben resolver, siendo dirigidos durante la escritura del código para poder aplicar patrones concretos a la solución de los problemas de programación. Visite wyvw.MindView.net para obtener más información acerca de los horarios y los lugares donde se imparte este seminario, junto con testimonios y detalles adicionales. Consultoría y revisión de diseño Mi empresa también proporciona servicios de consultoría, de tutoría, de revisión de diseño y revisión de implementación como ayuda durante el ciclo de desarrollo de un proyecto, incluyendo los primeros proyectos Java de cualquier empresa. Visite ww¡v.MindView.nel para obtener más infonnación sobre disponibilidad y otros detalles. Recursos 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 documentos 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. Editores y entornos lOE Existe una sana competición dentro de este área. Muchos de los productos son gratuitos (y los que no lo son, normalmente disponen de versiones de prueba gratuitas), por lo que lo mejor es probar los distintos· productos y ver cuál se adapta mejor a sus necesidades. He aquí algunos de ellos: JEdit, es un editor gratuito diseñado por Slava Peslov, escrito en Java, con lo que obtenemos la ventaja adicional de poder ver eu acción no. aplicación de escrÍtorio desarrollada Java. Este editor está basado fuertemente en la utilización de plüg· ins, muchos de los cuales han sido escritos por la comnoidad Java. Puede descargarlo en wwwjedit.org. NetBeans, un entorno IDE gratuito de Sun, disponible en www. neibeans.OIg. Diseñado para la construcción de interfaces GUI mediante el procedimiento de arrastrar y colocar, para la creación, edición y depuración de código,etc. Eclipse, un proyecto de código abierto respaldado por lBM, entre otros. La plataforma Eclipse también está diseñada para constituir no. base amplíable de desarrollo, de modo que se pneden construir aplicaciones autónomas sobre Eclipse. Este proyecto desarrolló el lenguaje SWT descrÍto en el Capítulo 22, Interfaces gráficas de usuario. Puede descargarlo en www.Eclipse.otg. IntelliJ IDEA, el entorno comercial favorito de no gran número de programadores Java,. muchos de los cuales afirman que IDEA siempre está a noo o dos pasos de Eclípse, posiblemente porque IntelliJ no trata tanto de Crear un entorno !DE como una plataforma de desarrollo, sino' que se limita a los aspectos del entorno IDE. Puede descargar una versión gratuita en lIV\11Wjetbrains.com. 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 voluminoso, resulta caro y la calidad de los ejemplos no me parece adecuada. Sin embargo, es tul buen lugar en el que consultar 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 comprender 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 respecta 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 actualmente, 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 demasiado 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 imaginarse 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 principal 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 explicaciones 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 tambié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 embargo, los seres humanos tienen una tendencia psicológica muy curiosa. Si quieren creer que algo resolverá sus problemas, tratará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 realidad, que no es tan bueno). El mecanismo que subyace a este comportamiento es que si podemos conseguir que otras personas 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 herramientas 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 necesitamos 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 estudios. 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 trabajo 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 historias 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 ultracomplejos: 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 algoritmos 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 favorito ·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. Mi propia lista de libros No todos estos libros están actualmente disponibles, pero algunos de ellos pueden encontrarse en la librerías de segunda mano. Computer [nterfacing wit/¡ Pascal & C (publicado por Eisys, 1988. Disponible a la venta únicamente en www.MindView. net). Una introducción a la electrónica escrita en los años en que CPIM seguía siendo el rey y DOS no pasaba de ser un advenedizo. Por aquel entonces, yo usaba lenguajes ·de alto nivel y a menudo el puerto paralelo de la computadora para controlar diversos proyectos electrónicos. Adaptado a partir de las colurnnasque yo escribía en la primera y mejor revista de aquellas en las que he contribuido, Micro Cornucopia. Esta revista, llamada también Micro C cerró mucho ,antes de{jue Internet ·apareciera. La creación de este libro fue una 'experiencia editorial extremadamente satisfactoria, Using 'C++ (OsbomelMcGraw-Hi11, 1989). Uno de los primeros libros sobre C++. Está agotado y ha sido sustituido por su segunda edición, cuyo título es C++ [nside & Oul. C++ [nside & Out (OsbomelMcGraw-Hi11, 1993). Como ya digo, es la segunda edición de Using C++. El lenguaje CH de este libro es razonablemente preciso, pero fue escrito en tomo a 1992y posteriormente fue s ustituido por Thinking in C++, Puede encontrar más detalles acerca de este libro y descargar el código fuente en www.MindView.net. Thinking inC++, 1" Edición (Prentice Hall, 1995). Consiguió el premio Jolt de la revista Software Development Magazine como mejor Iibm del año, Thinking in C++, 2" Edición, Volumen 1 (Prentice Hall, 2000). Puede descargarlo de www.MindView.net.Actualizadopara adaptarlo al estándar del lenguaje recién finalizado. T/¡inking in C++, 2" Edición, Volumen 2. Escrito en colaboración con ChuckAllison (Prentice HalI, 2003). Puede descargarlo en www.MindView.net. Black Belt C++: T/¡e Master's Colleetion, Bruce Eckel,editor (M&T Books, 1994). Agotado. Una colección de capítulos escntos por diversos expertos en C++ y basados en sus presentaciones dentro del óclo dedicado a e++ en la conferencia Software Development Conference,de la cual fui motlerador.La cubierta de este libro fue la ·que me animó :a decidir todos los ,futuros diseños de cubierta, T/¡illking in Java, 1" Edición (prentice HaH, 1998). La primera edición de este hbro ganó ,elpremia .a la productividad de la 'levista Software Developmenl Magazine, el premio del editor de la revista Java Developer s Journal y el premio de los lectores de la re"ista Java World como mejor libro, Puede ,descargarlo en www.MindView.net. Tlrinking in Java, 2" Edición :(Prentice Hall, 2000). Esta edición ganó el premio del editor de la revista JavaWorld ·como mejor libro. Puede descargarse en www.MindView.net. TlJiliking in Java, 3" Edición, (Prentice Hall, 2003). Esta ·edición ganó el premio 10lt de la revista Softw""e Development Magazine como mejor libro del año, junto con otros premios que se indican en la contraportada. Puede descargarlo de )1lW;w.MindView.net. índice dinániico, tardío o en tiempo de ejecu- . 51 ! ~ . 49 & &. 55 &&. 51 &~. 55 l ' 55 11 . I~ 51 'S5 + + . 48 String. conversión con operador + 44,59,3 17 < .NET·20 .new, sintaxis' 214 ,lhis, sintaxis 2L4 @ @. símbolo para anotaciones' -693 @author' 38 .@Deprecated, anotación' 693 @deprecated, marcador Javado.c . 39 @docRoot . 38 @inheritDoc . .38 .@interfaces y extends, palabra -clave- 700 @link' 37 @Overrid.e . 693 @param' 38 @Retention . 694 @return· 38 @see ' 37 @since . 38 @SuppressWarnings . 693 @Target . 694 @Test "694 @Test para @Unit . 709 @TestObje.ctCleanup para @Unit . 715 @TestObjectCreatepara @Unit . 713 @throws' 38 @Unit'709 @version . 38 [J, operador de indexación' 110 A A·55 ~ '55 < ·49 «, '55 «~. 55 <~ . 49 ~ ' 49 > > '49 ~ '49 »· 55 »~ . 55 A abstracción' 1 Abstraet Window Toolkit (AWT) . 857 abstraet, -palabra ,clave . 189 AbstractButton . 876 AbstractSequentialList . 558 AbstractSet· 514 8_ CCesO: clases internas y derechos de . 213 control de . 121, 136 control de, violación con la reflexión' 387 de clase' 134 de paquete' 129 dentro de un directorio a través del paquete predeterminado' 130 especificadores de . 5, 121, 128 'acoplamiento: ~) . 49 mayor que (» . 49 menor (J igual que «~) 49 49 menor que mensaje, enVÍo de . 3 Mensajero' 396, 516, 559 menús: JDialog, JApplet, JFr.me . 890 JPopupMenu . 894 meta-anotaciones' 695 metadatos . 693 Method . 375, 922 MethodDescriptors . 922 método de factoría, patrón de diseño· 208, 222,371,399, 604 método de plantillas, patrón de diseño 231,365,426,558, 631,768,840, 843 métodos: acoplamiento de' fas llamadas a . 168 adición de más métodos al diseño· 137 aplicación de un método' a una secuencia· 469 clases internas en ámbitos y . 217 comportamiento de los métodos polimórficos entre de' cortstmctores . 181 creación de alias en las, llamadas a . 46 distinción entre métodos sobrecargados ·88 final , 159, 168, 182 genéricos· 403 inicialización de variables de . 102 llamadas a métodos' en linea' 159 polimorfismo 165 private . 173, protected' 153 recursivos' 322 sobrecarga' 87 static·96 sustitución de métodos private . 173 Meyer, Jeremy' 693,121 ,906 Meyers, Scott . 5 micropruebas de rendimiento 566, 835 Microsoft Visual BASrC . 918 miembro: «) . . de datos' 4 inici'nlizadores de . 177 objeto' 6 migración, compatibilidad de la' 419 mixin' 458 mkdirs( ) . 596 mnemónicos (atajos de teclado) . 894 módul" '46 monitor, para concurrencia· 756 MOllO · 20 montaje de clases' 357 multidifusión . 926 multidimensional, matriz· 488 multiparadigma, programación . 2 multiplicación·46 multitarea· 729 :tv1XML, Macromedia Flex fonnato de entrada' 932 mxmlc" Macromedia Flex compilador' 933 N net.mindview.util.SwingConsole· 86 1 Neville, Sean . 932 new,. E/S' .' 616 new, operador' 97 Y matrices' 111 newlnstance( ) . 356, 878 reflexión' 356 next( ), ¡terator . 253 mo·616 buffer' 616 channel . 616 rendimiento· 630 no equivalente (!=) . 49 no modificable, colección o mapa' 576 nombres: al combinar interfaces· 202 colisiones de . 126 cualificados' 356 de paquetes' 31,124 North, BorderLayout . 866 NOT, lógico (!) . 51 notación exponencial 54 notify AI!( ) . 784 notifyListeners( ) . 929 nul! . 26 NullPointerException . 294 o Object: clase raíz estándar 142 herencía 142 ObjeclOutputStream . 639 objeto· 2 activo· 850 alcanzable 578 asignación de objetos por copia de referencias . 45 bloqueo, para concurrencia· 756 Class . 353, 651, 757 creación' 86 creación de alias· 45 de transferencia de datos· 396, 516, 559 equals( ) . 50 equivalencia de' objetos y de referencias 50 equivalente (--) 49 final' 156 getClass( ) . 354 hashCode( ) . 541 información de tipos· 35-1 interfaz de un . 3 matrices son objetos-de primera clase' 485 miembro' 6 proceso de creación del· 107 red de objetos' 639 serializaci6n . 639 wait() y notifyAl1() ·784 Objeto nulo, patrón de diseño' 381 Octal, 53 onda sinusoidal · 896 OpenLaszlo. alternativa a Flex . 932 operación atómica " 760 operaciones no soportadas en contenedores Java 529 operaciones opcionales en contenedores Java' 528 + Y +~ para String . 59, 142 +, para String . 317 operadores· 44 bit a bit· 55 coma, operador· 74 complemento a lUlO . 55 de desplazamiento· 55 de indexación [ 1. Il O de proyección· 60 errores comunes' 60 lógicos' 51 lógicos y cortocircuitos 52 matemáticos' 46. 633 precedencia· 44 relacionales· 49 sobrecarga de . 59 sobrecarga de operador para String . 318 String, conversión con operador + . 44, 59 temario· 58 ooarios . 48, 55 OR: bit a bit· 55, 60 lógico (11) . 51 orden: de inicialización· 105, 162, 182 de llamadas a constructores· 176 972 Piensa en Java ordenación' 504 alfabética ' 260 lex icográfica ' 260 Ybúsquedas en listas' 575 ordinal( ), para enum . 660 organización del código ' 128 OSExecute . 615 OutputStream . 596, 598 OutputStreamWriter . 600, 601 p paintComponent() . 896, 900 paquete (package) . 122 acceso de . 221 acceso de, y protected . j 53 nombres unívocos de . 124 nombres, uso de mayúsculas' 31 predeterminado' 122, 130 Yestructura de directorios' 128 parámetro de recopilación, . 458, 478 pato, tipos' 464, 472 patrón de diseño: adaptador' 198, 204, 402,472,474, 515 basado en comandos' 235, 384, 672, 734 basado en estados' 184 basado en estrategia' 196,203,474, 494,504,588,593,676,8 11 cadena de responsabilidad' 676 Decorador ' 461 Iterador' 214, 252 Iterador nu lo' 381 método basado en adaptadores' 270 método de factoria' 208, 222, 37 1, 399,604 método de plantillas' 231, 365, 426, 558,631,768,840,843 objeto de transferencia de datos (Mensajero) . 396, 516, 559 Objeto nulo' 381 Peso mosca' 519, 854 Proxy·378 Singleton . 136 solitario 136 Visitante' 706 pattern, expresiones regulares' 333 persistencia ' 649 ligera' 639 Peso mosca, patrón de diseño . 519, 854 PhantornReferenee . 578 Piedra, papel, tijera . 685 pila' 255, 256, 398, 582 Piped1nputStream . 597 PipedOutputStream' 597, 598 PipedReader . 601, 800 PipedWriter . 60 1, 800 planificador de hebras' 732 plantillas C++ . 394, 417 Plauger, P.! .. 961 polimorfismo' 9, 165- 187,351 , 391 comportamiento de los métodos polimórficos dentro de constructores . 181 Y constructores' 175 Y despacho múltiple - 684 POO (programación orientada a objetos): características básicas' 2 conceptos bás icos' 1 protocolo' 192 Simula-67, lenguaje de programación'3 suplantación' 2 posicionamiento absoluto 868 post-decremento' 48 postfija . 48 post-incremento · 48 pre-decremento . 48 predetenninado, constructor- 92, 144,377 predetenninado, paquete - 122, 130 predicción, operadores de, 60 preferenees, MI . 656 prefija' 48 pre-incremento' 48 prerrequisitos para este libro' 1 primitivos: comparación' 50 final' 156 final y static . 156 inicialización de campos de clases ' 102 tipos · 25 tipos de datos primitivos y uso con operadores . 62 printf( ) . 324 printStaekTrace( ) . 287, 289 PrintStream . 600 PrintWriter . 602, 605, 606 constructor en Java SES, 610 prioridad, en concurrencia· 738 PriorityBlockingQueue, concurrencia· 811 PriorityQueue . 264, 537 private' 5,121,128, 130, 153, 159,756 clases internas· 232 interfaces anidadas· 207 sustitución de métodos 173 proceso concurrente' 729 ProeessBuilder' 615 ProcessFiles . 720 productor-consumidor) concurrencia· 791 programación basada en agentes 853 programación multiparadigma . 2 programación orientada a aspectos (AOP) 458 programación orientada a objetos (POO), VeásePOO programación visual· 918 entornos de . 858 programador de clientes· 5 promoción a int . 62., 70 PropertyChangeEvent . 931 PropertyDescriptors . 922 propiedades: hoja de propiedades personalizada· 931 indexadas 931 restringidas· 931 ProptertyVetoException . 931 proteeted' 5, 121,128,13 1, 153 Yacceso de paquete· 13 1, 153 protoeol - 192 Protocolo Java de inicio a través de red (INLP) 906 Proxy, patrón de disefio . 378 proxy: y java.lang. ref. Referenee . 579 para métodos DO modificables de la clase CollectioDs . 530 proyección . 11 asSubclass() . 361 mediante una clase genérica· 448 Y tipos genéricos· 447 Y tipos primitivos· 70 publie' 5,121,128, 128 clase, y unidades de compilación· 122 e interface· 192 puntero, exclusión de punteros en Java· 229 PushbackInputStream . 599 PushbaekReader . 602 Python 1,5,9, 18, 22, 464,510,729,962 queue - 241,255, 263,537,796 rendimiento·561 R RAD (Rapid Applieation Development) 375 random( ) . 260 RandomAccess. interfaz de marcado para contenedores· 275 RandomAccessFile' 602, 603, 608, 616 read() . 596 nio' 616 readDouble( ) . 608 Reader' 596,600, 601 readExtemal( ) . 643 readLine() . 305, 602, 606, 613 readObjeet( ) . 639 con Serializable . 647 ReadWriteLock . 848 reanudación en el tratamiento de excepciones . 281 recuadros de mensaje en Swing . 888 recuento de referencias, depurador de memoria· tOO recursión no intencionada con toString( ) . 321 red de objetos' 639 red, E/S de - 616 redireccionamiento de la. EIS estándar 613 redisefio . 121 índice 973 ReentrantLoek' 759, 78 1 Reference de java.lang.ref· 578 referencia: asignación de objetos por copia de referencias . 45 equivalencia de referencias y equivalencia de objetos ' 50 final, 156 hallar el tipo exacto de referencia base' 352 null . 26 referencias Class genéricas 359 reflexión' 375, 387, 870, 920 diferencia entre RITl y . 375 ejemplo' 877 procesador de anotaciones' 696, 700 tipos latentes y genéricos' 467 Y Beans' 918 regeneración de una excepción' 289 regex . 333 región protegida en el tratamiento de excepciones' 279 registro y excepciones' 283 Regla de Brian de la sincronización' 757 rehashing' 571 reificación y genéricos' 419 removeActionListener( ) . 924, 929 removeXXXListener( ) . 869 renameTo() . 596 rendimiento: nio ' 630 optimización del . 834 pruebas de . 558 Y fmal . 161 reset( ) . 603 respuesta rápida, interfaces de usuario de . 750 resta· 46 resume( ) e interb~oqueos . 775 retomo: sobrecarga de los valores de . 92 tipos de retomo covariantes . 183,453 valor de retomo de constructor' 86 retrollamada' 588, 863 Y clases internas' 229 retum: devolución de múltiples objetos' 396 devolución de una matriz' 487 Y fmally . 299 reutilización' 6 código reutilizable ' 918 de código' 139 rewind( ) . 620 RTTI (runtime type information) 186,351 Class, objeto' 353, 878 ClassCastException . 362 Constructor, clase' 375 F ield ' 375 getConstructor( ) . 878 instanceof, palabra clave' 362 isInstance( ) . 368 Method' 375 newlnstance( ) . 878 reflexión' 375 shape, ejemplo' 351 Rumbaugh, James' 960 RuntimeExeeption ' 294, 3 13 rutina de tratamiento de excepciones, 280 rvalor' 44 s salto incondicional' 76 ScheduledExecutor, para concurrencia 814 sección crítica y bloque synchronized 765 secuencia, aplicación de un método a una ·468 seek( ) . 602, 608 selección aleatoria y enum . 666 semáforo contador' 817 seminarios xxiii fonnaci6n proporcionada por MindView, Inc .. 955 señales perdidas, concurrencia 788 separación de la interfaz y la implementación . 5, 133, 869 SequenceInputStream' 597, 602 Serializable' 639, 643, 646, 653, 926. Véase también serialización readObject() . 647 writeObject( ) . 647 serialización: control en la . 642 defaultReadObject( ) . 648 defaultWriteObject() . 647 Versionado . 649 y almacenamiento de objeto· 649 Y transient . 646 Set'241 , 244,258, 533 comparación de rendimiento' 567 relaciones matemáticas' 409 setActionCommand( ) . 894 setBorder( ) . 88 1 setErr(PrintStream) . 614 setIcon( ) . 879 setIn(InputStream) . 614 setLayout( ) . 866 setMnemonic( ) . 894 setOut(PrintStream) . 614 setToolTipText( ) . 880 shuffie( ) . 576 signatura de l método' 30 Simula-67, lenguaje de programación' 3 simulación' 821 SingleThreadExecutor . 735 size( ), ArrayList . 242 sizeof( ), no existe en Java' 62 sleep( ), en concurrencia· 737 Smalltalk . 2 sobrecarga: de constructores' 87 de los valores de retorno' 92 de métodos' 87 de operadores' 59 distinción entre métodos sobrecargados ·88 genéricos' 449 ocultación de nombres durante la herencia' 151 operadores + y += para String' 142, 318 SocketChannel . 632 SoftReference . 578 Software Development Conference . xxii i so licitud' 3 Solitario (singleton), patrón de diseño 136 SortedMap . 544 SortedSet . 536 South, BorderLayout . 866 split(), String . 196,332 sprintf( ) . 329 SQL generado mediante anotaciones' 698 stateChanged( ) . 897 stabe: palabra clave 32, 192 clases internas' 224 inicialización 163,354 método' 96, stop( ) e interbloqueos ' 775 StreamTokenizer . 602 String: CASE_INSENSITIVE_ ORDER . 575 concatenación con el operador += . 59 conversión con operador + . 44, 59 expresiones regulares ' 331 fonn at( ) . 329 indexOf( ) . 377 inmutabilidad, 3 17 métodos' 317, 322 operadores + y += para ' 142 ordenación lexicográfica y alfabética' 507 ordenación, CASE _INSENSITIVE_ ORDER· 588 split( ), método ' 196 toString( ) . 140 StringBuffer . 597 StringBufferlnputStream . 597 StringBuilder, String y toString( ) . 318 StringReader . 60 1, 604 String Writer . 60 I Stroustrup, Bjame . 119 Stub . 387 subobjeto . 143 sucesos: JavaBeans . 918 modelo de Swing . 869 multidifusión y JavaBeans . 927 programación dirigida por' 862 974 Piensa en Java Unified Modeling Language (UML) . 4, 960 unmodifiableList(), Collections . 530 UnsupportedOperationException . 530 upcasting. Véase generalizacíón userNodeForPackage(), API preferences . 657 Utilidades de java.uti1.Collec,tions . 572 Y concurrencia' 910 switch: palabra clave· 81 Y enuro · 662 synchronized: contenedores' 577 decidír qué métodos sincronizar' 929 estático' 757 Regla de Brian de la sincronización 757 sección critica y bloque· 765 wait( ) y notifYAlI( ) . 784 Y herencia · 929 SynchronousQueue, concurrencia· 826 System.arraycopy( ) . 502 System.err· 282, 613 System.in·613 System.out . 613 systemNodeForPackage( ), API preferences· 657 en el tratamiento de excepciones' 280 ternario, operador' 58 this, palabra clave' 93 ThreadFactory pen;onalizada . 741 tbrow, palabra clave' 280 Throwable, clase base para Exception 287 tiene-un, relación' 6, 153 TimeUnit . 738, 811 tipos: base· 7 comprobación de . 309, 392 derivado ' 7 enumerados 117 estructurales· 464, 472 genéricos y contenedores seguros respecto al tipo· 242 hallar el tipo exacto de referencia base' 352 inferencia del argumento de . 403 latentes· 464,472 marcador de, en genéricos' 424 matrices y comprobación de . 483 parametrizados . 393 pato· 464, 472 primitivos' 25 seguridad de tipos en Java ' 60 seguridad dinámica de . 456 tipo de datos equivalente a clase' 3 tipos de datos primitivos y uso con operadares' 62 toArray() . 571 TooManyListenersException . 926 toString() . 140 directrices para usar StringBuilder' 319 transferFrom() . 618 transferTo()·618 transieot, palabra clave' .646 TreeMap . 542, 544, 571 TreeSet . 258, 533, 536, 567 true . .5 1 try, bloque· 150, 280, 297 en excepciones' 280 tryLock( ), bloqueo de archivos· 632 tupla· 396, 408, 413 TYPE, campo para literales de clases primitivas . 357 T u tabla de base de datos, SQL generado mediante anotaciones' 698 UML(Unified Modeling Language) . 4, 6, 960 tamaño de un HashMap o RasbSet . 571 unario, operador: XDoclet . 693 XML·654 XOM, biblioteca XML . 654 XOR (Exclusive-OR) . 55 tareas y hebras, terminología' 748 teclado: navegación y Swing ,858 atajo más (+) ·48 menos (-)·48 UncaugbtExceptionHandler, clase Thread ·752 Unicode . 601 'Unidad de c.ompilación . 122 unidad de traducción' 122 unidifusión . 926 respuesta a un suceso Swing . 862 sistema dirigido pOI sucesos' 231 Y escuchas· 869 sugerencias' 880 swna' 46 super: . palabra clave' 143 Y clases internas' 236 superclase· 143 limites· 361 supertipo. comodines de . 438 suplantación en la POO . 2 suspende ) e interbloqueos · 775 sustitución 9: de métodos private . 173 Y clases internas' 236 Y sobrecarga' 151 sustitución: herencia yex.tension . 184 principio de . 9 pura ·9, 185 SWF, formato de código intermedio Flash ·932 Swing·857 componentes' 876 HTML en los componentes' 902 modelo de sucesos' 869 de· 894 Teoria del compromiso delegado' 751 terminación: alta (big endian) . 624 baja (Jíttle endian) . 624 condición de, y finalize( ) . 98 v values() para enumeraciones· 659, 663 Varga, Ervin . 7, 780 variable: defmición . 73 inicialización de variables de métodos' 102 listas de argumentos variables' 113 local · 29 Vector· 566, 581 vector de cambio' 232 Venners, Bill . 98 versionado, serialización . 649 Visitante, patrón de diseño' 706 Visual BASIC, Microsoft· 918 volatile . 754, 760, 763 w wait() . 784 Waldrop, M. Mitchell ·961 WeakHashMap . 542, 580 WeakReference . 578 Web Start, Java· 906 West, BorderLayout . 866 while . 72 windowC losing( ) . 899 write( ) . 596 nio·618 writeBytes( ) 607 writeChars( ) . 607 writeDouble( ) . 607 writeExtemal( ) . 643 writeObject( ) . 639 con Serializable . 646 Writer · 596,600,601 > x Z ZipEutry . 637 ZipInputStream . 634 ZipOutputStream . 634