JAVA Soluciones de programación www.fullengineeringbook.net Acerca del autor Herbert Schildt es una de las principales autoridades en Java, C, C++ y C#, y es un maestro programador en Windows. Se han vendido más de 3.5 millones de ejemplares de sus libros sobre programación y se han traducido a todos los idiomas importantes. Es autor de gran cantidad de bestsellers de Java, incluidos Java: Manual de referencia, Java: Manual de referencia y Fundamentos de Java. Entre sus otros bestsellers se incluyen Manual de referencia y El arte de programar en Java. Schildt tiene grado universitario y maestría de la Universidad de Illinois. Su sitio Web es www.HerbSchildt.com. www.fullengineeringbook.net JAVA Soluciones de programación Herbert Schildt Traducción Eloy Pineda Rojas Traductor profesional MÉXICO • BOGOTÁ • BUENOS AIRES • CARACAS • GUATEMALA • LISBOA • MADRID NUEVA YORK • SAN JUAN • SANTIAGO • AUCKLAND • LONDRES • MILÁN MONTREAL • NUEVA DELHI • SAN FRANCISCO • SINGAPUR • ST. LOUIS • SIDNEY • TORONTO www.fullengineeringbook.net Director editorial: Fernando Castellanos Rodríguez Editor de desarrollo: Miguel Ángel Luna Ponce Supervisora de producción: Jacqueline Brieño Álvarez Formación: Gráfica FX JAVA Soluciones de programación Prohibida la reproducción total o parcial de esta obra, por cualquier medio, sin la autorización escrita del editor. DERECHOS RESERVADOS © 2009 respecto a la primera edición en español por McGRAW-HILL INTERAMERICANA EDITORES, S.A. DE C.V. A Subsidiary of The McGraw-Hill Companies, Inc. Corporativo Punta Santa Fe Prolongación Paseo de la Reforma 1015 Torre A Piso 17, Colonia Desarrollo Santa Fe, Delegación Álvaro Obregón C.P. 01376, México, D. F. Miembro de la Cámara Nacional de la Industria Editorial Mexicana, Reg. Núm. 736 ISBN10 : 970-10-6756-8 ISBN13 : 978-970-10-6756-7 Translated from the 1st English edition of Herb Schildt’s Java Programming Cookbook By: Herbert Schildt Copyright © 2008 by The McGraw-Hill Companies. All rights reserved. ISBN: 978-0-07-226315-2 1234567890 0876543219 Impreso en México Printed in Mexico www.fullengineeringbook.net Contenido Prefacio 1 Revisión general ¿Qué encontrará en el interior? ¿Cómo están organizadas las recetas? Una cuantas palabras de precaución Es necesaria experiencia en Java ¿Qué versión de Java? 2 Trabajo con cadenas y expresiones regulares Una revisión general de las clases de cadena de Java La API de expresiones regulares de Java Introducción a las expresiones regulares Caracteres normales Clases de caracteres El carácter comodín Cuantificadores Cuantificadores avaros, renuentes y posesivos Comparadores de límites El operador O Grupos Secuencias de marcas Recuerde incluir el carácter de escape \ en cadenas de Java Ordene una matriz de cadenas de manera inversa Paso a paso Análisis Ejemplo Opciones Ignore las diferencias entre mayúsculas y minúsculas cuando ordene una matriz de cadenas Paso a paso Análisis Ejemplo Opciones Ignore las diferencias entre mayúsculas y minúsculas cuando busque o reemplace subcadenas Paso a paso Análisis Ejemplo Opciones Divida una cadena en partes empleando split( ) Paso a paso xix 1 1 2 3 3 4 5 6 8 8 9 9 10 10 11 11 11 12 13 13 14 14 14 16 17 18 18 19 19 21 22 22 22 23 24 25 25 v www.fullengineeringbook.net vi 3 Java: Soluciones de programación Análisis Ejemplo Opciones Recupere pares clave/valor de una cadena Paso a paso Análisis Ejemplo Opciones Compare y extraiga subcadenas empleando la API de expresiones regulares Paso a paso Análisis Ejemplo Opciones Divida en fichas una cadena empleando la API de expresiones regulares Paso a paso Análisis Ejemplo Ejemplo adicional Opciones 26 26 28 28 29 29 29 32 32 33 33 33 34 35 36 37 38 40 47 Manejo de archivos Una revisión general del manejo de archivos Flujos La clase RandomAccessFile La clase File Las interfaz de E/S Los flujos de archivos comprimidos Lea bytes de un archivo Paso a paso Análisis Ejemplo Opciones Escriba bytes en un archivo Paso a paso Análisis Ejemplo Opciones Use el búfer para la E/S de un archivo basada en bytes Paso a paso Análisis Ejemplo Opciones Lea caracteres de un archivo Paso a paso 49 50 50 53 54 55 57 59 59 59 60 61 62 63 63 63 64 65 66 66 66 68 69 69 www.fullengineeringbook.net Contenido Análisis Ejemplo Opciones Escriba caracteres en un archivo Paso a paso Análisis Ejemplo Opciones Use el búfer para la E/S de un archivo basada en caracteres Paso a paso Análisis Ejemplo Opciones Lea y escriba archivos de acceso aleatorio Paso a paso Análisis Ejemplo Opciones Obtenga atributos de archivos Paso a paso Análisis Ejemplo Opciones Establezca atributos de archivos Paso a paso Análisis Ejemplo Opciones Elabore una lista de un directorio Paso a paso Análisis Ejemplo Ejemplo adicional Opciones Comprima y descomprima datos Paso a paso Análisis Ejemplo Opciones Cree un archivo ZIP Paso a paso Análisis Ejemplo Opciones www.fullengineeringbook.net vii 69 70 71 72 72 73 73 74 75 76 76 77 79 80 80 80 81 83 83 84 84 84 86 86 87 87 87 89 90 90 90 91 93 94 95 95 96 96 99 100 100 101 102 105 viii 4 Java: Soluciones de programación Descomprima un archivo ZIP Paso a paso Análisis Ejemplo Opciones Serialice objetos Paso a paso Análisis Ejemplo Opciones 105 105 106 107 109 110 111 111 112 115 Formato de datos Revisión general de Formatter Fundamentos de formación Especificación de un ancho mínimo de campo Especificación de precisión Uso de las marcas de formato La opción en mayúsculas Uso de un índice de argumentos Cuatro técnicas simples de formación numérica que emplean Formatter Paso a paso Análisis Ejemplo Opciones Alinee verticalmente datos numéricos empleando Formatter Paso a paso Análisis Ejemplo Ejemplo adicional: centro de datos Opciones Justifique a la izquierda la salida con Formatter Paso a paso Análisis Ejemplo Opciones Forme fecha y hora empleando Formatter Paso a paso Análisis Ejemplo Opciones Especifique un idioma local usando Formatter Paso a paso Análisis Ejemplo Opciones 117 118 119 121 121 122 122 123 124 124 124 125 126 126 126 127 127 128 131 131 131 131 132 133 133 134 134 136 137 138 138 138 139 140 www.fullengineeringbook.net Contenido 5 ix Use flujos con Formatter Paso a paso Análisis Ejemplo Opciones Use printf( ) para desplegar datos formados Paso a paso Análisis Ejemplo Ejemplo adicional Opciones Forme fecha y hora con DateFormat Paso a paso Análisis Ejemplo Opciones Forme fecha y hora con patrones empleando SimpleDateFormat Paso a paso Análisis Ejemplo Opciones Forme valores numéricos con NumberFormat Paso a paso Análisis Ejemplo Opciones Forme valores monetarios usando NumberFormat Paso a paso Análisis Ejemplo Opciones Forme valores numéricos con patrones empleando DecimalFormat Paso a paso Análisis Ejemplo Opciones 140 140 140 141 142 143 143 143 144 145 145 146 147 148 148 149 150 151 151 152 153 153 154 154 155 156 156 157 157 157 157 158 158 158 159 160 Trabajo con colecciones Revisión general de las colecciones Tres cambios recientes Las interfaz de Collection Las clases de la colección La clase ArrayList La clase LinkedList La clase HashSet La clase LinkedHashSet 161 162 163 164 173 173 174 175 175 www.fullengineeringbook.net x Java: Soluciones de programación La clase TreeSet La clase PriorityQueue La clase ArrayDeque La clase EnumSet Revisión general de los mapas Las interfaz de Map Las clases de Map Algoritmos Técnicas básicas de colecciones Paso a paso Análisis Ejemplo Opciones Trabaje con listas Paso a paso Análisis Ejemplo Opciones Trabaje con conjuntos Paso a paso Análisis Ejemplo Ejemplo adicional Opciones Use Comparable para almacenar objetos en una colección ordenada Paso a paso Análisis Ejemplo Opciones Use un Comparator con una colección Paso a paso Análisis Ejemplo Opciones Itere en una colección Paso a paso Análisis Ejemplo Opciones Cree una cola o una pila empleando Deque Paso a paso Análisis Ejemplo Opciones www.fullengineeringbook.net 176 176 177 178 178 178 183 185 186 187 187 188 190 191 191 192 192 195 195 196 196 197 198 201 201 202 202 203 204 205 205 205 206 209 209 210 210 211 213 214 214 215 216 217 Contenido 6 xi Invierta, gire y ordene al azar una List Paso a paso Análisis Ejemplo Opciones Ordene una List y busque en ella Paso a paso Análisis Ejemplo Opciones Cree una colección comprobada Paso a paso Análisis Ejemplo Opciones Cree una colección sincronizada Paso a paso Análisis Ejemplo Opciones Cree una colección inmutable Paso a paso Análisis Ejemplo Opciones Técnicas básicas de Map Paso a paso Análisis Ejemplo Opciones Convierta una lista de Properties en un HashMap Paso a paso Análisis Ejemplo Opciones 218 219 219 219 220 221 221 221 222 223 224 224 224 225 227 227 228 228 228 231 231 231 232 232 233 233 234 235 235 238 238 239 239 239 240 Applets y servlets Revisión general de las applets La clase Applet Arquitectura de Applet El ciclo de vida de la applet Las interfaz AppletContext, AudioClip y AppletStub Revisión general de la servlet El paquete javax.servlet El paquete javax.servlet.http 241 241 242 244 245 246 246 246 249 www.fullengineeringbook.net xii Java: Soluciones de programación La clase HttpServlet La clase Cookie El ciclo de vida de la servlet Uso de Tomcat para desarrollo de servlets Cree un esqueleto de Applet basado en AWT Paso a paso Análisis Ejemplo Opciones Cree un esqueleto de Applet basado en Swing Paso a paso Análisis Ejemplo Opciones Cree una GUI y maneje sucesos en una Applet de Swing Paso a paso Análisis Nota histórica: getContentPane( ) Ejemplo Ejemplo adicional Opciones Pinte directamente en la superficie de la Applet Paso a paso Análisis Ejemplo Opciones Pase parámetros a Applets Paso a paso Análisis Ejemplo Opciones Use AppletContext para desplegar una página Web Paso a paso Análisis Ejemplo Opciones Cree una servlet simple usando GenericServlet Paso a paso Análisis Ejemplo Opciones Maneje solicitudes HTTP en una servlet Paso a paso Análisis Ejemplo www.fullengineeringbook.net 251 251 253 254 255 256 256 256 257 257 258 258 259 260 260 261 261 263 263 266 268 269 269 270 271 273 275 275 275 276 277 278 278 278 278 281 282 282 282 283 284 185 285 285 286 Contenido 7 xiii Ejemplo adicional Opciones Use una cookie con una servlet Paso a paso Análisis Ejemplo Opciones 287 290 290 290 290 291 293 Multiprocesamiento Fundamentos del multiprocesamiento La interfaz Runnable La clase Thread Cree un subproceso al implementar Runnable Paso a paso Análisis Ejemplo Opciones Cree un subproceso al extender Thread Paso a paso Análisis Ejemplo Opciones Use el nombre y el ID de un subproceso Paso a paso Análisis Ejemplo Opciones Espere a que termine un subproceso Paso a paso Análisis Ejemplo Opciones Sincronice subprocesos Paso a paso Análisis Ejemplo Opciones Establezca comunicación entre subprocesos Paso a paso Análisis Ejemplo Opciones Suspenda, reanude y detenga un subproceso Paso a paso 295 www.fullengineeringbook.net 297 298 299 300 300 300 303 304 305 305 305 306 307 307 308 308 310 311 311 311 312 313 314 315 315 316 318 318 319 319 320 322 323 323 xiv 8 Java: Soluciones de programación Análisis Ejemplo Opciones Use un subproceso de daemon Paso a paso Análisis Ejemplo Ejemplo adicional: una clase simple de recordatorio Opciones Interrumpa un subproceso Paso a paso Análisis Ejemplo Opciones Establezca y obtenga una prioridad de subproceso Paso a paso Análisis Ejemplo Opciones Monitoree el estado de un subproceso Paso a paso Análisis Ejemplo Ejemplo adicional: un monitor de subprocesos en tiempo real Opciones Use un grupo de subprocesos Paso a paso Análisis Ejemplo Opciones 324 325 327 328 329 329 329 331 336 336 337 337 337 339 341 341 342 342 344 344 345 345 346 349 353 353 354 354 355 357 Swing Revisión general de Swing Componentes y contenedores Componentes Contenedores Los paneles de contenedor de nivel superior Revisión general del administrador de diseño Manejo de sucesos Sucesos Orígenes de sucesos Escuchas de sucesos Cree una aplicación simple de Swing Paso a paso Análisis 359 360 361 362 362 363 363 364 365 365 365 366 366 367 www.fullengineeringbook.net Contenido Nota histórica: getContentPane( ) Ejemplo Opciones Establezca el administrador de diseño del panel de contenido Paso a paso Análisis Ejemplo Opciones Trabaje con JLabel Paso a paso Análisis Ejemplo Opciones Cree un botón simple Paso a paso Análisis Ejemplo Opciones Use iconos, HTML y mnemotécnica con JButton Paso a paso Análisis Ejemplo Opciones Cree un botón interruptor Paso a paso Análisis Ejemplo Opciones Cree casillas de verificación Paso a paso Análisis Ejemplo Opciones Cree botones de opción Paso a paso Análisis Ejemplo Opciones Ingrese texto con JTextField Paso a paso Análisis Ejemplo Ejemplo adicional: cortar, copiar y pegar Opciones Trabaje con JList www.fullengineeringbook.net xv 369 369 371 372 372 372 373 375 376 376 377 379 382 383 384 384 385 387 390 391 391 393 395 396 397 397 398 400 400 401 401 401 405 405 406 406 407 410 411 411 412 413 416 419 420 xvi 9 Java: Soluciones de programación Paso a paso Análisis Ejemplo Opciones Use una barra de desplazamiento Paso a paso Análisis Ejemplo Opciones Use JScrollPane para manejar el desplazamiento Paso a paso Análisis Ejemplo Opciones Despliegue datos en una JTable Paso a paso Análisis Ejemplo Opciones Maneje sucesos de JTable Paso a paso Análisis Ejemplo Opciones Despliegue datos en un JTree Paso a paso Análisis Ejemplo Opciones Cree un menú principal Paso a paso Análisis Ejemplo Opciones 420 420 422 424 426 427 427 429 431 433 433 433 433 436 438 439 440 441 444 446 447 447 450 455 456 458 458 461 464 466 467 467 469 471 Miscelánea Acceda a un recurso mediante una conexión HTTP Paso a paso Análisis Ejemplo Opciones Use un semáforo Paso a paso Análisis Ejemplo Opciones 473 474 474 474 475 476 480 481 482 482 485 www.fullengineeringbook.net Contenido Devuelva un valor de un subproceso Paso a paso Análisis Ejemplo Opciones Use reflexión para obtener información acerca de una clase en tiempo de ejecución Paso a paso Análisis Ejemplo Ejemplo adicional: una utilería de reflexión Opciones Use reflexión para crear dinámicamente un objeto y llamar métodos Paso a paso Análisis Ejemplo Opciones Cree una clase personalizada de excepción Paso a paso Análisis Ejemplo Opciones Calendarice una tarea para ejecución futura Paso a paso Análisis Ejemplo Opciones Índice www.fullengineeringbook.net xvii 486 487 487 488 491 491 492 492 493 494 496 496 497 497 498 501 501 502 502 504 505 506 507 507 508 510 511 www.fullengineeringbook.net Prefacio D urante muchos años, amigos y lectores me han pedido que escriba un libro de soluciones para Java, compartiendo algunas de las técnicas y métodos que utilizo cuando programo. Desde el principio me gustó la idea, pero no lograba darme tiempo para ella con un calendario de escritura muy ocupado. Como muchos lectores saben, escribo acerca de muchas facetas de programación, con énfasis especial en Java, C/C++ y C#. Debido a los rápidos ciclos de revisión de estos lenguajes, dedico casi todo mi tiempo disponible a actualizar mis libros para que cubran las versiones más recientes de esos lenguajes. Por fortuna, a principios de 2007 se abrió una ventana de oportunidad y finalmente pude dedicar tiempo a escribir este libro de soluciones de Java. Debo admitir que rápidamente se volvió uno de los proyectos que más he disfrutado. Este libro destila la esencia de muchas técnicas de propósito general en un conjunto de soluciones paso a paso. En cada solución se describe un conjunto de componentes clave, como clases, interfaz y métodos. Luego se muestran los pasos necesarios para ensamblar esos componentes en una secuencia de código que logre los resultados deseados. Esta organización facilita la búsqueda de técnicas en que está interesado y luego ponerlas en acción. En realidad, “en acción” es una parte importante de este libro. Creo que los buenos libros de programación contienen dos elementos: teoría sólida y aplicación práctica. En las soluciones, las instrucciones paso a paso y los análisis proporcionan la teoría. Para llevar esa teoría a la práctica, cada solución incluye un ejemplo completo de código. En los ejemplos se demuestra de manera concreta, sin ambigüedades, la manera en que pueden aplicarse. En otras palabras, en los ejemplos se eliminan las “adivinanzas” y se ahorra tiempo. Aunque ningún libro puede incluir todas las soluciones que pudieran desearse (hay un número casi ilimitado de soluciones posibles), traté de abarcar un amplio rango de temas. Mis criterios para incluir una solución se analizan de manera detallada en el capítulo 1, pero, en resumen, incluí las que serían útiles para muchos programadores y que responderían las preguntas más frecuentes. Aún con estos criterios, fue difícil decidir qué incluir y qué dejar fuera. Ésta fue la parte más desafiante de la escritura del libro. Al final, se impusieron la experiencia, el juicio y la intuición. Por fortuna, ¡he incluido algo para satisfacer al gusto de cada programador! xix www.fullengineeringbook.net xx Java: Soluciones de programación Código de ejemplo en Web El código fuente para todos los ejemplos de este libro está disponible de manera gratuita en Web en http://www.mcgraw-hill-educacion.com/ Más de Herbert Schildt Java, Soluciones de programación es sólo uno de los muchos libros de programación de Herb. He aquí algunos otros que le resultarán de interés: Para aprender más acerca de Java recomendamos: Java: Manual de referencia Fundamentos de Java Java: Manual de referencia Para aprender más acerca de C++, estos libros le resultarán especialmente útiles. C++: Soluciones de programación C++: A Begginer’s Guide C++ The complete reference STL Programming From the Ground Up The Art of C++ Para aprender acerca de C#, sugerimos los siguientes libros de Schildt: C#: The Complete Reference C#: A Begginer’s Guide Si quiere aprender acerca del lenguaje C, entonces le interesará el siguiente título: C: The complete reference Cuando necesite respuestas sólidas, rápidas, busque algo de Herbert Schildt, la autoridad reconocida en programación. www.fullengineeringbook.net 1 CAPÍTULO Revisión general E ste libro es una colección de técnicas que muestra cómo realizar varias tareas de programación en Java. Cada solución ilustra la manera de realizar una operación específica. Por ejemplo, hay soluciones que leen bytes de un archivo, iteran una colección, forman datos numéricos, construyen componentes de Swing, crean un servlet, etc. Cada técnica de este libro describe un conjunto de elementos de programa claves y la secuencia de pasos necesarios para usarlos y realizar una tarea de programación. A fin de cuentas, el objetivo de este libro es ahorrarle tiempo y esfuerzo durante el desarrollo de programas. Muchas tareas de programación incluyen un conjunto de clases de API, interfaces y métodos que deben aplicarse en una secuencia específica. El problema es que a veces no sabe cuáles clases de API usar o en qué orden llamar a los métodos. En lugar de tener que recorrer una gran cantidad de documentación y de tutoriales en línea para determinar la manera de realizar alguna tarea, puede buscar su receta. Cada receta muestra una manera de llegar a una solución, describiendo los elementos necesarios y el orden en que deben usarse. Con esta información puede diseñar una solución que se adecue a sus necesidades específicas. ¿Qué encontrará en el interior? Para elegir las soluciones para este libro, me concentré en las siguientes categorías: • Procesamiento de cadenas (incluidas expresiones regulares) • Manejo de archivos • Formateo de datos • Applets y servlets • Swing • Las colecciones del marco conceptual • Multiprocesamiento 1 www.fullengineeringbook.net 2 Java: Soluciones de programación Elegí estas categorías porque se relacionan con un amplio rango de programadores (evité temas especializados que se aplican sólo a un subconjunto estrecho de casos). Cada una de estas categorías se vuelve la base de un capítulo. Además de las soluciones relacionadas con los temas anteriores, tengo otros que quiero incluir pero para los cuales no fue posible un capítulo completo. Agrupé esas soluciones en el capítulo final. Por supuesto, la elección de temas sólo fue el principio del proceso de selección. Dentro de cada categoría, tuve que decidir lo que se incluía y lo que se dejaba fuera. En general, incluí una solución si cumplía los dos criterios siguientes: 1. La técnica es útil para un amplio rango de programadores. 2. Proporciona una respuesta a una pregunta frecuente de programación. El primer criterio se explica por sí solo y se basa en mi experiencia. Incluí soluciones que describen la manera de realizar un conjunto de tareas que se encontrarían comúnmente cuando se crean aplicaciones de Java. Algunas de ellas ilustran un concepto general que puede adaptarse para resolver varios tipos diferentes de problemas. Por ejemplo, en el capítulo 2 se muestra una solución que usa la API de expresión regular para buscar y extraer subcadenas de una cadena. Este procedimiento general es útil en varios contextos, como encontrar una dirección de correo electrónico, o un número telefónico dentro de una frase, o extraer una palabra clave de una consulta de base de datos. Otras soluciones describen técnicas más específicas pero de uso más amplio. Por ejemplo, en el capítulo 4 se muestra la manera de formar la fecha y hora usando SimpleDateFormat. El segundo criterio se basa en mi experiencia como autor de libros de programación. A través de los muchos años que llevo escribiendo, los lectores me han hecho miles y miles de preguntas del tipo “¿Cómo lo hago?”. Estas preguntas vienen de todas las áreas de la programación de Java y van de las muy fáciles a las muy difíciles. Sin embargo, he encontrado que un núcleo central de preguntas surge una y otra vez. He aquí un ejemplo: “¿Cómo formo la salida?”. He aquí otra: “¿Cómo comprimo un archivo?”. Hay muchas otras. Este mismo tipo de preguntas también se presenta con frecuencia en varios foros de programadores en Web. He utilizado estas preguntas comunes como guía para mi selección de soluciones. Las soluciones de este libro abarcan varios niveles de habilidad. Algunas ilustran técnicas básicas, como la lectura de bytes de un archivo o la creación de una JTable de Swing. Otras son más avanzadas, como la creación de una servlet o el uso de reflejo para crear una instancia de un objeto en tiempo de ejecución. Por tanto, el nivel de dificultad de una solución individual puede ir de relativamente fácil a muy avanzada. Por supuesto, casi todo en programación es fácil una vez que sabe cómo hacerlo, pero es difícil cuando no lo sabe. Por tanto, no se sorprenda si algunas soluciones parecen obvias. Eso sólo significa que ya sabe cómo realizar esa tarea. ¿Cómo están organizadas las soluciones? Cada solución de este libro sigue el mismo formato, que tiene las siguientes partes: • Una descripción del problema que resuelve. • Una tabla de componentes clave usados. • Los pasos necesarios para completar la solución. • Un análisis a profundidad de los pasos. • Un ejemplo de código que pone la solución en acción. • Opciones que sugieren otras maneras de llegar a la solución. www.fullengineeringbook.net Capítulo 1: Revisión general 3 Una solución empieza por describir la tarea que se realizará. Los componentes clave empleados se muestran en una tabla. Ésta incluye las clases de API, las interfaces y los métodos necesarios para crear una solución. Por supuesto, tal vez para ponerla en práctica se requiera el uso de elementos adicionales, pero los componentes clave son los fundamentales para la tarea a mano. Cada solución presenta después instrucciones paso a paso que resumen el procedimiento. Estas son seguidas por un análisis a profundidad de los pasos. En muchos casos el resumen bastará, pero los detalles están allí, si los necesita. A continuación, se presenta un ejemplo de código que muestra la solución en acción. Todos los ejemplos de código se presentan completos. Esto evita la ambigüedad y le permite ver con precisión y claramente lo que está sucediendo sin tener que llenar los detalles adicionales. En ocasiones, se incluye un ejemplo extra que ilustra un poco más cómo puede aplicarse la solución. Cada una concluye con un análisis de varias opciones. Esta sección resulta especialmente importante porque sugiere diferentes maneras de implementar una solución u otras maneras de pensar acerca del problema. Unas cuantas palabras de precaución Hay unos cuantos elementos importantes que debe tener en cuenta cuando use este libro. En primer lugar, se muestra una manera de llegar a una solución. Es probable (y a menudo sucede) que haya otras maneras. Es probable que su aplicación específica requiera un método diferente del mostrado. Las soluciones de este libro pueden servir como puntos de partida y ayudarle a elegir un acercamiento general a una solución, además de que pueden despertar su imaginación. Sin embargo, en todos los casos, debe determinar lo que es apropiado y lo que no lo es para su aplicación. En segundo lugar, es importante comprender que los ejemplos de código no tienen un rendimiento óptimo. En realidad, están optimizados para ser claros y de fácil comprensión. El objetivo es ilustrar de manera evidente los pasos de la solución. En muchos casos tendrá pocos problemas para escribir un código más condensado y eficiente. Además, los ejemplos no son más que eso: ejemplos. Hay usos simples que no reflejan necesariamente la manera en que escribirá código para su propia aplicación. En todas las circunstancias, debe crear una solución propia que satisfaga las necesidades de su aplicación. En tercer lugar, cada ejemplo contiene un manejo de errores que resulta apropiado para ese ejemplo específico, pero que tal vez no lo sea en otras situaciones. En todos los casos, debe manejar de manera apropiada los diversos errores y excepciones que pueden producirse cuando adapta una solución para usarla en su propio código. Permítame dejar en claro de nuevo este punto importante: cuando se implementa una solución, debe proporcionar un manejo apropiado de errores para su aplicación. No puede suponer simplemente que la manera en que se manejan (o no se manejan) los errores o las excepciones en un ejemplo es suficiente o adecuada para su uso. Por lo general, en las aplicaciones reales se requerirá el manejo de excepciones. Es necesaria experiencia en Java Este libro está dirigido a todos los programadores en Java, sean principiantes o profesionales con experiencia. Sin embargo, se supone que conoce los fundamentos de la programación en Java, incluidos palabras clave de Java, sintaxis y clases de API básicas. También debe tener la capacidad de crear, compilar y ejecutar programas en Java. Nada de esto se enseña en esta obra. (Como ya se explicó, este libro trata sobre la aplicación de Java a diversos problemas de programación reales. www.fullengineeringbook.net 4 Java: Soluciones de programación No busca enseñar los fundamentos del lenguaje Java). Si necesita mejorar sus habilidades en Java, recomiendo mi libro Java: Manual de referencia, séptima edición. Publicado por McGraw-Hill. ¿Qué versión de Java? Como muchos lectores lo saben, Java se encuentra en un estado de evolución constante desde su creación. Con cada nueva versión, se agregan características. En muchos casos, con cada nueva versión también se vuelven obsoletas otras características. Como resultado, no todo el código moderno de Java puede compilarse en un compilador antiguo de Java. Esto resulta importante porque el código de este libro se basa en Java SE 6, que (al momento de escribir el libro) es la versión actual de Java. El kit del desarrollador para Java SE 6 es JDK 6. También es el JDK usado para probar todos los ejemplos de código. Como tal vez ya lo sepa, a partir de JDK 5 se agregaron varias características importantes a Java. Entre éstas se incluyen genéricos, enumeraciones y autoencuadre. Algunas de las técnicas de este libro emplean estas características. Si está utilizando una versión de Java anterior a JDK 5, entonces no podrá compilar los ejemplos que utilizan estas nuevas características. Por tanto, se recomienda mucho que utilice una versión moderna de Java. www.fullengineeringbook.net 2 CAPÍTULO Trabajo con cadenas y expresiones regulares U na de las tareas de programación más comunes es el manejo de cadenas. Casi todos los programas tratan con cadenas, de una forma u otra, porque suelen ser el conducto por el cual los seres humanos interactúan con la información digital. Debido a la parte importante que juega el manejo de cadenas, Java le proporciona amplio soporte. Como lo saben todos los programadores que usan Java, la clase más importante para trabajar con cadenas es String. Proporciona un amplio conjunto de métodos para el manejo de cadenas. Muchos de estos métodos proporcionan las operaciones de cadena básicas con que están familiarizados la mayoría de los programadores que usan Java. Entre éstos se incluyen métodos que comparan dos cadenas, buscan la aparición de una cadena en otra, etc. Sin embargo, String también contiene varios métodos menos conocidos que aumentan de manera importante sus opciones, porque operan con expresiones regulares. Una expresión regular define un patrón general, no una secuencia específica de caracteres. Este patrón también puede usarse para buscar subcadenas que coincidan con un patrón. Se trata de un concepto poderoso que está revolucionando la manera en que los programadores que usan Java piensan en el manejo de cadenas. Java empezó a proporcionar soporte a expresiones regulares en la versión 1.4. Las expresiones regulares están soportadas por la API de expresiones regulares, que está empaquetada en java. util.regex. Como se acaba de explicar, las expresiones regulares también tienen soporte en varios métodos de String. Con la adición de las expresiones regulares, se han facilitado muchas tareas de manejo de cadenas que, de otra manera, serían difíciles. Este capítulo contiene soluciones que ilustran varias técnicas de manejo de cadenas que van más allá de las operaciones básicas de búsqueda, comparación y reemplazo encontradas en String. Varias también usan expresiones regulares. En algunos casos, se emplean las capacidades de expresión regular de String. En otros se usa la propia API de expresiones regulares. He aquí las soluciones incluidas en este capítulo: • Ordene una matriz de cadenas de manera inversa • Ignore las diferencias entre mayúsculas y minúsculas cuando ordene una matriz de cadenas • Ignore las diferencias entre mayúsculas y minúsculas cuando busque o reemplace subcadenas • Divida una cadena en partes empleando split( ) • Recupere pares clave/valor de una cadena 5 www.fullengineeringbook.net 6 Java: Soluciones de programación • Compare y extraiga subcadenas empleando la API de expresiones regulares • Divida en fichas una cadena empleando la API de expresiones regulares Una revisión general de las clases de cadena de Java Una cadena es una secuencia de caracteres. A diferencia de otros lenguajes de programación, Java no implementa las cadenas como matrices de caracteres. En cambio, las implementa como objetos. Esto le permite a Java definir una serie rica de métodos que actúan sobre las cadenas. Aunque éstas son un territorio familiar para casi todos los programadores que usan Java, aún es útil revisar sus atributos y capacidades clave. Casi todas las cadenas que usará en un programa son objetos de tipo String. String es parte de java.lang. Por tanto, queda a disposición automáticamente de todos los programas en Java. Uno de los aspectos más interesantes de String es que crea cadenas inmutables. Esto significa que una vez que se crea una instancia de String, no puede modificarse su contenido. Aunque ésta parece una restricción importante, no lo es. Si necesita cambiar una cadena, simplemente cree una nueva que contenga la modificación. La cadena original permanecerá sin cambios. Si ya no se necesita ésta, descártela. La cadena que no se usa será reciclada la próxima vez que se ejecute el recolector de basura. Mediante el uso de cadenas inmutables, String puede implementarse de manera más eficiente de lo que sería si se usara una modificable. Es posible crear cadenas de varias maneras. Puede construir explícitamente una cadena al usar uno de los constructores de String. Por ejemplo, hay constructores que crean una instancia de String a partir de una matriz de caracteres, una matriz de bytes u otra cadena. Sin embargo, la manera más fácil de crear una cadena consiste en usar una literal de cadena, que es una cadena entre comillas. Todas las literales de cadena son automáticamente objetos de tipo String. Por tanto, una literal de cadena puede asignarse a una referencia a String, como se muestra aquí: String cad = "Prueba"; Esta línea crea un objeto de String que contiene la palabra "Prueba" y luego le asigna a cad una referencia a ese objeto. String sólo soporta un operador: +. Éste une dos cadenas. Por ejemplo, String cadA = "Hola,"; String cadB = " allá"; String cadC = cadA + cadB; Esta secuencia da como resultado cadC, que contiene la secuencia "Hola, allá". String define varios métodos que operan en cadenas. Debido a que la mayoría de los lectores tienen por lo menos una familiaridad pasable con String, no es necesaria una descripción detallada de todos sus métodos. Más aún, las soluciones de este capítulo describen por completo los métodos de String que emplean. Sin embargo, es útil revisar las capacidades centrales de manejo de cadenas de String al agruparlas en categorías. String define los siguientes métodos que buscan el contenido de una cadena en otra: contains Devuelve verdadero si una cadena contiene otra. endsWith Devuelve verdadero si una cadena termina con una cadena específica. indexOf Devuelve el índice dentro de una cadena en que se encuentra la primera aparición de otra cadena. Devuelve –1 si no se encuentra la cadena. lastIndexOf Devuelve el índice dentro de la cadena que invoca en que se encuentra la última aparición de la cadena especificada. Devuelve –1 si no se encuentra la cadena. startsWith Devuelve verdadero si una cadena empieza con una cadena específica. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 7 Los siguientes métodos comparan una cadena con otra: compareTo Compara una cadena con otra. compareToIgnoreCase Compara una cadena con otra. Se ignoran las diferencias entre mayúsculas y minúsculas. contentEquals Compara una cadena con una secuencia específica de caracteres. equals Devuelve verdadero si dos cadenas contienen la misma secuencia de caracteres. equalsIgnoreCase Devuelve verdadero si dos cadenas contienen la misma secuencia de caracteres. Se ignoran las diferencias entre mayúsculas y minúsculas. matches Devuelve verdadero si una cadena coincide con una expresión regular específica. regionMatches Devuelve verdadero si la región especificada de una cadena coincide con la región especificada de otra. Cada uno de los métodos dentro del siguiente grupo reemplaza una parte de una cadena con otra: replace Reemplaza todas las apariciones de un carácter o una subcadena con otra. replaceFirst Reemplaza la primera secuencia de caracteres que coincide con una expresión regular específica. replaceAll Reemplaza todas las secuencias de caracteres que coinciden con una expresión regular específica. Los siguientes dos métodos cambian las mayúsculas y minúsculas de las letras dentro de una cadena: toLowercase Convierte la cadena a minúsculas. toUpperCase Convierta la cadena a mayúsculas. Además de los métodos de manejo de cadena centrales que acabamos de describir, String define otros más. Dos de uso muy común son length( ), que devuelve el número de caracteres en una cadena, y charAt( ), que devuelve el carácter en un índice específico. En su mayor parte, las soluciones de este capítulo usan String, y suelen ser su mejor opción cuando trabaja con cadenas. Sin embargo, en los pocos casos en que necesita que se modifique una cadena, Java ofrece otras dos opciones. La primera es StringBuffer, que ha sido parte de Java desde el principio. Es similar a String, excepto que permite cambiar el contenido de una cadena. Por tanto, proporciona métodos, como setCharAt( ) e insert( ), que modifican la cadena. La segunda opción es la más nueva StringBuilder, que se agregó a Java en la versión 1.5. Resulta www.fullengineeringbook.net 8 Java: Soluciones de programación similar a StringBuffer, excepto que no es segura para subprocesos. Por tanto, es más eficiente cuando no se usan multiprocesamientos. (En aplicaciones con multiprocesamientos, debe usar StringBuffer, porque es segura para subprocesos). Tanto StringBuffer como StringBuilder están empaquetados en java.lang. La API de expresiones regulares de Java Las expresiones regulares tienen soporte en Java con las clases Matcher y Pattern, que están empaquetadas en java.util.regex. Estas clases funcionan juntas. Utilizará Pattern para definir una expresión regular. Comparará el patrón contra otra sección empleando Matcher. Los procedimientos precisos se describen en las soluciones en que se usan. Las expresiones regulares también se usan en otras partes de la API de Java. Tal vez lo más importante sea que varios métodos de String, como split( ) y matches( ), aceptan una expresión regular como argumento. Por tanto, con frecuencia usará una expresión regular sin usar explícitamente Pattern o Matcher. Varias de las soluciones de este capítulo usan expresiones regulares. La mayor parte de ellas lo hacen mediante métodos de String, pero tres de ellas usan explícitamente Pattern y Matcher. Para tener un control detallado del proceso de comparación, a menudo es necesario usar Pattern y Matcher. Sin embargo, en muchos casos la funcionalidad de expresiones regulares que proporciona String es suficiente y más conveniente. Varios métodos que usan expresiones regulares lanzarán una excepción PatternSyntaxException cuando se hace un intento por usar una expresión regular sintácticamente incorrecta. Esta excepción se define mediante la API de expresiones regulares y está empaquetada en java.util.regex. Necesitará manejar esta excepción de una manera apropiada en su aplicación. Introducción a las expresiones regulares Antes de que pueda usar expresiones regulares, debe comprender cómo están construidas. Si es nuevo en las expresiones regulares, entonces esta revisión general le ayudará a iniciarse en ellas. Antes de seguir, es importante establecer que el tema de las expresiones regulares es más bien amplio. En realidad, se han escrito libros completos sobre ellas. Está más allá del alcance de este libro describirlas de manera detallada. En cambio, aquí se presenta una breve introducción que incluye suficiente información para que comprenda los ejemplos de las soluciones. También le permitirá empezar a experimentar con expresiones regulares propias. Sin embargo, si las usa de manera reiterada, entonces tendrá que estudiarlas con mucho mayor detalle. Tal como se usa aquí el término, una expresión regular es una cadena de caracteres que describe un patrón. Un patrón comparará cualquier secuencia de caracteres que satisfaga el patrón. Por tanto, éste constituye una forma general que coincidirá con diversas secuencias específicas. En conjunto con un motor de expresiones regulares (como las proporcionadas por la API de expresiones regulares de Java), puede usarse un patrón para buscar coincidencias en otra secuencia de caracteres. Es esta capacidad la que da a las expresiones regulares su poder cuando se manipulan cadenas. Una expresión regular consta de uno o más de los siguientes elementos: caracteres normales, clases de caracteres (conjuntos de caracteres), el carácter comodín, cuantificadores, comparadores de límites, operadores y grupos. Aquí se examinará cada uno de manera breve. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 9 NOTA Hay cierta variación en la manera en que los diferentes motores de expresiones regulares manejan éstas. En este análisis se analiza la implementación de Java. Caracteres normales Un carácter normal (es decir una literal de carácter) se compara tal cual. Por tanto, si un patrón consta de xy, entonces la única secuencia de entrada con la que coincidirá es "xy". Caracteres como nueva línea y tabulador se especifican empleando secuencias de escape, que empiezan con una \. Por ejemplo, una nueva línea se especifica como \n. Clases de caracteres Una clase de caracteres es un conjunto de caracteres. Una clase de caracteres se especifica al poner los caracteres en la clase entre corchetes. Una clase coincidirá con cualquier carácter que sea parte de la clase. Por ejemplo, la clase [wxyz] buscará coincidencias de w, x, y o z. Para especificar un conjunto invertido, anteceda los caracteres con un ^. Por ejemplo, [^wxyz] buscará coincidencias con cualquier carácter, excepto w, x, y o z. Puede especificar un rango de caracteres empleando un guión. Por ejemplo, para especificar una clase de caracteres que coincida con los dígitos 1 a 9, use [1–9]. Una clase puede contener dos o más rangos con sólo especificarlos. Por ejemplo, la clase [0–9A–Z] busca todos los dígitos y las letras mayúsculas, de la A a la Z. La API de expresiones regulares de Java proporciona varias clases predefinidas. He aquí algunas de las de uso más común: Clase predefinida Coincide con \d Los dígitos del 0 al 9. \D Todos los caracteres que no son dígitos. \s Espacio en blanco. \S Todo lo que no es un espacio en blanco. \w Caracteres que pueden ser parte de una palabra. En Java, son las letras mayúsculas y minúsculas, los dígitos del 0 al 9 y los guiones de subrayado. Suele denominárseles caracteres de palabra. \W Todos los caracteres que no son de palabra. Además de estas clases, Java proporciona una amplia cantidad de clases de caracteres adicionales que tienen la siguiente forma general: \p{nombre} Aquí, nombre especifica el nombre de la clase. He aquí algunos ejemplos: \p{Lower} \p{Upper} \p{Punct} Contiene las letras minúsculas. Contiene las letras mayúsculas. Contiene todos los signos de puntuación. Hay otros más. Debe consultar la documentación de la API para conocer las clases de caracteres que soporta su JDK. Una clase puede contener otra. Por ejemplo, [[abc][012]] define una clase que buscará coincidencias con los caracteres a, b o c o los dígitos 0, 1 o 2. Por tanto, contiene la unión de los dos www.fullengineeringbook.net 10 Java: Soluciones de programación conjuntos. Por supuesto, este ejemplo podría escribirse de manera más conveniente como [abc012]. Sin embargo, las clases anidadas son muy útiles en otros contextos, como cuando se trabaja con conjuntos predefinidos o cuando quiere crear la intersección de dos conjuntos. Para crear una clase que contenga la intersección de dos o más conjuntos de caracteres, use el operador &&. Por ejemplo, esto crea un conjunto que busca coincidencias de todos los caracteres de palabra, excepto para las letras mayúsculas [\w && [^A–Z]]. Otros dos puntos: La parte exterior de una clase de caracteres, – se trata como un carácter normal. Asimismo, la parte exterior de una clase, la ^ se usa para especificar el inicio de una línea, como se describe en breve. El carácter comodín El carácter comodín es . (punto) y coincide con cualquier carácter. Por tanto, un patrón que consta de un . buscará coincidencias de estas (y otras) secuencias de entrada: "A", "a", "x" y "!" En esencia, el punto es una clase predefinida que coincide con todos los caracteres. Para crear un patrón que busque coincidencias con un punto, anteceda éste con una \. Por ejemplo, dada esta cadena de entrada. Final del juego. esta expresión juego\. busca coincidencias con la secuencia "juego". Cuantificadores Un cuantificador determina cuántas veces se buscarán coincidencias con una expresión. A continuación se muestran los cuantificadores: + * ? Busca una o más coincidencias. Busca cero o más coincidencias. Busca cero o una coincidencia. Por ejemplo, x+ buscará una o más x, como "x", "xx", "xxx", etc. El patrón .* buscará coincidencias de cualquier carácter cero o más veces. El patrón ,? buscará cero o una coma. También puede especificar un cuantificador que buscará coincidencias de un patrón un número específico de veces. He aquí una forma general: {núm} Por tanto, x{2} encontrará "xx", pero no "x" o "xxx". Puede especificar que se busquen coincidencias de un patrón por lo menos un número mínimo de veces al usar este cuantificador: {mín,} Por ejemplo, x{2,} buscará xx, xxx, xxxx, etc. Puede especificar que se busques coincidencias de un patrón por lo menos un número mínimo de veces, pero no más de un número máximo usando este cuantificador: {mín, máx} www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 11 Cuantificadores avaros, renuentes y posesivos En realidad hay tres tipos de cuantificadores: avaros, renuentes y posesivos. Los ejemplos de cuantificadores que se acaban de mostrar son de la variedad avara. Encuentran la secuencia coincidente más larga. Un cuantificador renuente (también denominado cuantificador holgazán) encuentra la secuencia coincidente más corta. Para crear uno renuente, incluya al final un ? Un cuantificador posesivo busca la secuencia coincidente más larga y no encontrará una secuencia más corta, aunque habilite toda la expresión a fin de tener éxito. Para crear un cuantificador posesivo, coloque un + al final. Recorramos ejemplos de cada tipo de cuantificador que trata de encontrar una coincidencia en la cadena "sarape simple". El patrón s.+e buscará coincidencias con la secuencia más larga, que es toda la cadena "sarape simple", porque el cuantificador avaro .+ buscará coincidencias con todos los caracteres después de la primera s hasta la e final. El patrón s.+?e encontrará "sarape", que es la coincidencia más corta. Esto se debe a que el cuantificador renuente .+? se detendrá después de encontrar la primera secuencia coincidente. El patrón s.++e fallará, porque el cuantificador posesivo .++ encontrará todos los caracteres coincidentes después de la s inicial. Debido a que es posesivo, no liberará la e final para permitir coincidencias con el patrón general. Por tanto, no se encontrará la e final y la coincidencia fallará. Comparadores de límites En ocasiones querrá especificar un patrón que empieza o termina en algún límite, como al final de una palabra o el principio de una línea. Para ello, usará los comparadores de límites. Tal vez los comparadores de límites de uso más amplio sean ^ y $. Encuentran coincidencias en el inicio y el final de la línea en que se está buscando, que como opción predeterminada son el principio y el final de la cadena de entrada. Por ejemplo, dada la cadena "prueba1 prueba2", el patrón prueba.?$ encontrará "prueba2", pero el patrón ^prueba.? encontrará "prueba1". Si quiere que encuentre una coincidencia con uno de estos caracteres por sí solo, necesitará usar la secuencia de escape \^ o \$. Aquí se muestran los demás comparadores de límites. Comparador Coincide con \A Inicio de cadena \b Límite de palabra \B Límite que no es de palabra \G Fin de la coincidencia anterior \Z Final de la cadena (no incluye el terminador de línea) \z Final de la cadena (incluye el terminador de línea) El operador O Cuando creamos un patrón, puede especificar una o más opciones al usar el operador O, que es |. Por ejemplo, la expresión puede|podría buscará la palabra "puede" o la palabra "podría". A menudo el operador O se usa dentro de un grupo entre paréntesis. Para comprender la razón, imagine que quiere encontrar todos los usos de "puede" o "podría" además de cualquier palabra que se encuentre junto a ellas. He aquí una manera de componer una expresión que hace esto: \w+\s+(puede | podría)\s+\w+\b www.fullengineeringbook.net 12 Java: Soluciones de programación Dada la cadena Ella podría ir. No puede ahora. esta expresión regular encuentra estas dos coincidencias: Ella podría ir No puede ahora Si se eliminan los paréntesis alrededor de |, como en \w+\s+puede | podría\s+\w+\b la expresión encontraría estas dos coincidencias: podría ir No puede La razón es que el | ahora separa a toda la subexpresión \w\s+puede de la subexpresión podría \s+\w+\b. Por tanto, toda la expresión coincidirá con frases que empiezan con alguna palabra seguida de "puede" o frases que empiezan con "podría" seguida por alguna palabra. Grupos Un grupo se crea al incluir un patrón dentro de paréntesis. Por ejemplo, la expresión entre paréntesis (puede | podría) en la sección anterior forma un grupo. Además de vincular los elementos de una subexpresión, los grupos tienen un segundo propósito. Una vez que ha definido un grupo, otra parte de una expresión regular puede hacer referencia a la secuencia capturada por ese grupo. Cada conjunto de paréntesis define un grupo. El paréntesis de apertura del extremo izquierdo define al grupo uno, el siguiente paréntesis de apertura define al grupo dos, etc. Dentro de una expresión regular, se hace referencia a los grupos por número. El primer grupo es \1, el segundo \2, etcétera. Trabajemos con un ejemplo. Suponga que quiere encontrar frases dentro de la misma oración en que se usan las formas singular y plural de una palabra. Por razones de simplicidad, también suponga que sólo quiere encontrar plurales, que siempre terminan con s. Por ejemplo, dadas estas frases Tengo un perro, pero él tiene cuatro perros. Ella tiene un gato, ¿pero quiere una buena cantidad de gatos? Ella también tiene un perro. Pero no quisiera tener cuatro perros. quiere encontrar la frase "perro, pero él tiene cuatro perros" porque contiene una forma singular y una plural de perro dentro de la misma oración y la frase "gato, ¿pero quiere una buena cantidad de gatos?", porque tiene gato y gatos dentro de la misma oración. No quiere encontrar instancias que abarquen dos o más oraciones, de modo que no querrá encontrar las formas "perro" y "perros" contenidos en las dos últimas frases. He aquí una manera de escribir una expresión regular que haga esto. \b(\w+)\b[^.?!]*?\1s Aquí, la expresión entre paréntesis (\w+) crea un grupo que contiene una palabra. Este grupo se usa después para buscar entradas coincidentes posteriores cuando se hace referencia a él con \1. Cualquier número de caracteres puede encontrarse entre la palabra y su plural, siempre y cuando no se localicen terminadores de oración (.?!). Por tanto, el resto de la expresión sólo tiene éxito www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 13 cuando se encuentra una palabra posterior dentro de la misma oración que sea el plural de la palabra que está contenida en \1. Por ejemplo, cuando se aplica a la primera oración de ejemplo, (\w+) encontrará palabras coincidentes, poniendo la palabra en el grupo \1. Para que toda la expresión tenga éxito, una palabra posterior en la oración debe ser igual a la palabra contenida en \1 y debe ir seguida de inmediato por una s. Esto sólo ocurre cuando \1 contiene la palabra "perro", porque "perros" se encuentra después en la misma oración. Un tema adicional. Puede crear un grupo de no captura al añadir ?: después de los paréntesis de apertura, como en (?:\s*). Son posibles otros tipos de grupos que usan búsqueda hacia delante o hacia atrás positiva o negativa, pero están más allá del alcance de este libro. Secuencias de marcas El motor de expresiones regulares de Java soporta varias opciones que controlan la manera en que se buscan coincidencias de un patrón. Estas opciones se establecen o limpian al usar la siguiente construcción: (?f), donde f especifica que se establezca una marca. Hay seis marcas, que se muestran aquí: d Habilita el modo de línea de Unix. i Ignora las diferencias entre mayúsculas y minúsculas. m Habilita el modo multilínea, en que ^ y $ buscan coincidencias al principio y el final de las líneas, en lugar de toda la cadena de entrada. s Habilita el modo "punto en todo", que hace que el punto (.) busque coincidencias de todos los caracteres, incluido el terminador de línea. u Junto con i, causa que se hagan coincidencias no sensibles a mayúsculas y minúsculas de acuerdo con el estándar de Unicode, en lugar de suponer sólo caracteres ASCII. x Ignora espacios en blanco y comentarios con # en una expresión regular. Puede deshabilitar un modo al anteceder su marca con un signo de menos. Por ejemplo, (?–i) deshabilita la coincidencia no sensible a mayúsculas y minúsculas. Recuerde incluir el carácter de escape \ en cadenas de Java Un breve recordatorio antes de pasar a las soluciones. Cuando se crean cadenas de Java que contienen expresiones regulares, recuerde que debe usar la secuencia de escape \\ para especificar una \. Por tanto, la siguiente expresión regular \b\w+\b Debe escribirse así cuando se especifique como una literal de cadena en un programa de Java: "\\b\\w+\\b" Olvidar la inclusión del carácter de escape \ es una fuente común de problemas porque no siempre da como resultado errores en tiempo de compilación o de ejecución. En cambio, su expresión regular simplemente no encontrará coincidencias donde pensaba que las hallaría. Por ejemplo, si usa \b en lugar de \\b en la cadena anterior verá que una expresión trata de buscar coincidencias del carácter de retroceso, en lugar de utilizarse como límite de palabra. www.fullengineeringbook.net 14 Java: Soluciones de programación Ordene una matriz de cadenas de manera inversa Componentes clave Clases e interfaces Métodos java.lang.String int compareTo(String cad) java.util.Arrays static <T> void sort(T[ ] matriz, Comparator<? Super T> comp) java.util.Comparator<T> int compare(T objA, T objB) Ordenar es una tarea común en programación, y ordenar matrices de cadenas no es la excepción. Por ejemplo, tal vez quiera ordenar una lista de los artículos vendidos por una tienda en línea o una lista de nombres de clientes y direcciones de correo electrónico. Por fortuna, Java facilita el ordenamiento de matrices de cadenas porque proporciona el método de utilería sort( ), que está definido por la clase Arrays en java.util. En su forma predeterminada, sort( ) ordena cadenas en orden alfabético, sensible a mayúsculas y minúsculas, y esto es adecuado por muchas situaciones. Sin embargo, en ocasiones querrá ordenar una matriz de cadenas en orden alfabético inverso. Esto requiere un poco más de trabajo. Hay varias maneras de tratar el problema de ordenar a la inversa. Por ejemplo, una solución inocente consiste en ordenar la matriz y luego copiarla de atrás hacia delante en otra matriz. Además de carecer de elegancia, esta técnica también es ineficiente. Por fortuna, Java proporciona una manera simple, pero efectiva de ordenar a la inversa una matriz de cadenas. Este método usa un Comparator personalizado para especificar la manera en que debe aplicarse el orden y una versión de sort( ) que toma Comparator como argumento. Paso a paso Para ordenar una matriz de cadenas a la inversa se requieren tres pasos: 1. Cree un Comparator que invierte la salida de una comparación entre dos cadenas. 2. Cree un objeto de ese Comparator. 3. Pase la matriz que se ordenará y el Comparator a una versión de java.util.Arrays.sort( ) que tome un comparador como argumento. Cuando sort( ) regrese, la matriz se ordenará a la inversa. Análisis Comparator es una interfaz genérica que se declara como se muestra aquí: Comparator<T> www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 15 El parámetro de tipo T especifica el tipo de datos que se habrá de comparar. En este caso, String se pasará a T. Comparator define los dos métodos siguientes: int compare(T objA, T objB) boolean equals(Object obj) De éstos, sólo compare( ) debe implementarse. El método equals( ) simplemente especifica una sobreescritura de equals( ) en Object. La implementación de equals( ) le permite determinar si dos Comparator son iguales. Sin embargo, esta capacidad no siempre es necesaria. Cuando no se necesita (como sucede en este capítulo), no es necesario sobreescribir la implementación de Object. El método en que estamos interesados es compare( ). Determina la manera en que se compara un objeto con otro. Por lo general, debe devolver menos de cero si objA es menor que objB, más que cero si objA es mayor que objB y cero si los dos objetos son iguales. Al implementar compare( ) de esta manera se logra que opere de acuerdo con el orden natural de los datos. En el caso de cadenas, esto significa orden alfabético. Sin embargo, tiene la libertad de implementar compare( ) para adecuarse a las necesidades de su tarea. Para ordenar a la inversa una matriz de cadenas, necesitará crear una versión de compare( ) que invierta la salida de la comparación. He aquí la manera de implementar un operador inverso para String. // Crea un Comparator que devuelve la salida // de una comparación de cadena inversa. class CompCadInv implements Comparator<String> { // Implementa el método compare( ) de modo que // invierte el orden de la comparación de la cadena. public int compare(String cadA, String cadB) { // Compara cadB con cadA, en lugar de cadA con cadB. return cadB.compareTo(cadA); } } Revisemos de cerca CompCadInv. En primer lugar, observe que implementa Comparator. Esto significa que un objeto de tipo CompCadInv se puede usar en cualquier lugar que se necesite un Comparator. Asimismo, observe que implementa una versión específica de String de Comparator. Por tanto, CompCadInv no es, en sí, genérico. Sólo funciona con cadenas. Ahora, observe que el método compare( ) llama al método compareTo( ) de String para comparar dos cadenas. compareTo( ) es especificada por la interfaz Comparable, que está implementada por String (y muchas otras clases). Una clase que implementa Comparable garantiza que los objetos de esa clase pueden ordenarse. A continuación se muestra la forma general de compareTo( ), como la implementa String: int compareTo(String cad) Devuelve menos de cero si la cadena de invocación es menor que cad, más de cero si es mayor que cad y cero si son iguales. Una cadena es menor que otra si se encuentra antes en el orden alfabético y es mayor si se encuentra después. www.fullengineeringbook.net 16 Java: Soluciones de programación El método compare( ) de CompCadInv devuelve el resultado de la llamada a compareTo( ). Sin embargo, observe que compare( ) llama a compareTo( ) en orden inverso. Es decir, se llama a compareTo( ) en cadB mientras cadA se pasa como argumento. Para una comparación normal, cadA invocaría a compareTo( ), pasando cadB. Sin embargo, como cadB invoca a compareTo( ), se invierte el resultado de la comparación. Por tanto, se invierte el orden de las dos cadenas. Una vez que ha creado un comparador inverso, se crea un objeto de ese comparador y se pasa a esta versión de sort( ) definida por java.util.Arrays: static<T> void sort(T[ ] matriz, Comparator<? Super T> comp) Observe la cláusula super. Asegura que la matriz pasada a sort( ) sea compatible con el tipo de Comparator. Después de la llamada a sort( ), la matriz estará en orden alfabético invertido. Ejemplo En el siguiente ejemplo se invierte el orden de una matriz de cadenas. Para fines de demostración, también se les ordena de manera natural empleando la versión predeterminada de sort( ). // Ordena una matriz de cadenas en orden inverso. import java.util.*; // Crea un Comparator que devuelve la salida // de una comparación de cadena inversa. class CompCadInv implements Comparator<String> { // Implementa el método compare( ) de modo que // invierte el orden de la comparación de la cadena. public int compare(String cadA, String cadB) { // Compara cadB con cadA, en lugar de cadA con cadB. return cadB.compareTo(cadA); } } // Demuestra el comparador de cadena inverso. class OrdenCadInv { public static void main(String args[ ]) { // Crea una matriz simple de cadenas. String cads[ ] = { "perro", "caballo", "cebra", "vaca", "gato" }; // Muestra el orden inicial. System.out.print("Orden inicial: "); for(String s : cads) System.out.print(s + " "); System.out.println("\n"); // Ordena la matriz a la inversa. // Empieza por crear un comparador de cadena inversa. CompCadInv cci = new CompCadInv( ); www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 17 // Ahora, ordena las cadenas empleando el comparador inverso. Arrays.sort(cads, cci); // Muestra el orden inverso. System.out.print("Orden inverso: "); for(String s : cads) System.out.print(s + " "); System.out.println("\n"); // Para comparación, ordena la cadena de manera natural. Arrays.sort(cads); // Muestra el orden natural. System.out.print("Orden natural: "); for(String s : cads) System.out.print(s + " "); System.out.println("\n"); } } A continuación se muestra la salida de este programa: Orden inicial: perro caballo cebra vaca gato Orden inverso: vaca perro gato cebra caballo Orden natural: caballo cebra gato perro vaca Opciones Aunque esta solución ordena cadenas en orden alfabético inverso, la misma técnica básica puede generalizarse para otras situaciones. Por ejemplo, puede revertir el orden de otros tipos de datos al crear el Comparator apropiado. Simplemente adapta el método mostrado en el ejemplo. El método compareTo( ) definido por String es sensible a mayúsculas y minúsculas. Esto significa que ambos tipos de letra se ordenarán por separado. Tiene la opción de ordenar datos sin importar las diferencias entre mayúsculas y minúsculas al emplear compareToIgnoreCase( ). (Consulte Ignore las diferencias entre mayúsculas y minúsculas cuando ordene una matriz de cadenas). Puede ordenar cadenas con base en alguna subcadena específica. Por ejemplo, si cada cadena contiene un nombre y una dirección de correo electrónico, entonces puede crear un comparador que ordene a partir de la parte de la dirección de cada cadena. Una manera de realizar esto consiste en usar el método regionMatches( ). También puede ordenar por algún criterio diferente de una estricta relación alfabética. Por ejemplo, cadenas que representan tareas pendientes pueden ordenarse por prioridad. www.fullengineeringbook.net 18 Java: Soluciones de programación Ignore las diferencias entre mayúsculas y minúsculas cuando ordene una matriz de cadenas Componentes clave Clases e interfaces Métodos java.lang.String int compareToIgnoreCase(String cad) java.util.Arrays static<T> void sort(T[ ] matriz, Comparator<? super T> comp) java.util.Comparator<T> int compare(T objA, T objB) En Java, el orden natural de las cadenas es sensible a mayúsculas y minúsculas. Esto significa que las letras mayúsculas están separadas y son diferentes de las minúsculas. Como resultado, cuando ordena una matriz de cadenas, podrían ocurrir algunas sorpresas que no son bienvenidas. Por ejemplo, si ordena una matriz String que contiene las siguientes palabras: alfa beta Gama Zeta El orden resultante será como se muestra aquí: Gama Zeta alfa beta Como verá, aunque Gama y Zeta normalmente se encontrarían después de alfa y beta, están al principio de la matriz ordenada. La razón es que, en Unicode, las mayúsculas están representadas por valores menores que los usados para las minúsculas. Por tanto, aunque Zeta se encontraría normalmente al final de la lista cuando se ordena alfabéticamente, se encuentra antes de alfa cuando son importantes las diferencias entre mayúsculas y minúsculas. ¡Esto puede llevar a órdenes que producen resultados técnicamente exactos, pero indeseables! NOTA Como algo interesante hay que mencionar que una mayúscula vale exactamente 32 menos que su equivalente en minúsculas. Por ejemplo, el valor de Unicode para la A es 65. Y para la a es 97. Por fortuna, es muy fácil ordenar una matriz de cadenas con base en el verdadero orden alfabético al crear un Comparator que ignore si una letra está en mayúsculas o minúsculas durante el proceso de ordenamiento. La técnica es similar a la descrita en Ordene una matriz de cadenas de manera inversa. Los detalles se describen a continuación. Paso a paso Para ignorar las diferencias entre mayúsculas y minúsculas cuando ordene una matriz de cadenas se incluyen estos tres pasos: 1. Cree un Comparator que ignore las diferencias entre mayúsculas y minúsculas de dos cadenas. 2. Cree un objeto de ese Comparator. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 19 3. Pase la matriz que se ordenará y el Comparator a la versión de comparador de java.util. Arrays.sort( ). Cuando regrese sort( ), la matriz se ordenará sin importar las diferencias entre mayúsculas y minúsculas. Análisis Para ignorar las diferencias entre mayúsculas y minúsculas cuando se ordena una matriz de cadenas, necesitará implementar un Comparator no sensible a mayúsculas y minúsculas para cadenas. Para ello, defina una versión de compare( ) que ignore las diferencias cuando compara objetos de String. Puede utilizar un método similar al usado para ordenar una matriz de cadenas en orden inverso, mostrado antes. (Consulte Ordene una matriz de cadenas de manera inversa para conocer detalles acerca de Comparator.) He aquí una manera de implementar un comparador para String que ignore las diferencias entre mayúsculas y minúsculas. // Crea un Comparator que devuelve el resultado // de una comparación no sensible a mayúsculas y minúsculas. class CompIgnMayMin implements Comparator<String> { // Implementa el método compare( ) para que ignore las // diferencias entre mayúsculas y minúsculas cuando compare cadenas. public int compare(String cadA, String cadB) { return cadA.compareToIgnoreCase(cadB); } } Observe que compare( ) llama al método compareToIgnoreCase( ) de String para comparar dos cadenas. Este método ignora las diferencias entre mayúsculas y minúsculas cuando compara dos cadenas. A continuación se muestra la forma general de compareToIgnoreCase( ): int compareToIgnoreCase(String cad) Devuelve menos de cero si la cadena de invocación es menor que cad, más de cero si es mayor y cero si son iguales. El método compare( )devuelve el resultado de la llamada a compareToIgnoreCase( ). Por tanto, se ignoran las diferencias entre las dos cadenas que se están comparando. Una vez que ha creado un comparador no sensible a diferencias entre mayúsculas y minúsculas, se crea un objeto de ese comparador y se pasa a esta versión de sort( ) definida por java.util.Arrays: static<T> void sort(T[ ] matriz, Comparator<? Super T> comp) Después de llamar a sort( ), la matriz estará en verdadero orden alfabético, con las diferencias entre mayúsculas y minúsculas ignoradas. Ejemplo En el siguiente ejemplo se ignoran las diferencias entre mayúsculas y minúsculas cuando se ordena una matriz de cadenas. Para fines de demostración, también se ordenan utilizando el orden predeterminado. // Ordena una matriz de cadenas, ignora la diferencia // entre mayúsculas y minúsculas. import java.util.*; www.fullengineeringbook.net 20 Java: Soluciones de programación // Crea un Comparator que devuelve el resultado // de una comparación no sensible a mayúsculas y minúsculas. class CompIgnMayMin implements Comparator<String> { // Implementa el método compare( ) para que ignore las // diferencias entre mayúsculas y minúsculas cuando compare cadenas. public int compare(String cadA, String cadB) { return cadA.compareToIgnoreCase(cadB); } } // Demuestra el comparador de cadenas que es insensible a // diferencias entre mayúsculas y minúsculas. class OrdIgnMayMin { public static void main(String args[ ]) { // Crea una matriz simple de cadenas. String cads[ ] = { "alfa", "Gama", "Zeta", "beta", }; // Muestra el orden inicial. System.out.print("Orden inicial: "); for(String s : cads) System.out.print(s + " "); System.out.println("\n"); // Ordena la matriz pero ignora las diferencias entre // mayúsculas y minúsculas. Crea un comparador de // cadenas que no es sensible a mayúsculas y minúsculas. CompIgnMayMin cimm = new CompIgnMayMin( ); // Ordena las cadenas usando el comparador. Arrays.sort(cads, cimm); // Muestra el orden no sensible a mayúsculas y minúsculas. System.out.print("Orden insensible a may\u00a3sculas y min\u00a3sculas: "); for(String s : cads) System.out.print(s + " "); System.out.println("\n"); // Para comparación, ordena las cadenas en el orden predeterminado, // que es sensible a diferencias entre mayúsculas y minúsculas. Arrays.sort(cads); // Muestra el orden sensible a mayúsculas y minúsculas. System.out.print("Orden predeterminado, sensible a diferencias: "); for(String s : cads) System.out.print(s + " "); System.out.println("\n"); } } A continuación se muestra la salida de este programa. Observe que el orden predeterminado coloca las mayúsculas antes que las minúsculas, lo que a menudo da un orden insatisfactorio. Empleando el comparador IgnoreCaseComp se corrige este problema. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 21 (N del T. Observe también la inclusión de la secuencia de escape \u00a3, que se utiliza para desplegar la "ú" en pantalla). Orden inicial: alfa Gama Zeta beta Orden insensible a mayúsculas y minúsculas: alfa beta Gama Zeta Orden predeterminado, sensible a diferencias en mayúsculas y minúsculas: Gama Zeta alfa beta Opciones Aunque el método compareToIgnoreCase( ) funciona bien en muchos casos (como cuando se comparan cadenas en inglés), no funcionara para todos los idiomas. Para asegurar un pleno soporte internacional, debe usar el método compare( ) especificado por java.text.Collator. Se muestra a continuación: int compare(String cadA, String cadB) Devuelve menos de cero si la cadena de invocación es menor que cad, más de cero si es mayor y cero si son iguales. Usa el orden de intercalación estándar definido por el idioma local. Puede obtener un Collator para el idioma local al llamar a su método de fábrica getInstance( ). A continuación, se establece el nivel de fuerza del intercalador para que sólo se usen las diferencias primarias entre caracteres. Esto se hace al llamar a setStrenght( ), pasando Collator.PRIMARY como argumento. Empleando este método, es posible reescribir CompIgnMayMin de la siguiente manera: // Este Comparator usa un Collator para determinar // el orden lexicográfico apropiado, insensible a mayúsculas // y minúsculas de dos cadenas. class CompIgnMayMin implements Comparator<String> { Collator col; CompIgnMayMin( ) { // Obtiene un Collator para este idioma. col = Collator.getInstance( ); // Sólo tiene que considerar diferencias primarias. col.setStrength(Collator.PRIMARY); } // Usel el método compare( ) de Collator para comparar cadenas. public int compare(String cadA, String cadB) { return col.compare(cadA, cadB); } } Si sustituye esta versión en el ejemplo, producirá los mismos resultados que antes. Sin embargo, ahora funcionará de una manera independiente del idioma local. NOTA En algunas situaciones, cuando use Collator, tal vez le resulte útil la clase java.text.CollationKey. www.fullengineeringbook.net 22 Java: Soluciones de programación Ignore las diferencias entre mayúsculas y minúsculas cuando busque o reemplace subcadenas Componentes clave Clases Métodos Java.lang.String boolean matches(String expReg) String replaceAll(String expReg, String, cadReem) String contiene varios métodos que le permiten buscar una subcadena específica en una cadena. Por ejemplo, puede usar contains( ), indexOf( ), lastIndexOf( ), startsWith( ) o endsWith( ), dependiendo de sus necesidades. Sin embargo, en todos éstos, la búsqueda se realiza de manera sensible a mayúsculas y minúsculas. Por tanto, si está buscando la subcadena "el" en la cadena "El cielo es azul", la búsqueda fallará. Esto representa un problema en muchas situaciones de búsqueda. Por fortuna, String le ofrece una manera fácil de realizar búsquedas de un modo insensible a mayúsculas y minúsculas mediante el uso del método matches( ) y expresiones regulares. El reemplazo de una subcadena de manera insensible a la diferencia entre mayúsculas y minúsculas se encuentra relacionado con la búsqueda bajo las mismas condiciones. String proporciona dos métodos que reemplazan una subcadena con otra. El primero es replace( ), que se usa para reemplazar un carácter con otro, o una subcadena con otra. Sin embargo, funciona de una manera sensible a la diferencia entre mayúsculas y minúsculas. Para realizar una búsqueda que no sea sensible, puede utilizar el método replaceAll( ). Reemplaza todas las apariciones de cadenas que coinciden con una expresión regular. Por tanto, puede usarse para realizar operaciones de búsqueda y reemplazo que ignoren las diferencias entre mayúsculas y minúsculas. Paso a paso Para buscar o reemplazar una subcadena de una manera que no sea sensible a las diferencias entre mayúsculas y minúsculas, se requieren estos dos pasos: 1. Construya una expresión regular que especifique la secuencia de caracteres que está buscando. Anteceda la secuencia con la marca para ignorar diferencias entre mayúsculas y minúsculas (?!). Esto hace que se encuentren coincidencias independientemente de las mayúsculas y minúsculas. 2. Para buscar el patrón, llame a matches( ), especificando la expresión regular. Como opción, para reemplazar todas las apariciones de una subcadena con otra, llame a replaceAll( ), especificando la expresión regular y el reemplazo. Análisis Es muy fácil crear una expresión regular que coincida con una subcadena específica, pero que ignore las diferencias entre mayúsculas y minúsculas. Sólo anteceda la secuencia de caracteres deseada con la marca para ignorar esas diferencias, (?i). Por ejemplo, para buscar la secuencia "esta" de manera insensible a mayúsculas y minúsculas, usaría este patrón: (?i)esta. Esto encontrará, por ejemplo, "esta", "Esta" y "ESTA". También encontrará "cesta", porque no existe www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 23 el requisito de que se busque "esta" como una palabra separada. Para encontrar sólo palabras completas, usaría esta expresión: \b(?i)esta\b. Una vez que haya definido el patrón insensible a las diferencias entre mayúsculas y minúsculas, puede llamar a matches( ) para determinar si alguna secuencia de caracteres coincide con ese patrón. Aquí se muestra su forma general: boolean marches( )String expReg) Devuelve verdadero si se encuentra una secuencia que coincide con expReg en la cadena que se invoca y falso, de lo contrario. Por ejemplo, esta llamada a matches( ) devuelve verdadero: "Las aguas son profundas".matches("(?i)las.* "); Debido a que la literal "las" está antecedida por (?i), coincidirá con "Las" en la cadena que se invoca. Las diferencias entre mayúsculas y minúsculas se ignoran. Sin el uso de (?i), la búsqueda fallaría debido a las diferencias entre "las" y "Las". El método matches( ) lanzará una excepción PatternSyntaxException si expReg no es válida. Puede reemplazar todas las apariciones de una subcadena con otra al llamar a replaceAll( ). Aquí se muestra su forma general: String replaceAll(String expReg, String, cadReem) Devuelve una nueva cadena en que todas las apariciones del patrón especificado por expReg en la cadena que se invoca se han reemplazado con cadReem. Si especifica la marca para ignorar diferencias entre mayúsculas y minúsculas cuando se construye la expReg, entonces se ignorarán esas diferencias. Por ejemplo, suponga que cad contiene la siguiente cadena (nótese que obviamos la inclusión de escapes para los caracteres con acento): ¿Qué día es hoy? ¿Es viernes? Entonces la siguiente llamada reemplaza todas las apariciones de "es" con "fue": cad.replaceAll("(?i)es", "fue"); La cadena resultante es: ¿Qué día fue hoy? ¿Fue viernes? El método replaceAll( ) también lanzará una PatternSyntaxException si expReg no es válida. Ejemplo En el siguiente ejemplo se muestra cómo buscar y reemplazar de una manera que no sea sensible a las diferencias entre mayúsculas y minúsculas: // Ignora las diferencias entre mayúsculas y minúsculas cuando se // buscan subcadenas para reemplazo. class DemoIgnMayMin { public static void main(String args[ ]) { String cad = "Se trata de una PRUEBA."; System.out.println("Ignora may\u00a3sculas y min\u00a3sculas al buscar.\n" + "Buscando ‘prueba’ en: " + cad); www.fullengineeringbook.net 24 Java: Soluciones de programación // Usa matches( ) para encontrar cualquier versión de prueba. if(cad.matches("(?i).*prueba.*")) System.out.println("prueba se encuentra en la cadena."); System.out.println( ); cad = "alfa beta, Alfa beta, alFa beta, ALFA beta"; // Usa replaceAll( ) para ignorar las diferencias entre mayúsculas // y minúsculas cuando se reemplaza una subcadena con otra. // En este ejemplo, se reemplazan todas las versiones de alfa con zeta. System.out.println("Ignora may\u00a3sculas y min\u00a3sculas al reemplazar.\n" + "Reemplaza cualquier versi\u00a2n de ‘alfa’ " + "con ‘zeta’ en:\n" + " " + cad); String resultado = cad.replaceAll("(?i)alfa", "zeta"); System.out.println("Luego del reemplazo:\n" + " " + resultado); } } Aquí se muestra la salida: Ignora mayúsculas y minúsculas al buscar. Buscando ‘prueba’ en: Se trata de una PRUEBA. prueba se encuentra en la cadena. Ignora mayúsculas y minúsculas al reemplazar. Reemplaza cualquier versión de ‘alfa’ con ‘zeta’ en: alfa beta, Alfa beta, alFa beta, ALFA beta Luego del reemplazo: zeta beta, zeta beta, zeta beta, zeta beta Como un punto de interés hay que señalar que, debido a que las expresiones regulares se encuentran dentro del código de este ejemplo y se sabe que son válidas sintácticamente, no hay necesidad de capturar una PatternSyntaxException, porque no ocurrirá ninguna excepción. (Es el mismo caso de otros varios ejemplos de este capítulo). Sin embargo, en su propio código, tal vez necesite manejar este posible error. Esto resulta especialmente cierto si las expresiones regulares se construyen en tiempo de ejecución, como sucede a partir de la entrada del usuario. Opciones Aunque matches( ) ofrece el poder y la elegancia de usar una expresión regular, no es la única manera de buscar una cadena de manera insensible a las diferencias entre mayúsculas y minúsculas. En algunas situaciones tal vez pueda usar esta versión del método regionMatches( ): boolean regionMatches(boolean ignMayMin, int inicio, String cad2, int inicio2, int numCars) Si ignMayMin es verdadero, entonces la búsqueda ignora las diferencias entre mayúsculas y minúsculas. El punto inicial de la búsqueda en la cadena que se invoca se especifica con inicio. El punto de partida de la búsqueda en la segunda cadena se pasa en inicio2, la segunda cadena se especifica con cad2 y el número de caracteres para comparar se especifica con numCars. Por supuesto, regionMatches( ) sólo compara las partes especificadas de las dos cadenas, pero eso bastará para algunas necesidades. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 25 Otra manera de ignorar las diferencias entre mayúsculas y minúsculas cuando se busca una subcadena consiste en convertir primero ambas cadenas a minúsculas (o mayúsculas, según lo elija). Luego, llame a contains( ) para determinar si la cadena contiene la subcadena. En general, este método es menos atractivo que usar un patrón insensible a las diferencias entre mayúsculas y minúsculas con matches( ). Sin embargo, podría ser una buena elección si tiene otra razón para convertir las cadenas a sólo mayúsculas o sólo minúsculas. Por ejemplo, si la cadena que se buscará contiene una serie de comandos de base de datos, entonces normalizar las cadenas podría simplificar otras partes de su programa. Si sólo quiere reemplazar la primera aparición de un patrón, use replaceFirst( ) en lugar de replaceAll( ). Aquí se muestra cómo: String replaceFirst(String expReg, String cadReem) La primera secuencia en la cadena que se invoca y que coincida con regExp se reemplazará con cadReem. Se lanzará una PatternSyntaxException si expReg no es válida. También es posible usar la API de expresiones regulares de Java para realizar una búsqueda y reemplazo que no sea sensible a las diferencias entre mayúsculas y minúsculas. Divida una cadena en partes empleando split( ) Componentes clave Clases Métodos java.lang.String String[ ]split(String expReg) En ocasiones, querrá descomponer una cadena en un conjunto de subcadenas, con base en algún criterio. Por ejemplo, dada una dirección de correo electrónico, tal vez quiera obtener dos subcadenas. La primera es el nombre del destinatario; la segundo es el URL. En este caso, el separador es @. En el pasado, habría hecho una búsqueda manual de @ empleando un método como indexOf( ) y luego substring( ) para recuperar cada parte de la dirección. Sin embargo, a partir de Java 1.4 quedó disponible una opción más conveniente: split( ). El método split( ) emplea una expresión regular para proporcionar una manera muy conveniente de descomponer una cadena en un conjunto de subcadenas en un solo paso. Debido a que los caracteres que forman los delimitadores entre subcadenas están especificados por un patrón, puede usar split( ) para manejar algunos problemas complicados de descomposición. Por ejemplo, el uso de split( ) es fácil para obtener una lista de las palabras contenidas dentro de una cadena. Simplemente especifique una expresión regular que busque todos los caracteres que no son de palabra. Muchos otros tipos de divisiones también son fáciles. Por ejemplo, para dividir una cadena que contiene una lista de valores separados por comas, como 10, 20, 30, etc., simplemente especifique una coma como expresión delimitante. Sin importar cómo lo use, split( ) es uno de los métodos basados en expresiones regulares más importantes de String. Una vez que lo domine, le sorprenderá la frecuencia con que habrá de usarlo. Paso a paso La división de una cadena en partes incluye estos dos pasos: 1. Cree una expresión regular que define el delimitador que se usará para dividir la cadena. 2. Llame a split( ) en la cadena, pasándola en la expresión regular. Se regresa una matriz de cadenas que contiene www.fullengineeringbook.net las piezas. 26 Java: Soluciones de programación Análisis El método split( ) definido por String descompone la cadena que se invoca. El sitio donde termina una subcadena y empieza la siguiente se determina con una expresión regular. Por tanto, la expresión regular especifica los delimitadores que terminan una subcadena. (La primera subcadena está delimitada por el principio de la cadena. La subcadena final está delimitada por el final de la cadena.) Hay dos formas de split( ). Aquí se muestra la usada en la solución: String[ ]split(String expReg) La expresión delimitante está especificada por expReg. El método devuelve una matriz que contiene las piezas de la cadena. Si no hay coincidencias con expReg entonces se devuelve toda la cadena. El método split( ) lanzará una PatternSyntaxException si expReg no contiene una expresión regular válida. Cuando se construye una expresión regular para usar en split( ) tenga presente que está definiendo el patrón que separa una subcadena de otra. No está especificando un patrón que buscará coincidencias con las piezas que quiere. Por ejemplo, dada la cadena Se trata de una prueba. para dividir esta cadena en palabras que están separadas por espacios, pase la siguiente expresión regular a expReg. "\\s+" Esto da como resultado una matriz que contiene las siguientes subcadenas: Se trata de una prueba. La subcadena final "prueba". incluye el punto porque sólo los espacios coinciden con la expresión regular. Sin embargo, como se explicó, la última subcadena termina con el final de la cadena de entrada, y no necesita terminar en una coincidencia con la expresión regular. Ejemplo En el siguiente ejemplo se muestran varios ejemplos que dividen una cadena en partes basada en varias expresiones regulares. // Usa split( ) para extraer una subcadena de una cadena. class DemoDiv { static void showSplit(String[ ] cads) { for(String cad : cads) System.out.print(cad + "|"); System.out.println("\n"); } // Demuestra split( ). public static void main(String args[ ]) { String resultado[ ]; // Divide en los espacios. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares String cadPrueba = "Se trata de una prueba."; System.out.println("Cadena original: " + cadPrueba); resultado = cadPrueba.split("\\s+"); System.out.print("Dividida en espacios: "); showSplit(resultado); // Divide en límites de palabras. cadPrueba = "Uno, dos y tres."; System.out.println("Cadena original: " + cadPrueba); resultado = cadPrueba.split("\\W+"); System.out.print("Dividida en l\u00a1mites de palabras: "); showSplit(resultado); // Divide alguna cadena en comas y cero o más espacios. System.out.println("Cadena original: " + cadPrueba); resultado = cadPrueba.split(",\\s*"); System.out.print("Dividida en comas: "); showSplit(resultado); // Divide en límites de palabras, pero permite // puntos y @ insertados. cadPrueba = "Jerry Jerry@HerbSchildt.com"; System.out.println("Cadena original: " + cadPrueba); resultado = cadPrueba.split("[\\W && [^.@]]+"); System.out.print("Permite que . y @ sean parte de una palabra: "); showSplit(resultado); // Divide en signos de puntuación y cero o más espacios al final. cadPrueba = "Se! trata, de. una:; prueba?"; System.out.println("Cadena original: " + cadPrueba); resultado = cadPrueba.split("[.,!?:;]+\\s*"); System.out.print("Dividida en signos de puntuaci\u00a2n: "); showSplit(resultado); } } A continuación se muestra la salida: Cadena original: Se trata de una prueba. Dividida en espacios: Se|trata|de|una|prueba.| Cadena original: Uno, dos y tres. Dividida en límites de palabras: Uno|dos|y|tres| Cadena original: Uno, dos y tres. Dividida en comas: Uno|dos y tres.| Cadena original: Jerry Jerry@HerbSchildt.com Permite que y @ sean parte de una palabra: Jerry|Jerry@HerbSchildt.com| Cadena original: Se! trata, de. una:; prueba? Dividida en signos de puntuación: Se|trata|de|una|prueba| www.fullengineeringbook.net 27 28 Java: Soluciones de programación Opciones Aunque el método split( ) es muy poderoso, le falta un poco de flexibilidad. Por ejemplo, es difícil usar split( ) para descomponer una cadena basada en delimitadores que cambien en relación con el contexto. Aunque es posible idear expresiones regulares muy complejas que puedan manejar operaciones de búsqueda de coincidencias muy sofisticadas, éste no es siempre el mejor método. Más aún, habrá ocasiones en que quiera dividir en fichas por completo una cadena, en que se obtengan todas las partes de la cadena (incluidos delimitadores). En estas situaciones, puede usar las clases Pattern y Matcher proporcionadas por la API de expresiones regulares de Java para obtener un control más conveniente, detallado. Este método se muestra en Divida en fichas una cadena empleando la API de expresiones regulares. Puede limitar el número de coincidencias que encontrará split( ), y por tanto el número de subcadenas que se devolverán, empleando esta forma de split( ): String[ ]split(String expReg, int num) Aquí, expReg especifica la expresión delimitante. El número de veces que se buscará coincidencias se pasa en num. Se lanzará PatternSyntaxException si expReg no contiene una expresión regular válida. Recupere pares clave/valor de una cadena Componentes clave Clases Métodos java.lang.String String[ ]split(String expReg) String trim( ) Un uso especialmente bueno de split( ) se encuentra en la recuperación de pares clave/valor de una cadena. Los pares clave/valor son muy comunes en programación, sobre todo la basada en Web. Por esto, Java les proporciona un soporte importante. Por ejemplo, la clase Properties y las varias implementaciones de la interfaz Map operan con pares de claves y valores. Por desgracia, no siempre se dan los pares clave/valor en un forma conveniente para usarla como HashMap, por ejemplo. A menudo, se reciben en forma de cadena y nos queda el trabajo de extraerlos. La solución que se muestra aquí ilustra un método para realizar esta tarea común. Los pares clave/valor se representan en una cadena de varias maneras. En la solución que se muestra aquí, se supone la siguiente organización: clave = valor, clave = valor, clave = valor,... Cada par clave/valor está separado del siguiente por una coma. Cada clave está vinculada con su valor por un signo de igual. Se permiten espacios, pero no son necesarios. La técnica mostrada aquí puede adaptarse fácilmente para manejar otros formatos. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 29 Paso a paso La extracción de pares clave/valor de una cadena incluye tres pasos: 1. Elimine cualquier espacio en blanco al principio o al final de la cadena que contiene los pares clave/valor al llamar a trim( ). 2. Divida la cadena que contiene los pares clave/valor en subcadenas individuales que contienen una sola clave y un valor al usar una coma (con espacios en blanco opcionales) como delimitador en una llamada a split( ). He aquí una manera de especificar la expresión delimitadora: \s*,\s*. Cada elemento de la matriz resultante contiene un par clave/valor. 3. Divida cada subcadena individual de clave/valor en su clave y valor al llamar a split( ) con = como delimitador. Permita, pero no imponga, espacios en blanco. He aquí una manera de especificar el delimitador: \s*=\s*. La matriz resultante contiene una clave individual y su valor. Análisis La operación de split( ) se describe en la solución anterior. Para eliminar los espacios al principio y el final de la cadena de entrada, se usa el método trim( ) de String. Aquí se muestra: String trim( ) Elimina los espacios al principio y al final de la cadena que se invoca y devuelve el resultado. La manera en que la cadena de entrada se forma determina la manera en que se construyen las expresiones delimitadoras. Como se explicó, esta solución supone un formato muy común. Sin embargo, si su cadena de entrada difiere, entonces debe ajustar de manera apropiada los delimitadores. Ejemplo En el siguiente ejemplo se crea un método de propósito general llamado obtenerParesCV( ) que reduce una cadena que contiene uno o más pares clave/valor en las partes que lo integran. Cada par clave/valor está almacenado en un objeto de tipo parCV, que es definido por el programa. obtenerParesCV( ) devuelve una matriz de parCV. El método obtenerParesCV( ) le permite especificar las expresiones regulares que definen los delimitadores que separan cada par clave/valor del siguiente, y que separan una clave individual de su valor. Por tanto, puede usarse obtenerParesCV( ) para dividir una amplia variedad de formatos clave/valor. Si la cadena de entrada no está en un formato esperado, obtenerParesCV( ) lanza una ExcepcionDivCV que es una excepción definida por el programa. Si cualquier expresión delimitadora no es válida, se lanza una PatternSyntaxException. // Usa split( ) para extraer pares clave/valor de una cadena. import java.util.regex.PatternSyntaxException; // Una clase que mantiene pares clave/valor como Strings. class ParCV { String clave; String valor; www.fullengineeringbook.net 30 Java: Soluciones de programación ParCV(String c, String v) { clave = c; valor = v; } } // una clase de excepción para errores de obtenerParesCV( ). class ExcepcionDivCV extends Exception { // Llenar los detalles necesarios. } // Esta clase encapsula el método estático obtenerParesCV( ). class DividirCV { // Este método extrae los pares clave/valor almacenados // en una cadena y devuelve una matriz que contiene // cada clave y valor encapsulado en un objeto ParCV. // // Se pasa a la cadena que contiene los pares clave/valor // y dos expresiones regulares que describen los delimitadores // usados para extraer los pares clave/valor. El parámetro parSep // especifica el patrón que separa un par clave/valor // del siguiente dentro de la cadena. El parámetro sepCV // especifica el patrón que separa una clave de un valor. // // Lanza una PatternSyntaxException si una expresión // separadora no es válida y una ExcepcionDivCV si // la cadena entrante no contiene pares clave/valor // en el formato esperado. public static ParCV[ ] obtenerParesCV(String cad, String parSep, String sepCV) throws PatternSyntaxException, ExcepcionDivCV { // Primero, recorta la cadena entrante para eliminar espacios // al principio y al final. cad = cad.trim( ); // Luego, divide la cadena entrante en cadenas individuales // y cada una contiene un par clave/valor. La expresión // en parSep determina la secuencia de caracteres que // separa un par clave/valor del siguiente. String[ ] cadsCV = cad.split(parSep); // Ahora, construye una matriz ParCV que contendrá // cada clave y valor como cadenas individuales. ParCV[ ] pscv = new ParCV[cadsCV.length]; // Extrae cada clave y valor. String[ ] tmp; for(int i = 0; i < cadsCV.length; i++) { tmp = cadsCV[i].split(sepCV); // // Si una matriz devuelta por split( ) tiene más // o menos de 2 elementos, entonces la cadena de // entrada contiene algo más que pares www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares // clave/valor, o se encuentra en un formato no // válido. En este caso, se lanza una excepción. if(tmp.length != 2) throw new ExcepcionDivCV( ); // De otra manera, se almacena el siguiente par clave/valor. pscv[i] = new ParCV(tmp[0], tmp[1]); } return pscv; } } // Demuestra obtenerParesCV( ). class DemoParCV { public static void main(String args[ ]) { String cadPrueba = "Nombre = Juan, Edad = 27, NumID = 1432, Sueldo = 37.25"; System.out.println("Cadena clave/valor: " + cadPrueba); // Obtiene una matriz que contiene las claves y los valores. ParCV parescv[ ]; try { // Esta llamada a obtenerParesCV( ) especifica que los // pares clave/valor están separados por una coma (y // cualquier cantidad de espacios) y que una clave // está separada de su valor por un = (y cualquier // cantidad de espacios). parescv = DividirCV.obtenerParesCV(cadPrueba, "\\s*,\\s*", "\\s*=\\s*"); } catch(PatternSyntaxException exc) { System.out.println("Expresi\u00a2n no v\u00a0lida de separador."); return; } catch(ExcepcionDivCV exc) { System.out.println("Error al obtener claves y valores."); return; } // Despliega cada clave y su valor. for(ParCV pcv : parescv) System.out.println("Clave: " + pcv.clave + "\tValor: " + pcv.valor); } } www.fullengineeringbook.net 31 32 Java: Soluciones de programación Aquí se muestra la salida: Cadena Clave: Clave: Clave: Clave: clave/valor: Nombre = Juan, Edad = 27, NumID = 1432, Sueldo = 37.25 Nombre Valor: Juan Edad Valor: 27 NumID Valor: 1432 Sueldo Valor: 37.25 Opciones Aunque el uso de split( ) suele ser el método más fácil, por mucho, puede extraer pares clave/valor al emplear una combinación de otros métodos de String, como indexOf( ) y substring( ). Este tipo de implementación puede ser apropiado cuando la cadena que contiene los pares clave/valor no usa un formato uniforme. La extracción de pares clave/valor es un caso especial de un concepto más general: división en fichas. En algunos casos, sería más fácil (o más conveniente) dividir en fichas el flujo de entrada en todas sus partes constituyentes y luego determinar qué constituye un par clave/valor empleando otra lógica de programa. Este tipo de método sería especialmente útil en la cadena de entrada que contiene más que sólo pares clave/valor. Consulte Divida en fichas una cadena empleando la API de expresiones regulares, para conocer más detalles acerca de la división en fichas. Compare y extraiga subcadenas empleando la API de expresiones regulares Componentes clave Clases Métodos java.util.regex.Pattern Pattern.compile(String expReg) Matcher matcher(CharSequence cad) java.util.regex.Matcher boolean find( ) String group( ) Aunque el soporte de String a expresiones regulares es muy útil, no proporciona acceso a todas las características soportadas por la API de expresiones regulares. Hay ocasiones en que necesitará usar directamente la API de expresiones regulares. En tales ocasiones querrá obtener una subcadena que busque coincidencias de un patrón general. Por ejemplo, suponga que tiene una cadena que contiene una lista de la información para ponerse en contacto con los empleados de una empresa, que incluye números telefónicos y direcciones de correo electrónico. Más aún, suponga que quiere extraer las direcciones de correo electrónico de esta cadena. ¿Cómo realiza esto? Aunque la API de Java ofrece varias maneras de resolver este problema, la más sencilla consiste en usar las clases Pattern y Matcher definidas por la API de expresiones regulares. En esta solución se muestra cómo completar esta tarea. Paso a paso Para obtener una subcadena que busca coincidencias de una expresión regular se requieren los siguientes pasos: www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 33 1. Cree una instancia de Pattern al llamar a su método de fábrica compile( ). Pase a compile( ) la expresión regular que describe el patrón que está buscando. 2. Cree un Matcher que contiene la cadena de entrada al llamar a matcher( ) en el objeto Pattern. 3. Llame a find( ) en Matcher para buscar una coincidencia. Devuelve verdadero si se encuentra la secuencia, y falso si no. 4. Si find( ) tiene éxito, llame a group( ) en Matcher para obtener la secuencia coincidente. Análisis La clase Pattern no define constructores. En cambio, se crea un patrón al llamar el método de fábrica compile( ). Aquí se muestra la forma usada en esta solución: static Pattern compile(String expReg) Aquí, expReg es la expresión regular que quiere encontrar. El método compile( ) transforma expReg en un patrón que la clase Matcher puede usar para buscar coincidencias de patrones. Devuelve un objeto Pattern que contiene el patrón. Si expReg especifica una expresión no válida, se lanza una PatternSyntaxException. Una vez que haya creado un objeto de Pattern, lo usará para crear uno de Matcher. Matcher no tiene constructores. En cambio, se crea un objeto de Matcher al llamar al método de fábrica matcher( ) definido por Pattern. Aquí se muestra: Matcher matcher(CharSequence cad) Aquí, cad es la secuencia de caracteres contra la que se comparará el patrón. CharSequence es una interfaz que define un conjunto de caracteres de sólo lectura. Está implementado por la clase String, entre otras. Por tanto, puede pasar una cadena a matcher( ). Para determinar si una subsecuencia de la secuencia de entrada coincide con el patrón, se llama a find( ) en Matcher. Tiene dos versiones; la que usamos aquí es: boolean find( ) Devuelve verdadero si hay una secuencia coincidente y falso, de otra manera. A este método se le puede llamar varias veces, lo que le permite encontrar todas las secuencias coincidentes. Cada llamada a find( ) empieza donde se deja la anterior. Para obtener la cadena que contiene la coincidencia actual, se llama a group( ). La forma usada aquí es: String group( ) Se devuelve la cadena coincidente. Si no existe una coincidencia, entonces se lanza una IllegalStateException. Ejemplo Con el siguiente programa se muestra un ejemplo que extrae direcciones de correo electrónico de la forma nombre@XYZ de una cadena que contiene información para ponerse en contacto con los empleados de una empresa imaginaria llamada XYZ. // Extrae una subcadena al buscar coincidencias de una expresión regular. import java.util.regex.*; class UsaExpReg { public static void main(String args[ ]) { www.fullengineeringbook.net 34 Java: Soluciones de programación // Crea una instancia de Pattern cuya expresión regular // coincide con direcciones de correo electrónico de // cualquier empleado de XYZ.com. Pattern pat = Pattern.compile("\\b\\w+@XYZ\\.com\\b"); // Crea un Matcher para el patrón. Matcher mat = pat.matcher("Informaci\u00a2n de contacto de la empresa\n" + "Juan 555–1111 juan@XYZ.com\n" + "Martha 555–2222 Martha@XYZ.com\n" + "Daniel 555–3333 Daniel@XYZ.com"); // Encuentra y despliega todas las direcciones de correo electrónico. while(mat.find( )) System.out.println("Coincidencia: " + mat.group( )); } } A continuación se muestra la salida: Coincidencia: juan@XYZ.com Coincidencia: Martha@XYZ.com Coincidencia: Daniel@XYZ.com Opciones Puede habilitar coincidencias insensibles a diferencias entre mayúsculas y minúsculas empleando esta forma de compile( ). static Pattern compile(String expReg, int opciones) Aquí, expReg es la expresión regular que describe el patrón y opciones contiene uno o más de los siguientes valores (definidos por Pattern): CASE_INSENSITIVE CANON_EQ COMMENTS DOTALL LITERAL MULTILINE UNICODE_CASE UNIX_LINES Excepto por CANON_EQ, estas opciones tienen el mismo efecto que las marcas correspondientes de expresiones regulares, como (?i), descritas antes en este capítulo. (No hay una marca correspondiente de expresión regular para CANON_EQ.) Para coincidencias insensibles a diferencias entre mayúsculas y minúsculas, pase CASE_INSENSITIVE a opciones. Puede obtener el índice de la coincidencia correspondiente dentro de la cadena de entrada al llamar a start( ). El índice uno pasado al final de la coincidencia actual se obtiene al llamar a end( ). Aquí se muestran estos métodos: int start( ) int end( ) Ambos lanzan una IllegalStateException si no se ha tenido ninguna coincidencia. Esta información es útil si quiere eliminar la coincidencia de la cadena, por ejemplo. Dividir en fichas una cadena es una tarea de programación que casi cualquier programador enfrentará en un momento u otro. Dividir en fichas es el proceso de reducir una cadena a sus partes individuales, que se denominan fichas. Por tanto una ficha representa el elemento indivisible más pequeño que puede extraerse de una cadena y que tiene algún significado. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 35 Divida en fichas una cadena empleando la API de expresiones regulares Componentes clave Clases Métodos java.util.regex.Pattern Pattern.compile(String expReg) Matcher matcher(CharSequence cad) java.util.regex.Matcher boolean find( ) String group( ) Matcher usePattern(Pattern, patron) Por supuesto, lo que constituye una ficha depende del tipo de entrada que se está procesando, y del propósito. Por ejemplo, cuando se divide en fichas una cadena que contiene texto, las partes individuales son palabras, signos de puntuación y números. Cuando se dividen los elementos de un programa, las partes incluyen palabras clave, identificadores, operadores, separadores, etc. Cuando se divide un flujo de datos que contiene información de bolsa de valores, las fichas podrían ser el nombre de la empresa, su precio actual y se relación P/E. El punto clave es que habrá de cambiar lo que constituye una ficha, dependiendo de la circunstancia. Es posible dividir una cadena en fichas con dos métodos básicos. El primero se basa en definir los delimitadores que separan una ficha de la otra. Este método suele ser útil cuando se divide en fichas un flujo de datos que usa un formato fijo. Por ejemplo, los datos accionarios en tiempo real podrían estar disponibles en la siguiente forma: nombre, precio, relación P/E | nombre, precio, relación P/E | ... Aquí, los datos están en un formato fijo en que cada fragmento de información de la compañía está separado del siguiente por una barra vertical, y los valores asociados con cada empresa están separados de los demás por comas. En este caso, pude usar el método aplit( ) definido por String para reducir esa cadena en sus fichas individuales al especificar ",|" como el conjunto de delimitadores. Sin embargo, no siempre es apropiado o conveniente un método basado en un delimitador. En algunos casos lo que constituye un delimitador variará de un caso a otro dentro de la misma cadena. Los programas de cómputo son un ejemplo importante de esto. Por ejemplo, dado este fragmento hecho = long <= (12/puerto23); ¿Cuál conjunto de delimitadores dividirá esta cadena en sus fichas individuales? Evidentemente, no puede usar espacios en blanco, porque 12 no está separado de /, aunque ambos constituyen fichas individuales. Además, el identificador puerto23 contiene letras y dígitos, de modo que no es válido especificar dígitos como delimitadores. Más aún, los operadores =, <= y / deben devolverse como fichas, lo que significa que no pueden usarse como delimitadores. En general, lo que separa a una ficha de otra se basa en la sintaxis y la semántica del programa, no en un formato fijo. Cuando se enfrenta este tipo de situación de división en fichas, debe usarse el segundo método. En lugar de dividir en fichas con base en delimitadores, el segundo método extrae cada ficha con base en el patrón con el que coincide. Por ejemplo, cuando se divide un programa, un identificador típico buscará coincidencias de un patrón que empieza con una letra o un carácter de www.fullengineeringbook.net 36 Java: Soluciones de programación subrayado y es seguido por otras letras, dígitos o líneas de subrayado. Un comentario coincidirá con un patrón que inicia con // y termina con el final de línea, o que empieza con /* y termina con */. Un operador coincidirá con un patrón de operador, que puede definirse para que incluya operadores de un solo carácter (como +) y de varios caracteres (como +=). La ventaja de esta técnica es que no es necesario que las fichas se presenten en algún orden predefinido ni estén separadas por un conjunto fijo de delimitadores. En cambio, las fichas se identifican con el patrón con el que coinciden. Este es el método que se usará en esta solución. Como verá, es flexible y muy adaptable. La división en fichas es una tarea tan importante que Java le proporciona amplio soporte integrado. Por ejemplo, proporciona tres clases especialmente diseñadas para este fin: StreamTokenizer, Scanner y la obsoleta StringTokenizer. Más aún (como ya se mencionó), la clase String contiene el método split( ), que también puede usarse para división en fichas en ciertas situaciones simples. Aunque estas clases son útiles en sí mismas, son más útiles para la división en fichas de una cadena que está definida a partir de delimitadores. Para dividir en fichas con base en patrones, por lo general encontrará que la API de expresiones regulares de Java es una mejor opción. Éste es el método empleado por la solución de esta sección. Al usar la API de expresiones regulares, obtendrá control directo y detallado de los procesos de división en fichas. Además, la implementación de un divisor mediante el empleo de las clases Pattern y Matcher proporciona una solución elegante que es clara y fácil de comprender. Paso a paso Para dividir en fichas una cadena basada en patrones mediante el uso de la API de expresiones regulares, se requieren los siguientes pasos: 1. Cree un conjunto regular de expresiones que defina los patrones que utilizará para la búsqueda. Cada tipo de ficha que quiera obtener debe representarse con un patrón. Para permitir que el motor de expresiones regulares recorra progresivamente la cadena, empiece cada patrón con el especificador de límite \G. Esto requiere que la siguiente coincidencia empiece en el punto en que terminó la anterior. 2. Compile los patrones en los objetos de Pattern empleando Pattern.compile( ). 3. Cree un Matcher que contenga la cadena que habrá de dividirse en fichas. 4. Obtenga la siguiente ficha al adaptar el siguiente algoritmo: mientras(patrones fijos a probarse) { Especifique el patrón que se buscará al llamar a usePattern( ) en el Matcher. Busque el patrón al llamar a find( ) en el Matcher Si se encuentra una coincidencia, se obtiene una ficha De otra manera, se prueba el siguiente patrón. } A menudo, las patrones deben probarse en un orden específico. Por tanto, cuando implemente este algoritmo, debe probar cada patrón en el orden correcto. 5. Una vez que se haya encontrado una ficha, obténgala al llamar a group( ) en el Matcher. 6. Repita los pasos 4 y 5 hasta que se alcance el final de la cadena. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 37 Análisis El procedimiento básico con el fin de crear un Pattern y un Matcher y para usar find( ) y group( ) a fin de buscar una cadena se describió en la solución anterior (Compare y extraiga subcadenas empleando la API de expresiones regulares), y ese análisis no se repetirá aquí. En cambio, nos concentraremos en los dos elementos clave que permiten que una cadena se divida en fichas. El primero es la definición del conjunto de patrones que describe a las fichas. El segundo es usePattern( ), que cambia el patrón que se busca. La clave para usar la API de expresiones regulares con el fin de dividir en fichas una cadena es el comparador de límite \G. Al empezar el patrón con éste, cada coincidencia deben empezar precisamente donde se dejó la anterior. (En la primera coincidencia, \G coincidirá con el principio de la cadena). Empleando este mecanismo, puede llamarse varias veces al método find( ) en un Matcher. Cada coincidencia posterior empezará donde terminó la anterior. Esto permite que el motor de expresiones regulares recorra la cadena, sin que omita alguna ficha en el proceso. He aquí algunos ejemplos de expresiones regulares que coinciden con palabras, signos de puntuación, espacios en blanco y números. Coincidencias Patrón Palabras \G\p{Alpha}+ Signos de puntuación \G\p{Punct} Espacio en blanco \G\s+ Números \G\d+\.?\d* En cada caso, la ficha que habrá de buscarse debe empezar inmediatamente después de la coincidencia anterior. Por ejemplo, dada esta cadena Salta una, no 2, veces Primero, "Salta" coincide con el patrón de palabra. El siguiente patrón que se encontrará es el espacio en blanco, porque es el único patrón que puede seguir a la coincidencia anterior. A continuación, "una", es encontrada por el patrón de palabra, una vez más porque es el único patrón que puede seguir a la coincidencia anterior. El patrón de signo de puntuación coincidirá después con la coma, el patrón de palabra coincidirá con "no", el patrón de número con "2", etc. Un punto clave es que debido a que cada patrón debe empezar al final del anterior, no se omitirá ninguna ficha cuando el motor de expresiones regulares trate de encontrar una coincidencia. Por ejemplo, después de que se ha encontrado "Salta", fallará un intento de encontrar un número porque "2" no se encuentra inmediatamente después de esta coincidencia. Con el fin de habilitar un Matcher para que use diferentes patrones, usará el método usePattern( ). Este método cambia el patrón sin restablecer todo el Matcher. Aquí se muestra: Matcher usePattern(Pattern expReg) El patrón que habrá de usarse es especificado por expReg. Cuando se trata de obtener la siguiente ficha, el orden en que se prueban los patrones es importante. Por ejemplo, considere estos dos patrones: \G[<>=!] \G((<=)|(>=)|(==)(!=)) www.fullengineeringbook.net 38 Java: Soluciones de programación El primer patrón coincide con los operadores de un solo carácter <, >, = y !; el segundo con <=, >=, == y !=, que son los operadores de dos caracteres. Para dividir correctamente en fichas una cadena que contiene ambos tipos de operadores, debe primero buscar los operadores de dos caracteres y luego los de uno. Si invierte este orden, cuando se encuentre <= se recuperarán < y = como dos fichas individuales, en lugar de una sola. Ejemplo En el siguiente programa se pone en práctica el análisis anterior. Divide en fichas una cadena en sus componentes textuales: palabras, números o signos de puntuación. Aunque se trata de un ejemplo simple, ilustra las técnicas básicas empleadas para dividir en fichas cualquier tipo de entrada. // Un divisor simple para texto. import java.util.regex.*; class DivisorSimpleDeTexto { // Crea patrones que coinciden con texto simple. static Pattern fin = Pattern.compile("\\G\\z"); static Pattern palabra = Pattern.compile("\\G\\w+"); static Pattern punt = Pattern.compile("\\G\\p{Punct}"); static Pattern espacio = Pattern.compile("\\G\\s"); static Pattern numero = Pattern.compile("\\G\\d+\\.?\\d*"); // Este método devuelve la siguiente ficha recuperada del // Matcher pasado a mat. static String obtenerFichaTexto(Matcher mat) { // Primero, omite los espacios iniciales. mat.usePattern(espacio); mat.find( ); // // // // // // Luego, obtiene la siguiente ficha de la cadena al tratar de encontrar cada patrón. Se devuelve la ficha encontrada por el primer patrón coincidente. Es importante el orden en que se prueban los patrones. Si se revisa una palabra antes que un número, podrían cambiar los resultados. // Primero, se busca un número. mat.usePattern(numero); if(mat.find( )) return mat.group( ); // Si no hay un número, se busca una palabra. mat.usePattern(palabra); if(mat.find( )) return mat.group( ); // Si no hay una palabra, se busca un signo de puntuación. mat.usePattern(punt); if(mat.find( )) return mat.group( ); // Por último, se busca el final de la cadena. mat.usePattern(fin); if(mat.find( )) return ""; www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares // No se reconoce una ficha. return null; // ficha no válida } // Demuestra la división en fichas. public static void main(String args[ ]) { String ficha; // Crea un Matcher. Matcher mat = fin.matcher("La primera herramienta es un martillo," + " con un costo de $132.99."); // Despliega las fichas de la cadena. do { ficha = obtenerFichaTexto(mat); if(ficha == null) { System.out.println("Ficha no v\u00a0lida"); break; } if(ficha.length( ) != 0) System.out.println("Ficha: " + ficha); else System.out.println("Final de la cadena"); } while(ficha.length( ) != 0); } } Este programa produce la siguiente salida: Ficha: La Ficha: primera Ficha: herramienta Ficha: es Ficha: un Ficha: martillo Ficha: , Ficha: con Ficha: un Ficha: costo Ficha: de Ficha: $ Ficha: 132.99 Ficha: . Final de la cadena DivisorSimpleDeTexto empieza por definir varios patrones que pueden usarse para dividir en fichas texto en español (sin acentos), números o signos de puntuación. Tome nota de que cada patrón empieza con \G. Esto impone que cada patrón empiece donde se quedó la coincidencia anterior. Además, tome nota de que el patrón de palabra se especifica empleando \w. Por tanto, puede coincidir con letras y dígitos. En un momento verá por qué esto es importante. www.fullengineeringbook.net 39 40 Java: Soluciones de programación A continuación está el método obtenerFichaTexto( ). Aquí es donde tiene lugar la división en fichas. Al parámetro mat de obtenerFichaTexto( ) se le pasa un Matcher que contiene la cadena que habrá de dividirse. Empleando este Matcher, se trata de encontrar la siguiente ficha de la cadena. Se hace esto al omitir primero los espacios en blanco iniciales. Luego, se prueba cada uno de los siguientes patrones en secuencia. Esto se hace al establecer primero el patrón empleado por mat al llamar a usePattern( ). Luego se llama a find( ) en mat. Recuerde que find( ) devuelve verdadero cuando encuentra una coincidencia, y falso si no la halla. Por tanto, si el primer patrón falla, se prueba el siguiente, etc. Se devuelve la ficha encontrada por el primer patrón. Si se encuentra el final de la cadena, entonces se devuelve una cadena de longitud cero. Si no se encuentran coincidencias, entonces se encontró alguna secuencia de caracteres que no es parte normal del texto en español, sin acentos, y se devuelve null. Resulta importante comprender que el orden en que se prueban los patrones puede afectar la manera en que se encuentran las fichas. En este ejemplo, el divisor prueba primero si hay coincidencias con el patrón de número antes de que prueba con palabras. Esto es necesario porque, para este ejemplo, se usó el patrón \w para definir el patrón palabra. Como sabe, la clase \w busca letras y dígitos. Por tanto, en el ejemplo, si busca palabras antes que números, entonces el número 132.99 se dividirá incorrectamente en tres fichas: 132 (palabra), un punto (puntuación) y 99 (palabra). Por eso es necesario que la primera búsqueda sea de números. Por supuesto, en este ejemplo sería posible definir el patrón palabra para que excluya dígitos y evite esto, pero estás soluciones fáciles no siempre están disponibles. Por lo general, necesitará seleccionar cuidadosamente el orden en que se prueban los patrones. Otro tema importante es que, para mayor claridad, obtenerFichaTexto( ) llama a usePattern( ) y find( ) en dos instrucciones separadas. Por ejemplo: mat.usePattern(numero); if(mat.find( )) return mat.group( ); Sin embargo, esta secuencia puede escribirse así, de manera más compacta: if (mat.usePattern(numero).find( )) return mat.group( ); Esto funciona porque usePattern( ) devuelve el Matcher con el que opera. Esta forma más compacta es común en código profesional. Ejemplo adicional Aunque en el ejemplo anterior se muestra el mecanismo básico para la división en fichas de una cadena, no ilustra la verdadera capacidad y flexibilidad del método. Por esto, se incluye un ejemplo sustancialmente más complejo que crea un divisor de propósito general. Este divisor puede adaptarse para dividir en fichas casi cualquier tipo de cadena de entrada basada en casi cualquier tipo de ficha. El divisor de propósito general está contenido por completo dentro de una clase llamada DivisorFichas, que define varios patrones integrados. Cuatro son patrones de propósito general, que dividen una cadena en palabras, números, signos de puntuación y espacios en blanco, que son similares a los usados en el ejemplo anterior. El segundo conjunto define fichas que representan un subconjunto del lenguaje Java. (Puede agregar otros patrones, si lo desea). Se incluye una enumeración que representa el tipo de cada ficha. El tipo describe el patrón básico con el que coincide la ficha, como palabra, puntuación, operador, etc. Tanto la ficha como su tipo se devuelven cada vez que se obtiene una nueva ficha. La vinculación de una ficha con su tipo es común en todas las situaciones de división en fichas, excepto las más simples. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 41 El constructor predeterminado crea un objeto que puede dividir en fichas texto normal en español, sin acentos ni caracteres de escape. El constructor parametrizado le permite especificar una matriz de tipos de ficha que quiere que use el divisor, en el orden en que habrán de aplicarse. Por tanto, este constructor le permite adecuar de manera precisa un divisor para manejar casi cualquier tipo de cadena de entrada. Con el siguiente programa se demuestra la división en fichas de texto normal y fragmentos de programa. // // // // Una clase divisora de propósito general. Usa el comparador de límite \G para permitir que la clase recorra la cadena de entrada de principio a fin. Puede adaptarse para dividir diferentes tipos de secuencias de entrada. import java.util.regex.*; class DivisorFichas { // Éste es el Matcher usado por el divisor. private Matcher mat; // Aquí se definen varios patrones de fichas. Puede // agregar otros propios, si lo desea. // EOS es the patrón que describe el final // de la cadena de entrada. private static Pattern EOS = Pattern.compile("\\G\\z"); // El patrón desconocido busca un carácter para // permitir que la división en fichas avance. Por supuesto, // los usuarios de DivisorFichas tienen la libertad de detenerla // si se encuentra una ficha desconocida. private static Pattern desconocido = Pattern.compile("\\G."); // Algunos patrones generales de fichas. private static Pattern palabra = Pattern.compile("\\G\\p{Alpha}+"); private static Pattern punt = Pattern.compile("\\G\\p{Punct}"); private static Pattern espacio = Pattern.compile("\\G\\s+"); private static Pattern numero = Pattern.compile("\\G\\d+\\.?\\d*"); // Algunos patrones relacionados con programas parecidos a Java. // Esto coincide con una palabra clave o un identificador, // que no debe empezar con un dígito. private static Pattern pcOIden = Pattern.compile("\\G[\\w&&\\D]\\w*"); // Especifica los varios separadores. private static Pattern separador = Pattern.compile("\\G[( ){}\\[\\];,.]"); // Esto busca los diversos operadores. private static Pattern opUnico = Pattern.compile("\\G[=><!~?:+\\–*/&|\\^%@]"); private static Pattern opDoble = Pattern.compile("\\G((<=)|(>=)|(==)|(!=)|(\\|\\|)|" + "(\\&\\&)|(<<)|(>>)|(––)|(\\+\\+))"); www.fullengineeringbook.net 42 Java: Soluciones de programación private static Pattern opAsign = Pattern.compile("\\G((\\+=)|(–=)|(/=)|(\\*=)|(<<=)|" + "(>>=)|(\\|=)|(&=)|(\\^=)|(>>>=))"); private static Pattern opTriple = Pattern.compile("\\G>>>"); // Esto busca una literal de cadena. private static Pattern literalCadena = Pattern.compile("\\G\".*?\""); // Esto busca un comentario. private static Pattern comentario = Pattern.compile("\\G((//.*(?m)$)|(/\\*(?s).*?\\*/))"); // Esta enumeración define los tipos de ficha // y asocia un patrón con cada tipo. // Si define otro tipo de ficha, entonces // la agrega a la enumeración. enum TipoFicha { // Inicializa los valores de enumeración con sus // patrones correspondientes. PALABRA(palabra), PUNT(punt), ESPACIO(espacio), NUMERO(numero), PC_O_IDENT(pcOIden), SEPARADOR(separador), OP_UNICO(opUnico), OP_DOBLE(opDoble), OP_TRIPLE(opTriple), OP_ASIGN(opAsign), LITERAL_CADENA(literalCadena), COMENTARIO(comentario), FINAL(EOS), DESCONOCIDO(desconocido); // Esto conserva el patrón asociado con cada tipo de ficha. Pattern pat; TipoFicha(Pattern p) { pat = p; } } // Esta matriz contiene la lista de fichas que este // divisor buscará, en el orden en que se realizará // la búsqueda. Por tanto, el orden de los elementos // en esta matriz es importante porque determina // el orden en que se probarán las coincidencias. TipoFicha patrones[ ]; // Cada vez que se obtiene una nueva ficha, ésta // y su tipo se regresan en un objeto de tipo Ficha. class Ficha { String ficha; // la cadena contiene la ficha TipoFicha tipo; // el tipo de la ficha } // Esto contiene la ficha actual. Ficha fichaActual; www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares // Crea un DivisorFichas personalizado. Divide el texto en fichas. DivisorFichas(String cad) { mat = desconocido.matcher(cad); // Esto describe las fichas para la // división simple de texto en fichas. TipoFicha fichasTexto[ ] = { TipoFicha.NUMERO, TipoFicha.PALABRA, TipoFicha.PUNT, TipoFicha.FINAL, TipoFicha.DESCONOCIDO }; patrones = fichasTexto; fichaActual = new Ficha( ); } // Crea un DivisorFichas personalizado que coincide // con la lista de fichas dados los tipos pasados a dfp. DivisorFichas(String cad, TipoFicha dfp[ ]) { mat = desconocido.matcher(cad); // Siempre agregue FINAL y DESCONOCIDO al final // de la matriz de patrones. TipoFicha tmp[ ] = new TipoFicha[dfp.length+2]; System.arraycopy(dfp, 0, tmp, 0, dfp.length); tmp[dfp.length] = TipoFicha.FINAL; tmp[dfp.length+1] = TipoFicha.DESCONOCIDO; patrones = tmp; fichaActual = new Ficha( ); } // Este método devuelve la siguiente ficha de la // cadena de entrada. Lo que constituye una ficha // se determina con el contenido de la matriz // de patrones. Por tanto, al cambiar la matriz, // se cambia el tipo de fichas que se obtiene. Ficha obtenerFicha( ) { // En primer lugar, omite cualquier espacio en blanco inicial. mat.usePattern(espacio).find( ); for(int i=0; i<patrones.length; i++) { // Selecciona el siguiente patrón de ficha que se usará. mat.usePattern(patrones[i].pat); // Ahora, pruebe a encontrar una coincidencia. if(mat.find( )) { fichaActual.tipo = patrones[i]; fichaActual.ficha = mat.group( ); break; } } www.fullengineeringbook.net 43 44 Java: Soluciones de programación return fichaActual; // devuelve la ficha y su tipo } } // Demuestra DivisorFichas. class DemoDivisorFichas { public static void main(String args[ ]) { DivisorFichas.Ficha t; // Demuestra el texto que se dividirá en fichas. DivisorFichas divf = new DivisorFichas("Este es un texto de ejemplo. Hoy es lunes, " + " 28 de febrero de 2008"); // Lee y despliega las fichas de texto hasta que se lee // la ficha FINAL. System.out.println("Dividiendo texto en fichas."); do { // Obtiene la siguiente ficha. t = divf.obtenerFicha( ); // Despliega la ficha y su tipo. System.out.println("Ficha: " + t.ficha + "\tTipo: " + t.tipo); } while(t.tipo != DivisorFichas.TipoFicha.FINAL); // Ahora crea un divisor para un subconjunto de Java. // Recuerde que el orden importa. Por ejemplo, un // intento para buscar un doble operador, como <= // debe presentarse antes de que se haga un intento por // buscar un solo operador, como <. DivisorFichas.TipoFicha fichasProg[ ] = { DivisorFichas.TipoFicha.NUMERO, DivisorFichas.TipoFicha.PC_O_IDENT, DivisorFichas.TipoFicha.LITERAL_CADENA, DivisorFichas.TipoFicha.COMENTARIO, DivisorFichas.TipoFicha.OP_ASIGN, DivisorFichas.TipoFicha.OP_TRIPLE, DivisorFichas.TipoFicha.OP_DOBLE, DivisorFichas.TipoFicha.OP_UNICO, DivisorFichas.TipoFicha.SEPARADOR, }; // Demuestra la división en fichas de un programa. divf = new DivisorFichas("// comentario\n int count=10; if(a<=b) count––;"+ "a = b >>> c; a = b >> d; resultado = meth(3);" + "w = a<0 ? b*4 : c/2; done = !done;" + "for(int i=0; i<10; i++) sum += i;" + "String cad = \"una literal de cadena\"" + "class Prueba { /* ... */ }", fichasProg); www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares // Despliega cada ficha y su tipo. System.out.println("\nDividiendo fragmentos del programa."); do { t = divf.obtenerFicha( ); System.out.println("Ficha: " + t.ficha + "\tTipo: " + t.tipo); } while(t.tipo != DivisorFichas.TipoFicha.FINAL); } } A continuación se muestra una parte de la salida: Dividiendo texto en fichas. Ficha: Este Tipo: PALABRA Ficha: es Tipo: PALABRA Ficha: un Tipo: PALABRA Ficha: texto Tipo: PALABRA Ficha: de Tipo: PALABRA Ficha: ejemplo Tipo: PALABRA Ficha: . Tipo: PUNT Ficha: Hoy Tipo: PALABRA Ficha: es Tipo: PALABRA Ficha: lunes Tipo: PALABRA Ficha: , Tipo: PUNT Ficha: 28 Tipo: NUMERO Ficha: de Tipo: PALABRA Ficha: febrero Tipo: PALABRA Ficha: de Tipo: PALABRA Ficha: 2008 Tipo: NUMERO Ficha: Tipo: FINAL Dividiendo fragmentos Ficha: // comentario Ficha: int Tipo: Ficha: count Tipo: Ficha: = Tipo: Ficha: 10 Tipo: Ficha: ; Tipo: Ficha: if Tipo: Ficha: ( Tipo: Ficha: a Tipo: Ficha: <= Tipo: Ficha: b Tipo: Ficha: ) Tipo: Ficha: count Tipo: Ficha: –– Tipo: Ficha: ; Tipo: Ficha: a Tipo: Ficha: = Tipo: Ficha: b Tipo: Ficha: >>> Tipo: . . . del programa. Tipo: COMENTARIO PC_O_IDENT PC_O_IDENT OP_UNICO NUMERO SEPARADOR PC_O_IDENT SEPARADOR PC_O_IDENT OP_DOBLE PC_O_IDENT SEPARADOR PC_O_IDENT OP_DOBLE SEPARADOR PC_O_IDENT OP_UNICO PC_O_IDENT OP_TRIPLE www.fullengineeringbook.net 45 46 Java: Soluciones de programación Éste es un ejemplo muy complejo y garantiza un examen a profundidad de su operación. La clase DivisorFichas define estos elementos clave: Elemento Descripción La variable de instancia mat Contiene una referencia al Matcher que usará la instancia de DivisorFichas. Varios patrones precompilados de fichas Son los patrones que describen los diversos tipos de fichas. La enumeración TipoFicha Esta enumeración representa el tipo de cada ficha. También vincula a un tipo de ficha con su patrón. La matriz patrones Esta matriz contiene un conjunto ordenado de objetos de TipoFicha que especifican los tipos de fichas que se obtendrán. El orden de los elementos de la matriz es el orden en que DivisorFichas prueba los patrones, buscando una coincidencia. La clase Ficha Esta clase de conveniencia vincula la ficha actual con su tipo. Está anidada dentro de DivisorFichas. Por tanto, para hacer referencia a ella fuera de ésta, debe calificarla con DivisorFichas, como en DivorFichas.Fichas. La variable de instancia fichaActual Contiene una referencia a la ficha actual, que es la obtenida por la llamada más reciente a obtenerFicha( ). Es devuelta por obtenerFicha( ). Los constructores DivisorFichas Construyen un divisor de fichas, el constructor de un parámetro divide en fichas el texto normal en español, sin acentos. El constructor de dos parámetros permite que el divisor sea configurado para trabajar con otros tipos de entrada. El método obtenerFicha( ) Devuelve la siguiente ficha de la cadena. Revisemos más de cerca cada parte. En primer lugar, cada instancia de DivisorFichas tiene su propio Matcher, al que se hace referencia mediante mat. Esto significa que pueden usarse dos o más divisores de fichas dentro del mismo programa, cada uno operando de manera independiente. A continuación, observe los diversos patrones. Todas las expresiones regulares empiezan con \G, lo que significa que una secuencia coincidente debe empezar al final de la coincidencia anterior. Como ya se explicó, esto permite que el motor de expresiones regulares avance por la cadena. (Si no incluye el comparador de límite \G, entonces el motor de expresiones regulares encontrará una secuencia coincidente en cualquier lugar de la cadena, omitiendo posiblemente varias fichas en el proceso). Si agrega patrones adicionales, entonces debe estar seguro de empezar con \G. Observe los patrones que dividen en fichas un subconjunto de Java. El patrón pcOIdent coincide con palabras clave o identificadores. (No suele ser práctico distinguir entre ambos durante el proceso de división. Otras partes de un compilador o un intérprete suelen manejar esta tarea). Otros patrones coinciden con los diversos separadores u operadores. Observe que se requieren cuatro patrones diferentes para manejar los operadores, y que cada uno busca un tipo distinto de operador, incluidos los compuestos por un solo operador, por dos o por tres. Los operadores de asignación pudieran manejarse como operadores de doble carácter, pero por razones de claridad, se les dio un patrón propio. Se proporcionan también patrones que coinciden con los comentarios y las literales de cadena. www.fullengineeringbook.net Capítulo 2: Trabajo con cadenas y expresiones regulares 47 Después que se han compilado los patrones, se crea la enumeración TipoFicha. Define los tipos de ficha que corresponden a los patrones y vinculan a cada patrón con su tipo. Esta vinculación muestra la capacidad de las enumeraciones de Java, que son tipos de clase más que simples listas de enteros con nombre (como lo son en varios otros lenguajes). La matriz patrones contiene una lista de objetos TipoFicha que especifican los tipos de ficha que obtendrá el divisor. El orden de los tipos en la matriz especifica el orden en que DivisorFichas buscará una ficha. Debido a que el orden en que se realiza la búsqueda puede ser importante (por ejemplo, debe buscar <= antes de <), debe cuidar el orden apropiado de la matriz. En todos los casos, las últimas dos entradas de la matriz deben ser FINAL y DESCONOCIDO. A continuación, se define la clase Ficha. Esta clase anidada no es técnicamente necesaria, pero permite que una ficha y su tipo se encapsulen dentro de un solo objeto. La variable fichaActual, que es una instancia de Ficha, contiene la ficha obtenida más recientemente. obtenerFicha( ) devuelve una referencia a ella. Desde el punto de vista técnico, fichaActual no es necesaria porque bastaría con construir un nuevo objeto de Ficha para cada ficha obtenida, y hacer que obtenerFicha( ) devuelva el nuevo objeto. Sin embargo, cuando se divida en fichas una cadena muy larga, se crearía un gran número de objetos y luego se descartarían. Esto daría como resultado ciclos adicionales de recolección de basura. Al emplear sólo un objeto de Ficha, se elimina esta posible ineficiencia. El constructor DivisorFichas de un parámetro crea un divisor que maneja el texto regular en español, sin acentos, al reducirlo a palabras, signos de puntuación y números. La cadena que habrá de dividirse en fichas se pasa al constructor. Asigna a patrones una matriz que maneja estos elementos de texto simple. El constructor de dos parámetros le permite especificar una matriz de TipoFicha que se asignará a patrones. Esto le permite configurar un divisor que manejará diferentes tipos de entrada. En DemoDivisorFichas, la matriz fichasProg se construye de tal manera que puede dividir en fichas un subconjunto de lenguaje Java. Otros tipos de división pueden crearse al especificar una matriz tipoFicha diferente. Un tema adicional: observe que este constructor siempre agrega FINAL y DESCONOCIDO a la lista de tipos de ficha. Esto es necesario para asegurar que obtenerFicha( ) encuentra el final de la cadena y que devuelve DESCONOCIDO cuando no puede encontrar ninguno de los patrones. El método obtenerFicha( ) obtiene la siguiente ficha de la cadena. Siempre lo hace correctamente porque la ficha será FINAL cuando se ha alcanzado el final de la cadena y DESCONOCIDO si no se encuentran fichas coincidentes. Tome en cuenta que obtenerFicha( ) omite cualquier espacio inicial. Por lo general, las fichas no incluyen espacios en blanco, de modo que obtenerFicha( ) simplemente los descarta. Para usar el divisor de fichas, cree primero un DivisorFichas que busque las fichas que desee. A continuación, configure un bucle que llame a obtenerFicha( ) en el divisor hasta que se llegue al final de la cadena. Cuando esto ocurre, el tipo de la ficha devuelta será EOS. En el programa, este procedimiento se demuestra con la clase DemoDivisorFichas. Una ficha desconocida puede ignorarse (como en el ejemplo) o tratarse como una condición de error. Opciones Como se mencionó, cuando se divide en fichas con base en delimitadores, puede usar StringTokenizer (ahora obsoleto), StreamTokenizer, Scanner o el método split( ). Si su entrada puede dividirse en fichas con base en delimitadores, entonces es fácil implementar estas otras soluciones efectivas. www.fullengineeringbook.net 48 Java: Soluciones de programación Como se ha mostrado en los ejemplos anteriores, la división en fichas basada en patrones puede implementarse de manera eficiente empleando las clases Pattern y Matcher definidas por la API de expresiones regulares. Éste es el método que prefiero. Sin embargo, son posibles otras soluciones. Una opción está basada en la clase Scanner, que puede usarse para división en fichas basada en patrones al emplear su método findWithinHorizon( ) para obtener una ficha. Como se mencionó antes, por lo general Scanner se basa en delimitadores. Sin embargo, el método findWithinHorizon( ) ignora éstos y trata de encontrar una coincidencia con la expresión regular que se pasa como argumento. La coincidencia se prueba dentro de una parte especificada de la cadena de entrada, que puede ser toda la cadena, si es necesario. Aunque este método funciona, Matcher ofrece un método más simple y directo. (Por supuesto, si quiere alternar entre división basada en delimitadores y en patrones, entonces el uso de Scanner sería la solución perfecta). La división en fichas basada en patrones también puede implementarse a mano, haciendo que la cadena se revise carácter por carácter, y que las fichas se construyan carácter por carácter. Éste era el método que solía usarse antes de que se tuviera la API de expresiones regulares. Para algunos casos, aún podría ser el método más eficiente. Sin embargo, las expresiones regulares ofrecen código sustancialmente más compacto y fácil de mantener. Como opción predeterminada, se buscará una coincidencia en toda la cadena pasada a Matcher. Sin embargo, puede restringir la búsqueda a una región más pequeña al llamar a region( ), como se muestra aquí: Matcher region(int inicio, int final) El índice en que se empezará a buscar se pasa mediante inicio. La búsqueda se detiene en final–1. Puede obtener los límites de búsqueda actuales al llamar a regionStart( ) y regionEnd( ). www.fullengineeringbook.net 3 CAPÍTULO Manejo de archivos E l manejo de archivos es una parte integral de casi todos los proyectos de programación. Los archivos proporcionan los medios para que un programa almacene datos, acceda a datos almacenados o comparta datos. Como resultado, hay muy pocas aplicaciones que no interactúan con un archivo en una forma u otra. Aunque ningún aspecto del manejo de archivo es particularmente difícil, interviene una gran cantidad de clases, interfaces y métodos. La marca de un profesional es la capacidad de aplicarlos efectivamente a sus proyectos. Es importante comprender que la E/S de archivos es un conjunto del sistema general de E/S de Java. Más aún, el sistema de E/S de Java es muy grande. No resulta sorprendente, puesto que da soporte a dos jerarquías distintas de clases de E/S: una para bytes y otra para caracteres. Contiene clases que permiten que una matriz de bytes, una matriz de caracteres o una cadena se use como fuente o destino de operaciones de E/S. También proporciona la capacidad de establecer u obtener varios atributos relacionados con un archivo, como su estado de lectura/escritura, si el archivo es un directorio o si está oculto. Incluso obtiene una lista de archivos dentro de un directorio. A pesar de su tamaño, el sistema de E/S de Java es sorprendentemente fácil de usar. Una razón es su diseño bien pensado. Al estructurar el sistema de E/S alrededor de un conjunto cuidadosamente creado de clases, esta API de gran tamaño se vuelve manejable. Una vez que se comprende la manera de usar las clases centrales, es fácil aprender sus capacidades más avanzadas. La consistencia del sistema de E/S hace que el código resulte fácil de mantener o adaptar, y su rica funcionalidad proporciona soluciones a casi todas las tareas de manejo. El núcleo del sistema de E/S de Java está empaquetado en java.io. Se ha incluido con Java desde la versión 1.0 y contiene las clases e interfaces que usará con más frecuencia cuando realice operaciones de E/S, incluidas las que actúan sobre archivos. Para ponerlo de manera simple, cuando necesite leer o escribir archivos, java.io es el paquete al que normalmente acudirá. Como resultado, todas las soluciones de este capítulo usan sus opciones, de una manera u otra. Otro paquete que incluye clases de manejo de archivos es java.util.zip. Las clases de java.util.zip pueden crear un archivo comprimido, o descomprimir un archivo. Estas clases se construyen sobre la funcionalidad proporcionada por las clases de E/S definidas en java.io. Por tanto, están integradas en la estrategia general de E/S. Tres soluciones demuestran el uso de la compresión de datos cuando se manejan archivos. En este capítulo se proporcionan varias soluciones que demuestran el manejo de archivos. Se empieza por describir varias operaciones fundamentales, como lectura o escritura de bytes o caracteres. Luego se demuestran varias técnicas que le ayudan a utilizar y administrar archivos. 49 www.fullengineeringbook.net 50 Java: Soluciones de programación He aquí las soluciones contenidas en este capítulo: • Lea bytes de un archivo • Escriba bytes en un archivo • Use el búfer para la E/S de un archivo basada en bytes • Lea caracteres de un archivo • Escriba caracteres en un archivo • Use el búfer para la E/S de un archivo basada en caracteres • Lea y escriba archivos de acceso aleatorio • Obtenga atributos de archivos • Establezca atributos de archivos • Elabore una lista de un directorio • Comprima y descomprima datos • Cree un archivo ZIP • Descomprima un archivo ZIP • Serialice objetos NOTA Apartir de la versión 1.4, Java empezó a proporciona un método adicional para E/S llamado NIO (New I/O, nueva E/S). Crea un método basado en canal para E/S y está empaquetado en java.nio. El sistema NIO no tiene el objetivo de reemplazar a las clases de E/S basadas en flujo que se encuentran en java.io, sino que las complementa. Debido a que el enfoque de este capítulo está puesto en la E/S basada en flujo, no se incluyen soluciones basadas en NIO. El lector interesado encontrará un análisis de NIO (y de E/S en general) en mi libro Java: The Complete Reference. Una revisión general del manejo de archivos En Java, el manejo de archivos es simplemente un aspecto especial de un concepto más amplio debido a que la E/S de archivo está fuertemente integrada con el sistema general de E/S de Java. En general, si comprende una parte del sistema E/S, es fácil aplicar ese conocimiento a otras situaciones. Hay dos aspectos del sistema de E/S que hace posible esta característica. El primero es que el sistema de E/S de Java está construido a partir de un conjunto coherente de jerarquías de clase, por encima del cual se encuentran las clases abstractas que definen gran parte de la funcionalidad básica compartida por todas las subclases concretas específicas. El segundo aspecto es el flujo, el cual une al sistema de archivos porque todas las operaciones de E/S ocurren a través de uno. Debido a la importancia del flujo, empezaremos allí la revisión general de las capacidades de manejo de Java. Flujos Un flujo es una abstracción que produce o consume información. Un flujo está vinculado a un dispositivo físico mediante el sistema de E/S. todos los flujos se comportan de la misma manera, aunque difieran los dispositivos reales a los que están vinculados. Por tanto las mismas clases y los mismos métodos de E/S pueden aplicarse a diferentes tipos de dispositivos. Por ejemplo, los mismos métodos que usa para escribir en la consola pueden usarse para escribir en un archivo www.fullengineeringbook.net Capítulo 3: Manejo de archivos 51 de disco o una conexión de red. Los flujos centrales de Java están implementados dentro de las jerarquías de clase definidas en el paquete java.io. Son los mismos flujos que usaría normalmente cuando maneja archivos. Sin embargo, algunos otros paquetes también definen flujos. Por ejemplo, java.util.zip proporciona flujos que crean y operan en datos comprimidos. Las versiones modernas de Java definen dos tipos de flujos: de bytes y de caracteres. (La versión 1.0 original de Java sólo definía flujos de bytes, pero se agregaron rápidamente los de caracteres). Los flujos de bytes proporcionan un medio conveniente para manejar entrada y salida de bytes. Se usan, por ejemplo, cuando se leen o escriben datos binarios. Resultan especialmente útiles cuando se trabaja con archivos. Los flujos de caracteres están diseñados para manejar la entrada y salida de caracteres, lo que mejora la internacionalización. El hecho de que Java defina dos tipos diferentes de flujos hace muy grande el sistema de E/S porque se necesitan dos jerarquías de clases separadas (una para bytes y otra para caracteres). La gran cantidad de clases puede hacer que los sistemas parezcan más intimidantes de lo que son en realidad. En su mayor parte, la funcionalidad de los flujos de bytes son equiparables a la de los flujo de caracteres. Otro tema interesante es que, en el nivel inferior, toda E/S aún está orientada a bytes. Los flujos basados en caracteres proporcionan medios convenientes y eficientes para manejar caracteres. Las clases de flujo de bytes Los flujos de bytes están definidos por dos jerarquías de clase: una para entrada y otra para salida. En la parte superior de éstas se encuentran dos clases abstractas: InputStream y OutputStream. InputStream define las características comunes a los flujos de entrada de bytes, y OutputStream describe el comportamiento de los flujos de salida de bytes. Los métodos especificados por InputStream y OutputStream se muestran en las tablas 3–1 y 3–2. A partir de InputStream y OutputStream se crean varias subclases, que ofrecen funcionalidad variable. Estas clases se muestran en la tabla 3–3. Entre las clases de flujo de bytes, dos están directamente relacionadas con los archivos: FileInputStream y FileOutputStream. Debido a que son implementaciones concretas de InputStream y OutputStream, pueden usarse en cualquier lugar en que se necesite un InputStream o un OutputStream. Por ejemplo, una instancia de FileInputStream puede envolverse en otra clase de flujo de bytes, como BufferedInputStream. Ésta es una razón por la que el método de E/S basada en flujos de Java es tan poderosa: permite la creación de una jerarquía de clase completamente integrada. Las clases de flujo de caracteres Los flujos de caracteres se definen con el uso de jerarquías de clase que son diferentes de las de flujos de bytes. Las jerarquías de flujo de caracteres tienen a la cabeza estas dos clases abstractas: Reader y Writer. Reader se usa para entrada y Writer para salida. En las tablas 3–4 y 3–5 se muestran los métodos definidos por estas clases. Las clases concretas derivadas de Reader y Writer operan en flujos de caracteres de Unicode. En general, las clases basadas en caracteres son equiparables a las clases basadas en bytes. Las clases de flujo de caracteres se muestran en la tabla 3–6. Entre las clases de flujo de caracteres, dos están directamente relacionadas con archivos: FileReader y FileWriter. Debido a que son implementaciones concretas de Reader y Writer, pueden usarse en cualquier lugar en que se necesite un lector o un escritor de archivos. Por ejemplo, una instancia de FileReader puede envolverse en un BufferedReader para incluir en búfer las operaciones de entrada. www.fullengineeringbook.net 52 Java: Soluciones de programación Método Descripción int available( ) throws IOException Devuelve el número de bytes de entrada disponibles para lectura. void close( )throws IOException Cierra el origen de la entrada. void mark(int numBytes) Coloca una marca en el punto actual en el flujo de entrada que seguirá siendo válido hasta que se lea numBytes. No todos los flujos implementan mark( ). boolean markSupported( ) Devuelve verdadero si el flujo que se invoca da soporte a mark( )/reset( ). abstract int read( ) throws IOException Devuelve una representación de entero del siguiente byte disponible de entrada. Se devuelve –1 cando se encuentra el final del archivo. int read(byte bufer[ ])throws IOException Trata de leer hasta el byte bufer.length en bufer y devuelve el número real de bytes que se leyó correctamente. Se devuelve –1 cuando se encuentra el final del archivo. int read(byte bufer[ ], int despl, int numBytes)throws IOException Trata de leer hasta los bytes numBytes en bufer, a partir de bufer[despl], devolviendo el número de bytes que se leyó correctamente. Se devuelve –1 cuando se encuentra el final del archivo. void reset( ) throws IOException Restablece el apuntador de entrada a la marca establecida antes. No todos los flujos soportan reset( ). long skip(long numBytes) throws IOException Ignora (es decir, omite) un numBytes de bytes de entrada, regresando el número de bytes que se ignoró realmente. Tabla 3-1 Los metodos definidos por inputstream Método Descripción void close( ) throws IOException Cierra el flujo de salida. void flush( ) throws IOException Finaliza el estado de salida, de modo que se limpie cualquier búfer. Es decir, regenera los búferes de salida. abstract void write(int b) throws IOException Escribe el byte de orden inferior de b en el flujo de salida. void write(byte bufer[ ]) throws IOException Escribe una matriz completa de bytes en el flujo de salida. void write(byte bufer[ ], int despl., int numBytes) throws IOException Escribe un subrango de numBytes bytes, a partir del búfer de la matriz, empezando en el bufer[despl]. Tabla 3-2 Los métodos especificados por OutputStream www.fullengineeringbook.net Capítulo 3: Manejo de archivos 53 Clase de flujo de bytes Descripción BufferedInputStream Flujo de entrada en búfer. BufferedOutputStream Flujo de salida en búfer. ByteArrayInputStream Flujo de entrada que se lee de una matriz de bytes. ByteArrayOutputStream Flujo de salida que se lee de una matriz de bytes. DataInputStream Un flujo de entrada que contiene métodos para leer los tipos estándar de datos de Java. DataOutputStream Un flujo de salida que contiene métodos para escribir los tipos estándar de datos de Java. FileInputStream Flujo de entrada que se lee de un archivo. FileOutputStream Flujo de salida que se escribe en un archivo. FilterInputStream Implementa InputStream y permite que se modifique (filtre) el contenido de otro flujo. FilterOutputStream Implementa OutputStream y permite que se modifique (filtre) el contenido de otro flujo. InputStream Clase abstracta que describe la entrada del flujo. OutputStream Clase abstracta que describe la salida del flujo. PipedInputStream Canalización de entrada. PipedOutputStream Canalización de salida. PrintStream Flujo de salida que contiene print( ) y println( ). PushbackInputStream Flujo de entrada que permite que se devuelvan bytes al flujo. RandomAccessFile Soporta E/S de archivo de acceso aleatorio. SequenceInputStream Flujo de entrada que es una combinación de dos o más flujos de entrada que se leerán de manera secuencial, uno tras otro. Tabla 3-3 Las clases de flujo de bytes Una superclase de Filereader es InputStreamReader. Traduce bytes en caracteres. Una superclase de FileWriter es OutputStreamWriter. Traduce caracteres en bytes. Estas clases son necesarias porque todos los archivos están, en esencia, orientados a bytes. La clase RandomAccessFile Las clases de flujo que acabamos de describir operan en archivos de manera estrictamente secuencial. Sin embargo, Java también le permite acceder al contenido de un archivo en orden no secuencial. Para ello, usará RandomAccessFile, que encapsula un archivo de acceso aleatorio (o directo). RandomAccessFile no se deriva de InputStream ni de OutputStream. En cambio, implementa las interfaces DataInput y DataOutput (que se describen en breve). RandomAccessFile www.fullengineeringbook.net 54 Java: Soluciones de programación Método Descripción abstract void close( ) throws IOException Cierra el origen de la entrada. void mark(int numCars) throws IOException Coloca una marca en el punto actual en el flujo de entrada que seguirá siendo válido hasta que se lea el numCars de caracteres. No todos los flujos soportan mark( ). boolean markSupported( ) Devuelve verdadero si el flujo da soporte a mark( )/reset( ). int read( ) throws IOException Devuelve una representación entera del siguiente carácter disponible desde el flujo de entrada. Se devuelve –1 cuando se alcanza el final del archivo. int read(char bufer[ ]) throws IOException Trata de leer hasta bufer.length caracteres en bufer y devuelve el número real de caracteres que se leyó correctamente. Se devuelve –1 cuando se alcanza el final del archivo. abstract int read(char bufer[ ], int despl, int numCars) throws IOException Trata de leer hasta numCars de caracteres en bufer[despl] y devuelve el número de caracteres que se leyó correctamente. Se devuelve –1 cuando se alcanza el final del archivo. boolean ready( ) throws IOException Devuelve verdadero si no está pendiente una entrada. De otra manera, devuelve falso. void reset( ) throws IOException Restablece el puntero de entrada a la marca establecida antes. No todos los flujos dan soporte a reset( ). long skip(long numCars) throws IOException Omite un numCars de caracteres en la entrada, devolviendo el número de caracteres que se omitió en realidad. Tabla 3-4 Los métodos definidos por Reader soporta acceso aleatorio porque permite cambiar la ubicación en el archivo en que ocurrirá la siguiente operación de lectura o escritura. Esto se hace al llamar al método seek( ). La clase File Además de las clases que dan soporte a E/S, Java proporciona la clase File, que encapsula información acerca de un archivo. Esta clase es extremadamente útil cuando se manipula un archivo (en lugar de su contenido) o el sistema de archivos del equipo. Por ejemplo, con File puede determinar si un archivo está oculto, establecer la fecha de un archivo o definirlo como de sólo lectura, hacer una lista del contenido de un directorio o crear un nuevo directorio, entre muchas otras cosas. Por tanto, File pone el sistema de archivos bajo control. Esto hace que File sea una de las clases más importantes en el sistema de E/S de Java. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 55 Método Descripción Writer append(char car) throws IOException Adjunta car al final del flujo de salida que se invoca. Devuelve una referencia al flujo. Writer append(CharSequence cars) throws IOException Adjunta cars al final del flujo de salida que se invoca. Devuelve una referencia al flujo. Writer append(CharSequence cars, int inicio, int final) throws IOException Adjunta un subrango de cars, especificada por inicio y final, hasta el final del flujo de salida. Devuelve una referencia al flujo. abstract void close( ) throws IOException Cierra el flujo de salida. abstract void flush( ) throws IOException Finaliza el estado de salida para que se limpien los búferes. Es decir, regenera los búferes de salida. void write(int car) throws IOException Escribe el carácter en los 16 bits de orden inferior de car en el flujo de salida. void write(char bufer[ ]) throws IOException Escribe una matriz completa de caracteres en el flujo de salida. abstract void write(char bufer[ ], int despl, int numCars) throws IOException Escribe un subrango de numCars de caracteres de la matriz bufer, empezando en bufer[despl] hasta el flujo de salida. void write(String cad) throws IOException Escribe cad en el flujo de salida. void write(String cad, int despl, int numCars) throws IOException Escribe un subrango de numCars de caracteres desde la cadena cad, empezando en el despl especificado. Tabla 3-5 Los métodos definidos por Writer Las interfaces de E/S El sistema de E/S de Java incluye las siguientes interfaces (que están empaquetadas en java.io). Closeable DataInput DataOutput Externalizable FileFilter FilenameFilter Flushable ObjectInput ObjectInputValidation ObjectOutput ObjectStreamConstants Serializable Los usados de manera directa o indirecta por las soluciones de este capítulo son DataInput, DataOutput, Closeable, Flushable, FileFilter, FilenameFilter, ObjectInput y ObjectOutput. Las interfaces DataInput y DataOutput definen varios métodos de lectura y escritura, como readInt( ) y writeDouble( ), que pueden leer y escribir tipos de datos primitivos de Java. También especifican métodos read( ) y write( ), que equivalen a los especificados por InputStream y OutputStream. Todas las operaciones están orientadas a bytes. RandomAccessFile implementa las interfaces DataInput y DataOutput. Por tanto, las operaciones de archivo de acceso aleatorio en Java están orientadas a bytes. www.fullengineeringbook.net 56 Java: Soluciones de programación Clase de flujo de caracteres Significado BufferedReader Flujo de caracteres de entrada en búfer. BufferedWriter Flujo de caracteres de salida en búfer. CharArrayReader Flujo de entrada que se lee desde una matriz de caracteres. CharArrayWriter Flujo de salida que se escribe en una matriz de caracteres. FileReader Flujo de entrada que se lee de un archivo. FileWriter Flujo de salida que se escribe en un archivo. FilterReader Lector filtrado. FilterWriter Escritor filtrado. InputStreamReader Flujo de entrada que traduce bytes en caracteres. LineNumberReader Flujo de entrada que cuenta líneas. OutputStreamWriter Flujo de salida que traduce caracteres en bytes. PipedReader Canalización de entrada. PipedWriter Canalización de salida. PushbackReader Flujo de entrada que permite que los caracteres se devuelvan al flujo de entrada. Reader Clase abstracta que describe entrada de flujo de caracteres. StringReader Flujo de entrada que lee de una cadena. StringWriter Flujo de salida que escribe en una cadena. Writer Clase abstracta que describe la salida de flujo de caracteres. Tabla 3-6 Las clases de flujo de caracteres Las interfaces Closeable y Flushable son implementadas por varias de las clases de E/S. Proporcionan una manera uniforme de especificar que un flujo puede cerrarse o limpiarse. La interfaces Closeable sólo define un método, close( ), que se muestra aquí: void close( ) throws IOException Este método cierra un flujo abierto. Una vez cerrado, no es posible volver a usar el flujo. Todas las clases de E/S que abren un flujo implementan Closeable. La interfaz Flushable también especifica sólo un método, flush( ), que se muestra aquí: void flush( ) throws IOException La llamada a flush( ) causa que se escriba físicamente cualquier salida en búfer en el dispositivo correspondiente. Esta interfaz es implementada por las clases de E/S que escriben en un flujo. FileFilter y FilenameFilter se usan para filtrar listados de directorios. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 57 Las interfaces ObjectInput y ObjectOutput se usan cuando se serializan objetos (guardándolos y restaurándolos). Los flujos de archivos comprimidos En java.util.zip, Java proporciona un conjunto muy poderoso de flujos de archivos especializados que manejan la compresión y descompresión de datos. Todas son subclases de InputStream u OutputStream, descritas antes. A continuación se muestran los flujos de archivo comprimidos. DeflaterInputStream Lee datos, comprimiéndolos en el proceso. DeflaterOutputStream Escribe datos, comprimiéndolos en el proceso. GZIPInputStream Lee un archivo GZIP. GZIPOutputStream Escribe un archivo GZIP. InflaterInputStream Lee datos, descomprimiéndolos en el proceso. InflaterOutputStream Escribe datos, descomprimiéndolos en el proceso. ZipInputStream Lee un archivo ZIP. ZipOutputStream Escribe un archivo ZIP. Empleando los flujos de archivos comprimidos, es posible comprimir datos de manera automática mientras se escriben en un archivo o descomprimir los datos automáticamente cuando se leen de un archivo. También puede crear archivos comprimidos que son compatibles con los formatos ZIP y GZIP estándar, y puede descomprimir los archivos en esos formatos. La compresión real es proporcionada por las clases Inflater y Deflater, también empaquetadas en java.util.zip. Usan la biblioteca de compresión ZLIB. Por lo general no necesitará tratar con estas clases directamente cuando comprima o descomprima archivos, porque su operación predeterminada es suficiente. Consejos para el manejo de errores La E/S de archivos plantea un desafío especial cuando se trata de manejo de errores. Hay dos razones para esto. En primer lugar, las fallas de E/S son una posibilidad muy real cuando se leen o escriben archivos. A pesar del hecho de que el hardware de computación (e Internet) es mucho más confiable que en el pasado, aún falla a una tasa muy elevada, y cualquiera de esas fallas debe manejarse de manera consistente con las necesidades de su aplicación. La segunda razón de que el manejo de errores presente un desafío cuando se trabaja con archivos es que casi todas las operaciones de archivo pueden generar una o más excepciones. Esto significa que casi todo el código de manejo de archivos debe incluirse en un bloque try. La excepción de E/S más común es IOException. Muchos constructores y métodos en el sistema de E/S pueden lanzar esta excepción. Como regla general, es generada cuando algo sale mal al leer o escribir datos, o al abrir un archivo. Otras excepciones comunes relacionadas con E/S, como FileNotFoundException y ZipException, son subclases de IOException. Hay otra excepción común relacionada con el manejo de archivos: SecurityException. Muchos constructores o métodos lanzarán una SecurityException si la aplicación que invoca no tiene permiso para acceder a un archivo o realizar una operación específica. Necesitará manejar esta excepción de una manera apropiada para su aplicación. Por simplicidad, los ejemplos de este capítulo no manejan excepciones de seguridad, pero tal vez sea necesario que sus aplicaciones las incluyan. www.fullengineeringbook.net 58 Java: Soluciones de programación Debido a que muchos constructores y métodos pueden generar una IOException, no es poco común ver código que simplemente envuelve todas las operaciones de E/S dentro de un solo bloque try y luego captura la IOException que pueda ocurrir. Aunque sea adecuado experimentar con E/S de archivo o posiblemente con simples programas de utilería que son para su propio uso personal, este método no suele ser adecuado para código comercial. Esto se debe a que no es fácil tratar de manera individual con cada posible error. En cambio, para un control detallado, es mejor poner cada operación dentro de su propio bloque try. De esta manera, puede reportar y responder de manera precisa al error que ocurrió. Este es el método demostrado por los ejemplos en este capítulo. Otra manera en que a veces se maneja IOException es lanzarla fuera del método en que ocurre. Para esto, debe incluir una cláusula throws IOException en la declaración del método. Este método es bueno en algunos casos, porque reporta una falla de E/S al llamador. Sin embargo, en otras situaciones es un método abreviado poco satisfactorio porque causa que todos los usuarios del método manejen la excepción. Los ejemplos de este capítulo no usan este método. En cambio, manejan explícitamente todas las excepciones IOException. Esto permite que cada manejador de error reporte precisamente el error que ocurrió. Si maneja excepciones IOException al lanzarlas fuera del método en que ocurren, debe tener un cuidado adicional en cerrar cualquier archivo que haya sido abierto por el método. La manera más fácil de hacer esto consiste en envolver el código de su método en un bloque try y luego usar una cláusula finally para cerrar los archivos antes que el método que regresa. En los ejemplos de este capítulo, cualquier excepción de E/S que ocurra se maneja con el simple despliegue de un mensaje. Aunque este método sea aceptable para los programas de ejemplo, por lo general las aplicaciones reales necesitarán proporcionar una respuesta más sofisticada a un error de E/S. Por ejemplo, tal vez quiera dar al usuario la capacidad de volver a probar la operación, especificar una operación alterna o manejar de otra manera el problema. El objetivo principal es evitar la pérdida o corrupción de los datos. Parte de la tarea de ser un gran programador consiste en saber cómo manejar efectivamente las cosas que podrían salir mal cuando falla una operación de E/S. Un punto final: un error común que ocurre cuando se manejan archivos consiste en olvidar el cierre de un archivo cuando se termina de usarlo. Los archivos abiertos usan recursos del sistema. Por tanto, hay límites al número de archivos que pueden abrirse en cualquier momento. El cierre de un archivo también asegura que cualquier dato escrito en el archivo se escriba realmente en el dispositivo físico. Por tanto, la regla es muy simple: si abre un archivo, ciérrelo. Aunque los archivos suelen cerrarse automáticamente cuando termina una aplicación, es mejor no depender de esto porque puede llevar a una programación deficiente y a malos hábitos. Es mejor cerrar explícitamente cada archivo, manejando de manera apropiada cualquier excepción que pudiera ocurrir. Por esto, en los ejemplos de este capítulo todos los archivos se cierran de manera explícita, aunque el programa se esté terminando. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 59 Lea bytes de un archivo Componentes clave Clases Métodos java.io.FileInputStream int read( ) void close( ) Las dos operaciones con archivos más fundamentales son la lectura de y la escritura de bytes en un archivo. En esta solución se muestra cómo realizar la primera. (En la siguiente se muestra cómo manejar la segunda). Aunque Java ofrece la capacidad de leer otros tipos de datos, como caracteres, valores de punto flotante o líneas de texto, la E/S de bytes es la base del manejo de archivos, porque todas las operaciones con archivos están, en esencia, orientadas a bytes. Más aún, este tipo de operaciones puede usarse con cualquier tipo de archivo, sin importar lo que contenga. Por ejemplo, si quiere escribir una utilería de archivos que despliegue la representación hexadecimal de los bytes de un archivo, entonces necesitar usar clases de E/S basadas en bytes. Esto permite que el programa cree un "volcado hexadecimal" para cualquier tipo de archivo, incluidos los que contienen texto, imágenes, código ejecutable, etcétera. Una manera de leer bytes de un archivo consiste en usar FileInputStream. Se deriva de InputStream, que define la funcionalidad básica de todos los flujos de entrada de bytes. Implementa la interfaz Closeable. Paso a paso Para leer bytes de un archivo empleando FileInputStream se requieren estos pasos: 1. Abra el archivo al crear una instancia de FileInputStream. 2. Lea el archivo empleando el método read( ). 3. Cierre el archivo al llamar a close( ). Análisis A fin de abrir un archivo para entrada, cree un objeto de FileInputStream, que define tres constructores. El que usaremos es: FileInputStream(String nombreArchivo) throws FileNotFoundException Aquí, nombreArchivo especifica el nombre del archivo que desea abrir. Si el archivo no existe, entonces se lanza FileNotFoundException. Para leer del archivo, puede usar cualquier versión del método read( ), que se hereda de InputStream. Aquí se muestra el que usaremos: int read( ) throws IOException Lee un solo byte del archivo y lo devuelve como un valor entero. read( ) devuelve –1 cuando se encuentra el final del archivo. Lanzará una IOException si ocurre un error de E/S. Otras versiones de read( ) pueden recibir como entrada varios bytes a las vez y ponerlos en una matriz. www.fullengineeringbook.net 60 Java: Soluciones de programación Cuando haya terminado con el archivo, debe cerrarlo al llamar a close( ). Aquí se muestra: void close( ) throws IOException Se lanza una IOException si ocurre un error cuando se cierra el archivo. Ejemplo En el siguiente programa se usa FileInputStream para desplegar el contenido de un archivo, byte por byte, en formato hexadecimal. // // // // // // // // // Despliega un archivo en formato hexadecimal. Para usar este programa, especifique el nombre del archivo que quiere ver. Por ejemplo, para ver un archivo llamado prueba.exe, use la siguiente línea de comandos: java VolcarHex prueba.exe import java.io.*; class VolcarHex { public static void main(String args[ ]) { FileInputStream fin; // Primero se asegura de que se ha especificado un // archivo en la línea de comandos. if(args.length != 1) { System.out.println("Uso: java VolcarHex Archivo"); return; } // Ahora, abre el archivo. try { fin = new FileInputStream(args[0]); } catch(FileNotFoundException exc) { System.out.println("Archivo no encontrado"); return; } // Lee bytes y despliega sus valores hexadecimales. try { int i; int count = 0; // Lee bytes hasta que se encuentra EOF do { i = fin.read( ); if(i != –1) System.out.printf("%02X ", i); count++; www.fullengineeringbook.net Capítulo 3: Manejo de archivos 61 if(count == 16) { System.out.println( ); count = 0; } } while(i != –1); } catch(IOException exc) { System.out.println("Error al leer el archivo"); } // Cierra el archivo. try { fin.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo"); } } } He aquí una parte de un ejemplo de la salida producida cuando se ejecuta este programa en su propio archivo de clase. CA 00 68 53 1A 49 44 70 FE 04 61 74 01 6E 69 6F BA 00 01 72 00 6E 76 46 BE 15 00 69 09 65 69 69 00 07 12 6E 54 72 73 63 00 00 4C 67 69 43 6F 68 00 17 6A 3B 70 6C 72 61 32 07 61 01 6F 61 46 3B 00 00 76 00 46 73 69 01 1D 19 61 04 69 73 63 00 09 01 2F 74 63 65 68 06 00 00 6C 69 68 73 61 74 03 05 61 70 61 01 73 68 00 66 6E 6F 01 00 24 69 14 69 67 07 00 19 54 73 0A 63 2F 00 0C 4C 69 24 Opciones Hay otras dos formas de read( ) que puede usar para leer bytes de un archivo. En primer lugar, puede leer un bloque de bytes del archivo a usar esta forma de read( ): int read( )(byte buf[ ]) throws IOException Llena la matriz a la que hace referencia buf con bytes leídos del archivo. Devuelve el número de bytes que se lee en realidad, lo que podría ser menos que buf.length si se encuentra el final del archivo. Al tratar de leer al final del archivo se causa que read( ) devuelva –1. Por ejemplo, en el programa anterior, he aquí otra manera de secuenciar las lecturas y desplegar los bytes en el archivo: // Lee bytes y despliega sus valores hexadecimales. try { int lon; byte datos[ ] = new byte[16]; // Lee bytes hasta que se encuentra EOF. do { lon = fin.read(datos); for(int j=0; j<lon; j++) System.out.printf("%02X ", datos[j]); www.fullengineeringbook.net 62 Java: Soluciones de programación System.out.println( ); } while(lon != –1); } catch(IOException exc) { System.out.println("Error al leer el archivo"); } Con este método se crea una matriz de 16 bytes y se usa para leer hasta 16 bytes de datos con cada llamada a read( ). Esto es más eficiente que realizar 16 operaciones de lectura separadas, como en el ejemplo. Aquí se muestra la ultima forma de read( ): int read(byte buf[ ], int indInicio, int num) throws IOException Esta versión lee num de bytes del archivo y los almacena en buf, empezando en el índice especificado por indInicio. Devuelve el número de bytes que se lee en realidad, que podría ser menor que num si se encuentra el final del archivo. Tratar de leer el final del archivo causa que read( ) devuelva –1. FileInputStream proporciona constructores adicionales que le permiten crear un objeto al pasar un objeto de File o uno de FileDescriptor. Estos constructores ofrecen una opción conveniente en algunas situaciones. Para leer caracteres (es decir, objetos de tipo char) en lugar de bytes, use FileReader. (Consulte Lea caracteres de un archivo). Puede almacenar en búfer la entrada de un archivo al envolver un FileInputStream dentro de un BufferedInputStream. Esto hace más eficientes las operaciones con archivos. (Consulte Use el búfer para la E/S de un archivo basada en bytes). Escriba bytes en un archivo Componentes clave Clases Métodos java.io.FileOutputStream int write(int valbyte) void close( ) Como se estableció en la solución anterior, hay dos operaciones de archivo fundamentales: lectura y escritura de bytes en un archivo. En la solución anterior se mostró cómo leer bytes. En ésta se muestra cómo escribirlos. Aunque Java le ofrece la capacidad de escribir otros tipos de datos, la salida basada en bytes es útil en circunstancias en que deben escribirse datos simples (es decir, sin formato) en un archivo. Por ejemplo, si quiere guardar en disco el contenido de un búfer de pantalla, entonces la opción correcta es la salida basada en bytes. También es la opción correcta cuando se crean varias utilerías de archivo, como las de copia, división, mezcla o búsqueda de archivos, porque los operadores de archivos basados en bytes pueden usarse con cualquier tipo de archivo, sin importar lo que contenga o el formato de sus datos. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 63 Para escribir bytes en un archivo, puede usar FileOutputStream. Se deriva de OutputStream, que define la funcionalidad básica de todos los flujos de salida de bytes. Implementa las interfaces Closeable y Flushable. Paso a paso Para escribir en un archivo empleando FileOutputStream, se requieren estos pasos: 1. Abra el archivo al crear un objeto de FileOutputStream. 2. Escriba en el archivo empleando el método write( ). 3. Cierre el archivo al llamar a close( ). Análisis A fin de abrir un archivo para salida, cree un objeto de FileOutputStream, que define varios constructores. El que usaremos es: FileOutputStream(String nombreArchivo) throws FileNotFoundException Aquí, nombreArchivo especifica el nombre del archivo que desea abrir. Si el archivo no puede crearse, entonces se lanza FileNotFoundException. Cualquier archivo existente que tenga el mismo nombre habrá de destruirse. Para escribir en un archivo, puede usar cualquier versión del método write( ), que se hereda de OutputStream. Aquí se muestra su forma más simple: void write(int valbyte) throws IOException Con este método se escribe el byte especificado por valbyte en el archivo. Aunque valbyte se declara como entero, sólo se escriben los ocho bytes de orden inferior en el archivo. Lanzará una IOException si ocurre un error durante la escritura. Otras versiones de write( ) pueden dar salida a una matriz de bytes. Cuando haya terminado con el archivo, debe cerrarlo al llamar a close( ). Aquí se muestra: void close( ) throws IOException Se lanza una IOException si ocurre un error mientras se cierra el archivo. Ejemplo En el siguiente ejemplo se usa FileOutputStream para escribir bytes en un archivo. Primero crea un archivo llamado Prueba.dat. luego escribe cada tercer byte en la matriz vals, que contiene los códigos ASCII de la letra A a la J. Por tanto, después de que se ejecuta el programa, Prueba.dat contendrá los caracteres ASCII ACEGI. // Usa FileOutputStream para escribir los bytes en un archivo. import java.io.*; class EscribirBytes { public static void main(String args[ ]) { // Esta matriz contiene el código ASCII de la // letra A a la J. byte[ ] vals = { 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 }; www.fullengineeringbook.net 64 Java: Soluciones de programación FileOutputStream fout; try { // Abre el archivo de salida. fout = new FileOutputStream("Prueba.dat"); } catch(FileNotFoundException exc) { System.out.println("Error al abrir el archivo de salida"); return; } try { // Escribe cada tercer valor de la matriz vals en el archivo. for(int i=0; i<vals.length; i+=2) fout.write(vals[i]); } catch(IOException exc) { System.out.println("Error al escribir el archivo"); } try { fout.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo"); } } } Opciones Hay otras dos formas de write( ) que puede usar para escribir bytes en un archivo. En primer lugar, puede escribir un bloque de bytes empleando esta forma de write( ): void write(byte buf[ ]) throws IOException El contenido de la matriz a la que hace referencia buf se escribe en el archivo. Por ejemplo, en el programa anterior, si quisiera escribir todo el contenido de la matriz vals en el archivo, podría usar esta simple llamada a write( ). fout.write(vals) Esto sería más eficiente que escribir de byte en byte. Puede escribir una parte de una matriz en un archivo al usar esta forma de write( ): void write(byte buf[ ], int indInicio, int num) throws IOException Esta versión escribe un num de bytes de buf en un archivo, empezando en el índice especificado por indInicio. FileOutputStream proporciona varios constructores adicionales. En primer lugar, puede especificar el archivo que se abrirá al pasar un objeto de File o uno de FileDescriptor. También puede especificar si la salida se adjuntará al final del archivo empleando uno de estos constructores: FileOutputStream(String nombreArchivo, boolean adjunt) throws FileNotFoundException FileOutputStream(String objetoArchivo, boolean adjunt) throws FileNotFoundException www.fullengineeringbook.net Capítulo 3: Manejo de archivos 65 En la primera versión, nombreArchivo especifica el nombre del archivo que quiere abrir. En la segunda versión, objetoArchivo especifica el objeto de File que describe el archivo que quiere abrir. Si adjunt es true, entonces el contenido del archivo existente se conservará y toda la salida se escribirá al final del archivo. Esto es útil cuando quiere agregar algo a un archivo existente. De otra manera, cuando adjunt es false, el contenido del archivo anterior, con el mismo nombre, se destruirá. En ambos casos, si no puede abrirse el archivo, se lanzará FileNotFoundException. Verá los efectos de abrir un archivo usando el modo de adjuntar si sustituye la línea en el ejemplo: fout = new FileOutputStream("Prueba.dat", true); Ahora, cada vez que ejecute el programa, los caracteres se agregarán al final del contenido anterior de Prueba.dat. Puede almacenar en búfer la salida a un archivo al envolver un FileOutputStream dentro de un BufferedOutputStream. (Consulte Use el búfer para la E/S de un archivo basada en bytes.) Para escribir caracteres (es decir, objetos de tipo char) en lugar de bytes, use FileWriter. (Consulte Escriba caracteres en un archivo.) Use el búfer para la E/S de un archivo basada en bytes Componentes clave Clases Métodos java.io.BufferedInputStream int read( ) void close( ) java.io.BufferedOutputStream void int read( ) void close( ) En las dos soluciones anteriores se mostró el procedimiento general para leer y escribir bytes en un archivo empleando FileInputStream y FileOutputStream. Aunque no hay nada erróneo en esto, ninguna de esas clases proporciona uso automático del búfer. Esto significa que cada operación de lectura o escritura interactúa, al final de cuentas, con varios controladores de E/S en el nivel del sistema (lo que proporciona acceso al archivo físico). Éste no suele ser un método eficiente. En muchos casos, una mejor manera consiste en usar el búfer para el flujo de datos. En el caso de la salida, los datos se almacenan en un búfer hasta que éste se llena. Luego todo el búfer se escribe en el archivo en una sola operación. En el caso de entrada, se lee un búfer completo del archivo y luego cada operación de entrada obtiene sus datos del búfer. Cuando el búfer se agota, se obtiene el siguiente búfer del archivo. Este mecanismo causa que el número de operaciones individuales de archivo se reduzca en gran medida. Aunque es posible usar manualmente el búfer para las operaciones de E/S al leer y escribir matrices de bytes (en lugar de bytes individuales), ésta no es una solución conveniente ni apropiada en muchos casos. En cambio, para lograr los beneficios de rendimiento de usar el búfer para las operaciones de E/S de archivos, por lo general querrá envolver un flujo de archivo dentro de una de las clases de flujo de búfer de Java. Al hacerlo así se mejorará de manera importante la velocidad de sus operaciones de E/S sin esfuerzo adicional de su parte. www.fullengineeringbook.net 66 Java: Soluciones de programación Para crear un flujo de entrada en búfer, use BufferedInputStream. Deriva de InputStream y de FilterInputStream. Implementa la interfaz Closeable. Para crear un flujo de salida en búfer, use BufferedOutputStream. Deriva de OutputStream y de FilterOutputStream. Implementa las interfaces Closeable y Flushable. Paso a paso Para usar el búfer con un flujo de archivo, se requieren estos pasos: 1. Cree el flujo de archivo. Para entrada, sería una instancia de FileInputStream. En el caso de la salida, sería una instancia de FileOutputStream. 2. Envuelva el flujo de archivo en el flujo de búfer apropiado. Para el caso de la entrada, envuelva el flujo de archivo en un BufferedInputStream. Para el caso de la salida, en un BufferedOutputStream. 3. Realice todas las operaciones de E/S mediante el flujo en búfer. 4. Cierre el flujo que usará el búfer. El cierre de éste causa automáticamente que se cierre el flujo de archivo. Análisis Para crear un flujo de entrada que se almacenará en búfer, use BufferedInputStream. Define dos constructores. El que usaremos es: BufferedInputStream(InputStream flujo) Esto crea un flujo en entrada en búfer que usa el búfer para el flujo de entrada especificado por flujo. Usa el tamaño de búfer predeterminado. Para crear un flujo de salida que se almacenará en búfer, use BufferedOutputStream. Define dos constructores. El que usaremos es: BufferedOutputStream(OutputStream flujo) Esto crea un flujo en salida en búfer que usa el búfer para el flujo de salida especificado por flujo. Usa el tamaño de búfer predeterminado. Para leer de un flujo en búfer, puede usar read( ). Para escribir en un flujo en búfer, puede usar write( ). (Consulte Lea bytes de un archivo y Escriba bytes en un archivo, para conocer más detalles). Cuando haya terminado con el flujo en búfer, debe cerrarlo al llamar a close( ). Aquí se muestra: void close( ) throws IOException. Al cerrar un flujo en búfer también se causa que el flujo original se cierre automáticamente. Si ocurre un error, se lanzará una IOException. Ejemplo En el siguiente ejemplo se muestran BufferedInputStream y BufferedOutputStream en acción. Se crea un programa que copia un archivo. Debido a que se usan flujos de bytes, puede copiarse cualquier tipo de archivo, incluidos los que contienen texto, datos o programas. www.fullengineeringbook.net Capítulo 3: // // // // // // // // // // Manejo de archivos Usa flujos en buffer para copiar un archivo. Para usar este programa, especifique el nombre de los archivos de origen y de destino. Por ejemplo, para copiar un archivo llamado ejemplo.dat en uno llamado ejemplo.bak, use la siguiente línea de comandos: java CopiarArchivoBufer ejemplo.dat ejemplo.bak import java.io.*; class CopiarArchivoBufer { public static void main(String args[ ]) { BufferedInputStream fin; BufferedOutputStream fout; // Primero se asegura de que se han especificado ambos archivos. if(args.length != 2) { System.out.println("Uso: CopiarArchivoBufer De A"); return; } // Abre un archivo de entrada que está envuelto en un BufferedInputStream. try { fin = new BufferedInputStream(new FileInputStream(args[0])); } catch(FileNotFoundException exc) { System.out.println("Archivo de entrada no encontrado"); return; } // Abre un archivo de salida que está envuelto en un BufferedOutputStream. try { fout = new BufferedOutputStream(new FileOutputStream(args[1])); } catch(FileNotFoundException exc) { System.out.println("Error al abrir el archivo de salida"); // Cierra el archivo de entrada abierto. try { fin.close( ); } catch(IOException exc2) { System.out.println("Error al cerrar el archivo de entrada"); } return; } // Copia el archivo. // Debido al uso de flujos en búfer, las operaciones // de lectura y escritura se incluyen en búfer // automáticamente, lo que da un mejor rendimiento. try { int i; www.fullengineeringbook.net 67 68 Java: Soluciones de programación do { i = fin.read( ); if(i != –1) fout.write(i); } while(i != –1); } catch(IOException exc) { System.out.println("Error de archivo"); } try { fin.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de entrada"); } try { fout.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de salida"); } } } Opciones La mejora en el rendimiento que se genera con el uso de flujos en búfer puede ser muy importante. En realidad, no tiene que depender de cronometrajes en nanosegundos. Suele ser muy evidente con la simple observación. Esto es cierto en el programa de copia de archivos que acabamos de mostrar. Para ver la diferencia de lo que hace el uso del búfer, use primero el programa de ejemplo como se muestra para copiar un archivo muy grande y observe cuánto tarda en ejecutarse. Luego, modifique el programa para que use FileInputStream y FileOutputStream directamente. En otras palabras, no envuelva el archivo dentro de un flujo de entrada en búfer. Luego, vuelva a ejecutar el programa, copiando el mismo archivo largo. Si el archivo que está copiando es lo suficientemente grande, notará con facilidad que la versión que no usa el búfer toma más tiempo. Aunque el tamaño de los búferes que proporcionan automáticamente BufferedInputStream y BufferedOutputStream suelen ser suficientes, es posible especificar su tamaño. Podría hacer esto si sus datos están organizados en bloques de tamaño fijo y estará operando de bloque en bloque. Con el fin de especificar el tamaño del búfer para entrada en búfer, use el siguiente constructor. BufferedInputStream(InputStream flujo int longit) Esto crea un flujo de entrada en búfer que usa el búfer para el flujo especificado en flujo. La longitud del búfer se pasa vía longit, que debe ser mayor que cero. Para especificar el tamaño del búfer para una salida en búfer, use el siguiente constructor: BufferedOutputStream(OutputStream flujo int longit) Esto crea un flujo de salida en búfer que usa el búfer para el flujo especificado en flujo. La longitud del búfer se pasa vía longit, que debe ser mayor que cero. BufferedInputStream sobreescribe los métodos mark( ) y reset( ) especificados por InputStream. Esto es importante porque en éste último, mark( ) no hace nada y reset( ) lanza una IOException. Al sobreescribir estos métodos, BufferedInputStream le permite moverse dentro de un búfer. En ciertas situaciones, esta capacidad resulta útil. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 69 Lea caracteres de un archivo Componentes clave Clases Métodos java.io.FileReader int read( ) void close( ) Aunque los flujos de bytes son técnicamente suficientes para manejar todas las tareas de entrada de archivos (porque todos los archivos pueden tratarse como flujos de bytes), Java ofrece un mejor método cuando se opera con datos de carácter: los flujos de carácter. Los flujos basados en carácter operan directamente en objetos de tipo char (en lugar de bytes). Por tanto, cuando se trabaja con archivos que contienen texto, los flujos basados en carácter suelen ser la mejor opción. Para leer caracteres de un archivo, puede usar FileReader, que se deriva de InputStreamReader y Reader. (InputStreamReader proporciona el mecanismo que traduce bytes en caracteres). FileReader implementa las interfaces Closeable y Readable. [(Readable está empaquetada en java. lang y define un objeto que proporciona caracteres mediante el método read( )]. Cuando se lee un archivo vía FileReader, la traducción de bytes en caracteres se maneja automáticamente. Paso a paso Para leer un archivo empleando FileReader se requieren tres pasos principales: 1. Abra el archivo empleando FileReader. 2. Lea del archivo usando el método read( ). 3. Cierre el archivo al llamar a close( ). Análisis Para abrir un archivo, simplemente cree un objeto de FileReader, que define tres constructores. El que usaremos es: FileReader(String nombreArchivo) throws FileNotFoundException Aquí, nombreArchivo es el nombre de un archivo. Lanza una FileNotFoundException, si el archivo no existe. Para leer un carácter de un archivo, puede usar esta versión de read( ) (heredada de Reader): int read( ) throws IOException Cada vez que se le llama, lee un solo carácter de un archivo y devuelve el carácter como un valor entero. read( ) devuelve –1 cuando se encuentra el final del archivo. Lanzará una IOException si ocurre un error de E/S. Otras versiones de read( ) pueden dar entrada a varios caracteres al mismo tiempo y ponerlos en una matriz. Cuando haya terminado con el archivo, debe cerrarlo al llamar a close( ). Aquí se muestra: void close( ) throws IOException Si ocurre un error cuando trata de cerrar el archivo, se lanza una IOException. www.fullengineeringbook.net 70 Java: Soluciones de programación Ejemplo En el siguiente programa se usa FileReader para ingresar y desplegar el contenido de un archivo de texto, de carácter en carácter: // // // // // // // // // Usa un FileReader para desplegar un archivo de texto. Para usar este programa, especifique el nombre del archivo que quiere ver. Por ejemplo, para ver un archivo llamado Prueba.txt, use la siguiente línea de comandos. java MostrarArchivo Prueba.txt import java.io.*; class MostrarArchivo { public static void main(String args[ ]) { FileReader fr; // Primero se asegura de que se haya especificado un archivo. if(args.length != 1) { System.out.println("Uso: MostrarArchivo archivo"); return; } try { // Abre el archivo. fr = new FileReader(args[0]); } catch(FileNotFoundException exc) { System.out.println("No se ha encontrado el archivo"); return; } // En este punto, el archivo está abierto // y puede leerse su contenido. try { int car; // Lee el archivo de carácter en carácter. do { car = fr.read( ); if(car != –1) System.out.print((char)car); } while(car != –1); } catch(IOException exc) { System.out.println("Error al leer el archivo"); } www.fullengineeringbook.net Capítulo 3: Manejo de archivos 71 try { fr.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo"); } } } Opciones Hay otras tres formas de read( ) disponibles para FileReader. La primera es: int read(char buf[ ]) throws IOException Este método rellena la matriz a la que hace referencia buf y lee caracteres de un archivo. Devuelve el número de caracteres que se lee en realidad, que podría ser menos que buf.length si se encuentra el final del archivo. Al tratar de leer el final del archivo se causa que read( ) devuelva –1. Por ejemplo, en el programa anterior, he aquí otra manera de escribir la parte del código que lee y despliega los caracteres en el archivo: try { int cuenta; char cars[ ] = new char[80]; // Lee el archivo de búfer en búfer. do { cuenta = fr.read(cars); for(int i=0; i < cuenta; i++) System.out.print(cars[i]); } while(cuenta != –1); } catch(IOException exc) { System.out.println("Error al leer el archivo"); } Este método crea una matriz de 80 caracteres y lo usa para leer hasta 80 caracteres con cada llamada a read( ). Esto es más eficiente que realizar 80 operaciones de lectura separadas, como se hace en el ejemplo. Aquí se muestra la siguiente forma de read( ): int read(char buf[ ], int indInicio, int num) throws IOException Esta versión lee un num de caracteres del archivo final y los almacena en buf, empezando en el índice especificado en indInicio. Devuelve el número de caracteres que lee en realidad, lo que podría ser menos que num si se encuentra el final del archivo. Tratar de leer el final del archivo causa que read( ) devuelva –1. Aquí se muestra la versión final de read( ): int read(CharBuffer buf) throws IOException www.fullengineeringbook.net 72 Java: Soluciones de programación Esta versión de read( ) está especificada por la interfaz Readable. Lee caracteres en el búfer al que se hace referencia con buf. Devuelve el número de caracteres que se lee en realidad, que podría ser menor que el tamaño del búfer, si se encuentra el final del archivo. Si se trata de leer el final del archivo, read( ) devuelve –1. CharBuffer está empaquetado en java.nio y lo usa el sistema NIO. FileReader proporciona dos constructores adicionales que le permiten crear un objeto al pasar un objeto de File o al pasarlo en un FileDescriptor. Estos constructores pueden ofrecer una opción conveniente en algunas situaciones. Para leer los bytes contenidos en un archivo en lugar de los caracteres, use FileInputStream. (Consulte Lea bytes de un archivo). Puede usar el búfer para la entrada desde un archivo al envolver un FileReader en un BufferReader. (Consulte Use el búfer para la E/S de un archivo basada en bytes). Lea bytes de un archivo Componentes clave Clases Métodos java.io.FileWriter void write(String cad) void close( ) Aunque un flujo de bytes puede usarse para escribir caracteres en un archivo, no es la mejor opción. En casos en que está tratando exclusivamente con texto, los flujos de archivo basados en caracteres de Java son una mucho mejor solución porque automáticamente manejan la traducción de caracteres en bytes. No sólo puede ser esto más eficiente, también puede simplificar la internacionalización. Para escribir caracteres en un archivo, puede usar FileWriter, que se deriva de OutputStreamWriter y Writer. (OutputStreamWriter proporciona el mecanismo que traduce caracteres en bytes.) FileWriter implementa las interfaces Closeable, Flushable y Appendable. [(Appendable está empaquetado en java.lang. Define un objeto al que pueden agregarse caracteres vía el método append( )]. Cuando se escribe un archivo vía FileWriter, la traducción de caracteres en bytes se maneja automáticamente. Paso a paso Para escribir un archivo empleando FileWriter, se requieren estos pasos: 1. Abra el archivo usando FileWriter. 2. Escriba en el archivo usando el método write( ). 3. Cierre el archivo al llamar a close( ). www.fullengineeringbook.net Capítulo 3: Manejo de archivos 73 Análisis Para abrir un archivo, simplemente cree un objeto de FileWriter, que define varios constructores. El que usaremos es FileWriter(String nombreArchivo) throws IOException Aquí, nombreArchivo es el nombre de un archivo. Lanza una IOException, si falla. Para escribir en el archivo, use el método write( ), que se hereda de Writer. Aquí se muestra una versión: void write(String cad) throws IOException Esta versión de write( ) escribe cad en el archivo. Lanza una IOException si ocurre un error mientras se escribe. Están disponibles varias otras versiones de write( ) que escriben caracteres individuales, partes de una cadena o el contenido a la matriz char. Cuando haya terminado con el archivo, debe cerrarlo al llamar a close( ). Aquí se muestra: void close( ) throws IOException Ejemplo En este ejemplo se usa un FileWriter para escribir una matriz de cadenas (cada una contiene un número ID de empleado de cinco dígitos y una dirección de correo electrónico) en un archivo llamado Empleados.dat. Observe que después de que se escribe cada cadena en la matriz, se escribe una nueva línea. Esto hace que cada cadena tenga su propia línea. Sin la nueva línea, la siguiente cadena empezaría en la misma línea, inmediatamente después de la anterior. Por supuesto, no todas las aplicaciones necesitarán la adición de la nueva línea. // Usa FileWriter para escribir una matriz de cadenas en un archivo. import java.io.*; class EscribeCars { public static void main(String args[ ]) { String cad; FileWriter fw; String cads[ ] = { "32435 Tom@HerbSchildt.com", "86754 Mary@HerbSchildt.com", "35789 TC@HerbSchildt.com" }; try { // Abre el archivo de salida. fw = new FileWriter("Empleados.dat"); } catch(IOException exc) { System.out.println("Error al abrir el archivo"); return ; } try { www.fullengineeringbook.net 74 Java: Soluciones de programación // Escriba las cadenas en cads en el archivo. for(int i=0; i < cads.length; i++) { fw.write(cads[i]); // escribe líneas en un archivo fw.write("\n"); // salida a una nueva línea } } catch(IOException exc) { System.out.println("Error al escribir el archivo"); } try { fw.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo"); } } } Después de que se ejecuta este programa, Empleados.dat contendrán la siguiente salida: 32435 Tom@HerbSchildt.com 86754 Mary@HerbSchildt.com 35789 TC@HerbSchildt.com Opciones Hay varias otras formas de write( ) que puede usar. Aquí se muestran: void write(int car) throws IOException void write(char buf[ ]) throws IOException void write(char buf[ ], int indInicio, int num) throws IOException void write(String cad, int indInicio, int num) throws IOException La primera forma escribe los 16 bits de orden menor de char. La segunda forma escribe el contenido de buf. La tercera forma escribe un num de caracteres desde buf, empezando en el índice especificado por indInicio. La forma final escribe un num de caracteres desde cad, empezando en el índice especificado por indInicio. La última forma de write( ) resulta especialmente útil en ocasiones. Por ejemplo, suponiendo las cadenas de empleadas usada en el ejemplo (un número de ID de empleado de 5 dígitos, seguido por un espacio, seguido por la dirección de correo electrónico de empleado), la siguiente llamada a write( ) sólo escribirá la dirección de correo electrónico en un archivo: fw.write(cads[i], 6, cads[i].length( )–6); Si se sustituye esta línea en el ejemplo, entonces el archivo Empleados.dat contendrán lo siguiente: Tom@HerbSchildt.com Mary@HerbSchildt.com TC@HerbSchildt.com Como es evidente, la parte de ID de empleado de cada cadena no está almacenada en el archivo. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 75 FileWriter proporciona varios constructores adicionales. En primer lugar, puede crear un objeto al pasar un objeto de File o al pasarlo en un FileDescriptor. También puede especificar si la salida se habrá de adjuntar al final del archivo al usar uno de esos constructores: FileWriter(String nombreArchivo, boolean adjunt) throws IOException FileWriter(File objetoArchivo, boolean adjunt) throws IOException En la primera versión, nombreArchivo especifica el nombre del archivo que quiere abrir. En la segunda versión, objetoArchivo especifica el archivo de File que describe el archivo. En ambos casos, si adjunt es verdadero, entonces el contenido de un archivo existente se preservará y toda la salida se escribirá al final del archivo. Esto es útil cuando quiere agregar algo a un archivo existente. De otra manera, cuando adjunt es falso, se destruirá el contenido de cualquier archivo existente con el mismo nombre. Si el archivo no puede abrirse, entonces ambos constructores lanzan IOException. Puede ver los efectos de abrir un archivo empleando el modo de adjuntar al sustituir esta línea en el ejemplo: fw = new FileWriter("Empleados.dat", true); Ahora, cada vez que ejecuta el programa, los datos de los empleados se agregarán al final del archivo existente. Para escribir bytes en lugar de caracteres, use FileOutputStream. (Consulte Escriba bytes en un archivo). Puede usar el búfer para la salida de un archivo el envolver un FileWriter en un BufferWriter. (Consulte Use el búfer para la E/S de un archivo basada en bytes). Use el búfer para la E/S de un archivo basada en caracteres Componentes clave Clases Métodos java.io.BufferedReader int read( ) void close( ) java.io.BufferedWriter void write(int valbytes) void close( ) Aunque FileReader y FileWriter proporcionan las capacidades para leer y escribir en un archivo de texto, emplearlos solos tal vez no siempre sea el método más eficiente. La razón es que cada operación de lectura o escritura individual traduce (directa o indirectamente) en una operación en el archivo, y el acceso al archivo consume tiempo. A menudo, un mejor método es proporcionar un búfer para los datos. En este método, cada operación de entrada lee desde un bloque de datos y cada operación de salida escribe en un bloque de datos. Por tanto, el número de operaciones de archivo es reducido, lo que da como resultado el rendimiento mejorado. Aunque es posible usar el búfer manualmente en las operaciones de E/S al leer y escribir matrices de caracteres (en lugar de caracteres individuales), este método no será óptimo en todos los casos. En cambio, para lograr los beneficios del rendimiento de usar el búfer para las operaciones de E/S, por lo general querrá envolver un flujo de caracteres dentro de una clase de www.fullengineeringbook.net 76 Java: Soluciones de programación lector o escritor en búfer de Java. Al hacerlo así, en ocasiones aumentará de manera importante la velocidad de sus operaciones de E/S sin esfuerzo adicional de su parte. Para crear un lector de flujo de entrada en búfer, use BufferedReader. Deriva de Reader e implementa las interfaces Closeable y Readable. [(Readable está empaquetado en java.lang y define un objeto que proporciona caracteres vía el método read( )]. Para crear un lector de flujo de salida en búfer, use BufferedWriter. Deriva de Writer e implementa las interfaces Closeable, Flushable y Appendable. [(Appendable está empaquetado en java.lang. Define un objeto al que pueden agregarse caracteres mediante el método append( )]. Paso a paso Para usar el búfer con un flujo de archivo basado en caracteres, se requieren estos pasos: 1. Cree el flujo base. Para entrada, será una instancia de FileReader. En el caso de la salida, será una instancia de FileWriter. 2. Envuelva el flujo de archivo en el lector o escritor en búfer apropiado. Para el caso de la entrada, use BufferedReader. Para el caso de la salida, use BufferedWriter. 3. Realice todas las operaciones de E/S mediante el lector o escritor que usa el búfer. 4. Cierre el flujo en búfer. El cierre de éste causa automáticamente que se cierre el flujo de archivo. Análisis Para crear un lector que usará el búfer, use BufferedReader. Define dos constructores. El que usaremos es: BufferedReader(Reader lect) Esto crea un lector que usa el búfer para el flujo de entrada especificado por lect. Usa el tamaño de búfer predeterminado. En el caso de operaciones de entrada de archivo, pasará un FileReader a lect. Para crear un escritor que usa el búfer para flujo de salida, use BufferedWriter. Define dos constructores. El que usaremos es: BufferedWriter(OutputStream escr) Esto crea un escritor que usa el búfer para el flujo de salida especificado por escr. Usa el tamaño de búfer predeterminado. Por tanto, para usar el búfer con las operaciones de archivo, pasará un FileWriter a escr. Por ejemplo, suponiendo un archivo llamado Prueba.dat, en las siguientes líneas se muestra cómo crear un BufferedReader y un BufferedWriter vinculado con ese archivo. BufferedReader bw = new BufferedReader (new FileReader("Prueba.dat")); BufferedWriter br = new BufferedWriter (new FileWriter("Prueba.dat")); Para leer de un BufferedReader, puede usar cualquiera de los métodos de read( ), que se heredan de Reader. Aquí se muestra el que usaremos: int read( ) throws IOException www.fullengineeringbook.net Capítulo 3: Manejo de archivos 77 Devuelve el siguiente carácter en el flujo (en los 16 bits de orden inferior de un entero) o –1 si se llega al final del flujo. Si ocurre un error durante la lectura, se lanza una IOException. Para escribir en un flujo en búfer, puede usar cualquier versión del método write( ), que se hereda de Writer. Aquí se muestra el que usaremos: void write(int car) throws IOException Este método escribe el carácter especificado por car en el archivo. Aunque valbyte se declare como entero, sólo se escriben en el flujo los 16 bits de orden inferior. Si ocurre un error durante la escritura, se lanza una IOException. Otras versiones de write( ) pueden dar salida a una matriz de caracteres. Cuando haya terminado con el lector o escritor que usa el búfer, debe cerrarlo al llamar a close( ). Aquí se muestra: void close( ) throws IOException. Al cerrar un BufferedReader o un BufferedWriter también se causa que el flujo original se cierre automáticamente. Ejemplo El siguiente ejemplo usa un BufferedReader y un BufferedWriter para copiar un archivo. En el proceso, invierte las mayúsculas y minúsculas de las letras. En otras palabras, las minúsculas se vuelven mayúsculas, y viceversa. Todos los demás caracteres quedan sin cambio. // // // // // // // // // // // Usa un BufferedReader y un BufferedWriter para copiar un archivo de texto, invirtiendo las mayúsculas y minúsculas en el proceso. Para usar este programa, especifique el nombre de los archivos de origen y destino. Por ejemplo, para copiar un archivo llamado prueba.txt en uno llamado prueba.inv, use la siguiente línea de comandos: java CopiarInvertir test.txt test.inv import java.io.*; class CopiarInvertir { public static void main(String args[ ]) { BufferedReader br; BufferedWriter bw; // Primero se asegura de que se han especificado ambos archivos. if(args.length != 2) { System.out.println("Uso: CopiarInvertir De A"); return; } www.fullengineeringbook.net 78 Java: Soluciones de programación // Abre un FileReader envuelto en un BufferedReader. try { br = new BufferedReader(new FileReader(args[0])); } catch(FileNotFoundException exc) { System.out.println("No se ha encontrado el archivo de entrada"); return; } // Abre un FileWriter envuelto en un BufferedWriter. try { bw = new BufferedWriter(new FileWriter(args[1])); } catch(IOException exc) { System.out.println("Error al abrir el archivo de salida"); // Cierra el lector de entrada abierto. try { br.close( ); } catch(IOException exc2) { System.out.println("Error al cerrar el archivo de entrada"); } return; } // Copia el archivo, invirtiendo las mayúsculas y minúsculas // en el proceso. Debido a que se usan flujos en búfer, las // operaciones de lectura y escritura usan el búfer, // automáticamente, lo que da un mejor rendimiento. try { int i; char ch; do { i = br.read( ); if(i != –1) { if(Character.isLowerCase((char) i)) bw.write(Character.toUpperCase((char) i)); else if(Character.isUpperCase((char) i)) bw.write(Character.toLowerCase((char) i)); else bw.write((char) i); } } while(i != –1); } catch(IOException exc) { System.out.println("Error de archivo"); } try { br.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de entrada"); } www.fullengineeringbook.net Capítulo 3: Manejo de archivos 79 try { bw.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de salida"); } } } Opciones Es fácil ver de primera mano la mejora en el rendimiento que se obtiene al envolver un flujo de archivo en un flujo que usa el búfer, al hacer el siguiente experimento. En primer lugar, ejecute el programa de ejemplo, como se muestra, en un archivo de texto muy largo. Ahora observe cuánto tarda en ejecutarse. Luego, modifique el programa para que use FileReader y FileWriter directamente. En otras palabras, no envuelva ninguno en una clase que use el búfer. Luego, vuelva a ejecutar el programa con el mismo archivo de texto largo. Si el archivo que está empleando es lo suficientemente largo, observará con facilidad que la versión que no usa el búfer ocupa más tiempo. Como regla general, el búfer predeterminado proporcionado por BufferedReader y BufferedWriter es suficiente, pero es posible especificar un tamaño de búfer elegido por usted. Sin embargo, esto es más aplicable a situaciones en que un archivo está organizado en bloques de caracteres, y en que cada bloque tiene un tamaño fijo. Francamente, cuando se trabaja con archivos de texto, esta situación no es común. Por tanto, el tamaño de búfer predeterminado suele ser la elección apropiada. Para especificar el tamaño del búfer para un BufferedReader, use este constructor: BufferedReader(Reader lect, int longit) Esto crea un lector que usa el búfer con base en lect, que tiene una longitud de búfer longit, que debe ser mayor que cero. Para especificar el tamaño del búfer para un BufferWriter, use este constructor: BufferedWriter(OutputStream esch, int longit) Esto crea un escritor que usa el búfer en Esch que tiene una longitud de búfer de longit, que debe ser mayor que cero. BufferedReader sobreescribe los métodos mark( ) y reset( ) especificados por Reader. Esto es importante porque las implementaciones predeterminadas proporcionadas por Reader (heredadas por FileReader) simplemente lanzan una IOException. Al sobreescribir estos métodos, BufferedReader le permite moverse dentro de un búfer. Esta capacidad le resultará útil en ciertas situaciones. BufferedReader proporciona un método que le resultará especialmente útil en algunos casos: readLine( ). Este método lee una línea de texto completo. Aquí se muestra: String readLine( ) throws IOException Devuelve una cadena que contiene los caracteres leídos. Devuelve null si se hace un intento de leer al final del flujo. La cadena devuelta por readLine( ) no termina con los caracteres de final de línea, como un retorno de carro o un alimentador de línea. Sin embargo, la operación de lectura sí consume esos caracteres. www.fullengineeringbook.net 80 Java: Soluciones de programación Lea y escriba archivos de acceso aleatorio Componentes clave Clases Métodos java.io.RandomAccessFile void seek(long nuevaPos) long length( ) int read( ) void write(int val) void close( ) En la solución anterior se ha mostrado cómo leer y escribir archivos en forma lineal, un byte o carácter tras otro. Sin embargo, Java también le permite acceder al contenido de un archivo en orden aleatorio o directo. Para ello, utilizará, RandomAccessFile, que encapsula un archivo de acceso aleatorio. RandomAccessFile soporta colocación de solicitudes, lo que significa que puede leer o escribir en cualquier lugar dentro del archivo. RandomAccessFile está orientada a byte, pero no deriva de InputStream u OutputStream. En cambio, implementa las interfaces DataInput y DataUutput, que definen los métodos básicos de E/S, como readInt( ) y writeDouble( ), que leen y escriben tipos primitivos de Java. También proporciona varios métodos read( ) y write( ) basados en bytes. Además se implementa la interfaz Closeable. Paso a paso Para leer y escribir bytes en un orden no secuencial, se requieren los siguientes pasos: 1. Abra un archivo de acceso aleatorio al crear una instancia de RandomAccessFile. 2. Use el método seek( ) para colocar el apuntador al archivo en el lugar en que quiera leer o escribir. 3. Use los métodos de RandomAccessFile para leer o escribir datos. 4. Cierre el archivo. Análisis RandomAccessFile proporciona dos constructores. El que usaremos aquí se muestra a continuación: RandomAccessFile(String nombreArchivo, String acceso) throws FileNotFoundException El nombre del archivo se pasa en nombreArchivo y acceso determina el tipo de acceso a archivo que se permite. Si acceso es "r", puede leerse el archivo, pero no escribirse en él. Si es "rw", el archivo se abre en el modo de lectura y escritura. Otros valores de acceso válido se describen bajo Opciones. Se lanza una FileNotFoundException si el archivo no puede abrirse para acceso "r" o si el archivo no puede abrirse o crearse para acceso "rw". www.fullengineeringbook.net Capítulo 3: Manejo de archivos 81 La ubicación dentro del archivo en que ocurrirá la siguiente operación de E/S está determinada por la posición del apuntador a archivo. Se trata, en esencia, de un índice en el archivo. La posición del apuntador al archivo se establece al llamar al método seek( ), que se muestra a continuación: void seek(long nuevaPos) throws IOException Aquí, nuevaPos especifica la nueva posición, en bytes, del apuntador desde el principio del archivo. Después de una llamada a seek( ), la siguiente operación de lectura o escritura ocurrirá en la nueva posición del archivo. El valor de nuevaPos debe ser mayor que cero. Si se trata de usar un valor menor de cero se lanzará una IOException. Por tanto, no es posible buscar antes del principio de un archivo. Sin embargo, sí es posible buscar después del final. RandomAccessFile da soporte a varios métodos read( ) y write( ), de los cuales, muchos están especificados por las interfaces DataInput y DataOutput. Aquí se muestran las usadas en el ejemplo: int read( ) throws IOException void write(int val) throws IOException El método read( ) devuelve el byte en la ubicación del apuntador a archivo actual; write( ) escribe val en la posición actual del apuntador a archivo. Cuando se trabaja con archivos de acceso aleatorio, en ocasiones es útil saber la longitud del archivo en bytes. Una razón es que puede buscar hasta el final del archivo. Puede obtener la longitud actual del archivo al llamar a length( ), que se muestra aquí: long length( ) throws IOException Devuelve el tamaño del archivo. Ejemplo En el siguiente ejemplo se ilustran las operaciones de acceso aleatorio. Invierte el contenido de un archivo al intercambiar la posición de los bytes, del principio al final. Por ejemplo, dado un archivo que contiene ABCDE Después de ejecutar InvertirArchivo en él, el archivo contendrá EDCBA // // // // // // // // Usa RandomAccessFile para invertir un archivo. Para usar este programa, especifique el nombre del archivo. Por ejemplo, para invertir un archivo llamado prueba.txt use la siguiente línea de comandos: java InvertirArchivo prueba.txt import java.io.*; class InvertirArchivo { public static void main(String args[ ]) www.fullengineeringbook.net 82 Java: Soluciones de programación { // Primero, se asegura de que se ha especificado un archivo. if(args.length != 1) { System.out.println("Uso: InvertirArchivo nombre"); return; } RandomAccessFile raf; try { // Abre el archivo. raf = new RandomAccessFile(args[0], "rw"); } catch(FileNotFoundException exc) { System.out.println("No se puede abrir el archivo"); return ; } try { int x, y; // Invierte el archivo. for(long i=0, j=raf.length( )–1; i < j; i++, j––) { // Lee el siguiente conjunto de bytes. raf.seek(i); x = raf.read( ); raf.seek(j); y = raf.read( ); // Intercambia los bytes. raf.seek(j); raf.write(x); raf.seek(i); raf.write(y); } } catch(IOException exc) { System.out.println("Error al escribir el archivo"); } try { raf.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo"); } } } www.fullengineeringbook.net Capítulo 3: Manejo de archivos 83 Opciones Hay una segunda forma de RandomAccessFile que abre el archivo especificado por una instancia de File. Aquí se muestra esta forma: RandomAccessFile(File objetoArchivo, String acceso) throws FileNotFoundException Abre el archivo especificado por objetoArchivo con el modo de acceso pasado en acceso. RandomAccessFile da soporte a dos modos de acceso adicionales. La primera es "rws", y causa que cada cambio a los datos o metadatos de un archivo afecte de inmediato al dispositivo físico. La segunda es "rwd" y causa que cada cambio a los datos de un archivo afecte de inmediato al dispositivo físico. Obtenga atributos de archivos Componentes clave Clases Métodos java.io.File boolean canRead( ) boolean canWrite( ) boolean exists( ) boolean isDirectory( ) boolean isFile( ) boolean isHidden( ) long lastModified( ) long length( ) Las soluciones anteriores ilustran técnicas usadas para leer y escribir archivos. Sin embargo, hay otro aspecto del manejo de archivos que no se relaciona con la manipulación del contenido de un archivo, sino que trata con sus atributos, como su longitud, la hora en que fue modificado por última vez, si es de sólo lectura, etc. Los atributos de archivo pueden ser muy útiles cuando se administran archivos. Por ejemplo, tal vez quiera confirmar que un archivo no es de sólo lectura antes de tratar de escribir en él. O tal vez quiera saber la longitud de un archivo antes de copiarlo, de modo que confirme que cabe en el dispositivo de destino. Para obtener y establecer los atributos asociados con un archivo, usará la clase File. En esta solución se describe el procedimiento empleado para obtener atributos de archivo. En la siguiente solución se muestra cómo pueden establecer varios de ellos. www.fullengineeringbook.net 84 Java: Soluciones de programación Paso a paso Para obtener los atributos asociados con un archivo se requieren estos pasos: 1. Cree un objeto de File que represente el archivo. 2. Si es necesario, confirme que el archivo existe al llamar a exists( ) con la instancia File. 3. Obtenga el atributo o los atributos en que está interesado al llamar a uno o más métodos de File. Análisis File define cuatro constructores. Aquí se muestra el que usaremos: File(String nombre) El nombre del archivo se especifica con nombre, que puede incluir un nombre de ruta completo. Tenga en cuenta dos temas importantes. En primer lugar, la creación de un objeto de File no hace que un archivo se abra, ni implica que un archivo con ese nombre realmente exista. En cambio, crea un objeto que representa un nombre de ruta. En segundo lugar, nombre también puede especificar un directorio. En cuanto a File, un directorio es simplemente un tipo especial de archivo. Para el caso de análisis, en las siguientes descripciones se usa el término "archivo" para aludir a ambos casos, a menos que se indique otra cosa. A menudo querrá confirmar que el archivo representado por un objeto de File realmente exista antes de tratar de obtener información acerca de él. Para ello, use el método exists( ), que se muestra aquí: boolean exists( ) devuelve verdadero si existe el archivo, y falso, si no. Para obtener los atributos de un archivo, usará uno o más de los métodos siguientes: boolean canRead( ) Devuelve verdadero si el archivo existe y puede leerse. boolean canWrite( ) Devuelve verdadero si el archivo existe y puede escribirse. boolean isDirectory( ) Devuelve verdadero si el archivo existe y es un directorio. boolean isFile( ) Devuelve verdadero si el archivo existe y es un archivo, en lugar de un directorio o algún otro objeto soportado por el sistema de archivos. boolean isHidden( ) Devuelve verdadero si el archivo está oculto. long lastModified( ) Devuelve la fecha y hora en que el archivo se modificó por última vez en milisegundos, a partir del 1 de enero de 1970. Este valor puede usarse para construir un objeto de Fecha, por ejemplo, se devuelve 0 si el archivo no existe. long length( ) Devuelve el tamaño del archivo, o cero si el archivo no existe. Ejemplo El siguiente ejemplo crea un método llamado mostrarAtribs( ), que despliega los atributos asociados con un archivo. Para usar el programa, especifique el archivo en la línea de comandos. // Despliega los atributos asociados con un archivo. // // Para usar el programa, especifique el nombre del archivo www.fullengineeringbook.net Capítulo 3: Manejo de archivos // en la línea de comandos. Por ejemplo, para desplegar los // atributos del archivo prueba.pba, use esta línea de comandos: // // java MostrarAtributosArchivo prueba.pba // import java.io.*; import java.util.*; class MostrarAtributosArchivo { // Despliega los atributos de un archivo. public static void mostrarAtribs(String nombre) { File f; // Crea un objeto de archivo para el archivo. f = new File(nombre); // Primero, confirma que el archivo exista. if(!f.exists( )) { System.out.println("No se ha encontrado el archivo."); return; } // Despliega varios atributos de archivo. System.out.println("Atributos de archivo: "); if(f.canRead( )) System.out.println(" De lectura"); if(f.canWrite( )) System.out.println(" De escritura"); if(f.isDirectory( )) System.out.println(" Es un directorio"); if(f.isFile( )) System.out.println(" Es un archivo"); if(f.isHidden( )) System.out.println(" Se encuentra oculto"); System.out.println(" Modificado por \u00a3ltima vez el " + new Date(f.lastModified( ))); System.out.println(" Longitud: " + f.length( )); } // Demuestra el método showAttributes( ). public static void main(String args[ ]) { // Primero se asegura de que se ha especificado un archivo. if(args.length != 1) { System.out.println("Uso: MostrarAtributosArchivo nombrearchivo"); return; } mostrarAtribs(args[0]); } } www.fullengineeringbook.net 85 86 Java: Soluciones de programación Cuando se ejecuta en su propio archivo de origen, MostrarAtributosArchivo produce la siguiente salida: Atributos de archivo: De lectura De escritura Es un archivo Modificado por última vez el Thu Mar 27 19:25:35 CST 2008 Longitud: 6 Opciones Si está usando Java 6 o posterior, y si su plataforma lo soporta, puede determinar si su aplicación ejecutará su archivo al llamar canExecute( ), que se muestra aquí: boolean canExecute( ) Devuelve verdadero si el archivo puede ejecutarse al invocar el programa, y falso si no. Además del constructor File usado por la solución, File proporciona otros tres. Se muestran a continuación: File(String dir, String nombre) File(File dir, String nombre) File(URI uri) En las dos primeras formas, dir especifica un directorio principal y nombre especifica el nombre de un archivo o subdirectorio. En la última forma, uri especifica un objeto URI que describe el archivo. Establezca atributos de archivos Componentes clave Clases Métodos java.io.File boolean boolean boolean boolean exists( ) setLastModified(long nuevaHora) setreadOnly( ) setWritable(boolean puedeEscribirse boolean quien) En la solución anterior se mostró cómo obtener varios atributos de un archivo empleando la clase File. Ésta también le da la capacidad de establecer atributos. En esta solución se muestra el procedimiento. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 87 Paso a paso Para establecer los atributos asociados con un archivo se requieren estos pasos: 1. Cree el objeto de File que representa al archivo. 2. Si es necesario, confirme que el archivo existe al llamar a exists( ) en la instancia de File. 3. Establezca el atributo o los atributos en que está interesado al llamar a uno o más métodos de File. Análisis Los constructores de File y el método exists( ) se describen con detalle en la solución anterior. (Consulte Obtenga atributos de archivo, para conocer más detalles.) Como se mencionó, a menudo querrá confirmar que en realidad existe el archivo representado por un objeto de File antes de que trate de obtener información acerca de él. Esto se hace al llamar a exists( ). Para establecer los atributos de un archivo, usará uno o más de los siguientes métodos. Observe que setWritable( ) requiere Java 6 o posterior. boolean setLastModified(long nuevaHora) Establece la estampa de tiempo del archivo en nuevaHora. Devuelve verdadero si se realiza correctamente. La hora se representa como el número de milisegundos a partir de 1 de enero de 1970. Puede obtener una hora en esta forma al usar la clase Calendar. boolean setReadOnly( ) Hace que el archivo sea de sólo lectura. Devuelve verdadero si se realiza correctamente. boolean setWritable(boolean puedeEscribirse, boolean quien) Establece el atributo de permiso de escritura del archivo. Si puedeEscribirse es true, entonces las operaciones de escritura se habilitan. De otra manera, se niegan. Si quien es true, entonces el cambio sólo se aplica al propietario del archivo. De otra manera, el cambio se aplica de manera general. (Requiere Java 6 o posterior.) Ejemplo En el siguiente ejemplo se muestra cómo establecer los atributos de un archivo. // Establezca atributos de archivo. // // Para usar el programa, especifique el nombre del archivo // en la línea de comandos. Por ejemplo, establezca los atributos // de un archivo prueba.pba, use la siguiente línea de comandos // // java EstablecerAtributosArchivo prueba.pba // import java.io.*; import java.util.*; class EstablecerAtributosArchivo { // muestra el estatus de lectura/escritura de un archivo. static void rwStatus(File f) { www.fullengineeringbook.net 88 Java: Soluciones de programación if(f.canRead( )) System.out.println(" De lectura"); else System.out.println(" No es de lectura"); if(f.canWrite( )) System.out.println(" De escritura"); else System.out.println(" No es de escritura"); } public static void main(String args[ ]) { // Primero, se asegura de que se ha especificado un archivo. if(args.length != 1) { System.out.println("Uso: EstablecerAtributosArchivo nombrearchivo"); return; } File f = new File(args[0]); // Confirma que existe el archivo. if(!f.exists( )) { System.out.println("No se ha encontrado el archivo."); return; } // Despliega el estado de lectura/escritura // y la estampa de tiempo originales. System.out.println("permisos originales de hora y lectura/escritura:"); rwStatus(f); System.out.println(" Modificado por \u00a3ltima vez el " + new Date(f.lastModified( ))); System.out.println( ); // Actualiza la estampa de tiempo. long t = Calendar.getInstance( ).getTimeInMillis( ); if(!f.setLastModified(t)) System.out.println("No se puede establecer la hora."); // Establece el archivo en sólo lectura. if(!f.setReadOnly( )) System.out.println("No puede establecerse como de s\u00a2lo lectura."); System.out.println("Permisos de lectura/escritura y hora modificados:"); rwStatus(f); System.out.println(" Modificado por \u00a3ltima vez el " + new Date(f.lastModified( ))); System.out.println( ); // Devuelve el estatus de lectura/escritura. if(!f.setWritable(true, false)) System.out.println("No puede regresar a lectura/escritura."); System.out.println("Ahora los permisos de lectura/escritura son: "); rwStatus(f); } } www.fullengineeringbook.net Capítulo 3: Manejo de archivos 89 Aquí se muestra la salida: Permisos originales de hora y lectura/escritura: De lectura De escritura Modificado por última vez el Thu Mar 27 19:25:35 CST 2008 Permisos de lectura/escritura y hora modificados: De lectura No es de escritura Modificado por última vez el Thu Mar 27 23:17:44 CST 2008 Ahora los permisos de lectura/escritura son: De lectura De escritura Opciones Además del constructor usado por esta solución, File define otros tres. Consulte Obtenga atributos de archivos para conocer la descripción. Si está usando Java 6 o posterior y si su plataforma lo soporta, puede establecer un atributo de permiso ejecutable de un archivo al llamar a setExecutable( ). Tiene dos formas, que se muestran aquí: boolean setExecutable(boolean puedeEjecu) boolean setExecutable(boolean puedeEjecu, boolean quien) Si puedeEjecu es true, entonces se permiten las operaciones de ejecución. De otra manera, se niegan. La primera forma sólo afecta al propietario del archivo. En la segunda forma, si quien es true, el cambio se aplica sólo al propietario del archivo. Si quien es false, se aplica de manera general. Si está usando Java 6 o posterior, y si su plataforma lo soporta, puede establecer o limpiar el atributo de sólo lectura de un archivo al llamar a setReadable( ). Tiene dos formas, que se muestran aquí: boolean setReadable(boolean puedeLeer) boolean setReadable(boolean puedeLeer, boolean quien) Si puedeLeer es true, entonces se permiten las operaciones de lectura. De otra manera, se niegan. La primera forma afecta sólo al propietario del archivo. En la segunda forma, si quien es true, entonces el cambio sólo se aplica al propietario del archivo. Si es false, se aplica de manera general. Además de establecer los atributos de archivo, File también le permite eliminar un archivo, cambiar su nombre y crear un directorio. Aquí se muestran los métodos que lo hacen: boolean delete( ) Elimina un archivo (o un directorio vacío). Devuelve verdadero si es correcto. boolean mkdir( ) Crea un directorio. Devuelve verdadero si se aplica correctamente. boolean mkdirs( ) Crea toda una ruta de directorio (que incluye todos los directorios principales necesarios). Devuelve verdadero si se aplica correctamente. boolean renameTo(File nuevoNombre) Cambia el nombre del archivo por nuevoNombre. Devuelve verdadero si se aplica correctamente. www.fullengineeringbook.net 90 Java: Soluciones de programación Elabore una lista de un directorio Componentes clave Clases Métodos java.io.File File[ ] listFiles( ) File[ ] listFiles(FileFilter ff) String getName( ) Boolean isDirectory( ) java.io.FileFilter boolean accept(File nombre) Otra tarea común relacionada con archivos consiste en obtener una lista de los archivos dentro de un directorio. Por ejemplo, tal vez quiera obtener una lista de todos los archivos de un directorio para que puedan transmitirse a un sitio de respaldo o para que pueda confirmar que se han instalado apropiadamente todos los archivos de una aplicación. Cualquiera que sea el propósito, puede obtenerse una lista de directorios empleando convenientemente la clase File. Paso a paso Java proporciona diversas maneras de obtener un listado de directorios. Para el método usado en esta solución se requieren estos pasos: 1. Cree un objeto de File que representa el directorio. 2. Confirme que existe el objeto de File y en realidad representa un directorio válido. (En los casos en que se sabe que el objeto de File representa un directorio existente, puede omitirse este paso). 3. Si quiere obtener una lista filtrada de archivos (como las que tienen una extensión de archivo especificada), cree un objeto de FileFilter que describe el patrón que los archivos deben cumplir. 4. Para obtener un listado de directorios, llame a listFiles( ) en el objeto de File. Devuelva una matriz de objetos de File que represente a los archivos en el directorio. Hay tres versiones de listFiles( ). Uno le da todos los archivos; los otros dos le permiten filtrar los archivos. 5. Para obtener el nombre del archivo, llame al método getName( ) de File. 6. Si sólo está interesado en archivos normales y no en directorios, use el método isDirectory( ) para determinar cuáles archivos representan directorios. Análisis Consulte Obtenga atributos de archivos para conocer una descripción de los constructores de File y de los métodos exists( ) e isDirectory( ). Es importante comprender que un listado de directorios sólo puede obtenerse si el objeto de File representa un directorio. Por tanto, a menudo es necesario confirmar que el objeto es, por supuesto, un directorio válido antes de tratar de obtener la lista de archivos. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 91 File define tres versiones de listFiles( ). La primera devuelve una matriz de cadenas que contiene todos los archivos (incluidos los que representan subdirectorios). Por tanto, obtiene una lista no filtrada de archivos. Se muestra aquí: File[ ] listFiles( ) Para obtener una lista de archivos filtrados, puede usar una de dos formas. La usada en esta solución se muestra aquí: File[ ] listFiles(FileFilter ff) Obtiene una lista de sólo los archivos (y directorios) que cumplen el criterio especificado por ff. Dado que la matriz contiene el listado de directorios, puede obtener el nombre de cada archivo al llamar a getName( ), que está definido por File. Aquí se muestra: String getName( ) Se devuelve la representación de cadena del archivo (o directorio). Para restringir la lista de archivos sólo a los que cumplen con ciertos criterios, implemente un FileFilter, que es una interfaz que sólo especifica un método accept( ). Aquí se muestra: boolean accept(File nombre) Este método debe devolver verdadero para archivos que quiera que sean parte del listado de directorios y falso para los que serán excluidos. Dependiendo de la manera en que implemente accept( ), puede usarse para incluir todos los archivos cuyos nombres llenan un patrón general. Por ejemplo, he aquí una implementación simple que acepta todos los archivos de origen de Java: // Un filtro de archivo simple para archivos de origen de Java. class ArchivosJava implements FileFilter { public boolean accept(File f) { if(f.getName( ).endsWith(".java")) return true; return false; } } Para un filtrado más complejo, puede usar expresiones regulares. Con este método, puede crear fácilmente filtros que manejen comodines, busquen coincidencias alternas, ignoren diferencias entre mayúsculas y minúsculas, etc. (Una revisión general de las expresiones regulares se encuentra en el capítulo 2). Ejemplo En el siguiente ejemplo se ilustran las formas filtradas y no filtradas de listFiles( ). Para usar el programa, especifique en la línea de comandos el nombre del directorio del que habrá de elaborarse una lista. El programa despliega primero todos los archivos del directorio especificado. Luego, usa un filtro para desplegar sólo los archivos fuente de Java. // // // // // // // Despliega una lista de todos los archivos y subdirectorios en el directorio especificado en la línea de comandos. Por ejemplo, para desplegar la lista del contenido de un directorio llamado \MisProgramas, use java ListaArchivos \MisProgramas import java.io.*; www.fullengineeringbook.net 92 Java: Soluciones de programación // Un filtro de archivo simple para archivos fuente de Java. class ArchivosJava implements FileFilter { public boolean accept(File f) { if(f.getName( ).endsWith(".java")) return true; return false; } } class ListaArchivos { public static void main(String args[ ]) { // Primero se asegura de que se ha especificado un archivo. if(args.length != 1) { System.out.println("Uso: ListaArchivos nombredir"); return; } File dir = new File(args[0]); // Confirma la existencia. if(!dir.exists( )) { System.out.println(args[0] + " no encontrado."); return; } // Confirma que es un directorio. if(!dir.isDirectory( )) { System.out.println(args[0] + " no es un directorio."); return; } File[ ] listaArchivos; // Obtiene una lista de todos los archivos. listaArchivos = dir.listFiles( ); // Despliega los archivos. System.out.println("Todos los archivos:"); for(File f : listaArchivos) if(!f.isDirectory( )) System.out.println(f.getName( )); // Obtiene una lista de archivos fuente de Java únicamente. // // Empieza por crear un filtro para los archivos .java. ArchivosJava aj = new ArchivosJava( ); // Ahora, pasa el archivo a list( ). listaArchivos = dir.listFiles(aj); // Despliega los archivos filtrados. System.out.println("\nArchivos fuente de Java:"); www.fullengineeringbook.net Capítulo 3: Manejo de archivos 93 for(File f : listaArchivos) if(!f.isDirectory( )) System.out.println(f.getName( )); } } Aquí se muestra una salida de ejemplo: Todos los archivos: VolcarHex.java EscribirBytes.java CopiarArchivoBufer.java MostrarArchivo.java CopiarInvertir.java EscribirCars.java VolcarHex.class EscribirBytes.class CopiarArchivoBufer.class EscribirCars.class CopiarInvertir.class MostrarArchivo.class Archivos fuente de Java: VolcarHex.java EscribirBytes.java CopiarArchivoBufer.java MostrarArchivo.java CopiarInvertir.java EscribirCars.java Ejemplo adicional Las capacidades de las expresiones regulares le permiten crear filtros de archivo muy poderosos que usan complejos filtros con comodines. Las expresiones regulares también le permiten implementar filtros que aceptan una o más opciones. Por ejemplo, puede crear un filtro de archivo que acepta archivos que terminan en ".class" o ".java". He aquí otro ejemplo. Con el uso de expresiones regulares, puede crear un filtro de archivo que acepte archivos que contengan cualquiera de estas dos subcadenas: "bytes" o "cars". Este filtro aceptaría estos nombres de archivo: EscribirBytes.java EscribirCars.java Mediante el uso de expresiones regulares, las capacidades de un filtro de archivo son casi ilimitadas. Una de las maneras más fáciles para usar una expresión regular en un filtro de archivo consiste en usar el método matches( ) de String. Recordará del capítulo 2 que tiene esta forma general: boolean matches(String expReg) Devuelve verdadero si la expresión regular encuentra una coincidencia en la cadena que invoca; de lo contrario, es falso. www.fullengineeringbook.net 94 Java: Soluciones de programación He aquí un filtro de archivo que usa una expresión regular para buscar todos los archivos cuyo nombre contiene la secuencia "cars" o "bytes". Observe que ignora las mayúsculas y minúsculas de las letras // Un filtro de archivo que acepta nombres que incluyen // "bytes" o "cars". class MiFf implements FileFilter { public boolean accept(File f) { if(f.getName( ).matches(".*(?i)(bytes|cars).*")) return true; return false; } } Si se aplica este filtro a la misma dirección usada en el ejemplo anterior, se aceptan los siguientes archivos: EscribirBytes.java EscribirCars.java EscribirBytes.class EscribirCars.class Puede probar este filtro al sustituirlo en el programa de ejemplo anterior. Tal vez quiera experimentar con él un poco, probando diferentes patrones. Encontrará que las expresiones regulares ofrecen una tremenda cantidad de capacidad y control sobre el proceso de filtrado. Opciones Como se acaba de describir, listFiles( ) devuelve la lista de archivos como una matriz de objetos de File. Esta suele ser la forma que usted querrá. Sin embargo, puede obtener una lista que contiene sólo los nombres de los archivos en una matriz de objetos de String. Esto se hace al usar el método list( ), que también está definido por File. Hay dos versiones de list( ). La primera obtiene todos los archivos. Por tanto, obtiene una lista de archivos no filtrados. Se muestra aquí: String[ ] list( ) Para obtener una lista de archivos filtrados, use esta segunda forma de list( ): String[ ] list(FilenameFilter fnf) Obtiene una lista sólo de los archivos que cumplen el criterio especificado por fnf. Observe que la forma filtrada de list( ) usa un FilenameFilter, en lugar de un FileFilter. FilenameFilter es una interfaz que ofrece una manera alterna de construir un filtro: sólo especifica un método, accept( ), que se muestra aquí: boolean accept(File dir, String nombre) En este método, el directorio se pasa a dir y el nombre de archivo a name. Debe devolver verdadero para archivos que coinciden con el nombre de archivo especificado por nombre. De otra manera, devuelve falso. Puede usar FilenameFilter con listFiles( ) para obtener una lista de archivos filtrada. Aquí se muestra esta forma de listFiles( ): File[ ] listFiles(FilenameFilter fnf) www.fullengineeringbook.net Capítulo 3: Manejo de archivos 95 Comprima y descomprima datos Componentes clave Clases Métodos java.util.zip.DeflaterOutputStream void write(int valbyte) void close( ) java.util.zip.InflaterInputStream int read( ) void close( ) Como se mencionó cerca del principio del capítulo, Java proporciona clases de flujo que comprimen y descomprimen automáticamente los datos. Estas clases están empaquetadas en java.util.zip. Cuatro de estas clases se usan para leer y escribir archivos GZIP y ZIP estándares. Sin embargo, también es posible usar el algoritmo de compresión directamente, sin crear uno de estos archivos estándar. Hay una muy buena razón por la que desearía hacer esto: le permite a su aplicación operar de manera directa sobre datos comprimidos. Esto podría resultar especialmente valioso cuando se usan archivos de datos muy grandes. Por ejemplo, una base de datos que contiene un inventario de un vendedor en línea muy grande podría contener varios miles de entradas. Al almacenar esa base de datos en forma comprimida, su tamaño puede reducirse de manera importante. Debido a que los flujos de archivo comprimidos de Java le dan la capacidad de leer y escribir un archivo comprimido directamente, todas las operaciones de archivo pueden tomar lugar sobre el archivo comprimido, sin la necesidad de crear una copia descomprimida. En esta solución se muestra la manera de implementar este esquema. Como verá, el uso de datos comprimidos es muy fácil porque incluye una capa superior casi transparente para el manejo básico de archivos requerido por la aplicación. En la base de la biblioteca de compresión se encuentran las clases Deflater e Inflater. Proporcionan el algoritmo que comprime y descomprime los datos. Como se mencionó cerca del inicio de este capítulo, la implementación predeterminada de estas clases usa la biblioteca de compresión ZLIB. Los flujos de datos comprimidos, definidos por java.util.zip, usan las implementaciones predeterminadas de estas clases. Estas implementaciones predeterminadas son adecuadas para casi todas las tareas de compresión, y por lo general no necesitará interactuar directamente con Deflater o Inflater. En esta solución se usa DeflaterOutputStream para escribir en un archivo de datos comprimidos e InflaterInputStream para leer un archivo de datos comprimido. DeflaterOutputStream se deriva de OutputStream y FilterOutputStream e implementa las interfaces Closeable y Flushable. InflaterInputStream se deriva de InputStream y FilterInputStream e implementa la interfaz Closeable. Paso a paso Para comprimir datos y escribirlos en un archivo se requieren estos pasos: 1. Cree un flujo de archivo que use DeflaterOutputStream. 2. Escriba la salida en la instancia de DeflaterOutputStream. Puede usar uno de los métodos estándar de write( )para escribir los datos. Sin embargo, a menudo un DeflaterOutputStream se envuelve en un DataOutputStream, que le permite escribir convenientemente tipos de datos primitivos, como int y double. En cualquier caso, los datos se comprimirán automáticamente. 3. Cierre el flujo de salida cuando termine de escribir. www.fullengineeringbook.net 96 Java: Soluciones de programación Para leer los datos de un archivo comprimido se requieren estos pasos: 1. Cree un flujo de archivo que use InflaterInputStream. 2. Lea de la instancia de InflaterInputStream. Puede usar uno de los métodos estándar de read( ) para leer los datos. Sin embargo, a menudo InflaterInputStream está envuelto en un DataInputStream, que le permite leer convenientemente tipos de datos primitivos, como int o double. En cualquier caso, los datos se descomprimirán automáticamente. 3. Cierre el flujo de entrada cuando haya terminado de escribir. Análisis DeflaterOutputStream e InflaterInputStream son el núcleo de las capacidades de compresión de archivos de Java. Pueden usarse explícitamente (como se hace en la solución), o implícitamente cuando crea un archivo GZIP o ZIP. DeflaterOutputStream escribe datos en un archivo, comprimiéndolos en el proceso. InflaterInputStream lee los datos de un archivo, descomprimiéndolos en el proceso. DeflaterOutputStream e InflaterInputStream definen tres constructores, cada uno. He aquí los usados en esta solución. DeflaterOutputStream(OutputStream flujoSal) InflaterInputStream(InputStream flujoEnt) Aquí, flujoSal especifica el flujo de salida y flujoEnt especifica el de entrada. Estos constructores usan el compresor y el descompresor predeterminados. Como se mencionó, éstos son objetos de tipo Inflater y Deflater. Proporcionan los algoritmos que realizan la compresión y descompresión real de los datos. Los otros constructores le permiten especificar un compresor o descompresor, y un tamaño de búfer. Sin embargo, el compresor, el descompresor y el tamaño de búfer son adecuados para casi todas las tareas. Una vez que están abiertos los flujos basados en compresión, la compresión y la descompresión ocurren automáticamente cada vez que tiene lugar una operación de escritura o lectura. Por tanto, los datos contenidos en un archivo escrito por DeflaterOutputStream estarán en un formato comprimido. Los datos leídos de un archivo comprimido mediante un InflaterInputStream, estarán en su formato descomprimido (es decir, simple). Esto significa que puede almacenar datos en forma comprimida, pero su programa tendrá acceso transparente a él (como si estuvieran almacenados en un archivo no comprimido). Esta es una de las razones por las que la biblioteca de compresión de Java es tan poderosa. Para escribir datos, puede usar cualquier de los métodos estándar de write( ) definidos por OutputStream. Para leer datos, puede usar cualquiera de los métodos estándar de read( ) definidos por InputStream. (Estos se describen en Escriba bytes en un archivo y Lea bytes de un archivo.) Sin embargo, a menudo es mejor envolver DeflaterOutputStream en un DataOutputStream y envolver InflaterInputStream en un DataInputStream. Al hacerlo así se tiene acceso a métodos como writeInt( ), writeDouble( ), readInt( ) y readDouble( ), que le permiten leer y escribir datos primitivos de manera conveniente. Ejemplo En el siguiente ejemplo se muestra cómo crear un archivo de datos comprimido y luego leer los datos en el archivo. El archivo de datos es una colección de valores double. Sin embargo, el archivo empieza con un entero que contiene una cuenta de double en el archivo. Tome en cuenta que DeflaterOutputStream está envuelto en un DataOutputStream. InflaterInputStream está envuelto en un DataInputStream. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 97 Esto permite que valores de tipos primitivos, como int o double, se escriban de manera conveniente en una forma comprimida y que luego se vuelvan a leer. El programa crea el archivo de datos al escribir una matriz de seis valores double en un archivo llamado datos.cmprs. En primer lugar, escribe una cuenta del número de valores que seguirá al llamar a writeInt( ). La cuenta es seis, en el ejemplo. Luego escribe los valores double al llamar a writeDouble( ). Debido a que se usa DeflaterOutputStream, los datos se comprimen automáticamente antes de almacenarse en el archivo. El programa lee después los valores. Hace esto al obtener primero la cuenta al llamar a readInt( ). Luego lee ese número de valores al llamar a readDouble( ). En el proceso, promedia los valores. Lo que es importante comprender es que el archivo se comprime y descomprime "al vuelo". En ningún momento existe una versión descomprimida del archivo. Es interesante observar que el archivo comprimido que crea el programa tiene 36 bytes de largo. Sin compresión, el archivo tendría 52 bytes. // // // // // // // // // Crea un archivo comprimido de datos al usar un DeflaterOutputStream y luego leer los datos con un InflaterInputStream. Este programa usa la biblioteca de compresión predeterminada de Java, que es ZLIB. El archivo comprimido creado por este programa no está en un formato específico, como ZIP or GZIP. Simplemente contiene una versión comprimida de los datos. import java.io.*; import java.util.zip.*; class DemoCompresion { public static void main(String args[ ]) { DataOutputStream fout; DataInputStream fin; double datos[ ] = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 }; // Abre el archivo de salida. try { fout = new DataOutputStream( new DeflaterOutputStream( new FileOutputStream("datos.cmprs"))); } catch(FileNotFoundException exc) { System.out.println("Error al abrir el archivo de salida"); return; } // Comprime los datos usando ZLIB. try { // Sólo escribe los datos normalmente. El // DeflaterOutputStream los descomprimirá // automáticamente. www.fullengineeringbook.net 98 Java: Soluciones de programación // Primero, escribe el tamaño de los datos. fout.writeInt(datos.length); // Ahora, escribe los datos. for(double d : datos) fout.writeDouble(d); } catch(IOException exc) { System.out.println("Error en el archivo comprimido"); } try { fout.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de salida"); } // Ahora, abre datos.cmprs para entrada. No es necesario // crear una copia descomprimida del archivo porque la // descompresión se maneja al vuelo, con InflaterInputStream. // Así, la descompresión es automática y transparente. try { fin = new DataInputStream( new InflaterInputStream( new FileInputStream("datos.cmprs"))); } catch(FileNotFoundException exc) { System.out.println("No se ha encontrado el archivo de entrada"); return; } // Descomprime el archivo al vuelo. try { // Primero, recupera la cantidad de datos // contenidos en el archivo. int num = fin.readInt( ); double prom = 0.0; double d; System.out.print("Datos: "); // Ahora, lee los datos. La descompresión es automática. for(int i=0; i < num; i++) { d = fin.readDouble( ); prom += d; System.out.print(d + " "); } System.out.println("\nEl promedio es " + prom / num); www.fullengineeringbook.net Capítulo 3: Manejo de archivos 99 } catch(IOException exc) { System.out.println("Error al leer el archivo de entrada"); } try { fin.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de entrada"); } } } Aquí se muestra la salida: Datos: 1.1 2.2 3.3 4.4 5.5 6.6 El promedio es 3.85 Opciones Como se mencionó, tanto DeflaterOutputStream como InflaterInputStream ofrecen constructores que le permiten especificar el compresor, el descompresor o el tamaño del búfer. La biblioteca de compresión también soporta sumas de verificación a través de las siguientes clases: Alder32 y CRC32. Puede crear flujos que usan una suma de verificación con estas clases: CheckedInputStream y CheckedOutputStream. Aunque usar DeflaterOutputStream e InflaterInputStream directamente es aceptable, a menudo querrá usar sus subclases: GZIPInputStream GZIPOutputStream ZipInputStream ZipOutputStream Estas clases crean archivos comprimidos en formato GZIP o ZIP. La ventaja es que sus archivos de datos estarán en un formato que las herramientas estándar pueden comprender. Sin embargo, si eso no otorga beneficios a su aplicación, entonces el uso directo de DeflaterOutputStream e InflaterInputStream es un poco más eficiente. (En las siguientes soluciones se muestra cómo crear, comprimir y descomprimir un archivo ZIP). www.fullengineeringbook.net 100 Java: Soluciones de programación Cree un archivo ZIP Componentes clave Clases Métodos java.util.zip.ZipInputStream void closeEntry( ) ZipEntry getNextEntry( ) java.util.zip.ZipOutputStream void closeEntry( ) void putNextEntry(ZipEntry ze) void write(int val) java.util.zip.ZipEntry long getCompressedSize( ) long GetSize( ) Hay dos formatos de archivo comprimido muy populares: GZIP y ZIP. Java proporciona soporte a ambos. La creación de un archivo GZIP es fácil: simplemente cree un GZIPOutputStream y luego escriba en él. Leer un archivo GZIP es igualmente fácil: sólo cree un GZIPInputStream y luego léalo. Sin embargo, la situación es un poco más complicada si quiere crear un archivo ZIP. En esta solución se crea el procedimiento básico. Antes de empezar, es importante tomar en cuenta que los archivos ZIP pueden ser muy complejos. En esta solución se muestra cómo crear un esqueleto básico. Si estará trabajando ampliamente con archivos ZIP, necesitará estudiar de cerca las especificaciones tanto de java.util. zip como del propio archivo ZIP. En general, un archivo ZIP puede contener uno o más archivos comprimidos. Cada archivo está asociado con una entrada que describe el archivo. Se trata de un objeto de tipo ZipEntry. Este tipo de objetos identifica a cada archivo dentro del archivo ZIP. Por tanto, para comprimir un archivo, primero escribirá su ZipEntry y luego sus datos. Paso a paso La creación de un archivo ZIP que contenga uno o más archivos comprimidos incluye estos pasos: 1. Cree el archivo ZIP al abrir un ZipOutputStream. Cualquier dato escrito en este flujo se comprimirá automáticamente. 2. Abra el archivo que se comprimirá. Puede usar cualquier flujo de archivo apropiado, como FileInputStream envuelto en un BufferedInputStream. 3. Cree una ZipEntry para representar el archivo que se está comprimiendo. A menudo el nombre de este archivo se vuelve el nombre de la entrada. Escriba la entrada en la instancia ZipOutputStream al llamar a putNextEntry( ). 4. Escriba el contenido del archivo de entrada en el ZipOutputStream. Los datos se comprimirán automáticamente. 5. Cierra la ZipEntry del archivo de entrada al llamar a closeEntry( ). 6. Por lo general, querrá reportar el progreso de la compresión, incluido el tamaño de reducción del archivo. Para ayudarle en esto, ZipEntry proporciona getSize( ), que obtiene el tamaño sin comprimir del archivo, y getCompressedSize( ), que obtiene el tamaño comprimido. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 101 7. Repita del paso 3 al 5 hasta que se hayan escrito todos los archivos que desea almacenar en el archivo ZIP. 8. Cierre los archivos. Análisis Para crear un archivo ZIP, usará una instancia de ZipOutputStream. Los datos escritos en un ZipOutputStream se comprimen automáticamente en un archivo ZIP. ZipOutputStream se deriva de DeflaterOutputStream e implementa las interfaces Closeable y Flushable. Sólo define al constructor, que se muestra aquí: ZipOutputStream(OutputStream flujo) El flujo que recibirá los datos comprimidos se especifica con flujo. Esto normalmente será una instancia de FileOutputStream. Antes de que pueda comprimir un archivo, debe crear una instancia de ZipEntry que representa el archivo y escribe la entrada en el archivo ZIP. Por tanto, cada archivo almacenado en un archivo ZIP está asociado con una instancia de ZipEntry, que define dos constructores. El que usaremos es ZipEntry(String nombreEntrada) Aquí, nombreEntrada especifica el nombre de la entrada. Por lo general, será el nombre del archivo que se está almacenando. Para escribir la ZipEntry en el archivo, llame a putNextEntry( ) en la instancia de ZipOutputStream. Aquí se muestra: void putNextEntry(ZipEntry encabezado) throws IOException La entrada que habrá de escribirse se pasa en ze. Se lanza una IOException si ocurre un error de E/S. También se lanzará una ZipException en el caso de un error de formato. Debido a que se trata de una subclase de IOException, a menudo basta con el manejo de ésta última. Sin embargo, querrá manejar ambas excepciones individualmente cuando se necesita mayor control. Después de que se ha escrito ZipEntry, puede escribir los datos comprimidos en el archivo. Esto se logra con sólo llamar a uno de los métodos de write( ) soportados por ZipOutputStream. El que se usa aquí es void write(int val) throws IOException Por supuesto, esto es simplemente una sobreescritura del mismo método definido por OutputStream. Esta versión lanza una IOException si ocurre un error de E/S. Cuando se escriben datos en la instancia de ZipOutputStream, se comprimen automáticamente. Después de que se ha escrito el archivo, debe cerrar su entrada al llamar a closeEntry( ), que se muestra aquí: void closeEntry( ) throws IOException Se lanza una IOException si ocurre un error de E/S. También lanzará una ZipException en el caso de un error de formato. www.fullengineeringbook.net 102 Java: Soluciones de programación Usted determina la efectividad de la compresión al llamar a getSize( ) y getCompressedSize( ) en la ZipEntry asociada con un archivo. Aquí se muestran: long getSize( ) long getCompressedSize( ) El tamaño sin comprimir es devuelto por getSize( ); el comprimido, por getCompressedSize( ). Ambos métodos devuelven –1 si el tamaño no está disponible. Debe cerrarse la ZipEntry asociada con el archivo antes de que se llame a los métodos. Cuando haya terminado de comprimir todos los archivos que se almacenarán en el archivo ZIP, cierre todos los flujos. Esto incluye el ZipOutputStream. Tenga en cuenta que la versión de close( ) implementada por ZipOutputStream lanzará una IOException si ocurre un error de E/S y una ZipException en el caso de un error de formato. Ejemplo En el siguiente ejemplo se pone la solución en acción. Puede usarse para crear un archivo ZIP que contenga la forma comprimida de uno o más archivos. Para usar el programa, especifique el nombre del archivo ZIP, luego la lista de archivos que habrán de comprimirse. Por ejemplo, para comprimir los archivos ejemploA.dat y ejemploB.dat en un archivo llamado ejemplos.zip, use la siguiente línea de comandos: java Zip ejemplos.zip ejemploA.dat ejemploB.dat Mientras se ejecuta el programa, se reporta el avance. Esto incluye el nombre del archivo que se está comprimiendo, su tamaño original y su tamaño una vez comprimido. Como suele ser el caso, el programa usa los nombres de los archivos como nombres de las entradas de ZipEntry. Sin embargo, hay un punto importante que debe comprenderse acerca del programa. No almacena ninguna información de ruta de directorio para los archivos. Si un nombre de archivo tiene una ruta asociada con él, se elimina cuando se crea la ZipEntry. (Observe que se usa File.separator para especificar el carácter separador. Esto se debe a que difiere entre Unix y Windows). Como la mayoría de los lectores sabe, las herramientas ZIP más populares le dan la opción de incluir nombre de ruta o ignorarlos. Sin embargo, esto se le deja como ejercicio. // // // // // // // // // // // // // // // // // Crea un archivo ZIP simple. Para usar este programa, especifique el nombre del archivo que recibirá los datos comprimidos y uno o más archivos de origen que contiene los datos que habrán de comprimirse. Este programa no retiene ninguna información de ruta de directorio sobre los archivos que habrán de comprimirse. Por ejemplo, para comprimir los archivos ejemploA.dat y ejemploB.dat en un archivo llamado ejemplos.zip, use la siguiente línea de comandos: java Zip ejemplos.zip ejemploA.dat ejemploB.dat www.fullengineeringbook.net Capítulo 3: Manejo de archivos import java.io.*; import java.util.zip.*; class Zip { // Elimina cualquier información de ruta de un // nombre de archivo. static String elimRuta(String nombreAr) { int pos = nombreAr.lastIndexOf(File.separatorChar); if(pos > –1) nombreAr = nombreAr.substring(pos+1); return nombreAr; } public static void main(String args[ ]) { BufferedInputStream fin; ZipOutputStream fout; // Primero se asegura de que se hayan especificado // los archivos. if(args.length < 2) { System.out.println("Uso: Zip <listaarchivos>"); return; } // Abre el archivo de salida. try { fout = new ZipOutputStream( new BufferedOutputStream( new FileOutputStream(args[0]))); } catch(FileNotFoundException exc) { System.out.println("Error al abrir el archivo de salida"); return; } for(int n=1; n < args.length; n++) { // Abre el archivo de entrada. try { fin = new BufferedInputStream( new FileInputStream(args[n])); } catch(FileNotFoundException exc) { System.out.println("No se ha encontrado el archivo de entrada"); // Cierra el archivo de salida abierto. try { fout.close( ); } catch(ZipException exc2) { System.out.println("Archivo ZIP no v\u00a0lido"); } catch(IOException exc2) { System.out.println("Error al cerrar el archivo de salida"); } return; } www.fullengineeringbook.net 103 104 Java: Soluciones de programación // Crea la siguiente ZipEntry. En este programa, // no se retiene información de ruta de directorios, // así que se elimina primero cualquier ruta asociada // con el nombre de archivo al llamar a elimRuta( ). ZipEntry ze = new ZipEntry(elimRuta(args[n])); // Comprime el siguiente archivo. try { fout.putNextEntry(ze); int i; do { i = fin.read( ); if(i != –1) fout.write(i); } while(i != –1); fout.closeEntry( ); } catch(ZipException exc) { System.out.println("Archivo ZIP no v\u00a0lido"); } catch(IOException exc) { System.out.println("Error de archivo de salida"); } try { fin.close( ); // Reporta el progreso y el tamaño de la reducción. System.out.println("Comprimiendo " + args[n]); System.out.println(" Tama\u00a4o original: " + ze.getSize( ) + " Tama\u00a4o comprimido: " + ze.getCompressedSize( ) + "\n"); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de entrada"); } } try { fout.close( ); } catch(ZipException exc2) { System.out.println("Archivo ZIP no v\u00a0lido"); } catch(IOException exc) { System.out.println("Error al cerrar el archivo ZIP"); } } } He aquí una ejecución de ejemplo: Comprimiendo VolcarHex.java Tamaño original: 1384 Tamaño comprimido: 612 Comprimiendo EscribirBytes.java Tamaño original: 895 Tamaño comprimido: 428 www.fullengineeringbook.net Capítulo 3: Manejo de archivos 105 Opciones La clase ZipEntry le da la capacidad de establecer u obtener el valor de varios atributos relacionados con una entrada de archivo ZIP. Puede obtener el nombre de la entrada al llamar a getName( ); puede establecerlo al llamar a setName( ). Puede establecer la hora de creación de la entrada al llamar a setTime( ); puede obtenerla al llamar a getTime( ). Como opción predeterminada, se usa la hora actual del sistema. Hay un método que podría resultar muy útil: isDirectory( ). Devuelve verdadero si la entrada representa un directorio. Sin embargo, hay un pequeño problema con este método, porque sólo devuelve verdadero si el nombre de la entrada termina con /. (Esto es para seguir la especificación del archivo ZIP). Sin embargo, como la mayoría de los lectores sabe, los entornos Windows usan \ como separador de ruta. Por tanto, isDirectory( ) no funciona apropiadamente cuando se le da una ruta de Windows. Hay maneras obvias de evitar esta limitación, como sustituir cada \ con / cuando se crea el nombre de la entrada. Puede experimentar con esta y otras soluciones si quiere almacenar información de directorios. Lea bytes de un archivo Componentes clave Clases Métodos java.util.zip.ZipEntry long getCompressedSize( ) long getSize( ) String getName( ) java.util.zip.ZipFile void close( ) Enumeration<? Extends ZipEntry> entries( ) InputStream getInputStream(ZipEntry ze) En la solución anterior se mostraron los pasos necesarios para crear un archivo ZIP. En esta solución se describe la manera de descomprimir un archivo ZIP. Hay dos métodos generales que puede usar para descomprimir un archivo ZIP. El primero usa ZipInputStream para obtener cada entrada en el archivo ZIP. Aunque no hay nada malo en esto, es necesario manejar manualmente el proceso. El segundo método usa ZipFile para afinar el proceso. Este es el método que se emplea en esta solución. Paso a paso La descompresión de un archivo ZIP incluye los siguientes pasos: 1. 2. Abra el archivo ZIP al crear una instancia de ZipFile. Obtenga una enumeración de las entradas del archivo ZIP al llamar a entries( ) en la instancia de ZipFile. www.fullengineeringbook.net 106 Java: Soluciones de programación 3. Recorra en ciclo la enumeración de las entradas, descomprimiendo cada entrada, por turno, como se describe en los pasos siguientes. 4. Obtenga el flujo de una entrada al llamar a getInputStream( ) en la entrada actual. 5. Obtenga el nombre de la entrada al llamar a getName( ) y úsela como nombre del flujo de salida que recibirá el archivo descomprimido. 6. Para descomprimir la entrada, copie bytes del flujo de entrada al de salida. La descompresión es automática. 7. Por lo general, querrá reportar el progreso de la descompresión, incluido el tamaño de los archivos comprimido y expandido. Para hacer esto puede usar métodos proporcionados por ZipEntry. Para obtener el tamaño descomprimido de un archivo llame a getSize( ). Para obtener el tamaño comprimido, llame a getCompressedSize( ). 8. Cierre los flujos de entrada y de salida. 9. Repita del paso 4 al 7, hasta que se hayan descomprimido todas las entradas. 10. Cierre la instancia de ZipFile. Análisis La manera más fácil de descomprimir un archivo ZIP consiste en usar ZipFile, porque afina el proceso de encontrar y leer cada entrada del archivo. Proporciona tres constructores. El usado aquí es: ZipFile(String nombreArchivoZip) throws IOException El nombre del archivo ZIP que se descomprimirá se especifica con nombreArchivoZip. Se lanza una IOException si ocurre un error de E/S. Se lanza una ZipException si se especifica un archivo ZIP que no es válido. ZipException es una subclase de IOException. Por tanto, al capturar una IOException se manejarán ambas excepciones. Sin embargo, para un manejo de errores más preciso, es necesario manejar ambas excepciones individualmente. Puede obtenerse una enumeración de las entradas en el archivo ZIP al llamar a entries( ) en la instancia de ZipFile. Aquí se muestra: Enumeration<? Extends ZipEntry> entries( ) Empleando la enumeración devuelta por entries( ) puede recorrer en ciclo las entradas del archivo, descomprimiendo cada una por turnos. (También omite una entrada, si lo desea. No hay una regla que diga que debe descomprimir todas las entradas de un archivo ZIP). Para obtener un flujo de entrada, llame a getInputStream( ) en la instancia de ZipFile. Aquí se muestra: InputStream getInputStream(ZipEntry ze) throws IOException El flujo leerá de la entrada pasada en ze. Se lanza una IOException si ocurre un error de E/S. Se lanza una ZipException si la entrada está en un formato inadecuado. Puede obtener el nombre de la entrada al llamar a getName( ) en la instancia de ZipEntry. Aquí se muestra: String getName( ) Puede usar entonces el nombre de la entrada como nombre del archivo de salida restaurado (es decir, descomprimido). www.fullengineeringbook.net Capítulo 3: Manejo de archivos 107 Una vez que haya descomprimido todas las entradas del archivo, debe cerrar la instancia de ZipFile al llamar a close( ), como se muestra aquí: void close( ) throws IOException Además de cerrar el archivo ZIP, también cerrará cualquier flujo abierto por llamadas a getInputStream( ). (Sin embargo, prefiero no depender de esto. En cambio, prefiero cerrar cada flujo de entrada por separado, a medida que avanza la descompresión, para no tener varios flujos abiertos, pero sin usar, con recursos asignados). Los tamaños de las entradas comprimida y descomprimida pueden obtenerse al llamar a getCompressedSize( ) y getSize( ), respectivamente. Estos métodos se describieron en la solución anterior. Puede usar esta información para proporcionar retroalimentación acerca del progreso de la descompresión. Ejemplo En el siguiente ejemplo se descomprime un archivo ZIP. Reconocerá un nombre de ruta completo para la entrada, pero no creará la ruta del directorio, si no existe. Se trata de una capacidad que puede agregar, si lo desea. // // // // // // // // // // // // // // Descomprime un archivo ZIP. Para usar este programa, especifique el nombre del archivo comprimido. Por ejemplo, para descomprimir un archivo llamado ejemplo.zip use la siguiente línea de comandos: java Unzip ejemplo.zip Nota: este programa descomprimirá una entrada que incluye información de ruta de directorios, pero no creará la ruta del directorio, si aún no existe. import java.io.*; import java.util.zip.*; import java.util.*; class Unzip { public static void main(String args[ ]) { BufferedInputStream fin; BufferedOutputStream fout; ZipFile zf; // Primero se asegura de que se ha especificado un // archivo de entrada. if(args.length != 1) { System.out.println("Uso: Unzip nombre"); return; } www.fullengineeringbook.net 108 Java: Soluciones de programación // Abre el archivo zip. try { zf = new ZipFile(args[0]); } catch(ZipException exc) { System.out.println("Archivo ZIP no v\u00a0lido"); return; } catch(IOException exc) { System.out.println("Error al abrir el archivo ZIP"); return; } // Obtiene una enumeración de las entradas en el archivo. Enumeration<? extends ZipEntry> archivos = zf.entries( ); // Descomprime cada entrada. while(archivos.hasMoreElements( )) { ZipEntry ze = archivos.nextElement( ); System.out.println("Descomprimiendo " + ze.getName( )); System.out.println(" Tama\u00a4o comprimido: " + ze.getCompressedSize( ) + " Tama\u00a4o expandido: " + ze.getSize( ) + "\n"); // Abre el flujo en la entrada especificada. try { fin = new BufferedInputStream(zf.getInputStream(ze)); } catch(ZipException exc) { System.out.println("Archivo ZIP no v\u00a0lido"); break; } catch(IOException exc) { System.out.println("Error al abrir la entrada"); break; } // Abre el archivo de salida. Usa el nombre proporcionado // por la entrada. try { fout = new BufferedOutputStream( new FileOutputStream(ze.getName( ))); } catch(FileNotFoundException exc) { System.out.println("No se puede crear el archivo de salida"); // Cierra el flujo de entrada abierto. try { fin.close( ); } catch(IOException exc2) { System.out.println("Error al cerrar el archivo ZIP de entrada"); } break; } www.fullengineeringbook.net Capítulo 3: Manejo de archivos // Descomprime la entrada. try { int i; do { i = fin.read( ); if(i != –1) fout.write(i); } while(i != –1); } catch(IOException exc) { System.out.println("Error de archivo mientras se descomprime"); } // Cierra el archivo de salida para la entrada actual. try { fout.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo de salida"); } // Cierra el flujo para la entrada actual. try { fin.close( ); } catch(IOException exc) { System.out.println("Error al cerrar la entrada"); } } // Cierra el archivo Zip. try { zf.close( ); } catch(IOException exc) { System.out.println("Error al cerrar el archivo Zip"); } } } Aquí se muestra la salida de ejemplo: Descomprimiendo VolcarHex.java Tamaño comprimido: 612 Tamaño expandido: 1384 Descomprimiendo EscribirBytes.java Tamaño comprimido: 428 Tamaño expandido: 895 Opciones ZipFile proporciona dos constructores adicionales, que se muestran aquí: ZipFile(File f) throws ZipException, IOException ZipFile(File f, int como) throws IOException Cada uno abre el archivo ZIP especificado por f. El valor de cómo debe ser ZipFile.OPEN_READ o ZipFile.OPEN_READ | ZipFile.OPEN_DELETE www.fullengineeringbook.net 109 110 Java: Soluciones de programación Cuando se especifica OPEN_DELETE, habrá de eliminarse el archivo ZIP. Sin embargo, aún podrá leer el archivo a través de la instancia de ZipFile abierta. Esta opción le permite borrar automáticamente un archivo ZIP después de que se ha descomprimido. Por ejemplo, si sustituye esta línea en el ejemplo, eliminará el archivo ZIP después de descomprimirlo: zf = new ZipFile(new File(args[0]), ZipFile.OPEN_READ | ZipFile.OPEN_DELETE); Por supuesto, debe usar esta opción con cuidado porque borra el archivo ZIP, lo que significa que no puede usarse nuevamente más adelante. Y un tema adicional: el segundo constructor también puede lanzar una ZipException. Si quiere extraer una entrada específica de un archivo ZIP, puede llamar a getEntry( ) en la instancia de ZipFile. Aquí se muestra cómo: ZipEntry getEntry(String nombreent) El nombre de la entrada se pasa en nombreent. Se devuelve una ZipEntry para la entrada. Si ésta no se puede encontrar, se devuelve null. Serialice objetos Componentes clave Clases Métodos java.io.ObjectInputStream void close( ) Object readObject( ) java.io.ObjectOutputStream void close( ) void writeObject(Object objetivo) Serializable Además de bytes, caracteres y tipos primitivos de Java, también puede escribir objetos en un archivo. Una vez almacenados, estos objetos pueden leerse y restaurarse. Al proceso se le denomina serialización, la cual se utiliza por diferentes propósitos, incluidos el almacenamiento de datos que consta de objetos e invocación a métodos remotos (RMI, Remote Method Invocation). Debido a que un objeto puede contener referencias a otros objetos, la serialización de objetos puede incluir un proceso muy sofisticado. Por fortuna, java.io proporciona las clases ObjectOutputStream y ObjectInputStream para manejar esta tarea. En esta solución se describen los procedimientos básicos necesarios para guardar y restaurar un objeto. ObjectOutputStream extiende OutputStream e implementa la interfaz ObjectOutput, que extiende la interfaz DataOutput. ObjectOutput agrega el método writeObject( ), que escribe objetos en un flujo. ObjectOutputStream también implementa las interfaces Closeable y Flushable, junto con ObjectStreamConstants. www.fullengineeringbook.net Capítulo 3: Manejo de archivos 111 ObjectInputStream extiende InputStream e implementa la interfaz ObjectInput, que extiende la interfaz DataInput. ObjectInput agrega el método readObject( ), que lee objetos de un flujo. ObjectInputStream también implementa las interfaces Closeable y ObjectStreamConstants. Con el fin de que un objeto se serialice, su clase debe implementar la interfaz Serializable. Ésta no define miembros. Simplemente se usa para indicar que una clase puede serializarse. Si una clase es serializable, todas sus clases también lo son. Sin embargo, las variables declaradas como transient no se guardan en las partes de serialización. Además, las variables static no se guardan. Por tanto, la serialización guarda el estado actual del objeto. Un tema adicional: la solución que se muestra aquí serializa un objeto usando el mecanismo predeterminado proporcionado por ObjectInputStream y ObjectOutputStream. Es posible tomar control manual del proceso, pero esas técnicas están más allá del alcance de este libro. Paso a paso Guardar un objeto en un archivo incluye estos pasos: 1. Asegúrese de que el objeto que quiere guardar implementa la interfaz Serializable. 2. Abra el archivo al crear un objeto ObjectOutputStream. 3. Escriba el objeto en el archivo al llamar a writeObject( ). 4. Cierre el flujo cuando haya terminado. Leer un objeto de un archivo requiere estos pasos: 1. Abra el archivo al crear un objeto ObjectInputStream. 2. Lea el objeto del archivo al llamar a readObject( ). Debe cambiar el objeto al tipo que se está leyendo. 3. Cierre el flujo cuando haya terminado. Análisis La clase ObjectOutputStream se usa para escribir objetos en un flujo. Aquí se muestra el constructor público para esta clase: ObjectOutputStream(OutputStream flujo) throws IOException El argumento flujo es el flujo de salida en que se escribirán los objetos serializados. Lanza una IOException si el archivo no puede abrirse. Una vez que se ha abierto el flujo de salida, puede escribir un objeto en él al llamar a writeObject( ), mostrado aquí: final void writeObject(Object obj) throws IOException El objeto que habrá de escribirse se pasa a obj. Una IOException se lanza si ocurre un error mientras se escribe el objeto. Si trata de escribir un objeto que no implementa Serializable, se lanza una NotSerializableException. También es posible una InvalidClassException. www.fullengineeringbook.net 112 Java: Soluciones de programación La clase ObjectInputStream se usa para leer objetos de un flujo. Aquí se muestra su constructor público: ObjectInputStream(InputStream flujo) throws IOException El flujo del que se leerán los objetos se especifica con flujo. Lanza una IOException si no puede abrirse el archivo. Una vez que queda abierto el flujo de entrada, puede escribir un objeto en él al llamar a readObject( ), que se muestra aquí: final Object readObject(Object obj) throws IOException, ClassNotFoundException Devuelve el siguiente objeto del archivo. Se lanza una IOException si ocurre un error mientras se lee el objeto. Si no puede encontrarse la clase del objeto, se lanza una ClassNotFoundException. También son posibles otras excepciones. Cuando haya terminado con una ObjectInputStream o una ObjectOutputStream, debe cerrarla al llamar a close( ). Es el mismo para ambas clases y se muestra aquí: void close( ) throws IOException Se lanza una IOException si ocurre un error mientras se cierra el archivo. Ejemplo A continuación se muestra un ejemplo de serialización. Crea una clase llamada MiClase que contiene tres campos: una cadena, una matriz y un objeto de File. Luego crea dos objetos de MiClase, despliega su contenido y los guarda en un archivo llamado objetivo.dat. Luego lee los objetos de nuevo y despliega su contenido. Como lo muestra la salida, el contenido de los objetos reconstruidos son los mismos que los del original. // Demuestra la serialización de objetos. import java.io.*; // Una clase serializable simple. class MiClase implements Serializable { String cad; double[ ] vals; File na; public MiClase(String c, double[ ] nums, String nombrearch) { cad = c; vals = nums; na = new File(nombrearch); } public String toString( ) { String datos = " cad: " + cad + "\n for(double d : vals) vals: "; datos += d + " "; www.fullengineeringbook.net Capítulo 3: datos += "\n Manejo de archivos na: " + na.getName( ); return datos; } } public class DemoSerial { public static void main(String args[ ]) { double v[ ] = { 1.1, 2.2, 3.3 }; double v2[ ] = { 9.0, 8.0, 7.7 }; // Crea dos objetos de MiClase. MiClase obj1 = new MiClase("Esto es una prueba", v, "Prueba.txt"); MiClase obj2 = new MiClase("Alfa Beta Gama", v2, "Ejemplo.dat"); // Abra el archivo de salida. ObjectOutputStream fout; try { fout = new ObjectOutputStream(new FileOutputStream("obj.dat")); } catch(IOException exc) { System.out.println("Error al abrir el archivo de salida"); return; } // Escribe objetos en un archivo. try { System.out.println("Escribiendo objetos en un archivo."); System.out.println("obj1:\n" + obj1); fout.writeObject(obj1); System.out.println("obj2:\n" + obj2); fout.writeObject(obj2); } catch(IOException exc) { System.out.println("Error al escribir objeto"); } try { fout.close( ); } catch(IOException exc) { System.out.println("Error al cerrar archivo de salida"); return; } // Lee el objeto del archivo. ObjectInputStream fin; // Abre el archivo de salida. try { www.fullengineeringbook.net 113 114 Java: Soluciones de programación fin = new ObjectInputStream(new FileInputStream("obj.dat")); } catch(IOException exc) { System.out.println("Error al abrir archivo de entrada"); return; } System.out.println("\nLeyendo objetos del archivo."); try { MiClase inputObj; inputObj = (MiClase) fin.readObject( ); System.out.println("Primer objeto:\n" + inputObj); inputObj = (MiClase) fin.readObject( ); System.out.println("Segundo objeto:\n" + inputObj); } catch(IOException exc) { System.out.println("Error al leer datos de objeto"); } catch(ClassNotFoundException exc) { System.out.println("Definici\u00a2n de clase no encontrada"); } try { fin.close( ); } catch(IOException exc) { System.out.println("Error al cerrar archivo de entrada"); return; } } } Aquí se muestra la salida: Escribiendo objetos en un archivo. obj1: cad: Esto es una prueba vals: 1.1 2.2 3.3 na: Prueba.txt obj2: cad: Alfa Beta Gama vals: 9.0 8.0 7.7 na: Ejemplo.dat Leyendo objetos del archivo. Primer objeto: cad: Esto es una prueba vals: 1.1 2.2 3.3 na: Prueba.txt Segundo objeto: cad: Alfa Beta Gama vals: 9.0 8.0 7.7 na: Ejemplo.dat www.fullengineeringbook.net Capítulo 3: Manejo de archivos 115 Opciones Los miembros de una clase que no quiere almacenar pueden modificarse con el especificador transient. Esto es útil cuando una variable de instancia contiene un valor que no es importante para el estado del objeto. Al no guardar campos innecesarios, el tamaño del archivo se reduce. Puede confirmar esto al hacer na transitorio en el ejemplo. Después de esto, na no se guardará cuando se guarde el objeto de MiClase. Por tanto, no se reconstruirá y su valor será nulo después de la llamada a readObject( ). Esto dará como resultado que se lance una NullPointerException cuando toString( ) trate de acceder a ella. Varios de los métodos de entrada proporcionados por ObjectInputStream lanzan un EOFException cuando se encuentra el final del archivo mientras se lee un objeto. EOFException es una subclase de IOException. Por tanto, si su código necesita discernir entre una condición EOF y otros tipos de errores de E/S, entonces querrá capturar EOFException explícitamente cuando lea objetos. Puede adquirir cierto grado de control manual sobre la serialización de objetos al implementar la interfaz Externalizable. Especifica los métodos readExternal( ) y writeExternal( ), que implementará para leer y escribir objetos. www.fullengineeringbook.net www.fullengineeringbook.net 4 CAPÍTULO Formato de datos S i está desplegando la hora y la fecha, trabajando con valores monetarios o simplemente desea limitar el número de dígitos decimales, la formación de los datos de una manera específica, legible para los seres humanos es una parte importante de muchos programas. También es un área de la programación que plantea muchas preguntas del tipo "¿Cómo se hace?". Una razón para esto es el tamaño y la complejidad del problema: hay muchos tipos diferentes de datos, formatos y opciones. Otra razón es la riqueza de las capacidades de formato de Java. En muchos casos, Java ofrece más de una manera de formar datos. Por ejemplo, puede formar una fecha usando java.util. Formatter o java.text.DateFormat. En este capítulo se examina el tema de la formación y se presentan soluciones que muestran varias maneras de resolver diversas tareas comunes de formato. El eje principal de este capítulo es java.util.Formatter, que es una clase para formación de propósito general y que tiene una gran cantidad de opciones. Formatter también es usada por printf( ), que tiene soporte en PrintStream y PrintWriter. El método printf( ) es, en esencia, un método abreviado para uso con formato estilo Formatter, cuando se captura información directamente como un flujo. Como resultado, la mayoría de las soluciones usan Formatter, de manera directa o indirecta. Java incluye varias clases alternas para formación que se basan en Formatter (que se agregó en Java 5). Dos de ellas son java.text.DateFormat y java.text.NumberFormat. Ofrecen un método diferente para formar fecha, hora y datos numéricos que podrían ser útiles en algunos casos. (NumberFormat resulta especialmente útil cuando se forman valores numéricos de una manera sensible al idioma local). Otras dos clases de formación son java.text.SimpleDateFormat y java.text. DecimalFormat, que son subclases de DateFormat y NumberFormat, respectivamente. Le permiten formar fechas, horas y números con base en patrones. Aunque el principal eje de este capítulo es Formatter, varias soluciones utilizan estas opciones, principalmente para tener un abanico más completo, pero también porque ofrecen soluciones simples, pero elegantes a algunos tipos de tareas de formación. He aquí las soluciones de este capítulo: • • • • • • Cuatro técnicas simples de formación numérica que emplean Formatter Alinee verticalmente datos numéricos empleando Formatter Justifique a la izquierda la salida con Formatter Forme fecha y hora empleando Formatter Especifique un idioma local usando Formatter Use flujos con Formatter www.fullengineeringbook.net 117 118 Java: Soluciones de programación • • • • • • Use printf( ) para desplegar datos formados Forme fecha y hora con DateFormat Forme fecha y hora con patrones empleando SimpleDateFormat Forme valores numéricos con NumberFormat Forme valores monetarios usando NumberFormat Forme valores numéricos con patrones empleando DecimalFormat Revisión general de Formatter Formatter es una clase de formación de propósito general, y casi todas las soluciones de este capítulo dependen de ella. Está empaquetada en java.util e implementa las interfaces Closeable y Flushable. Aunque en las soluciones individuales se analizan sus características de manera detallada, resulta útil para presentar una revisión general de sus capacidades y su modo básico de operación. Es importante establecer desde el principio que Formatter es una clase relativamente nueva, agregada en Java 5. Por tanto, necesitará usar una versión moderna de Java para usar sus capacidades. Formatter trabaja al convertir la forma binaria de los datos usados por el programa en un texto formado, legible para el ser humano. Da salida al texto formado a un objeto de destino, que puede ser un búfer o un flujo (incluido un flujo de archivo). Si el destino es un búfer, entonces el contenido de éste puede obtenerse de su programa, cada vez que sea necesario. Es posible permitir que Formatter proporcione este búfer automáticamente, o puede especificarlo de manera explícita cuando se cree un objeto de Formatter. Si el destino es un flujo, entonces la salida se escribe en el flujo y no está disponible de otra manera en su programa. La clase Formatter define muchos constructores, que le permiten construir un Formatter de diversas maneras. Tal vez el de uso más amplio sea el constructor predeterminado: Formatter( ) Usa automáticamente la configuración de región e idioma local predeterminada y asigna un StringBuilder como constructor para que contenga la salida formada. Otros constructores le permiten especificar el destino y el idioma local. También puede especificar un archivo u otro tipo de OutputStream como contenedor de la salida formada. He aquí una muestra de constructores de Formatter: Formatter(Locale loc) Formatter(Appendable destino) Formatter(Appendable destino, Locale loc) Formatter(String nombrearchivo) throws FileNotFoundException Formatter(OutputStream flujoSal) Formatter(PrintStream flujoSal) El parámetro loc especifica una configuración de región e idioma local. Si no se especifica una, se usa la predeterminada. Al especificar un idioma local, puede formar datos en relación con un país, un idioma, o ambos. El parámetro destino especifica un destino para la salida formada. Este destino debe implementarse en la interfaz Appendable, que describe objetos a los que pueden agregarse datos al final. (Appendable se implementa con stringBuilder, PrintStream y PrintWriter, entre varios otros objetos). Si destino es nulo, entonces Formatter asigna automáticamente un StringBuilder para usar un búfer para la salida formada. El parámetro nombrearchivo especifica el www.fullengineeringbook.net Capítulo 4: Formato de datos 119 Método Descripción void close( ) Cierra el Formatter que invoca. Esto causa que se libere cualquier recurso empleado por el objeto. Después de que se ha cerrado un Formatter, ya no puede reutilizarse. void flush( ) Limpia el búfer de formato. Esto causa que cualquier salida que se encuentra en búfer se escriba en el destino. Formatter format(String cadFmt, Object ... args) Forma los argumentos pasados mediante args, de acuerdo con los especificadotes de formato contenidos en cadFmt. Devuelve el objeto que invoca. Formatter format(Locale loc, String cadFmt, Object ... args) Forma los argumentos pasados mediante args, de acuerdo con los especificadotes de formato contenidos en cadFmt. El idioma local especificado por loc se usa para este formato. Devuelve el objeto que invoca. IOException ioException( ) Si el objeto que es el destino para la salida lanza una IOException, entonces se devuelve esta excepción. De otra manera, se devuelve null. Locale locale( ) Devuelve la configuración de región y de idioma local del objeto que se invoca. Appendable out( ) Devuelve una referencia al objeto que es el destino para la salida. String toString( ) Devuelve la cadena obtenida al llamar a toString( ) en el objeto de destino. Si se trata de un búfer, entonces se devolverá la salida formada. Tabla 4-1 Los métodos definidos por Formatter nombre de un archivo que recibirá la salida formada. El parámetro flujoSal especifica una referencia a un flujo de salida que recibirá la salida. Formatter define los métodos mostrados en la tabla 4-1. Excepto por ioException( ), cualquier intento por usar uno de estos métodos después de que se ha cerrado la instancia de Formatter dará como resultado una FormatterClosedException. Fundamentos de formación Después de que ha creado un Formatter, puede usar su método format( ) para crear una cadena formada. Aquí se muestran sus dos formas: Formatter format(Locale loc, String cadFmt, Object ... args) Formatter format(String cadFmt, Object ... args) En la primera forma, el parámetro loc especifica la configuración de región y de idioma local. En la segunda, se usa el idioma de la instancia de Formatter. Por esto, probablemente la segunda forma sea la más común. En el caso de ambas formas, cadFmt incluye dos tipos de elementos. El primer tipo está integrado por caracteres que simplemente se copian en el destino. El segundo tipo contiene especificadores de formato que definen la manera en que están formados los argumentos subsecuentes pasados vía args. En su forma más simple, un especificador de formato empieza con un signo porcentual seguido por el especificador de conversión de formato. Todos estos especificadores contienen un solo carácter. Por ejemplo, el especificador de formato para los datos de punto flotante es %f. En general, debe www.fullengineeringbook.net 120 Java: Soluciones de programación haber el mismo número de argumentos que especificadores de formato, y ambos deben coincidir en orden, de izquierda a derecha. Por ejemplo, tome en consideración este fragmento: Formatter fmt = new Formatter( ); fmt.format("Formatter es %s poderosa %d %f", "muy", 88, 3.1416); Esta secuencia crea un Formatter que contiene la siguiente cadena: Formatter es muy poderosa 88 3.141600 En este ejemplo, los especificadores de formato %s, %d y %f se reemplazan con los argumentos que siguen a la cadena de formato. Por tanto %s es reemplazada por "muy", %d es reemplazada con 88 y %f es reemplazada con 3.1416. Todos los demás caracteres simplemente se usan tal como están. Como podrá suponer, el especificador de formato %s especifica una cadena y %d un valor entero. Como se mencionó antes, %f especifica un valor de punto flotante. Es importante comprender que en el caso de cualquier instancia determinada de Formatter, cada llamada a format( ) agrega salida al final de la salida anterior. Por tanto, si el destino de formato es un búfer, entonces cada llamada a format( ) adjunta la salida al final del búfer. En otras palabras, una llamada a format( ) no restablece el búfer. Por ejemplo, estas dos llamada a format( ) fmt.format("%s %s", "Esto", "es"); fmt.format("%s", " una prueba. "); crea una cadena que contiene "Esto es una prueba." Por tanto, es posible hacer una secuencia de llamadas a format( ) para construir la cadena deseada. El método format( ) acepta una amplia variedad de especificadores de formato, que se muestran en la tabla 4-2. Observe que muchos especificadores tienen formas en mayúsculas y minúsculas. Cuando se usa un especificador en mayúsculas, entonces las letras se muestran en mayúsculas. De otra manera, los especificadores en mayúsculas y minúsculas realizan la misma conversión. Es importante comprender que Java revisa el tipo de cada especificador de formato contra su argumento correspondiente. Si el argumento no coincide, se lanza una IllegalFormatException, que también se lanza si un especificador de formato está mal formado o si no se proporciona un argumento correspondiente que coincida con un especificador de formato. Hay varias subclases de IllegalFormatException que describen errores específicos. (Consulte la documentación de la API de Java para conocer detalles). Si está usando una versión basada en búfer de Formatter, después de llamar a format( ), puede obtener la cadena formada si llama a toString( ) en Formatter. Devuelve el resultado de llamar a toString( ) en el búfer. Así, continuando con el ejemplo anterior, la siguiente instrucción permite obtener la cadena formada que está contenida en fmt: String cad = fmt.toString( ); Por supuesto, si sólo quiere desplegar la cadena formada, no hay razón para asignarla primero a un objeto String. Cuando un objeto Formatter se pasa a println( ), por ejemplo, se llama automáticamente al método toString( ), que (en este caso) devuelve el resultado de llamar a toString( ) en el búfer. Otro tema importantes es que puede obtener una referencia a un destino al llamar a out( ). Devuelve una referencia al objeto Appendable en que se escribió la salida formada. En el caso de un Formatter basado en búfer, esto será una referencia al búfer, que es un StringBuilder, como opción predeterminada. www.fullengineeringbook.net Capítulo 4: Formato de datos Especiļ¬cador de formato Conversión aplicada %a %A Hexadecimal de punto flotante %b %B Booleano %c Carácter %d Entero decimal %h %H Código de hash del argumento %e %E Notación científica %f Punto flotante decimal %g %G Usa %e o %f, el que sea más corto %o Entero octal %n Inserta un carácter de nueva línea %s %S Cadena %t %T Hora y fecha %x %X Entero hexadecimal %% Inserta un signo % 121 Tabla 4-2 Los especificadores de formato Especificación de un ancho mínimo de campo Un entero colocado entre el signo % y el especificador de conversión de formato actúa como especificador de ancho mínimo de campo. Esto llena la salida con espacios para asegurar que alcanza una cierta longitud mínima. Si la cadena o el número es mayor que el mínimo, aún se imprimirá completo. El relleno predeterminado se hace con espacios. Si quiere rellenar con ceros, coloque un 0 antes de especificador de ancho de campo. Por ejemplo, %05d rellenará con ceros un número que cuente con menos de cinco dígitos para que su longitud total sea de cinco. El especificador de ancho de campo puede usarse con todos los especificadores de formato, excepto %n. Especificación de precisión Es posible aplicar un especificador de precisión a los especificadores de formato %f, %e, %g y %s. Sigue al especificador de ancho mínimo de campo (si hay uno) y está formado por un punto seguido de un entero. Su significado exacto depende del tipo de datos al que se aplica. Cuando el especificador de precisión se aplica a datos de punto flotante formados por % o %e, determina el número de lugares decimales desplegados. Por ejemplo, %10.4f despliega un número de por lo menos diez caracteres de ancho con cuatro lugares decimales. Cuando se usa %g, el especificador de precisión determina el número de dígitos significativos. La precisión predeterminada es 6. www.fullengineeringbook.net 122 Java: Soluciones de programación Aplicado a cadenas, el especificador de precisión especifica la longitud máxima de campo. Por ejemplo, %5.7s despliega una cadena de por lo menos cinco y no más de siete caracteres de largo. Si la cadena es mayor que el ancho máximo del campo, se truncarán los caracteres del final. Uso de las marcas de formato Formatter reconoce un conjunto de marcas de formato que le permiten controlar varios aspectos de la conversión. Todas las marcas de formato son caracteres únicos, y una marca de formato sigue a % en una especificación de formato. Aquí se muestran las marcas: Marca Efecto – Justificación a la izquierda # Alterna el formato de conversión 0 La salida es rellenada con ceros en lugar de espacios espacio La salida numérica positiva es precedida por un espacio + La salida numérica positiva es precedida por un signo + . Los valores numéricos incluyen separadores de grupo ( Los valores numéricos negativos están encerrados entre paréntesis De éstos, el # requiere cierta explicación. El # puede aplicarse a %o, %x, %a, %e y %f. Para los casos de %a, %e y %f, el # asegura que habrá un punto decimal aunque no haya dígitos decimales. Si antecede el especificador de formato %x con un #, el número hexadecimal se imprimirá con un prefijo 0x. Anteceder el especificador %o con # causa que el número se imprima con un cero al principio. La opción en mayúsculas Como ya se mencionó, varios de los especificadores de formato tienen versiones en mayúsculas que causan que la conversión use mayúsculas cuando sea apropiado. En la siguiente tabla se describe el efecto. Especiļ¬cador Efecto %A Causa que los dígitos hexadecimales de la a a f se desplieguen en mayúsculas, como de la A a la F. Además, el prefijo Ox se despliega como OX, y la p se desplegará como P. %B Pone en mayúsculas los valores verdadero y falso. %E Causa que el símbolo e que indica el exponente se despliegue en mayúsculas. %G Causa que el símbolo e que indica el exponente se despliegue en mayúsculas. %H Causa que los dígitos hexadecimales de la a la f se desplieguen en mayúsculas, como de la A a la F. %S Pone en mayúsculas la cadena correspondiente. %T Causa que todas las salidas alfabéticas relacionadas con la fecha o la hora (como los nombres de los meses o el indicador de AM/PM) se desplieguen en mayúsculas. %X Causa que los dígitos hexadecimales de la a a f se desplieguen en mayúsculas, como de la A a la F. Además, el prefijo Ox se despliega como OX, si está presente. www.fullengineeringbook.net Capítulo 4: Formato de datos 123 Uso de un índice de argumentos Formatter incluye una característica muy útil que le permite especificar el argumento al que se aplica un especificador de formato. Por lo general, los especificadores de formato y los argumentos coinciden en orden, de izquierda a derecha. Es decir, el primer especificador de formato coincide con el primer argumento, el segundo especificador con el segundo argumento, etc. Sin embargo, al usar un índice de argumentos, se controla específicamente cuál argumento coincide con un especificador de formato predeterminado. Un índice de argumentos va inmediatamente después del % en el especificador de formato. Tiene el siguiente formato: n$ donde n es el índice del argumento deseado, empezando en 1. Por ejemplo, considere este ejemplo: fmt.format ("%3$s %1$s %2$s", "alfa", "beta", "gama"); produce la cadena: gama alfa beta En este ejemplo, el primer especificador de formato coincide con "gama", el segundo con "alfa" y el tercero con "beta". Por tanto, los argumentos se usan en un orden diferente al que va estrictamente de izquierda a derecha. Una ventaja de los índices de argumento es que le permiten reutilizar un argumento sin tener que especificarlo dos veces. Por ejemplo, considere esta línea: fmt.format("%s en may\u00a3sculas es %1$S", "Prueba"); Produce la siguiente cadena: Prueba en mayúsculas es PRUEBA Como puede ver, el argumento "Prueba" es usado por ambos especificadores de formato. Hay un método abreviado conveniente llamado índice relativo, que le permite reutilizar el argumento con el que coincide el especificador de formato anterior. Simplemente especifique < para el índice de argumento. Por ejemplo, la siguiente llamada a format( ) produce los mismos resultados que el ejemplo anterior: fmt.format("%s en may\u00a3sculas es %<S", "Prueba"); Revisión general de NumberFormat y DateFormat NumberFormat y DateFormat son clases abstractas que son parte de java.text. NumberFormat se utiliza para formar valores numéricos; DateFormat para formar la fecha y la hora. Ambos también proporcionan soporte a análisis sintáctico de datos, y ambos funcionan de una manera sensible al idioma local. Estas clases utilizan Formatter y proporcionan otra manera de formar información. Una subclase concreta de NumberFormat es DecimalFormat. Soporta un método basado en patrones para formar valores numéricos. Una subclase concreta de DateFormat es SimpleDateFormat, que también soporta un método basado en patrones para formación. La operación de estas clases se describe en las soluciones que los usan. www.fullengineeringbook.net 124 Java: Soluciones de programación Cuatro técnicas simples de formación numérica que emplean Formatter Componentes clave Clases Métodos java.util.Formatter Formatter format(String cadFmt, Object ... args) Algunas de las preguntas que los principiantes plantean con más frecuencia se relacionan con la formación de valores numéricos. He aquí cuatro de ellas: • ¿Cómo controlo el número de lugares decimales desplegados cuando doy salida a un valor de punto flotante? Por ejemplo, ¿cómo despliego sólo dos lugares decimales? • ¿Cómo incluyo separadores de grupo en un número? Por ejemplo, en inglés, las comas se usan para separar grupos de tres dígitos, como 1,234,709. ¿Cómo se crean esas agrupaciones? • ¿Hay una manera simple de incluir una + al principio de un valor positivo? Si lo hay, ¿cuál es? • ¿Puedo desplegar valores negativos dentro de paréntesis? Si es posible, ¿cómo se hace? Por fortuna, es fácil responder todas estas preguntas porque Formatter ofrece soluciones muy simples a estos tipos de tareas de formato. En esta solución se muestra cómo hacerlo. Paso a paso Para formar un valor numérico empleando Formatter se requieren los pasos siguientes: 1. Construya un Formatter. 2. Cree un especificador para el formato deseado, como se describe en los pasos siguientes: 3. Para especificar el número de lugares decimales desplegados, use un especificador de precisión con los formatos %f o %e. 4. Para incluir separadores de grupo, use la marca , con %f, %g o %d. 5. Para desplegar un + al principio del valor positivo, especifique la marca +. 6. Para desplegar valores negativos dentro de paréntesis, use la marca (. 7. Pase el especificador de formato y el valor a format( ) para crear un valor formado. Análisis Para conocer una descripción de los constructores de Formatter y el método format( ), consulte Revisión general de Formatter, que se presentó casi al principio de este capítulo. Para especificar el número de lugares decimales (en otras palabras, el número de dígitos fraccionales) que se desplegará, use un especificador de precisión con el formato %f o %g. El especificador de precisión consta de un punto seguido por la precisión. Precede de inmediato al especificador de conversión. Por ejemplo, %.3f causa que se desplieguen tres dígitos decimales. www.fullengineeringbook.net Capítulo 4: Formato de datos 125 Para incluir separadores de grupo (que son las comas, en español), use la marca ,. Por ejemplo, %,d inserta el separador de grupo en un valor entero. Para anteceder valores negativos con un signo +, use la marca +. Por ejemplo, %+f causa que un valor positivo, de punto flotante, sea antecedido por un +. En algunos casos, como cuando se crean declaraciones de pérdidas y ganancias, se acostumbra desplegar valores negativos entre paréntesis. Esto se logra fácilmente con el uso de la marca (. Por ejemplo, %(,2f despliega un valor usando dos dígitos decimales. Si el valor es negativo, se incluye entre paréntesis. Ejemplo Con el siguiente programa se pone la solución en acción. // Usa Formatter para: // // . Especifica el número de dígitos decimales. // . Usa un separador de grupo. // . Antecede un valor positivo con un signo +. // . Muestra valores negativos entre paréntesis. import java.util.*; class FormatosNumericos { public static void main(String args[]) { Formatter fmt = new Formatter( ); // Limita el número de dígitos decimales // al especificar la precisión. fmt.format("Precisi\u00a2n predeterminada: %f\n", 10.0/3.0); fmt.format("Dos d\u00a1gitos decimales: %.2f\n\n", 10.0/3.0); // Usando separadores de grupo. fmt.format("Sin separadores de grupo: %d\n", 123456789); fmt.format("Con separadores de grupo: %,d\n\n", 123456789); // Muestra valores positivos con + al principio // y valores negativos entre paréntesis. fmt.format("Formato predeterminado positivo y negativo: %.2f %.2f\n", 423.78, –505.09); fmt.format("Con + y parentesis: %+.2f %(.2f\n", 423.78, –505.09); // Despliega la salida formada. System.out.println(fmt); } } Aquí se muestra la salida: Precisión predeterminada: 3.333333 Dos dígitos decimales: 3.33 www.fullengineeringbook.net 126 Java: Soluciones de programación Sin separadores de grupo: 123456789 Con separadores de grupo: 123,456,789 Formato predeterminado positivo y negativo: 423.78 –505.09 Con + y paréntesis: +423.78 (505.09) Opciones La clase java.text.NumberFormat también puede usarse para formar valores numéricos. No soporta todas las opciones disponibles mediante Formatter, pero le permite especificar el número mínimo y máximo de dígitos fraccionales que se desplegará. También puede formar valores en el formato de moneda de la configuración local. Además, puede usar DecimalFormat para formar valores numéricos. (Consulte las soluciones relacionadas con NumberFormat y DecimalFormat cerca del final del capítulo, para conocer más detalles). Puede usar la marca # con %e y %f para asegurar que habrá un punto decimal aunque no se muestren dígitos decimales. Por ejemplo, %#.0f causa que el valor 100.0 se despliegue como 100.. Puede usar más de una marca a la vez. Por ejemplo, para mostrar números negativos con separadores de grupo dentro de paréntesis, use este especificador de formato; %,(f. Alinee verticalmente datos numéricos empleando Formatter Componentes clave Clases Métodos java.util.Formatter Formatter format(String cadFmt, Object ... args) Una tarea de formato común incluye la creación de tablas en que se alinean los valores numéricos de una columna. Por ejemplo, tal vez quiera que se alineen los datos financieros de una declaración de pérdidas y ganancias. Como regla general, la alineación de valores numéricos en una columna implica que los puntos decimales se alineen. En el caso de valores enteros, los dígitos de las unidades deben alinearse. La manera más fácil de alinear verticalmente datos numéricos incluye el uso de un especificador de ancho mínimo de campo. A menudo, también querrá especificar la precisión para mejorar la presentación. Este es el método usado en esta solución. Paso a paso La alineación vertical de valores numéricos incluye estos pasos: 1. Construya un Formatter. 2. Cree un especificador de formato que defina el ancho del campo en que se desplegarán los valores. El ancho será igual o mayor al del valor más largo (incluido el punto decimal, el signo y los separadores de grupo). www.fullengineeringbook.net Capítulo 4: Formato de datos 127 3. Pase el especificador de formato y los datos a format( ) para crear un valor formado. 4. Organice los valores formados verticalmente, uno encima del otro. Análisis Para una descripción de los constructores de Formatter y el método format( ), consulte Revisión general de Formatter, presentado cerca del principio de este capítulo. En general, la alineación de valores numéricos en una tabla requiere que especifique un ancho mínimo de campo. Cuando se usa éste, la salida se rellena con espacios para asegurar que alcance una cierta longitud mínima. Sin embargo, comprenda que si la cadena o el número que se está formando es mayor que el mínimo, aún se imprimirá completo. Esto significa que debe hacer que el ancho mínimo sea por lo menos igual que el valor más largo, si quiere que los valores se alineen. Como opción predeterminada, se usan los espacios para rellenar la salida. Cuando se alinean datos de punto flotante, a menudo querrá que los valores se alineen de modo que los puntos decimales queden uno encima del otro. En el caso de datos enteros, los dígitos de las unidades se alinean verticalmente. Ejemplo En el siguiente ejemplo se muestra cómo usar un ancho de campo mínimo para alinear datos verticalmente. Despliega varios valores y sus raíces cúbicas. Usa un ancho de 12 y muestra cuatro lugares decimales. // Usa Formatter para alinear verticalmente valores numéricos. import java.util.*; class AlinearVertical { public static void main(String args[]) { double datos[] = { 12.3, 45.5764, –0.09, –18.0, 1232.01 }; Formatter fmt = new Formatter( ); // Crea una tabla que contiene valores y la // raíz cúbica de esos valores. fmt.format("%12s %12s\n", "Valor", "Ra\u00a1z c\u00a3bica"); for(double v : datos) { fmt.format("%12.4f %12.4f\n", v, Math.cbrt(v)); } // Despliega los datos formados. System.out.println(fmt); } } Aquí se muestra la salida: Valor 12.3000 45.5764 –0.0900 –18.0000 1232.0100 Raíz cúbica 2.3084 3.5720 –0.4481 –2.6207 10.7202 www.fullengineeringbook.net 128 Java: Soluciones de programación Ejemplo adicional: centro de datos En ocasiones querrá alinear datos verticalmente al centrarlos en lugar de justificarlos a la izquierda o la derecha. En este ejemplo se muestra la manera de hacerlo. Crea un método estático denominado centrar( ), que centra un elemento dentro de un ancho de campo especificado. El método pasa una referencia a Formatter, el especificador de formato que determina el formato de los datos, los datos que habrán de formarse (a manera de una referencia a Object) y el ancho del campo. El método centrar( ) tiene una restricción importante que debe señalarse: sólo funciona para la configuración de idioma predeterminada. Sin embargo, puede mejorarlo fácilmente al hacer que funcione con una configuración especificada. // Centra los datos dentro de un campo. import java.util.*; class DemoCentrar { // Centra los datos dentro de un ancho de campo específico. // El formato de los datos se pasa a cadFmt, // el Formatter se pasa a fmt, los datos que se formarán // pasan a obj, y el ancho del campo se pasa a ancho. static void centrar(String cadFmt, Formatter fmt, Object obj, int ancho) { String cad; try { // Primero, forma los datos para que pueda determinarse // su longitud. Use a Formatter temporal para // este propósito. Formatter tmp = new Formatter( ); tmp.format(cadFmt, obj); cad = tmp.toString( ); } catch(IllegalFormatException exc) { System.out.println("Solicitud de formato no v\u00a0lida"); fmt.format(""); return; } // Obtiene la diferencia entre la longitud de los // datos y la del campo. int dif = ancho – cad.length( ); // Si los datos son más largos que el ancho del campo, // entonces simplemente los usa como están. if(dif < 0) { fmt.format(cad); return; } www.fullengineeringbook.net Capítulo 4: // Agrega relleno al inicio del campo. char[] rell = new char[dif/2]; Arrays.fill(rell, ‘ ‘); fmt.format(new String(rell)); // Agrega los datos. fmt.format(cad); // Agrega relleno al final del campo. rell = new char[ancho–dif/2–cad.length( )]; Arrays.fill(rell, ‘ ‘); fmt.format(new String(rell)); } // Demuestra centrar( ). public static void main(String args[]) { Formatter fmt = new Formatter( ); fmt.format("|"); centrar("%s", fmt, "Origen", 12); fmt.format("|"); centrar("%10s", fmt, "Perds/Gans", 14); fmt.format("|\n\n"); fmt.format("|"); centrar("%s", fmt, "Menudeo", 12); fmt.format("|"); centrar("%,10d", fmt, 1232675, 14); fmt.format("|\n"); fmt.format("|"); centrar("%s", fmt, "Almacenes", 12); fmt.format("|"); centrar("%,10d", fmt, 23232482, 14); fmt.format("|\n"); fmt.format("|"); centrar("%s", fmt, "Rentas", 12); fmt.format("|"); centrar("%,10d", fmt, 3052238, 14); fmt.format("|\n"); fmt.format("|"); centrar("%s", fmt, "Regal\u00a1as", 12); fmt.format("|"); centrar("%,10d", fmt, 329845, 14); fmt.format("|\n"); fmt.format("|"); centrar("%s", fmt, "Intereses", 12); fmt.format("|"); centrar("%,10d", fmt, 8657, 14); fmt.format("|\n"); fmt.format("|"); centrar("%s", fmt, "Inversiones", 12); www.fullengineeringbook.net Formato de datos 129 130 Java: Soluciones de programación fmt.format("|"); centrar("%,10d", fmt, 1675832, 14); fmt.format("|\n"); fmt.format("|"); centrar("%s", fmt, "Patentes", 12); fmt.format("|"); centrar("%,10d", fmt, –2011, 14); fmt.format("|\n"); // Despliega los datos formados. System.out.println(fmt); } } Aquí se muestra la salida de ejemplo. Observe que los datos en ambas columnas están centrados dentro del ancho del campo. Las extensiones del ancho están indicados por las barras verticales. | Origen | Perds/Gans | | Menudeo | 1,232,675 | Almacenes | 23,232,482 | Rentas | 3,052,238 | Regalías | 329,845 | Intereses | 8,657 |Inversiones | 1,675,832 | Patentes | –2,011 | | | | | | | En el programa, preste especial atención a esta secuencia dentro de centrar( ): // Primero, forma los datos para que pueda determinarse // su longitud. Use a Formatter temporal para // este propósito. Formatter tmp = new Formatter( ); tmp.format(cadFmt, obj); cad = tmp.toString( ); Aunque los datos que se están formando se pasan como una referencia a Object vía obj, aún pueden formarse porque format( ) trata automáticamente de formar los datos con base en el especificador de formato. En general, todos los argumentos de format( ) son referencias a Object porque todos los argumentos se pasan vía un parámetro var-args de tipo Object. Una vez más, es el tipo de especificador de formato lo que determina la manera en que se interpreta el argumento. Si el tipo de argumento no coincide con los datos, entonces se lanzará una IllegalFormatException. Un tema adicional es que muchas de las llamadas a format( ) especifican una cadena de formato que no contiene ningún especificador de formato o algún argumento para formarse. Esto es perfectamente legal. Como se explicó, la cadena de formato puede contener dos tipos de elementos: los caracteres regulares que simplemente se pasan a la salida como están, y los especificadores de formato. Sin embargo, ninguno de los dos son obligatorios. Por tanto, cuando no se incluyen especificadores de formato, no se necesitan argumentos adicionales. www.fullengineeringbook.net Capítulo 4: Formato de datos 131 Opciones Como opción predeterminada, los espacios se usan como relleno para alcanzar un ancho de campo mínimo, pero puede rellenarlo con criptográficos, al colocar un 0 antes del especificador de ancho de campo. Por ejemplo, si sustituye esta cadena de formato "012.4f %012,4F\n" en el primer ejemplo, la salida sería como ésta. Valor 0000012.3000 0000045.5764 –000000.0900 –000018.0000 0001232.0100 Raíz cúbica 0000002.3084 0000003.5720 –000000.4481 –000002.6207 0000010.7202 En algunos casos, puede usar justificación a la izquierda para alinear valores verticalmente. Esta técnica se demuestra en la siguiente solución. Justifique a la izquierda la salida con Formatter Componentes clave Clases Métodos java.util.Formatter Formatter format(String cadFmt, Object ... args) Cuando se usa un ancho de campo mínimo, la salida se alinea a la derecha, como opción predeterminada. Sin embargo, esto no es siempre lo que se necesita. Por ejemplo, cuando se forman cadenas dentro de un campo de ancho fijo, a menudo las cadenas necesitan justificarse a la izquierda. Esto es fácil de lograr cuando usa Formatter, porque sólo requiere el uso de la marca de justificación a la izquierda: –. Paso a paso La justificación a la izquierda de datos incluye estos pasos: 1. Construya un Formatter. 2. Cree un especificador de formato que defina el ancho del campo en que se desplegarán los datos. Use la marca de justificación a la izquierda – para que los datos sigan esa alineación dentro de ese campo. 3. Pase el especificador de formato y los datos a format( ) para crear la salida justificada a la izquierda. Análisis Para conocer una descripción de los constructores de formato y el método format( ), consulte Revisión general de Formatter, que se presentó al principio de este capítulo. www.fullengineeringbook.net 132 Java: Soluciones de programación La marca de justificación a la izquierda sólo puede usarse en un especificador de formato que incluye una especificación de ancho mínimo de campo. Aunque la justificación a la izquierda funciona con valores numéricos, suele aplicarse a cadenas cuando se presentan datos en una tabla. Esto permite que las cadenas se alineen verticalmente, justificándose a la izquierda, una sobre otra. Ejemplo En el siguiente ejemplo se muestra cómo puede usarse la justificación a la izquierda para mejorar el aspecto de una tabla. La tabla presenta una declaración de pérdidas y ganancias muy simple en que la columna de la izquierda describe una categoría de ingresos y la de la derecha muestra la utilidad (o pérdida) de esa categoría. Las cadenas de la primera columna están justificadas a la izquierda. Esto hace que se alineen a la izquierda dentro de un campo de 12 columnas. Observe que los valores de la segunda columna están justificados a la derecha (lo que les permite alinearse verticalmente). // Usa Formatter para alinear cadenas a la izquierda en una tabla. import java.util.*; class Tabla { public static void main(String args[]) { Formatter fmt = new Formatter( ); fmt.format("%–12s %12s\n\n", "Origen", "Perds/Gans"); fmt.format("%–12s fmt.format("%–12s fmt.format("%–12s fmt.format("%–12s fmt.format("%–12s fmt.format("%–12s fmt.format("%–12s %,12d\n", %,12d\n", %,12d\n", %,12d\n", %,12d\n", %,12d\n", %,12d\n", "Menudeo", 1232675); "Almacenes", 23232482); "Rentas", 3052238); "Regal\u00a1as", 329845); "Intereses", 8657); "Inversiones", 1675832); "Patentes", –2011); // Despliega la tabla formada. System.out.println(fmt); } } Aquí se muestra la salida: Origen Perds/Gans Menudeo Almacenes Rentas Regalías Intereses Inversiones Patentes 1,232,675 23,232,482 3,052,238 329,845 8,657 1,675,832 –2,011 www.fullengineeringbook.net Capítulo 4: Formato de datos 133 Opciones Para comprender la utilidad de la justificación a la izquierda, haga este experimento; elimine las marcas de justificación a la izquierda de los especificadores de cadena. En otras palabras, cambie las siguientes llamadas a format( ), como se muestra a continuación: fmt.format("%12s %12s\n\n", "Origen", "Perds/Gans"); fmt.format("%12s fmt.format("%12s fmt.format("%12s fmt.format("%12s fmt.format("%12s fmt.format("%12s fmt.format("%12s %,12d\n", %,12d\n", %,12d\n", %,12d\n", %,12d\n", %,12d\n", %,12d\n", "Menudeo", 1232675); "Almacenes", 23232482); "Rentas", 3052238); "Regal\u00a1as", 329845); "Intereses", 8657); "Inversiones", 1675832); "Patentes", –2011); Después de hacer estos cambios, la salida tendrá ahora este aspecto: Origen Perds/Gans Menudeo Almacenes Rentas Regalías Intereses Inversiones Patentes 1,232,675 23,232,482 3,052,238 329,845 8,657 1,675,832 –2,011 Como verá, los resultados no son tan adecuados. Forme fecha y hora empleando Formatter Componentes clave Clases Métodos java.util.Formatter Formatter format(String cadFmt, Object ... args) java.util.Calendar static Calendar getInstance( ) java.util.Date Uno de los especificadores de conversión más poderosos proporcionados por Formatter es %t, que forma información de fecha y hora. Debido a la amplia variedad de formatos en que puede representarse la hora y la fecha, el especificador %t soporta muchas opciones. Por ejemplo, la hora puede representarse empleando un reloj de 12 o 24 horas. La fecha puede mostrarse usando formas www.fullengineeringbook.net 134 Java: Soluciones de programación cortas, como 15/10/2008, o largas, como miércoles, 15 de octubre, 2008. Las opciones de fecha y hora se especifican empleando uno o más de los sufijos que siguen al formato t. En esta solución se describe el proceso. Paso a paso Para formar la fecha, la hora, o ambas, siga estos pasos: 1. Cree un objeto de Formatter. 2. Empleando los sufijos mostrados en la tabla 4-3 para indicar el formato preciso, cree un especificador de formato %t que describa la manera en que quiera desplegar la fecha, la hora, o ambos. 3. Obtenga una instancia que contenga la fecha y la hora que desee formar. Esto debe ser un objeto de tipo Calendar, Date, Long o long. 4. Pase el especificador de formato y la hora a format( ) para crear un valor formado. Análisis Para conocer una descripción de los constructores de Formatter y el método format( ), consulte Revisión general de Formatter, presentada cerca del principio de este capítulo. El especificador %t funciona un poco diferente que otros porque requiere el uso de un sufijo para describir la parte y el formato precisos de la fecha y la hora deseados. Los sufijos se muestran en la tabla 4-3. Por ejemplo, para desplegar minutos, usaría %tM, donde M indica minutos en un campo de dos caracteres. El argumento correspondiente al especificador %t debe ser de tipo Calendar, Date, Long o long. El especificador %t causa que cualquier información alfabética asociada con la fecha y la hora, como los nombres de días o el indicador AM/FM, se desplieguen en minúsculas. Si quiere desplegar estos elementos en mayúsculas, use, en cambio, %T. Como ya se explicó, el argumento que corresponde a %t debe ser una instancia de Calendar, Date, Long o long. Sin embargo, con más frecuencia usará una instancia de Calendar o de Date (que a menudo contendrán la fecha y la hora actuales del sistema). Tanto Calendar como Date están empaquetados en java.util. Para obtener la fecha y la hora actuales usando Calendar, llame al método de fábrica getInstance( ). Aquí se muestra: static Calendar getInstance( ) Devuelve una instancia de Calendar que contiene la fecha y hora en que se creó el objeto. Para obtener la fecha y la hora actuales usando Date, simplemente cree un objeto de Date empleando este constructor: Date( ) Esto crea una instancia de Date( ), que contiene la fecha y la hora actuales del sistema. Como se muestra en la tabla 4-3, Formatter le da un control muy detallado del formato de la información de fecha y hora. La mejor manera de comprender el efecto de cada sufijo consiste en experimentar. www.fullengineeringbook.net Capítulo 4: Formato de datos Suļ¬jo Se reemplaza con a Nombre de día de la semana abreviado A Nombre completo de día de la semana b Nombre abreviado de mes B Nombre completo del mes c Cadena de fecha y hora estándar, formada como mes día hh::mm::ss zona año C Primeros dos dígitos del año d Día del mes como decimal (01-31) D mes/día/año e Día del mes como decimal (1-31) F año-mes-día h Nombre del mes abreviado H Hora (00 a 23) I Hora (01 a 12) j Día del año como decimal (001 a 366) k Hora (0 a 23) l Hora (01 a 12) L Milisegundo (000 a 999) m Mes como decimal (01 a 13) M Minuto como decimal (00 a 59) N Nanosegundo (000000000 a 999999999) p Equivalente local de AM o PM en minúsculas Q Milisegundos desde 1/1/1970 r hh:mm:ss (formato de 12 horas) R hh:mm (formato de 24 horas) S Segundos (00 a 60) s Segundos desde 1/1/1970 UTC T hh:mm:ss (formato de 24 horas) y Año en decimales sin siglo (00 a 99) Y Año en decimales incluyendo siglo (0001 a 9999) z Desplazamiento desde UTC Z Nombre de la zona horaria Tabla 4-3 Los sufijos de formato de fecha y hora www.fullengineeringbook.net 135 136 Java: Soluciones de programación Ejemplo He aquí un programa que demuestra una variedad de formatos de fecha y hora. Observe algo más en este programa: la última llamada a format( ) usa indizamiento relativo para permitir que el mismo valor de datos sea usado por tres especificadores de formato. Aquí se muestra esta línea: fmt.format("Hora y minuto: %t1:%1$tM %1$Tp", cal); Debido al indizamiento relativo, el argumento cal sólo necesita pasarse una vez, en lugar de tres veces. // Despliega varios formatos de hora y fecha // empleando el especificador %t con Formatter. import java.util.*; class FechaYHora { public static void main(String args[]) { Formatter fmt = new Formatter( ); // Obtiene la fecha y hora actuales. Calendar cal = Calendar.getInstance( ); // Despliega el formato de 12 horas. fmt.format("Hora usando el reloj de 12 horas: %tr\n", cal); // Despliega el formato de 24 horas. fmt.format("Hora usando el reloj de 24 horas: %tT\n", cal); // Despliega el formato de fecha corta. fmt.format("Formato de fecha corta: %tD\n", cal); // Despliega la fecha usando nombres completos. fmt.format("Formato de fecha larga: "); fmt.format("%tA %1$tB %1$td, %1$tY\n", cal); // Despliega información completa de fecha y hora. // La primera versión usa minúsculas. // La segunda versión usa mayúsculas. // Como se explicó, las mayúsculas se seleccionan // usando %T en lugar de %t. fmt.format("Hora y fecha en min\u00a3sculas: %tc\n", cal); fmt.format("Hora y fecha en may\u00a3sculas: %Tc\n", cal); // Despliega la hora y el minuto, e incluye un indicador // de AM o PM. Observe que se usa %T en mayúsculas. // Esto hace que AM o PM esté en mayúsculas. fmt.format("Hora y minuto: %tl:%1$tM %1$Tp\n", cal); www.fullengineeringbook.net Capítulo 4: Formato de datos 137 // Despliega las fechas y horas formadas. System.out.println(fmt); } } Aquí se muestra la salida: Hora usando el reloj de 12 horas: 06:53:10 PM Hora usando el reloj de 24 horas: 18:53:10 Formato de fecha corta: 03/30/08 Formato de fecha larga: domingo marzo 30, 2008 Hora y fecha en minúsculas: dom mar 30 18:53:10 CST 2008 Hora y fecha en mayúsculas: DOM MAR 30 18:53:10 CST 2008 Hora y minuto: 6:53 PM Opciones Como ya se mencionó, la hora, la fecha o ambas pueden estar en un objeto de Calendar, Date, Long o long. En el ejemplo se usa un objeto de Calendar, pero puede usar uno de los otros objetos si son más convenientes. Por ejemplo, he aquí una manera de reescribir la primera llamada a format( ): fmt.format("Hora usando el reloj de 12 horas: %tr\n", new Date); Pasa una instancia de Date, en lugar de una de Calendar. Una versión larga de la fecha y la hora puede obtenerse de Calendar al llamar a getTimeInMillis( ), y de Date al llamar a getTime( ). Si quiere usar algo diferente de la hora y la fecha actuales del sistema, puede usar el método set( ) definido por Calendar para establecer la fecha y la hora. Como opción, puede usar GregorianCalendar, que es una subclase de Calendar. Proporciona constructores que le permiten especificar la fecha y la hora de manera explícita. Por ejemplo, aquí la fecha y la hora se construyen usando GregorianCalendar: fmt.format("Hora y fecha en min\u00a3sculas: %tc\n", new GregorianCalendar(2007, 1, 28, 14, 30, 0)); La fecha se establece en febrero 28, 2007. La hora es 2:30:00 pm. Una opción importante para el formato de fecha y hora es java.text.DateFormat. Ofrece una manera diferente de crear formatos de hora y fecha que pudiera ser más fácil usar en algunos casos. De especial interés es su subclase SimpleDateFormat, que le permite especificar la hora y la fecha al usar un patrón. (Consulte Forme fecha y hora con DateFormat y Forme fecha y hora con patrones empleando SimpleDateFormat). www.fullengineeringbook.net 138 Java: Soluciones de programación Especifique un idioma local usando Formatter Componentes clave Clases Métodos java.util.Formatter Formatter format (Locale loc, String cadFmt, Object ... args) java.util.Formatter Locale.FRANCE Locale.GERMAN Locale.ITALY Aunque Formatter usa como opción predeterminada la configuración de región y de idioma predeterminada, es posible especificar explícitamente una propia. Al hacerlo, se le permite formar datos de una manera compatible con otros países e idiomas. La información local está encapsulada dentro de la clase Locale, que se encuentra empaquetada en java.util. Hay dos maneras básicas de especificar un idioma local cuando forma. En primer lugar, puede pasar una instancia de Locale a uno de los constructores de Formatter que habilitan configuración local. (En la revisión general de formato, presentada al principio de este capítulo, se muestran algunos). En segundo lugar, puede usar la forma que soporta idioma local de format( ). Éste es el método que se usa en esta solución. Paso a paso Para formar datos en relación con una configuración de idioma específica, siga estos pasos: 1. Cree u obtenga un objeto de Locale que representa el idioma local. 2. Cree un objeto de Formatter. 3. Use el método format( ) para formar los datos, especificando el objeto Locale. Esto causa que el formato incorpore automáticamente atributos sensibles a la configuración de idioma. Análisis Para conocer una descripción de los constructores de Formatter y el método format( ), consulte Revisión general de Formatter, que se presentó cerca del principio de este capítulo. La configuración de región y de idioma local está representada por los objetos de Locale, que describen una región geográfica o cultural. Locale es una de las clases que ayudan a internacionalizar un programa. Contiene información que determina, por ejemplo, los formatos usados para desplegar fechas, horas y números en diferentes países e idiomas. La internacionalización es un tema extenso que se encuentra más allá del alcance de este capítulo. Sin embargo, es fácil usar Locale para adecuar los formatos producidos por Formatter. Los constructores para Locale son: Locale(String idioma) Locale(String idioma, String pais) Locale(String idioma, String pais, String datos) www.fullengineeringbook.net Capítulo 4: Formato de datos 139 Estos constructores crean un objeto de Locale para representar un idioma específico y, en el caso de los últimos dos, un país. Estos valores deben contener códigos de idioma y país estándar ISO. (Consulte la documentación de la API de Locale, para conocer información sobre códigos de país e idioma). En los datos puede proporcionar información específica del explorador y el vendedor. En lugar de construir una instancia de Locale usted mismo, a menudo puede usar una de las configuraciones locales predefinidas por Locale, que se muestran a continuación: CANADA GERMAN KOREAN CANADA_FRENCH GERMANY PRC CHINA ITALIAN SIMPLIFIED_CHINESE CHINESE ITALY TAIWÁN ENGLISH JAPAN TRADITIONAL_CHINESE FRANCE JAPANESE UK FRENCH KOREA US Todos estos campos son objetos estáticos de tipo Locale que se han inicializado en el idioma y el país indicados. Por ejemplo, el campo Locale.CANADA representa el objeto Locale para Canadá. El campo LOCALE.JAPANESE representa el objeto Locale para el idioma japonés. Ejemplo Con el siguiente ejemplo se ilustra el uso de las configuraciones regionales y de idioma cuando se forman la fecha y la hora. Primero muestra el formato de fecha y hora predeterminados (que es español de México en la salida de ejemplo). Luego muestra los formatos para Italia, Alemania y Francia. // Demuestra el formato específico de idioma local. import java.util.*; class DemoFormatoLocal { public static void main(String args[]) { Formatter fmt = new Formatter( ); // Obtiene la fecha y hora actuales. Calendar cal = Calendar.getInstance( ); // Despliega información completa de fecha y hora // para varias configuraciones locales. fmt = new Formatter( ); fmt.format("Idioma predeterminado: %tc\n", cal); fmt.format(Locale.GERMAN, "Para Locale.GERMAN: %tc\n", cal); fmt.format(Locale.ITALY, "Para Locale.ITALY: %tc\n", cal); fmt.format(Locale.FRANCE, "Para Locale.FRANCE: %tc\n", cal); System.out.println(fmt); } } www.fullengineeringbook.net 140 Java: Soluciones de programación Aquí se muestra una salida de ejemplo: Idioma predeterminado: dom mar 30 20:17:15 CST 2008 Para Locale.GERMAN: So Mrz 30 20:17:15 CST 2008 Para Locale.ITALY: dom mar 30 20:17:15 CST 2008 Para Locale.FRANCE: dim. mars 30 20:17:15 CST 2008 Opciones Como se mencionó, puede crear un Formatter específico de la configuración de región y de idioma local. Posteriores llamadas a format( ) tomarán como destino esa configuración local. Puede obtener información específica de la configuración local y de idioma relacionada con monedas empleando la clase java.util.Currency. Por ejemplo, su método getSymbol( ) devuelve una cadena que contiene el símbolo de moneda para la configuración de región y de idioma local (que es $ para México). El método getDefaultFractionDigits( ) devuelve el número de lugares decimales (es decir, dígitos fraccionales) que se despliegan normalmente (que es 2, en español). Use flujos con Formatter Componentes clave Clases Métodos java.util.Formatter Formatter format(String cadFmt, Object ... args)) Aunque con frecuencia el destino para datos formados será un búfer (por lo general de tipo StringBuilder), también puede crear instancias de Formatter que salgan a un flujo. Esta capacidad le permite escribir salida formada directamente en un archivo, por ejemplo. También puede usar esta característica para escribir salida formada directamente a la consola. En esta solución se muestra el proceso. Paso a paso Para escribir directamente en un flujo datos formados se requieren estos pasos: 1. Cree un Formatter que esté vinculado con un flujo. 2. Cree el especificador de formato deseado. 3. Pase el especificador de formato y los datos a format( ). Debido a que Formatter está vinculado a un flujo, la salida formada se escribe automáticamente en el flujo. Análisis Para vincular un Formatter con un flujo, debe usar uno de los constructores que funcionan con flujos de Formatter. Los que se usan aquí son Formatter(OutputStream cadSal) Formatter(PrintStream cadSal) www.fullengineeringbook.net Capítulo 4: Formato de datos 141 El parámetro cadSal especifica una referencia a un flujo que recibirá salida. La primera versión funciona con cualquier OutputStream, incluido FileOutputStream. La segunda versión funciona con PrintStream. La clase PrintStream proporciona los métodos print( ) y println( ) que se usan para tipos de datos básicos de Java (como int, String y double) en una forma legible para los seres humanos. Como tal vez ya lo sepa, System.out es una instancia de PrintStream. Por tanto, un Formatter vinculado con System.out escribirá directamente en la consola. Para conocer una descripción del método format( ) definida por Formatter, consulte Revisión general de Formatter, presentada cerca del inicio de este capítulo. Ejemplo En el siguiente ejemplo se muestra cómo usar Formatter para escribir en la consola y en un archivo. // Escribe la salida formada directamente en la consola. // y en un archivo. import java.io.*; import java.util.*; class FlujosFormatter { public static void main(String args[]) { // Crea un Formatter vinculado con la consola. Formatter fmtCon = new Formatter(System.out); // Crea un Formatter vinculado con un archivo. Formatter fmtArchivo; try { fmtArchivo = new Formatter(new FileOutputStream("prueba.fmt")); } catch(FileNotFoundException exc) { System.out.println("No puede abrir el archivo"); return; } // Primero, escribe en la consola. fmtCon.format("Es un n\u00a3mero negativo: %(.2f\n\n", –123.34); fmtCon.format("%8s %8s\n", "Valor", "Cuadrado"); for(int i=1; i < 20; i++) fmtCon.format("%8d %8d\n", i, i*i); // Ahora, escriba en el archivo. fmtArchivo.format("Es un n\u00a3mero negativo: %(.2f\n\n", –123.34); fmtArchivo.format("%8s %8s\n", "Valor", "Cuadrado"); www.fullengineeringbook.net 142 Java: Soluciones de programación for(int i=1; i < 20; i++) fmtArchivo.format("%8d %8d\n", i, i*i); fmtArchivo.close( ); // Usa ioException( ) para revisar errores de archivo. if(fmtArchivo.ioException( ) != null) { System.out.println("Ocurri\u00a2 un error de E/S"); } } } Aquí se muestra la salida que se despliega en la consola y se escribe en el archivo: Es un número negativo: (123.34) Valor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Cuadrado 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 Opciones Cuando se escriben datos formados en la consola, suele ser más fácil usar el método printf( ) definido por PrintStream. (Consulte Use printf( ) para desplegar datos formados). Cuando se crea un Formatter vinculado con un OutputStream, hay constructores adicionales que le permiten especificar la configuración de región e idioma local y el conjunto de caracteres que definen la correlación entre caracteres y bytes. www.fullengineeringbook.net Capítulo 4: Formato de datos 143 Use printf( ) para desplegar datos formados Componentes clave Clases Métodos java.io.PrintStream PrintStream printf(String cadFmt, Object ... args) java.io.PrintWriter PrintWriter printf(String cadFmt, Object ... args) Aunque usar un Formatter vinculado con System.out es una manera fácil de desplegar datos formados en la consola, aún se requieren dos pasos. Primero, debe crearse un Formatter y, después, llamarse a format( ). Aunque ciertamente no hay nada incorrecto con este método, Java proporciona una opción más conveniente: el método printf( ). Este método está definido tanto por PrintStream como por PrintWriter. El método printf( ) usa automáticamente Formatter para crear una cadena formada. Luego da salida a la cadena en el flujo que invoca. Debido a que System.out es una instancia de PrintStream, puede llamarse a printf( ) en él. Por tanto, al llamar a printf( ) en System.out, la salida formada puede desplegarse en la consola en un paso. Por supuesto, printf( ) puede llamarse en cualquier instancia de PrintStream o PrintWriter. Por tanto, también puede usarse para escribir salida formada en un archivo. NOTA El método printf( ) está basado en la función printf( ) de C/C++, y es muy similar. Esto facilita la conversión de código de C/C++ a Java. Paso a paso El uso de printf( ) incluye estos pasos: 1. Obtenga una referencia a PrintStream o a PrintWriter. 2. Construya la cadena de formato que contiene los especificadores de formato que estará usando. 3. Pase la cadena de formato y los datos correspondientes a printf( ). Los datos formados se escribirán en el flujo que invoca. Análisis El método printf( ) está definido por PrintStream y PrintWriter. He aquí dos formas de PrintStream: PrintStream printf(String cadFmt, Object ... args) PrintStream printf(Locale loc, String cadFmt, Object ... args) www.fullengineeringbook.net 144 Java: Soluciones de programación La primera versión escribe args en el flujo que invoca en el formato especificado por cadFmt, empleando la configuración local. El segundo le permite especificar una configuración. Ambas devuelven el PrintStream que invoca. He aquí dos formas de PrintWriter: PrintWriter printf(String cadFmt, Object ... args) PrintWriter printf(Locale loc, String cadFmt, Object ... args) La primera versión escribe args en el flujo que invoca en el formato especificado por cadFmt, empleando la configuración de región e idioma local predeterminada. La segunda le permite especificar una configuración. Ambas devuelven el PrintWriter que invoca. En general, printf( ) trabaja de una manera similar al método format( ) definido por Formatter. La cadFmt consta de dos tipos de elementos. El primer tipo está compuesto por caracteres que simplemente se escriben en el flujo. El segundo tipo contiene especificadores de formato que definen la manera en que argumentos subsecuentes, especificados por args, están formados. (Consulte la revisión general de Formatter que se presentó al principio del capítulo, para conocer más detalles). Se lanzará una IllegalFormatException si un especificador de formato está mal formado, si no coincide con su argumento correspondiente o si hay más especificadores de formato que argumentos. Debido a que System.out es un PrintStream, puede llamar a printf( ) en System.out. Por tanto, printf( ) puede usarse en lugar de println( ) cuando se escribe en la consola cada vez que se desea salida formada. Esto hace que resulte muy fácil desplegar salida formada. Otro muy buen uso de printf( ) es escribir salida formada en un archivo. Sólo envuelva FileOutputStream en un PrintStream, o un FileWriter en un PrintWriter, y luego llame a printf( ) en ese flujo. Ejemplo En el siguiente ejemplo se usa printf( ) para escribir varios tipos diferentes de datos formados en la consola. // Usa printf( ) para desplegar varios tipos de datos formados. import java.util.*; class DemoImprimir { public static void main(String args[]) { // Primero use el método printf( ) de PrintStream. System.out.printf("Dos d\u00a1gitos decimales: %.2f\n", 10.0/3.0); System.out.printf("Uso de separadores de grupo: %,.2f\n\n", 1546456.87); System.out.printf("%10s %10s %10s\n", "Valor", "Ra\u00a1z", "Cuadrado"); for(double i=1.0; i < 20.0; i++) System.out.printf("%10.2f %10.2f %10.2f\n", i, Math.sqrt(i), i*i); System.out.println( ); Calendar cal = Calendar.getInstance( ); www.fullengineeringbook.net Capítulo 4: Formato de datos 145 System.out.printf("Hora y fecha actuales: %tc\n", cal); } } Aquí se muestra la salida: Dos dígitos decimales: 3.33 Uso de separadores de grupo: 1,546,456.87 Valor 1.00 2.00 3.00 4.00 5.00 6.00 7.00 8.00 9.00 10.00 11.00 12.00 13.00 14.00 15.00 16.00 17.00 18.00 19.00 Raíz 1.00 1.41 1.73 2.00 2.24 2.45 2.65 2.83 3.00 3.16 3.32 3.46 3.61 3.74 3.87 4.00 4.12 4.24 4.36 Cuadrado 1.00 4.00 9.00 16.00 25.00 36.00 49.00 64.00 81.00 100.00 121.00 144.00 169.00 196.00 225.00 256.00 289.00 324.00 361.00 Hora y fecha actuales: dom mar 30 21:46:56 CST 2008 Ejemplo adicional Un muy buen uso para printf( ) consiste en crear una estampa de tiempo. Las estampas de tiempo son necesarias en muchas situaciones de programación. Por ejemplo, tal vez quiera imprimir la fecha y la hora en que se generó un reporte, o mantener un archivo de registro que guarde las horas en que ocurrieron los eventos. Cualquiera que sea la razón, la creación de una estampa de tiempo al usar printf( ) es un asunto fácil. En el siguiente ejemplo se muestra una manera de hacerlo. El programa define un método llamado estampaTiempo( ) que da salida a la hora y la fecha actuales (además de un mensaje opcional) al PrintWriter que se pasa. Tome en cuenta la poca cantidad de código que se necesita crear y la salida de una estampa de tiempo. El programa demuestra su uso al escribir estampas de tiempo en un archivo llamado archreg.txt. Una estampa de tiempo se escribe cuando el archivo se abre, y otra cuando se cierra. // Usa printf( ) para crear una estampa de tiempo. import java.io.*; import java.util.*; class EstampaTiempo { // Da salida a una estampa de tiempo al PrintWriter www.fullengineeringbook.net 146 Java: Soluciones de programación // especificado. Puede anteceder la estampa con un mensaje // al pasar una cadena a msj. Si no se desea un mensaje, // pasa una cadena vacía. static void estampaTiempo(String msj, PrintWriter pw) { Calendar cal = Calendar.getInstance( ); pw.printf("%s %tc\n", msj, cal); } public static void main(String args[]) { // Crea un PrintWriter que está vinculado a un archivo. PrintWriter pw; try { pw = new PrintWriter(new FileWriter("archreg.txt", true)); } catch(IOException exc) { System.out.println("No se puede abrir archreg.txt"); return; } estampaTiempo("Archivo abierto", pw); try { Thread.sleep(1000); // inactivo por 1 segundo } catch(InterruptedException exc) { pw.printf("Inactividad interrumpida"); } estampaTiempo("Archivo cerrado", pw); pw.close( ); // Cuando se usa un PrintWriter, busque errores al // llamar a checkError( ). if(pw.checkError( )) System.out.println("Error de E/S."); } } Después de que ejecuta este programa, archreg.txt contendrá la hora y fecha en que se abrió y cerró el archivo. He aquí un ejemplo: Archivo abierto lun mar 31 22:30:25 CST 2008 Archivo cerrado lun mar 31 22:30:26 CST 2008 Opciones PrintStream define el método format( ), que es una opción de printf( ). Tiene estas formas generales: PrintStream format(String cadFmt, Object ... args) PrintStream format(Locale loc, String cadFmt, Object ... args) Funciona exactamente como printf( ). PrintWriter también define el método format( ), como se muestra aquí: www.fullengineeringbook.net Capítulo 4: Formato de datos 147 PrintWriter format(String cadFmt, Object ... args) PrintWriter format(Locale loc, String cadFmt, Object ... args) También funciona exactamente igual que printf( ). Forme fecha y hora con DateFormat Componentes clave Clases Métodos java.txt.DateFormat static final DateFormat getDateInstance(int fmt) static final DateFormat getTimeInstance(int fmt) final String format(Date f) java.util.Date La clase DateFormat ofrece una opción de las capacidades proporcionadas por Formatter cuando se forma el tiempo y la fecha. DateFormat es sensible a la configuración de región e idioma local, que significa que puede formar información de fecha y hora para varios idiomas y países. DateFormat está empaquetado en java.text. Es una clase abstracta. Sin embargo, proporciona varios métodos de que devuelven objetos de DateFormat, que extiende Format (que es una clase abstracta que define los métodos de formación básicos.) Es una superclase de SimpleDateFormat. NOTA DateFormat también le da la capacidad de analizar sintácticamente cadenas que contienen información de fecha y hora en objetos de Date. Por tanto, tiene capacidades más allá de la simple formación. Paso a paso Hay varias maneras de formar la fecha usando DateFormat. En esta solución se dan los siguientes pasos: 1. Obtenga una instancia de DateFormat al llamar al método estático getDateInstance( ), especificando el formato de fecha deseado. 2. Obtenga una instancia de Date que contiene la fecha que habrá de formarse. 3. Produzca una cadena que contiene un valor de fecha formado al llamar al método format( ), especificando el objeto de Date. www.fullengineeringbook.net 148 Java: Soluciones de programación Hay varias maneras de formar la hora al usar DateFormat. En esta solución se dan los siguientes pasos: 1. Obtenga una instancia de DateFormat al llamar al método estático getTimeInstance( ), especificando el formato de hora deseado. 2. Obtenga una instancia de Date que contiene la hora que habrá de formarse. 3. Produzca una cadena que contiene un valor de fecha formado al llamar al método format( ), especificando el objeto de Date. Análisis Para obtener un objeto de DateFormat adecuado para la formación de fecha, llame al método estático getDateInstance( ). Está disponible en varias formas. La que se usa en esta solución es static final DateFormat getDateInstance(int fmt) Para obtener un objeto de DateFormat, adecuado para formar la hora, use getTimeInstance( ). También está disponible en varias versiones. La que se usa en esta solución es static final DateFormat getTimeInstance(int fmt) Para ambos métodos, el argumento fmt debe tener uno de los siguientes valores: DEFAULT, SHORT, MEDIUM, LONG o FULL. Se trata de constantes int definidas por DateFormat. Hace que se presenten diferentes detalles acerca de la fecha y hora cuando se forman. Los formatos de fecha y hora son sensibles a convenciones de idioma y país. En las versiones de getDateInstance( ) y getTimeInstance( ) que se acaban de mostrar usan la configuración actual de región e idioma local (es decir, predeterminado). Otras versiones le permiten especificar explícitamente la configuración local. A continuación, obtenga un objeto de java.util.Date que contiene la fecha o la hora que habrá de obtenerse. Una manera es crear un objeto de Date al usar este constructor: Date( ) Esto crea un objeto de Date que contiene la fecha y hora actuales del sistema. Una vez que haya obtenido las instancias de DateFormat y Date, puede formar una fecha u hora al llamar a format( ). Hay varias formas de este método. Aquí se muestra el que usaremos: final String format(Date f) El argumento es un objeto de Date que habrá de desplegarse. El método devuelve una cadena que contiene la información formada. Ejemplo En el siguiente ejemplo se muestra cómo formar información de fecha y hora. Empieza con la creación de un objeto de Date. Esto captura la información de fecha y hora actuales. Luego da salida a las formas corta y larga de la fecha y la hora para la configuración de región e idioma local (que es México en la salida de ejemplo). // Despliega formatos de fecha y hora cortos y largos. import java.text.*; import java.util.*; www.fullengineeringbook.net Capítulo 4: Formato de datos 149 class DemoFormatoFecha { public static void main(String args[]) { Date fecha = new Date( ); DateFormat df; df = DateFormat.getDateInstance(DateFormat.SHORT); System.out.println("Forma corta: " + df.format(fecha)); df = DateFormat.getDateInstance(DateFormat.LONG); System.out.println("Forma larga: " + df.format(fecha)); System.out.println( ); df = DateFormat.getTimeInstance(DateFormat.SHORT); System.out.println("Forma corta: " + df.format(fecha)); df = DateFormat.getTimeInstance(DateFormat.LONG); System.out.println("Forma larga: " + df.format(fecha)); } } Aquí se muestra la salida de ejemplo: Forma corta: 31/03/08 Forma larga: 31 de marzo de 2008 Forma corta: 11:35 PM Forma larga: 11:35:45 PM CST Opciones Hay formas adicionales de getDateInstance( ) y getTimeInstance( ). Puede usar el formato y la configuración de región e idioma local predeterminados al llamar a estas versiones: static final DateFormat getDateInstance( ) static final DateFormat getTimeInstance( ) Puede especificar el formato y la configuración de región y de idioma local al llamar a estas versiones: static final DateFormat getTimeInstance(int fmt, Locale local) static final DateFormat getDateInstance(int fmt, Locale local) El parámetro fmt es como se describió antes. El parámetro local se usa para especificar una configuración de región y de idioma local que regirá la conversión. En el programa de ejemplo, puede especificar explícitamente que la fecha habrá de formarse para Estados Unidos al sustituir esta llamada a getDateInstance( ): df = DateFormat.getDateInstance(DateFormat, SHORT, Locale.US); Para formar la fecha para Japón, puede usar df = DateFormat.getDateInstance(DateFormat, SHORT, Locale.JAPAN); Para conocer un análisis de la creación de objetos de Locale, consulte Especifique un idioma local usando Formatter. Si estará formando la hora y la fecha, puede usar getDateTimeInstance( ) para obtener un www.fullengineeringbook.net 150 Java: Soluciones de programación objeto de DateFormat que puede usarse para ambos. Tiene estas tres versiones: static final DateFormat getDateTimeInstance( ) static final DateFormat getDateTimeInstance(int fmtFecha, int fmtHora) static final DateFormat getDateTimeInstance(int fmtFecha, int fmtHora, Locale local) Aquí, fmtFecha especifica el formato de fecha y fmtHora especifica el formato de hora. La configuración de región y de idioma local se especifica con local. Si no se usan argumentos, entonces se aplican las opciones predeterminadas del sistema. Por ejemplo, al insertar la siguiente secuencia en el programa de ejemplo se causa que se desplieguen la fecha y la hora actuales en los formatos predeterminados: df = DateFormat.getDateTimeInstance( ); System.out.println("Forma predeterminada de fecha y hora: " + df.format(fecha)); He aquí una salida de ejemplo: Forma predeterminada de fecha y hora: 1/04/2008 12:49:16 AM Otra manera de formar la fecha y hora consiste en usar la clase java.text.SimpleDateFormat. Se trata de una subclase concreta de DateFormat. Una ventaja de esta clase es que le permite crear un patrón que describe cuáles piezas de la fecha u hora quiere desplegar. Esto le permite crear fácilmente formatos predeterminados de hora y fecha. (Consulte Forme fecha y hora con patrones empleando SimpleDateFormat). Por supuesto, también puede formar la fecha y hora empleando Formatter, o al llamar al método printf( ). (Consulte Forme fecha y hora empleando Formatter y Use printf( ) para desplegar datos formados). Forme fecha y hora con patrones empleando SimpleDateFormat Componentes clave Clases Métodos java.text.SimpleDateFormat final String format(Date f) java.util.Date void close() java.text.SimpleDateFormat es una subclase concreta de DateFormat. Le permite definir sus propios patrones de formación que se usan para desplegar información de fecha y hora. Como tal, representa una opción interesante a DateFormat y Formatter. www.fullengineeringbook.net Capítulo 4: Formato de datos 151 Paso a paso Para formar la fecha y la hora usando SimpleDateFormat, se requieren los pasos siguientes: 1. Cree un patrón que describa el formato de fecha y hora deseado. 2. Cree una instancia de SimpleDateFormat, que especifique el patrón. 3. Obtenga una instancia de Date que contiene la fecha que habrá de formarse. 4. Produzca una cadena que contiene el valor de fecha formado al llamar al método format( ), especificando el objeto de Date. Análisis SimpleDateFormat define varios constructores. Aquí se muestra el que usaremos aquí: SimpleDateFormat(String cadFmt) El argumento cadFmt describe un patrón que muestra cómo se desplegará la información de fecha y hora. Un patrón consta de un conjunto de símbolos que determina la información que se desplegará. En la tabla 4-4 se muestran estos símbolos y se da una descripción de cada uno. Símbolo Descripción a AM o PM d Día del mes (1-31) h Hora en AM/PM (1-12) k Horas de un día (1-24) m Minutos de una hora (0-59) s Segundos de un minuto (0-59) w Semanas de un año (1-52) y Año z Zona horaria D Día del año (1-366) E Día de la semana (por ejemplo, martes) F Día de la semana de un mes G Era, es decir (AC o DC) H Hora del día (0-23) K Hora en AM/PM (0-11) M Mes S Milisegundos de un segundo W Semana del mes (1-5) Z Zona horaria en formato RFC822 Tabla 4-4 Símbolos de formato para SimpleDateFormat www.fullengineeringbook.net 152 Java: Soluciones de programación En casi todos los casos, el número de veces que se repite un símbolo determina la manera en que se presentarán los datos. La información de texto se despliega en forma abreviada si la letra del patrón se repite menos de cuatro veces. De otra manera, se usa la forma no abreviada. Por ejemplo, un patrón zzzz despliega Hora estándar central y el patrón zzz despliega CST. En el caso de números, el número de veces que se repite la letra del patrón determina cuántos dígitos se presentan. Por ejemplo, hh:mm:ss puede presentar 01:51:15, pero h:m:s despliega el mismo valor como 1:51:15, M o MM causa que el mes se despliegue con uno o dos dígitos. Sin embargo, tres o más repeticiones de M causan que el mes se despliegue como una cadena de texto. He aquí un ejemplo de patrón: "dd MMM yyy hh:mm:ss" Este patrón incorpora el día del mes, el nombre del mes, el año y la hora (usando un reloj de 12 horas). He aquí un ejemplo de la salida que producirá: 08 Feb 2007 10:12:33 Observe que se usan dos dígitos para el día del mes y cuatro para el año. Además, tome nota de la forma corta del nombre del mes. Una vez que haya construido un SimpleDateFormat con el patrón deseado, obtenga una instancia de Date que contenga la fecha deseada y luego use format( ) para crear la salida formada. (Consulte Forme fecha y hora con DateFormat para conocer los detalles de la creación de una Date y el empleo de format( )). Ejemplo En el siguiente programa se muestran varios patrones de fecha y hora. // Demuestra SimpleDateFormat. import java.text.*; import java.util.*; public class DemoSDF { public static void main(String args[]) { Date fecha = new Date( ); SimpleDateFormat fechaSimple; // Hora en formato de 12 horas. fechaSimple = new SimpleDateFormat("hh:mm:ss a"); System.out.println(fechaSimple.format(fecha)); // Hora en formato de 24 horas. fechaSimple = new SimpleDateFormat("kk:mm:ss"); System.out.println(fechaSimple.format(fecha)); // Fecha y hora con mes como número. fechaSimple = new SimpleDateFormat("dd MMM yyyy hh:mm:ss a"); System.out.println(fechaSimple.format(fecha)); www.fullengineeringbook.net Capítulo 4: Formato de datos 153 // Fecha y hora con día y mes completamente escritos. fechaSimple = new SimpleDateFormat("EEEE MMMMM dd yyyy kk:mm:ss"); System.out.println(fechaSimple.format(fecha)); } } Aquí se muestra una salida de ejemplo: 01:52:18 AM 01:52:18 01 abr 2008 01:52:18 AM martes abril 01 2008 01:52:18 Opciones Aunque la capacidad de SimpleDateFormat de usar patrones es una característica excelente, a menudo Formatter es una mejor opción porque permite que una fecha u hora formada se integre fácilmente en una cadena formada más larga. Puede localizar el formato de fecha y hora al usar este constructor de SimpleDateFormat: SimpleDateFormat(String cadFmt, Locale loc) Aquí, loc especifica la configuración de región e idioma local. (Consulte Especifique un idioma local usando Formatter para conocer más detalles acerca de Locale). Forme valores numéricos con NumberFormat Componentes clave Clases Métodos java.text.NumberFormat static final NumberFormat getInstance( ) void setMaximumFractionDigits( int numDigitos) void setMinimumFractionDigits( int numDigitos) void setGroupingUsed( boolean usoGrupo) final String format(double val) Como se mencionó antes, algunas de las preguntas más frecuentes planteadas por los principiantes se relacionan con la formación de valores numéricos. La razón es fácil de comprender: el formato numérico predeterminado es muy simple, no hay separadores de grupo, y no se tiene control sobre el número de lugares decimales desplegados o la manera en que se representan los valores negativos. www.fullengineeringbook.net 154 Java: Soluciones de programación Además, en muchos casos querrá representar valores monetarios en un formato de moneda, que es, por supuesto, sensible a la configuración de región e idioma local. Por fortuna, Java proporciona varias maneras diferentes de formar valores numéricos, incluida la clase Formatter descrita antes. Sin embargo, en algunas situaciones, sobre todo las relacionadas con monedas, java.text.NumberFormat ofrece una opción útil. En esta solución se muestra el procedimiento básico requerido para usarlo. NumberFormat es una clase abstracta. Sin embargo, proporciona varios métodos de fábrica que devuelven objetos de NumberFormat. Esta clase extiende Format (que es una clase abstracta que define los métodos básicos de formación). Es una superclase de ChoiceFormat y DecimalFormat. NOTA NumberFormat también le da la capacidad de analizar sintácticamente cadenas numéricas en valores numéricos. Por tanto, tiene posibilidades mayores que las de la simple formación. Paso a paso Para formar un valor numérico mediante NumberFormat se requieren estos pasos: 1. Obtenga una instancia de NumberFormat al llamar a getInstance( ). 2. Ajuste el formato al llamar a varios métodos definidos por NumberFormat. 3. Produzca una cadena que contenga el valor formado al llamar al método format( ) definido por NumberFormat. Análisis Para obtener un objeto de NumberFormat, llame al método estático getInstance( ). Hay dos formas definidas por NumberFormat. La más simple (y la que usaremos) se muestra a continuación: static final NumberFormat getInstance( ) Devuelve una instancia de NumberFormat para la configuración actual de región y de idioma local. NumberFormat define varios métodos que le permiten determinar cómo se forma un valor numérico. Aquí se muestran los que usaremos: void setMaximumFractionDigits(int numDigitos) void setMinimumFractionDigits(int numDigitos) void setGroupingUsed(boolean usoGrupo) Para establecer el número máximo de dígitos desplegados a la derecha del punto decimal, llame a setMaximumFractionDigits( ), pasando el número de dígitos a numDigitos. Para establecer el número mínimo de dígitos desplegados a la derecha del punto decimal, llame a setMinimumFractionDigits( ), pasando el número de dígitos a numDigitos. Por ejemplo, para asegurar que siempre se desplieguen dos lugares decimales, utilizaría esta secuencia de llamadas (donde nf es una referencia a un objeto de NumberFormat); nf.setMinimumFractionDigits(2); nf.setMaximumFractionDigits(2); www.fullengineeringbook.net Capítulo 4: Formato de datos 155 Cuando el número de dígitos fraccionales es menor de los contenidos en el valor, el resultado se redondea Como opción predeterminada, el separador de grupo (que es una coma en español de México) se inserta cada tres dígitos a la izquierda del punto decimal. Puede eliminar el separador de grupo al llamar a setGroupingUsed( ), que se muestra aquí, con un argumento falso: void setGroupingUsed(boolean usoGrupos) Una vez que haya configurado la instancia de NumberFormat, puede formar un valor al llamar a format( ). Hay varias formas de este método. Aquí se muestra el que usaremos: final String format(double val) Devuelve una versión legible para el ser humano del valor pasado a val en el formato que haya especificado. Ejemplo Con el siguiente ejemplo se demuestra la formación de valores numéricos mediante la clase NumberFormat. // Usa java.text.NumberFormat para formar algunos valores numéricos. import java.text.NumberFormat; class DemoFormatoNumero { public static void main(String args[]) { NumberFormat nf = NumberFormat.getInstance( ); System.out.println("Formato predeterminado: " + nf.format(1234567.678)); // Establece el formato en dos lugares decimales. nf.setMinimumFractionDigits(2); nf.setMaximumFractionDigits(2); System.out.println("Formato con dos lugares decimales: " + nf.format(1234567.678)); nf.setGroupingUsed(false); System.out.println("Formato sin agrupamientos: " nf.format(1234567.678)); + // Observe que se proporcionan dos lugares // decimales, aunque no todos los dígitos // están presentes en estos casos. System.out.println("Observe dos lugares decimales: " + nf.format(10.0) + ", " + nf.format(–1.8)); } } www.fullengineeringbook.net 156 Java: Soluciones de programación Aquí se muestra la salida para este programa: Formato Formato Formato Observe predeterminado: 1,234,567.678 con dos lugares decimales: 1,234,567.68 sin agrupamientos: 1234567.68 dos lugares decimales: 10.00, –1.80 Opciones Para obtener un control más detallado sobre la formación, incluida la capacidad de especificar un patrón de formato, pruebe la clase DecimalFormat, que es una subclase de NumberFormat (Consulte Forme valores numéricos con patrones empleando DecimalFormat). El método format( ) usado por la solución forma valores de punto flotante. Puede formar valores enteros empleando esta versión de format( ): final String format(long val) Aquí, val es el valor que habrá de formarse. Hay otras versiones de format( ) que le permiten especificar un StringBuffer para escribir la salida y una posición de campo. Puede formar en relación con una configuración de región e idioma local empleando esta versión de getInstance( ): static NumberFormat getInstance(Locale loc) Esta configuración se pasa vía loc. (Locale está descrita en Especifique un idioma local usando Formatter.) Puede formar valores en su formato monetario estándar empleando getCurrencyInstance( ). (Consulte Forme valores monetarios usando NumberFormat). Puede formar valores como porcentajes empleando getPercentInstance( ). Forme valores monetarios usando NumberFormat Componentes clave Clases Métodos java.text.NumberFormat static final NumberFormat getCurrencyInstance( ) final String format(double val) Una de las características especialmente útiles de NumberFormat es la capacidad de formar valores monetarios. Para ello, simplemente obtenga un objeto de NumberFormat al llamar a getCurrencyInstance( ) en lugar de getInstance. Subsecuentes llamadas a format( ) darán como resultado el valor que se forme con las convenciones de moneda de la configuración regional y de idioma local (o especificada). www.fullengineeringbook.net Capítulo 4: Formato de datos 157 Paso a paso Para formar valores monetarios empleando NumberFormat se requieren estos pasos: 1. Obtenga una instancia de NumberFormat al llamar a getCurrencyInstance( ). 2. Produzca una cadena que contenga el valor formado al llamar al método format( ). Análisis Para formar un valor como moneda, use getCurrencyInstance( ) para obtener un objeto de NumberFormat. Tiene dos formas. La usada aquí es static final NumberFormat getCurrencyInstance( ) Devuelve un objeto que representa la moneda de la configuración actual de región y de idioma local. Cuando se forman monedas, los dígitos fraccionales, agrupamiento y símbolos de moneda se proporcionan automáticamente. (¡Realmente es así de fácil!). Una vez que haya configurado la instancia de NumberFormat, puede formar un valor al llamar a format( ), lo que se describió en la solución anterior. Ejemplo En el siguiente ejemplo se muestra cómo formar valores como moneda al usar la clase NumberFormat. // Usa java.text.NumberFormat para formar un valor de moneda. import java.text.NumberFormat; import java.util.*; class DemoFormatoMoneda { public static void main(String args[]) { NumberFormat nf = NumberFormat.getCurrencyInstance( ); System.out.println("1989.99 y –210.5 en formato de moneda: " + nf.format(1989.99) + " " + nf.format(–210.5)); } } Aquí se muestra la salida de este programa: 1989.99 y –210.5 en formato de moneda: $1,989.99 ($210.50) Opciones Puede formar en relación con la configuración regional y de idioma al usar esta versión de getCurrencyInstance( ): static NumberFormat getCurrencyInstance(Locale loc) La configuración se pasa mediante loc. (Locale se describe en Especifique un idioma local usando Formatter). www.fullengineeringbook.net 158 Java: Soluciones de programación Forme valores numéricos con patrones empleando DecimalFormat Componentes clave Clases Métodos java.text.DecimalFormat final String format(double val) java.text.DecimalFormat es una subclase concreta de NumberFormat. Le permite definir sus propios patrones de formación que se usan para desplegar información numérica, incluidos valores enteros y de punto flotante. Como tales, ofrecen una opción interesante a NumberFormat y Formatter. Paso a paso Para formar un valor numérico empleando DecimalFormat se requieren los pasos siguientes: 1. Cree un patrón que describa el formato numérico deseado. 2. Cree una instancia de DecimalFormat, especificando el patrón. 3. Produzca una cadena que contenga un valor formado al llamar al método format( ), especificando el valor que habrá de formarse. Análisis DecimalFormat define tres constructores. Aquí se muestra el que se usará: DecimalFormat(String cadFmt) El argumento cadFmt describe un patrón que muestra cómo habrá de desplegarse un valor numérico. Un patrón consta de símbolos que determinan la manera en que se desplegará el valor. En la tabla 4-5 se muestran los símbolos y se da una descripción de cada uno. Símbolo Descripción . Punto decimal , Separador de grupo # Dígito, no se muestran los ceros al final 0 Dígito, se muestran los ceros al final – Menos % Muestra el valor como porcentaje (en otras palabras, multiplica el valor por 100) E La E en notación científica ‘ Convierte en cita un símbolo para usarlo como un carácter normal \u00a4 Símbolo de moneda apropiado para la configuración de región y de idioma local /u2030 Muestra el valor en términos de miles (en otras palabras, multiplica el valor por 1000) Tabla 4-5 Una muestra de símbolos de formación para DecimalFormat www.fullengineeringbook.net Capítulo 4: Formato de datos 159 Todos los patrones de DecimalFormat constan de dos subpatrones: uno para valores positivos y otro para negativos. Sin embargo, el subpatrón negativo debe especificarse de manera explícita. Si no está presente, entonces el patrón negativo consta del patrón positivo con un prefijo de signo menos. Los dos subpatrones están separados por un punto y coma (;). Una vez que se ha especificado un patrón, toda la salida numérica se formará para que coincida con el patrón. He aquí un ejemplo: "#,###.00;(#,###.00)" Esto crea un patrón que usa separadores de grupo cada tres dígitos y muestra ceros al final. También muestra valores negativos dentro de paréntesis. Una vez que haya construido un DecimalFormat con el patrón deseado, use format( ) (heredado de NumberFormat) para crear la salida formada. (Consulte Forme valores numéricos con NumberFormat para conocer más detalles). Ejemplo En el siguiente ejemplo se demuestra DecimalFormat. // Demuestra DecimalFormat. import java.text.*; public class DemoDF { public static void main(String args[]) { DecimalFormat df; // Usa separadores de grupo y muestra ceros al final. // Los valores negativos se muestran entre paréntesis. df = new DecimalFormat("#,###.00;(#,###.00)"); System.out.println(df.format(7123.00)); System.out.println(df.format(–7123.00)); // No muestra ceros al final. df = new DecimalFormat("#,###.##;(#,###.##)"); System.out.println(df.format(7123.00)); System.out.println(df.format(–7123.00)); // Despliega un porcentaje. df = new DecimalFormat("#%"); System.out.println(df.format(0.19)); System.out.println(df.format(–0.19)); // Despliega un valor de moneda. df = new DecimalFormat("\u00a4#,##0.00"); System.out.println(df.format(4232.19)); System.out.println(df.format(–4232.19)); } } www.fullengineeringbook.net 160 Java: Soluciones de programación Aquí se muestra la salida: 7,123.00 (7,123.00) 7,123 (7,123) 19% –19% $4,232.19 –$4,232.19 Opciones Aunque la capacidad de DecimalFormat para usar patrones puede facilitar algunos tipos de formato, a menudo Formatter es una mejor opción porque permite que un valor formado se integre fácilmente en una cadena formada más larga. NumberFormat, que es la superclase de DecimalFormat, ofrece varios formatos estándar que funcionan bien para muchas aplicaciones y que puede configurarse automáticamente para idiomas específicos. En muchos casos, esto será más conveniente que construir patrones manualmente. (Consulte Forme valores numéricos con NumberFormat). www.fullengineeringbook.net 5 CAPÍTULO Trabajo con colecciones L a estructura o marco conceptual de colecciones (Collections Framework) es, presumiblemente, el subsistema más poderoso en la API de Java, porque proporciona versiones listas para usar de las estructuras de datos de uso más amplio en programación. Por ejemplo, proporciona soporte a pilas, colas, matrices dinámicas y listas vinculadas. También define árboles, tablas de hash y mapas. Estos "motores de datos" facilitan el trabajo con grupos (es decir, colecciones) de datos. No es necesario, por ejemplo, escribir sus propias rutinas de listas vinculadas. Puede simplemente usar la clase LinkedList proporcionada por la estructura de colecciones. La estructura de colecciones tiene un profundo efecto sobre la manera en que están escritos los programas de Java. Debido a la facilidad con que puede emplearse una de las clases de la colección, apenas es necesario crear su propia solución personalizada. Mediante el uso de una colección estándar, se obtienen tres ventajas importantes: 1. El tiempo de desarrollo se reduce porque las colecciones están completamente probadas y listas para usarse. No necesita dedicar tiempo a desarrollar sus propias implementaciones personalizadas. 2. Las colecciones estándar están implementadas eficientemente. Como regla general, hay pocos beneficios (en caso de que los haya) en crear una implementación personalizada. 3. Su código es más fácil de mantener. Debido a que las colecciones estándar son parte de la API de Java, la mayoría de los programadores están familiarizados con ellas y con la manera en que operan. Por tanto, cualquier persona que trabaje con su código comprenderá un método basado en colecciones. Debido a estas ventajas, las colecciones se han vuelto una parte integral de muchos programas de Java. Las colecciones son un tema extenso. Ningún capítulo único puede demostrar todas sus características o explorar todos sus detalles. Como resultado, el enfoque de este capítulo está en las soluciones que demuestran varias técnicas clave, como el uso de un comparador, la iteración de una colección, la creación de colecciones sincronizadas, etc. El capítulo empieza con una revisión general de las clases e interfaces que abarcan el núcleo de la estructura de colecciones. He aquí las soluciones de este capítulo: • Técnicas básicas de colecciones • Trabaje con listas • Trabaje con conjuntos www.fullengineeringbook.net 161 162 Java: Soluciones de programación • Use Comparable para almacenar objetos en una colección ordenada • Use un Comparator con una colección • Itere en una colección • Cree una cola o una pila empleando Deque • Invierta, gire y ordene al azar una List • Ordene una List y busque en ella • Cree una colección comprobada • Cree una colección sincronizada • Cree una colección inmutable • Técnicas básicas de Map • Convierta una lista de Properties en un HashMap NOTA Aunque este capítulo presenta una revisión general de la estructura de colecciones que es suficiente para las soluciones de este capítulo, no se analiza el marco conceptual de manera detallada. Puede encontrarse una cobertura adicional de las colecciones en mi libro Java: Manual de referencia, séptima edición. Revisión general de las colecciones Las colecciones no siempre han sido parte de Java. Originalmente, Java dependía de clases como Dictionary, HashTable, Vector, Stack y Properties para proporcionar las estructuras básicas de datos. Aunque estas clases son muy útiles, no eran parte de un todo unificado. Para remediar esta situación, Java 1.2 agregó el marco conceptual de colecciones, que estandarizó la manera en que su programa maneja los grupos de objetos. Cada versión subsecuente de Java ha agregado y mejorado esta importante API. El núcleo del marco conceptual de colecciones está empaquetado en java.util. Contiene las interfaces que definen colecciones y proporciona varias implementaciones concretas de estas interfaces. Se trata de colecciones que los programadores normalmente consideran cuando usan el término "Collections Framework" y son el eje de este capítulo. Además de las colecciones definidas en java.util, hay otras varias colecciones en java.util. concurrent. Estas colecciones son relativamente nuevas y se agregaron en Java 5. Soportan programación concurrente, pero operan de manera diferente de las demás colecciones. Debido a que la programación concurrente es un tema en sí mismo, no se incluyen las colecciones concurrentes en este capítulo. (Consulte el capítulo 7 para conocer soluciones que usan java.util. concurrent). La estructura de colecciones se define con tres características principales • Un conjunto de interfaces estándar que definen la funcionalidad de una colección • Implementaciones concretas de las interfaces • Algoritmos que operan en las colecciones Las interfaces de la colección determinan las características de ésta. En la parte superior de la jerarquía de la interfaz está Collection, que define las características comunes a todas las colecciones. Las subinterfaces agregan atributos relacionados a tipos específicos de colecciones. Por ejemplo, la interfaz Set especifica la funcionalidad de un conjunto, que es una colección de www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 163 elementos únicos (es decir, no duplicados). Varias clases, como ArrayList, HashSet y LinkedList proporcionan implementaciones concretas de las interfaces de la colección. Estas implementaciones concretas proporcionan soluciones "integradas" a la mayor parte de las tareas de almacenamiento y recuperación de datos. Los algoritmos son métodos estáticos definidos dentro de la clase Collections que operan sobre las colecciones. Por ejemplo, hay algoritmos que buscan, ordenan o revierten colecciones. En esencia, los algoritmos proporcionan medios estándar de manipulación de colecciones. Cuando trabaja con una colección, a menudo querrá recorrer en bucle sus elementos. Una manera de hacerlos es con un iterador, que está definido por la interfaz Iterator. Un iterador ofrece una manera estandarizada y general de acceder a los elementos de una colección, de una en una. En otras palabras, un iterador proporciona un medio para enumerar el contenido de una colección. Debido a que cada colección implementa Iterator, puede usarse un iterador para recorrer en bucle los elementos de cualquier clase de la colección. Otra característica definida por la estructura de colecciones es el mapa. Un mapa almacena pares clave/valor, aunque los mapas son parte de la estructura de colecciones, no son "colecciones" en el estricto sentido del término, porque no implementan la interfaz Collection. Sin embargo, puede obtener una vista de colección de un mapa. Esa vista contiene los elementos del mapa almacenados en una colección. Por tanto, puede procesar el contenido de un mapa como una colección, si lo decide. Tres cambios recientes Como tal vez lo sepa, el lenguaje Java experimentó un cambio sustancial cuando se agregaron varias características nuevas en Java 5. Tres de estas características tuvieron un profundo impacto en la estructura de colecciones: elementos genéricos, autoencuadre y el estilo for-each del bucle for. Aunque estas características son ahora una parte establecida de la programación en Java, no todos los programadores están conscientes de la manera en que han impactado a las colecciones. Debido a que las soluciones de este capítulo hacen uso de estas características, es necesario un breve análisis. Con la versión de Java 5, toda la API de Java, incluida la estructura de colecciones, experimentó una reingeniería para adecuarse a los elementos genéricos. Como resultado, hoy en día todas las colecciones son genéricas, y muchos de los métodos que operan en colecciones toman parámetros de tipo genérico. Los elementos genéricos mejoran las colecciones al agregar seguridad de tipo. Antes de los elementos genéricos, todas las colecciones se almacenaban en las referencias a Object, lo que significa que cualquier colección podría almacenarse en cualquier tipo de objeto. Por tanto, era posible almacenar por accidente tipos incompatibles en una colección. Al hacer esto daba como resultado errores de falta de correspondencia de tipo en tiempo de ejecución. Con los elementos genéricos, el tipo de datos que se está almacenado se especifica explícitamente, y pueden evitarse la falta de correspondencia de tipo en tiempo de ejecución. Las características de autoencuadre/desencuadre facilitan el almacenamiento de tipos primitivos en colecciones. Una colección sólo puede almacenar referencias, no tipos primitivos. En el pasado, si quería almacenar un tipo primitivo en una colección, tenía que incluirlo manualmente en su envoltura de tipo. Por ejemplo, para almacenar un valor int, necesitaba crear un objeto Integer que contenía ese valor. Cuando se recuperaba el valor, necesitaba desencuadrarse manualmente (usando una conversión explícita) en su tipo primitivo apropiado. Debido al autoencuadre/desencuadre, Java puede ahora realizar en forma automática el encuadre y el desencuadre apropiados necesarios cuando se almacenan o recuperan tipos primitivos. No es necesario realizar manualmente estas operaciones. Esto facilita mucho el almacenamiento de tipos primitivos en una colección. Todas las clases de colecciones implementan ahora la interfaz Iterable. Esto permite que se recorra una colección en bucle mediante el uso del estilo for-each del bucle for. En el pasado. La iteración www.fullengineeringbook.net 164 Java: Soluciones de programación de una colección requería el uso de un iterador. Aunque los iteradores aún son necesarios para algunos usos, en muchos casos los bucles basados en iterador pueden reemplazarse con bucles for. RECUERDE Debido a que los ejemplos de código usan de manera extensa estas características más recientes, debe usar JDK 5 o posterior para compilarlos y ejecutarlos. Las interfaces de Collection La estructura de colecciones está definida por un conjunto de interfaces, que se muestran en la tabla 5-1. En la parte superior de la jerarquía de las interfaces se encuentra una Collection. Todas las colecciones deben implementarla. De Collection se derivan varias subinterfaces, como List y Set, que definen tipos específicos de colecciones, como listas y conjuntos. Además de las interfaces de colección, las colecciones también usan las interfaces Comparator, RandomAccess, Iterator y ListIterator. Comparator define la manera en que dos objetos se comparan. Iterator y ListIterator enumeran los objetos dentro de una colección. Al implementar RandomAccess, una lista indica que soporta acceso eficiente, aleatorio a sus elementos. La estructura de colecciones soporta colecciones modificables e inmutables. Para permitir esto, las interfaces de colección permiten métodos que modifican una colección para que sea opcional. Si se trata de usar uno de estos métodos en una colección inmutable, se lanza una UnsupportedOperationException. Todas las colecciones integradas son modificables, pero es posible obtener una vista inmutable de una colección. (La obtención de una colección inmutable se describe en Cree una colección inmutable). La interfaz Collection La interfaz Collection especifica la funcionalidad común a todas las colecciones, y cualquier clase que defina una colección debe implementarla. Collection es una interfaz genérica que tiene esta declaración: Interface Collection<E> Interfaz Descripción Collection Le permite trabajar con grupos de objetos. Collection se encuentra en la parte superior de la jerarquía de colecciones, y todas las clases de la colección deben implementarla. Deque Extiende Queue para manejar una cola de doble final. List Extiende Collection para que maneje secuencias (listas de objetos). NavigableSet Extiende SortedSet para manejar la recuperación de elementos basados en búsquedas de las coincidencias más cercanas. Queue Extiende Collection para manejar tipos especiales de listas en que los elementos sólo se eliminan de la cabeza. Set Extiende Collection para manejar conjuntos, que deben contener elementos únicos. SortedSet Extiende Set para manejar conjuntos ordenados. Tabla 5-1 Las interfaces de Collection www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 165 Aquí, E especifica el tipo de objetos que contendrá la colección. Collection extiende la interfaz Iterable. Esto significa que todas las colecciones pueden recorrerse en bucle mediante el uso del estilo for-each del bucle for. (Sólo las clases que implementan Iterable puede iterarse con for). Los métodos declarado por Collection se resumen en la tabla 5-2. Son posibles varias excepciones. Una UnsupportedOperationException se lanza si se hace un intento de modificar una colección inmutable. Una ClassCastExcepction se genera cuando un objeto es incompatible con otro, como cuando se hace un intento por agregar un objeto incompatible a una colección. Se lanza una NullPointerException si se hace un intento de almacenar un objeto null y no se permiten elementos null en la colección. Una IllegalArgumentException se lanza si se usa un argumento no válido. Una IllegalStateException se lanza si se hace un intento de agregar un elemento a una columna de longitud fija que está llena. Método Descripción boolean add(E obj) Agrega obj a la colección que invoca. Devuelve verdadero si se agregó obj a la colección. Devuelve falso si obj ya es miembro de la colección y ésta no permite duplicados. boolean addAll(Collection<? extends E> col) Agrega todos los elementos de col a la colección que invoca. Devuelve verdadero si la operación tuvo éxito (es decir, se agregaron los elementos). De otra manera, devuelve falso. void clear( ) Elimina todos los elementos de la colección que invoca. boolean contains(Object obj) Devuelve verdadero si obj es un elemento de la colección que invoca. De otra manera, devuelve falso. boolean containsAll(Collection<?> col) Devuelve verdadero si la colección que invoca contiene todos los elementos de col. De otra manera, devuelve falso. boolean equals(Object obj) Devuelve verdadero si la colección que invoca y obj son iguales. De otra manera, devuelve falso. El significado preciso de "igualdad" puede diferir de una colección a otra. Por ejemplo, puede implementarse equals( ) para que compare los valores de elementos almacenados en la colección. Como opción, equals( ) podría comparar referencias a esos elementos. int hashCode( ) Devuelve el código de hash para la colección que invoca. boolean isEmpty( ) Devuelve verdadero si la colección que invoca está vacía. De otra manera, devuelve falso. Iterator<E> iterator( ) Devuelve un iterador para la colección que invoca. boolean remove(Object obj) Elimina un elemento que coincide con obj de la colección que invoca. Devuelve verdadero si se eliminó el elemento. De otra manera, devuelve falso. boolean removeAll(Collection<?> col) Elimina todos los elementos de col de la colección que invoca. Devuelve verdadero si la colección cambió (es decir, se eliminaron elementos). De otra manera, devuelve falso. Tabla 5-2 El método definido por Collection www.fullengineeringbook.net 166 Java: Soluciones de programación Método Descripción boolean reatainAll(Collection<?> col) Elimina todos los elementos de la colección que invoca, excepto los de col. Devuelve verdadero si la colección cambió (es decir, se eliminaron elementos). De otra manera, devuelve false. int size( ) Devuelve el número de elementos contenidos en la colección que invoca. Object[ ] toArray( ) Devuelve una matriz que contiene todos los elementos almacenados en la colección que invoca. Los elementos de la matriz son copias de los elementos de la colección. <T> T[ ] toArray(T matriz[ ]) Devuelve una matriz que contiene los elementos de la colección que invoca. Los elementos de la matriz son copias de los de la colección. Si el tamaño de la matriz es igual al número de elementos, se devuelven en matriz. Si el tamaño de la matriz es menor que el número de elementos, se asigna una nueva matriz del tamaño necesario y se devuelve. Si el tamaño de la matriz es mayor que el número de elementos, el elemento de la matriz que sigue al último elemento de la colección se establece en null. Se lanza una ArrayStoreException si cualquier elemento de la colección tiene un tipo que no es un subtipo de la matriz. Tabla 5-2 El método definido por Collection (continuación) La interfaz List La interfaz List extiende Collection y declara el comportamiento de una colección que almacena una secuencia de elementos. Los elementos pueden insertarse (o es posible acceder a ellos) por su posición en la lista, empleando un índice basado en el cero. Una lista puede contener elementos duplicados. List es una interfaz genérica que tiene esta declaración: interface List<E> Aquí, E especifica el tipo de objetos que contendrá la lista. Además de los métodos definidos por Collection, List define algunos propios, que se resumen en la tabla 5-3. Ponga especial atención a los métodos get( ) y set( ). Proporcionan acceso a los elementos de la lista mediante un índice. El método get( ) obtiene el objeto almacenado en una ubicación específica, y set( ) asigna un valor al elemento especificado en la lista. List especifica que el método equals( ) debe comparar el contenido de dos listas, devolviendo verdadero sólo si son exactamente iguales. (En otras palabras, deben contener los mismos elementos, en la misma secuencia). Si no, equals( ) devuelve falso. Por tanto, cualquier colección que implementa List también implementa equals( ) de esta manera. Varios de estos métodos lanzarán una UnsupportedOperationException si se hace un intento por modificar una colección inmutable, y se genera una ClassCastException cuando un objeto es incompatible con otro, como cuando se hace un intento por agregar un objeto incompatible a una colección. Se lanza una NullPointerException si se hace un intento de almacenar un objeto null y no se permiten elementos null en la lista. Se lanza una IllegalArgumentException si se usa un argumento no válido. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 167 Método Descripción void add(int ind, E obj) Inserta obj en la lista que invoca en el índice pasado en ind. Cualquier elemento existente en el punto de inserción o más allá se desplaza hacia arriba. Por tanto, no se sobrescriben elementos. boolean addAll(int ind, Collection<? extends E> c) Inserta todos los elementos de c en la lista que invoca en el índice pasado en ind. Cualquier elemento existente en el punto de inserción o más allá se desplaza hacia arriba. Por tanto, no se sobrescriben elementos. Devuelve verdadero si la lista que invoca cambia y devuelve falso, de otra manera. E get (int ind) Devuelve el objeto almacenado en el índice especificado dentro de la colección que invoca. int indexOf(Object obj) Devuelve el índice de la primera instancia de obj en la lista que invoca. Si obj no es un elemento de la lista, se devuelve –1. int lastIndexOf(Object obj) Devuelve el índice de la última instancia de obj en la lista que invoca. Si obj no es un elemento de la lista, se devuelve –1. ListIterator<E> listIterator( ) Devuelve un iterador al principio de la lista que invoca. ListIterator<E> listIterator(int ind) Devuelve un iterador a la lista que empieza en el índice especificado. E remove(int ind) Elimina el elemento en la posición ind de la lista que invoca y devuelve el elemento eliminado. La lista resultante se compacta. Es decir, los índices de elementos subsecuentes disminuyen en uno. E set(int ind, E obj) Asigna obj a la ubicación especificada por ind dentro de la lista que invoca. List<E> subList(int inicio, int ļ¬nal) Devuelve una lista que incluye elementos de inicio a ļ¬n –1 en la lista que invoca. Los elementos en la lista devuelta también hacen referencia al objeto que invoca. Tabla 5-3 Los métodos definidos por List La interfaz Set La interfaz Set define un conjunto. Extiende Collection y declara el comportamiento de una colección que no permite elementos duplicados. Por tanto, el método add( ) devuelve falso si se hace un intento por agregar elementos duplicados a un conjunto. No define métodos adicionales propios. Set es una interfaz genérica que tiene esta declaración: Interface Set<E> Aquí, E especifica el tipo de objeto que contendrá el conjunto. Set especifica que el método equals( ) debe comparar el contenido de dos conjuntos, devolviendo verdadero sólo si ambos contienen los mismos elementos. Si no, entonces equals( ) devuelve falso. Por tanto, cualquier colección que implemente Set, implementa equals( ) de esta manera. www.fullengineeringbook.net 168 Java: Soluciones de programación La interfaz SortedSet La interfaz SortedSet extiende Set y declara el comportamiento de un conjunto ordenado en orden ascendente. SortedSet es una interfaz genérica que tiene esta declaración: Interface SortedSet<E> Aquí, E especifica el tipo de objetos que contendrá el conjunto. Además de los métodos definidos por Set, la interfaz SortedSet declara los métodos resumidos en tabla 5-4. Estos métodos hacen que el procesamiento sea más conveniente. Varios métodos lanzan NoSuchElementException cuando no hay elementos en el conjunto que invoca. Se lanza una ClassCastException cuando un objeto es incompatible con los elementos de un conjunto. Se lanza una NullPointerException si se hace un intento de usar un objeto null y no se permiten elementos null en el conjunto. Se lanza una IllegalArgumentException si se usa un argumento no válido. La interfaz NavigableSet Una reciente adición a la estructura de colecciones es NavigableSet. Se añadió en Java 6 y extiende SortedSet. NavigableSet declara el comportamiento de una colección que da soporte a la recuperación de elemento con base en una coincidencia más cercana a un valor o varios valores determinados. NavigableSet es una interfaz genérica que tiene esta declaración: Interface NavigableSet<E> Aquí, E especifica el tipo de objetos que contendrá el conjunto. Además de los métodos que hereda de SortedSet, NavigableSet agrega los resumidos en la tabla 5-5. Se lanza una ClassCastException cuando un objeto es incompatible con los elementos de un conjunto. Se lanza una NullPointerException si se hace un intento de usar un objeto null y no se permiten elementos null en el conjunto. Se lanza una IllegalArgumentException si se usa un argumento no válido. Método Descripción Comparator<? Super E> comparator( ) Devuelve el comparador de un conjunto ordenado. Si se usa el ordenamiento natural para este conjunto, se devuelve null. E first( ) Devuelve el primer elemento del conjunto ordenado que invoca. SortedSet<E> headSet(E ļ¬n) Devuelve un SortedSet que contiene esos elementos menos que ļ¬nal que está contenido en el conjunto ordenado que invoca. Éste último también hace referencia a los elementos en el conjunto ordenado devuelto. E last( ) Devuelve el último elemento en el conjunto ordenado que invoca. SortedSet<E> subSet(E inicio, E ļ¬n) Devuelve un SortedSet que incluye los elementos que se encuentran entre inicio y ļ¬n–1. El objeto que invoca también hace referencia a los elementos en la colección devuelta. SortedSet<E> tailSet(E inicio) Devuelve un SortedSet que incluye los elementos mayores o iguales a inicio que están contenidos en el conjunto ordenado. El objeto que invoca también hace referencia a elementos en el conjunto devuelto. Tabla 5-4 Los métodos definidos por SortedSet www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 169 Método Descripción E ceiling(E obj) Busca en el conjunto el elemento más pequeño e tal que e >= obj. Si se encuentra ese elemento, se devuelve. De otra manera, se devuelve null. Iterator<E> descendingIterator( ) Devuelve un iterador que va del mayor al menor. En otras palabras, devuelve un iterador inverso. NavigableSet<E> descendingSet( ) Devuelve un NavigableSet que es lo inverso del conjunto que invoca. El conjunto resultante está respaldado por el conjunto que invoca. E floor(E obj) Busca en el conjunto el elemento más grande e tal que e <= obj. Si se encuentra ese elemento, se devuelve. De otra manera, se devuelve null. NavigableSet<E> headSet(E limiteSuperior, boolean incl) Devuelve un NavigableSet que incluye todos los elementos del conjunto que invoca que es menor que limiteSuperior. Si incl es true, entonces se incluye un elemento igual a limiteSuperior. El conjunto resultante está respaldado por el conjunto que invoca. E higher(E obj) Busca en el conjunto el elemento más pequeño e tal que e > obj. Si se encuentra ese elemento, se devuelve. De otra manera, se devuelve null. E lower(E obj) Busca en el conjunto el elemento más grande e tal que e < obj. Si se encuentra ese elemento, se devuelve. De otra manera, se devuelve null. E pollFirst( ) Devuelve el primer elemento, eliminando el elemento en el proceso. Debido a que el conjunto está ordenado, es el elemento con el valor menor. Se devuelve null si el conjunto está vacío. E pollLast( ) Devuelve el último elemento, eliminando el elemento en el proceso. Debido a que el conjunto está ordenado, es el elemento con el valor mayor. Se devuelve null si el conjunto está vacío. NavigableSet<E> subSet(E limiteInferior, boolean inclbajo, E, limiteSuperior, boolean inclalto) Devuelve un NavigableSet que incluye todos los elementos del conjunto que invoca que son mayores que limiteInferior y menores que limiteSuperior. Si inclbajo es verdadero, entonces se incluye un elemento igual a limiteInferior. Si inclalto es verdadero, entonces se incluye un elemento igual a limiteSuperior. El conjunto resultante es respaldado por el conjunto que invoca. NavigableSet<E> tailSet(E limiteInferior, boolean incl) Devuelve un NavigableSet que incluye todos los elementos del conjunto que invoca que son mayores que limiteInferior. Si incl es verdadero, entonces se incluye un elemento igual a limiteInferior. El conjunto resultante es respaldado por el conjunto que invoca. Tabla 5-5 Los métodos definidos por NavigableSet www.fullengineeringbook.net 170 Java: Soluciones de programación La interfaz Queue La interfaz Queue extiende Collection y declara el comportamiento de una cola. Las colas son a menudo listas primero en entrar primero en salir, pero hay tipos de colas en que el orden está basado en otros criterios. Queue es una interfaz genérica que tiene esta declaración: interface Queue<E> Aquí, E especifica el tipo de objetos que contendrá el conjunto. Además de los métodos que hereda Queue de Collection, define varios de su propiedad. Se muestran en la tabla 5-6. Varios métodos lanzan una ClassCastException cuando un objeto es incompatible con los elementos de la cola. Se lanza una NullPointerException si se hace un intento de almacenar un objeto null y no se permiten elementos null en la cola. Una IllegalArgumentException se lanza si se usa un argumento no válido. Se lanza una IllegalStateException si se hace un intento por agregar un elemento a una cola de longitud fija que está llena. Algunos métodos lanzan una NoSuchElementException si se hace un intento por eliminar un elemento de una cola vacía. Queue tiene varias características interesante. En primer lugar, los elementos sólo pueden eliminarse desde la cabeza de la cola. En segundo lugar, hay dos métodos que obtienen y eliminan elementos: poll( ) y remove( ). La diferencia entre ellos es que poll( ) devuelve null si la cola está vacía, pero remove( ) lanza una excepción NoSuchElementException. En tercer lugar, hay dos métodos, element( ) y peek( ), que obtienen, pero no eliminan el elemento en la cabeza de la cola. Sólo difieren en que element( ) lanza una excepción NoSuchElementException si la cola está vacía, pero peek( ) devuelve null. Por último, tome nota de que offer( ) sólo trata de agregar un elemento a una cola. Debido a que las colas de longitud fija son permitidas y ese tipo de cola podría estar lleno, offer( ) puede fallar. Si lo hace, offer( ) devuelve falso. Esto difiere de add( ) (heredado de Collection), que lanzará una IllegalStateException si se hace un intento por agregar un elemento a una cola llena, de longitud fija. Por tanto, Queue le da dos maneras de manejar estados de cola llena y vacía cuando realiza operaciones de cola: al manejar excepciones o al monitorear valores de devolución. Debe elegir el método más apropiado para su aplicación. Método Descripción E element( ) Devuelve el elemento en la cabeza de la cola. El elemento no se elimina. Lanza NoSuchElementException, si la cola está vacía. boolean offer(E obj) Trata de agregar obj a la cola. Devuelve verdadero si se agregó obj y falso de otra manera. E peek( ) Devuelve el elemento a la cabeza de la cola. Devuelve null si la cola está vacía. El elemento no se elimina. E poll( ) Devuelve el elemento a la cabeza de la cola, eliminando éste en el proceso. Devuelve null si la cola está vacía. E remove( ) Elimina el elemento a la cabeza de la cola, devolviendo el elemento en el proceso. Lanza NoSuchElementException, si la cola está vacía. Tabla 5-6 Los métodos definidos por Queue www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 171 La interfaz Deque Otra adición reciente a la estructura de colecciones es Deque. Se agregó en Java 6 y extiende Queue. Deque declara el comportamiento de una cola de doble extremo. Las colas de doble extremo funcionan como colas primero en entrar primero en salir, o como pilas último en entrar primero en salir. Deque es una interfaz genérica que tiene esta declaración: interface Deque<E> Aquí, E especifica el tipo de objetos que contendrá Deque. Además de los métodos que hereda de Queue, Deque agrega estos métodos resumidos en la tabla 5-7. Varios métodos lanzan una ClassCastException cuando un objeto es incompatible con los elementos en la cola de doble extremo. Se lanza una NullPointerException si se hace un intento por almacenar un objeto null y no se permiten elementos null en la cola de doble extremo. Se lanza una IllegalArgumentException si se usa un argumento no válido. Se lanza una IllegalStateException si se hace un intento de agregar un elemento a una cola de doble extremo de longitud fija que está llena. Se lanza una NoSuchElementException si se hace un intento por eliminar un elemento de una cola de doble extremo vacía. Tal vez las características más importantes de Deque son push( ) y pop( ). Estos métodos suelen usarse para habilitar el funcionamiento de Deque como una pila. Para poner un elemento en la parte superior de la pila, se llama a push( ). Para eliminar el elemento superior, se llama a pop( ). Además, observe el método descendingIterator( ). Devuelve un iterador que devuelve elementos en orden inverso. En otras palabras, devuelve un iterador que va del final de la colección al principio. Método Descripción void addFirst(E obj) Agrega obj a la cabeza de la cola de doble extremo. Lanza una IllegalStateException si una cola de doble extremo de capacidad restringida se queda sin espacio. void addLast(E obj) Agrega obj al final de la cola de doble extremo. Lanza una IllegalStateException si una cola de doble extremo de capacidad restringida se queda sin espacio. Iterator<E> descendingIterator( ) Devuelve un iterador que va del final a la cabeza de la cola de doble extremo. En otras palabras, devuelve un iterador inverso. E getFirst( ) Devuelve el primer elemento en la cola de doble extremo. El objeto no se elimina de la cola de doble extremo. Lanza una NoSuchElementException si la cola de doble extremo está vacía. E getLast( ) Devuelve el último elemento en la cola de doble extremo. El objeto no se elimina de la cola de doble extremo. Lanza una NoSuchElementException si la cola de doble extremo está vacía. boolean offerFirst(E obj) Trata de agregar obj a la cabeza de la cola de doble extremo. Devuelve verdadero si se agregó obj y falso, de otra manera. Por tanto, este método devuelve falso cuando se hace un intento por agregar obj a una cola de doble extremo llena, de capacidad restringida. boolean offerLast(E obj) Trata de agregar obj al final de la cola de doble extremo. Devuelve verdadero si se agregó obj y falso, de otra manera. Tabla 5-7 Los métodos definidos por Deque www.fullengineeringbook.net 172 Java: Soluciones de programación Método Descripción E peekFirst( ) Devuelve el elemento que se encuentra a la cabeza de la cola de doble extremo. Devuelve null si la cola de doble extremo está vacía. No se elimina el objeto. E peekLast( ) Devuelve el elemento que se encuentra al final de la cola de doble extremo. Devuelve null si la cola de doble extremo está vacía. No se elimina el objeto. E pollFirst( ) Devuelve el elemento que se encuentra a la cabeza de la cola de doble extremo, eliminando el elemento en el proceso. Devuelve null si la cola de doble extremo vacía. E pollLast( ) Devuelve el elemento que se encuentra al final de la cola de doble extremo, eliminando el elemento en el proceso. Devuelve null si la cola de doble extremo está vacía. E pop( ) Devuelve el elemento que se encuentra a la cabeza de la cola de doble extremo, eliminándolo en el proceso. Lanza NoSuchElementException si la cola de doble extremo está vacía. void push(E obj) Agrega obj a la cabeza de la cola de doble extremo. Lanza una IllegalStateException si una cola de doble extremo de capacidad restringida se queda sin espacio. E removeFirst( ) Devuelve el elemento a la cabeza de la cola de doble extremo, eliminando el elemento en el proceso. Lanza NoSuchElementException si la cola de doble extremo está vacía. boolean removeFirstOcurrence(Object obj) Elimina la primera presentación de obj de la cola de doble extremo. Devuelve verdadero si tiene éxito y falso si la cola de doble extremo no contiene obj. E removeLast( ) Devuelve el elemento que se encuentra al final de la cola de doble extremo, eliminando el elemento en el proceso. Lanza NoSuchElementException si la cola de doble extremo está vacía. boolean removeLastOcurrence(Object obj) Elimina la última presentación de obj de la cola de doble extremo. Devuelve verdadero si tiene éxito y falso si la cola de doble extremo no contiene obj. Tabla 5-7 Los métodos definidos por Deque (continuación) Una implementación de Deque puede tener capacidad restringida, lo que significa que sólo puede agregarse a la cola un número limitado de elementos. Cuando éste es el caso, puede fallar un intento de agregar un elemento a la cola de doble extremo. Deque le permite manejar esta falla de dos maneras. En primer lugar, métodos como push( ), addFirst( ) y addLast( ) lanzan una IllegalStateException si una cola de doble extremo de capacidad restringida está llena. En segundo lugar, otros métodos, como offerFirst( ) y offerLast( ), devuelven falso si el elemento no puede agregarse. Tiene la opción de seleccionar el método más adecuado para su aplicación. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 173 Una situación similar ocurre en la eliminación de elementos de una cola de doble extremo. Métodos como pop( ) y removeFirst( ) lanzan una NoSuchElementException si se llaman en una colección vacía. Métodos como pollFirst( ) o pollLast( ) devuelven null si la colección está vacía. Una vez más, elija el método más compatible con su aplicación. Las clases de la colección Las clases de la colección implementan las interfaces de la colección. Algunas de las clases proporcionan implementaciones completas que pueden usarse tal como están. Otras son abstractas, proporcionando esqueletos de implementaciones que se usan como puntos de partida para crear colecciones concretas. Las clases de la colección definidas en java.util están resumidas en la tabla siguiente. Las soluciones de este capítulo usan varias de esas clases. A continuación se presenta una breve revisión general de cada colección concreta. Clase Descripción AbstractCollection Implementa la mayor parte de la interfaz Collection. Es una superclase para todas las clases de la colección concreta. AbstractList Extiende AbstractCollection e implementa la mayor parte de la interfaz List. AbstractQueue Extiende AbstractCollection e implementa la mayor parte de la interfaz Queue. AbstractSequentialList Extiende AbstractList para que lo use una colección que emplee acceso secuencial en lugar de aleatorio de sus elementos. AbstractSet Extiende AbstractCollection e implementa la mayor parte de la interfaz Set. ArrayDeque Implementa una cola de doble extremo al extender AbstractCollection e implementa la interfaz Deque. ArrayList Implementa una matriz dinámica al extender AbstractList. EnumSet Extiende AbstractSet para que se use con elementos enum. HashSet Extiende AbstractSet para que se use con una tabla de hash. LinkedHashSet Extiende HashSet para permitir la iteración de inserción de orden. LinkedList Implementa una lista vinculada al extender AbstractSequentialList. También implementa la interfaz Deque. PriorityQueue Extiende AbstractQueue para que soporte una cola basada en prioridad. TreeSet Implementa un conjunto almacenado en un árbol. Extiende AbstractSet e implementa la interfaz SortedSet. La clase ArrayList ArrayList da soporte a matrices dinámicas que pueden crecer o reducirse de acuerdo con lo necesario. En otras palabras, puede aumentarse o reducirse el tamaño de una ArrayList en tiempo de ejecución. Una ArrayList se creó con un tamaño inicial. Cuando se excede este tamaño, la colección se alarga automáticamente. Cuando se eliminan los objetos, la matriz puede reducirse. www.fullengineeringbook.net 174 Java: Soluciones de programación ArrayList extiende AbstractList e implementa la interfaz List. (También implementa RandomAccess para indicar que da soporte a acceso rápido aleatorio a sus elementos). Es una clase genérica que tiene esta declaración: Class ArrayList<E> Aquí, E especifica el tipo de objetos que contendrá la lista. ArrayList define estos constructores: ArrayList( ) ArrayList(Collection<? Extends E> col) ArrayList(int capacidad) El primer constructor elabora una lista de matrices vacías. El segundo, construye una lista de matrices que se inicializan con los elementos de la colección col. El tercero, una lista de matrices que tienen la capacidad inicial especificada. La capacidad es el tamaño de la matriz que se usa para almacenar los elementos y crece automáticamente a medida que se agregan elementos a la lista de matrices. En los casos en que sabe que cierto número mínimo de elementos habrá de almacenarse, puede establecer la capacidad inicial de antemano. Esto evita reasignaciones subsecuentes, que son costosas en cuanto a tiempo. ArrayList representa una opción útil a las matrices normales de Java. En este lenguaje, las matrices tienen una longitud fija. Después de que se ha creado una matriz, no puede crecer o reducirse, lo que significa que debe saber de antemano cuántos elementos contendrá una matriz. Sin embargo, en ocasiones esto no es posible. Por ejemplo, tal vez quiera usar una matriz que contendrá una lista de valores (como números de ID de producto) recibidos vía una conexión de Internet en que la lista termina con un valor especial. Por tanto, el número de valores no es conocido hasta que se haya recibido el terminador. En tal caso, ¿de qué tamaño hará la matriz que recibirá los valores? ArrayList ofrece una solución a este tipo de problema. Además del método definido por las interfaces que implementa, ArrayList define dos métodos propios: ensureCapacity( ) y trimToSize( ). Aquí se muestran: void ensureCapacity(int cap) void trimToSize( ) El método ensureCapacity( ) le permite aumentar manualmente la capacidad de una ArrayList, que es el número de elementos que puede contener antes de que sea necesario agrandar la lista. La nueva capacidad se especifica con cap. Como opción, trimToSize( ) le permite reducir el tamaño de una ArrayList de modo que se adecue de manera precisa al número de elementos que contiene. La clase LinkedList LinkedList proporciona una estructura de datos de lista vinculada. Extiende AbstractSequentialList e implementa las interfaces List y Deque. Debido a que usa una lista doblemente vinculada, una LinkedList crecerá automáticamente cuando se agregue un elemento a la lista y se reducirá cuando se elimine uno. Las listas vinculadas son especialmente útiles en situaciones en que los elementos se insertan o eliminan de la parte media de la lista. Los nuevos elementos simplemente se vinculan en ellas. Cuando se elimina un elemento, los vínculos a viejos elementos se reemplazan con vínculos a los elementos que anteceden y siguen al elemento eliminado. La reorganización de vínculos suele ser más eficiente que la compactación o expansión física de una matriz, por ejemplo. LinkedList es una clase genérica que tiene esta declaración: Class LinkedList<E> www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 175 Aquí, E especifica el tipo de objetos que contendrá la lista. LinkedList tiene los dos constructores que se muestran aquí: LinkedList( ) LinkedList(Collection<? Extends E> col) El primer constructor crea una lista vinculada vacía. El segundo, una que se inicializa con los elementos de la colección col. La clase HashSet HashSet crea una colección que usa una tabla de hash para almacenamiento. Una tabla de hash almacena información a la que se le ha aplicado hash. En este proceso, se usa el contenido informativo de una clave para determinar un valor único, llamado código de hash. Éste se emplea después como el índice en que se almacenan los datos relacionados con la clave. La transformación de la clave en su código de hash se realiza automáticamente (nunca ve el propio código de hash). Además, su código no puede indizar directamente la tabla de hash. La ventaja de la aplicación de hash es que permite que el tiempo de ejecución de add( ), contains( ), remove( ) y size( ) permanezca constante aun en conjuntos grandes. No se permiten elementos duplicados en un HashSet. HashSet extiende AbstractSet e implementa una interfaz Set. Se trata de una clase genérica que tiene esta declaración: class HashSet<E> Aquí, E especifica el tipo de objetos que contendrá el conjunto. HashSet define los siguientes constructores: HashSet( ) HashSet(Collection<? extends E> col) HashSet(int capacidad) HashSet(int capacidad, float relRelleno) La primera forma construye un conjunto de hash predeterminado. La segunda, inicializa el conjunto de hash al usar los elementos de colección. La tercera, inicializa la capacidad del conjunto de hash en capacidad. La cuarta, inicializa la capacidad y la relación de relleno (también denominada capacidad de carga) del conjunto de hash. La relación de relleno debe estar entre 0.0 y 1.0, y determina cuán lleno puede estar el conjunto de hash antes de que puede aumentarse su tamaño. Específicamente, cuando el número de elementos es mayor que la capacidad del conjunto de hash multiplicado por su relación de relleno, el conjunto de relleno se expande. En el caso de constructores que no toman una relación de relleno, se usa 0.75. He aquí un punto clave relacionado con HashSet: los elementos no se encuentran en ningún orden determinado. Por ejemplo, no están ordenados, ni se almacenan en un orden de inserción. Esto se debe a que el proceso de hash no tiende en sí mismo a la creación de conjuntos ordenados. Por tanto, no está especificado el orden en que se obtienen los elementos cuando se enumera la colección mediante un iterador o un estilo for-each del bucle for. La clase LinkedHashSet LinkedHashSet usa una tabla de hash para almacenamiento, pero también mantiene una lista de doble vínculo de los elementos de la colección. Esta lista se encuentra en el orden en que se insertan los elementos en la colección. Esto permite la iteración del orden de inserción en el conjunto. Es decir, cuando se recorre un LinkedHashSet empleando un iterador o un estilo for-each del bucle www.fullengineeringbook.net 176 Java: Soluciones de programación for, los elementos se devolverán en el orden en que se insertaron. Sin embargo, aún se conservan los beneficios de búsqueda rápida de los elementos con hash. LinkedHashSet extiende HashSet y no agrega miembros propios. Es una clase genérica que tiene esta declaración: class LinkedHashSet<E> Aquí, E especifica el tipo de objetos que contendrá el conjunto. LinkedhashSet define los siguientes constructores: LinkedHashSet( ) LinkedHashSet(Collection<? extends E> col) LinkedHashSet(int capacidad) LinkedHashSet(int capacidad, float relRelleno) Funcionan igual que sus equivalentes correspondientes en HashSet. La clase TreeSet TreeSet crea una colección ordenada que usa un árbol para almacenamiento. Los objetos se almacenan en orden ascendente. Debido a la estructura del árbol, los tiempos de acceso y recuperación son muy rápidos. Esto hace que TreeSet sea una excelente opción para acceso rápido a grandes cantidades de información ordenada. La única restricción es que no se permiten elementos duplicados en el árbol. TreeSet extiende AbstractSet e implementa la interfaz NavigableSet. Se trata de una clase genérica que tiene esta declaración: class TreeSet<E> Aquí, E especifica el tipo de objetos que contendrá el conjunto. TreeSet tiene los siguientes constructores: TreeSet( ) TreeSet(Collection<? extends E> col) TreeSet(Comparator<? Super E> comp) TreeSet(SortedSet<E> ss) La primera forma construye un conjunto de árbol vacío que se ordenará de manera ascendente, de acuerdo con el orden natural de sus elementos. La segunda, construye un conjunto de árbol que contiene los elementos de col. La tercera, un conjunto vacío que estará ordenado de acuerdo con el comparador especificado por comp. La cuarta, uno que contiene los elementos de ss. La clase PriorityQueue PriorityQueue crea una cola en que los elementos se almacenan de acuerdo con su prioridad. Como opción predeterminada, la prioridad se basa en el orden natural de los elementos. Sin embargo, puede modificar este comportamiento al especificar un comparador cuando se construye la PriorityQueue. PriorityQueue extiende AbstractQueue e implementa la interfaz Queue. Es una clase genérica que tiene esta declaración: class PriorityQueue<E> www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 177 Aquí, E especifica el tipo de objetos almacenados en la cola. Las colas de PriorityQueue son dinámicas, y crecen conforme sea necesario. PriorityQueue define los seis constructores que se muestran aquí: PriorityQueue( ) PriorityQueue(int capacidad) PriorityQueue(int capacidad, Comparator<? Super E> comp) PriorityQueue(Collection<? extends E> col) PriorityQueue(PriorityQueue<? extends E> col) PriorityQueue(SortedSet<? extends E> col) El primer constructor construye una cola vacía, con una capacidad inicial de 11. El segundo, construye una cola que tiene la capacidad inicial especificada. El tercero, una cola con una capacidad especificada y un comparador. Los últimos tres constructores crean colas que están inicializadas con los elementos de la colección pasados en col. En todos los casos, la capacidad crece automáticamente a medida que se agregan elementos. Si se especifica un comprador cuando se construye una PriorityQueue, entonces se usa el comparador predeterminado para el tipo de datos almacenados en la cola. El comparador predeterminado ordenará la cola de manera ascendente. Por tanto, la cabeza de la cola será el valor más pequeño. Sin embargo, al proporcionar un comparador predeterminado, puede especificar un esquema de orden diferente. Por ejemplo, cuando se almacenan elementos que incluyen una estampa de tiempo, puede priorizar la cola de tal manera que los elementos más antiguos sean los primeros de la cola. Consulte Use un Comparator con una colección para conocer un ejemplo. Además de los métodos especificados por las interfaces que implementa, PriorityQueue define un método adicional: comparator( ). Devuelve una referencia al comparador usado por una PriorityQueue en orden de prioridad; debe llamar a poll( ) o remove( ). La clase ArrayDeque ArrayDeque se agregó en Java 6, lo que la hace una adición reciente a la estructura de colecciones. Crea una matriz dinámica basada en la interfaz Deque. Esto hace que resulte especialmente útil para implementar pilas y colas cuyos tamaños no se conocen de antemano. Además de implementar Deque, ArrayDeque extiende AbstractCollection. ArrayDeque es una clase genérica que tiene esta declaración: class ArrayDeque<E> Aquí, E especifica el tipo de objetos almacenados en la colección: ArrayDeque define los siguientes constructores: ArrayDeque( ) ArrayDeque(int tam) ArrayDeque(Collection<? extends E> col) www.fullengineeringbook.net 178 Java: Soluciones de programación El primer constructor crea una cola vacía con una capacidad inicial de 16. El segundo, construye una cola de doble extremo que tiene la capacidad inicial especificada. El tercero, crea una cola de doble extremo que se inicializa cuando los elementos de la colección pasados en col. En todos los casos, la capacidad crece conforme sea necesario para manejar los elementos agregados a la colección. La clase EnumSet EnumSet es una colección específicamente diseñada para usarla con valores del tipo enum. Extiende AbstractSet e implementa Set. Es una clase genérica que tiene esta declaración: class EnumSet<E extends Enum<E>> Aquí, E especifica los elementos. Observe que E debe extender Enum<E>, que impone el requisito de que los elementos deben ser del tipo enum especificado. EnumSet no define constructores. En cambio, usa métodos de fábrica, como allOf( ) o range( ), para obtener una instancia de EnumSet. Revisión general de los mapas Los mapas son parte de la estructura de colecciones pero no son, en sí mismos, colecciones, porque no implementan la interfaz Collection. En lugar de almacenar grupos de objetos, los mapas almacenan pares clave/valor. Una característica que define a los mapas es la capacidad de recuperar un valor dando su clave. En otras palabras, si se da una clave, puede encontrar su valor. Esto hace que un mapa sea una excelente opción en muchas aplicaciones basadas en búsqueda. Por ejemplo, podría usar un mapa para almacenar nombres y números telefónicos. Los nombres son claves y los números son valores. Podría encontrar un número dando el nombre de una persona. En un mapa, tanto claves como valores son objetos. No pueden ser tipos primitivos. Más aún, todas las claves deben ser únicas. Esto tiene sentido porque una clave se usa para encontrar un valor. Por tanto, no puede tener la misma clave asignada a dos valores diferentes. Sin embargo, dos claves diferentes pueden tener asignado el mismo valor. Por tanto, las claves deben ser únicas, pero los valores pueden estar duplicados. Los mapas no implementan la interfaz Iterable. Esto significa que no puede recorrer en bucle un mapa empleando un estilo for-each del bucle for. Tampoco puede obtener un iterador para un mapa. Sin embargo, puede obtener una vista de colección de un mapa, que le permite el uso de su bucle for o un iterador. Las interfaces de Map Los mapas están definidos por el mismo conjunto de interfaz que se muestra aquí: Interfaz Descripción Map Asigna claves únicas a valores. Map.Entry Describe un elemento (un par clave/valor) en un mapa. Es una clase interna de Map. NavigableMap Extiende SortedMap para manejar la recuperación de entradas basadas en las búsquedas de la coincidencia más cercana. SortedMap Extiende Map para que las claves se mantengan en orden ascendente. A continuación, se examina cada interfaz. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 179 La interfaz Map La interfaz Map vincula claves únicas con valores. Cada par clave/valor constituye una entrada en el mapa. Por tanto, dados una clave y un valor, puede almacenar una entrada en un Map. Después de que la entrada se ha almacenado, puede recuperar su valor usando su clave. Map es genérica y se declara de la siguiente manera: interface Map<C, V> Aquí, C especifica el tipo de claves y V especifica el tipo de valores. Los métodos declarados por Map se resumen en la tabla 5-8. Preste especial atención a get( ) y put( ). Para almacenar un valor en un mapa, use put( ), especificando la clave y el valor. Para obtener un valor, llame a get( ), pasando la clave como argumento. Se devuelve el valor. Por tanto, get( ) y put( ) definen los métodos fundamentales de almacenamiento y recuperación usados por todas las implementaciones de Map. Método Descripción void clear( ) Elimina todos los pares clave/valor del mapa que invoca. boolean containsKey(Object c) Devuelve verdadero si el mapa que invoca contiene una c como clave. De otra manera, devuelve falso. boolean containsValue(Object v) Devuelve verdadero si el mapa contiene v como valor. De otra manera, devuelve falso. Set<Map.Entry<C, V>> entrySet( ) Devuelve un Set que contiene las entradas del mapa. El conjunto contiene objetos de tipo Map.Entry. Por tanto, este método proporciona una vista de conjunto del mapa que invoca. boolean equals(Object obj) Devuelve verdadero si obj es un Map y contiene las mismas entradas. De otra manera, devuelve falso. V get (Object c) Devuelve el valor asociado con la clave c. Devuelve null si no se encuentra la clave. int hashCode( ) Devuelve el código de hash para el mapa que invoca. boolean isEmpty( ) Devuelve verdadero si el mapa que invoca está vacío. De otra manera, devuelve falso. Set<C> keySet( ) Devuelve un Set que contiene las claves del mapa que invoca. Este método proporciona una vista de conjunto de las claves en el mapa que invoca. V put(C c, V v) Pone una entrada en el mapa que invoca, sobrescribiendo cualquier valor anterior asociado con la clave. La clave y el valor son c y v, respectivamente. Devuelve null si la clave aún no existe. De otra manera, se devuelve el valor previo vinculado con la clave. void putAll(Map<? extends C, ? extends V> m) Pone todas las entradas desde m hacia este mapa. V remove(Object c) Elimina la entrada cuya clave es igual a c. Devuelve el valor eliminado, o null si la clave no se encuentra en el mapa. int size( ) Devuelve el número de pares clave/valor en el mapa. Collection<V> values( ) Devuelve una colección que contiene los valores del mapa. Este método proporciona una vista de colección de los valores en el mapa. Tabla 5-8 Los métodos definidos por Map www.fullengineeringbook.net 180 Java: Soluciones de programación Varios métodos lanzan una ClassCastException cuando un objeto es incompatible con los elementos de un mapa. Una NullPointerException se lanza si se hace un intento de usar un objeto null y no se permite este tipo de objeto en el mapa. Una UnsupportedOperationExeption se lanza cuando se hace un intento de cambiar un mapa no modificable. Se lanza una IllegalArgumentException si se usa un argumento no válido. La interfaz SortedMap La interfaz SortedMap extiende Map. Las entradas en el mapa se mantienen y ordenan de acuerdo con las claves. Los mapas ordenados permiten manipulaciones muy eficientes de submapas (en otras palabras, subconjuntos de un mapa). SortedMap es genérica y se declara como se muestra aquí: interface SortedMap<C, V> Aquí, C especifica el tipo de claves y V especifica el tipo de valores. Además de los métodos especificados por Map, SortedMap agrega varios propios, que se resumen en la tabla 5-9. Varios métodos lanzan un NoSuchElementException cuando no hay elementos en el mapa que invoca. Una ClassCastException se lanza cuando un objeto es incompatible con los elementos de un mapa. Una NullPointerException se lanza si se hace un intento de usar un objeto null y no se permite este tipo de objeto en el mapa. Se lanza una IllegalArgumentException si se usa un argumento no válido. La interfaz NavigableMap La interfaz NavigableMap es una adición reciente a la estructura de colecciones (se agregó en Java 6). Extiende SortedMap y declara el comportamiento de un mapa que soporta la recuperación de entradas basadas en la coincidencia más cercana a una clave o varias claves dadas. NavigableMap es una interfaz genérica que tiene esta declaración: interface NavigableMap<C, V> Aquí, C especifica el tipo de claves y V el tipo de valores asociados con las claves. Método Descripción Comparator <? super C> comparator( ) Devuelve el comparador del mapa ordenado. Si se usa orden natural para el mapa que invoca, se devuelve null. C firstKey( ) Devuelve la primera clave en el mapa que invoca. SortedMap<C, V> headMap(C ļ¬nal) Devuelve un mapa ordenado para las entradas de mapa con claves que son menores que ļ¬nal. C lastKey( ) Devuelve la última clave en el mapa que invoca. SortedMap<C, V> subMap(C inicio, C ļ¬nal) Devuelve un mapa que contiene las entradas con claves que son mayores o iguales a inicio y menores que ļ¬nal. SortedMap<C, V> tailMap(C inicio) Devuelve un mapa que contiene las entradas con claves que son mayores o iguales a inicio. Tabla 5-9 Los métodos definidos por SortedMap www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 181 Además de los métodos que hereda de SortedMap, NavigableMap agrega los resumidos en la tabla 5-10. Varios métodos lanzan una ClassCastException cuando un objeto es incompatible con las claves del mapa. Se lanza un NullPointerException si se hace un intento de usar un objeto null y no se permiten claves null en el mapa. Se lanza una IllegalArgumentException si se usa un argumento no válido. Método Descripción Map, Entry<C, V> ceilingEntry(C obj) Busca en el mapa la clave c más pequeña, tal que c >= obj. Si se encuentra esa clave, se devuelve su entrada. De otra manera, se devuelve null. C ceilingKey(C obj) Busca en el mapa la clave c más pequeña, tal que c >= obj. Si se encuentra esa clave, se devuelve. De otra manera, se devuelve null. NavigableSet<C> descendingKeySet( ) Devuelve un NavigableSet que contiene las claves en el mapa que invoca en orden inverso. Por tanto, devuelve una vista de conjunto inverso de las claves. El conjunto resultante es respaldado por el mapa. NavigableMap<C,V> descendingMap( ) Devuelve un NavigableMap que es inverso al mapa que invoca. El mapa resultante es respaldado por aquel. Map.Entry<C, V> firstEntry( ) Devuelve la primera entrada del mapa. Se trata de la entrada con el clave menor. Map.Entry<C, V> floorEntry(C obj) Busca en el mapa la clave más grande c tal que c <= obj. Si se encuentra, se devuelve su entrada. De otra manera, se devuelve null. C floorKey(C obj) Busca en el mapa la clave más grande c tal que c <= obj. Si se encuentra, se devuelve. De otra manera, se devuelve null. NavigableMap<C, V> headMap(C limiteSuperior, boolean incl) Devuelve un NavigableMap que incluye todas las entradas del mapa que invoca que tienen claves que son menores que limiteSuperior. Si incl es verdadero, se incluye un elemento igual a limiteSuperior. El mapa resultante es respaldado por aquel. Map.Entry<C, V> higherEntry(C obj) Busca en el conjunto la clave más grande c tal que c > obj. Si se encuentra, se devuelve su entrada. De otra manera, se devuelve null. C higherKey(C obj) Busca en el conjunto la clave más grande c tal que c > obj. Si se encuentra, se devuelve. De otra manera, se devuelve null. Tabla 5-10 Los métodos definidos por NavigableMap (continúa) www.fullengineeringbook.net 182 Java: Soluciones de programación Método Descripción Map.Entry<C, V> lastEntry( ) Devuelve la última entrada del mapa. Es la entrada con la clave más grande. Map.Entry<C, V> lowerEntry(C obj) Busca en el conjunto la clave más grande c tal que c < obj. Si se encuentra, se devuelve su entrada. De otra manera, se devuelve null. C lowerKey(C obj) Busca en el conjunto la clave más grande c tal que c < obj. Si se encuentra, se devuelve. De otra manera, se devuelve null. NavigableSet<C> navigableKeySet( ) Devuelve un NavigableSet que contiene las claves del mapa que invoca. El conjunto resultante es respaldado por éste. Map.Entry<C, V> pollFirstEntry( ) Devuelve la primera entrada, eliminando ésta en el proceso. Debido a que el mapa está ordenado, es la entrada con el valor de clave inferior. Se devuelve null si el mapa está vacío. Map.Entry<C, V> pollLastEntry( ) Devuelve la última entrada, eliminando ésta en el proceso. Debido a que el mapa está ordenado, es la entrada con el valor de clave mayor. Se devuelve null si el mapa está vacío. NavigableMap<C, V> subMap(C limiteInferior, boolean inclBajo, C limiteSuperior, boolean inclAlto) Devuelve un NavigableMap que incluye todas las entradas del mapa que invoca y que tiene claves mayores que limiteInferior y menores que limiteSuperior. Si inclBaja es verdadero, entonces se incluye un elemento igual a limiteInferior. Si inclAlto es verdadero, entonces se incluye un elemento igual a limiteSuperior. El mapa resultante está respaldado por el mapa que invoca. NavigableMap<C, V> tailMap(C limiteInferior, boolean incl) Devuelve un NavigableMap que incluye todas las entradas del mapa que invoca y que tiene claves mayores que limiteInferior. Si incl es verdadero, entonces se incluye un elemento igual a limiteInferior. El mapa resultante está respaldado por el mapa que invoca. Tabla 5-10 Los métodos definidos por NavigableMap (continuación) La interfaz Map.Entry La interfaz Map.Entry le permite trabajar con una entrada de mapa. El método entrySet( ) declarado por la interfaz Map da como resultado un Set que contiene las entradas del mapa. Cada uno de estos elementos del conjunto es un objeto de Map.Entry, que es genérico y se declara así: interface Map.Entry<C, V> www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 183 Método Descripción boolean equals(Object obj) Devuelve verdadero si obj es un Map.Entry con clave y valor iguales a las del objeto que invoca. C getKey( ) Devuelve la clave para esta entrada del mapa. V getValue( ) Devuelve el valor para esta entrada del mapa. int hashCode( ) Devuelve el código de hash para esta entrada del mapa. V setValue(V v) Establece el valor de esta entrada del mapa en v. Se lanza una ClassCastException si v no es el tipo correcto del mapa. Se lanza una IllegalArgumentException si hay un problema con v. Se lanza una NullPointerException si v es null y el mapa no permite claves null. Se lanza una UnsupportedOperationException si no puede cambiarse el mapa. Tabla 5-11 Los métodos definidos por Map.Entry Aquí, C especifica el tipo de claves y V el tipo de valores. En la tabla 5-11 se resumen los métodos declarados por Map.Entry. Las clases de Map Varias clases proporcionan implementaciones de las interfaces de mapa. Aquí se muestran las definidas en java.util. Clase Descripción AbstractMap Implementa la mayor parte de la interfaz de Map. Es una superclase de todas las implementaciones de mapa concretas. EnumMap Extiende AbstractMap para usar con claves de enum. HashMap Extiende AbstractMap para usar una tabla de hash. TreeMap Extiende AbstractMap para usar un árbol. También implementa NavigableMap. WeakHashMap Extiende AbstractMap para usar una tabla de hash con claves débiles, las cuales permiten que un elemento en un mapa sea recolectado con la basura cuando su clave no se usa de otra manera. LinkedHashMap Extiende HashMap para permitir las iteraciones de inserción de orden. IdentityHashMap Extiende AbstractMap y usa igualdad de referencia cuando se comparan entradas. Esta clase no es para uso general. WeakHashMap e IdentityHashMap son mapas de uso especial y no se analizarán más aquí. Las otras clases de mapas se describirán en las siguientes secciones. www.fullengineeringbook.net 184 Java: Soluciones de programación La clase HashMap HashMap usa una tabla de hash para almacenar el mapa. Extiende AbstractMap e implementa la interfaz Map. HashMap es una clase genérica que tiene esta declaración: class HashMap<C, V> Aquí, C especifica el tipo de claves y V el de valores. Se definen los siguientes constructores: HashMap( ) HashMap(Map<? extends C, ? extends V> m) HashMap(int capacidad) HashMap(int capacidad, float relRelleno) La primera forma construye un mapa de hash predeterminado. La segunda, inicializa el mapa de hash al usar los elementos de m. la tercera, inicializa la capacidad del mapa de hash en capacidad. La cuarta forma inicializa la capacidad y la relación de relleno del mapa de hash al usar sus argumentos. El significado de capacidad y relación de relleno es el mismo que para HashSet, descrito antes. Debido a que HashMap usa una tabla de hash, sus elementos no están en ningún orden particular. Por tanto, el orden en que se agregan los elementos a un mapa de hash no es necesariamente el orden en que los lee el iterador, ni están ordenados. La clase TreeMap TreeMap usa un árbol para contener un mapa. Un TreeMap proporciona un medio eficiente de almacenar pares clave/valor en orden y permite una rápida recuperación. TreeMap extiende AbstractMap e implementa la interfaz NavigableMap. Es una clase genérica que tiene esta declaración: class TreeMap<C, V> Aquí, C especifica el tipo de claves y V el de valores. Están definidos los siguientes constructores de TreeMap: TreeMap( ) TreeMap(Comparator<? super C> comp) TreeMap(Map<? extends C, ? extends V> m) TreeMap(SortedMap<C, ? extends V> sm) La primera forma construye un mapa de árbol vacío que se ordenará al usar el orden natural de sus claves. La segunda forma construye un mapa basado en árbol vacío que se ordenará al usar el Comparator comp. La tercera forma inicializa un mapa de árbol con las entradas desde m, que se ordenará al usar el orden natural de las claves. La cuarta forma inicializa un mapa de árbol con las entradas de sm, que se ordenará en el mismo orden que sm. La clase LinkedHashMap LinkedHashMap mantiene una lista vinculada de las entradas en el mapa, en el oren en que se insertaron. Esto permite la iteración en el orden de inserción en el mapa. Es decir, cuando se itera a través de una vista de colección de un LinkedHashMap, los elementos se devolverán en el orden www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 185 en que se insertaron. También puede crear un LinkedHashMap que devuelve sus elementos en el orden en que se accedieron por última vez. LinkedHashMap extiende HashMap. Es una clase genérica que tiene esta declaración: Class LinkedHashMap>C, V> Aquí C especifica los tipos de clave y V los de valores. LinkedHashMap define los siguientes constructores: LinkedHashMap( ) LinkedHashMap(Map<? extends C, ? extends V> m) LinkedHashMap(int capacidad) LinkedHashMap(int capacidad, float relRelleno) LinkedHashMap(int capacidad, float relRelleno, boolean Orden) La primera forma construye en forma predeterminada una LinkedHashMap. La segunda, inicializa la LinkedHashMap con los elementos de m. La tercera inicializa la capacidad y la relación de relleno. El significado de capacidad. La cuarta inicializa la capacidad y relación de relleno son los mismos que para HashMap. La última forma le permite especificar si los elementos se almacenarán en la lista vinculada en el orden de inserción o el orden del último acceso. Si Orden es verdadero, entonces se usa el orden de acceso. Si es falso, entonces se usa el de inserción. La clase EnumMap EnumMap es específicamente para uso con claves de un tipo enum. Extiende AbstractMap e implementa Map. EnumMap es una clase genérica que tiene esta declaración: class EnumMap<C extends Enum<C>, V> Aquí, C especifica el tipo de clave y V el tipo de valor. Observe que C debe extender Enum<C>, que impone el requisito de que las claves deben ser de un tipo enum. EnumMap define los siguientes constructores: EnumMap(Class<C> tipoC) EnumMap(Map<C, ? extends V> m) EnumMap(EnumMap<C, ? extends V> em) El primer constructor crea una EnumMap vacía de tipo tipoC. El segundo crea un mapa EnumMap que contiene las mismas entradas que m. El tercero crea una EnumMap inicializada con los valores en em. Algoritmos La estructura de colecciones proporciona muchos algoritmos que operan en colecciones y mapas. Estos algoritmos se declaran como métodos estáticos dentro de la clase Collections. Su descripción está fuera del alcance de este libro. Sin embargo, se usan varios en las soluciones y se describen en las soluciones en que se usan. www.fullengineeringbook.net 186 Java: Soluciones de programación Técnicas básicas de colecciones Componentes clave Clases e interfaces Métodos java.util.Collection<E> boolean add(E obj) boolean addAll(Collection<? extends E> col) void clear( ) boolean contains(Object obj) boolean containsAll(Collection<?> col) boolean isEmpty( ) boolean remove (Object obj) boolean removeAll(Collection<?> col) boolean retainAll(Collection<?> col) int size( ) <T> T[ ] toArray (T matriz[ ]) java.util.ArrayList>E> Debido a que todas las clases de colecciones implementan la interfaz Collection, todas las colecciones comparten una funcionalidad común. Por ejemplo, todas las colecciones le permiten agregar elementos a la colección, determinar si el objeto es parte de la colección, u obtener el tamaño de la colección. En esta solución se demuestra esta funcionalidad común al mostrar cómo • Crear una colección. • Agregar elementos a una colección. • Determinar el tamaño de una colección. • Determinar si una colección contiene un elemento específico. • Recorrer en bucle una colección con un estilo for-each del bucle for. • Eliminar elementos. • Determinar si una colección está vacía. • Crear una matriz que contiene la colección. En esta solución se usa la colección ArrayList, pero sólo se usan los métodos definidos por Collection, de modo que algunos principios generales pueden aplicarse a cualquier clase de colección. NOTA Los iteradores también tienen soporte en todas las colecciones, pero se describen por separado. Consulte Itere en una colección. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 187 Paso a paso Para crear y usar una colección se requieren estos pasos: 1. Cree una instancia de la colección deseada. En esta solución, se usa ArrayList, pero podría seleccionarse cualquier otra colección (excepto por EnumSet, que está específicamente diseñada para usar con los tipos enum). 2. Realice varias operaciones en la colección al usar los métodos definidos por Collection, como se describe en los siguientes pasos. 3. Agregue elementos a la colección al llamar a add( ) o addAll( ). 4. Obtenga el número de elementos en la colección al llamar a size( ). 5. Determine si una colección contiene uno o más elementos específicos al llamar a contains( ) o containsAll( ). 6. Determine si la colección está vacía (es decir, no contiene elementos) al llamar a isEmpty( ). 7. Elimine elementos de la colección al llamar a remove( ), removeAll( ) o reatainAll( ). 8. Elimine todos los elementos de una colección al llamar a clear( ). 9. Itere en los elementos de la colección al usar el estilo for-each del bucle for. 10. Obtenga una matriz que contiene los elementos de la colección al llamar a toArray( ). Análisis Los métodos definidos por Collection se mostraron en la tabla 5-1, que se encuentra en Revisión general de las colecciones, al principio de este capítulo. He aquí una breve descripción de su operación. Los objetos se agregan a una colección al llamar a add( ). Observe que add( ) toma un argumento de tipo E, lo que significa que los objetos agregados a una colección deben ser compatibles con el tipo de datos esperados por la colección. Puede agregar todo el contenido de una colección a otra al llamar addAll( ). Por supuesto, ambos deben contener elementos compatibles. Puede eliminar un objeto al usar remove( ). Para eliminar un grupo de objetos, llame a removeAll( ). Puede eliminar todos los elementos, excepto los de un grupo especificado al llamar a retainAll( ). Para vaciar una colección, llame a clear( ). Puede determinar si una colección contiene un objeto específico al llamar a contains( ). Para determinar si una colección contiene todos los miembros de otro, llame a containsAll( ). Puede determinar cuándo una colección está vacía al llamar a isEmpty( ). El número de elementos que se conservan en una colección se determina al llamar a size( ). El método toArray( ) devuelve una matriz que contiene los elementos de la colección que invoca. Hay dos versiones de toArray( ). La primera devuelve una matriz de Object. La segunda devuelve una matriz de elementos que contiene el mismo tipo que la matriz especificada como parámetro. Por lo general, la segunda forma es más conveniente porque devuelve el tipo de matriz deseado, y ésta es la versión usada en esta solución. El método toArray( ) es útil porque proporciona una ruta entre colecciones y matrices. Esto le permite procesar una colección empleando la sintaxis de matriz estándar. En el ejemplo que sigue se usa ArrayList para que contenga la colección. En Revisión general de las colecciones, que se presentó cerca del principio de este capítulo, se describen sus constructores. El usado por la solución es su constructor predeterminado, que crea una colección vacía. www.fullengineeringbook.net 188 Java: Soluciones de programación Ejemplo Con el siguiente ejemplo se demuestran las técnicas básicas de colecciones recién descritas. // Demuestra técnicas de colecciones básicas. import java.util.*; class BasesColecciones { public static void main(String args[ ]) { // Crea una colección. ArrayList<Integer> col = new ArrayList<Integer>( ); // Muestra el tamaño inicial. System.out.println("Tama\u00a4o inicial: " + col.size( )); // Almacena algunos objetos en la colección. for(int i=0; i<10; i++) col.add(i + 10); // Muestra adiciones después del tamaño. System.out.println("Tama\u00a4o tras las adiciones: " + col.size( )); // Usa un bucle for-each para mostrar la colección. System.out.println("Contenido de col: "); for(int x : col) System.out.print(x + " "); System.out.println("\n"); // Ve si la colección contiene un valor. if(col.contains(12)) System.out.println("col contiene el valor 12"); if(col.contains(−9)) System.out.println("col contiene el valor −9"); System.out.println( ); // Crea otra colección y luego la agrega a la primera. ArrayList<Integer> col2 = new ArrayList<Integer>( ); col2.add(100); col2.add(200); col2.add(8); col2.add(−10); // Muestra col2. System.out.println("Contenido de col2: "); for(int x : col2) System.out.print(x + " "); System.out.println("\n"); www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones // Agrega col2 a col. col.addAll(col2); // Muestra la colección resultante. System.out.println("Contenido de col tras agregar col2: "); for(int x : col) System.out.print(x + " "); System.out.println("\n"); // Usa containsAll( ) para confirmar que col ahora contiene // todo col2. if(col.containsAll(col2)) System.out.println("col contiene todo lo de col2."); System.out.println( ); // Ahora elimina objetos de la colección. col.remove((Integer)10); col.remove((Integer)200); // Muestra la colección resultante. System.out.println("Contenido de col tras eliminar elementos: "); for(int x : col) System.out.print(x + " "); System.out.println("\n"); // Ahora elimina toda la colección col2. col.removeAll(col2); // Muestra la colección resultante. System.out.println("Contenido de col tras eliminar col2: "); for(int x : col) System.out.print(x + " "); System.out.println("\n"); // Agrega col2 a col una vez más, y llama a retainAll( ). col.addAll(col2); // agrega col2 a col // Elimina todos los elementos excepto los de col2. col.retainAll(col2); // Muestra la colección resultante. System.out.println("Contenido de col tras retener col2: "); for(int x : col) System.out.print(x + " "); System.out.println("\n"); // Obtiene una matriz de una colección. Integer[ ] matrizi = new Integer[col.size( )]; matrizi = col.toArray(matrizi); www.fullengineeringbook.net 189 190 Java: Soluciones de programación // Despliega el contenido de la matriz. System.out.println("Contenido de matrizi: "); for(int i=0; i<matrizi.length; i++) System.out.print(matrizi[i] + " "); System.out.println("\n"); // Elimina todos los elementos de la colección. System.out.println("Eliminando todos los elementos de col."); col.clear( ); if(col.isEmpty( )) System.out.println("col est\u00a0 vac\u00a1a."); } } Aquí se muestra la salida: Tamaño inicial: 0 Tamaño tras las adiciones: 10 Contenido de col: 10 11 12 13 14 15 16 17 18 19 col contiene el valor 12 Contenido de col2: 100 200 8 –10 Contenido de col tras agregar col2: 10 11 12 13 14 15 16 17 18 19 100 200 8 –10 col contiene todo lo de col2. Contenido de col tras eliminar elementos: 11 12 13 14 15 16 17 18 19 100 8 –10 Contenido de col tras eliminar col2: 11 12 13 14 15 16 17 18 19 Contenido de col tras retener col2: 100 200 8 –10 Contenido de matrizi: 100 200 8 –10 Eliminando todos los elementos de col. col está vacía. Opciones Aunque ArrayList se usó para demostrar las técnicas de colecciones básicas, pudo usarse cualquiera de las clases de colecciones (excepto EnumSet, que es específicamente para tipos enum). www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 191 Por ejemplo, trate de sustituir ArrayList con LinkedList en el ejemplo. El programa se compilará y ejecutará adecuadamente. La razón es, por supuesto, que todas las colecciones implementan la interfaz Collection, y sólo los métodos definidos por Collection se usan en el ejemplo. Más aún, debido a que todas las colecciones implementan Iterable, todas las colecciones pueden recorrerse en bucle usando la versión for-each de for, como se muestra en el ejemplo. Aunque un for-each de for suele ser el método más conveniente para recorrer en bucle los elementos de una colección, también puede emplearse un iterador. Esta técnica se describe en Itere en una colección. Trabaje con listas Componentes clave Clases e interfaces Métodos java.io.FileInputStream void add(int ind, E obj) E get(int ind) int indexOf(Object obj) int LastIndexOf(Object obj) E remove(int ind) E set(int ind, E obj) java.util.ArrayList<E> void ensureCapacity(int capacity) java.util.LinkedList<E> Tal vez las colecciones de uso más común sean las basadas en la interfaz List, que extiende Collection y define una colección de propósito general que almacena una secuencia. En casi todos los casos, una lista puede contener elementos duplicados. Una lista también permite que se acceda a los elementos por posición de índice. El paquete java.util define varias implementaciones concretas de List. Las primeras dos son ArrayList y LinkedList. ArrayList implementa las interfaces List y RandomAccess y agrega dos métodos propios. LinkedList implementa las interfaces List y Deque. List también es implementada por las clases heredades Vector y Stack, pero no se recomienda su uso en nuevo código. En esta solución se demuestran las listas. En ella se usan tanto ArrayList como LinkedList. Paso a paso El uso de una lista incluye los pasos siguientes: 1. Cree una implementación concreta de List. En el caso de operaciones parecidas a matrices, use ArrayList. Para una implementación de propósito general, use LinkedList. www.fullengineeringbook.net 192 Java: Soluciones de programación 2. Agregue elementos a la lista. Los elementos pueden agregarse al final o insertarse en un índice específico al llamar a add( ). 3. Los elementos de la lista pueden accederse mediante los métodos definidos por Collection y List. Por ejemplo, puede obtener el elemento en un índice específico al llamar a get( ) o establecer el valor de un elemento al llamar a set( ). Análisis En la solución anterior se describieron las operaciones básicas de colecciones definidas por Collection (que son heredadas por List). La interfaz List también especifica varios métodos propios. Se muestran en la tabla 5-3, en la sección Revisión general de las colecciones. Aquí se muestran las usadas en esta solución: void add(int ind, E obj) E get(int ind) int indexOf(Object obj) int LastIndexOf(Object obj) E remove(int ind) E set(int ind, E obj) Esta versión de add( ) inserta obj en la colección en el índice especificado por ind. El método get( ) devuelve el elemento en ind. indexOF( ) devuelve el índice de la primera aparición de obj; lastIndexOf( ) devuelve el de la última aparición. Ambos devuelven –1 si el objeto no se encuentra en la lista. El método remove( ) elimina el elemento de ind y devuelve el elemento eliminado. Para establecer el elemento en un índice especificado, llame a set( ). El índice se pasa vía ind, y el nuevo objeto se pasa en obj. Devuelve el elemento anterior a ese índice. Cuando use una ArrayList, puede especificar una capacidad inicial al llamar a ensureCapacity( ), que se muestra aquí: void ensureCapacity(int cap) La capacidad es el número de elementos que pede contener una ArrayList antes de que necesite agrandarse. La capacidad se especifica con cap. En casos en que sabe que un cierto número mínimo de elementos se almacenará, puede establecer la capacidad inicial de antemano. Esto evita posteriores reasignaciones, que son costosas en tiempo. Con la excepción de ensureCapacity( ), y de trimToSize( ) descrito al final de esta solución, una LinkedList tiene las mismas opciones que una ArrayList. La diferencia es la implementación. Aunque una LinkedList permite el acceso a un elemento mediante un índice (como se mostrará en el ejemplo), ese acceso será menos eficiente que el de una ArrayList. Sin embargo, las inserciones y eliminación de una LinkedList son muy eficientes, porque los vínculos simplemente se reorganizan. No es necesario compactar ni expandir una matriz. En general, una LinkedList debe usarse para aplicaciones en que se necesitan listas arbitrariamente largas y en que se eliminarán o insertarán elementos con frecuencia. Una ArrayList debe usarse cuando se requiere una matriz dinámica. Ejemplo En el siguiente ejemplo se demuestra List, empleando ArrayList y LinkedList: // Demuestra List. import java.util.*; www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones class DemoList { public static void main(String args[ ]) { System.out.println("Creando una ArrayList llamada al."); ArrayList<Character> al = new ArrayList<Character>( ); // Agrega elementos. al.add(‘A’); al.add(‘B’); al.add(‘D’); System.out.println("al en orden de \u00a1ndice: "); for(int i=0; i < al.size( ); i++) System.out.print(al.get(i) + " "); System.out.println("\n"); // Ahora, inserta un elemento en el índice 2. al.add(2, ‘C’); System.out.println("al tras agregar C: "); for(int i=0; i < al.size( ); i++) System.out.print(al.get(i) + " "); System.out.println("\n"); // Elimina B. al.remove(1); System.out.println("al tras eliminar B: "); for(int i=0; i < al.size( ); i++) System.out.print(al.get(i) + " "); System.out.println("\n"); // Establece el valor del último elemento. al.set(al.size( )–1, ‘X’); System.out.println("al tras establecer el \u00a3ltimo elemento: "); for(int i=0; i < al.size( ); i++) System.out.print(al.get(i) + " "); System.out.println("\n"); // Agrega otro C. al.add(‘C’); System.out.println("al tras agregar otro C: "); for(int i=0; i < al.size( ); i++) System.out.print(al.get(i) + " "); System.out.println("\n"); www.fullengineeringbook.net 193 194 Java: Soluciones de programación System.out.println("El \u00a1ndice del primer C: " + al.indexOf(‘C’)); System.out.println("El \u00a1ndice del \u00a3ltimo C: " + al.lastIndexOf(‘C’)); System.out.println(""); // Limpia la lista. al.clear( ); // Asegura una capacidad de por lo menos 26. al.ensureCapacity(26); // Agrega de la letra a a la z. for(int i=0; i < 26; i++) al.add(i, (char) (‘a’ + i)); System.out.println("al tras limpiar, " + "asegurar capacidad,\n" + "y luego agregar de la a a la z: "); for(int i=0; i < al.size( ); i++) System.out.print(al.get(i) + " "); System.out.println("\n"); // Ahora se crea una lista vinculada desde al. System.out.println("Creando una LinkedList llamada ll."); LinkedList<Character> ll = new LinkedList<Character>(al); // LinkedList sostiene las mismas operaciones, excepto // ensureCapacity( ) y trimToSize( ), que ArrayList. // Por ejemplo, puede iterar en el contenido de // una LinkedList usando el método get( ): System.out.println("Contenido de ll:"); for(int i=0; i < ll.size( ); i++) System.out.print(ll.get(i) + " "); System.out.println( ); } } Aquí se muestra la salida: Creando una ArrayList llamada al. al en orden de índice: A B D al tras agregar C: A B C D al tras eliminar B: A C D www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 195 al tras establecer el último elemento: A C X al tras agregar otro C: A C X C El índice del primer C: 1 El índice del último C: 3 al tras limpiar, asegurar capacidad, y luego agregar de la a a la z: a b c d e f g h i j k l m n o p q r s t u v w x y z Creando una LinkedList llamada ll. Contenido de ll: a b c d e f g h i j k l m n o p q r s t u v w x y z Opciones En el ejemplo se usa indización explícita para iterar en el contenido de una lista. Sin embargo, todas las implementaciones de List pueden iterarse con el estilo for-each del bucle for, como se muestra en la solución anterior, o mediante un iterador. El uso de un iterador se describe en Itere en una colección. LinkedList implementa la interfaz Deque, que da acceso a LinkedList a todos los métodos definidos por Deque. Consulte Cree una cola o una pila empleando Deque para conocer un ejemplo. ArrayList le permite reducir el tamaño de una colección al mínimo necesario para conservar el número de elementos actualmente almacenados. Esto se hace al llamar a trimToSize( ). Aquí se muestra: void trimToSize( ) Trabaje con conjuntos Componentes clave Clases e interfaces Métodos java.util.Set<E> java.util.Set<E> java.util.SortedSet<E> E first( ) E last( ) java.util.NavigableSet<E> E higher(E obj) E lower(E obj) java.util.HashSet<E> java.util.TreeSet<E> www.fullengineeringbook.net 196 Java: Soluciones de programación Como List, Set es una subinterfaz de Collection. La interfaz Set difiere de List de una manera muy importante: una implementación de Set no permite elementos duplicados. Set no agrega métodos a los heredados de Collection. Sin embargo, es necesario que add( ) devuelva falso si se hace un intento de agregar un elemento duplicado. La interfaz Set no requiere que la colección se mantenga en orden. Sin embargo, SortedSet, que extiende Set, sí lo requiere. Por tanto, si quiere un conjunto ordenado, entonces usará una colección que implemente la interfaz SortedSet. A partir de Java 6, NavigableSet también extiende Set. El paquete java.util define dos implementaciones concretas de Set: HashSet y LinkedHashSet. También proporciona TreeSet, que es una implementación concreta de SortedSet y NavigableSet. En esta solución se demuestran los conjuntos. Usa HashSet y TreeSet. Paso a paso El uso de un conjunto incluye los pasos siguientes: 1. Cree una implementación concreta de Set. En el caso de operaciones básicas con conjuntos, use HashSet o LinkedHashSet. En el caso de un conjunto ordenado, al igual que uno navegable, use TreeSet. 2. Agregue elementos al conjunto. Recuerde que no se permiten duplicados. 3. Se tiene acceso a todas las instancias de Set mediante los métodos definidos por Collection. Cuando se usa un TreeSet, también puede usar los métodos definidos por SortedSet y NavigableSet. Análisis Las operaciones básicas con colecciones definidas por Collection (que son heredadas por Set) se describen en Técnicas básicas de colecciones. Cuando trabaje con un SortedSet (como un TreeSet), puede usar los métodos definidos por Set, pero también los proporcionados por SortedSet. Los usados en esta solución son first( ), que devuelve una referencia al primer elemento del conjunto, y last( ), que devuelve una al último. Aquí se muestran estos métodos: E first( ) E last( ) Cada uno obtiene el elemento indicado. NavigableSet, que es implementada por TreeSet, define varios métodos que le permiten buscar el conjunto de coincidencias más cercanas. En esta solución se usan ambos métodos. El primero es higher( ), que obtiene el primer elemento que es mayor a un valor especificado. El segundo es lower( ), que obtiene el primer elemento que es menor a un valor especificado. Aquí se muestran: E higher(E obj) E lower(E obj) Cada uno obtiene el elemento indicado. NOTA NavigableSet Se agregó en Java 6. Por tanto, es necesario usar una versión moderna de Java. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 197 Ejemplo En el siguiente ejemplo se demuestran las colecciones basadas en Set, SortedSet y NavigableSet. Usa la conversión predeterminada proporcionada por toString( ) para convertir un conjunto en una cadena para darle salida. // Demuestra Set. import java.util.*; class DemoSet { public static void main(String args[ ]) { HashSet<String> hs = new HashSet<String>( ); // Agrega elementos. hs.add("uno"); hs.add("dos"); hs.add("tres"); // Despliega el conjunto usando la conversión predeterminada toString( ). System.out.println("He aqu\u00a1 el HashSet: " + hs); // Trata de agregar de nuevo tres. if(!hs.add("tres")) System.out.println("Intento de agregar un duplicado. " + "Set no se modifica: " + hs); // Ahora usa TreeSet, que implementa // SortedSet y NavigableSet. TreeSet<Integer> ts = new TreeSet<Integer>( ); // Agrega elementos. ts.add(8); ts.add(19); ts.add(–2); ts.add(3); // Observe que este conjunto está ordenado. System.out.println("\nHe aqu\u00a1 el TreeSet: " + ts); // Usa first( ) y last( ) de SortedSet. System.out.println("El primer elemento en ts: " + ts.first( )); System.out.println("El \u00a3ltimo elemento en ts: " + ts.last( )); // Usa higher( ) y lower( ) de NavigableSet. System.out.println("Primer elemento > 15: " + ts.higher(15)); System.out.println("Primer elemento < 15: " + ts.lower(15)); } } www.fullengineeringbook.net 198 Java: Soluciones de programación Aquí se muestra la salida: He aquí el HashSet: [tres, uno, dos] Intento de agregar un duplicado. Set no se modifica: [tres, uno, dos] He aquí el TreeSet: [–2, 3, 8, 19] El primer elemento en ts: –2 El último elemento en ts: 19 Primer elemento > 15: 19 Primer elemento < 15: 8 Observe cómo el contenido de los conjuntos se muestra usando sus conversiones predeterminadas toString( ). Cuando una colección se convierte en una cadena empleando toString( ), los elementos se muestran dentro de corchetes, y los elementos están separados por comas. Ejemplo adicional Debido a que Set no permite elementos duplicados, puede usarse para definir las operaciones de conjunto básicas definidas por la teoría de los conjuntos. Se trata de: • Unión • Intersección • Diferencia • Diferencia simétrica • Es un subconjunto • Es un superconjunto El significado de cada operación se describe aquí. Para comprender mejor el análisis, suponemos los siguientes conjuntos: Conjunto1: A, B, C, D Conjunto2: C, D, E, F La unión de los dos conjuntos produce un conjunto que contiene todos los elementos de ambos conjuntos. Por ejemplo, la unión de Conjunto1 y Conjunto2 es A, B, C, D, E, F Observe que sólo se incluye un caso de C y de D porque no se permiten duplicados en un conjunto. La intersección de dos conjuntos produce uno nuevo que contiene sólo los elementos comunes para ambos. Por ejemplo, la intersección de Conjunto1 y Conjunto2 produce el siguiente conjunto: C, D Debido a que C y D son los únicos elementos que ambos conjuntos comparten, son los únicos elementos en la intersección. La diferencia entre los dos conjuntos produce un nuevo conjunto que contiene los elementos del primer conjunto que no aparecen en el segundo. Por ejemplo, Conjunto1 – Conjunto2 produce el siguiente conjunto: A, B www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 199 Debido a que C y D son miembros de Conjunto2, se restan del Conjunto1 y no son, por tanto, parte del conjunto resultante. La diferencia simétrica entre dos conjuntos está compuesta por los elementos que se encuentran en un conjunto o en otro, pero no en ambos. Por ejemplo, la diferencia simétrica de Conjunto1 y Conjunto2 es A, B, E, F C y D no son parte del resultado porque son miembros de ambos conjuntos. Dados dos conjuntos ConjuntoX y ConjuntoY, el primero es un subconjunto del segundo sólo si todos sus elementos son también elementos del otro. ConjuntoX es un superconjunto de ConjuntoY sólo si todos los elementos de ConjuntoY también son elementos de ConjuntoX. En el siguiente programa se implementan las operaciones de conjuntos que se acaban de definir. // Este programa crea una clase llamada OpsCjt que define // métodos que realizan las siguientes operaciones de conjuntos: // // unión // intersección // diferencia // diferencia simétrica // es subconjunto // es superconjunto import java.util.*; // Una clase que da soporte a varias operaciones con conjuntos. class OpsCjt { // Unión public static <T> Set<T> union(Set<T> cjtA, Set<T> cjtB) { Set<T> tmp = new TreeSet<T>(cjtA); tmp.addAll(cjtB); return tmp; } // Intersección public static <T> Set<T> intersection(Set<T> cjtA, Set<T> cjtB) { Set<T> tmp = new TreeSet<T>( ); for(T x : cjtA) if(cjtB.contains(x)) tmp.add(x); return tmp; } // Diferencia public static <T> Set<T> difference(Set<T> cjtA, Set<T> cjtB) { Set<T> tmp = new TreeSet<T>(cjtA); tmp.removeAll(cjtB); return tmp; } // Diferencia simétrica public static <T> Set<T> symDifference(Set<T> cjtA, Set<T> cjtB) { www.fullengineeringbook.net 200 Java: Soluciones de programación Set<T> tmpA; Set<T> tmpB; tmpA = union(cjtA, cjtB); tmpB = intersection(cjtA, cjtB); return difference(tmpA, tmpB); } // Devuelve verdadero si CjtA is es un subconjunto de CjtB public static <T> boolean isSubset(Set<T> cjtA, Set<T> cjtB) { return cjtB.containsAll(cjtA); } // Devuelve verdadero si CjtA is es un superconjunto de CjtB public static <T> boolean isSuperset(Set<T> cjtA, Set<T> cjtB) { return cjtA.containsAll(cjtB); } } // Demuestra las operaciones de conjuntos. class DemoOpsCjt { public static void main(String args[ ]) { TreeSet<Character> cjt1 = new TreeSet<Character>( ); TreeSet<Character> cjt2 = new TreeSet<Character>( ); cjt1.add(‘A’); cjt1.add(‘B’); cjt1.add(‘C’); cjt1.add(‘D’); cjt2.add(‘C’); cjt2.add(‘D’); cjt2.add(‘E’); cjt2.add(‘F’); System.out.println("cjt1: " + cjt1); System.out.println("cjt2: " + cjt2); System.out.println( ); System.out.println("Uni\u00a2n: " + OpsCjt.union(cjt1, cjt2)); System.out.println("Intersecci\u00a2n: " + OpsCjt.intersection(cjt1, cjt2)); System.out.println("Diferencia (cjt1 – cjt2): " + OpsCjt.difference(cjt1, cjt2)); System.out.println("Diferencia simetrica: " + OpsCjt.symDifference(cjt1, cjt2)); System.out.println( ); // Ahora, demuestra isSubset( ) y isSuperset( ). TreeSet<Character> cjt3 = new TreeSet<Character>(cjt1); www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 201 cjt3.remove(‘D’); System.out.println("cjt3: " + cjt3); System.out.println("\u00a8Es cjt1 un subconjunto de cjt2? " OpsCjt.isSubset(cjt1, cjt3)); System.out.println("\u00a8Es cjt1 un subconjunto de cjt2? " OpsCjt.isSuperset(cjt1, cjt3)); System.out.println("\u00a8Es cjt3 un subconjunto de cjt1? " OpsCjt.isSubset(cjt3, cjt1)); System.out.println("\u00a8Es cjt3 un superconjunto de cjt1? OpsCjt.isSuperset(cjt3, cjt1)); + + + " + } } Aquí se muestra la salida: cjt1: [A, B, C, D] cjt2: [C, D, E, F] Unión: [A, B, C, D, E, F] Intersección: [C, D] Diferencia (cjt1 – cjt2): [A, B] Diferencia simetrica: [A, B, E, F] cjt3: [A, B, C] ¿Es cjt1 un subconjunto de cjt2? false ¿Es cjt1 un subconjunto de cjt2? true ¿Es cjt3 un subconjunto de cjt1? true ¿Es cjt3 un superconjunto de cjt1? false Opciones Otra implementación de Set proporcionada por la estructura de colecciones es LinkedHashSet. Usa una tabla de hash para almacenar los elementos del conjunto, pero también mantiene una lista vinculada de los elementos en el orden en que se agregaron al conjunto. Un iterador para LinkedHashSet iterará en los elemento al seguir los vínculos. Esto significa que el iterador devolverá elementos en el orden de su inserción en el conjunto. Consulte Itere en una colección para conocer más detalles. Use Comparable para almacenar objetos en una colección ordenada Componentes clave Clases e interfaces Métodos java.lang.Comparable<T> int compareTo(T obj2) java.util.TreeSet>E> www.fullengineeringbook.net 202 Java: Soluciones de programación En general, una colección puede almacenar cualquier tipo de objeto. Sin embargo, las colecciones ordenadas, como PriorityQueue o TreeSet, colocan una condición en esos objetos: deben implementar la interfaz Comparable. He aquí por qué: la interfaz Comparable define el método compareTo( ), que determina el "orden natural" de los objetos de la clase. Como se usa aquí, orden natural significa el orden que normalmente se esperaría. Por ejemplo, el orden natural de las cadenas es alfabético; en el caso de valores numéricos, es el orden numérico. (En otras palabras, la A antes de la B, el 1 antes del 2, etc.). Las colecciones ordenadas usan el orden natural definido por una clase para determinar el orden de los objetos. Por tanto, si quiere almacenar objetos en una colección ordenada, su clase debe implementar Comparable. En esta solución se muestra el procedimiento general. (El mismo método general también se aplica a mapas ordenados). Paso a paso Para almacenar objetos de clases que se crean en una colección ordenada, se requieren los siguientes pasos: 1. Cree la clase cuyos objetos se almacenarán en la colección ordenada. La clase debe implementar la interfaz Comparable. 2. Implemente el método compareTo( ) especificado por la interfaz Comparable para definir el orden natural de la clase. 3. Si es necesario, sobrescriba equals( ) para que sea consistente con los resultados producidos por compareTo( ). 4. Construya una colección ordenada, especificando su clase como un argumento de tipo. 5. Agregue objetos de su clase a la colección. El método compareTo( ) determina el orden en que se almacenarán los elementos. Análisis Si un objeto se almacenará en una colección o mapa ordenado, su clase debe implementar la interfaz Comparable, que define una manera estándar en que dos objetos de la misma clase habrán de compararse. Está implementada por muchas clases de API de Java, incluidas Byte, Character, Double, Float, Long, Short, String e Integer. Comparable es genérica y se declara así: interface Comparable<T> Aquí, T representa el tipo de objeto que habrá de compararse. Comparable especifica sólo un método: compareTo( ). Compara dos objetos y devuelve el resultado. La salida de esta comparación determina el orden natural de las instancias de una clase. Aquí se muestra el método compareTo( ): int compareTo(T obj) Este método compara el objeto que invoca con obj. Se devuelve 0 si los valores son iguales. Se devuelve un valor negativo si el objeto que invoca tiene un valor menor. De otra manera, se devuelve un valor positivo. Por supuesto, usted determina la manera en que tiene lugar la comparación cuando implementa compareTo( ). Se lanzará una ClassCastException si los dos objetos no son compatibles. Hay una regla que normalmente debe seguirse cuando se implementa Comparable para una clase (en especial si los objetos de esa clase se almacenarán en SortedSet o SortedMap). La salida de compareTo( ) debe ser consistente con la de equals( ). En otras palabras, para cada www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 203 caso en que equals( ) devuelva true, compareTo( ) debe devolver 0. Esto es importante porque SortedSet no permite elementos duplicados. Debido a que la implementación predeterminada de equals( ) proporcionada por Object usa igualdad de referencias, a menudo querrá sobreescribir equals( ) para que sus resultados sean consistentes con su implementación de compareTo( ). Ejemplo En el siguiente ejemplo se muestra cómo puede usarse Comparable para definir el orden natural de un objeto de modo que pueda almacenarse en una colección ordenada. Usa un TreeSet para almacenar objetos de tipo Producto, que encapsula información de un producto, incluido su nombre e ID. El método compareTo( ) ordena instancias de Producto con base en el nombre. Por tanto, la colección se ordenará por el nombre del producto. // // // // // // Demuestra Comparable con una colección ordenada. Si un objeto se almacenará en una colección o un mapa ordenado, su clase debe implementar la interfaz Comparable, que define el "orden natural" de la clase. import java.util.*; // Esta clase encapsula el nombre y el número de ID // de un producto. Implementa Comparable de modo // que el orden natural esté determinado por el // nombre de los productos. class Producto implements Comparable<Producto> { String nombreProd; int idProd; Producto(String cad, int id) { nombreProd = cad; idProd = id; } // Compara dos productos con base en sus nombres. public int compareTo(Producto p2) { return nombreProd.compareToIgnoreCase(p2.nombreProd); } // Sobreescribe equals( ) para que sea consistente con compareTo( ). public boolean equals(Object p2) { return nombreProd.compareToIgnoreCase(((Producto)p2).nombreProd)==0; } } // Demuestra la interfaz Comparable. class DemoComp { public static void main(String args[ ]) { www.fullengineeringbook.net 204 Java: Soluciones de programación // Crea un TreeSet que usa el orden natural. TreeSet<Producto> listaProd = new TreeSet<Producto>( ); // Agrega algunos listaProd.add(new listaProd.add(new listaProd.add(new listaProd.add(new productos a listaProd. Producto("Estante", 13546)); Producto("Teclado", 04762)); Producto("Escritorio", 12221)); Producto("Archivero", 44387)); // Despliega los productos, ordenados por nombre. System.out.println("Productos ordenados por nombre:\n"); for(Producto p : listaProd) System.out.printf("%–14s ID: %d\n", p.nombreProd, p.idProd); } } Aquí se muestra la salida: Productos ordenados por nombre: Archivero Escritorio Estante Teclado ID: ID: ID: ID: 44387 12221 13546 2546 Opciones La implementación de Comparable es la mejor manera de especificar el orden que se usará para los objetos de una clase, pero es una opción que resulta útil en algunos casos. Puede crear un comparador personalizado que determine la manera en que se comparan los objetos. Este comparador se pasa al constructor de la colección cuando se crea. Luego el comparador se usa para comparar dos objetos. En la siguiente solución encontrará un ejemplo. Su implementación de compareTo( ) no necesita ordenar elementos en orden ascendente. Al revertir la salida de la comparación, los elementos pueden organizarse en orden inverso. Por ejemplo, trate de cambiar el método compareTo( ) en el ejemplo, como se muestra aquí. Llama a compareToIgnoreCase( ) en p2 en lugar de hacerlo en el objeto que invoca. Por tanto, la salida de la comparación se invierte. // Invierte la salida de una comparación entre dos productos. public int compareTo(Producto p2) { return p2.nombreProd.compareToIgnoreCase(nombreProd); Después de hacer este cambio, la colección se ordenará a la inversa. Por cierto, observe que la comparación inversa aún es consistente con la sobreescritura de equals( ). Sólo ha cambiado el orden. Cualquier objeto que esté usando como clave en un mapa ordenado (como TreeMap) también debe implementar Comparable. Use el mismo procedimiento básico que se acaba de describir. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 205 Use un Comparator con una colección Componentes clave Clases e interfaces Métodos java.util.Comparator<T> int compare(T obj1, T obj2) java.util.PriorityQueue<E> boolean add(E obj) E poll( ) Como se explicó en la solución anterior, los objetos almacenados en una colección ordenada (como TreeSet o PriorityQueue) siguen el orden natural, como opción predeterminada. Este orden se determina por la salida del método compareTo( ) definida por la interfaz Comparable. Si quiere ordenar elementos de una manera distinta al orden natural del objeto, tiene dos opciones. En primer lugar, puede cambiar la implementación de compareTo( ). Sin embargo, al hacerlo se cambiará el orden natural de su clase, lo que afectará a todos los usos de su clase. Además, tal vez quiera cambiar el orden de las clases, para lo que no tiene acceso al código fuente. Por estas razones, la segunda opción suele ser mejor. Puede crear un comparador que defina el orden deseado. Un comparador es un objeto que implementa la interfaz Comparator. El comparador puede pasarse a una colección ordenada cuando se crea la colección, y se usa para determinar el orden de los elementos dentro de la colección. En esta solución se muestra cómo crear y usar un comparador personalizado. En el proceso, se demuestra PriorityQueue. Paso a paso Para usar un comparador personalizado que controle el orden de los objetos dentro de una colección se requieren estos pasos: 1. Cree una clase que implemente Comparator para el tipo de datos que se almacenará en la colección. 2. Codifique el método compare( ) para que ordene los datos de la manera deseada. Si es necesario, debe ser consistente con la salida de equals( ) cuando se usa en los mismos objetos. 3. Cree una colección ordenada (como TreeSet o PriorityQueue), especificando el comparador para el constructor. Esto hace que la colección use el comparador para ordenar los elementos de la colección, en lugar de utilizar el orden natural. Análisis Comparator está empaquetada en java.util y es una interfaz genérica que tiene esta declaración: interface Comparator<T> Aquí, T especifica el tipo de objeto que se está comparando. www.fullengineeringbook.net 206 Java: Soluciones de programación La interfaz Comparator define dos métodos compare( ) y equals( ). El método compare( ), que se muestra aquí, compara el orden de dos elementos: int compare(T obj1, T obj2) Aquí, obj1 y obj2 son los objetos que se compararán. Este método devuelve cero si los objetos son iguales. Devuelve un valor positivo si obj1 es mayor que obj2. De otra manera, se devuelve un valor negativo. El método puede lanzar una ClassCastException si los tipos de los objetos no son compatibles para la comparación. Su implementación de compare( ) determina la manera en que se ordenan los objetos. Por ejemplo, para usar un orden inverso, puede crear un comparador que invierta la salida de una comparación. El método equals( ), que se muestra aquí, prueba si un objeto es igual al comparador que invoca: boolean equals(Object objct) Aquí, obj es el objeto cuya igualdad se probará. El método devuelve verdadero si obj y el objeto que invoca son objetos de Comparator y usan el mismo orden. De otra manera, devuelve falso. Por lo general es innecesario sobreescribir equals( ), y comparadores más simples no lo harán. (Nota: esta versión de equals( ) no compara dos objetos de tipo T. Compara dos comparadores.) Cuando crea un comparador para usarlo con SortedSet (o cualquier colección que requiera elementos únicos), la salida de compare( ) debe ser consistente con el resultado de equals( ), cuando se usa en los dos mismos objetos. En otras palabras, dada una clase X, entonces la implementación de X de equals( ) debe devolver el mismo resultado que compare( ) definido por Comparator<X> cuando dos objetos X son iguales. La razón para este requisito es que un SortedSet no puede contener elementos duplicados. Después de que ha creado un comparador, puede pasarlo al constructor de las colecciones ordenadas. En el ejemplo siguiente se usa una PriorityQueue. He aquí el constructor que se usa: PriorityQueue(int capacidad, Comparator<? super E> comp) Aquí, capacidad especifica la capacidad inicial de la cola y comp especifica el comparador. Cuando se usa una PriorityQueue, agregará elementos a la cola usando add( ) u offer( ). Cualquiera de ellos insertará el elemento en el orden determinado por el orden natural de éste, o por el comparador especificado cuando se construyó PriorityQueue (que es el caso con esta solución). Para obtener elementos de una PriorityQueue en orden de prioridad, debe usar poll( ). (No puede usar un iterador para este fin, porque los elementos no se devolverán en orden de prioridad.) El método poll( ) se muestra a continuación: E poll( ) Elimina y devuelve el siguiente elemento de la cola en orden de prioridad. Devuelve null si la cola está vacía. Ejemplo En el siguiente ejemplo se usa un comparador para crear una lista de mensajes ordenados por prioridad. Cada mensaje es un objeto de tipo Mensaje, que es una clase que encapsula una cadena y el código de prioridad. Los niveles de prioridad se especifican por una enum llamada NivelP, que define tres prioridades: Baja, Media y Alta. Mensaje implementa Comparable, que define el orden natural de los elementos. Es el orden definido por NivelP, que va de alta a baja. La clase www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 207 ComparadorMsjInv crea un comparador para objetos de Mensaje que invierte el orden natural. Por tanto, cuando se usa ComparadorMsjInv, los mensajes se organizan de bajo a alto. Luego se crean dos PriorityQueue. La primera usa el orden natural de Mensaje para ordenar los mensajes. La segunda usa ComparadorMsjInv para el mismo fin. Como lo muestra la salida, el orden de los mensajes en la segunda cola es el inverso del de la primera. Un tema adicional es que, debido a que Mensaje está diseñado para usarse con PriorityQueue, no es necesario sobreescribir equals( ) para que sea consistente con compare( ) en Mensaje o compareTo( ) en ComparadorMsjInv. La implementación predeterminada proporcionada por Object es suficiente. En realidad, tratar de hacer equals( ) consistente con compare( ) o compareTo( ) sería incorrecto en este caso, porque las comparaciones están basadas en la prioridad de un mensaje, no en su contenido. NOTA PriorityQueue se agregó en Java 5. Por tanto, debe estar usando una versión moderna de Java para compilar y ejecutar el siguiente ejemplo. // Usa un Comparator para crear una PriorityQueue para mensajes. import java.util.*; // Esta clase encapsula un mensaje ordenado por prioridad. // Implementa Comparable, que define su "orden natural". class Mensaje implements Comparable<Mensaje> { String msj; // Esta enumeración define los niveles de prioridad. enum NivelP { Alta, Media, Baja } NivelP prioridad; Mensaje(String cad, NivelP pri) { msj = cad; prioridad = pri; } // Compara dos mensajes con base en sus prioridades. public int compareTo(Mensaje msj2) { return prioridad.compareTo(msj2.prioridad); } } // Un comparador inverso para Mensaje. class ComparadorMsjInv implements Comparator<Mensaje> { public int compare(Mensaje msj1, Mensaje msj2) { return msj2.prioridad.compareTo(msj1.prioridad); } } www.fullengineeringbook.net 208 Java: Soluciones de programación // Demuestra Comparadores con PriorityQueue. class DemoCMsjPri { public static void main(String args[ ]) { Mensaje m; // Crea una cola de prioridad que usa el orden natural. PriorityQueue<Mensaje> pq = new PriorityQueue<Mensaje>(3); // Agrega algún mensaje a pq. pq.add(new Mensaje("Junta en las oficinas principales a las 3pm", Mensaje.NivelP.Baja)); pq.add(new Mensaje("Fuego en la bodega.", Mensaje.NivelP.Alta)); pq.add(new Mensaje("Informe vencido del martes", Mensaje.NivelP.Media)); // Despliega los mensajes en el orden natural de prioridad. System.out.println("Mensajes en orden natural de prioridad: "); while((m = pq.poll( )) != null) System.out.println(m.msj + " Prioridad: " + m.prioridad); System.out.println( ); // Ahora, crea una cola de prioridad que almacena // los mensajes en orden inverso. PriorityQueue<Mensaje> pqInv = new PriorityQueue<Mensaje>(3, new ComparadorMsjInv( )); // Agrega los mismos mensajes a pqInv. pqInv.add(new Mensaje("Junta en las oficinas principales a las 3pm", Mensaje.NivelP.Baja)); pqInv.add(new Mensaje("Fuego en la bodega.", Mensaje.NivelP.Alta)); pqInv.add(new Mensaje("Informe vencido del martes", Mensaje.NivelP.Media)); // Despliega los mensajes en el orden inverso de prioridad. System.out.println("Mensajes en orden inverso de prioridad: "); while((m = pqInv.poll( )) != null) System.out.println(m.msj + " Prioridad: " + m.prioridad); } } Aquí se muestra la salida: Mensajes en orden natural de prioridad: Fuego en la bodega. Prioridad: Alta Informe vencido del martes Prioridad: Media Junta en las oficinas principales a las 3pm Prioridad: Baja www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 209 Mensajes en orden inverso de prioridad: Junta en las oficinas principales a las 3pm Prioridad: Baja Informe vencido del martes Prioridad: Media Fuego en la bodega. Prioridad: Alta Opciones Una de las principales ventajas de Comparator es que puede definir un comparador para una clase en que no tenga código fuente. Por ejemplo, puede almacenar cadenas en orden inverso al usar el siguiente comparador: // Un comparador inverso para String. class ComparadorMsjInv implements Comparator<String> { public int compare(String cad1, String cad2) { return cad2.compareTo(cad1); } } Algo interesante es que este comparador también es consistente con equals( ), como se definió con String. Es una manera muy fácil de obtener un comparador inverso para un tipo de datos específico: llamar al algoritmo reverseOrder( ) definido por Collections. Devuelve un Comparator que realiza una comparación inversa. Aquí se muestra: static <T> Comparator<T> reverseOrder( ) Entre otras cosas, un comparador inverso puede usarse para ordenar una colección de manera descendente. El mismo procedimiento básico usado para crear un comparador para una colección también se aplica para usar una con un mapa ordenado, como TreeMap. Cuando se ordena un mapa, su orden se basa en sus claves. Itere en una colección Componentes clave Interfaces Métodos java.util.Iterator<E> boolean hasNext( ) E next( ) void remove( ) java.util.ListIterator<E> boolean hasNext( ) boolean hasPrevious( ) E next( ) E previous( ) void remove( ) java.util.Collection<E> Iterator<E> iterator( ) java.util.List<E> ListIterator<E> listIterator( ) www.fullengineeringbook.net 210 Java: Soluciones de programación Una de las operaciones más comunes realizadas en una colección es iterar en sus elementos. Por ejemplo, tal vez quiera desplegar cada elemento o realizar alguna transformación en él. Una manera de recorrer en bucle los elementos de una colección consiste en emplear un iterador, que es un objeto que implementa la interfaz Iterator o ListIterator. Iterator le permite iterar en una colección, obteniendo o eliminando elementos. ListIterator extiende Iterator para permitir el recorrido bidireccional de una lista, además de la modificación de elementos. En esta solución se muestra el procedimiento general para usar ambos tipos de iteradores. Paso a paso Para iterar en una colección empleando un iterador, siga estos pasos: 1. Obtenga un iterador para la colección al llamar al método iterator( ) de la colección. 2. Configure un bucle que hace una llamada a hasNext( ). 3. Dentro del bucle, obtenga cada elemento al llamar a next( ). 4. Haga que el bucle itere siempre y cuando hasNext( ) devuelve verdadero. En el caso de colecciones que implementan List, puede usar ListIterator para recorrer en bucle la lista. Para ello, siga estos pasos: 1. Obtenga un iterador para la colección al llamar al método listIterator( ) de la colección. 2. Dentro de un bucle, use hasNext( ) para determinar si la colección tiene un elemento posterior. Use hasPrevious( ) para determinar si la colección tiene un elemento anterior. 3. Dentro del bucle, obtenga el siguiente elemento al llamar next( ) u obtenga el elemento anterior al llamar a previous( ). 4. Detenga la iteración cuando no haya disponible un elemento siguiente o anterior. Análisis Iterator y ListIterator son interfaces genéricas que se declaran como se muestra aquí: interface Iterator<E> interface ListIterator<E> Aquí, E especifica el tipo de objetos que se está iterando. La interfaz Iterator declara los siguientes métodos: Método Descripción boolean hasNext( ) Devuelve verdadero si hay más elementos. De otra manera, devuelve falso. E next( ) Devuelve el siguiente elemento. Lanza NoSuchElementException si no hay un elemento siguiente. void remove( ) Elimina el elemento actual. Lanza IllegalStateException si se hace un intento por llamar a remove( ) que no es antecedido por una llamada a next( ). Se lanza una UnsupportedOperationException si la colección es inmutable. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 211 Los métodos definidos por Iterator permiten que una colección se recorra en bucle de manera estrictamente secuencial, de principio a fin. Un Iterator no puede recorrerse hacia atrás. ListIterator extiende Iterator y agrega varios métodos propios. Los usados por esta solución se muestran aquí. Método Descripción boolean hasPrevious( ) Devuelve verdadero si hay un elemento anterior. De otra manera, devuelve falso. E previous( ) Devuelve el elemento anterior. Se lanza una NoSuchElementException si no hay un elemento previo. Como puede ver, ListIterator agrega la capacidad de moverse hacia atrás. Por tanto, un ListIterator puede obtener el siguiente elemento de la colección, o el anterior. Antes de que pueda acceder a una colección a través de un iterador, debe obtener uno. Esto se hace al llamar al método iterator( ), que devuelve un iterador al inicio de la colección. Este método está especificado por la interfaz Iterable, que es heredado por Collection. Por tanto, todas las colecciones proporcionan el método iterator( ). Aquí se muestra: Iterator<E> iterator( ) Al usar el iterador devuelto por iterator( ), puede acceder a todos los elementos de la colección, de principio a fin, de elemento en elemento. En el caso de colecciones que implementan List, también puede obtener un iterador al llamar a listIterator( ), que es especificado por List. Hay dos versiones de listIterator( ), que se muestran aquí: ListIterator<E> listIterator( ) ListIterator<E> listIterator(int ind) La primera versión devuelve un iterador de listas al principio de la lista. La segunda devuelve un iterador de listas que inician en el índice especificado. El índice pasado mediante ind debe estar dentro del rango de la lista. De otra manera, se lanza una IndexOutOfBoundsException. Como se explicó, un iterador de listas le da la capacidad de acceder a la colección en dirección hacia delante o hacia atrás y le permite modificar un elemento. Ejemplo En el siguiente ejemplo se demuestran un iterador y un iterador de listas. Crea una lista telefónica simple que está almacenada en LinkedList. Despliega la lista en dirección hacia delante mediante un Iterator. Luego, despliega la lista en orden inverso, al usar ListIterator. // // // // // Usa un Iterador para recorrer en bucle una colección en dirección hacia delante. Usa un ListIterator para recorrer en bucle una colección en dirección inversa. import java.util.*; www.fullengineeringbook.net 212 Java: Soluciones de programación // Esta clase encapsula un nombre y un número telefónico. class EntradaTelefono { String nombre; String numero; EntradaTelefono(String n, String num) { nombre = n; numero = num; } } // Demuestra Iterator y ListIterator. class DemoItr { public static void main(String args[ ]) { LinkedList<EntradaTelefono> agenda = new LinkedList<EntradaTelefono>( ); agenda.add(new EntradaTelefono("Ernesto", "555–3456")); agenda.add(new EntradaTelefono("Carlos", "555–3976")); agenda.add(new EntradaTelefono("Karen", "555–1010")); // Usa un Iterador para mostrar la lista. Iterator<EntradaTelefono> itr = agenda.iterator( ); EntradaTelefono et; System.out.println("Itera en la lista en " + "direcci\u00a2n hacia delante:"); while(itr.hasNext( )) { et = itr.next( ); System.out.println(et.nombre + ": " + et.numero); } System.out.println( ); // Usa un ListIterator para mostrar la lista en orden inverso. ListIterator<EntradaTelefono> litr = agenda.listIterator(agenda.size( )); System.out.println("Itera en la lista en " + "direcci\u00a2n inversa:"); while(litr.hasPrevious( )) { et = litr.previous( ); System.out.println(et.nombre + ": " + et.numero); } } } www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 213 Aquí se muestra la salida: Itera en la lista en dirección hacia delante: Ernesto: 555–3456 Carlos: 555–3976 Karen: 555–1010 Itera en la lista en dirección inversa: Karen: 555–1010 Carlos: 555–3976 Ernesto: 555–3456 Opciones El estilo for-each del bucle for ofrece una opción a un iterador en situaciones en que no modificará el contenido de una colección u obtendrá elementos en orden inverso. El for-each de for funciona con cualquier colección porque el for puede iterar en cualquier objeto que implemente la interfaz Iterable. Debido a que todas las clases de las columnas implementan esta interfaz, todas pueden ser iteradas por el for. Aunque en el ejemplo anterior se usó un Iterator para recorrer en bucle la lista de la agenda en dirección hacia delante, esto sólo se hizo para el ejemplo. Cuando se usa una LinkedList (o cualquier colección que implementa la interfaz List), puede usar un ListIterator para recorrer en bucle la colección en cualquier dirección. Por ejemplo, la parte del programa anterior que despliega la lista hacia delante puede reescribirse como se muestra aquí para que use ListIterator. // Usa un ListIterador para mostrar la lista en dirección // hacia delante. ListIterator<EntradaTelefono> itr = agenda.listIterator( ); EntradaTelefono et; System.out.println("Itera en la lista en " + "direcci\u00a2n hacia delante:"); while(itr.hasNext( )) { et = itr.next( ); System.out.println(et.nombre + ": " + et.numero); } Este código es funcionalmente equivalente a la misma secuencia del ejemplo. La única diferencia es que se usa un ListIterator en lugar de un Iterator. ListIterator también especifica el método set( ), que puede usarse para cambiar el valor de un elemento obtenido al llamar a next( ) o previous( ). Aquí se muestra: void set(E obj) Aquí, obj reemplaza el último elemento iterado. El método set( ) permite actualizar el valor de una lista mientras se está iterando. www.fullengineeringbook.net 214 Java: Soluciones de programación Cree una cola o una pila empleando Deque Componentes clave Clases e interfaces Métodos java.util.Collection boolean isEmpty( ) java.util.Deque<E> boolean add(E obj) E pop( ) void push(E obj) E remove( ) java.util.ArrayDeque<E> A partir de Java 6, la estructura de colecciones ha proporcionado la interfaz Deque, que define las características de una cola de doble extremo. Hereda la interfaz Queue, que especifica los métodos para colas de un solo extremo (que son las colas en que los elementos se agregan o eliminan en un solo extremo). Deque agrega métodos a Queue que permiten que se añadan o eliminen elementos en cualquier extremo. Esto permite que implementaciones de Deque se usen como colas tipo primero en entrar primero en salir (FIFO, First-In First-Out) o pilas tipo último en entrar primero en salir (LIFO, Last-In First-Out). En esta solución se muestra este proceso. Deque se implementa con LinkedList y ArrayDeque. En esta solución se usa ArrayDeque para demostrar la creación de colas y pilas, pero el procedimiento general se aplica a cualquier implementación de Deque. NOTA Aunque Java aún proporciona java.util.Stack, es una clase heredada que se ha vuelto obsoleta con las implementaciones de Deque. Debe evitarse su uso en nuevo código. En cambio, debe usarse ArrayDeque o LinkedList cuando se necesite una pila. Paso a paso He aquí una manera de implementar una cola tipo primero en entrar primero en salir basada en ArrayDeque: 1. Cree una ArrayDeque, especificando el tipo de objetos que se almacenará. 2. Agregue objetos al final de la cola al llamar a add( ). 3. Elimine objetos de la cabeza de la cola, al llamar a remove( ). 4. Puede determinar si la cola está vacía al llamar a isEmpty( ). Esto resulta útil cuando se eliminan objetos de la cola. He aquí una manera de implementar una pila tipo último en entrar primero en salir basada en ArrayDeque: 1. Cree una ArrayDeque, especificando el tipo de objetos que se almacenará. 2. Agregue objetos a la parte superior de la pila al llamar a push( ). www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 215 3. Elimine objetos de la parte superior de la pila, al llamar a pop( ). 4. Puede determinar si la pila está vacía al llamar a isEmpty( ). Esto resulta útil cuando se eliminan objetos de la pila. Análisis ArrayDeque es una implementación concreta de Deque que da soporte a una matriz dinámica. Por tanto, crece a medida que se agregan elementos. Da soporte a tres constructores. Aquí se muestra el usado en las soluciones: ArrayDeque( ) Crea una ArrayDeque con una capacidad inicial de 16. Por supuesto, esta colección crecerá a medida que se agreguen elementos. En Revisión general de las colecciones, que se presenta casi al principio del capítulo, se encuentra una descripción de otros constructores de ArrayDeque. Con el uso de Deque, hay varias maneras en que se puede implementar una cola. El método usado en la solución depende de add( ) y remove( ). Aquí se muestran: boolean add(E obj) E remove( ) El método add( ) agrega obj al final de la cola. Devuelve verdadero si se ha agregado el elemento y falso si no puede agregarse, por alguna razón. Lanzará una IllegalStateException si se hace un intento de agregar un elemento a una cola llena, de capacidad restringida. (ArrayDeque crea una matriz dinámica que no tiene capacidad restringida). El método remove( ) devuelve el elemento que se encuentra a la cabeza de la cola. También elimina ese elemento. Lanza NoSuchElementException si la cola está vacía. También hay varias maneras de implementar una pila empleando Deque. El método usado por esta solución emplea push( ) y pop( ). Aquí se muestran: void push(E obj) E pop( ) El método push( ) agrega un elemento a la parte superior de la pila. Lanzará una IllegalStateException si se hace un intento de agregar un elemento a una pila llena, de capacidad restringida. El método pop( ) elimina y devuelve el elemento que se encuentra en la parte superior de la pila. Lanzará una NoSuchElementException si la pila está vacía. Una manera fácil de evitar que se genere una NoSuchElementException cuando se eliminan objetos de una pila o una cola es llamar al método isEmpty( ). Este método está especificado por Collection y, por tanto, está disponible para todas las colecciones. NOTA Las implementaciones de capacidad restringida de Deque están permitidas. En una Deque de capacidad restringida, los métodos add( ) y push( ) lanzarán una IllegalStateException si la colección está llena. Ninguna de las dos implementaciones de Deque proporcionadas por java.util, ArrayDeque o LinkedList, tienen capacidad restringida. www.fullengineeringbook.net 216 Java: Soluciones de programación Ejemplo En el siguiente ejemplo se muestra la manera de implementar una pila y una cola al usar Deque. Usa la colección ArrayDeque, pero el programa también funcionará si la sustituye con una LinkedList, porque ambas implementan la interfaz Deque. // Crea una pila y una cola usando ArrayDeque. import java.util.*; class DemoPiCo { public static void main(String args[ ]) { // Crea dos ArrayDeques. Una para la pila // y otra para la cola. ArrayDeque<String> pila = new ArrayDeque<String>( ); ArrayDeque<String> cola = new ArrayDeque<String>( ); // Demuestra la pila. System.out.println("Empujando pila.push("A"); System.out.println("Empujando pila.push("B"); System.out.println("Empujando pila.push("C"); System.out.println("Empujando pila.push("D"); A"); B"); C"); D"); System.out.print("Sacando de la pila: "); while(!pila.isEmpty( )) System.out.print(pila.pop( ) + " "); System.out.println("\n"); // Demuestra la cola. System.out.println("Agregando cola.add("A"); System.out.println("Agregando cola.add("B"); System.out.println("Agregando cola.add("C"); System.out.println("Agregando cola.add("D"); A"); B"); C"); D"); System.out.print("Consumiendo la cola: "); while(!cola.isEmpty( )) System.out.print(cola.remove( ) + " "); System.out.println( ); } } www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 217 Aquí se muestra la salida: Empujando A Empujando B Empujando C Empujando D Sacando de la pila: D C B A Agregando A Agregando B Agregando C Agregando D Consumiendo la cola: A B C D Opciones Como se mencionó, hay muchas maneras de implementar una pila o una cola empleando Deque. Una opción utiliza un segundo conjunto de métodos que agregan o eliminan elementos de una cola o pila. A diferencia de add( ) y push( ), que lanzarán una excepción cuando no pueda agregarse un elemento a una Deque de capacidad restringida, o remove( ) y pop( ), que lanzarán una excepción cuando se haga un intento de obtener un elemento de una Deque vacía, los métodos alternos no lo hacen. En cambio, devuelven valores que indican su éxito o falla. Aquí se muestran los métodos alternos para el comportamiento parecido al de una cola. boolean offerLast(E obj) Agrega obj al final de la cola. Devuelve verdadero si se tiene éxito y falso de otra manera. E poll( ) Devuelve el siguiente elemento de la cola o null si la cola está vacía. Los métodos alternos para un comportamiento parecido al de una pila son: boolean offerFirst(E obj) Agrega obj a la cabeza de la pila. Devuelve verdadero si se tiene éxito y falso de otra manera. E poll( ) Devuelve el siguiente elemento de la cola o null si la cola está vacía. Tome en cuenta que poll( ) puede usarse con pilas y colas porque ambas eliminan el siguiente elemento de la cabeza de la cola. La diferencia entre una pila y una cola es el lugar en que se agrega el elemento. Por ejemplo, he aquí otra manera de escribir la parte del ejemplo que pone elementos en la pila y luego los elimina. System.out.println("Empujando A"); pila.offerFirst("A"); System.out.println("Empujando B"); pila.offerFirst("B"); www.fullengineeringbook.net 218 Java: Soluciones de programación System.out.println("Empujando C"); pila.offerFirst("C"); System.out.println("Empujando D"); pila.offerFirst("D"); System.out.print("Sacando de la pila: "); String tmp; while((tmp = pila.poll( )) != null) System.out.print(tmp + " "); Observe el bucle que extrae elementos de la pila. Debido a que poll( ) devuelve null cuando no hay más elementos, su valor devuelto puede usarse para controlar el bucle while. (Personalmente, prefiero usar push( ) y pop( ) porque son nombres tradicionales para operaciones de pila y usan isEmpty( ) para evitar que la pila se quede vacía. Por supuesto, algunas situaciones obtendrán beneficios de poll( ), porque no lanza una excepción cuando la pila está vacía). Otra manera de evitar la generación de una NoSuchElementException cuando se llama a remove( ) o pop( ) consiste en usar peek( ). Aquí se muestra: E peek( ) Este método devuelve pero no elimina el elemento que se encuentra en la cabeza de la cola, que es el siguiente elemento que devolverá remove( ) o pop( ). (Recuerde que la diferencia entre una pila y una cola es el lugar en que se agrega el elemento, no en que se elimina). El método peek( ) devolverá null si la colección está vacía. Por tanto, puede usar peek( ) para determinar si la cola o la pila está vacía. Invierta, gire y ordene al azar una List Componentes clave Clases e interfaces Métodos java.util.Collections static void reverse(List<?> col) static void rotate(List<?> col, int n) static void shuffle(List<?> col) En esta solución se demuestra el uso de tres algoritmos relacionados, definidos por la clase Collections: reverse( ), rotate( ) y shuffle( ). Se relacionan entre sí porque cada uno cambia el orden de la colección a la que se aplica. El algoritmo reverse( ) invierte una lista, rotate( ) gira una lista (es decir, quita un elemento de un extremo de una lista y lo pone en el otro) y shuffle( ) dispone los elementos en un orden al azar. Sólo pueden aplicarse a colecciones que implementan la interfaz List. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 219 Paso a paso Para invertir, girar o desordenar una colección, siga estos pasos: 1. La colección que habrá de afectarse debe implementar la interfaz List. 2. Después de que se han almacenado los objetos en la lista, llame a Collections.reverse( ) para invertir la colección, Collections.rotate( ) para girar la colección, o Collections.shuffle( ) para ordenar al azar la colección. Análisis Para invertir una lista, use el algoritmo reverse( ). Aquí se muestra: static void reverse(List<?> col) Revierte la colección pasada a col. Para girar una lista, use el algoritmo rotate( ). Aquí se muestra: static void rotate(List<?> col, int n) Gira la colección pasada a col, n lugares a la derecha. Para girar a la izquierda, use un valor negativo para n. Para ordenar al azar los elementos de una lista, use shuffle( ). Tiene dos formas. La usada aquí es static void shuffle(List<?> col) Ordena al azar la colección pasada a col. Ejemplo En el siguiente ejemplo se muestran los efectos de reverse( ), rotate( ) y shuffle( ). // Usa reverse( ), rotate( ) y shuffle( ). import java.util.*; class DemoIGO { public static void main(String args[ ]) { LinkedList<Character> ll = new LinkedList<Character>( ); // Agrega de la A a la F a la lista. for(char n=’A’; n <= ‘F’; n++) ll.add(n); // Despliega la lista antes de ordenarla al azar. System.out.println("La lista original: "); for(Character x : ll) System.out.print(x + " "); System.out.println("\n"); www.fullengineeringbook.net 220 Java: Soluciones de programación // Invierte la lista. Collections.reverse(ll); // Despliega la lista invertida. System.out.println("La lista invertida: "); for(Character x : ll) System.out.print(x + " "); System.out.println("\n"); // Gira la lista. Collections.rotate(ll, 2); // Despliega la lista girada. System.out.println("La lista tras girarla 2 " + "lugares a la derecha: "); for(Character x : ll) System.out.print(x + " "); System.out.println("\n"); // Ordena al azar la lista. Collections.shuffle(ll); // Despliega la lista ordenada al azar. System.out.println("La lista ordenada al azar:"); for(Character x : ll) System.out.print(x + " "); System.out.println("\n"); } } Aquí se muestra la salida: La lista original: A B C D E F La lista invertida: F E D C B A La lista tras girarla 2 lugares a la derecha: B A F E D C La lista ordenada al azar: F B A E D C Opciones Hay una segunda forma de shuffle( ) que le permite especificar un generador de números aleatorios. Aquí se muestra esta versión: static void shuffle(List<?> col, Random genAlea) Aquí, genAlea es el generador de números aleatorios que se usará para ordenar la lista. Debe ser una instancia de Random, que está definida en java.lang. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 221 Lea bytes de un archivo Componentes clave Clases Métodos java.util.Collections static<T> int binarySearch(List<? extends Comparable<? super T>> col, T val) static<T extends Comparable<? super T>> void sort(List<T> col) java.util.List<E> E get(int ind) Casi ninguna de las colecciones proporcionadas por java.util almacena elementos en orden. La excepción es TreeSet, que mantiene un árbol ordenado. Sin embargo, al usar el algoritmo sort( ) proporcionado por Collections, puede ordenar cualquier colección que implemente la interfaz List. Una vez que haya ordenado la colección, puede realizar búsquedas muy rápidas en ella al llamar a binarySearch( ), que es otro algoritmo definido por Collections. En esta solución se muestra el procedimiento básico. Paso a paso Para ordenar una colección, siga estos pasos: 1. La colección que se está ordenando debe implementar la interfaz List. Además, el tipo de objeto almacenado en la colección debe implementar la interfaz Comparable. 2. Después de que se han almacenado todos los objetos de la lista, llame a Collections.sort( ) para ordenar la colección. Para realizar una búsqueda rápida de una colección ordenada, siga estos pasos: 1. La colección en que se está buscando debe implementar la interfaz List y ordenarse como se describió. 2. Llame a Collections.binarySearch( ) para encontrar un elemento. 3. Utilizando el índice devuelto por binarySearch( ), puede obtener el elemento al llamar a get( ), que se especifica por List. Análisis Para ordenar una lista, use el algoritmo sort( ) definido por Collections. Tiene dos versiones. Aquí se muestra la usada por la solución. static<T extends Comparable<? super T>> void sort(List<T> col) Ordena la colección pasada a col. La colección debe implementar la interfaz List y sus elementos deben implementar la interfaz Comparable. Cuando este método regresa, col está ordenado. Resulta importante comprender que la colección sólo permanece ordenada hasta que se modifica de www.fullengineeringbook.net 222 Java: Soluciones de programación alguna manera que afecte el orden. Por ejemplo, si se agrega un elemento al final de una colección ordenada, por lo general la colección se desordenará (a menos que el elemento sea en realidad el último elemento ordenado). Por tanto, ordenar una colección no significa que permanecerá ordenada. Una vez que se ha ordenado una lista, puede buscarse en ella al llamar a binarySearch( ), también definido por Collections. Tiene dos versiones. Aquí se muestra la usada en esta solución: static<T> int binarySearch(List<? extends Comparable<? super T>> col, T val) La lista que se buscará se pasa mediante col. El objeto que se buscará se pasa mediante val. El método devuelve el índice en que se encuentra la primera aparición del elemento, o un valor negativo si val no está en la lista. (El valor absoluto de un valor devuelto negativo es el índice en que debe insertarse el elemento en la lista para mantener ésta ordenada). Puede obtener el objeto en el índice devuelto por binarySearch( ) al llamar a get( ). Aquí se muestra: E get(int ind) Devuelve una referencia al elemento especificado. Ejemplo En el siguiente ejemplo se muestra cómo ordenar una lista y luego buscar en la lista ordenada. // Ordena y busca en una LinkedList. import java.util.*; class DemoOrdenyBuscar { public static void main(String args[ ]) { LinkedList<Character> ll = new LinkedList<Character>( ); // Agrega de la A a la Z a la lista. for(char n=’A’; n <= ‘Z’; n++) ll.add(n); // Ordena la lista al azar. Collections.shuffle(ll); // Despliega la lista ordenada al azar. System.out.println("La lista desordenada:"); for(Character x : ll) System.out.print(x + " "); System.out.println("\n"); // Ordena la lista. Collections.sort(ll); www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 223 // Despliega la lista ordenada. System.out.println("La lista ordenada:"); for(Character x : ll) System.out.print(x + " "); System.out.println("\n"); // Busca un elemento. System.out.println("Buscando F."); int i = Collections.binarySearch(ll, ‘F’); if(i >= 0) { System.out.println("Se encontr\u00a2 en el \u00a1ndice " + i); System.out.println("El objeto es " + ll.get(i)); } } } Aquí se muestra la salida: La lista desordenada: O B C Z X U J L V A D M G N H R W S Y K E F I Q T P La lista ordenada: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Buscando F. Se encontró en el índice 5 El objeto es F Opciones Como regla general, si su aplicación requiere una colección ordenada, la clase TreeSet es una mejor opción que ordenar una lista. A medida que se agregan elementos a un TreeSet, se insertan en orden automáticamente en el árbol. Por tanto, un TreeSet siempre contiene una colección ordenada. En contraste, después de que se ordena una lista, sólo permanecerá ordenada hasta que se agregue el siguiente elemento fuera del orden. Sin embargo, un TreeSet no permite elementos duplicados. Por tanto, en el caso de aplicaciones que requieren una colección ordenada que contiene duplicados, el uso de Collections.sort( ) es su mejor opción. Puede especificar un comparador cuando llama a sort( ) o binarySearch( ), que determinan la manera en que se comparan los elementos de la lista. Esto le permite ordenar o buscar de una manera diferente del orden natural. También le permite ordenar o buscar en listas que almacenan objetos que no implementan Comparable. Aquí se muestran estas versiones: static<T> int binarySearch(List<? extends T>> col, T val, Comparator<? super T> comp) static<T void sort(List<T> col, Comparator<? super T> comp) Para conocer una descripción del uso de un comparador, consulte Use un Comparator con una colección. www.fullengineeringbook.net 224 Java: Soluciones de programación Cree una colección comprobada Componentes clave Clases Métodos java.util.Collections static <E> Collection<E> checkedCollection(Collection<E> col, Class<E> t) Toda la estructura de colecciones se convirtió en elementos genéricos con el lanzamiento de Java 5. Por tanto, proporciona un medio de tipo seguro para almacenar y recuperar objetos. Sin embargo, es posible omitir esta seguridad de tipo de varias maneras. Por ejemplo, el código heredado que usa una colección declarada como de tipo simple (el que no especifica un argumento de tipo) puede agregar cualquier tipo de objeto a la colección. Si su código moderno y genérico debe hacer interfaz con ese tipo de código heredado, entonces es posible que un objeto no válido esté contenido en la colección porque el código heredado no lo evitará. Por desgracia, con el tiempo (tal vez mucho después de que se ha agregado el elemento no válido) su código operará en ese elemento no válido y esto causará una ClassCastException. Para evitar esta posibilidad, la clase Collections ofrece varios métodos que le permiten crear colecciones comprobadas. Estas colecciones crean lo que la documentación de la API de Java denomina "vista de tipo dinámicamente seguro" de una colección. Esta vista es una referencia a la colección que vigila la compatibilidad del tipo de las inserciones en tiempo de ejecución. Un intento por insertar un elemento no compatible causará una ClassCastException. Por tanto, el error ocurrirá de inmediato, en el momento de la inserción, y no después en el programa. Esto significa que se puede suponer en el resto de su programa que la colección no está corrompida. Para crear una colección comprobada, usará uno de los métodos checked…, proporcionados por Collections. En esta solución se usa checkedCollection( ), pero el mismo procedimiento básico se aplica a otros métodos de checked… Paso a paso Para crear una colección comprobada, siga estos pasos: 1. Cree la colección en que quiera vigilar la inserción de elementos no válidos. 2. Cree una vista de tipo seguro de la colección al llamar a checkedCollection( ). 3. Realice todas las operaciones que agregan un elemento a la colección mediante la referencia de tipo seguro. Análisis La clase Collections define cuatro métodos que devuelven vistas comprobadas de una colección. La usada en esta solución es checkedCollection( ), que se muestra a continuación: static <E> Collection<E> checkedCollection(Collection<E> col, Class<E> t) www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 225 Para crear una colección comprobada, pase una referencia a la colección a col y especifique el tipo de elementos en t. Por ejemplo, si integer es el tipo de elementos almacenado en la colección, entonces pase a t la literal de clase Integer.class. El método devuelve una vista de tipo seguro en tiempo de ejecución de col. Cualquier intento por insertar un elemento incompatible en la colección comprobada causará una ClassCastException. Ejemplo Con el siguiente ejemplo se demuestra cómo se evita con una colección comprobada que se agregue un elemento no válido a una colección. // // // // // Crea y demuestra una colección comprobada. Nota: en este ejemplo se mezclan intencionalmente tipos genéricos y simples para demostrar el valor de las colecciones comprobadas. Normalmente, debe evitar el uso de tipos simples. import java.util.*; class DemoComprobadas { public static void main(String args[ ]) { // Crea una LinkedList para cadenas. LinkedList<String> cadLl = new LinkedList<String>( ); // Crea una referencia comprobada a List y le asigna cadLl. Collection<String> colComp = Collections.checkedCollection(cadLl, String.class); // // // // // Para ver la diferencia que hace una lista comprobada, convierta en comentario la línea anterior y quite la marca de comentario a la línea siguiente. Asigna cadLl a colComp sin envolverla primero en una colección comprobada. Collection<String> colComp = cadLl; // Agregue algunas cadenas a la lista. colComp.add("Alfa"); colComp.add("Beta"); colComp.add("Gama"); System.out.println("He aqu\u00a1 la lista:"); for(String x : colComp) System.out.print(x + " "); System.out.println("\n"); // Para demostrar la lista comprobada, primero // se crea una referencia simple a List. Collection colSimp; // Luego, se asigna colComp, que es una referencia a List<string>, // a la referencia simple. colSimp = colComp; www.fullengineeringbook.net 226 Java: Soluciones de programación // Ahora, agrega un objeto de Integer a la lista. // Esto se compila porque una List simple puede contener // cualquier tipo de objeto (se omite la seguridad de tipo). // Sin embargo, en tiempo de ejecución se genera una // ClassCastException porque la lista está envuelta en // una lista comprobada. colSimp.add(new Integer(23)); // Despliega la lista. // Si colComp NO está envuelta en la lista comprobada, entonces // no se encontrará la falta de concordancia en el tipo hasta // que se ejecute este bucle. for(String x : colComp) System.out.print(x + " "); System.out.println("\n"); } } Aquí se muestra la salida del programa: He aquí la lista: Alfa Beta Gama Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String at java.util.Collections$CheckedCollection.typeCheck(Collections. java:2202) at java.util.Collections$CheckedCollection.add(Collections.java:2243) at DemoComprobadas.main(DemoComprobadas.java:50) Observe que la excepción se encuentra cuando se hace el intento de insertar un objeto Integer en la lista, que está esperando objetos de tipo String. He aquí cómo funciona el programa. En primer lugar, se crea una LinkedList llamada cadLl que contiene objetos de String. Luego, el programa crea una colección comprobada, llamada colComp, agrega unas cuantas cadenas a la colección y las despliega. A continuación, se crea una referencia simple a Collection (es decir, sin tipo), llamada colSimp, y luego se le asigna colComp. Por tanto, después de este paso, colSimp hace referencia a la colección comprobada colComp. Este paso, en sí mismo, es completamente legal. A continuación, se hace un intento por agregar un Integer a colSimp. Por lo general esto también sería legal (aunque muy sospechoso) porque cualquier colección sin tipo (es decir, simple) puede contener cualquier tipo de objeto. Sin embargo, como colSimp hace referencia a una colección comprobada, esto causa que se lance una ClassCastException, evitando así que colComp contenga un elemento no válido. Si no se hubiera usado una colección comprobada, entonces el elemento no válido no habría causado una excepción hasta que se hiciera el intento de desplegar la lista después de que se ha insertado Integer. Para confirmar esto, convierta esta línea en un comentario: Collection<String> colComp = Collections.checkedCollection(cadLl, String.class); A continuación, elimine la marca de comentario de la línea siguiente: //Collection<String> colComp = cadLl; www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 227 Luego, vuelva a compilar y ejecutar el programa. La excepción ocurre cuando el último bucle for encuentra el elemento entero, que es después de que se agregó a la lista. Opciones Además de checkedCollection( ), Collections proporciona otros varios métodos que devuelven vistas comprobadas, adecuadas para tipos específicos de colecciones. Aquí se muestran: static <E> List<E> checkedList(List<E> col, Class<E> t) Devuelve una vista de tipo seguro en tiempo de ejecución de List. static <E> List<E> checkedSet(Set<E> col, Class<E> t) Devuelve una vista de tipo seguro en tiempo de ejecución de Set. static <E> SortedSet<E> checkedSortedSet(SortedSet<E> col, Class<E> t) Devuelve una vista de tipo seguro en tiempo de ejecución de SortedSet. Para cada método, un intento por insertar un elemento incompatible causará una ClassCastException. Debe usar uno de estos métodos cuando necesite llamar a métodos que son específicos de List, Set o SortedSet. Collections también define los siguientes métodos que le permiten obtener vistas de tipo seguro de un mapa. static <C, V> Map<C, V> checkedMap(Map<C, V> c, Class<C> tipoC, Class<V> tipoV) Devuelve una vista de tipo seguro en tiempo de ejecución de Map static <C, V> SortedMap<C, V> checkedSortedMap(SortedMap<C, V> c, Class<C> tipoC, Class<V> tipoV) Devuelve una vista de tipo seguro en tiempo de ejecución de SortedMap Para ambos métodos, un intento de insertar una entrada incompatible causará una ClassCastException. Cree una colección sincronizada Componentes clave Clases Métodos java.util.Collections static<T> Collection<T> synchronizedCollection(Collection<T> col) www.fullengineeringbook.net 228 Java: Soluciones de programación Las clases de la colección proporcionadas por la estructura de colecciones no están sincronizadas. Si necesita una colección de subproceso seguro, entonces necesitará usar los métodos de synchronized…, definidos por Collections. Una colección sincronizada es necesaria en los casos en que un subproceso estará (o por lo menos podría estar) modificando una colección cuando otro subproceso también tiene acceso a ella. En esta solución se muestra cómo crear una vista de colección sincronizada empleando synchronizedCollection( ), pero el mismo procedimiento se aplica a otros métodos de synchronized… Paso a paso Para crear una vista sincronizada de una colección se requieren estos pasos: 1. Cree la colección que habrá de sincronizarse. 2. Obtenga una vista sincronizada de la colección al llamar al método synchronizedCollection( ). 3. Opere en la colección mediante la vista sincronizada. Análisis Si varios subprocesos estarán usando una colección, entonces es necesario que cada subproceso opere en una vista sincronizada de esa colección. Si varios subprocesos tratan de usar una colección no sincronizada, entonces es posible que se lance una ConcurrentModificationException (además de otros errores). Esto puede suceder aunque sólo un subproceso lea, pero no modifique, la colección. Por ejemplo, si la colección está en el proceso de ser iterada por un subproceso y un segundo subproceso cambia la colección, entonces ocurrirá una ConcurrentModificationException. Para evitar este y otros errores asociados con colecciones no sincronizadas, debe obtener y usar exclusivamente una vista sincronizada de la colección. Para obtener una vista sincronizada (es decir, de subproceso seguro) de una colección, use uno de los cuatro métodos synchronized… definidos por la clase Collections. La versión usada por esta solución es synchronizedCollection( ). Aquí se muestra: static<T> Collection<T> synchronizedCollection(Collection<T> col) Devuelve una vista de subproceso seguro de la colección pasada a col. Esta vista puede usarse de manera segura en un entorno de multiprocesamiento. Sin embargo, todo el acceso (incluido el de sólo lectura) a la colección debe tener lugar a través de la referencia devuelta. Ejemplo En el siguiente ejemplo se demuestra el uso de la colección sincronizada. // Crea y demuestra una colección sincronizada. import java.util.*; // Crea un segundo subproceso de ejecución que agrega // un elemento a la colección que se pasa. // Luego itera en la colección. class MiSubproceso implements Runnable { Thread t; www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones Collection<String> col; MiSubproceso(Collection<String> c) { col = c; t = new Thread(this, "Segundo subproceso"); t.start( ); } public void run( ) { try { Thread.sleep(100); // deja que se ejecute el subproceso principal col.add("Omega"); // y otro elemento // Itera en los elementos. synchronized(col) { for(String cad : col) { System.out.println("Segundo subproceso: " + cad); // Deja que se ejecute el subproceso principal, si se puede. Thread.sleep(500); } } } catch(InterruptedException exc) { System.out.println("Segundo subproceso interrumpido."); } } } // Demuestra una colección sincronizada. class DemoSinc { public static void main(String args[ ]) { // Crea un TreeSet para cadenas. TreeSet<String> cadTs = new TreeSet<String>( ); // Crea una referencia sincronizada y la asigna a colSinc. Collection<String> colSinc = Collections.synchronizedCollection(cadTs); // // // // // Para ver la diferencia que marca una colección sincronizada, convierta en comentario la línea anterior y quite la marca de comentario de la línea siguiente. Asigna cadTs a colSinc sin envolverla primero en una colección sincronizada. Collection<String> colSinc = cadTs; // Agrega algunas cadenas al conjunto. colSinc.add("Gama"); colSinc.add("Beta"); colSinc.add("Alfa"); www.fullengineeringbook.net 229 230 Java: Soluciones de programación // Inicia el segundo subproceso. new MiSubproceso(colSinc); try { synchronized(colSinc) { for(String cad : colSinc) { System.out.println("Subproceso principal: " + cad); // Deja que se ejecute el segundo subproceso, si se puede. Thread.sleep(500); } } } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } } } La salida producida por este programa se muestra a continuación: Subproceso principal: Alfa Subproceso principal: Beta Subproceso principal: Gama Segundo subproceso: Alfa Segundo subproceso: Beta Segundo subproceso: Gama Segundo subproceso: Omega He aquí cómo funciona el programa. DemoSinc empieza por crear un TreeSet llamado cadTs. Este conjunto se envuelve después en una colección sincronizada llamada colSinc y se agregan tres elementos a ella. A continuación, main( ) empieza un segundo subproceso de ejecución, que es definido por MiSubproceso. Se pasa a este constructor una referencia a colSinc. Este subproceso empieza por quedar inactivo por un periodo corto, lo que permite que el subproceso principal reanude la ejecución, Luego MiSubproceso agrega otro elemento a la colección y empieza a iterar en ésta. Después de crear MiSubproceso, el subproceso principal empieza a iterar en el conjunto, demorando 500 milisegundos entre iteraciones. Esta demora permite que se ejecute el segundo subproceso, si se puede. Sin embargo, debido a que colSinc está sincronizada, el segundo subproceso no se puede ejecutar hasta que termine el primero. Por tanto, la iteración de los elementos en main( ) termina antes de que MiSubproceso agregue otro elemento al conjunto, evitando así una ConcurrentModificationException. Para probar la necesidad de usar una colección sincronizada cuando se usan varios subprocesos, haga este experimento. En el ejemplo, convierta en comentario estas líneas: Collection<String> colSinc = Collections.synchronizedCollection(cadTs); Luego, elimine la marca de comentario de esta línea: // Collection<String> colSinc = cadTs; Ahora, vuelva a compilar y ejecutar el programa. Debido a que colSinc ya no está sincronizada, tanto el subproceso principal como MiSubproceso pueden acceder a él simultáneamente. Esto da como resultado una ConcurrentModificationException cuando MiSubproceso trata de agregar un elemento. www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 231 Opciones Además de synchronizedCollection( ), Collections proporciona otros varios métodos que devuelven vistas sincronizadas para tipos específicos de colecciones. Aquí se muestran: static <T> List<T> synchronizedList(List<T> col) Devuelve una lista de subproceso seguro respaldada por col. static <T> Set<T> synchronizedSet(Set<T> col) Devuelve un conjunto de subproceso seguro respaldado por col. static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> col) Devuelve un conjunto ordenado de subproceso seguro respaldado por col. Collections también proporciona los siguientes métodos que devuelven vistas sincronizadas de los mapas: static <C, V> Map<C, V> synchronizedMap(Map<C, V> mapa) Devuelve un mapa de subproceso seguro respaldado por mapa. static <C, V> SortedMap<C, V> synchronizedSortedMap( SortedMap<C, V> mapa) Devuelve un mapa ordenado de subproceso seguro respaldado por mapa. Cree una colección inmutable Componentes clave Clases Métodos java.util.Collections static <T> Collection<T> unmodifiableCollection( Collection<? extends T> col) Collections proporciona un conjunto de métodos que crean una vista inmutable de una colección. Estos métodos empiezan con el nombre unmodifiable. La colección no puede cambiarse a través de este tipo de vista. Esto resulta útil en los casos en que quiera asegurarse de que una colección no se modificará, como cuando se pasa a código de terceros. Paso a paso Para crear una vista inmutable de una colección se requieren los pasos siguientes: 1. Cree una colección que será de sólo lectura. 2. Obtenga una vista inmutable de la colección al llamar al método unmodifiableCollection( ). 3. Opere en la colección a través de la vista de sólo lectura. www.fullengineeringbook.net 232 Java: Soluciones de programación Análisis Para crear una vista inmutable de una colección, use uno de los métodos unmodifiable…, definidos por la clase Collections. Define cuatro métodos que devuelven vistas inmutables de una colección. La usada en esta solución es unmodifiableCollection( ). Aquí se muestra: static <T> Collection<T> unmodifiableCollection(Collection<? extends T> col) Devuelve una referencia a una vista de sólo lectura de la colección pasada a col. Esta referencia puede entonces pasarse a cualquier código que no tenga permitido modificar la colección. Cualquier intento de modificar la colección mediante esta referencia dará como resultado una UnsupportedOperationException. Ejemplo En el siguiente ejemplo se demuestra el uso de una colección inmutable. // Crea y demuestra una colección inmutable. import java.util.*; class DemoNoMod { public static void main(String args[ ]) { // Crea una ArrayList para cadenas. ArrayList<Character> lista = new ArrayList<Character>( ); // Agrega un elemento. lista.add(‘X’); System.out.println("Elemento agregado a la lista: " + lista.get(0)); // Ahora, crea una vista inmutable de la lista. Collection<Character> colInmutable = Collections.unmodifiableCollection(lista); // Trata de agregar otro elemento. // Esto no funcionará y causará una excepción. colInmutable.add(‘Y’); } } Aquí se muestra la salida. Tome nota de que se lanza una UnsupportedOperationException cuando se hace un intento de modificar la colección. Elemento agregado a la lista: X Exception in thread "main" java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableCollection.add(Collections.java:1018) at DemoNoMod.main(DemoNoMod.java:22) www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 233 Opciones Además de unmodifiableCollection( ), Collections proporciona otros varios métodos que devuelven vistas inmutables adecuadas a tipos específicos de colecciones. Aquí se muestran: static <T> List<T> unmodifiableList(List<? extends T> col) Devuelve una lista no modificable respaldada por col. static <T> Set<T> unmodifiableSet(Set<? extends T> col) Devuelve un conjunto no modificable respaldado por col. static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<T> col) Devuelve un conjunto ordenado no modificable respaldado por col. Collections también proporciona los siguientes métodos que devuelven vistas inmutables de mapas: static <C, V> Map<C, V> unmodifiableMap( Map<? extends C, ? extends V> mapa) Devuelve un mapa no modificable respaldado por mapa. static <C, V> SortedMap<C, V> unmodifiableSortedMap( SortedMap<C,? extends V> mapa) Devuelve un mapa ordenado no modificable respaldado por mapa. Lea bytes de un archivo Componentes clave Clases e interfaces Métodos java.util.Map void clear( ) boolean containsKey(Object c) boolean containsValue(Object v) Set<Map.Entry<C, V>> entrySet( ) V get(Object c) boolean isEmpty( ) Set<C> KeySet( ) V put(C c, V v) void putAll(Map<? extends C, ? extends V> m) V remove(Object c) Int size( ) Collection<V> values( ) java.util.TreeMap www.fullengineeringbook.net 234 Java: Soluciones de programación Como se explicó en la revisión general, los mapas no son, técnicamente hablando, colecciones, porque no implementan la interfaz Collection. Sin embargo, los mapas son parte de la estructura de colecciones. Los mapas almacenan pares clave/valor, y todas las claves deben ser únicas. Todos los mapas implementan la interfaz Map. Por tanto, todos los mapas comparten una funcionalidad común. Por ejemplo, todos los mapas le permiten agregar un par clave/valor a un mapa u obtener un valor, dada su clave. En esta solución se demuestra esta funcionalidad común al mostrar cómo • Agregar un par clave/valor a un mapa. • Obtener un valor dado a una clave. • Determinar el tamaño del mapa. • Obtener un conjunto de entrada de los elementos del mapa. • Recorrer en bucle las entradas de un mapa empleando un conjunto de entrada. • Obtener una colección de las claves y los valores en un mapa. • Eliminar un par clave/valor de un mapa. • Cambiar el valor asociado con una clave. • Determinar si un mapa está vacío. En esta solución se usa TreeMap, que es una implementación concreta de Map, pero sólo se usan los métodos definidos por Map. Por tanto, se pueden aplicar los mismos principios generales a cualquier mapa. Paso a paso Para crear y usar un mapa se requieren estos pasos: 1. Cree una instancia del mapa deseado. En esta solución, se usa TreeMap, pero podría seleccionar cualquier otro mapa. 2. Realice varias operaciones en el mapa, al emplear los métodos definidos por Map como se describe en los pasos siguientes. 3. Agregue entradas al mapa al llamar a put( ) o putAll( ). 4. Obtenga el número de entradas en un mapa al llamar a size( ). 5. Determine si un mapa contiene una clave específica al llamar a containsKey( ). Determine si un mapa contiene un valor específico al llamar a containsValue( ). 6. Determine si un mapa está vacío (es decir, no contiene entradas) al llamar a isEmpty( ). 7. Obtenga un conjunto de entradas en el mapa al llamar a entrySet( ). 8. Use el conjunto de entradas obtenido de entrySet( ) para recorrer en bucle las entradas el mapa. 9. Obtenga un valor dada su clave al llamar a get( ). 10. Obtenga un conjunto de las claves del mapa al llamar a keySet( ). Obtenga un conjunto de valores al llamar a values( ). 11. Elimine entradas del mapa al llamar a remove( ). 12. Elimine todas las entradas del mapa al llamar a clear( ). www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 235 Análisis Map es una interfaz genérica que se define como se muestra a continuación: interface Map<C, V> Aquí C define el tipo de claves del mapa y V define el tipo de valores. Es permisible que C y V sean del mismo tipo. Los métodos definidos por Map se muestran en la tabla 5-8, que se encuentra en Revisión general de las colecciones, al principio de este capítulo. He aquí una breve descripción de su operación. Los objetos se agregan a un mapa al llamar a put( ). Observe que put( ) toma argumentos de tipo C y V. Esto significa que las entradas agregadas a un mapa deben ser compatibles con el tipo de datos esperado por el mapa. Puede agregar todo el contenido de un mapa a otro al llamar a putAll( ). Por supuesto, los dos mapas deben ser compatibles. Puede eliminar una entrada al usar remove( ). Para vaciar un mapa, llame a clear( ). Puede determinar si un mapa contiene una clave específica al llamar a containsKey( ). Para determinar si contiene un valor específico, llamaría a containsValue( ). Puede determinar cuando un mapa está vacío llamando a isEmpty( ). El número de elementos contenido realmente en un mapa se devuelve con size( ). Como se explicó en la revisión general, los mapas no implementan la interfaz Iterable y, por tanto, no apoyan iteradores. Sin embargo, aún puede recorrer en bucle el contenido de un mapa al obtener primero una vista de colección del mapa. Hay tres tipos de vistas de colección disponibles: • Un conjunto de entradas en el mapa • Un conjunto de claves en el mapa • Una colección de los valores del mapa Un conjunto de entradas en el mapa se obtiene al llamar a entrySet( ). Devuelve una colección Set que contiene las entradas. Cada entrada se conserva en un objeto de tipo Map.Entry. Dado un objeto de este tipo, puede obtener la clave al llamar a getKey( ). Para obtener el valor, llame a getValue( ). Empleando el conjunto de entradas, es fácil recorrer en bucle un mapa, obteniendo las entradas de una en una. Las otras dos vistas de colección le permiten tratar con las claves y los valores por separado. Para obtener un Set de las claves en el mapa, llame a keySet( ). Para obtener una Collection de los valores, llame a values( ). En el siguiente ejemplo se usa un TreeMap que contendrá el mapa. Consulte Revisión general de las colecciones presentada cerca del inicio del capítulo para conocer un análisis sobre los constructores. Ejemplo En el siguiente ejemplo se demuestran las técnicas básicas usadas para crear y usar un mapa. Se crea un mapa llamado numsAtom, que vincula los nombres de elementos con sus números atómicos. Por ejemplo, el hidrógeno tiene el número atómico 1; el oxígeno tiene el 8, etcétera. // Demuestra las técnicas básicas de Map. import java.util.*; class MapasBasicos { public static void main(String args[ ]) { www.fullengineeringbook.net 236 Java: Soluciones de programación // Crea un mapa de árbol. TreeMap<String, Integer> numsAtom = new TreeMap<String, Integer>( ); // Pone las entradas en el mapa. // Cada entrada consta del nombre de un elemento // y su número atómico. Por tanto, la clave es el // nombre del elemento y el valor es su número atómico. numsAtom.put("Hidr\u00a2geno", 1); numsAtom.put("Ox\u00a1geno", 8); numsAtom.put("Hierro", 26); numsAtom.put("Cobre", 29); numsAtom.put("Plata", 47); numsAtom.put("Oro", 79); System.out.println("El mapa contiene estas " + numsAtom.size( ) + " entradas:"); // Obtiene un conjunto de las entradas. Set<Map.Entry<String, Integer>> set = numsAtom.entrySet( ); // Despliega las claves y los valores del mapa. for(Map.Entry<String, Integer> me : set) { System.out.print(me.getKey( ) + ", n\u00a3mero at\u00a2mico: "); System.out.println(me.getValue( )); } System.out.println( ); // Y otro mapa para numsAtom. TreeMap<String, Integer> numsAtom2 = new TreeMap<String, Integer>( ); // Pone elementos en el mapa. numsAtom2.put("Zinc", 30); numsAtom2.put("Plomo", 82); // Inserta numsAtom2 en numsAtom. numsAtom.putAll(numsAtom2); // Muestra el mapa después de las adiciones. set = numsAtom.entrySet( ); // Despliega las claves y los valores del mapa. System.out.println("Ahora el mapa contiene estas " + numsAtom.size( ) + " entradas:"); for(Map.Entry<String, Integer> me : set) { System.out.print(me.getKey( ) + ", n\u00a3mero at\u00a2mico: "); System.out.println(me.getValue( )); } System.out.println( ); // Busca una clave. if(numsAtom.containsKey("Oro")) System.out.println("El oro tiene un n\u00a3mero at\u00a2mico de " + numsAtom.get("Oro")); www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 237 // Busca un valor. if(numsAtom.containsValue(82)) System.out.println("el n\u00a3mero at\u00a2mico 82 est\u00a0 en el mapa."); System.out.println( ); // Elimina una entrada. if(numsAtom.remove("Oro") != null) System.out.println("El oro se ha eliminado.\n"); else System.out.println("No se encontr\u00a2 la entrada.\n"); // Despliega el conjunto de claves después de la eliminación del oro. Set<String> claves = numsAtom.keySet( ); System.out.println("Las claves tras eliminar el oro:"); for(String cad : claves) System.out.println(cad + " "); System.out.println( ); // Despliega el valor establecido después de la eliminación del oro. Collection<Integer> vals = numsAtom.values( ); System.out.println("Los valores tras eliminar el oro:"); for(Integer n : vals) System.out.println(n + " "); System.out.println( ); // Limpia el mapa. System.out.println("Limpiando el mapa."); numsAtom.clear( ); if(numsAtom.isEmpty( )) System.out.println("Ahora el mapa est\u00a0 vac\u00a1o."); } } Aquí se muestra la salida: El mapa contiene estas 6 entradas: Cobre, número atómico: 29 Hidrógeno, número atómico: 1 Hierro, número atómico: 26 Oro, número atómico: 79 Oxígeno, número atómico: 8 Plata, número atómico: 47 Ahora el mapa contiene estas 8 entradas: Cobre, número atómico: 29 Hidrógeno, número atómico: 1 Hierro, número atómico: 26 Oro, número atómico: 79 Oxígeno, número atómico: 8 Plata, número atómico: 47 Plomo, número atómico: 82 Zinc, número atómico: 30 www.fullengineeringbook.net 238 Java: Soluciones de programación El oro tiene un número atómico de 79 el número atómico 82 está en el mapa. El oro se ha eliminado. Las claves tras eliminar el oro: Cobre Hidrógeno Hierro Oxígeno Plata Plomo Zinc Los valores tras eliminar el oro: 29 1 26 8 47 82 30 Limpiando el mapa. Ahora el mapa está vacío. Opciones Aunque TreeMap se usó para demostrar las técnicas básicas de mapas, pudo usarse cualquier clase de mapa. Por ejemplo, pruebe la sustitución de TreeMap con HashMap en el ejemplo. El programa se compilará y ejecutará apropiadamente. La razón es que, por supuesto, todos los mapas implementan la interfaz Map y sólo los métodos definidos por Map se usan en el ejemplo. Cuando se usa un mapa ordenado, como TreeMap, puede especificar un comparador personalizado de la misma manera que lo haría con una colección ordenada. Consulte Use un Comparator con una colección. Convierta una lista de Properties en un HashMap Componentes clave Clases e interfaces Métodos java.util.Properties Object setProperty(String c, String v) java.util.HashMap<C, V> Versiones anteriores de Java no incluían la estructura de colecciones. En cambio, se proporcionaba un grupo ad hoc de clases, como Vector, Properties y Hashtable. Aunque estas clases eran adecuadas, no formaban un todo coherente y se volvieron obsoletas con la estructura de colecciones. Sin embargo, estas clases heredadas aún tienen soporte en Java porque una cantidad importante de código aún depende de ellas. Para ayudar a zanjar la brecha entre las clases heredades y la estructura de colecciones, las clases heredades fueron tratadas para adecuarse a las colecciones. Por ejemplo, Vector implementa la interfaz List y Hashtable implementa Map. Esto facilita la www.fullengineeringbook.net Capítulo 5: Trabajo con colecciones 239 conversión de una clase heredada en una colección o un mapa. Una situación en que esto resulta particularmente útil es cuando se trabaja con propiedades que están almacenadas en la lista de Properties. Properties es una clase heredada que almacena claves y valores. Properties es única porque cada clave y valor es una cadena. Es una subclase de Hashtable que fue reformada para implementar la interfaz Map. Esto facilita la conversión de una lista de Properties heredada en un mapa que es parte de la estructura de colecciones. Debido a que Properties usa una tabla de hash para almacenamiento, tiene sentido que se convierta en un HashMap. En esta solución se muestra cómo hacerlo. Paso a paso Para convertir un objeto de Properties en un HashMap, siga estos pasos: 1. Obtenga una referencia al objeto de Properties que quiera convertir. 2. Cree un HashMap que especifique String para las claves y los valores. 3. Pase el objeto de Properties al constructor de HashMap. Para ello, debe modificar la referencia de Properties a Map. Análisis La manera más fácil de convertir una lista de Properties en una Map es pasar el objeto de Properties al constructor del mapa. Esto funciona porque todas las implementaciones del mapa proporcionadas por la estructura de colecciones definen un constructor que crea un mapa a partir de otra instancia de Map. En esta solución se usa un HashMap para almacenar la lista de Properties convertida porque usa una tabla de hash para almacenamiento. Por tanto, HashMap proporciona una implementación equivalente. El siguiente constructor de HashMap se usa para convertir una lista de Properties en un mapa: HashMap(Map<? extends C, ? extends V> Por supuesto, cuando se pasa un objeto de Properties, tanto C como V serán objetos de String. Hay un tema del que necesita estar consciente cuando pase un objeto de Properties a una implementación de Map. Properties implementa Map<Object, Object>. Por tanto, necesitará convertir la instancia de Properties en una referencia simple a Map cuando se pasa al constructor para crear un objeto de HashMap<String, String>. Esto causará un advertencia de conversión no comprobada, pero debido a que Properties contiene sólo claves y valores de String, el HashMap resultante será correcto. (En realidad, en Java 5, puede convertir una referencia a Properties en una a Map<String, String> para evitar la advertencia, pero esto no se permite en Java 6). Debido a que Properties implementa Map, pueden agregarse entradas a una lista de Properties llamando a put( ). Sin embargo, a menudo se usa el método heredado setProperty( ). Aquí se muestra: Object setProperty(String c, String v) Aquí, la clave se pasa en c y el valor en v. Este método se usa en el siguiente ejemplo para construir una lista de Properties. Ejemplo En el siguiente ejemplo se muestra cómo convertir una lista de Properties en un HashMap. // Convierte una lista de Properties en un mapa. import java.util.*; www.fullengineeringbook.net 240 Java: Soluciones de programación class PropAMap { public static void main(String args[ ]) { // Crea una lista de propiedades. Properties prop = new Properties( ); // Pone entradas en la lista de propiedades. // Cada entrada contiene el nombre // y la dirección de correo electrónico. prop.setProperty("Tom", "tom@hschildt.com"); prop.setProperty("Ken", "ken@hschildt.com"); prop.setProperty("Ralph", "Ralph@hschildt.com"); prop.setProperty("Steve", "Steve@hschildt.com"); // Crea un hash map que usa cadenas para sus // claves y valores. Inicializa ese mapa con la // lista de propiedades. Debido a que Properties // fue modificada para compatibilidad hacia atrás // para implementar la interfaz Map, puede // pasarse al constructor de HashMap. Sin embargo, // se requiere una conversión al tipo simple de Map. HashMap<String, String> mapProp = new HashMap<String, String>((Map) prop); // Obtiene un conjunto de entradas de mapa // y los despliega. Set<Map.Entry<String, String>> cjtProp; cjtProp = mapProp.entrySet( ); System.out.println("Contenido del mapa: "); for(Map.Entry<String, String> me : cjtProp) { System.out.print(me.getKey( ) + ": "); System.out.println(me.getValue( )); } } } Aquí se muestra la salida: Contenido del mapa: Tom: tom@hschildt.com Steve: Steve@hschildt.com Ralph: Ralph@hschildt.com Ken: ken@hschildt.com Opciones En los casos en que sólo quiera convertir en un mapa ciertas entradas de una lista de Properties, puede iterar en la propiedades, agregando manualmente las que quiera almacenar en el mapa. Para ello, puede obtener una vista de colección de la lista de Properties al llamar a entrySet( ), que está definida en la interfaz Map. Devuelve un conjunto que contiene las propiedades encapsuladas en objetos de Map.Entry. Luego puede iterar en el conjunto, como lo haría con cualquier otra colección, seleccionando los elementos que agregará al mapa. Otras clases heredadas pueden convertirse en colecciones o mapas empleando el mismo procedimiento general demostrado en la solución. Por ejemplo, como Vector se ha adecuado hacia atrás para implementar List, es posible pasar una instancia de vector a un constructor de ArrayList. www.fullengineeringbook.net 6 CAPÍTULO Applets y Servlets E n este capítulo se presentan varias soluciones basadas en applets y servlets. Una applet es un pequeño programa que se envía dinámicamente en Web y se ejecuta dentro de un explorador. Una servlet es un pequeño programa que se ejecuta en el lado del servidor de la conexión. Por tanto, una applet expande la funcionalidad del explorador y una servlet extiende la del servidor. Juntas, integran dos de los usos más importantes de Java. Este capítulo empieza con una revisión general de ambas. Luego ilustra varias técnicas centrales. He aquí las soluciones de este capítulo: • Cree un esqueleto de applet basado en AWT. • Cree un esqueleto de applet basado en Swing. • Cree una GUI y maneje sucesos en una applet de Swing. • Pinte directamente en la superficie de la Applet. • Pase parámetros a Applets. • Use AppletContext para desplegar una página Web. • Cree una servlet simple usando GenericServlet. • Maneje solicitudes HTTP en una Servlet. • Use una cookie con una Servlet. NOTA En los capítulos 7 y 8, donde se analizan el multiprocesamiento y Swing, se describen técnicas y características que pueden usarse en la programación de applet o que se relacionan con ésta. Revisión general de las applets Las applets son pequeñas aplicaciones que se descargan de Internet y que se ejecutan dentro de un explorador. No son aplicaciones independientes. Debido a que la máquina virtual de Java está a cargo de la ejecución de todos los programas de Java, incluidas applets, éstas últimas ofrecen una manera segura de descargar y ejecutar dinámicamente programas en Web. Antes de seguir adelante, necesita dejarse en claro un tema importante. Hay dos variedades de applets. Las primeras son las basadas directamente en la clase Applet, definida por java.applet. 241 www.fullengineeringbook.net 242 Java: Soluciones de programación Estas applets usan el kit de herramientas abstractas de ventana (AWT, Abstract Window Toolkit) para proporcionar una interfaz gráfica de usuario (GUI, Graphic User Interface), o no usa una GUI en absoluto. Este estilo de applet ha estado disponible desde la creación de Java. El segundo tipo de applet está basado en la clase Swing javax.swing.JApplet. Las applets de Swing usan las clases de Swing para proporcionar la GUI. Swing ofrece una interfaz de usuario más rica y a menudo más fácil de usar que AWT. Por tanto, las applets de Swing ahora son más populares. Debido a que JApplet hereda Applet, todas las características encontradas en Applet también están disponibles en JApplet, y la estructura básica de ambos tipos de applets son en gran medida iguales. (Sin embargo, las applets de Swing deben respetar unas cuantas restricciones adicionales). Debido a que aún se usan los dos tipos de applet, ambos se describen en este capítulo. NOTA Como regla general, si creará una applet que usa controles de GUI, como botones para oprimir, casillas de verificación, controles de texto y elementos parecidos, entonces normalmente creará una applet de Swing. Sin embargo, si su applet no usa una GUI, o si pinta directamente en la superficie de la ventana de la applet, entonces una applet de AWT es una opción válida. La clase Applet Todas las applets derivan (directa o indirectamente) de javax.applet.Applet. Contiene varios métodos que le proporcionan un control detallado de la ejecución de una applet y que le permiten acceder a otros recursos en Web. Se muestran en la tabla 6-1. Método Descripción void destroy( ) Es llamado por el explorador justo antes de que termine una applet. Su applet sobrescribirá este método si necesita realizar cualquier limpieza antes de su destrucción. AccesibleContext getAccesibleContext( ) Devuelve el contexto de accesibilidad para el objeto que invoca. AppletContext getAppletContext( ) Devuelve el contexto asociado con la applet. String getAppletInfo( ) Devuelve una cadena que describe la applet. AudioClip getAudioClip(URL url) Devuelve un objeto de AudioClip que encapsula el clip de audio encontrado en el lugar especificado por url. AudioClip getAudioClip(URL url, String nombreClip) Devuelve un objeto de AudioClip que encapsula el clip de audio encontrado en el lugar especificado por url y con el nombre especificado por nombreClip. URL getCodeBase( ) Devuelve el URL asociado con la applet que invoca. URL getDocumentBase( ) Devuelve el URL del documento HTML que invoca a la applet. I mage getImage(URL url) Devuelve un objeto de Image que encapsula la imagen encontrada en el lugar especificado por url. Image getImage(URL url, String nombreImagen) Devuelve un objeto Image que encapsula la imagen encontrada en el lugar especificado por url y que tiene el nombre especificado por nombreImagen. Tabla 6-1 Los métodos definidos por Applet (continúa) www.fullengineeringbook.net Capítulo 6: Applets y Servlets 243 Método Descripción Locale getLocale( ) Devuelve un objeto Locale, usado por varias clases y métodos sensibles a la configuración de región e idioma local. String getParameter(String nombreParam) Devuelve el parámetro asociado con nombreParam. Se devuelve null si no se encuentra el parámetro especificado. String[ ][ ] getParameterInfo( ) Devuelve una tabla de String que describe los parámetros reconocidos por la applet. Cada entrada de la tabla debe constar de tres cadenas que contienen el nombre del parámetro y una descripción de su tipo, rango, o ambos, y una explicación de su propósito. void init( ) Se llama cuando una aplicación inicia su ejecución. Es el primer método llamado por cualquier applet. boolean isActive( ) Devuelve verdadero si se ha iniciado la applet. Devuelve falso si se ha detenido la applet. static final AudioClip newAudioClip(URL url) Devuelve un objeto AudioClip que encapsula el clip de audio encontrado en la ubicación especificada por url. Este método es similar a getAudioClip( ). Excepto que es estático y puede ejecutarse sin la necesidad de un objeto de Applet. void play(URL url) Si se encuentra un clip de audio en la ubicación especificada por url, se reproduce el clip. void play(URL url, String nombreClip) Si se encuentra un clip de audio en la ubicación especificada por url y con el nombre especificado por nombreClip, se reproduce el clip. void resize(Dimension dim) Cambia el tamaño de la applet de acuerdo con la dimensión especificada por dimensión. Dimension es una clase empaqueta en java.awt. Contiene dos campos enteros: width y height. void resize(int ancho, int alto) Cambia el tamaño de la applet de acuerdo con la dimensión especificada por ancho y alto. final void setStub(AppletStub objTalon) Hace que objTalon sea el talón de la applet. Este método se usa en un sistema de motor en tiempo de ejecución y no es común que lo llame la applet. Un talón es una pequeña pieza de código que proporciona el vínculo entre su applet y el explorador. void showStatus(String cad) Despliega cad en la ventana de estado del explorador o el visor de applets. Si el explorador no soporta una ventana de estatus, entonces no se emprende ninguna acción. void start( ) Es llamada por el explorador cuando debe empezar (o reanudarse) una applet. Se le llama automáticamente después de Init( ), cuando una applet empieza. void stop( ) Es llamada por el explorador para suspender la ejecución de la applet. Una vez detenida, una applet se reinicia cuando el explorador llama a start( ). Tabla 6-1 Los métodos definidos por Applet (continuación) www.fullengineeringbook.net 244 Java: Soluciones de programación Applet está fuertemente integrada con AWT porque extiende tres clases de AWT: Component Container Panel La superclase inmediata de Applet es Panel. Define un contenedor simple que puede contener componentes. Panel se deriva de Container, que es la superclase de todos los contenedores. Container se deriva de Component, que especifica esas cualidades que definen un componente basado en AWT. Esto incluye (entre muchas otras cosas) la capacidad de desplegar y pintar una ventana y de recibir sucesos. Por tanto, una Applet es un componente y un contenedor para otros componentes. La clase JApplet, que se usa para applets de Swing, se deriva directamente de Applet. En general, casi todos los componentes de Swing derivan de JComponent. Las únicas excepciones son los cuatro contenedores de nivel superior de Swing, entre las que se incluye JApplet. Por tanto, ésta contiene todas las consultas disponibles para cualquier componente de AWT. Sin embargo, agrega soporte a Swing, incluidos métodos que acceden a los diversos paneles definidos por Swing. En Swing, los componentes no se agregan directamente a un contenedor de nivel superior. En cambio, se agregan al panel de contenido del contenedor, y JApplet maneja este proceso. (Consulte el capítulo 8 para conocer una revisión general de la arquitectura de componentes de Swing y sus varios paneles). Para usar una applet, debe especificarse en un archivo HTML. Al momento de escribir esto, Sun recomienda el uso de la etiqueta APPLET para este propósito, y ésta es la etiqueta usada por los ejemplos de este libro. Un explorador con opciones de Java ejecutará la applet cuando encuentre la etiqueta APPLET. Por ejemplo, el siguiente HTML ejecuta una applet llamada MiApplet. <applet code="MiApplet" width=200 height=60> </applet> Cuando se encuentra esta etiqueta, se ejecuta MiApplet en una ventana que tiene 200 píxeles de ancho y 60 de alto. Por conveniencia, cada ejemplo de applet en este capítulo incluye, en un comentario cerca de la parte superior de su código fuente, el HTML necesario para ejecutar la applet. Aunque las applets se crean para usarse dentro de un explorador, para propósitos de prueba, puede ejecutar una applet dentro de un visor de applets, como appletviewer, que se proporciona con JDK. Un visor de applets hace mucho más fácil y rápido el desarrollo. Para usar appletviewer con el fin de ejecutar una applet, especifique el nombre de un archivo que contiene la etiqueta APPLET que lanza la applet. Debido a que cada uno de los ejemplos de applet de este libro contiene la etiqueta APPLET necesaria en un comentario, puede especificar el nombre del archivo fuente; appletviewer encontrará la etiqueta y ejecutará la applet. Arquitectura de Applet Todas las applets (basadas en Applet o en JApplet) comparten la misma arquitectura general y tienen el mismo ciclo de vida. Arquitectónicamente, las applets parecen programas de GUI basados en ventanas. Esto significa que no están organizados como programas de consola. La ejecución de una applet no empieza en main( ). En realidad, pocas applets tienen siquiera métodos main( ). En cambio, la ejecución de una applet inicia y es controlada por métodos del ciclo de vida. La salida a una ventana de applet no la realiza System.out.println( ) y normalmente no usará un método como readLine( ) para entrada. En cambio, varios controles proporcionados por componentes de AWT y www.fullengineeringbook.net Capítulo 6: Applets y Servlets 245 Swing manejan las interacciones de usuario, como casillas de verificación, listas y botones. También es posible escribir salida directamente en una ventana de applet, pero usará un método como drawString( ) en lugar de println( ). Las applets están orientadas a sucesos. He aquí cómo funciona el proceso. Una applet espera hasta que ocurre un suceso, como el clic de un usuario o la selección de un elemento de una lista. El sistema del motor en tiempo de ejecución notifica a la applet sobre el suceso al llamar a un manejador de sucesos proporcionado por la applet. Una vez que esto sucede, la applet debe emprender la acción apropiada y luego regresar rápidamente. Éste es un tema crucial. En su mayor parte, una applet no debe ingresar en un "modo" de operación en que mantiene control por un periodo extenso. En cambio, debe realizar acciones específicas como respuesta a sucesos y luego devolver el control al sistema del motor en tiempo de ejecución. En las situaciones en que su applet necesita realizar una tarea repetitiva por cuenta propia (como desplegar un mensaje que se desplaza por su ventana), debe iniciar un subproceso adicional de la ejecución. El ciclo de vida de la applet Debido a que las applets se ejecutan dinámicamente bajo el control de un explorador, Applet define un conjunto de métodos de ciclo de vida que controlan la ejecución de una applet. Los métodos del ciclo de vida son init( ), start( ), stop( ) y destroy( ). Se proporcionan implementaciones predeterminadas para todos esos métodos, y no necesita sobreescribir los que no se usan. Es importante comprender el orden en que se ejecutan los métodos del ciclo de vida. Cuando empieza una applet, se llama a los siguientes métodos en esta secuencia: 1 init( ) 2 start( ) Cuando termina una applet, se presenta la siguiente secuencia de llamadas a métodos: 1 stop( ) 2 destroy( ) Miremos más de cerca estos métodos. init( ) es el primer método al que se llama. En init( ), su applet inicializará variables y realizará cualquier otra actividad de inicio. Sólo se le llama una vez. Al método start( ) se le llama después de init( ). También se le llama para reiniciar una applet después de que se le ha detenido, como cuando el usuario regresa a una página Web desplegada previamente y que contiene una applet. Por tanto, podría llamarse a start( ) más de una vez durante el ciclo de vida de una applet. Cuando se deja la página que contiene su applet, se llama al método stop( ). Usará stop( ) para suspender cualquier subproceso secundario creado por la applet y realizar cualquier otra actividad requerida para colocar la applet en un estado seguro e inactivo. Recuerde que una llamada a stop( ) no significa que la applet deba terminarse. Una applet detenida podría reiniciarse con una llamada a start( ), si el usuario regresa a la página. Se llama al método destroy( ) cuando ya no se necesita la applet. Se usa para realizar cualquier operación de cierre de la applet. www.fullengineeringbook.net 246 Java: Soluciones de programación Las interfaces AppletContext, AudioClip y AppletStub Además de la clase Applet, java.applet también define tres interfaces: AppletContext, AppletStub y AudioClip, que proporcionan soporte adicional a applets. AudioClip especifica tres métodos: play( ), loop( ) y stop( ), que le permiten reproducir un archivo de audio. La interfaz AppletStub especifica el vínculo entre una applet y el explorador. No suele utilizarse cuando se desarrollan applets. La interfaz de applet de uso más común es AppletContext. Encapsula información acerca del entorno de ejecución de la applet. Especifica varios métodos. El usado en este capítulo es showDocument( ), que causa que el explorador despliegue una página Web especificada. El contexto de una applet puede obtenerse al llamar a getAppletContext( ), que es definido por Applet. Revisión general de la servlet Las servlets son pequeños programas que se ejecutan en el lado del servidor de una conexión Web. Así como las applets extienden la funcionalidad de un explorador Web, las servlets extienden la funcionalidad de un servidor Web. Lo hacen así al proporcionar un medio conveniente para generar contenido dinámico, como información de precio y disponibilidad para productos vendidos en una tienda en línea. Las clases e interfaces de la API de servlets están empaquetadas en javax.servlet y java. servlet.http. Estos paquetes no son parte de la API de Java. En cambio, son extensiones estándar proporcionadas por Tomcat, el kit de herramientas estándar de desarrollo de servlets. Tomcat es un producto de fuente abierta mantenido por el Jakarta Project de la Apache Software Foundation. Contiene las bibliotecas de clases, la documentación y el soporte al motor en tiempo de ejecución que necesitará para crear y probar servlets. Puede descargar Tomcat de jakarta.apache.org. Al momento de escribir esto, la versión actual de Tomcat es 6.0.10, que soporta la especificación 2.5 de la servlet. Es la versión de Tomcat usada en este libro. Sin embargo, sería bueno que revisara el sitio Web de Jakarta para conocer la información más reciente. NOTA El tema de las servlets es muy amplio, al igual que la API de servlets. En esta revisión general se proporciona información suficiente para usar las soluciones de este capítulo, pero los desarrolladores serios de servlets querrán explorar las servlets con mucho mayor detalle que el que puede ofrecerse aquí. El paquete javax.servlet El paquete javax.servlet contiene varias interfaces y clases que establecen el marco conceptual en que operan las servlets. De estos, las soluciones que se encuentran en este capítulo hacen uso directo de sólo tres interfaces y una clase. Las interfaces son: Servlet ServletRequest ServletResponse Servlet define la funcionalidad básica que deben proporcionar todas las servlets. Esto incluye los métodos que controlan el ciclo de vida de la servlet. ServletRequest encapsula una solicitud. Se usa para obtener información vinculada con la solicitud, como sus parámetros y su tipo de contenido. ServletResponse encapsula una respuesta. Se usa para enviar información de regreso al cliente. www.fullengineeringbook.net Capítulo 6: Applets y Servlets 247 Método Descripción void destroy( ) Se le llama cuando la servlet está descargada. ServletConfig getServletConfig( ) Devuelve un objeto de ServletConļ¬g que contiene cualquier parámetro de inicialización. Éste es el mismo objeto que se pasa a init( ). String getServletInfo( ) Devuelve una cadena que describe la servlet. void init(ServletConfig sc) throws ServletException Se le llama cuando se inicializa la servlet. Los parámetros de inicialización para la servlet pueden obtenerse de sc. Debe lanzarse una UnavailableException si no puede inicializarse la servlet. void service(ServletRequest sol, ServletResponse res) throws ServletException, IOException Se le llama para procesar una solicitud de un cliente. La solicitud del cliente puede leerse de sol. La respuesta al cliente pueden escribirse en res. Se genera una excepción si ocurre un problema con la servlet o E/S. Tabla 6-2 Los métodos definidos por la interfaz Servlet La clase usada en este capítulo es GenericServlet. Implementa la interfaz Servlet. También implementa la interfaz ServletConfig, que encapsula información de configuración. (ServletConfig no se usa directamente en las soluciones de este capítulo, pero pueden resultar valiosas cuando se desarrollen sus propias servlets). En las siguientes secciones se echa un vistazo de cerca de esas interfaces y su clase. La interfaz Servlet Todas las servlets deben implementar la interfaz Servlet. Sus métodos se muestran en la tabla 6-2. Preste atención especial a los métodos init( ), service( ) y destroy( ). Son los métodos del ciclo de vida de la servlet. Son invocados por el servidor y rigen la ejecución de la servlet. (El ciclo de vida de la servlet se describirá en breve). La interfaz ServletRequest La interfaz ServletRequest permite que una servlet obtenga información acerca de una solicitud de cliente. Define muchos métodos. Una muestra se presenta en la tabla 6-3. La interfaz ServletResponse La interfaz ServletResponse permite que una servlet formule una respuesta para un cliente. Define varios métodos. Una muestra se presenta en la tabla 6-4. La clase GenericServlet La clase GenericServlet implementa la mayor parte de Servlet y toda ServletConfig. Su propósito es facilitar la creación de servlets. Simplemente extienda GenericServlet y sólo sobreescriba los métodos necesarios para su aplicación. El único método que debe sobreescrbir es service( ), que es dependiente de las necesidades específicas de su aplicación. Por esto, service( ) no es implementado por GenericServlet. La implementación de init( ) y destroy( ) no hace nada, de modo que si su servlet requiere inicialización o debe liberar recursos, antes de la terminación, también debe sobreescribir uno o ambos métodos. www.fullengineeringbook.net 248 Java: Soluciones de programación Método Descripción Object getAttribute(String atr) Devuelve el valor del atributo llamado atr. Enumeration getAttributeNames( ) Devuelve una enumeración de los nombres de atributo asociados con la solicitud. String getCharacterEncoding( ) Devuelve la codificación del carácter de la solicitud. int getContentLength( ) Devuelve la longitud del contenido. Se devuelve el valor –1 si la longitud no está disponible. String getContentType( ) Devuelve el tipo de la solicitud. Se devuelve un valor null si no puede determinarse el tipo. ServletInputStream getInputStream( ) throws IOException Devuelve un ServletInputStream que puede usarse para leer datos binarios de la solicitud. Se lanza una IllegalStateException si getReader( ) ya se ha invocado mediante esta solicitud. String getParameter(String nombrePam) Devuelve el valor del parámetro llamado nombrePam. Devuelve null si no se encuentra nombrePam. Enumeration getParameterNames( ) Devuelve una enumeración de los nombres de parámetro de esta solicitud. String[ ] getParameterValues (String nombrePam) Devuelve una matriz que contiene valores asociados con el parámetro especificado por nombrePam. Devuelve null si no se encuentra nombrePam. String getProtocol( ) Devuelve una descripción del protocolo. BufferedReader getReader( ) throws IOException Devuelve un lector incluido en búfer que puede usarse para leer texto de la solicitud. Se lanza una IllegalStateException si getInputStream( ) ya se ha invocado para esta solicitud. String getRemoteAddr( ) Devuelve la cadena equivalente de la dirección IP del cliente. String getRemoteHost( ) Devuelve la cadena equivalente del nombre de host del cliente. String getScheme( ) Devuelve el esquema de transmisión del URL, usado para la solicitud (por ejemplo, "http", "ftp"). String getServerName( ) Devuelve el nombre del servidor. int getServerPort( ) Devuelve el número de puerto. Tabla 6-3 Una muestra de los métodos definidos por Servlet Request GenericServlet agrega un método propio llamado log( ), que adjunta una cadena al archivo de registro del servidor. Se proporcionan dos versiones y se muestran aquí: void log(String cad) void log(String cad, Throwable exc) Aquí, cad es la cadena que se adjuntará al registro, y exc es la excepción que ocurrió. www.fullengineeringbook.net Capítulo 6: Applets y Servlets 249 Método Descripción String getCharacterEncoding( ) Devuelve la codificación de carácter para la respuesta. ServletOutputStream getOutputStream( ) throws IOException Devuelve un ServletOutputStream que puede usarse para escribir datos binarios en la respuesta. Se lanza una IllegalStateException si getWriter( ) ya se ha invocad para esta solicitud. PrintWriter getWriter( ) throws IOException Devuelve un PrintWriter que puede usarse para escribir datos de carácter en la respuesta. Se lanza una IllegalStateException si getOutputStream( ) ya se ha invocado para esta solicitud. void setContentLength(int tam) Establece la longitud del contenido para la respuesta en tam. void setContentType(Srtring tipo) Establece el tipo de contenido para la respuesta como tipo. Tabla 6-4 Una muestra de los métodos definidos por ServletRespons La clase ServletException El paquete javax-servlet define dos excepciones. La primera es ServletException, que indica que ha ocurrido un problema con la servlet. El segundo es UnavailableException, que extiende ServletException. Indica que una servlet no está disponible. El paquete javax.servlet.http El paquete javax.servlet.http proporciona una interfaz y clases que facilitan la construcción de servlets que funcionan con solicitudes y respuestas de HTTP. Las interfaces usadas en este capítulo son HttpServletRequest y HttpServletResponse. La primera extiende ServletRequest y permite que una servlet lea datos de una solicitud HTML. HttpServletResponse extiende ServletResponse y permite que una servlet escriba datos en una respuesta HTTP. Las clases usadas en este capítulo son HttpServlet y Cookie. HttpServlet proporciona métodos para manejar solicitudes y respuesta de HTTP. Cookie encapsula una cookie. Cada una se examina con mayor detalle en las siguientes secciones. La interfaz HttpServletRequest La interfaz HttpServletRequest permite que una servlet obtenga información acerca de una solicitud de cliente. Extiende ServletRequest y agrega métodos relacionados con solicitudes de HTTP. En la tabla 6-5 se presenta una muestra de sus métodos. La interfaz HttpServletResponse La interfaz HttpServletResponse permite a una servlet formular una respuesta a un cliente. Se definen varias constantes. Estas corresponden a los diferentes códigos de estatus que pueden asignarse a una respuesta HTTP. Por ejemplo, SC_OK indica que la solicitud HTTP tuvo éxito, y SC_NOT_FOUND indica que el recurso solicitado no está disponible. Una muestra de métodos de esta interfaz se muestra en la tabla 6-6. www.fullengineeringbook.net 250 Java: Soluciones de programación Método Descripción String getAuthType( ) Devuelve esquema de autentificación. Cookie[ ] getCookies( ) Devuelve una matriz de las cookies en esta solicitud. String getHeader(String nombreEnc) Devuelve el valor del encabezado llamado nombreEnc. Devuelve null si no se encuentra el encabezado. Enumeration getHeaderNames( ) Devuelve una enumeración de los nombres de encabezado. int getIntHeader(String nombreEnc) Devuelve el equivalente de int del encabezado llamado nombreEnc. Si no se encuentra el encabezado, se devuelve –1. Se lanza una NumberFormatException si ocurre un error en la conversión. String getMethod( ) Devuelve una cadena que contiene el nombre del método HTTP para esta solicitud. String getPathInfo( ) Devuelve cualquier información que está localizada después de la ruta de la servlet y antes de una cadena de consulta del URL. String getPathTranslated( ) Devuelve cualquier información que está localizada después de la ruta de la servlet y antes de una cadena de consulta del URL después de traducirla en una ruta real. String getQueryString( ) Devuelve la cadena de consulta. Devuelve null si no se encuentra la cadena de consulta. String getRemoteUser( ) Devuelve el nombre del usuario que emitió esta solicitud. String getRequestedSessionId( ) Devuelve el ID de la sesión. String getRequestURI( ) Devuelve el URI. StringBuffer getRequestURL( ) Devuelve el URL. String getServletPath( ) Devuelve la parte del URL que identifica a la servlet. HttpSession getSession( ) Devuelve la sesión para esta solicitud. Si no existe una sesión, se crea una y luego se devuelve. HttpSession getSession(boolean nuevo) Si nuevo es true y no existe sesión, crea una y devuelve una sesión para esta solicitud. De otra manera, devuelve la sesión existente para esta solicitud. Devuelve null si no existe una sesión y nuevo es false. boolean isRequestedSessionIdFromCookie( ) Devuelve verdadero si una cookie contiene el ID de la sesión. De otra manera, devuelve falso. boolean isRequestedSessionIdFromURL( ) Devuelve verdadero si un URL contiene el ID de la sesión. De otra manera, devuelve falso. boolean isRequestedSessionIdValid( ) Devuelve verdadero si el ID de la sesión solicitada es válido en el contexto de la sesión actual. Tabla 6-5 Una muestra de métodos definidos por HttpServletRequest www.fullengineeringbook.net Capítulo 6: Applets y Servlets 251 Método Descripción void addCookie(Cookie cookie) Agrega cookies a la respuesta HTTP. boolean containsHeader(String nombreEnc) Devuelve verdadero si está establecido el encabezado de la respuesta HTTP especificado por nombreEnc. String encodeURL(String url) Determina si el ID de sesión debe estar codificado en el URL como url. Si es así, devuelve la versión modificada de url. De otra manera, devuelve url. Todos los URL generados por una servlet deben procesarse con este método. String encodeRedirectURL(String url) Determina si el ID de la sesión debe codificarse en el URL identificado como url. Si es así, devuelve la versión modificada de url. De otra manera, devuelve url. Todos los URL pasados a sendRedirect( ) deben procesarse con este método. void sendError(int c) throws IOException Envía el código de error c al cliente. void sendError(int c, String s) throws IOException Envía el código de error c y el mensaje al cliente. void sendRedirect(String url) throws IOException Redirige el cliente a url. void setHeader(String nombreEnc, String valor) Establece el encabezado especificado por nombreEnc con el valor valor. void setIntHeader(String nombreEnc, int valor) Establece el encabezado especificado por nombreEnc con el valor valor. void setStatus(int cod) Establece el código de estatus para esta respuesta a cod. Tabla 6-6 Una muestra de métodos definidos por HttpServletResponse La clase HttpServlet La clase HttpServlet extiende GenericServlet. Suele usarse cuando se desarrollan servlets que reciben y procesan solicitudes HTTP. Define varios métodos do…, que manejan varias solicitudes HTTP. Por ejemplo, doGet( ) maneja una solicitud GET. Los métodos agregados por la clase HttpServlet se muestran en la tabla 6-7. Todos estos métodos están protegidos. La clase Cookie La clase Cookie encapsula una cookie. Una cookie se almacena en un cliente y es valiosa para rastrear actividades de usuario o guardar información de estado. Una servlet puede escribir una cookie en el equipo de un usuario mediante el método addCookie( ) de la interfaz HttpServletResponse. Los datos para esa cookie se incluyen después en el encabezado de la respuesta HTTP que se envía al explorador. www.fullengineeringbook.net 252 Java: Soluciones de programación Método Descripción void doDelete(HttpServletRequest sol, HttpServletResponse res) throws IOException, ServletException Maneja un DELETE HTTP. void doGet(HttpServletRequest sol, HttpServletResponse res) throws IOException, ServletException Maneja un GET HTTP. void doOptions(HttpServletRequest sol, HttpServletResponse res) throws IOException, ServletException Maneja un OPTIONS HTTP. void doPost(HttpServletRequest sol, HttpServletResponse res) throws IOException, ServletException Maneja un POST HTTP. void doPut(HttpServletRequest sol, HttpServletResponse res) throws IOException, ServletException Maneja un PUT HTTP. void doTrace(HttpServletRequest sol, HttpServletResponse res) throws IOException, ServletException Maneja un TRACE HTTP. long getLastModified(HttpServletRequest sol) Devuelve la hora (en milisegundos desde la medianoche del 1 de enero de 1970, GMT), desde que la sol se modificó por última vez. void service(HttpServletRequest sol, HttpServletResponse res) throws IOException, ServletException Devuelve una solicitud al método do…, apropiado. No sobreescriba este método. Tabla 6-7 Los métodos definidos por HttpServlet Los nombres y valores de cookies se almacenan en el equipo del usuario. Aquí se muestra parte de la información que se guarda con cada cookie: • El nombre de la cookie. • El valor de la cookie. • La fecha de expiración de la cookie. • El dominio y la ruta de la cookie. La fecha de expiración determina cuándo habrá de eliminarse la cookie del equipo del usuario. Si no se asigna explícitamente una fecha de expiración a una cookie, se eliminará cuando termine la sesión actual del explorador. De otra manera, la cookie se almacenará en un archivo. El dominio y la ruta de la cookie se determinan cuando se incluyen en el encabezado de una solicitud HTTP. Si el usuario ingresa un URL cuyo dominio y ruta coinciden con estos valores, entonces la cookie se proporciona al servidor Web. De otra manera, no se le proporciona. Hay un constructor para Cookie. Tiene la firma que se muestra aquí: Cookie(String nombre, String valor) www.fullengineeringbook.net Capítulo 6: Applets y Servlets 253 Método Descripción Object clone( ) Devuelve una copia de este objeto. String getComment( ) Devuelve el comentario. String getDomain( ) Devuelve el dominio. int getMaxAge( ) Devuelve la edad máxima (en segundos). String getName( ) Devuelve el nombre. String getPath( ) Devuelve la ruta a la que se devuelve la cookie. boolean getSecure( ) Devuelve verdadero si la cookie es segura, de otra manera, devuelve falso. String getValue( ) Devuelve el valor. int getVersion( ) Devuelve la versión de la especificación de cookies usada por la cookie. void setComment(String c) Establece el comentario en c. void setDomain(String d) Establece el dominio en d. void setMaxAge (int periodo) Establece la edad máxima de la cookie en periodo. Es el número de segundos después de los cuales se eliminará la cookie. void setPath(String r) Establece la ruta a la que la cookie se envía a r. void setSecure(boolean segura) Establece la marca de seguridad en segura. void setValue(String v) Establece el valor en v. void setVersion(int v) Establece la versión de la especificación de las cookies usadas por la cookie en v. Tabla 6-8 Los métodos definidos por Cookie Aquí, el nombre y el valor de la cookie se proporcionan al constructor como argumentos. Los métodos de la clase Cookie se resumen en la tabla 6-8. El ciclo de vida de la servlet Todas las servlets tienen el mismo ciclo de vida, que es regido por tres métodos definidos por la interfaz Servlet. Se trata de init( ), service( ) y destroy( ). El ciclo de vida empieza cuando el servidor invoca el método init( ). Esto ocurre cuando la servlet se carga por primera vez en memoria. Por tanto, init( ) se ejecuta sólo una vez. Se pasa una referencia a un objeto de ServletConfig, que se usa para pasar parámetros a la servlet. Después de que se ha inicializado la servlet, el servidor invoca al método service( ) para procesar una solicitud. La servlet puede leer datos que se han proporcionado en la solicitud mediante el parámetro ServletRequest. También puede formular una respuesta al cliente, empleando el parámetro ServletResponse. El método service( ) se llama para cada solicitud. (Para HttpServlet, service( ) invoca uno de los métodos de do…, para manejar la solicitud). La servlet permanece en el espacio de direcciones del servidor y está disponible para procesar cualquier otra solicitud recibida de los clientes hasta que el servidor la termina. Cuando la servlet ya no es necesaria, el servidor puede eliminarla de la memoria y liberar cualquier recurso empleado por la servlet al llamar al método destroy( ). No se harán llamadas a service( ) después de que se ha llamado a destroy( ). www.fullengineeringbook.net 254 Java: Soluciones de programación Uso de Tomcat para desarrollo de servlets Para crear servlets, necesitará acceso a un entorno de desarrollo de servlets. Como ya se mencionó, el usado en este capítulo es Tomcat, que es un producto de fuente abierta mantenido por el Jakarta Project de la Apache Software Foundation. Aunque Tomcat es fácil de usar, sobre todo si tiene experiencia como desarrollador Web que está usando una herramienta de desarrollo de alta calidad, aún es útil recorrer el procedimiento. Las instrucciones dadas aquí suponen que sólo está usando JDK y Tomcat. No se supone ningún entorno de desarrollo ni herramientas integrados. Las instrucciones presentadas aquí para usar Tomcat suponen un entorno de Windows. En el entorno, la ubicación predeterminada para Tomcat 6.0.10 es C:\apache-tomcat-6.0.10 Esta es la ubicación supuesta para las soluciones de este libro. Si carga Tomcat en una ubicación diferente, o si usa una versión distinta, necesitará hacer los cambios necesarios. Tal vez necesite establecer la variable de entorno JAVA_HOME en el directorio de nivel superior en que está instalado el kit de desarrollo de Java. Para JDK 6, el directorio predeterminado es C:\Archivos de programa\Java\JDK1.6.0 pero necesitará confirmar esto para su entorno. Para iniciar Tomcat, ejecute startup.bat de C:\apache-tomcat-6.0.10\bin\ Cuando haya terminado de probar las servlets, detenga Tomcat al ejecutar shutdown.bat. El directorio C:\apache-tomcat-6.0.10\lib\ Contiene servlet.api.jar. Este archivo JAR contiene las clases e interfaces necesarias para construir servlets. A fin de que este archivo sea accesible, actualice su variable de entorno CLASSPATH para que incluya C:\apache-tomcat-6.0.10\lib\servlet-api.jar Como opción, puede especificar este archivo de clase cuando compile las servlets. Por ejemplo, con el siguiente comando se compila el primer ejemplo de servlet: Javac EsqueletoServlet.java –classpath "C:\apache-tomcat-6.0.10\lib\servlet-api.jar" Una vez que haya compilado una servlet, debe habilitar Tomcat para que la encuentre. Esto significa ponerla en un directorio bajo webapps de Tomcat e ingresar su nombre en un archivo web.xml. Para que esto siga siendo simple, en los ejemplos de este capítulo se usan el directorio y el archivo web.xml que Tomcat proporciona para sus propias servlets de ejemplo. He aquí el procedimiento que seguirá. Primero, copie el archivo de clase de la servlet al siguiente directorio: C:\apache-tomcat-6.0.10\webapps\examples\WEB-INF\classes www.fullengineeringbook.net Capítulo 6: Applets y Servlets 255 A continuación, agregue el nombre de la servlet y correlaciónela con el archivo web.xml en el siguiente directorio: C:\apache-tomcat-6.0.10\webapps\examples\WEB-INF Por ejemplo, suponiendo el primer ejemplo, llamado EsqueletoServlet, agregará las siguientes líneas en la sección que define las servlets: <servlet> <servlet-name>EsqueletoServlet</servlet-name> <servlet-class>EsqueletoServlet</servlet-class> </servlet> A continuación, agregará las siguientes líneas a la sección que define las correlaciones entre servlets: <servlet-mapping> <servlet-name>EsqueletoServlet</servlet-name> <url-pattern>/servlet/EsqueletoServlet</url-pattern> </servlet-mapping> Siga el mismo procedimiento general para todas las soluciones de servlets. Una vez que haya compilado su servlet, copiado su archivo de clase al directorio apropiado y actualizado el archivo web-xml como se acaba de describir, puede probarlo al usar su explorador. Por ejemplo, para probar EsqueletoServlet, inicie el explorador y luego ingrese el URL mostrado aquí: http://localhost:8080/examples/servlet/EsqueletoServlet Como opción, puede ingresar el URL que se muestra a continuación: http://127.0.0.1:8080/examples/servlet/EsqueletoServlet Esto puede hacerse porque 127.0.0.1 está definida como la dirección IP del equipo local. Cree un esqueleto de applet basado en AWT Componentes clave Clases Métodos java.applet.Applet void void void void destroy( ) init( ) start( ) stop( ) Como se explicó, todas las applets comparten una arquitectura y un ciclo de vida comunes. Sin embargo, hay algunas diferencias menores entre el esqueleto usado por las applets de AWT y las www.fullengineeringbook.net 256 Java: Soluciones de programación de Swing. En esta solución se muestra cómo crear un esqueleto de applet de AWT. (La versión de Swing se describe en la siguiente solución.) El esqueleto puede usarse como punto de partida para el desarrollo de applets. Paso a paso Para crear un esqueleto de applet basada en AWT, siga estos pasos: 1. Importe java.applet.*. En realidad, en el caso de applets simples, tal vez sólo necesite importar java.applet.Applet, pero las applets reales a menudo necesitan otras partes del paquete. De modo que suele ser más fácil mejor importar todo java.applet. 2. Cree una clase para la applet. Esta clase debe extender Applet. 3. Sobreescriba los cuatro métodos del ciclo de vida: init( ), start( ), stop( ) y destroy( ). Análisis Los cuatro métodos del ciclo de vida se describieron en Revisión general de las applets, presentada antes. Como se explicó, se proporcionan implementaciones predeterminadas de estos métodos, de modo que no es técnicamente necesario sobreescribirlos. Sin embargo, desde un punto de vista práctico, casi siempre sobrescribirá init( ) porque se usa para inicializar la applet. A menudo también sobrescribirá start( ) y stop( ), sobre todo cuando la applet usa multiprocesamiento. Si la applet usa cualquier recurso, entonces usará destroy( ) para liberar esos recursos. Ejemplo En el siguiente ejemplo, se ensamblan los métodos del ciclo de vida en una applet llamada EsqueletoApplet. Aunque la aplicación no hace nada, aún puede ejecutarse. Observe la etiqueta APPLET en el HTML que se encuentra dentro del comentario, al principio del programa. Puede usarlo para lanzar la applet en su explorador o en appletviewer. Simplemente cree un archivo HTML que contenga la etiqueta. Como opción, puede pasar EsqueletoApplet.java directamente a appletviewer. Encontrará automáticamente la etiqueta y lanzará la applet. Sin embargo, este truco no funcionará con un explorador. // Un esqueleto de applet de AWT. import java.applet.*; /* <applet code=»EsqueletoApplet» width=300 height=100> </applet> */ public class EsqueletoApplet extends Applet { // Se le llama primero. public void init( ) { // Inicializa la applet. } // Se le llama en segundo lugar, después de init( ). // le llama cada vez que se reinicia la applet. public void start( ) { // Inicia o reanuda la ejecución. } www.fullengineeringbook.net También se Capítulo 6: Applets y Servlets 257 // Se le llama cuando se detiene la applet. public void stop( ) { // Se suspende la ejecución. } // Se le llama cuando se detiene la applet. // último método ejecutado. public void destroy( ) { // Realiza actividades de apagado. } Es el } Aquí se muestra la ventana producida por EsqueletoApplet cuando se ejecuta en appletviewer. (Observe que se utiliza el término subprograma en lugar de applet. Sin embargo, en todo este libro se ha preferido utilizar applet, porque es el término más extendido y aceptado). Opciones Puede usar el esqueleto de applet como punto de partida para sus propias applets de AWT. Por supuesto, sólo necesita sobreescribir los métodos del ciclo de vida que usa su applet. El esqueleto mostrado en el ejemplo es adecuado para su uso con una applet de AWT. En la siguiente solución se muestra cómo crear un esqueleto para una applet de Swing. Cree un esqueleto de applet basado en Swing Componentes clave Clases Métodos java.swing.JApplet void void void void java.swing.SwingUtilities static void invokeAndWait(Runnable obj) throws InterruptedException, InvocationTargetException destroy( ) init( ) start( ) stop( ) www.fullengineeringbook.net 258 Java: Soluciones de programación Las applets de Swing usan el mismo esqueleto básico y los mismos métodos del ciclo de vida que el esqueleto de la applet de AWT de la solución anterior. Sin embargo, las applets de Swing se derivan de una clase diferente y debe tenerse cuidado con la manera en que interactúan con componentes de GUI. En esta solución se muestra cómo crear un esqueleto de applet de Swing. Paso a paso A fin de crear un esqueleto para una applet basada en Swing, siga estos pasos: 1. Importe java.swing.*. 2. Cree una clase para la applet. En el caso de las applets de Swing, esta clase debe extender JApplet. 3. Sobreescriba los cuatro métodos del ciclo de vida: init( ), start( ), stop( ), destroy( ). 4. Cree cualquier componente de GUI en el subproceso que despacha el suceso. Para ello, use invokeAndWait( ) definido por SwingUtilities. Análisis Todas las applets basadas en Swing se derivan de la clase javax.swing.JApplet. JApplet es un contenedor de Swing de nivel superior que se deriva de Applet. Por tanto, JApplet hereda todos los métodos de Applet, incluidos los del ciclo de vida, descritos en la solución anterior. Los métodos del ciclo de vida realizan la misma función en una applet de Swing que en una de AWT. Aunque el esqueleto no contiene ningún control de GUI, casi todas las applets de Swing sí los contienen. Como se explicará en el capítulo 8 (que presenta soluciones que usan el conjunto de componentes de GUI de Swing), toda interacción con un componente de Swing debe tener lugar en el subproceso que despacha el suceso. En general, los programas de Swing están orientados a sucesos. Por ejemplo, cuando un usuario interactúa con un componente, se genera un suceso. El sistema en tiempo de ejecución pasa un suceso a la aplicación al llamar a un manejador de sucesos definido por la applet. Esto significa que el manejador se ejecuta en el subproceso que despacha el suceso proporcionado por Swing y no en el subproceso principal de la aplicación. Por tanto, aunque los manejadores de sucesos estén definidos por su programa, se les llama en un subproceso que no fue creado por éste. Para evitar problemas (como que dos subprocesos diferentes traten de actualizar el mismo componente al mismo tiempo), todos los componentes de GUI deben crearse y actualizarse desde el subproceso que despacha el suceso, no el subproceso principal de la aplicación. Sin embargo, init( ) no se ejecuta en el subproceso que despacha el suceso. Por tanto, no puede crearse una instancia directa de un componente de GUI. En cambio, debe crear un objeto Runnable que se ejecuta en el subproceso que despacha el suceso, y hacer que este objeto cree la GUI. Para habilitar el código de GUI para que una applet se cree en el subproceso que despacha el suceso, debe usar el método invokeAndWait definido por la clase SwingUtilities. Se muestra aquí: static void invokeAndWait(Runnable obj) throws InterruptedException, InvocationTargetException Aquí, obj es un objeto Runnable, que hará que el subproceso que despacha el suceso llame a su método run( ). El método no vuelve hasta después de que regresa obj.run( ). Puede usarlo para llamar a un método que construye la GUI para su applet de Swing. Por tanto, el esqueleto para el método init( ) se codificará normalmente como se muestra aquí: www.fullengineeringbook.net Capítulo 6: Applets y Servlets 259 public void init( ) { try { SwingUtilities.invokeAndWait(new Runnable ( ) { public void run( ) { makeGUI( ); // método que inicializa los componentes de Swing } }); } catch(Exception exc) { System.out.println("No puede crearse debido a "+ exc); } } Se llama a un método llamado makeGUI( ), dentro de run( ). Este es un método que comprobó que establece e inicializa los componentes de Swing. Por supuesto, el nombre makeGUI( ) es arbitrario. Puede usar un nombre distinto si así lo desea. Ejemplo El ejemplo siguiente reúne los métodos de ciclo de vida en una applet llamada SwingAppletSkel. También contiene el código de esqueleto necesario para iniciar una GUI. Aunque la applet no hace nada, aún puede ejecutarse. // Un esqueleto de applet de Swing. import javax.swing.*; /* <applet code="EsqueletoAppletSwing" width=300 height=100> </applet> */ public class EsqueletoAppletSwing extends JApplet { // Se le llama primero. public void init( ) { try { SwingUtilities.invokeAndWait(new Runnable ( ) { public void run( ) { makeGUI( ); // método que inicializa los componentes de Swing } }); } catch(Exception exc) { System.out.println("No puede crearse debido a "+ exc); } } // Se le llama en segundo lugar, después de init( ). También se // le llama cada vez que se reinicia la applet. public void start( ) { // Inicia o reanuda la ejecución. } // Se le llama cuando la applet se detiene. public void stop( ) { // Se suspende la ejecución. } www.fullengineeringbook.net 260 Java: Soluciones de programación // Se le llama cuando la applet termina. Es el // último método ejecutado. public void destroy( ) { // Realiza actividades de cerrado. } private void makeGUI( ) { // Aquí crea e inicializa los componentes de GUI. } } Cuando se ejecuta usando appletviewer, produce la misma ventana en blanco que EsqueletoApplet, mostrado en la solución anterior. Opciones Puede usar el esqueleto de applet como punto de partida para sus propias applets de Swing. Por supuesto, necesita sobreescribir sólo los métodos del ciclo de vida utilizados por su applet. Sin embargo, en todos los casos, asegúrese de crear cualquier componente de GUI en el subproceso que despacha el suceso. Si su applet no estará usando una GUI, entonces la mejor opción sería construir una applet de AWT. Consulte la solución anterior para conocer los detalles. Cree una GUI y maneje sucesos en una applet de Swing Componentes clave Clases e interfaces Métodos java.awt.event.ActionEvent String getActionCommand( ) java.awt.event.ActionListener void actionPerformed(ActionEvent ae) java.awt.event.ItemEvent Object getItem( ) java.awt.event.ItemListener void iTemStateChanged(ItemEvent ie) java.swing.JApplet Component add(Component comp) void setLayout(LayoutManager dis) javax.swing.JLabel void setText(String cad) String getText( ) javax.swing.JButton void addActionListener(ActionListener al) javax.swing.JCheckBox void addItemListener(ItemListener it) boolean isSelected( ) void setPreferredSize(Dimension tam) void setSelected(boolean est) www.fullengineeringbook.net Capítulo 6: Applets y Servlets 261 En esta solución se muestra cómo crear una applet de Swing que tiene una GUI y que maneja sucesos. Como se mencionó en la Revisión general de las applets, casi todas las applets basadas en GUI usarán Swing para proporcionar los componentes de la interfaz, como botones, etiquetas y campos de texto. Esto se debe a que Swing ofrece un conjunto de componentes mucho más rico y flexible que el propio AWT. Por tanto, es el método usado en esta solución, y en todas las demás soluciones de este capítulo que crean applets de GUI. Debido a que todos los componentes de Swing generan sucesos (excepto las etiquetas, que simplemente despliegan información), una applet por lo general proporcionará manejo de sucesos. El mismo método básico para manejar sucesos generados por componentes de Swing también se aplica a cualquier otro tipo de sucesos, como los de ratón. Por tanto, las mismas técnicas de manejo de sucesos son aplicables a aplicaciones de Swing y de AWT. Esté consciente de que la GUI que se muestra aquí es muy simple. Las GUI y los problemas que las rodean suelen ser muy complejos. Más aún, hay varias maneras en que pueden crearse los componentes y en que pueden manejarse los sucesos. El método mostrado aquí es sólo una manera. Cuando se construye una applet de GUI, debe adecuar su desarrollo para que coincida con su aplicación. NOTA Una revisión general de la arquitectura, los componentes y el manejo de sucesos de Swing se presentará en el capítulo 8 y ese análisis no se repite aquí. En esta solución simplemente se muestra cómo usar estas características en una applet de Swing. Paso a paso Para crear una applet con una GUI de Swing y para manejar los sucesos generados por la GUI se requieren los siguientes pasos: 1. Cree una clase de applet que extienda JApplet. 2. Si es necesario, establezca el administrador de diseño al llamar a setLayout( ). 3. Cree el componente requerido por la applet. En esta solución se usa un botón, tres casillas de verificación y una etiqueta. Son instancias de JButton, JCheckBox y JLabel, respectivamente. 4. Si es necesario, establezca el tamaño preferido de un componente al llamar a setPreferredSize( ). 5. Implemente escuchas de sucesos para los componentes. En esta solución se usa un escucha de acción para los sucesos de botón y un escucha de elemento para los sucesos de casillas de verificación, que son instancias de ActionListener e ItemListener, respectivamente. 6. Agregue los escuchas a los componentes. Por ejemplo, agregue los escuchas de acción al llamar a addActionListener( ) y el escucha de elemento al llamar a addItemListener( ). Cuando se recibe un suceso, se responde apropiadamente. 7. Agregue los componentes al panel de contenido de la applet. Análisis Hoy en día, casi todas las applets que usan una GUI se basarán en Swing. Se trata de un moderno juego de herramientas de GUI de Java, que proporciona un conjunto de componentes ricos. Como se explicó en Cree un esqueleto de applet basado en Swing, todas las applets de Swing deben extender JApplet. JApplet extiende Applet, agregando soporte a Swing. www.fullengineeringbook.net 262 Java: Soluciones de programación JApplet es un contenedor de Swing de alto nivel. Esto significa que da soporte a los cuatro paneles definidos para los contenedores de nivel superior: el panel raíz, el panel de vidrio, el panel de capas y el panel de contenido. Los componentes de GUI se agregan al panel de contenido de la applet, que es un JPanel (para conocer una descripción de estos paneles y otros elementos esenciales de Swing, consulte el capítulo 8). NOTA Es importante comprender que las applets basadas en AWT no tienen paneles. Por ejemplo, no tienen un panel de contenido. Los paneles se relacionan específicamente con Swing. Como opción predeterminada, el panel de contenido usa diseño de bordes, pero puede establecer el diseño, de acuerdo con lo necesario. Para esto, llame a setLayout( ) en el panel de contenido, pasándolo en el administrador de diseño deseado. A partir de Java 5, esto se hace con sólo llamar a setLayout( ) en la applet. El método se invoca de manera automáticamente en relación con el panel de contenido (consulte la nota histórica que se presenta más adelante). En el siguiente ejemplo se usa el diseño de flujo, que está encapsulado dentro de la clase FlowLayout. Un diseño de flujo coloca los componentes línea por línea, de arriba abajo. La posición de los componentes puede cambiar si se modifica el tamaño de la ventana. En esta solución se usan tres componentes: JButton, JCheckBox y JLabel. JButton crea un botón; JCheckBox, una casilla de verificación y JLabel, una etiqueta. Estos componentes se describen en el capítulo 8, pero los constructores usados en esta solución se muestran aquí porque es más conveniente: JButton(String cad) JCheckBox(String cad) JLabel(String cad) En el caso de JButton, cad especifica la cadena que se desplegará dentro del botón. En el caso de JCheckBox, cad especifica la cadena que describe la casilla de verificación. En el caso de JLabel, cad especifica una cadena que se desplegará dentro de la etiqueta. En ocasiones, querrá establecer el tamaño de un componente. Por ejemplo, tal vez quiera que se alineen componentes relacionados. Para establecer el tamaño, llame a setPreferredSize( ) en el componenteal que quiera cambiar el tamaño. Sin embargo, tome en cuenta que algunos administradores de diseño (como BorderLayout) pueden sobreescribir el tamaño preferido de un componente. Cuando se oprime un botón, genera un ActionEvent. Para que la applet responda al suceso, debe proporcionar un escucha de acción para el botón. Un escucha de acción es una instancia de ActionListener, y se agrega al botón al llamar a addActionListener( ) en el botón. ActionListener sólo define un método, actionPerformed( ). Se llama a este método cuando ocurre un suceso de acción, y se pasa a la instancia de ActionEvent que describe el suceso. Dentro de actionPerformed( ), puede obtener la cadena del comando de acción asociada con el botón. Como opción predeterminada, ésta es la cadena que se muestra dentro del botón. Puede usar el comando de acción para identificar un botón cuando se recibe un suceso de acción. Cuando se marca o desmarca una casilla de verificación, genera un ItemEvent. Para que la applet responda al suceso, debe proporcionar un escucha de elemento para la casilla de verificación. Un escucha de elemento es una instancia de ItemListener, y se agrega a la casilla de verificación al llamar a addItemListener( ) en la casilla de verificación. ItemListener sólo define un método, itemStateChanged( ). A este método se le llama cuando ocurre un suceso de elemento, y se le pasa una instancia de ItemEvent que describe el suceso. Dentro de ItemStateChanged( ) puede obtener una referencia a la casilla de verificación que generó el suceso al llamar a getItem( ). Devuelve una referencia a la casilla de verificación que cambió. En general, los manejadores de sucesos pueden implementarse de varias maneras. En el ejemplo de esta solución se usa una clase interna anónima. En este método, cada componente está vinculado con su propio manejador de sucesos. La ventaja de este método es que el componente www.fullengineeringbook.net Capítulo 6: Applets y Servlets 263 que genera el suceso es conocido y no tiene que determinarse en tiempo de ejecución. Otros métodos requieren que la clase de la applet implemente la interfaz de escucha, o que use una clase separada que implemente el escucha deseado. Con el fin de que los componentes se desplieguen, deben agregarse al panel de contenido de la applet. A partir de Java 5, esto se hace con sólo llamar a add( ) en la applet. El componente se agrega automáticamente al panel de contenido de la applet (consulte la nota histórica siguiente). Nota histórica: getContentPane( ) Antes de Java 5, cuando se agregaba un componente al panel de contenido, se eliminaba un componente de él o se configuraba el administrador de diseño para éste, tenía que obtener explícitamente una referencia al panel de contenido al llamar a getContentPane( ). Por ejemplo, en el pasado, para establecer el administrador de diseño en FlowLayout, necesitaba usar esta instrucción: getContentPane( ).setLayout(new FlowLayout( )); A partir de Java 5, la llamada a getContentPane( ) ya no es necesaria porque las llamadas a add( ), remove( ) y setLayout( ) se dirigen automáticamente al panel de contenido. Por esta razón, en la solución del libro no se llama a getContentPane( ). Sin embargo, si quiere escribir código que pueda compilarse en versiones anteriores de Java, entonces necesitará agregar llamadas a getContentPane( ), donde sea apropiado. Ejemplo En el siguiente ejemplo se muestra una applet que contiene una GUI de Swing simple. Incluye tres casillas de verificación, un botón y una etiqueta. La etiqueta despliega la interacción del usuario con las casillas de verificación. El botón limpia las tres casillas. Observe que todos los componentes están construidos dentro de makeGUI( ), que se ejecuta en el subproceso que despacha el suceso mediante invokeAndWait( ). Como se explicó antes, todas las interacciones del programa con componentes de Swing deben tener lugar en el subproceso de despacho de sucesos, no en el main, de la applet. // Una applet de Swing que construye una GUI y // maneja sucesos. import javax.swing.*; import java.awt.*; import java.awt.event.*; /* <object code=»AppletGUI» width=280 height=160> </object> */ public class AppletGUI extends JApplet { JLabel jetiq; JCheckBox jcvGuardar; JCheckBox jcvValidar; JCheckBox jcvAsegurar; // Inicializa la applet. www.fullengineeringbook.net 264 Java: Soluciones de programación public void init( ) { try { SwingUtilities.invokeAndWait(new Runnable ( ) { public void run( ) { makeGUI( ); } }); } catch(Exception exc) { System.out.println("No puede crearse porque "+ exc); } } // Inicializa la GUI. private void makeGUI( ) { // Establece el diseño del panel en diseño de flujo. setLayout(new FlowLayout( )); // // // // // // Nota: si está usando una versión de Java anterior a JDK 5, entonces necesitará usar getContentPane( ) para establecer explícitamente el diseño del panel, de contenido, como se muestra aquí: getContentPane( ).setLayout(new FlowLayout( )); // Crea la etiqueta que desplegará las selecciones. jetiq = new JLabel( ); // Crea tres casillas de verificación. jcvGuardar = new JCheckBox("Guardar los datos al salir"); jcvValidar = new JCheckBox("Validar los datos"); jcvAsegurar = new JCheckBox("Usar seguridad mejorada"); // Uniforma las dimensiones de las casillas de verificación. Dimension cvTam = new Dimension(200, 20); jcvGuardar.setPreferredSize(cvTam); jcvValidar.setPreferredSize(cvTam); jcvAsegurar.setPreferredSize(cvTam); // Maneja los sucesos de elemento de la casilla. ItemListener cvEscucha = new ItemListener( ) { public void itemStateChanged(ItemEvent ie) { // Obtiene el objeto que generó el suceso. JCheckBox cv = (JCheckBox) ie.getItem( ); // Reporta si se seleccionó o no. if(cv.isSelected( )) jetiq.setText(cv.getText( ) + " seleccionada."); else jetiq.setText(cv.getText( ) + " no seleccionada."); } }; // Agrega escuchas de elemento a las casillas de verificación. www.fullengineeringbook.net Capítulo 6: Applets y Servlets jcvGuardar.addItemListener(cvEscucha); jcvValidar.addItemListener(cvEscucha); jcvAsegurar.addItemListener(cvEscucha); // Agrega un botón que restablece las casillas de verificación. JButton jbtnRestablecer = new JButton("Restablecer opciones"); // Crea el escucha de acción para el botón. jbtnRestablecer.addActionListener( new ActionListener( ) { public void actionPerformed(ActionEvent ae) { jcvGuardar.setSelected(false); jcvValidar.setSelected(false); jcvAsegurar.setSelected(false); jetiq.setText("Se dejaron de seleccionar todas las opciones."); } }); // Agrega la etiqueta, las casillas de verificación y el botón // al panel de contenido de la applet. add(jcvGuardar); add(jcvValidar); add(jcvAsegurar); add(jbtnRestablecer); add(jetiq); // // // // // // // // Nota: si está usando una versión de Java anterior a JDK 5, entonces necesitará usar getContentPane( ) para establecer explícitamente el diseño del panel, de contenido, como se muestra aquí: getContentPane( ).add(jcvGuardar); etcétera. } } Aquí se muestra la applet cuando se ejecuta usando appletviewer: www.fullengineeringbook.net 265 266 Java: Soluciones de programación Ejemplo adicional Con la siguiente applet se crea un letrero simple que se desplaza por la pantalla. Usa un cronómetro para controlar la velocidad del desplazamiento. El texto que se desplaza se conserva en JLabel, que es una clase de etiqueta de Swing. Cada vez que el cronómetro se agota, el texto se desplaza un carácter de su posición. La velocidad del cronómetro determina la velocidad del desplazamiento. La dirección de éste puede invertirse al hacer clic en el botón Invertir. El cronómetro usado por el programa es una instancia de Timer, que es parte de javax. swing. Genera sucesos de acción a un intervalo regular hasta que se le detiene. Tiene el siguiente constructor: Timer(int periodo, ActionListener al) Aquí, periodo especifica el intervalo de cronometraje, en milisegundos, y al es el escucha de acción al que se notificará cada vez que el cronómetro se agote. Éste se inicia al llamar a start( ). Se detiene al llamar a stop( ). Timer resulta especialmente útil en la programación con Swing, porque dispara un suceso al final de cada intervalo de cronómetro. Debido a que los manejadores de sucesos se ejecutan en el subproceso de despacho de sucesos, el manejador de sucesos puede actualizar la GUI de manera segura para los subprocesos. El escucha de acción asociado con el cronómetro es la parte del programa que en realidad desplaza el texto dentro de la etiqueta. Su método actionPerformed( ) gira el texto a la izquierda o la derecha (ya sea que desplazarIzq sea cierto o falso) y luego establece el texto dentro de la etiqueta. Esto causa que el texto se desplace. Cada vez que se hace clic en el botón Invertir, el valor de desplazarIzq se invierte, con lo que se invierte, a su vez, la dirección del desplazamiento. // Una applet de Swing que desplaza el texto en una etiqueta y // proporciona un botón que invierte la dirección del desplazamiento. import javax.swing.*; import java.awt.*; import java.awt.event.*; /* <object code="Desplazador" width=140 height=60> </object> */ public class Desplazador extends JApplet { JLabel jetq; String msj = " ¡Java anima Web! "; boolean desplazarIzq = true; ActionListener Desplazador; // Este cronómetro controla el desplazamiento. Cuanto // menor sea su demora, más rápido el desplazamiento. Timer cronoDezp; // Inicializa la applet. public void init( ) { try { SwingUtilities.invokeAndWait(new Runnable ( ) { public void run( ) { www.fullengineeringbook.net Capítulo 6: Applets y Servlets makeGUI( ); } }); } catch(Exception exc) { System.out.println("No puede crearse porque "+ exc); } } // Inicia el cronómetro cuando se inicia la applet. public void start( ) { cronoDezp.start( ); } // Para el cronómetro cuando se detiene la applet. public void stop( ) { cronoDezp.stop( ); } // Detiene el cronómetro cuando se destruye la applet. public void destroy( ) { cronoDezp.stop( ); } // Inicializa la GUI del cronómetro. private void makeGUI( ) { // Usa el diseño de flujo. setLayout(new FlowLayout( )); // Crea la etiqueta en que se desplazará el mensaje. jetq = new JLabel(msj); jetq.setHorizontalAlignment(SwingConstants.CENTER); // Crea el escucha de acción para el cronómetro. Desplazador = new ActionListener( ) { // Cada vez que el cronómetro se agota, se desplaza // el texto un carácter. public void actionPerformed(ActionEvent ae) { if(desplazarIzq) { // Desplaza el mensaje un carácter a la izquierda. char car = msj.charAt(0); msj = msj.substring(1, msj.length( )); msj += car; jetq.setText(msj); } else { // Desplaza el mensaje un carácter a la derecha. char car = msj.charAt(msj.length( )–1); msj = msj.substring(0, msj.length( )–1); msj = car + msj; jetq.setText(msj); } } }; www.fullengineeringbook.net 267 268 Java: Soluciones de programación // Crea el cronómetro. Se desplaza cada 2 décimas de segundo. cronoDezp = new Timer(200, Desplazador); // Agrega un botón que invierte la dirección del desplazamiento. JButton jbtnInv = new JButton("Invertir"); // Crea el escucha de acción para el botón. jbtnInv.addActionListener( new ActionListener( ) { public void actionPerformed(ActionEvent ae) { desplazarIzq = !desplazarIzq; } }); // Agrega la etiqueta y el botón al panel de contenido de la applet. add(jetq); add(jbtnInv); } } Aquí se muestra la applet cuando se ejecuta usando appletviewer. (Observe que appletviewer sí acepta palabras con acentos.) Opciones Muchos de los componentes de Swing generan más de un tipo de suceso. Por ejemplo, además de disparar un suceso de acción, un JButton también generará uno de cambio (que es una instancia de ChangeEvent) cuando ocurre un cambio en el estado del componente. Por ejemplo, se genera un suceso de cambio cuando se coloca encima el puntero. Por tanto, el suceso que necesitará manejar su applet dependerá del tipo de componente y la situación en que se emplee. Como se mencionó, los manejadores de sucesos pueden implementarse de tres maneras básicas: como clases internas, como partes de la clase de la applet o como clases independiente. Hay una ventaja en hacer que la clase de la applet implemente el manejador o los manejadores de sucesos necesarios para la aplicación: Reduce el número de clases que se necesitan generar. Al reducir el número de clases, pueden reducirse los tiempos de descarga. Por supuesto, cuando la clase de la applet implementa un escucha, si dos objetos generan el mismo suceso (como un suceso de acción), debe determinarse de manera explícita cuál objeto lo generó. Consulte el capítulo 8 para conocer más información acerca el manejo de sucesos y el uso de los componentes de Swing. www.fullengineeringbook.net Capítulo 6: Applets y Servlets 269 Pinte directamente en la superficie de la applet Componentes clave Clases Métodos java.awt.Applet void void void void java.awt.Graphics void drawString(String msj, int x, int y) void drawLine(int inicioX, int inicioY, int ļ¬nX, int ļ¬nY) paint(Graphics g) repaint( ) setBackground(Color nuevoColor) setForeground(Color nuevoColor) Aunque con gran frecuencia la interacción con el usuario tomará lugar a través de uno o más componentes de una GUI, como botones, barras de desplazamiento y contadores, es posible escribir directamente en la superficie de la ventana de una applet. Tal vez quiera hacer eso si la applet sólo despliega texto e imágenes y no requiere otros componentes. En esta solución se muestra cómo pintar en la ventana de una applet. Antes de seguir adelante, es necesario dejar en claro tres puntos importantes. En primer lugar, el tema de pintar en un componente es muy extenso. Java proporciona una rica funcionalidad al respecto, y hay muchas técnicas especializadas. En esta solución se muestra sólo el mecanismo básico requerido para pintar en la superficie de una applet. No se trata de describir todas las minucias que intervienen. En segundo lugar, la técnica que se muestra aquí sólo es útil en situaciones en que la applet no usa componentes de GUI. En otras palabras, aparte de la salida creada al escribir directamente en la superficie de la applet, ésta no despliega otros elementos visuales. Tratar de mezclar salida directa en la superficie de una applet con otros componentes gráficos causará un problema porque uno sobrescribirá (o por lo menos podría sobreescribir) al otro. En tercer lugar, aunque la técnica que se muestra aquí funcionará con applets de JApplet, por lo general, cuando trabaja con Swing, querrá crear un panel separado en el que pueda pintar la salida. La razón es que Swing ofrece un soporte más fino para pintar, que sólo puede lograrse pintando en un componente de Swing, como JPanel. Por tanto, la técnica descrita aquí es más aplicable a applets de AWT. NOTA Cobertura detallada del soporte a AWT para pintura, incluidas imágenes, fuentes y medidas de fuentes, se encontrará en mi libro Java, Manual de referencia, séptima edición. Paso a paso Para pintar en la superficie de una applet, se requieren estos pasos: 1. En la clase de la applet, sobreescriba el método paint( ) especificado por Component (y heredado por Applet). www.fullengineeringbook.net 270 Java: Soluciones de programación 2. Dentro de su versión de paint( ), use uno o más de los métodos de salida de AWT definidos por la clase Graphics. Dos se usan en esta solución: drawString( ), que da salida a una cadena de texto, y drawLine( ), que dibuja una línea. 3. Puede establecer el color del dibujo al llamar a setForeground( ). Para establecer el fondo, llame a setBackground( ). 4. Para que se despliegue la salida, llame a repaint( ). Esto dará como resultado una llamada a paint( ). Análisis El método paint( ) está definido por Component y es heredado por Applet. Se muestra aquí: void paint(Graphics g) A este método se le llama cada vez que una applet debe volver a desplegar su salida. Esta situación puede ocurrir por varias razones. Por ejemplo, la ventana en que se está ejecutando la applet puede sobreescribirse con otra ventana y luego descubrirse. O la ventana de la applet puede minimizarse y luego restablecerse. También se llama al método paint( ) cuando la applet empieza la ejecución. Cualquiera que sea la causa, cada vez que la applet debe redibujar su salida, se llama a paint( ). Por tanto, es en paint( ) donde colocará el código que da salida a la superficie de una applet. El método paint( ) tiene un parámetro de tipo java.awt.Graphics. Éste contiene el contexto gráfico, que describe el entorno gráfico en que se está ejecutando la applet. Este contexto se usa con varios métodos de dibujo, como drawString( ). Graphics también define varios métodos que dan salida a la superficie de la applet. Dos de estos métodos se utilizan en esta solución. El primero es drawString( ), que da salida a una cadena de texto. Se muestra aquí: void drawString(String msj, int x, int y) Este método da salida a la cadena pasada en msj, empezando en la ubicación X,Y especificada por x y y. la cadena se dibuja en el color de fondo actual. En la ventana de Java, la esquina superior izquierda es la ubicación 0,0. Sin embargo, x y y especifican la orilla superior izquierda de la línea de base de los caracteres, no su esquina superior izquierda. Por tanto, debe tomar esto en consideración cuando trate de escribir una cadena en la esquina superior izquierda de la ventana. El segundo método de salida es drawLine( ), que dibuja una línea. Se muestra aquí: void drawLine(int inicioX, int inicioY, int finX, int finY) Dibuja una línea en el color de fondo actual. La línea empieza en inicioX,inicioY, y termina en finX,finY. Puede establecer el color de fondo al llamar a setBackground( ). Puede establecer el color del dibujo al llamar a setForeground( ). Estos métodos son especificados por Component. Se muestran aquí. void setBackground(Color nuevoColor) void setForeground(Color nuevoColor) www.fullengineeringbook.net Capítulo 6: Applets y Servlets 271 Aquí, nuevoColor especifica el nuevo color. La clase Color, que está empaquetada en java.awt, define las constantes mostradas aquí que pueden usarse para especificar colores: Color.black Color.magenta Color.blue Color.orange Color.cyan Color.pink Color.darkGray Color.red Color.gray Color.white Color.green Color.yellow Color.lightGray Las versiones en mayúsculas de las constantes también están definidas. Como regla general, una applet nunca llama directamente a paint( ). En cambio, cuando la superficie de la applet debe repintarse, ejecutará una llamada a repaint( ). El método repaint( ) esta definido por AWT. Requiere que el sistema del motor en tiempo de ejecución ejecute paint( ). Por tanto, para que otra parte de su applet dé salida a su ventana, simplemente almacena la salida y luego llama a repaint( ). Esto causa una llamada a paint( ), que puede desplegar la información almacenada. Por ejemplo, si parte de su applet necesita dar salida a una cadena, puede almacenar esta cadena en una variable String y luego llamar a repaint( ). Dentro de paint( ), dará salida a la cadena empleando drawString( ). NOTA Técnicamente, la llamada a repaint( ) en componentes pesados (incluidos Applet y JApplet), da como resultado una llamada a update( ), que en su implementación predeterminada llama a paint( ). Por tanto, si sobreescribe update( ), debe asegurarse de que se llame paint( ) al final. El método repaint( ) tiene cuatro formas. Aquí se muestra la usada en esta solución: void repaint( ) Esta versión causa que toda la ventana se repinte. Otras versiones de repaint( ) le dejan especificar la región que se repintará. Ejemplo En el siguiente ejemplo se muestra cómo pintar directamente en la superficie de una applet. Se trata de una applet de AWT porque usa componentes de GUI diferentes de la ventana principal de la applet. Usa drawString( ) para escribir una línea de texto y drawLine( ) para dibujar líneas. Cada vez que se hace clic dentro de la applet, el color del dibujo y el mensaje cambian. Luego, se llama a repaint( ). Esto causa que se repinte la applet para reflejar el nuevo color y el mensaje. Como experimento, trate de eliminar la llamada a repaint( ). Como verá, la applet no se actualizará al hacer clic. (Por supuesto, se repintará si la ventana necesita redibujarse, porque se cubrió y luego se descubrió, por ejemplo). Hay otro punto de interés en el programa. Los sucesos de ratón se despliegan mediante el uso de una clase interna anónima que se basa en MouseAdapter. Java proporciona varias clases de adaptador que facilitan la implementación de escuchas de suceso que definen varios métodos. www.fullengineeringbook.net 272 Java: Soluciones de programación Los adaptadores proporcionan métodos predeterminados (vacíos) para todos los métodos definidos por un suceso. Luego puede simplemente sobreescribir los métodos en que está interesado. No tiene que proporcionar implementaciones vacías de los demás. En este ejemplo, el único suceso de ratón en que estamos interesados es cuando se oprime el botón izquierdo; mousePressed( ) maneja este suceso. No se usan los otros métodos definidos por MouseListener (mouseEntered( ), mouseReleased( ), etc.), de modo que pueden manipularse con los manejadores vacíos. // Pinta en la superficie de una applet. import java.awt.*; import java.awt.event.*; import java.applet.*; /* <applet code=»AppletPintura» width=250 height=250> </applet> */ public class AppletPintura extends Applet { String msj = «Esto es negro»; int cuenta = 0; Color colorTexto = Color.black; public void init( ) { // Cambia el color del dibujo cada vez que // se hace clic dentro de la applet. addMouseListener(new MouseAdapter( ) { public void mousePressed(MouseEvent me) { cuenta++; if(cuenta > 3) cuenta = 0; switch(cuenta) { case 0: colorTexto = Color.black; msj = "Esto es negro"; break; case 1: colorTexto = Color.red; msj = "Esto es rojo"; break; case 2: colorTexto = Color.green; msj = "Esto es verde"; break; case 3: msj = "Esto es azul"; colorTexto = Color.blue; break; } www.fullengineeringbook.net Capítulo 6: Applets y Servlets 273 // Solicita que la ventana se redibuje. repaint( ); } }); } // Se le llama cuando debe redibujarse la ventana de la applet. public void paint(Graphics g) { // Establece el color del dibujo. setForeground(colorTexto); // Despliega un mensaje. g.drawString(msj, 30, 20); // Dibuja dos líneas. g.drawLine(50, 50, 200, 200); g.drawLine(50, 200, 200, 50); } } Aquí se muestra la applet cuando se ejecuta con appletviewer: Opciones Además de la forma usada en la solución, repaint( ) soporta otras tres formas, la primera especifica la región que habrá de repintarse: void repaint(int izq, int arriba, int ancho, int alto) www.fullengineeringbook.net 274 Java: Soluciones de programación Aquí, las coordenadas de la esquina superior izquierda de la región están especificadas por izq y arriba, y el ancho y el alto se pasan en ancho y alto. Estas dimensiones se especifican en píxeles. Puede ahorrar tiempo al especificar que una región se repinte. La pintura es costosa en cuanto al tiempo. Si necesita actualizar sólo una pequeña parte de la ventana, es más eficiente repintar únicamente esa región. Las últimas dos formas de repaint( ) le permiten especificar la demora máxima antes de que se repinte. La llamada a repaint( ) es, en esencia, una solicitud para que su applet se repinte en algún momento, pronto. Sin embargo, si su sistema es lento o está ocupado, tal vez no se repinte de inmediato. Esto puede ser un problema en muchas situaciones, incluida la animación, en que es necesario un tiempo de actualización consistente. Las siguientes versiones de repaint( ) ayudan a resolver este problema: void repaint(long demoraMax) void repaint(long demoraMax, int x, int y, int ancho, int alto) Aquí, demoraMax especifica el número máximo de milisegundos que pueden transcurrir antes de que se repinte. Además de drawString( ) y drawLine( ), la clase Graphics proporciona muchos otros métodos de dibujo. Por ejemplo, para dibujar un rectángulo, use drawRect( ). void drawRect(int arriba, int izq, int ancho, int alto) La esquina superior izquierda del rectángulo está en arriba,izq. Las dimensiones del rectángulo están especificadas por ancho y alto. Para dibujar un círculo o una elipse, use drawOval( ): void drawOval(int arriba, int izq, int ancho, int alto) La elipse se dibuja dentro de un rectángulo contenedor, cuya esquina superior izquierda está especificada por arriba,izq y cuyo ancho y alto están especificados por ancho y alto. Para dibujar un círculo, especifique un cuadro como rectángulo contenedor. Puede dibujar un arco empleando drawArc( ): void drawArc(int arriba, int izq, int ancho, int alto, int angInicio, int angBarrido) El arco está encerrado dentro del rectángulo cuya esquina superior izquierda está especificada por arriba,izq y cuyo ancho y alto están especificados por ancho y alto. El arco se dibuja de angInicio hasta la distancia angular especificada por angBarrido. Los ángulos se especifican en grados. Cero grados está en la horizontal, en la posición de las tres de la tarde. El arco se dibuja en sentido contrario a las manecillas del reloj si angBarrido es positivo, y en el sentido de éstas si es negativo. Por tanto, para dibujar un arco de las 12 a la 6, en una hipotética carátula de reloj, el ángulo inicial sería 90 y el ángulo de barrido sería 180. Hay una opción al uso de paint( ) y repaint( ) para manejar la salida a una ventana. La salida puede completarse al obtener un contexto gráfico al llamar a getGraphics( ), definido por Component, y luego usar este contexto para dar salida a la ventana. Sin embargo, esta opción debe usarse con cuidado porque estará pintando en la ventana de una manera que ni Swing ni AWT pueden controlar. Por tanto, pueden ocurrir conflictos (y probablemente ocurrirán). Por lo general es mejor, más seguro y fácil enrutar la salida de la ventana a través de paint( ), como se ilustró en la solución. www.fullengineeringbook.net Capítulo 6: Applets y Servlets 275 Pase parámetros a applets Componentes clave Clases Métodos java.applet.Applet String getParameter(String nombreParam) A menudo, es útil pasar uno o más parámetros a una applet. Por ejemplo, podrían usarse parámetros para configurar la applet o pasar información proporcionada por el diseñador de la página Web. Cualquiera que sea el propósito, es fácil pasar parámetros a una applet. En esta solución se muestra el proceso. Paso a paso Para pasar un parámetro a una applet, se requieren los siguientes pasos: 1. En la etiqueta APPLET que invoca la applet, use PARAM para especificar los parámetros que quiere pasar a la applet. 2. Dentro de la applet, llame a getParameter( ) para obtener el valor de un parámetro dado su nombre. 3. Dentro de la applet, convierta los parámetros numéricos de su representación de cadena a su representación binaria. Análisis Para pasar un parámetro a una applet, debe incluir el atributo PARAM en la etiqueta APPLET que invoca a la applet. He aquí la forma general: <PARAM name=nombreParam valor=valorParam> Aquí, nombreParam es el nombre del parámetro y valorParam es su valor. Todos los parámetros se pasan como cadenas. Dentro de la applet, use getParameter( ) para obtener el parámetro. Aquí se muestra: String getParameter(String nombreParam) Devuelve el valor del parámetro pasado en nombreParam. Si no se encuentra el parámetro, se devuelve null. Debido a que todos los parámetros se pasan como cadenas, necesitará convertir manualmente los parámetros numéricos a su formato binario. Una manera de hacer esto es usar uno de los métodos estáticos de parse… definidos por las envolturas de tipo numéricos, como Integer y Double. Por ejemplo, para obtener un valor int, use Integer.parseInit( ). Para obtener un valor double, use Double.parseDouble( ). Aquí se muestran: static int parseInt(String cad) throws NumberFormatException static int parseDouble(String cad) throws NumberFormatException www.fullengineeringbook.net 276 Java: Soluciones de programación La cadena pasada en cad debe representar un valor numérico para el formato deseado. Se lanza una NumberFormatException si cad no contiene una cadena de valor numérico. Ejemplo En el siguiente ejemplo se muestra cómo pasar parámetros a una applet. Usa dos parámetros. El primero es nombreUsuario, que contiene el nombre del usuario, y el segundo es numCuenta, que contiene un número de cuenta entero. El comentario en la parte superior del programa muestra una etiqueta APPLET de ejemplo que pasa estos parámetros a la applet. Dentro de la applet, se recupera el parámetro y el número de cuenta se convierta en su formato int. // Una applet de Swing que usa parámetros. import javax.swing.*; import java.awt.*; /* <applet code="AppletParam" width=320 height=60> <param name=NombreUsuario value=Jorge> <param name=NumCuenta value=12345> </applet> */ public class AppletParam extends JApplet { JLabel jetq; int numCuenta; String usuario; // Inicializa la applet. public void init( ) { // Obtiene los parámetros. usuario = getParameter("NombreUsuario"); if(usuario == null) usuario = "Desconocido"; // Los números se pasan como cadenas. Debe convertirlos // manualmente a su formato binario. try { numCuenta = Integer.parseInt(getParameter("NumCuenta")); } catch(NumberFormatException exc) { numCuenta = –1; } try { SwingUtilities.invokeAndWait(new Runnable ( ) { public void run( ) { makeGUI( ); } }); www.fullengineeringbook.net Capítulo 6: Applets y Servlets 277 } catch(Exception exc) { System.out.println("No puede crearse porque "+ exc); } } // Inicializa la GUI. private void makeGUI( ) { // Crea la etiqueta que desplegará los parámetros // pasados a esta applet. jetq = new JLabel("Nombre del usuario: " + usuario + ", Número de cuenta: " + numCuenta, SwingConstants.CENTER); // Agrega la etiqueta al panel de contenido de la applet. add(jetq); // // // // // // Nota: si está usando una versión de Java anterior a JDK 5, entonces necesitará usar getContentPane( ) para establecer explícitamente el diseño del panel, de contenido, como se muestra aquí: getContentPane( ).add(jetq); } } Aquí se muestra la salida: Opciones En el ejemplo, si no se encuentra un parámetro o no es válido, entonces se usa un valor predeterminado. En algunos casos querrá pedir al usuario que ingrese la información faltante cuando empieza la applet. De esta manera, la applet puede usarse en casos en que los valores de los parámetros se conocen de antemano, y cuando no se conocen. Cuando se pasa una cadena que contiene espacios, puede incluir esa cadena dentro de comillas. Por ejemplo, esto especifica que el nombre del usuario es Jorge Contreras: <param name=NombreUsuario value="Jorge Contreras"> www.fullengineeringbook.net 278 Java: Soluciones de programación Use AppletContext para desplegar una página Web Componentes clave Clases Métodos java.applet.Applet AppletContext getAppletContext( ) java.applet.AppletContext showDocument(URL url) En esta solución se muestra cómo usar una applet para desplegar una página Web. Podría usar este tipo de applet para elegir entre varias opciones, y luego desplegar la página que seleccionó. Para ello, obtendrá AppletContext de la applet y luego usará su método showDocument( ) para desplegar la página. Paso a paso Para desplegar una página Web de una applet, siga estos pasos: 1. Obtenga una referencia al AppletContext asociado a la applet al llamar a getAppletContext( ). 2. Construya un objeto de URL que represente la página deseada. 3. Usando la AppletContext, llame a showDocument( ), especificando el URL. Análisis Para obtener el AppletContext de una applet, debe llamar a getAppletContext que se muestra aquí: AppletContext getAppletContext( ) Devuelve una referencia al AppletContext asociado con la applet que invoca. Como se explicó en Revisión general de las applets, AppletContext encapsula información acerca del entorno de ejecución de la applet. Construya una instancia de URL que describa la página que quiera desplegar. URL define varios constructores. El usado aquí es URL(String url) throws MalformedURLException Aquí url debe especificar un URL válido, incluido el protocolo. Si no, se lanza una MalformedURLException. Para desplegar la página, llame a showDocument( ), que se muestra aquí, en el AppletContext, pasando el URL deseado: void showDocument(URL url) Aquí, url especifica la página que se desplegará. Ejemplo En el siguiente ejemplo se muestra cómo desplegar una página Web a partir de una applet. Permite al usuario elegir entre dos sitios Web, ambos pasados como parámetros. (Consulte la www.fullengineeringbook.net Capítulo 6: Applets y Servlets 279 solución anterior para conocer información sobre el paso de parámetro a applets). Para desplegar la página, el usuario debe oprimir el botón Mostrar página ahora. Esto causa que se invoque a showDocument( ), que da como resultado que el explorador vaya a la página especificada. Otro tema: observe que se usa showStatus( ), que está definido por Applet, para dar salida a un mensaje de error en la ventana de estatus del explorador si el URL es erróneo. La ventana de estatus puede ser útil para desplegar retroalimentación para el usuario. Sin embargo, su comportamiento puede diferir entre exploradores. NOTA Este ejemplo debe ejecutarse dentro de un explorador que esté en línea, y no con appletviewer. // // // // // // // // Una applet de Swing que usa el método showDocument( ) definido por AppletContext para desplegar una página Web. Toma dos parámetros. El primero especifica el URL del sitio Web primario y el segundo el URL de un sitio Web secundario. El usuario selecciona cuál sitio se despliega al marcar o desmarcar una casilla de verificación. Al oprimir el botón Mostrar página ahora se despliega la página. import import import import javax.swing.*; java.awt.*; java.awt.event.*; java.net.*; /* <applet code=»MostrarURL» width=220 height=100> <param name=sitioPrimario value=HerbSchildt.com> <param name=sitioSecundario value=McGrawHill.com> <param name=default value=0> </applet> */ public class MostrarURL extends JApplet { JLabel jetq; JCheckBox jcvPrimaria; String primario; String secundario; // Inicializa la applet. public void init( ) { primario = getParameter("sitioPrimario"); secundario = getParameter("sitioSecundario"); try { SwingUtilities.invokeAndWait(new Runnable ( ) { public void run( ) { makeGUI( ); } www.fullengineeringbook.net 280 Java: Soluciones de programación }); } catch(Exception exc) { System.out.println("No se puede crear porque "+ exc); } } // Inicializa la GUI. private void makeGUI( ) { // Establece el diseño del panel de contenido en diseño de flujo. setLayout(new FlowLayout( )); // // // // // // Nota: si está usando una versión de Java anterior a JDK 5, entonces necesitará usar getContentPane( ) para establecer explícitamente el diseño del panel de contenido, como se muestra aquí: getContentPane( ).setLayout(new FlowLayout( )); // Crea la etiqueta que desplegará el sitio Web // de destino. jetq = new JLabel("Transferir a " + primario); // Crea una casilla de verificación. jcvPrimaria = new JCheckBox("Use el sitio primario", true); // Maneja los sucesos de elemento de la casilla de verificación. jcvPrimaria.addItemListener(new ItemListener( ) { public void itemStateChanged(ItemEvent ie) { // Intercambia entre los sitios primario y secundario. if(jcvPrimaria.isSelected( )) jetq.setText("Transferir a " + primario); else jetq.setText("Transferir a " + secundario); } }); // Agrega un botón que transfiere al sitio Web seleccionado. JButton jbtnSaltar = new JButton("Mostrar página ahora"); // Crea el escucha de acción para el botón. jbtnSaltar.addActionListener( new ActionListener( ) { public void actionPerformed(ActionEvent ae) { showStatus("Transfiriendo a sitio seleccionado."); // Transfiere al sitio deseado. try { if(jcvPrimaria.isSelected( )) getAppletContext( ).showDocument( new URL("http://www." + primario)); www.fullengineeringbook.net Capítulo 6: Applets y Servlets 281 else getAppletContext( ).showDocument( new URL("http://www." + secundario)); } catch(MalformedURLException exc) { showStatus("Error en URL."); } } }); // Agrega la etiqueta, las casillas de verificación // y el botón al panel de contenido de la applet. add(jcvPrimaria); add(jetq); add(jbtnSaltar); // // // // // // // // Nota: si está usando una versión de Java anterior a JDK 5, entonces necesitará usar getContentPane( ) para establecer explícitamente el diseño del panel de contenido, como se muestra aquí: getContentPane( ).add(jcvPrimaria); etcétera. } } Aquí se muestra salida que se despliega en un explorador: Opciones La versión de showDocument( ) que se usa en el ejemplo le permite al explorador decidir cómo desplegar la nueva página, pero usted puede controlar esto. Para especificar la manera en que se desplegará la nueva página, utilice esta forma de showDocument( ): void showDocument(URL, url, String donde) Aquí, donde determina dónde se desplegará la página Web. Argumentos válidos para donde son "_self" (mostrar en el marco actual), "_parent" (mostrar en el marco principal), "_top" (mostrar en el marco superior) y "_blank" (mostrar en una nueva ventana). También puede especificar un nombre, lo que causa que el documento se muestre en una ventana con ese nombre. Si esa ventana aún no existe, se creará. www.fullengineeringbook.net 282 Java: Soluciones de programación Cree una servlet simple usando GenericServlet Componentes clave Clases e interfaces Métodos javax.servlet.GenericServlet void destroy( ) void init(ServletConfig sc) void service(ServletRequest srq, ServletResponse srp) javax.servlet.ServletResponse PrintWriter getWriter( ) void setContentType(String tipoCont) javax.servelet.ServletRequest String getServerName( ) En esta solución se crea una servlet simple al extender GenericServlet. Como se explicó en Revisión general de las servlets, todas las servlets deben implementar la interfaz Servlet. GenericServlet facilita esto porque proporciona implementaciones predeterminadas de todos los métodos definidos por Servlet, con la excepción del método service( ). Por tanto, al extender GenericServlet, puede crear una servidor sin que tenga que implementar todos los métodos requeridos. Paso a paso Para crear una servlet basada en GenericServlet, siga estos pasos: 1. Cree una clase que extienda GenericServlet. 2. Sobrescriba el método de ciclo de vida service( ). GenericServlet no proporciona implementación predeterminada para service( ) porque ésta es específica de cada servlet. También es posible que necesite sobreescribir init( ) para inicializar la servlet, y destroy( ), para liberar recursos cuando esté desactivada la servlet. Sin embargo, se proporcionan las versiones predeterminadas de estos métodos si no se requieren acciones de inicialización o terminación. 3. Dentro de service( ), maneje solicitudes al devolver una respuesta. Información acerca de las solicitudes está disponible mediante un objeto de ServletRequest. La respuesta se devuelve mediante un objeto de ServletResponse. El ejemplo responde a solicitudes al escribir en el flujo de salida vinculado al parámetro de respuesta. El flujo de salida se obtiene al llamar a getWriter( ). Antes de responder, puede establecer el tipo de contenido al llamar a setContentType( ). Análisis GenericServlet proporciona implementaciones predeterminadas de ServletConfig y la mayor parte de Servlet. El único método que no implementa es service( ), porque sus acciones están determinadas por las necesidades y la funcionalidad de su servlet. Por tanto, cuando use GenericServlet, siempre proporcionará una implementación de service( ). Aquí se muestra el método service( ): void service(ServletRquest srq, ServletResponse srp) throws ServletException, IOException www.fullengineeringbook.net Capítulo 6: Applets y Servlets 283 El primer argumento es un objeto de ServletRequest que permite a la servlet leer datos proporcionados en la solicitud del cliente. El segundo argumento es un objeto de ServletResponse. Esto le permite a la servlet formular una respuesta para el cliente. Lanza una ServletException si ocurre un error de servlet. Lanza una IOException si ocurre un error de E/S. Las interfaces ServletRequest y ServletResponse proporcionan métodos que le permiten obtener información acerca de la solicitud o proporcionar una respuesta. Una muestra de esos métodos se mostró en las tablas 6-2 y 6-3, en Revisión general de las servlets, al principio de este capítulo. En el ejemplo que sigue se usan tres de estos métodos. El primero es getServerName( ). Es definido por ServletRequest y se muestra a continuación: String getServerName( ) Devuelve el nombre del servidor. Los otros dos, getWriter( ) y setContentType( ), son definidos por ServletResponse. Aquí se muestran: PrintWriter getWriter( ) throws IOException void setContentType(String tipoCad) El método getWriter( ) devuelve una referencia a un flujo en el que puede escribirse una respuesta. Cualquier cosa escrita en ese flujo se envía al cliente como parte de la respuesta. Lanza una IOException si ocurre un error de E/S mientras trata de obtener el flujo. El método setContentType( ) establece el tipo de contenido (tipo MIME) de la respuesta. En el ejemplo, el tipo de contenido es text/html. Esto indica que el explorador debe interpretar el contenido como HTML. GenericServlet proporciona implementación predeterminada de los otros dos métodos del ciclo de vida, init( ) y destroy( ), pero usted puede sobreescribir éstos de acuerdo con las necesidades de su aplicación. Por supuesto, si su servlet no necesita inicializar ni liberar recursos, entonces por lo general no habrá razón para sobreescribir esos métodos. Ejemplo A continuación se presenta un esqueleto de servlet. Sobreescribe los tres métodos del ciclo de vida, y devuelve una respuesta cuando se llama a su método service( ). Esta respuesta es la misma para todas las solicitudes: simplemente devuelve un mensaje que muestra a cuáles métodos del ciclo de vida se ha llamado y el nombre del servidor. Sin embargo, su propia servlet proporcionará implementaciones apropiadas para su aplicación. NOTA Para conocer instrucciones sobre el uso de Tomcat para ejecutar una servlet, consulte Uso de Tomcat para desarrollo de servlets. // Una servlet simple. import java.io.*; import javax.servlet.*; public class EsqueletoServlet extends GenericServlet { String msj = «»; www.fullengineeringbook.net 284 Java: Soluciones de programación // Se le llama una vez, al inicio. public void init(ServletConfig sc) { msj = "Servlet inicializada."; } // Se le llama una vez, cuando se elimina la servlet. public void destroy( ) { msj += " Esto no se verá."; } // Se le llama varias veces para manejar las solicitudes. public void service(ServletRequest solicitud, ServletResponse respuesta) throws ServletException, IOException { // Establece el tipo de contenido. respuesta.setContentType("text/html"); // Obtiene el flujo de respuesta. PrintWriter pw = respuesta.getWriter( ); // Muestra el método service( ) al que se ha llamado // y despliega el nombre del servidor. msj += "<br>Dentro de service( ). Servidor: " + solicitud.getServerName( ); pw.println(msj); pw.close( ); } } La salida desplegada en el explorador se muestra aquí: Servlet inicializada. Dentro de service( ). Servidor: localhost Observe que el nombre del servidor es localhost. Esto se debe a que la servlet está ejecutándose en el mismo equipo que el explorador. Además, observe que no se muestra el mensaje agregado por destroy( ). Como lo indica el comentario dentro de destroy( ), no verá la cadena agregada por destroy( ), porque es el último método llamado cuando se ha eliminado la servlet. Por tanto, no se llama a service( ) después de que se ha llamado a destroy( ). Opciones Como se explicó, cuando se extiende GenericServlet, no se requiere sobreescribir init( ) o destroy( ) si no son necesarios. Se proporcionan las implementaciones predeterminadas. Para crear una servlet que maneja varias solicitudes de HTTP, como GET o POST, suele ser más conveniente extender HttpServlet que GenericServlet. En la siguiente solución se muestra cómo. www.fullengineeringbook.net Capítulo 6: Applets y Servlets 285 Maneje solicitudes HTTP en una servlet Componentes clave Clases e interfaces Métodos javax.servlet.http .HttpServlet void doGet(HttpServletRequest hsreq, HttpServletResponse hsrep) javax.servlet.http .HttpServletRequest String getParameter(String nombreParam) javax.servlet.http .HttpServletResponse PrintWriter getWriter( ) Si está creando una servlet que está respondiendo a solicitudes HTTP, como GET o POST, entonces por lo general querrá que su servlet extienda HttpServlet en lugar de GenericServlet. La razón es que HttpServlet proporciona métodos que manejan las diversas solicitudes HTTP. HttpServlet extiende GenericServlet y está empaquetada en javax.servlet.http. Paso a paso Para crear una servlet basada en HttpServlet, siga estos pasos: 1. Cree una clase que extienda HttpServlet. 2. Si es necesario, sobrescriba los métodos del ciclo de vida init( ) y destroy( ). Sin embargo, no sobrescriba service( ), porque es implementado por HttpServlet y enruta automáticamente solicitudes HTTP a los manejadores definidos por HttpServlet. 3. Sobrescriba el manejador o los manejadores necesarios para su servidor. El manejador usado en esta solución es doGet( ). Maneja solicitudes GET. Se dispone de información acerca de las solicitudes a través de un objeto de HttpServletRequest. La respuesta se devuelve vía un objeto de HttpServletResponse. En el ejemplo se responde a solicitudes al escribir en el flujo de salida vinculado con el parámetro de respuesta. El flujo de salida se obtiene al llamar a getWriter( ). Antes de responder, puede establecer el tipo de contenido al llamar a setContentType( ). 4. Puede obtener el valor de un parámetro asociado con una solicitud al llamar a getParameter( ) en el objeto de HttpServletRequest. Análisis HttpServlet define varios métodos de do…, que manejan varias solicitudes HTTP. El usado en esta solución es doGet( ). Maneja solicitudes GET y se muestra a continuación: void doGet(HttpServletRequest hsreq, HttpServletResponse hsrep) throws IOException, ServletException www.fullengineeringbook.net 286 Java: Soluciones de programación Se le llama cuando se recibe una solicitud de GET. Está disponible información acerca de la solicitud mediante hsreq. Para responder, se usa hsrep. Se lanza una IOException si ocurre un error de E/S mientras se maneja la solicitud. Se lanza una ServletException si falla la solicitud. HttpServletRequest extiende ServletRequest y agrega soporte a solicitudes HTTP. HttpServletResponse extiende ServletResponse y agrega soporte a respuestas HTTP. Para obtener el valor de un parámetro asociado con la solicitud, llame a getParameter( ). Este método se hereda de ServletRequest y se muestra aquí: String getParameter(String nombreParam) El nombre del parámetro se pasa en nombreParam. Se devuelve el valor (en forma de cadena). Si no se encuentra el parámetro, se devuelve null. Para conocer un análisis de setContentType( ) y getWrite( ), consulte Cree una servlet simple usando GenericServlet. Ejemplo En el siguiente ejemplo se crea una servlet que usa el teorema de Pitágoras para calcular la longitud de la hipotenusa dada la longitud de los dos lados opuestos de un triángulo recto. Las longitudes de los dos lados se pasan en parámetros. NOTA Para conocer instrucciones sobre el uso de Tomcat para ejecutar una servlet, consulte Uso de Tomcat para desarrollo de servlets. // Esta servlet calcula la longitud de la hipotenusa // dada la longitud de los dos lados opuestos. import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ServletHipot extends HttpServlet { public void doGet(HttpServletRequest solicitud, HttpServletResponse respuesta) throws ServletException, IOException { // Obtiene los parámetros que contienen las // longitudes de los dos lados. String lado1 = solicitud.getParameter("primerlado"); String lado2 = solicitud.getParameter("segundolado"); // Establece el tipo de contenido y obtiene un // flujo para la respuesta. respuesta.setContentType("text/html"); PrintWriter pw = respuesta.getWriter( ); try { double a, b; a = Double.parseDouble(lado1); b = Double.parseDouble(lado2); pw.println(«La hipotenusa es « + Math.sqrt(a*a + b*b)); } catch(NumberFormatException exc) { pw.println(«Datos no válidos»); www.fullengineeringbook.net Capítulo 6: Applets y Servlets 287 } pw.close( ); } } El siguiente HTML presenta un formulario que pide al usuario la longitud de los dos lados e invoca a ServletHipot para calcular y desplegar el resultado. Por tanto, para usar la servlet, primero cargue el siguiente HTML en su explorador y luego ingrese la longitud de ambos lados. Después, oprima el botón Calcular. Esto causa que se invoque ServletHipot. Calcula la longitud de la hipotenusa y despliega el resultado: <html> <body> <left> <form name="Form1" action="http://localhost:8080/examples/servlet/ServletHipot"> Calcula la hipotenusa <br><br> Ingrese la longitud del lado uno: <input type=textbox name = "primerlado" size=12 value=""> <br> Ingrese la longitud del lado dos: <input type=textbox name = "segundolado" size=12 value=""> <br><br> <input type=submit value="Calcular"> </form> </body> </html> En las siguientes figuras se muestra el HTML que pide al usuario la longitud de los lados y el resultado cuando se ejecuta dentro de un explorador. Ejemplo adicional Aunque en el ejemplo anterior se demuestra una servlet de HTTP y el uso de parámetros, es posible mejorarlo de dos maneras. En primer lugar, no es necesario tener un archivo HTML separado que invoque a ServletHipot. En cambio, la propia servlet puede desplegar una página que pida al usuario la longitud de los lados. En segundo lugar, la longitud de la hipotenusa puede desplegarse www.fullengineeringbook.net 288 Java: Soluciones de programación en la misma página, permitiendo al usuario calcular fácilmente la longitud de la hipotenusa para otros triángulos. En la siguiente versión de ServletHipot se implementa este método. NOTA Para conocer instrucciones sobre el uso de Tomcat para ejecutar una servlet, consulte Uso de Tomcat para desarrollo de servlets. // // // // // Una versión mejorada de ServletHipot. Esta versión mejora la mostrada en el ejemplo anterior de dos maneras. Primero, despliega el HTML necesario para ingresar la longitud de los lados y ejecutar la servlet. En segundo lugar, despliega el resultado en la misma página. import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ServletHipot extends HttpServlet { public void doGet(HttpServletRequest solicitud, HttpServletResponse respuesta) throws ServletException, IOException { // Obtiene los parámetros que contienen las longitudes // de los dos lados. String lado1 = solicitud.getParameter("primerlado"); String lado2 = solicitud.getParameter("segundolado"); // Esta cadena contendrá la longitud calculada // de la hipotenusa. String hipot; // Si falta un parámetro, entonces establece // las cadenas en una cadena nula. if(lado1 == null | lado2 == null) { lado1 = ""; lado2 = ""; hipot = ""; } else { // Calcula la hipotenusa. try { double a, b, h; a = Double.parseDouble(lado1); b = Double.parseDouble(lado2); h = Math.sqrt(a*a + b*b); hipot = «» + h; } catch(NumberFormatException exc) { hipot = "Datos no válidos"; } } www.fullengineeringbook.net Capítulo 6: Applets y Servlets 289 // Establece el tipo de contenido y obtiene un // flujo para la respuesta. respuesta.setContentType("text/html"); PrintWriter pw = respuesta.getWriter( ); // Despliega el formulario HTML. pw.print("<html> <body> <left>" + "<form name=\"Form1\"" + "action=\"http://localhost:8080/" + "examples/servlet/ServletHipot\">" + "Calcula la hipotenusa<br><br>" + "Ingrese la longitud del lado uno: " + "<input type=textbox name = " + "\"primerlado\" size=12 value=\"" + lado1 + "\">" + "<br>Ingrese la longitud del lado dos: " + "<input type=textbox name = " + "\"segundolado\" size=12 value=\"" + lado2 +"\"><br><br>" + "<input type=submit value=\"Calcular\">" + "</form>" + "Longitud de la hipotenusa: " + "<input READONLY type=textbox name = " + "\"hipot\" size=20 value=\"" + hipot +"\"> </body> </html>"); pw.close( ); } } La salida de ejemplo se muestra aquí: Observe cómo doGet( ) pide automáticamente al usuario las longitudes de los lados, si no son parte de la solicitud (como no lo serán cuando la servlet se ejecuta por primera vez). Si lado1 o lado2 es nulo, significa que falta uno de los dos parámetros. Esto hace que lado1, lado2 e hipot (el resultado) tomen el valor de cadena nula. De otra manera, se calcula la hipotenusa. Luego, se envía el HTML que incluye los indicadores de petición y los cuadros de texto de resultado. Si lado1, lado2 e hipot son cadenas nulas, entonces los cuadros de texto están vacíos, y el usuario ingresará las longitudes. De otra manera, se desplegarán éstas, junto con el resultado. Observe algo más: el cuadro de texto que despliega el resultado es de sólo lectura. Esto significa que el usuario no puede ingresar una cadena en él. www.fullengineeringbook.net 290 Java: Soluciones de programación Opciones Además de doGet( ), HttpServlet proporciona manejadores para otras varias solicitudes HTTP. Por ejemplo, puede usar doPost( ) para manejar una solicitud POST y doPut( ) para manejar una PUT. Cuando se crea una HttpServlet, se sobreescriben los manejadores requeridos por su aplicación. Use una cookie con una servlet Componentes clave Clases Métodos javax.servlet.http.Cookie String getName( ) String getValue( ) void setMaxAge(int periodo) javax.servlet.http.HttpServletRequest Cookie[ ] getCookies( ) javax.servlet.http.HttpServletResponse void addCookie(Cookie ck) Las cookies son una parte importante de muchas aplicaciones Web. Por esto, las servlets les proporcionan un soporte sustancial. Por ejemplo, una servlet puede crear cookies y también puede leerlas. Las cookies son instancias de la clase Cookie. En esta solución se muestra cómo crear una cookie y luego obtener su valor. Paso a paso Para usar cookies con una servlet se requieren estos pasos: 1. Cree un objeto de Cookie que contenga el nombre y el valor que quiera dar a la cookie. Todos los nombres y valores se representan como cadenas. 2. Para guardar una cookie, llame a addCookie( ) en el objeto de HttpServletResponse. 3. Para recuperar una cookie, primero obtenga una matriz de las cookies asociadas con una solicitud al llamar a getCookies( ) en el objeto de HttpServletRequest. Luego, busque la cookie cuyo nombre coincide con el que está buscando. Use getName( ) para obtener el nombre de cada cookie. Por último, obtenga el valor de la cookie al llamar a getValue( ). Análisis En una servlet, una cookie está encapsulada por la clase Cookie. Define un constructor, que se muestra aquí: Cookie(String nombre, String valor) Aquí, nombre especifica el nombre de la cookie y valor su valor. www.fullengineeringbook.net Capítulo 6: Applets y Servlets 291 Como opción predeterminada, se elimina una instancia de Cookie del explorador cuando se termina éste. Sin embargo, puede hacer que una cookie persista al establecer una edad máxima al llamar a setMaxAge( ): void setMaxAge(int periodo) La cookie persistirá hasta que periodo segundos hayan transcurrido. Por ejemplo, si quiere que una cookie permanezca durante 24 horas, pase 86,400 a setMaxAge( ). Para agregar una cookie, llame a addCookie( ) en el objeto HttpServletResponse, que se muestra aquí: void addCookie(Cookie ck) La cookie que se agregará se pasa vía ck. Para obtener una matriz de las cookies asociadas con una solicitud, llame a getCookies( ). Aquí se muestra: Cookie[ ] getCookies( ) Si no hay cookies vinculadas con la solicitud, se devuelve null. Dada una Cookie, puede obtener su nombre al llamar a getName( ). Su valor se obtiene al llamar a getValue( ). Aquí se muestran estos métodos: String getName( ) String getValue( ) Por tanto, con el uso de la matriz devuelta por getCookies( ), puede buscar una cookie específica por nombre al llamar a getName( ) en cada cookie, hasta que se encuentre una cookie con el nombre correspondiente. Empleando la cookie, puede obtener su valor al llamar a getValue( ). Ejemplo En el siguiente ejemplo se ilustran las cookies con las servlets. Se crea una servlet llamada ServletCookie que primero busca una cookie llamada "quien". Si la encuentra, usa el nombre vinculado con la cookie para desplegar un mensaje de bienvenida que incluye el nombre. Por ejemplo, si su nombre es Juan, entonces se despliega este mensaje: Hola, Juan. Me da gusto verte de nuevo. Sin embargo, si no se encuentra la cookie, entonces se le pide que ingrese un nombre y se crea la cookie "quien", empleando el nombre. La próxima vez que se ejecute la servlet, se encontrará la cookie. Tal como está escrito el ejemplo, la cookie persiste por sólo 60 segundos, de modo que debe volver a ejecutar ServletCookie dentro de un lapso de 60 segundos para encontrar la cookie. Más aún, necesitará salir y reiniciar el explorador, o usar la opción del explorador de actualizar/recargar entre ejecuciones para que la servlet vuelva a ejecutarse desde el principio. Observe que la servlet maneja explícitamente dos solicitudes HTTP: POST y GET. Cuando se recibe una solicitud GET, la servlet trata de recuperar la cookie "quien". Si no se encuentra, entonces se despliega un HTML que la pide al usuario. Cuando se recibe una solicitud POST, se crea una nueva cookie que contiene el nombre del usuario y que se agrega a la respuesta al llamar a addCookie( ). www.fullengineeringbook.net 292 Java: Soluciones de programación NOTA Para conocer instrucciones sobre el uso de Tomcat para ejecutar una servlet, consulte Uso de Tomcat para desarrollo de servlets. // // // // // // // Con este ejemplo se demuestra el uso de una cookie. Cuando se ejecuta por primera vez, no encuentra una cookie llamada "quien", Pide al usuario un nombre y luego crea una cookie llamada "quien" que contiene el nombre. Si no se encuentra la cookie, entonces se usa el nombre para mostrar un mensaje de bienvenida. // // // // // Nota: la cookie "quien" sólo persiste durante 60 segundos. Por tanto, debe ejecutar CookieServlet dos veces dentro de 60 segundos. Necesita reiniciar su explorador entre ejecuciones de CookieServlet, o usar Actualizar/Recargar. import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ServletCookie extends HttpServlet { // Recupera una cookie. public void doGet(HttpServletRequest solicitud, HttpServletResponse respuesta) throws ServletException, IOException { // Inicializa cliente en null. String cliente = null; respuesta.setContentType("text/html"); PrintWriter pw = respuesta.getWriter( ); // Obtiene cookies del encabezado de la solicitud HTTP. Cookie[ ] cookies = solicitud.getCookies( ); // Si hay cookies presentes, busca la cookie "quien". // Contiene el nombre del cliente. if(cookies != null) for(int i = 0; i < cookies.length; i++) { if(cookies[i].getName( ).equals("quien")) { cliente = cookies[i].getValue( ); pw.println("Hola, " + cliente + "." + " Me da gusto verte de nuevo."); } } // De otra manera, pide al cliente un nombre. if(cliente == null) { pw.print("<html> <body> <left>" + "<form name=\"Form1\"" + "method=\"post\" "+ www.fullengineeringbook.net Capítulo 6: Applets y Servlets 293 "action=\"http://localhost:8080/" + "examples/servlet/ServletCookie\">" + "Por favor, ingrese su nombre: " + "<input type=textbox name = " + "\"nombreCliente\" size=40 value=\"\"" + "<input type=submit value=\"Submit\">" + "</form> </body> </html>"); } pw.close( ); } // Crea una cookie. public void doPost(HttpServletRequest solicitud, HttpServletResponse respuesta) throws ServletException, IOException { // Obtiene el parámetro nombreCliente. String cliente = solicitud.getParameter("nombreCliente"); // Crea una cookie llamada "quien" que contiene // el nombre del cliente. Cookie cookie = new Cookie("quien", cliente); // La cookie persiste durante 60 segundos. cookie.setMaxAge(60); // Agrega una cookie a la respuesta HTTP. respuesta.addCookie(cookie); respuesta.setContentType("text/html"); PrintWriter pw = respuesta.getWriter( ); pw.println("Hola " + cliente + "."); pw.close( ); } } Opciones Hay varias opciones de cookies que podrían resultarle útiles. Para obtener la edad máxima de una cookie, llame a getMaxAge( ). Para asociar un comentario con una cookie, llame a setComment( ). Para recuperar el comentario, llame a getComment( ). Para establecer el valor de una cookie después de que la ha creado, llame a setValue( ). Las sesiones están de alguna manera relacionadas con las cookies, en un sentido conceptual. Pueden usarse para guardar información de estado. Una sesión está encapsulada por la clase HttpSession, que define métodos como getAttribute( ) y setAttribute( ), que se usan para establecer o recuperar información de estado. La sesión actual puede obtenerse (o crearse) al llamar a getSession( ) definida por HttpServletRequest. www.fullengineeringbook.net www.fullengineeringbook.net 7 CAPÍTULO Multiprocesamiento E ntre las características definitorias de Java están su soporte integrado para programación con multiprocesamiento. Este soporte, que ha estado presente en Java desde el principio, es proporcionado por la clase Thread, la interfaz Runnable, varios métodos proporcionados por Object y la palabra clave synchronized. El multiprocesamiento le permite escribir programas para obtener dos o más rutas de ejecución separadas que pueden ejecutarse al mismo tiempo. A cada ruta de ejecución se le denomina subproceso. Mediante el uso cuidadoso del multiprocesamiento, puede crear programas que usen de manera eficiente los recursos del sistema y mantengan una interfaz de usuario que responda activamente. Debido a que varios subprocesos pueden interactuar de maneras que no son siempre intuitivas, agregando un nivel de complejidad que no está presente en un programa de un solo proceso, algunos programadores evitan el multiprocesamiento cada vez que es posible. Sin embargo, el mundo de la programación moderna esta avanzando hacia el uso del multiprocesamiento, sin duda. Las arquitecturas altamente paralelas se están volviendo la norma. Para decirlo de manera simple, el multiprocesamiento seguirá jugando un papel crítico en muchas aplicaciones reales de Java (tal vez la mayor parte de ellas). En este capítulo se encuentran varias soluciones que muestran cómo crear y manejar subprocesos y el entorno de multiprocesamiento. Se empieza por describir los procedimientos básicos necesarios para crear un subproceso. Luego se muestran técnicas clave de multiprocesamiento, como la sincronización de subprocesos, el establecimiento de prioridades y la comunicación entre subprocesos. También se ilustra el uso de los subprocesos de daemon, los interruptores de subprocesos y la manera de monitorear el estatus de un subproceso. He aquí las soluciones de este capítulo: • Cree un subproceso al implementar Runnable. • Cree un subproceso al extender Thread. • Use el nombre y el ID de un subproceso. • Espere a que termine un subproceso. • Sincronice subprocesos. • Establezca comunicación entre subprocesos. • Suspenda, reanude y detenga un subproceso. • Use un subproceso de daemon. www.fullengineeringbook.net 295 296 Java: Soluciones de programación • Interrumpa un subproceso. • Establezca y obtenga una prioridad de subproceso. • Monitoree el estado de un subproceso. • Use un grupo de subprocesos. NOTA Una adición relativamente reciente a la API de Java son las utilerías de concurrencia, que están empaquetas en java.util.concurrent y sus subpaquetes. Por lo general se alude a ellas como la API concurrente. Agregada en Java 5, la API concurrente proporciona varios constructores de alto nivel que ayudan al desarrollo de programas de multiprocesamiento muy complejos. Por ejemplo, la API concurrente proporciona semáforos, cerrojos de conteo regresivo y futuros, para nombrar algunos. Aunque las utilerías de concurrencia no son el centro de este capítulo, tienen interés para los lectores que están desarrollando applets con gran cantidad de subprocesos. Fundamentos del multiprocesamiento En esencia, el multiprocesamiento es una forma de multitareas. Hay dos tipos distintos de multitareas, basada en procesos y basada en subprocesos. Es importante diferenciar entre los dos. En relación con este análisis, un proceso es, en esencia, un programa que se está ejecutando. Por tanto, la multitareas basada en procesos es la característica que le permite a su equipo ejecutar dos o más programas al mismo tiempo. Por ejemplo, es multitareas basada en procesos la que permite descargar un archivo al mismo tiempo que está compilando un programa, u ordenando una base de datos. En la multitareas basada en procesos, un programa es la unidad más pequeña de código que puede despachar el programador. En un entorno de multitareas basada en subprocesos, el subproceso es la unidad más pequeña de código despachable. Debido a que un programa puede contener más de un subproceso, un solo programa puede usar varios subprocesos para realizar dos o más tareas al mismo tiempo. Por ejemplo, un explorador puede empezar a generar una página Web mientras aún descarga el resto de la página. Esto es posible porque cada acción se realiza en un subproceso separado. Aunque los programas de Java usan los entornos de multitareas basada en procesos, ésta no se encuentra bajo el control directo de Java. El multiprocesamiento es multitareas. Todos los procesos tienen al menos un subproceso de ejecución, al que se le llama subproceso principal, porque es el que se ejecuta cuando el programa empieza. A partir del subproceso principal, puede crear otros subprocesos. Éstos otros pueden crear también subprocesos, etcétera. El multiprocesamiento es importante para Java por dos razones principales. En primer lugar, le permite escribir programas muy eficientes utilizando el tiempo de inactividad que está presente en la mayor parte de los programas. Casi todos los dispositivos de E/S, sean puertos de red, unidades de disco o el teclado, son mucho más lentos que la CPU. Por tanto, a menudo un programa gastará la mayor parte de su tiempo de ejecución esperando enviar información a un dispositivo o recibirla de éste. Al usar multiprocesamiento, su programa puede ejecutar otra tarea durante el tiempo de inactividad. Por ejemplo, mientras una parte de su programa está enviando un archivo en Internet, otra parte puede manejar la interacción con el usuario (como clics del ratón o la opresión de un botón), y otro más puede estar almacenando en búfer el siguiente bloque de datos que se enviará. La segunda razón por la que el multiprocesamiento es importante para Java se relaciona con el modelo de manejo de sucesos de Java. Un programa (como una applet) debe responder rápidamente a un suceso y luego regresar. Un manejador de sucesos no debe retener el control de la CPU por un periodo extenso. Si lo hace, no se manejarán otros sucesos de manera oportuna. Esto www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 297 hará que una aplicación parezca lenta. También es posible que un suceso se omita. Por tanto, si un suceso requiere alguna acción extendida, entonces debe realizarse en un subproceso separado. Un subproceso puede estar en uno de varios estados. Puede encontrarse en ejecución. Puede estar listo para ejecutarse en cuanto obtenga tiempo de la CPU. Un subproceso en ejecución puede suspenderse, lo que es una detención temporal de su ejecución. Luego puede reanudarse. Un subproceso puede ser bloqueado cuando espera un recurso. Un subproceso puede terminarse, en cuyo caso su ejecución termina y no puede reanudarse. Junto con la multitareas basada en subprocesos se incluye la necesidad de sincronización, que permite la ejecución de subprocesos que se coordinen de ciertas maneras bien definidas. Java tiene soporte extenso e integrado para la sincronización, lo que se logra mediante el uso de un monitor, incluido en todos los objetos, y la palabra clave synchronized. Por tanto, es posible sincronizar todos los objetos. Dos o más subprocesos pueden comunicarse entre sí mediante métodos definidos por Object. Estos métodos son wait( ), notify( ) y notifyAll( ). Permiten que una subproceso espere a otro. Por ejemplo, si un subproceso está usando un recurso compartido, entonces otro subproceso debe esperar hasta que se haya terminado el primer subproceso. El que espera puede reanudar la ejecución cuando el primer subproceso le notifique que el recurso está disponible. Hay dos tipos básicos de subprocesos: de usuario y de daemon. Un subproceso de usuario es el tipo creado como opción predeterminada. Por ejemplo, el subproceso principal es de usuario. En general, un programa continúa su ejecución, siempre y cuando haya por lo menos un subproceso de usuario activo. Es posible cambiar el estatus de un subproceso a daemon. Los subprocesos de daemon se terminan automáticamente cuando todos los subprocesos de daemon han terminado. Por tanto, están subordinados a los subprocesos de usuario. Los subprocesos pueden ser parte de un grupo. Un grupo de subprocesos le permite administrar subprocesos relacionados de manera colectiva. Por ejemplo, puede obtener una matriz de los subprocesos del grupo. El sistema de multiprocesamiento de Java está integrado a partir de la clase Thread y su interfaz que la acompaña, Runnable. Thread encapsula un subproceso de ejecución. Para crear un nuevo subproceso, su programa implementará la interfaz Runnable o extenderá Thread. Tanto Runnable como Thread se encuentran empaquetadas en java.lang. Por tanto, están automáticamente disponibles para todos los programas. La interfaz Runnable La interfaz java.lang.Runnable abstrae una unidad de código ejecutable. Puede construir un subproceso en cualquier objeto que implemente la interfaz Runnable. Por tanto, cualquier clase que pretenda ejecutar en un subproceso separado debe implementar Runnable. Runnable sólo define un método llamado run( ), que se declara así: void run( ) Dentro de run( ), definirá el código que constituye el nuevo subproceso. Es importante comprender que run( ) puede llamar a otros métodos, usar otras clases y declarar variables como el subproceso principal. La única diferencia es que run( ) establece el punto de entrada para otro subproceso de ejecución concurrente dentro de su programa. Este subproceso terminará cuando regrese run( ). Una vez que haya creado una instancia de una clase que implemente Runnable, puede crear un subproceso al construir un objeto de tipo Thread, pasándolo en la instancia de Runnable. Para empezar la ejecución del subproceso, llamará a start( ) en el objeto de Thread, como se describe en la siguiente sección. www.fullengineeringbook.net 298 Java: Soluciones de programación La clase Thread La clase Thread encapsula un subproceso. Está empaquetada en java.lang e implementa la interfaz Runnable. Por tanto, una segunda manera de crear un subproceso consiste en extender Thread y sobreescribir el método run( ). Thread también define varios métodos que ayudan a administrar subprocesos. He aquí las usadas en este capítulo: Método Significado static Thread currentThread( ) Devuelve una referencia a un objeto de Thread que representa el subproceso que invoca. long getID( ) Devuelve el ID de un subproceso. final String getName( ) Obtiene el nombre de un subproceso. final int getPriority( ) Obtiene la prioridad de un subproceso. Thread.State getState( ) Devuelve el estado actual del subproceso. static boolean holdsLock(Object obj) Devuelve verdadero si el subproceso que invoca contiene el bloqueo en obj. void interrupt( ) Interrumpe un subproceso. static boolean interrupted( ) Devuelve verdadero si el subproceso que invoca se ha interrumpido. final boolean isAlive( ) Determina si un subproceso aún se está ejecutando. final boolean isDaemon( ) Devuelve verdadero si el subproceso que invoca es de daemon. boolean isInterrupted( ) Devuelve verdadero si el subproceso en que se llama se ha interrumpido. final void join( ) Espera a que termine un subproceso. void run( ) Punto de entrada para el subproceso. final void setDaemon(boolean como) Si como es verdadero, el subproceso que invoca se establece en el estatus de daemon. final void setName(String nombreSubp) Establece el nombre de un subproceso en nombreSubp. final void setPriority(int nivel) Establece la prioridad de un subproceso en nivel. static void sleep(long milisegundos) Suspende un subproceso por un periodo especificado de milisegundos. void start( ) Inicia un subproceso al llamar a su método run( ). static void yield( ) Lleva la CPU a otro subproceso. Preste especial atención al método start( ). Después de que se ha credo una instancia de Thread, llame a start( ) para que empiece la ejecución del subproceso. El método start( ) llama a run( ), que es el método definido por Runnable que contiene el código que habrá de ejecutarse en el subproceso. Este proceso se describe con detalle en las siguientes soluciones. Otro método de especial interés es sleep( ). Suspende la ejecución de un subproceso por un periodo especificado. Cuando un subproceso queda inactivo, puede ejecutarse otro subproceso hasta que el inactivo reanude su ejecución. En varios ejemplos de este capítulo se usa sleep( ) para demostrar los efectos de subprocesos múltiples. www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 299 Thread define dos conjuntos de constructores, uno para construir un subproceso en una instancia separada de Runnable y el otro para construir un subproceso en clases que extienden Thread. He aquí los constructores que toman una instancia separada de Runnable. Thread(Runnable objSubp) Thread(Runnable objSubp, String nombreSubp) Thread(ThreadGroup grupoSubp, Runnable objSubp) Thread(ThreadGroup grupoSubp, Runnable objSubp, String nombreSubp) Aquí, objSubp es una referencia a una instancia de una clase que implementa Runnable. El método run( ) de este objeto contiene el código que se ejecutará como nuevo subproceso. El nombre del subproceso se pasa en nombreSubp. Si no se especifica un nombre (o si el nombre es null), entonces la JVM proporciona un nombre automáticamente. El grupo de subprocesos al que pertenece el subproceso (si lo hay) se pasa vía grupoSubp. Si no se especifica éste, entonces el administrador de seguridad (si lo hay) determina el grupo de subprocesos, o se establece en el mismo grupo que el subproceso que invoca. He aquí los constructores que crean un subproceso para clases que extienden Thread: Thread( ) Thread(String nombreSubp) Thread(ThreadGroup grupoSubp, String nombreSubp) El primer constructor crea un subproceso que usa el nombre predeterminado y el grupo de subprocesos, como ya se describió. El segundo le permite especificar el nombre. El tercero le permite especificar el grupo de subprocesos y el nombre. En el caso de ambos conjuntos de constructores, el subproceso se creará como uno de usuario, a menos que el subproceso que crea sea de daemon. En este caso, el subproceso se creará como de daemon. Hay otro constructor Thread que le permite especificar un tamaño de pila para el subproceso. Sin embargo, debido a los diferentes entornos de ejecución, la documentación de la API establece que "debe tenerse extremo cuidado con su uso". Por tanto, no se emplea en este libro. Cree un subproceso al implementar Runnable Componentes clave Clases e interfaces Métodos java.lang.Runnable void run( ) java.lang.Thread static void sleep(long milisegundos) void start( ) Tal vez la manera más común de construir un subproceso sea crear una clase que implemente Runnable y luego construir un Thread usando una instancia de esa clase. Si no estará sobrescribiendo ninguno de los métodos de Thread o extendiendo su funcionalidad de alguna manera, entonces la implementación de Runnable suele ser el mejor método. En esta solución se www.fullengineeringbook.net 300 Java: Soluciones de programación describe el proceso. El ejemplo también demuestra Thread.sleep( ), que suspende la ejecución de un subproceso por un periodo especificado. Paso a paso Para crear un subproceso mediante la implementación de Runnable, se requieren estos pasos: 1. Cree una clase que implemente la interfaz Runnable. Los objetos de esta clase pueden usarse para crear nuevos subprocesos de ejecución. 2. Dentro del método run( ) especificado por Runnable, coloque el código que quiera ejecutar en el subproceso. 3. Cree una instancia de la clase Runnable. 4. Cree un objeto de Thread, pasándolo en una instancia de Runnable. 5. Empiece la ejecución del subproceso al llamar a start( ) en la instancia de Thread. Análisis Como se explicó en Fundamentos del multiprocesamiento, Runnable especifica sólo un método, run( ), que se define así: void run( ) Dentro del cuerpo de este método, coloque el código que quiera que se ejecute en un subproceso separado. El subproceso seguirá su ejecución hasta que run( ) regrese. Para crear en realidad un subproceso, pase una instancia de Runnable a uno de los constructores de Thread. Aquí se muestra el usado en esta solución: Thread(Runnable objSubp, String nombreSubp) Aquí, objSubp es una instancia de una clase que implementa Runnable y nombreSubp especifica el nombre del subproceso. Para empezar la ejecución del subproceso, llame a start( ) en la instancia de Thread. Esto da como resultado una llamada a run( ) en el Runnable en que se construyó el subproceso. En el programa de ejemplo se usa Thread.sleep( ) para suspender temporalmente la ejecución de un subproceso. Cuando se vuelve inactivo un subproceso, otro puede ejecutarse. Por tanto, la inactividad hace que se renuncie a la CPU por un periodo específico. El método sleep( ) tiene dos formas. Aquí se muestra el usado en el ejemplo: static void sleep(long milisegundos) throws InterruptedException El número de milisegundos que se suspenderá se especifica en milisegundos. Este método puede lanzar una InterruptedException. Ejemplo En el siguiente ejemplo se muestran los pasos necesarios para crear y ejecutar un subproceso. Se define una clase llamada MiSubproceso que implementa Runnable. Dentro del método main( ) de DemoRunnable, se crea una instancia de MiSubproceso y se pasa a un constructor de Thread, que crea un nuevo subproceso de ejecución. Luego se inicia el subproceso al llamar a start( ) en el nuevo subproceso. www.fullengineeringbook.net Capítulo 7: Multiprocesamiento // Crea un subproceso al implementar Runnable. // Esta clase implementa Runnable, lo que significa que // puede usarse para crear un subproceso de ejecución. class MiSubproceso implements Runnable { int cuenta; MiSubproceso( ) { cuenta = 0; } // Punto de entrada del subproceso. public void run( ) { System.out.println("Iniciando MiSubproceso."); try { do { Thread.sleep(500); System.out.println("En MiSubproceso, la cuenta es " + cuenta); cuenta++; } while(cuenta < 5); } catch(InterruptedException exc) { System.out.println("MiSubproceso interrumpido."); } System.out.println("MiSubproceso terminando."); } } class DemoRunnable { public static void main(String args[]) { System.out.println("Iniciando subproceso principal."); // Primero, construye un objeto de MiSubproceso. MiSubproceso ms = new MiSubproceso( ); // Luego, construye un subproceso de ese objeto. Thread nuevoSubp = new Thread(ms); // Por último, empieza la ejecución del subproceso. nuevoSubp.start( ); // Da algo que hacer al subproceso principal. do { System.out.println("En el subproceso principal."); try { Thread.sleep(250); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } www.fullengineeringbook.net 301 302 Java: Soluciones de programación } while (ms.cuenta != 5); System.out.println("Terminando subproceso principal."); } } Aquí se muestra la salida de ejemplo. (La salida precisa puede variar, de acuerdo con la plataforma, la carga de tareas, velocidad del procesador y versión del sistema del motor en tiempo de ejecución de Java empleada). Iniciando subproceso principal. En el subproceso principal. Iniciando MiSubproceso. En el subproceso principal. En MiSubproceso, la cuenta es 0 En el subproceso principal. En el subproceso principal. En MiSubproceso, la cuenta es 1 En el subproceso principal. En el subproceso principal. En MiSubproceso, la cuenta es 2 En el subproceso principal. En el subproceso principal. En MiSubproceso, la cuenta es 3 En el subproceso principal. En el subproceso principal. En MiSubproceso, la cuenta es 4 MiSubproceso terminando. Terminando subproceso principal. Echemos un vistazo de cerca a la manera en que funciona este programa. Como se estableció, MiSubproceso implementa Runnable. Esto significa que un objeto de tipo Misubproceso es adecuado para usarse como subproceso y puede pasarse al constructor de Thread. Dentro del método run( ) de MiSubproceso, se establece un bucle que cuenta de 0 a 4. Éste es el código que se ejecutará en un subproceso separado. Dentro de run( ), observe la llamada a Thread.sleep(500). Este método estático causa que el subproceso desde el que se le llama suspenda su ejecución por el periodo especificado de milisegundos, que es de 500 (medio segundo) en este caso. El método sleep( ) se usa para demorar la ejecución del bucle dentro de run( ). Como resultado, la salida del mensaje en el bucle es demorada sólo una vez cada medio segundo. Dentro de main( ), se usa la siguiente secuencia para crear e iniciar un subproceso: // Primero, construye un objeto de MiSubproceso. MiSubproceso ms = new MiSubproceso( ); // Luego, construye un subproceso de ese objeto. Thread nuevoSubp = new Thread(ms); // Por último, empieza la ejecución del subproceso. nuevoSubp.start( ); En primer lugar, se crea una instancia de Runnable, que es un objeto de MiSubproceso en este caso. Luego, se pasa la instancia de Runnable al constructor de Thread para crear el subproceso. www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 303 Por último, se inicia el subproceso al llamar a start( ). Aunque es posible escribir esta secuencia de maneras diferentes o más compactas (consulte Opciones), todos los subprocesos basados en un Runnable separado construirán e iniciarán un subproceso empleando el mismo método general. Después de crear y empezar MiSubproceso, main( ) entra en un bucle que también contiene una llamada a sleep( ). En este caso, permanece inactivo por 250 milisegundos. Por tanto, el bucle de main( ) iterará dos veces para cada iteración del bucle en MiSubproceso, como lo confirma la salida. Opciones El ejemplo anterior está diseñado para mostrar claramente cada paso del proceso de creación/ ejecución del subproceso. Por tanto, cada paso se maneja individualmente. En primer lugar, se crea un objeto de MiSubproceso. Luego, se crea uno de Thread empleando este objeto. Por último, el subproceso se empieza al llamar a start( ). Sin embargo, es posible delinear este proceso al combinar estos tres pasos, de modo que cuando se cree un objeto de MiSubproceso, se pase automáticamente a Thread( ) y luego se inicie el subproceso. Para ello, cree la instancia de Thread dentro del constructor de MiSubproceso, y luego llame a start( ) en el subproceso. Aquí se muestra este método: // Afinación de la creación de un subproceso. // Esta clase implementa Runnable. Su constructor // crea automáticamente un objeto de Thread al pasar // esta instancia de MiSubproceso a Thread. El subproceso // se inicia luego al llamar a start( ). Por tanto, el // subproceso empieza su ejecución en cuanto se crea // el objeto de MiSubproceso. class MiSubproceso implements Runnable { int cuenta; MiSubproceso( ) { cuenta = 0; // Crea el subproceso e inicia su ejecución. new Thread(this).start( ); } // Punto de entrada del subproceso. public void run( ) { System.out.println("Iniciando MiSubproceso."); try { do { Thread.sleep(500); System.out.println("En MiSubproceso, la cuenta es " + cuenta); cuenta++; } while(cuenta < 5); } catch(InterruptedException exc) { System.out.println("MiSubproceso interrumpido."); } System.out.println("MiSubproceso terminando."); } } www.fullengineeringbook.net 304 Java: Soluciones de programación class DemoRunnable { public static void main(String args[]) { System.out.println("Iniciando el subproceso principal."); // Construye e inicia la ejecución de un objeto de MiSubproceso. MiSubproceso ms = new MiSubproceso( ); // Da al subproceso principal algo que hacer. do { System.out.println("En el subproceso principal."); try { Thread.sleep(250); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } } while (ms.cuenta != 5); System.out.println("Terminando subproceso principal."); } } Este programa produce la misma salida que la versión anterior. El método sleep( ) también tiene una segunda forma, que le permite especificar el periodo de demora en milisegundos o nanosegundos. Aquí se muestra: static void sleep(long milisegundos, int nanosegundos) Por supuesto, esta versión de sleep( ) es útil sólo si necesita precisión de nanosegundos. Otra manera de crear un subproceso consiste en extender Thread. Este método se muestra en la siguiente solución. Cree un subproceso al extender Thread Componentes clave Clases Métodos java.lang.Thread void run( ) static void sleep(long milisegundos) void start( ) En lugar de crear una clase separada que implementa Runnable, puede extender Thread para crear un subproceso. Esto funciona porque Thread implementa la interfaz Runnable. Usted simplemente sobreescribe el método run( ), agregando el código que ejecutará el subproceso. Esta solución muestra el proceso. www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 305 Paso a paso Para crear un subproceso al extender Thread se requieren estos pasos: 1. Cree una clase que extiende Threads. 2. Sobrescriba el método run( ), especificando el código que quiera ejecutar en un subproceso. 3. Cree una instancia de la clase que extiende Thread. 4. Empiece la ejecución del subproceso al llamar a start( ) en esa instancia. Análisis Cuando se crea un subproceso al extender Thread, debe sobreescribirse el método run( ), que es especificado por la interfaz Runnable. Dentro de run( ), coloque el código que quiera ejecutar con el subproceso. El subproceso terminará cuando se ejecute run( ). A continuación, construya un objeto de la clase que extiende Thread. Esto crea un nuevo subproceso. Para iniciar el subproceso, llame a start( ). La llamada a start( ) da como resultado la sobreescritura del método run( ) que se está invocando. Ejemplo En el siguiente ejemplo se muestra cómo crear un subproceso al extender Thread. Es funcionalmente equivalente al ejemplo en la solución anterior. // Crea un subproceso al extender Thread. // Esta clase extiende Thread. La construcción de una // instancia de esta clase crea un subproceso de ejecución. class MiSubproceso extends Thread { int cuenta; MiSubproceso( ) { cuenta = 0; } // Sobreescribe el método run( ). public void run( ) { System.out.println("Iniciando MiSubproceso."); try { do { Thread.sleep(500); System.out.println("En MiSubproceso, la cuenta es " + cuenta); cuenta++; } while(cuenta < 5); } catch(InterruptedException exc) { System.out.println("MiSubproceso interrumpido."); } www.fullengineeringbook.net 306 Java: Soluciones de programación System.out.println("MiSubproceso terminando."); } } class DemoSubpExtendido { public static void main(String args[]) { System.out.println("Iniciando el subproceso principal."); // Construye un objeto de MiSubproceso. Debido a que // MiSubproceso extiende Thread, esto crea un nuevo subproceso. MiSubproceso ms = new MiSubproceso( ); // Inicia la ejecución del subproceso. ms.start( ); // Da al subproceso principal algo que hacer. do { System.out.println("En el proceso principal."); try { Thread.sleep(250); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } } while (ms.cuenta != 5); System.out.println("Terminando subproceso principal."); } } La salida es la misma que se muestra en la solución anterior. Opciones Cuando extienda Thread, tal vez quiera invocar al constructor de Thread desde el interior del constructor de su clase de subproceso. Por ejemplo, si quiere dar un nombre al subproceso, entonces necesitará invocar esta versión del constructor de Thread. Thread(String nombreSubp) Por supuesto, esto se logra mediante una llamada a super. Por ejemplo, esta versión de MiSubproceso( ) da al subproceso el nombre "Alfa". MiSubproceso( ) { super("Alfa"); cuenta = 0; } Consulte Use el nombre y el ID de un subproceso para la información en nombres de subproceso. Debido a que es posible crear un subproceso al implementar Runnable en una clase separada, o al extender Thread, surge una pregunta natural: ¿cuál es el mejor método? Aunque no hay una regla inmutable para este efecto, muchos programadores creen que las clases deben extenderse sólo cuando están expandiéndose o cambiándose de alguna manera. Por tanto, www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 307 con frecuencia sólo se extiende Thread cuando hay una razón de hacerlo [por ejemplo, si está proporcionando una implementación predeterminada de start( )]. De otra manera, suele tener más sentido simplemente implementar Runnable. Use el nombre y el ID de un subproceso Componentes clave Clases Métodos java.lang.Thread long getId( ) final String getName( ) final void setName(String nombreSubp) Todos los subprocesos tienen un nombre. Este nombre fue creado por el sistema del motor en tiempo de ejecución o especificado por usted cuando se creó un subproceso. Aunque el código liberado no siempre usa un nombre para los subprocesos, estos nombres son muy útiles cuando se desarrollan, prueban y depuran. Aunque puede usarse un nombre de subproceso para identificar un subproceso, hacerlo así tiene dos desventajas. En primer lugar, los nombres de subprocesos no son necesariamente únicos. Más de un subproceso puede tener el mismo nombre. En segundo lugar, debido a que los nombres de subprocesos son cadenas, la comparación de un nombre incluye una comparación de cadena, que ocupa mucho tiempo. Para evitar estos problemas, puede usar otro mecanismo para identificar un subproceso. A partir de Java 5, a todos los subprocesos se les da un ID de subproceso, que es un valor entero grande. Los ID de subproceso son únicos y, debido a que son enteros, las comparaciones son muy rápidas. Por tanto, puede identificarse de manera eficiente un subproceso mediante su ID. En esta solución se muestra cómo usar nombres e ID de subprocesos. NOTA Recuerde que los ID de subproceso se agregaron en Java 5. Por tanto, los ID de subprocesos están disponibles sólo si está usando una versión moderna de Java. Paso a paso Para usar nombres e ID de subproceso se requieren los pasos siguientes: 1. Hay dos maneras de dar un nombre a un subproceso. En primer lugar, y lo que es más conveniente, puede especificar su nombre cuando se construye un subproceso, al pasarlo a uno de los constructores de Thread. En segundo lugar, puede cambiar el nombre de un subproceso al llamar a setName( ). 2. Puede obtener un nombre de subproceso al llamar a getName( ). 3. A partir de Java 5, cuando se crean los subprocesos reciben automáticamente números de ID. Su programa no puede asignar estos números, pero puede obtener el ID al llamar a getID( ). www.fullengineeringbook.net 308 Java: Soluciones de programación Análisis Thread proporciona varios constructores que le permiten especificar un nombre cuando se crea un subproceso. Aquí se muestra el usado en esta solución: Thread(Runnable objSubp, String nombreSubp) Aquí, objSubp especifica el Runnable que se ejecutará y nombreSubp especifica el nombre del subproceso. Como se explicó en Fundamentos del multiprocesamiento, si no asigna explícitamente un nombre al subproceso, entonces el sistema del motor en tiempo de ejecución proporciona uno. Por tanto, todos los subprocesos tienen nombre. La especificación del nombre simplemente le permite usar un nombre de su propia elección. Puede cambiar el nombre de un subproceso después de que se construye al llamar a setName( ) en la instancia de Thread. Aquí se muestra: final void setName(String nombreSubp) El nuevo nombre se pasa en nombreSubp. Es importante comprender que más de un subproceso puede usar el mismo nombre. Por tanto, los nombre de subproceso no son necesariamente únicos. Por ejemplo, podría tener tres subprocesos activos, cada uno con el nombre "MiSubproceso". Sin embargo, a menudo querrá usar nombres únicos para que un nombre de subproceso identifique a un subproceso específico, individual. Puede obtener el nombre actual de un subproceso al llamar a getName( ), que se muestra a continuación: final String getname( ) Se devuelve el nombre del subproceso. Debido a que los nombres de subproceso no necesariamente son únicos, tal vez encuentre situaciones en que preferirá identificar un subproceso por su número de ID en lugar de su nombre. A partir de Java 5, todos los subprocesos reciben automáticamente un ID entero único y grande cuando se crean. Puede obtener un ID de subproceso al llamar a getId( ), que se muestra aquí: long getId( ) Se devuelve el ID. Aunque los ID son únicos, pueden reciclarse. Por ejemplo, si un subproceso termina, el nuevo subproceso podría asignarse al ID del subproceso anterior. Ejemplo En el siguiente ejemplo se ilustran los nombres e ID de subprocesos. Se crea un Runnable llamado MiSubproceso cuyo constructor toma el nombre de un subproceso como parámetro. Dentro de MiSubproceso( ), el nombre se pasa a Thread( ) cuando se construye un subproceso. Observe que MiSubproceso( ) empieza automáticamente por ejecutar el nuevo subproceso. Dentro de run( ), el nombre del subproceso, su valor de ID y la cuenta se despliegan dentro de un bucle. Cuando la cuenta es igual a 3, el nombre del subproceso cambia a mayúsculas. Dentro de main( ), se crean dos nuevos subprocesos con los nombres Primer subproceso y Segundo subproceso. // Usa nombres e ID de subproceso. // MiSubproceso crea un subproceso que tiene un nombre // especificado. El nombre se coloca en mayúsculas // después de tres iteraciones del bucle en run( ). class MiSubproceso implements Runnable { www.fullengineeringbook.net Capítulo 7: Multiprocesamiento int cuenta; Thread subp; MiSubproceso(String nombreSubp) { cuenta = 0; // Construye un nuevo subproceso empleando este objeto // y el nombre especificado. subp = new Thread(this, nombreSubp); // Inicia la ejecución del subproceso. subp.start( ); } // Punto de entrada del subproceso. public void run( ) { System.out.println(subp.getName( ) + " iniciando."); try { do { Thread.sleep(500); // Despliega el nombre, ID y cuenta del subproceso. System.out.println("En " + subp.getName( ) + " (ID: " + subp.getId( ) + ")" + ", la cuenta es " + cuenta); cuenta++; // Cambia el nombre de un subproceso. if(cuenta == 3) subp.setName(subp.getName( ).toUpperCase( )); } while(cuenta < 5); } catch(InterruptedException exc) { System.out.println(subp.getName( ) + " interrumpido."); } System.out.println(subp.getName( ) + " terminando."); } } class DemoNombresEID { public static void main(String args[]) { System.out.println("Iniciando subproceso principal."); // Construye e inicia un subproceso MiSubproceso ms = new MiSubproceso("Primer subproceso"); // Construye e inicia un segundo subproceso. MiSubproceso ms2 = new MiSubproceso("Segundo subproceso"); www.fullengineeringbook.net 309 310 Java: Soluciones de programación // Da al subproceso principal algo que hacer. do { System.out.println("En el proceso principal."); try { Thread.sleep(250); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } // Espera hasta que ambos subprocesos terminan. } while (ms.cuenta != 5 && ms2.cuenta != 5); System.out.println("Finalizando subproceso principal."); } } Aquí se muestra la salida de ejemplo: Iniciando subproceso principal. En el proceso principal. Primer subproceso iniciando. Segundo subproceso iniciando. En el proceso principal. En el proceso principal. En Primer subproceso (ID: 8), la cuenta es 0 En Segundo subproceso (ID: 9), la cuenta es 0 En el proceso principal. En el proceso principal. En Primer subproceso (ID: 8), la cuenta es 1 En Segundo subproceso (ID: 9), la cuenta es 1 En el proceso principal. En el proceso principal. En Primer subproceso (ID: 8), la cuenta es 2 En Segundo subproceso (ID: 9), la cuenta es 2 En el proceso principal. En el proceso principal. En PRIMER SUBPROCESO (ID: 8), la cuenta es 3 En SEGUNDO SUBPROCESO (ID: 9), la cuenta es 3 En el proceso principal. En el proceso principal. En PRIMER SUBPROCESO (ID: 8), la cuenta es 4 PRIMER SUBPROCESO terminando. En SEGUNDO SUBPROCESO (ID: 9), la cuenta es 4 SEGUNDO SUBPROCESO terminando. Finalizando subproceso principal. Opciones Puede obtener una referencia al subproceso actualmente en ejecución al llamar a currentThread( ), que se muestra aquí: static Thread currentThread( ) www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 311 Este método es útil cuando quiere obtener información acerca del subproceso en ejecución, o cuando lo quiere manejar. Por ejemplo, puede obtener el ID para el subproceso principal al ejecutar esta instrucción dentro de main( ). System.out.println("El ID del subproceso principal es " + Thread.currentThread( ).getId( )); Puede obtener el nombre del subproceso principal, empleando el mismo método: System.out.println("El nombre del subproceso principal es " + Thread.currentThread( ).getName( )); Espere a que termine un subproceso Componentes clave Clases Métodos java.lang.Thread final void join( ) Cuando está usando varios subprocesos, no es poco común que uno espere a que el otro termine. Por ejemplo, un subproceso podría está realizando una tarea que debe ejecutarse por completo antes de que un segundo subproceso pueda seguir su ejecución. En otras situaciones, tal vez quiera que un subproceso, como el principal, termine después de que se realicen ciertas tareas de "limpieza", como liberar los recursos del sistema que ya no son necesarios. Cualquiera que sea la razón, Thread proporciona un medio conveniente de esperar a que termine un subproceso: el método join( ). En esta solución se demuestra el proceso. Paso a paso Para esperar a que un subproceso termine se requieren estos pasos: 1. Empiece la ejecución de un subproceso. 2. Llame a join( ) en el subproceso. Esta llamada debe ejecutarse desde el interior del subproceso que espera. 3. Cuando se regrese join( ), el subproceso habrá terminado. Análisis El método join( ) espera hasta que termina el subproceso en que se le llama. Su nombre (unir, en español) viene del concepto de que el subproceso que llama debe esperar hasta que el subproceso especificado se le una. Por tanto, join( ) causa que el subproceso que llama suspenda la ejecución hasta que termine el subproceso que se une. www.fullengineeringbook.net 312 Java: Soluciones de programación Hay tres formas de join( ). La usada en esta solución se muestra a continuación: final void join( ) throws InterruptedException Las otras dos formas de join( ) le permiten especificar la cantidad máxima de tiempo que quiere que espere el subproceso que invoca a que termine el subproceso especificado. Ejemplo En el siguiente ejemplo se ilustra join( ). Crea un subproceso basado en MiSubproceso que cuenta a 5 y luego termina. Dentro de main( ), se llama a join( ) en este subproceso. Por tanto, el subproceso principal espera hasta que haya terminado el subproceso. // Demuestra join( ). class MiSubproceso implements Runnable { int cuenta; MiSubproceso( ) { cuenta = 0; } // Cuenta hasta 5. public void run( ) { System.out.println("Iniciando MiSubproceso."); try { do { Thread.sleep(500); System.out.println("En MiSubproceso, la cuenta es " + cuenta); cuenta++; } while(cuenta < 6); } catch(InterruptedException exc) { System.out.println("MiSubproceso interrumpido."); } System.out.println("MiSubproceso terminando."); } } class DemoJoin { public static void main(String args[]) { System.out.println("Iniciando subproceso principal."); // Construye un subproceso basado en MiSubproceso. Thread subp = new Thread(new MiSubproceso( )); // Empieza la ejecución de subp. subp.start( ); www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 313 // Espera hasta que termina subp. try { subp.join( ); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } System.out.println("Finalizando subproceso principal."); } } Aquí se muestra una salida de ejemplo. (Su salida exacta podría variar.) Iniciando subproceso principal. Iniciando MiSubproceso. En MiSubproceso, la cuenta es 0 En MiSubproceso, la cuenta es 1 En MiSubproceso, la cuenta es 2 En MiSubproceso, la cuenta es 3 En MiSubproceso, la cuenta es 4 En MiSubproceso, la cuenta es 5 MiSubproceso terminando. Finalizando subproceso principal. Opciones La versión de join( ) usada en el ejemplo espera indefinidamente a que termine un subproceso. A menudo esto es lo que necesita, pero en ocasiones querrá limitar la cantidad de tiempo que esperará el subproceso que invoca. Recuerde que llamar a join( ) causa que el subproceso que invoca suspenda la ejecución. En algunos casos, tal vez sea necesario que el subproceso que invoca reanude la ejecución aunque el subproceso en que se llama a join( ) no haya terminado. Para manejar esta posibilidad, Thread define dos formas adicionales de join( ) que le permiten especificar un tiempo máximo de espera. Aquí se muestran: final void join(long milisegundos) throws InterruptedException final void join(long milisegundos, int nanosegundos) throws InterruptedException Para ambas versiones, el número de milisegundos de espera se especifica en milisegundos. La segunda forma le permite especificar la precisión en nanosegundos. Otra manera de esperar hasta que finalice un subproceso consiste en revisar su estado al llamar a isAlive( ). Aunque el uso de join( ) es casi siempre un mejor y más eficiente método, en caso especiales resulta más apropiado el uso de isAlive( ). Aquí se muestra: final boolean isAlive( ) Este método de isAlive( ) devuelve verdadero si el subproceso en que se llama aún está activo. Devuelve falso si el subproceso ha terminado. www.fullengineeringbook.net 314 Java: Soluciones de programación El problema con el uso de isAlive( ) para esperar a que un subproceso termine es que el bucle para revisión empleado para llamar a isAlive( ) sigue consumiendo ciclos de la CPU mientras espera. En contraste, join( ) suspende la ejecución del subproceso que invoca, con lo que se liberan ciclos de CPU. Para comprender por completo el problema, considere esta versión de main( ) del ejemplo anterior. Se ha reescrito para usar isAlive( ). // Esta versión de main( ) usa isAlive( ) para esperar a que // un subproceso finalice. NO es tan eficiente como la versión // que usa join( ) y sólo se utiliza para demostración. Este // método NO se recomienda para código real. public static void main(String args[]) { System.out.println("Iniciando proceso principal."); // Construye un subproceso basado en MiSubproceso. Thread subp = new Thread(new MiSubproceso( )); // Inicia la ejecución de subp. subp.start( ); // Espera hasta que subp termina. while(subp.isAlive( )) ; System.out.println("Finalizando subproceso principal."); } Esta versión de main( ) producirá los mismos resultados que antes. Sin embargo, el programa ya no está escrito de manera tan eficiente como antes, desde el punto de vista del rendimiento. La razón es que el subproceso principal ya no suspende la ejecución, esperando a que subp finalice. En cambio, sigue ejecutándose, haciendo llamadas repetidas a isAlive( ). Esto consume muchos ciclos de CPU innecesariamente. Por tanto, en este caso, el uso de join( ) es mucho mejor. Sincronice subprocesos Componentes clave Clases Métodos synchronized synchronized tipo nombreMetodo(list-args){ // cuerpo del método sincronizado } synchronized(refobj){ // instrucciones sincronizadas } www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 315 Cuando se usan subprocesos múltiples, a veces es necesario evitar que un subproceso tenga acceso a un objeto usado actualmente por otro. Esta situación puede ocurrir cuando dos o más subprocesos necesitan acceder a un recurso compartido que sólo puede ser usado por un subproceso a la vez. Por ejemplo, cuando un subproceso está escribiendo en un archivo, puede evitarse que un segundo subproceso también lo haga al mismo tiempo. Al mecanismo mediante el cual se controla el acceso de varios subprocesos a un objeto se le llama sincronización. La clave para la sincronización en Java es el concepto de monitor. Un monitor funciona al implementar el concepto de bloqueo. Cuando un subproceso ingresa en el monitor de un objeto, éste se bloquea y ningún otro subproceso puede tener acceso a él. Cuando el subproceso deja el monitor, el objeto se desbloquea y queda disponible para que lo use otro subproceso. Todos los objetos de Java tienen un monitor. Esta característica está integrada en el propio lenguaje. Por tanto, es posible sincronizar todos los objetos. La sincronización se basa en la palabra clave synchronized y en unos cuantos métodos bien definidos de todos los objetos. Debido a que la sincronización fue diseñada en Java desde cero, es mucho más fácil usarla de lo que se esperaría al principio. En realidad, para muchos programas, la sincronización de objetos es casi transparente. En esta solución se muestra cómo sincronizar el acceso a un objeto. Paso a paso Para sincronizar el acceso a un objeto, siga estos pasos: 1. Puede sincronizar uno o más métodos definidos por el objeto al especificar el modificador synchronized. Cuando se llama a un método sincronizado, el objeto se bloquea hasta que se regresa el método. 2. Puede sincronizar acciones específicas en un objeto al usar un bloque sincronizado de código, que se crea al usar la instrucción synchronized. Cuando se entra en un bloque sincronizado, el objeto se bloquea. Cuando se deja el bloque, el objeto se desbloquea. Análisis Hay dos maneras de sincronizar el acceso a un objeto. En primer lugar, puede modificar uno o más de sus métodos con la palabra clave synchronized. Un método sincronizado tiene la siguiente forma general: synchronized tipo nombreMetodo(list-args){ // cuerpo del método sincronizado } Aquí, tipo es el tipo de regreso del método y nombreMetodo es su nombre. Cuando se llama a un método sincronizado en un objeto, el subproceso que llama adquiere el monitor del objeto y éste se bloquea. Ningún otro subproceso puede ejecutar un método sincronizado en el objeto, hasta que el subproceso libera el bloqueo, ya sea regresando desde el método o llamando al método wait( ). (Para conocer un ejemplo que usa wait( ), consulte Establezca comunicación entre subprocesos,) Una vez que se ha liberado el monitor, puede adquirirlo otro subproceso. www.fullengineeringbook.net 316 Java: Soluciones de programación La segunda manera de sincronizar el acceso a un objeto consiste en usar el bloque sincronizado. Tiene la forma general: synchronized(refobj){ // instrucciones sincronizadas } Aquí, refobj es una referencia al objeto al que quiera limitar el acceso. Una vez que se ha ingresado en un bloque sincronizado, refobj se bloquea y ningún otro subproceso puede adquirir ese bloqueo hasta que éste finaliza. Por tanto, un bloque sincronizado asegura que una llamada a un método en refobj sólo procederá después de que el subproceso actual haya adquirido el bloqueo de refobj. Un segundo subproceso que desee acceso a refobj esperará hasta que el primer subproceso haya liberado el bloqueo. El beneficio principal de un bloque sincronizado es que le permite sincronizar el acceso a un objeto que de otra manera no sería seguro para el subproceso. En otras palabras, el uso de un bloque sincronizado le permite sincronizar el acceso a un objeto cuyos métodos no están sincronizados. Esto puede ser muy valiosos en varias situaciones. Por ejemplo, tal vez necesite sincronizar un objeto de una clase para la cual no tiene acceso al código fuente (como una clase proporcionada por un tercero). Ejemplo En el siguiente ejemplo se crea una clase llamada Prompter, que simula un teleprompter muy simple que despliega lentamente un mensaje, de palabra en palabra. El mensaje es desplegado por el método display( ), que permanece inactivo por uno o más segundos entre cada palabra. Este método está sincronizado, lo que significa que sólo lo puede usar un objeto a la vez. El programa crea dos subprocesos que usan la misma instancia de Prompter. Sin sincronización, el mensaje desplegado por un subproceso se confundiría con el desplegado por el otro. Sin embargo, al sincronizar display( ), los dos mensajes se mantienen separados. // Demuestra métodos sincronizados. // Un teleprompter muy simple que permanece inactivo // uno o más segundos entre palabras. class Prompter { int demora; // número de segundos de demora entre palabras Prompter(int d) { if(d <= 0) d = 1; demora = d; } // Debido a que display( ) está sincronizado, sólo // puede usarlo un subproceso a la vez. Esto evita // que diferentes mensajes se mezclen. synchronized void display(String msj) { for(int i=0; i < msj.length( ); i++) { System.out.print(msj.charAt(i)); if(Character.isWhitespace(msj.charAt(i))) { try { www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 317 Thread.sleep(demora*1000); } catch(InterruptedException exc) { return; } } } System.out.println( ); } } // Un subproceso que usa un Prompter. class UsaPrompter implements Runnable { Prompter prompter; // el Prompter que se usará String mensaje; // el mensaje que se desplegará UsaPrompter(Prompter p, String msj) { prompter = p; mensaje = msj; // Crea e inicia la ejecución del subproceso. new Thread(this).start( ); } // Usa prompter para mostrar el mensaje. public void run( ) { prompter.display(mensaje); } } // Demuestra que un método sincronizado evita // que varios subprocesos tengan acceso a un // objeto compartido al mismo tiempo. class DemoSinc { public static void main(String args[]) { // Construye un objeto de Prompter. Prompter p = new Prompter(1); // Construye dos subprocesos que usan p. Por tanto, ambos // subprocesos tratarán de usar p al mismo tiempo. Sin embargo, // como display( ) está sincronizado, sólo uno puede usar p a la vez. UsaPrompter promptA = new UsaPrompter(p, «Uno Dos Tres Cuatro»); UsaPrompter promptB = new UsaPrompter(p, «Izquierda Derecha Arriba Abajo»); } } Aquí se muestra la salida de ejemplo: Uno Dos Tres Cuatro Izquierda Derecha Arriba Abajo Como puede ver, el primer mensaje del subproceso se despliega por completo antes de que empiece el segundo. Debido a que display( ) está sincronizado, un subproceso no puede usar p hasta que obtiene www.fullengineeringbook.net 318 Java: Soluciones de programación su bloqueo. En este caso, promptA gana primero el bloqueo. Aunque display( ) está inactivo por un segundo entre palabras, el segundo subproceso, promptB, no puede ejecutarse porque el bloqueo de p ya le pertenece a promptA. Esto significa que éste último tiene acceso exclusivo a display( ) hasta que el bloqueo se libera cuando display( ) regresa. En este punto, promptB obtiene acceso al bloqueo y puede desplegarse su mensaje. Para apreciar por completo los beneficios de la sincronización, haga la prueba de eliminar el modificador sincronizado de display( ) y luego vuelva a ejecutar el programa. Verá algo similar a la siguiente salida, combinada: Uno Izquierda Dos Derecha Tres Arriba Cuatro Abajo Debido a que display( ) ya no está sincronizado, ambos subprocesos tienen acceso a p y la salida aparece mezclada. Opciones Aunque el uso de métodos sincronizados suele ser lo mejor, un bloque sincronizado proporciona una opción que resulta útil en algunos casos. Un bloque sincronizado le permite sincronizar un objeto que de otra manera no estaría sincronizado. Por ejemplo, empleando un bloque sincronizado, puede sincronizar el acceso a un método que no es modificado por synchronized. Puede experimentar con un bloque sincronizado al modificar el programa de ejemplo. En primer lugar, elimine el modificador synchronized de display( ). Luego sustituya esta versión de run( ) en UsaPrompter: public void run( ) { // Usa un bloque sincronizado para manejar el acceso al prompter. synchronized(prompter) { prompter.display(mensaje); } } El bloque sincronizado basado en prompter sincroniza el acceso a display( ) aunque ésta ya no sea un método sincronizado. Por tanto, después de hacer estos cambios, el programa aún se ejecutará correctamente. Establezca comunicación entre subprocesos Componentes clave Clases Métodos java.lang.Object final void wait( ) final void notify( ) www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 319 Como se demostró en la solución anterior, un método o bloque sincronizado evita el acceso asincrónico a un objeto. Sin embargo, lo hace de manera incondicional. Una vez que un subproceso ha ingresado en el monitor de un objeto, ningún otro subproceso puede obtener acceso al mismo objeto hasta que el primer subproceso ha dejado el monitor. Aunque este método es muy poderoso (y extremadamente útil), en ocasiones se requiere una técnica más sutil. Para comprender por qué, considere la siguiente situación. Un subproceso llamado S se está ejecutando dentro de un método sincronizado y necesita acceso a un recurso, llamado R, que no está disponible de manera temporal. ¿Qué debe hacer S? Si S entra en alguna forma de bucle de consulta que espera a R, S se une al objeto, evitando que otro subproceso lo use. Esta es una situación menos que óptima porque elimina parcialmente las ventajas del multiprocesamiento. Una mejor solución consiste en hacer que S renuncie temporalmente al control del objeto, permitiendo la ejecución de otro subproceso. Cuando R queda disponible, puede notificársele a S, que reanuda la ejecución. Este método depende de alguna forma de comunicación entre subprocesos, en la que un subproceso puede renunciar temporalmente al control de un objeto, y luego esperar hasta que se le notifique que puede reanudar la ejecución. Java soporta la comunicación entre subprocesos con los métodos wait( ) y notify( ) definidos por Object. En esta solución se demuestra su uso. Paso a paso Para establecer comunicación entre subprocesos mediante el uso de wait( ) y notify( ) se requieren los siguientes pasos: 1. Para que un subproceso espere hasta que sea notificado por algún otro subproceso, llame a wait( ). 2. Para notificar a un subproceso en espera, llame a notify( ). 3. Por lo general, un subproceso usa wait( ) para hacer una pausa en la ejecución hasta que haya ocurrido algún suceso o esté disponible algún recurso compartido. Se le notifica que puede seguir cuando otro subproceso llama a notify( ). Análisis Los métodos wait( ) y notify( ) son parte de todos los objetos porque están implementados por la clase Object. A estos métodos sólo puede llamárseles desde un método sincronizado. He aquí cómo se usan. Cuando la ejecución de un subproceso está temporalmente bloqueada, se llama a wait( ). Esto causa que el subproceso entre en inactividad y que se libere el monitor de ese objeto. Esto permite que otro subproceso use el objeto. En un punto posterior, el subproceso inactivo se reanuda cuando algún otro subproceso ingresa en el mismo monitor y llama a notify( ). Una llamada a notify( ) reanuda un subproceso en espera. Hay tres formas de wait( ) definidas por Object. Aquí se muestra la usada en esta solución: final void wait( ) throws InterruptedException Causa que el subproceso que invoca libere el monitor del objeto y espere (es decir, se suspenda la ejecución) hasta que reciba notificación de otro subproceso. Por supuesto, el subproceso que invoca debe haber adquirido un bloqueo del objeto antes de llamar a wait( ). En otras palabras, el subproceso debe haber ingresado en el monitor del objeto. Por tanto, debe llamarse a wait( ) desde el interior de un método o un bloque sincronizado. Para notificar a un subproceso en espera que puede reanudar la ejecución, llame a notify( ). Aquí se muestra: final void notify( ) www.fullengineeringbook.net 320 Java: Soluciones de programación Una llamada a notify( ) reanuda un subproceso en espera. Como en el caso de wait( ), también debe llamarse a notify( ) dentro de un contexto sincronizado; es decir, desde el interior de un método o un bloque sincronizado. Por tanto, el subproceso que llama debe haber ingresado en el monitor de un objeto y adquirido su bloqueo. Debe aclararse un tema importante: aunque wait( ) suele esperar hasta que se llama a notify( ) o notifyAll( ), hay la posibilidad de que en casos muy raros el subproceso que espera pudiera activarse debido a una activación espuria. En este caso, un subproceso en espera se reanuda sin que se haya llamado a notify( ) o notifyAll( ). (En esencia, el subproceso se reanuda sin una razón evidente). Debido a esta posibilidad remota, Sun recomienda que las llamadas a wait( ) se presenten dentro de un bucle que revisa la condición en que está esperando el subproceso. En el siguiente ejemplo se demuestra esta técnica. Ejemplo En el siguiente ejemplo se proporciona una demostración simple de wait( ) y notify( ). // Una demostración simple de wait( ) y notify( ). class ObSinc { boolean listo = false; // Este método espera hasta que recibe notificación // de que la variable listo es true. synchronized void esperar( ) { String nombreSubp = Thread.currentThread( ).getName( ); System.out.println(nombreSubp + " est\u00a0 ingresando en esperar( )."); System.out.println(nombreSubp + " llamando a wait( ) para que espere" + " notificaci\u00a2n para seguir adelante.\n"); try { // Espera notificación. while(!listo) wait( ); } catch(InterruptedException exc) { System.out.println("Interrumpido."); } System.out.println(nombreSubp + " recibi\u00a2 notificaci\u00a2n y se est\u00a0" + " reanudando la ejecuci\u00a2n."); } // Este método establece la variable listo en true // y envía una notificación. synchronized void seguirAdelante( ) { String nombreSubp = Thread.currentThread( ).getName( ); System.out.println("\nSubproceso " + nombreSubp + " est\u00a0 llamando a notify( ) dentro de seguirAdelante( ).\n" + www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 321 "Esto har\u00a0 que MiSubproceso reanude la ejecuci\ u00a2n.\n"); // Establece listo y notify( ). listo = true; notify( ); } } // Una clase de subproceso que usa ObSinc. class MiSubproceso implements Runnable { ObSinc obSinc; // Construye un nuevo subproceso. MiSubproceso(String nombre, ObSinc os) { obSinc = os; new Thread(this, nombre).start( ); } // Empieza la ejecución del subproceso. public void run( ) { obSinc.esperar( ); } } class DemoSubpCom { public static void main(String args[]) { try { ObSinc objS = new ObSinc( ); // Construye un subproceso en objS que espera // una notificación. new MiSubproceso("MiSubproceso", objS); // Quema algún tiempo de la CPU. for(int i=0; i < 10; i++) { Thread.sleep(250); System.out.print("."); } System.out.println( ); // El subproceso principal ahora notificará a objS. objS.seguirAdelante( ); // En este punto, MiSubproceso reanuda la ejecución. } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } } } Aquí se muestra la salida de ejemplo: MiSubproceso está ingresando en waitFor( ). MiSubproceso llamando a wait( ) para que espere notificación para seguir adelante. www.fullengineeringbook.net 322 Java: Soluciones de programación .......... El subproceso main está llamando a notify( ) dentro de goAhead( ). Esto hará que MiSubproceso reanude la ejecución. MiSubproceso recibió notificación y se está reanudando la ejecución. El programa merece una revisión detallada. Empieza por crear una clase llamada ObSinc que define una variable de instancia llamada listo y dos métodos sincronizados llamados esperar( ) y seguirAdelante( ). Observe que la variable listo tiene asignado inicialmente el valor false. El método esperar( ) espera a que listo sea true. Hace esto al ejecutar una llamada a wait( ). Esto causa que esperar( ) haga una pausa hasta que otros subprocesos llamen a notify( ) en el mismo objeto. El segundo método es seguirAdelante( ). Asigna true a listo y luego llama a notify( ). Por tanto, el subproceso que llama a esperar( ) esperará hasta que otro subproceso llame a seguirAdelante( ). Un tema clave que debe comprenderse es la manera en que se usa la variable listo, junto con wait( ), para esperar una notificación. Como se explicó antes, debido a la remota posibilidad de una activación espuria, Sun recomienda que todas las llamadas a wait( ) tengan lugar dentro de un bucle que pruebe la condición bajo la que está esperando el subproceso. En este caso, está esperando a que listo sea true. Aquí se codifica el bucle de espera: while(!listo) wait( ); Por tanto, siempre y cuando listo sea false, se llamará a wait( ). En realidad, en casi todos los casos, sólo se llamará una vez a wait( ) porque no ocurrirán activaciones espurias y la llamada a wait( ) no regresará hasta que el método seguirAdelante( ) llame a notify( ). Sin embargo, debido al problema de las activaciones espurias, es necesario el bucle. A continuación, el programa define la clase MiSubproceso. Crea un subproceso que usa un objeto ObSinc. Este objeto está almacenado en una variable de instancia llamada obSinc. El método run( ) de MiSubproceso ejecuta una llamada a esperar( ) en obSinc, que da como resultado una llamada a wait( ). Por tanto, MiSubproceso suspenderá la ejecución hasta que se haya ejecutado una llamada a notify( ) en el mismo objeto. Dentro de main( ), se crea una instancia de un objeto de ObSinc llamado objS. Luego, se crea un objeto de MiSubproceso, pasando un objS como el objeto de ObSinc. A continuación, main( ) despliega diez puntos (sólo para usar algún tiempo de la CPU). Luego, main( ) llama a seguirAdelante( ) en objS, que es el mismo ObSinc usado por la instancia de MiSubproceso. Esto da como resultado que listo sea true y se llame a notify( ), lo que permite que MiSubproceso reanude la ejecución. Opciones Hay dos formas adicionales de wait( ), que le permiten especificar la cantidad máxima de tiempo que un subproceso esperará para obtener acceso a un objeto. Aquí se muestran: final void wait(long milis) throws InterruptedException final void wait(long milis, int nanos) throws InterruptedException La primera forma espera hasta que se notifique o hasta que haya expirado el periodo especificado de milisegundos. La segunda forma le permite especificar el periodo de espera en nanosegundos. Puede notificar a todos los subprocesos en espera llamando a notifyAll( ), que se muestra a continuación: final void notifyAll( ) www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 323 Suspenda, reanude y detenga un subproceso Componentes clave Clases Métodos java.lang.Thread final void wait( ) final void notify( ) Después de que se llame a notifyAll( ), un subproceso en espera obtendrá acceso al monitor. En los primeros días de Java, era muy fácil suspender, reanudar y detener un subproceso. ¿Por qué? Porque la clase Thread definía métodos llamados suspend( ), resume( ) y stop( ) que estaban diseñados precisamente para ese fin. Sin embargo, ocurrió un suceso más bien inesperado cuando se lanzó Java 1.2: ¡estos métodos se dejaron a un lado! Por tanto, no deben usarse para nuevo código y su uso en código antiguo debe eliminarse. En lugar de usar métodos de API para suspender, reanudar y detener un subproceso, debe incrustar estas características en su clase de subproceso. Por tanto, cada clase de subproceso proporcionará sus propios mecanismos para suspender, reanudar y detener, dependiendo de wait( ) y notify( ). En esta solución se muestra la manera de hacer esto. Antes de seguir adelante, resulta útil comprender por qué se hicieron a un lado a suspend( ), resume( ) y stop( ). El método suspend( ) fue descontinuado porque su uso puede llevar a puntos muertos. En realidad, la documentación de la API de Java establece que suspend( ) es "inherentemente propenso a puntos muertos". Para comprender cómo, suponga dos subprocesos llamados A y B. Además, suponga que A contiene un bloqueo en un objeto y luego se suspende. Más aún, suponga que B es responsable de reanudar el subproceso A. Sin embargo, si B trata de obtener el mismo bloqueo antes de que se reanude A, se llegará a un punto muerto porque el bloqueo aún es conservado por A, que está suspendido. Por tanto, no se libera el bloqueo. Como resultado, B espera indefinidamente. El método resume( ) se descontinuó porque sólo se usa como contraparte de suspend( ). El método stop( ) se descontinuó porque, como lo establece la documentación de la API de Java, es "inherentemente inseguro". La razón es que una llamada a stop( ) causa que se liberen todos los bloqueos mantenidos por ese subproceso. Esto podría llevar a la corrupción de un objeto que se está sincronizando debido a la liberación anticipada de un bloqueo. Por ejemplo, si una estructura de datos se está actualizando cuando se detiene el subproceso, se liberará el bloqueo de ese objeto, pero los datos quedarán incompletos o se corromperán. Paso a paso Una manera de implementar la capacidad de suspender y reanudar un subproceso incluye estos pasos: 1. Cree una variable volatile boolean llamada suspendido que indicará un estado suspendido del subproceso. Si suspendido es verdadero, el subproceso se suspenderá. Si es falso, se reanudará la ejecución. 2. Dentro del método run( ) del subproceso, cree un bucle while que use suspendido como condición. Dentro del cuerpo del bucle, llame a wait( ). En otras palabras, mientras suspendido sea verdadero, haga repetidas llamadas a wait( ). Por tanto, cuando suspendido es verdadero, tiene lugar una llamada a wait( ), lo que causa que el subproceso entre en pausa. www.fullengineeringbook.net 324 Java: Soluciones de programación Cuando suspendido es falso, se sale del bucle y se reanuda la ejecución. 3. Para suspender el subproceso, asigne suspendido en true. Para reanudar, establezca suspendido en false y luego llame a notify( ). Una manera de implementar la capacidad de detener un subproceso incluye estos pasos: 1. Cree una variable volatile boolean llamada detenido que indique el estado detenido/en ejecución del subproceso. 2. Cuando detenido sea verdadero, termine el subproceso, a menudo con simplemente dejar que regrese el método run( ). De otra manera, permita que el subproceso continúe la ejecución. 3. Cuando se cree el subproceso, establezca detenido en false. Para detener un subproceso, establézcalo en true. Análisis Para conocer un análisis de wait( ) y notify( ), consulte Establezca comunicación entre subprocesos. Para suspender, reanudar y detener la ejecución de un subproceso, puede establecer un bucle similar al mostrado aquí dentro del método run( ) del subproceso: volatile boolean suspendido; volatile boolean detenido; // ... // Usa un bloque sincronizado para suspender o detener variables. synchronized(this) { while(suspendido) wait( ); if(detenido) break; } Aquí, suspendido es una variable de instancia de la clase del subproceso. Para suspender el subproceso, establezca suspendido en true. Tal vez quiera hacer esto al crear un método para este fin, como se muestra aquí: // Suspende el subproceso. synchronized void miSuspendido( ) { suspendido = true; } La ventaja de usar ese método es que puede marcar suspendido como privado, lo que evita que cambie de maneras no intencionales. Para reanudar un subproceso, establezca suspendido en false y luego llame a notify( ). Una vez más, tal vez quiera hacer esto con un método, como el que se muestra aquí: // Reanuda el subproceso. synchronized void miReanudado( ) { suspendido = false; notify( ); } La llamada a notify( ) causa que regrese la llamada a wait( ) en el bloque sincronizado, con lo que www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 325 se permite que el subproceso reanude la ejecución. Para permitir que un subproceso se detenga, cree una variable llamada detenido que sea inicialmente falsa. Luego, incluya código que revise detenido. Si es verdadero, termina el subproceso. de otra manera, no emprende acción. A menudo querrá crear un método, como el mostrado aquí, que establezca el valor de detenido: // Detiene el subproceso. synchronized void miDetenido( ) { detenido = true; // Lo siguiente permite que se detenga un subproceso suspendido. suspendido = false; notify( ); } Observe que este método maneja la situación en que un subproceso suspendido se detiene. Debe permitirse que el subproceso suspendido se reanude para que pueda finalizar. Ejemplo En el siguiente ejemplo se ponen en acción las piezas que acabamos de describir y se demuestra una manera de suspender, reanudar y detener la ejecución de un subproceso. // Suspende, reanuda y detiene un subproceso. // Esta clase proporciona sus propios medios para // suspender, reanudar y detener un subproceso. class MiSubproceso implements Runnable { Thread subp; private volatile boolean suspendido; private volatile boolean detenido; MiSubproceso(String nombre) { subp = new Thread(this, nombre); suspendido = false; detenido = false; subp.start( ); } // Ejecuta el subproceso. public void run( ) { System.out.println("Iniciando " + subp.getName( )); try { for(int i = 1; i < 1000; i++) { // Despliega puntos. System.out.print("."); www.fullengineeringbook.net 326 Java: Soluciones de programación Thread.sleep(250); // Usa un bloque sincronizado para suspender o detener. synchronized(this) { // Si suspendido es true, entonces espera hasta // que se notifique. Luego vuelve a revisar // suspendido. Se asigna true a la variable // suspendido al llamar a miSuspendido( ). Se // asigna false al llamar a miReanudado( ). while(suspendido) wait( ); // Si se detiene el subproceso, se sale del bucle y se // termina el subproceso. Se asigna true a la variable // detenido al llamar a miDetenido( ). if(detenido) break; } } } catch (InterruptedException exc) { System.out.println(subp.getName( ) + " interrumpido."); } System.out.println("\nSaliendo de " + subp.getName( )); } // Detiene el subproceso. synchronized void miDetenido( ) { detenido = true; // Lo siguiente permite que se detenga un subproceso suspendido. suspendido = false; notify( ); } // Suspende el subproceso. synchronized void miSuspendido( ) { suspendido = true; } // Reanuda el subproceso. synchronized void miReanudado( ) { suspendido = false; notify( ); } } // Demuestra miSuspendido( ), miReanudado( ) y miDetenido( ). class DemoControlSubprocesos { public static void main(String args[]) { MiSubproceso ms = new MiSubproceso("MiSubproceso"); try { www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 327 // Que empiece la ejecución de ms. Thread.sleep(3000); // Suspende ms. System.out.println("\nSuspendiendo MiSubproceso."); ms.miSuspendido( ); Thread.sleep(3000); // Ahora, reanuda ms. System.out.println("\nReanudando MiSubproceso."); ms.miReanudado( ); Thread.sleep(3000); // Suspende y reanuda una segunda ocasión. System.out.println("\nSuspendiendo de nuevo MiSubproceso."); ms.miSuspendido( ); Thread.sleep(3000); System.out.println("\nReanudando de nuevo MiSubproceso."); ms.miReanudado( ); Thread.sleep(3000); // Ahora detiene el subproceso. System.out.println("\nDeteniendo el subproceso."); ms.miDetenido( ); } catch (InterruptedException e) { System.out.println("Subproceso principal interrumpido"); } } } Aquí se muestra la salida: Iniciando MiSubproceso ............ Suspendiendo MiSubproceso. Reanudando MiSubproceso. ............ Suspendiendo de nuevo MiSubproceso. . Reanudando de nuevo MiSubproceso. ............ Deteniendo el subproceso. Saliendo de MiSubproceso Opciones Como se esperaría, hay más de una manera de implementar la suspensión, reanudación y detención. Una opción útil, que se basa en un método descrito en la documentación de la API de www.fullengineeringbook.net 328 Java: Soluciones de programación Java, elimina el exceso de trabajo de ingresar repetidamente en un bloque sincronizado. Esto se realiza al revisar primero los valores de las variables suspendido y detenido antes de que se ingrese en el bloque sincronizado. Si ninguna de las dos variables es verdadera, entonces se omite el bloque sincronizado, con lo que se evita el exceso de trabajo. // Revisa suspendido y detenido antes de entrar en el bloque sincronizado. if(suspendido || detenido) synchronized(this) { // Si suspendido es true, entonces espera hasta // que se notifique. Luego vuelve a revisar // suspendido. Se asigna true a la variable // suspendido al llamar a miSuspendido( ). Se // asigna false al llamar a miReanudado( ). while(suspendido) wait( ); // Si se detiene el subproceso, se sale del bucle y se // termina el subproceso. Se asigna true a la variable // detenido al llamar a miDetenido( ). if(detenido) break; } Una pregunta que suele plantearse acerca de la suspensión y la detención de un subproceso es "Si realmente soy cuidadoso, ¿no podría usar simplemente los métodos descontinuados suspend( ) , resume( ) y stop( )?" La respuesta es ¡No! Debido a que Sun explícitamente establece que se han descontinuado, y porque su uso puede provocar problemas serios, no pueden usarse. Emplearlos Use un subproceso de daemon Componentes clave Clases Métodos java.lang.Thread final boolean isDaemon( ) final void setDaemon(boolean como) significaría, por lo menos, que su código no refleja las "mejores prácticas". Lo peor sería que su código fallara después de lanzarlo, causando daños a la propiedad o a las personas. Java soporta dos tipos generales de subprocesos: de usuario y de daemon. La diferencia entre los dos es simplemente ésta: un subproceso de daemon se terminará automáticamente cuando todos los subprocesos de usuario de una aplicación se hayan finalizado, pero un subproceso de usuario seguirá ejecutándose hasta que su método run( ) termine. Por tanto, un subproceso de daemon está inherentemente subordinado a uno de usuario. Esto es importante porque una aplicación seguirá www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 329 ejecutándose hasta que todos los subprocesos de usuario hayan finalizado, pero la aplicación no esperará a los de daemon. En esta solución se muestra cómo crear un subproceso de daemon. El uso principal de un subproceso de daemon consiste en proporcionar algún servicio usado por uno o más subprocesos de usuario, y suele ejecutarse en segundo plano. Una vez iniciado, el subproceso de daemon pasa a proporcionar ese servicio hasta que termina la aplicación. No es necesario implementar manualmente alguna especie de condición de terminación a la que debe responder el subproceso; su terminación es automática. Por supuesto, en principio un subproceso de usuario puede usarse en cualquier lugar en que se emplearía uno de daemon, pero luego tendría que terminarlo a mano. Por tanto, en casos en que la única "condición de terminación" es que el subproceso ya no sea necesario, la característica de terminación automática de un subproceso de daemon es un ventaja importante. Paso a paso Para crear y usar un subproceso de daemon, se requieren estos pasos: 1. Si un subproceso de daemon crea una instancia de Thread, entonces el nuevo subproceso será automáticamente de daemon. 2 . Si un subproceso de usuario crea una instancia de Thread, será un subproceso de usuario. Sin embargo, puede cambiarse por uno de daemon al llamar a setDaemon( ). Esta llamada debe tomar lugar antes de que el subproceso se inicia vía una llamada a start( ). 3. Puede determinar si un subproceso es de daemon al llamar a isDaemon( ). Análisis Cuando se crea, una instancia de Thread será del mismo tipo que el subproceso que la creó. Por tanto, si un subproceso de usuario crea un subproceso, éste será de usuario, como opción predeterminada. Cuando un subproceso de daemon crea uno, el nuevo subproceso será automáticamente de daemon. Para cambiar un subproceso de usuario en uno de daemon, llame a setDaemon( ), que se muestra a continuación: final void setDaemon(boolean como) Si como es true, el subproceso será de daemon; si como es false, será de usuario. En ambos casos, debe llamarse a setDaemon( )antes de que inicie el subproceso. Esto significa que debe llamarse antes de que se invoque a start( ). Si llama a setDaemon( ) en un subproceso activo, se lanza una IllegalThreadStateException. Puede determinar si un subproceso es de daemon o de usuario al llamar a isDaemon( ), que se muestra aquí: final boolean isDaemon( ) Devuelve verdadero si el subproceso que invoque es de daemon y falso si es de usuario. Ejemplo He aquí un ejemplo sencillo que ilustra un subproceso de daemon. En el programa, el subproceso ejecuta un bucle infinito que despliega puntos. Por tanto, seguirá ejecutándose hasta que el programa finalice. El subproceso principal permanece inactivo durante 10 segundos y luego finaliza. Debido a que el subproceso principal era el único subproceso de usuario en el programa, el www.fullengineeringbook.net 330 Java: Soluciones de programación subproceso de daemon se termina automáticamente cuando el subproceso principal finaliza. // Demuestra un subproceso de daemon. // Esta clase crea un subproceso de daemon. class MiDaemon implements Runnable { Thread subp; MiDaemon( ) { // Crea el subproceso. subp = new Thread(this); // Lo define como de daemon subp.setDaemon(true); // Inicia el subproceso. subp.start( ); } // Punto de entrada del subproceso. // Despliega un punto por segundo. public void run( ) { try { for(;;) { System.out.print("."); Thread.sleep(1000); } } catch(InterruptedException exc) { System.out.println("MiDaemon interrumpido."); } } } class DemoDaemon { public static void main(String args[]) { // Construye e inicia la ejecución de un subproceso MiDaemon. MiDaemon sd = new MiDaemon( ); if(sd.subp.isDaemon( )) System.out.println("sd es un subproceso de daemon."); // Mantiene el subproceso principal vivo por 10 segundos. System.out.println("Inactivo en el subproceso principal."); try { Thread.sleep(10000); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } System.out.println("\nTerminando subproceso principal."); // En este punto, el subproceso de daemon www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 331 // terminará automáticamente. } } Aquí se muestra la salida: sd es un subproceso de daemon. Inactivo en el subproceso principal. .......... Terminando subproceso principal. Como puede ver, el subproceso de daemon finaliza automáticamente cuando lo hace la aplicación. Como experimento, convierta en comentario la línea que marca que el subproceso es de daemon, como se muestra aquí: // subproceso.setDaemon(true) A continuación, vuelva a compilar y ejecutar el programa. Como verá, la aplicación ya no terminará cuando lo hace el subproceso, y seguirán desplegándose puntos. (Necesitará usar ctrl+c para detener el programa). Ejemplo adicional: una clase simple de recordatorio Un buen uso para un subproceso de segundo plano es un servicio que realiza alguna actividad en un momento predeterminado, esperando en silencio hasta que llega el momento. Uno de estos servicios es un "recordatorio" que despliega un mensaje en un momento específico para recordarle cierto evento, como una cita, una reunión o una conferencia. En el siguiente ejemplo se crea una clase de "recordatorio" simple llamada Recordatorio que implementa esta característica. Recordatorio crea un subproceso que espera un tiempo especificado en el futuro y luego despliega un mensaje. Recordatorio no está diseñado como un programa independiente, sino para usarse como un accesorio de una aplicación más grande. Por esto, está implementado como un subproceso de daemon. Si la aplicación que utiliza una instancia de Recordatorio termina antes de que se haya alcanzado el tiempo programado, el subproceso Recordatorio se terminará automáticamente. No hará que la aplicación se "cuelgue", esperando a que se alcance el tiempo determinado. Esto también significa que no hay necesidad de detener explícitamente el subproceso cuando la aplicación finaliza. Recordatorio le permite especificar el momento que se recordará de dos maneras. En primer lugar, puede usar un periodo de demora, en segundos. Esta demora se suma entonces a la hora del sistema actual. El mensaje de recordatorio se desplegará después de que haya transcurrido la demora. Este método es bueno para recordatorios que se necesitarán en el futuro cercano. En segundo lugar, puede especificar un objeto de Calendario que contiene la hora y la fecha en que quiere recibir el recordatorio. Esta manera es buena para recordatorios que se necesitan en un futuro más distante. // // // // // // // // // Una clase de recordatorio simple que ejecuta un subproceso de daemon. Para usar Recordatorio, páselo en el mensaje que se desplegará y luego especifique una demora desde la hora actual o futura en que quiera que el mensaje de recordatorio se despliegue. Si la aplicación que crea un Recordatorio finaliza antes de la hora especificada, entonces se www.fullengineeringbook.net 332 Java: Soluciones de programación // termina automáticamente el subproceso Recordatorio. import java.util.*; // Una implementación simple de una clase de "recordatorio". // Un objeto de esta clase inicia un subproceso de daemon // que espera hasta el momento especificado. Luego // despliega un mensaje. class Recordatorio implements Runnable { // Hora y fecha en que se desplegará el mensaje de recordatorio. Calendar horaRecordatorio; // Mensaje que se desplegará. String mensaje; // Usa este constructor para desplegar un mensaje // después de transcurrido un número específico de // segundos. Este valor se suma a la hora actual // para calcular la hora deseada para el recordatorio. // // En la práctica, tal vez quiera cambiar la // demora a minutos en lugar de segundos, pero // los segundos facilitan la prueba. Recordatorio(String msj, int demora) { mensaje = msj; // Obtiene la hora y la fecha actuales. horaRecordatorio = Calendar.getInstance( ); // Agrega la demora a la fecha y hora. horaRecordatorio.add(Calendar.SECOND, demora); System.out.printf("Recordatorio establecido para %tD %1$tr\n", horaRecordatorio); // Crea el subproceso de recordatorio. Thread subpD = new Thread(this); // Lo define como daemon. subpD.setDaemon(true); // Inicia la ejecución. subpD.start( ); } // Notifica a la hora y fecha especificada. Recordatorio(String msj, Calendar cal) { mensaje = msj; // Usa la fecha y hora especificadas como // hora del recordatorio. horaRecordatorio = cal; System.out.printf("Recordatorio establecido para %tD %1$tr\n", horaRecordatorio); www.fullengineeringbook.net Capítulo 7: Multiprocesamiento // Crea el recordatorio. Thread subpD = new Thread(this); // Lo define como daemon. subpD.setDaemon(true); // Inicia la ejecución. subpD.start( ); } // Ejecuta el recordatorio. public void run( ) { try { for(;;) { // Obtiene la hora y la fecha actuales. Calendar horaActual = Calendar.getInstance( ); // Revisa si es la hora del recordatorio. if(horaActual.compareTo(horaRecordatorio) >= 0) { System.out.println("\n" + mensaje + "\n"); break; // deja que termine el subproceso } Thread.sleep(1000); } } catch(InterruptedException exc) { System.out.println("Recordatorio interrumpido."); } } } class DemoRecordatorio { public static void main(String args[]) { // Obtiene un recordatorio dentro de 2 segundos. Recordatorio ms = new Recordatorio("Llamar a Alberto", 2); // Obtiene un recordatorio el 15 de abril de 2009 a las 8:35 pm. Recordatorio ms2 = new Recordatorio("Junta con Roberto", new GregorianCalendar(2009, 3, 5, 20, 35)); // Mantiene el subproceso principal vivo durante 20 segundos. for(int i=0; i < 20; i++) { try { Thread.sleep(1000); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } System.out.print("."); } www.fullengineeringbook.net 333 334 Java: Soluciones de programación System.out.println("\nFinalizando subproceso principal."); } } Aquí se muestra la salida: Recordatorio establecido para 04/15/09 08:34:56 PM Recordatorio establecido para 04/15/09 08:35:00 PM .. Llamar a Alberto .... Junta con Roberto .............. Finalizando subproceso principal. He aquí unas cuantas cosas que tal vez quiera probar. Para conocer los beneficios del uso de un subproceso de daemon para ejecutar el recordatorio, haga la prueba de convertir en comentario la llamada a setDaemon( ) en ambos constructores de Recordatorio, como se muestra aquí: // subpD.setDaemon(true): Debido a que el subproceso ya no está marcado como de daemon, sigue siendo de usuario y la aplicación no terminará hasta que haya terminado el subproceso de recordatorio. Por tanto, la aplicación "se colgará" hasta que se alcance el tiempo futuro. Para facilitar la prueba y la experimentación, el periodo de demora empleado por el primer constructor Recordatorio se supone en segundos. Sin embargo, para uso real, es probable que esta demora se especifique mejor en minutos. Tal vez quiera probar este cambio. Una optimización que sería aplicable en algunos casos consiste simplemente en hacer que el método run( ) permanezca inactivo por un periodo igual a la diferencia entre la hora en que quiere que Recordatorio se inicie y la hora futura especificada. Con este método, no es necesario seguir verificando la hora. Sin embargo, este método no funcionará en situaciones en que el recordatorio se desplegará con base en la hora del sistema, que podría cambiar (por ejemplo, si el usuario cambia las zonas horarias o si se aplica la hora de verano para aprovechar la luz solar). Por último, puede hacer la prueba de desplegar el recordatorio en una ventana emergente que contiene el mensaje en lugar de usar un método basado en consola. Esto se logra fácilmente usando las clases JFrame y JLabel de Swing. JFrame (que es un contenedor de Swing de nivel superior) crea la ventana estándar y JLabel despliega el mensaje. Para probar esto, sustituya la siguiente versión de run( ) en el programa anterior: // Despliega el recordatorio en una ventana emergente. public void run( ) { try { for(;;) { // Obtiene la fecha y la hora actuales. Calendar horaActual = Calendar.getInstance( ); // Ve si es la hora del recordatorio. if(horaActual.compareTo(horaRecordatorio) >= 0) { www.fullengineeringbook.net Capítulo 7: Multiprocesamiento // Crea una GUI en el subproceso que despacha el suceso // como lo recomienda Sun. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { // Despliega una ventana emergente en el momento adecuado. // Primero, crea una ventana para el mensaje. jfrm = new JFrame( ); // Establece su tamaño. Para mayor simplicidad, // aquí se usa un tamaño arbitrario. Sin embargo, // puede calcular un tamaño exacto para que se // amolde al mensaje, si lo quiere. jfrm.setSize(200, 50); // Crea una etiqueta que contiene el mensaje. JLabel jlab = new JLabel(mensaje); // Agrega el mensaje a la ventana. jfrm.add(jlab); // Muestra la ventana. jfrm.setVisible(true); } }); // Hace una pausa de 5 segundos. Thread.sleep(5000); // Ahora, elimina la ventana. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { // Elimina la ventana de la pantalla. jfrm.setVisible(false); // Elimina la ventana del sistema. jfrm.dispose( ); } }); break; // deja que finalice el subproceso } Thread.sleep(1000); } } catch(InterruptedException exc) { System.out.println("Recordatorio interrumpido."); } } También necesitará importar javax.swing.* y agregar la siguiente variable de instancia a la clase Recordatorio: www.fullengineeringbook.net 335 336 Java: Soluciones de programación JFrame jfrm; Como lo establece el comentario antes de la llamada a invokeLater( ), Sun recomienda que todas las GUI de Swing se construyan y actualicen desde el subproceso que despacha el suceso para evitar problemas. Por tanto, éste es el método que se usa aquí. (Para conocer más información acerca de Swing y las soluciones de Swing, consulte el capítulo 8). Opciones Aunque un subproceso de daemon se termina automáticamente cuando finaliza la aplicación que lo usa, aún es posible que un subproceso de daemon termine por cuenta propia. Por ejemplo, esta versión de run( ) del primer ejemplo terminará después de cinco iteraciones: public void run( ) { try { for(int i=0; i < 5; i++) { System.out.print("."); Thread.sleep(1000); } } catch(InterruptedException exc) { System.out.println("MiDaemon interrumpido."); } System.out.println("Finalizando el subproceso de daemon."); } Por supuesto, en la práctica, si un subproceso tiene un punto de terminación bien definido, entonces por lo general no querrá marcarlo como daemon. Sin embargo, tal vez quiera terminar un subproceso de daemon en casos en que no se necesita un servicio de segundo plano porque no se Interrumpa un subproceso Componentes clave Clases Métodos java.lang.Thread static boolean interrupted( ) void interrupt( ) ha cumplido alguna condición previa. En tales casos, la terminación del subproceso lo elimina del sistema, evitando que tenga impacto en el rendimiento. En ocasiones, es útil que un subproceso pueda interrumpir otro. Por ejemplo, es probable que un subproceso esté esperando un recurso que ya no está disponible (como la conexión de red que se ha perdido.) Un segundo subproceso podría interrumpir al primero, permitiendo tal vez que se use un www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 337 recurso alterno. Por fortuna, Java facilita la opción de que un subproceso interrumpa otro porque la clase Thread define métodos para este fin. En esta solución se demuestra su uso. Paso a paso Para interrumpir un subproceso se requieren estos pasos: 1. Para interrumpir un subproceso, llame a interrupt( ) en el subproceso. 2. Para determinar si un subproceso se ha interrumpido, llame a interrupted( ). Análisis Un subproceso puede interrumpir a otro al llamar a interrupt( ) en la instancia de Thread. Aquí se muestra cómo: void interrupt( ) El método interrupt( ) tiene algunos efectos un poco diferentes, dependiendo de lo que está haciendo el subproceso interrumpido. Si no está suspendido, entonces la llamada a interrupt( ) da como resultado que se establezca el estado de interrupción en el subproceso. dicho estado puede determinarse al llamar a interrupted( ) o isInterrupted( ), que se describirán en breve. Si el subproceso está en un estado de espera, entonces son posibles tres escenarios: La primera situación, y la más común, en que un subproceso suspendido es interrumpido, ocurre cuando el subproceso está esperando a que regrese una llamada a sleep( ), wait( ) o join( ). En este caso, la llamada a interrupted( ) da como resultado el lanzamiento de una InterruptedException al subproceso interrumpido. En el proceso, se limpia el estatus de interrumpido del subproceso. Otras dos situaciones menos comunes también son posibles. Si el subproceso está esperando en una instancia de InterruptableChannel, entonces la llamada a interrupt( ) da como resultado una ClosedByInterruptException y se establece su estatus de interrupción. Si el subproceso está esperando un Selector, entonces la llamada a interrupt( ) causa que se establezca el estado de interrumpido y el selector regresa como si se hubiera presentado una llamada a wakeup( ). Puede determinar si un subproceso se ha interrumpido al llamar a interrupted( ) o isInterrupted( ). El método usado en esta solución es interrupted( ) y se muestra a continuación: static boolean interrupted( ) Devuelve verdadero si el subproceso que invoca se ha interrumpido y falso de otra manera. En el proceso, se limpia el estatus de interrumpido del subproceso. Ejemplo En el siguiente ejemplo se muestra cómo interrumpir un subproceso. Se muestra lo que sucede cuando se interrumpe un subproceso mientras está suspendido [en este caso, por una llamada a sleep( )] y lo que sucede cuando se interrumpe mientras está activo. El punto clave es que cuando se interrumpe un subproceso mientras está suspendido debido a una llamada a sleep( ), join( ) o wait( ), recibe una InterruptedException. Si se interrumpe mientras está activo, se establece su estado de interrumpido, pero no se recibe esta excepción. // Interrumpe un subproceso. // Esta clase maneja la interrupción de dos maneras. // En primer lugar, si se interrumpe mientras se usa www.fullengineeringbook.net 338 Java: Soluciones de programación // sleep( ), captura la InterruptedException que se lanzará. // En segundo lugar, llama a interrupted( ) mientras // está activo para revisar su estado de interrupción. // Si se interrumpe mientras está activo, el subproceso // termina. class MiSubproceso implements Runnable { // Ejecuta el subproceso. public void run( ) { String nombreSubp = Thread.currentThread( ).getName( ); System.out.println("Iniciando " + nombreSubp); try { // En primer lugar, permanece inactivo por 3 segundos. // Si se interrumpe sleep( ), entonces se recibirá una // InterruptedException. Thread.sleep(3000); // Luego, mantiene activo el subproceso al desplegar // puntos. Usa un bucle de demora en lugar de sleep( ) // para hacer más lento el subproceso. Esto significa // que el subproceso permanece activo. La interrupción // del subproceso en este punto no causa una // InterruptedException. En cambio, se establece su // estatus de interrumpido. for(int i = 1; i < 1000; i++) { if(Thread.interrupted( )) { System.out.println("Subproceso interrumpido mientras est\u00a0 activo."); break; } // Despliega puntos. System.out.print("."); // No queda inactivo en este punto. En cambio, quema // tiempo de la CPU para mantener activo el subproceso. for(long x = 0; x < 10000000; x++) ; } } catch (InterruptedException exc) { System.out.println(nombreSubp + " interrumpido."); } System.out.println("Saliendo de " + nombreSubp); } } // Demuestra la interrupción del subproceso. class DemoInterrumpido { public static void main(String args[]) { MiSubproceso ms = new MiSubproceso( ); www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 339 MiSubproceso ms2 = new MiSubproceso( ); Thread subp = new Thread(ms, "MiSubproceso #1"); Thread subp2 = new Thread(ms2, "MiSubproceso #2"); try { // Inicia el subproceso. subp.start( ); // Da a subp tiempo para ejecutarse. Thread.sleep(1000); // Ahora, interrumpe subp cuando está inactivo. subp.interrupt( ); // A continuación, inicia el segundo subproceso. subp2.start( ); System.out.println( ); // Esta vez, espera hasta que empieza subp2 // mostrando puntos. Thread.sleep(4000); // Ahora, interrumpe subp2 cuando está activo. subp2.interrupt( ); } catch (InterruptedException e) { System.out.println("Subproceso principal interrumpido"); } } } Aquí se muestra la salida. (La salida exacta puede variar.) Iniciando MiSubproceso #1 MiSubproceso #1 interrumpido. Saliendo de MiSubproceso #1 Iniciando MiSubproceso #2 ...........Subproceso interrumpido mientras está activo. Saliendo de MiSubproceso #2 Opciones En el programa de ejemplo, una interrupción que ocurre mientras el subproceso está suspendido se maneja de manera diferente a como manejaría una interrupción que ocurre mientras el subproceso se está ejecutando. Esta suele ser la opción preferida, porque una interrupción a wait( ), sleep( ) o join( ) podría requerir una respuesta diferente a la que se necesita cuando se interrumpe un subproceso activo. Sin embargo, ambos tipos de interrupciones pueden manejarse de la misma manera en casos en que la misma respuesta resulta apropiada. Simplemente se lanza una InterruptedException si interrupted( ) devuelve verdadero. Esto da como resultado que el www.fullengineeringbook.net 340 Java: Soluciones de programación manejador InterruptedException del subproceso capture la excepción y procese la interrupción. Para ver este método en acción, sustituya este bloque try en el método run( ) de MiSubproceso en el ejemplo: try { // En primer lugar, permanece inactivo por 3 segundos. // Si se interrumpe sleep( ), entonces se recibirá una // InterruptedException. Thread.sleep(3000); // Luego, mantiene activo el subproceso al desplegar // puntos. Usa un bucle de demora en lugar de sleep( ) // para hacer más lento el despliegue. Esto significa // que el subproceso permanece activo. La interrupción // del subproceso en este punto no causa una // InterruptedException. En cambio, se establece su // estatus de interrumpido. for(int i = 1; i < 1000; i++) { if(Thread.interrupted( )) { // Lanza una excepción para que ambos tipos de // interrupciones sean procesados por el mismo manejador. throw new InterruptedException( ); } // Despliega puntos. System.out.print("."); // Quema tiempo de la CPU para mantener activo el subproceso. for(long x = 0; x < 10000000; x++) ; } } catch (InterruptedException exc) { System.out.println(nombreSubp + " interrumpido."); } En esta versión, preste especial atención a su instrucción if: if(Thread.interrupted( )) { // Lanza una excepción para que ambos tipos de // interrupciones sean procesados por el mismo manejador. throw new InterruptedException( ); } Si interrupted( ) devuelve verdadero, se crea y se lanza un objeto de InterruptedException. Esta excepción será capturada por la instrucción catch. Esto permite que cada tipo de interrupción sea manejado por el mismo manejador. El método interrupted( ) restablece el estatus de interrumpido del subproceso. Si no quiere limpiar este estatus, entonces puede revisar una interrupción al llamar a isInterrupted( ) en lugar de Thread. Aquí se muestra: boolean isInterrupted( ) Devuelve verdadero si el subproceso que invoca se ha interrumpido y falso de otra manera. El estatus de interrumpido del subproceso permanece sin cambio. Por tanto, puede llamarse www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 341 Establezca y obtenga una prioridad de subproceso Componentes clave Clases Métodos java.lang.Thread void setPriority(int nivel) int getPriority( ) static void yield( ) repetidamente y regresará el mismo resultado. Este método es muy útil en algunas situaciones porque permite que un subproceso tome un curso diferente de acción si se le interrumpe mientras está activo o si se está ejecutando normalmente. Cada subproceso tiene asociado su configuración de prioridad. El programador usa la prioridad del subproceso para decidir cuándo se ejecuta un subproceso. Por tanto, en una aplicación de multiprocesamiento, la prioridad de un subproceso determina (en parte) cuánto tiempo de CPU recibe un subproceso en relación con otros subprocesos activos. En general, los subprocesos de baja prioridad reciben poco; los subprocesos de alta prioridad reciben mucho. Debido a que la prioridad del subproceso afecta el acceso del subproceso a la CPU, tiene un impacto profundo en las características de ejecución del subproceso y la manera en que éste interactúa con otros subprocesos. En esta solución se muestra cómo establecer la prioridad de un subproceso e ilustra el efecto que tienen diferentes prioridades sobre la ejecución del subproceso. También muestra cómo un subproceso puede llamar a otro. Antes de pasar a la solución, es necesario aclarar un punto: aunque la prioridad de un subproceso juega un papel importante en la determinación del momento en que un subproceso tiene acceso a la CPU, no es el único factor. Si un subproceso está bloqueado, como cuando se espera a que un recurso quede disponible, se suspenderá, permitiendo que otro subproceso se ejecute. Por tanto, si un subproceso de alta prioridad está esperando algún recurso, se ejecutará uno de menor prioridad. Más aún, en algunos casos un subproceso de alta prioridad puede en realidad ejecutarse con menor frecuencia que un subproceso de baja prioridad. Por ejemplo, considere una situación en que un subproceso de alta prioridad se usa para obtener datos de una red. Debido a que la transmisión de datos en una red es relativamente lenta, en comparación con la velocidad de la CPU, el subproceso de alta prioridad gastará mucho de su tiempo esperando. Mientras el subproceso de alta prioridad está suspendido, puede ejecutarse un subproceso de menor prioridad. Por supuesto, cuando los datos quedan disponibles, el calendarizador puede hacer a un lado al subproceso de baja prioridad y reanudar la ejecución del subproceso de alta prioridad. Otro factor que afecta la calendarización de subprocesos es la manera en que el sistema operativo implementa multitareas, y si el sistema operativo usa la calendarización preferente o no preferente. El punto clave es que sólo porque se da a un subproceso una alta prioridad y a otro una baja prioridad, eso no necesariamente significa que un subproceso se ejecutará más rápido que el otro. Es sólo que el subproceso de alta prioridad tiene mayor posibilidad de acceso a la CPU. Paso a paso Para establecer y obtener la prioridad de un subproceso se requieren estos pasos: 1. Para establecer la prioridad de un subproceso, llame a setPriority( ). 2. Para obtener la prioridad de un subproceso, llame a getPriority( ). www.fullengineeringbook.net 342 Java: Soluciones de programación 3. Para forzar a un subproceso de alta prioridad a ceder ante un subproceso de baja prioridad, llame a yield( ). Análisis Cuando se crea un subproceso, se le da la misma prioridad que al subproceso que lo crea. Para cambiar la prioridad, llame a setPriority( ). Ésta es la forma general: final void setPriority(int nivel) Aquí, nivel especifica el nuevo parámetro de prioridad para el subproceso que llama. El valor de nivel debe estar dentro del rango MIN_PRIORITY y MAX_PRIORITY. Actualmente, estos valores son 1 y 10, respectivamente. La prioridad predeterminada se especifica mediante el valor NORM_PRIORITY, que en la actualidad es 5. Estas prioridades se definen como variable static final dentro de Thread. Puede obtener la prioridad actual al llamar al método getPriority( ) de Thread, que se muestra aquí: final int getPriority( ) El valor devuelto estará dentro del rango especificado por MIN_PRIORITY y MAX_PRIORITY. Puede causar que un subproceso ceda la CPU al llamar a yield( ), que se muestra aquí: static void yield( ) Al llamar a yield( ), un subproceso permite otros subprocesos, incluidos los de baja prioridad, para obtener acceso a la CPU. Ejemplo En el siguiente ejemplo se demuestran dos subprocesos con diferentes prioridades. Los subprocesos se crean como instancias de la clase SubpPrioridad. El método run( ) contiene un bucle que cuenta el número de iteraciones. El bucle se detiene cuando la cuenta alcanza 100 000 000 o la variable estática detener es true. Al principio, detener es false, pero el primer subproceso en terminar la cuenta establece detener en true. Esto causa que el segundo subproceso termine con su siguiente fragmento de tiempo. Después de que ambos subprocesos se detienen, se despliega el número de iteraciones para cada bucle. // Demuestra las prioridades de un subproceso. class SubpPrioridad implements Runnable { long cuenta; Thread subp; static boolean detener = false; // Construye un nuevo subproceso empleando la // prioridad especificada por pri. SubpPrioridad(String nombre, int pri) { subp = new Thread(this, nombre); // Establece la prioridad. subp.setPriority(pri); www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 343 cuenta = 0; subp.start( ); } // Empieza la ejecución del subproceso. public void run( ) { do { cuenta++; if((cuenta % 10000) == 0) { if(subp.getPriority( ) > Thread.NORM_PRIORITY) // Para ver el efecto de yield( ), elimine la marca de // comentario de la siguiente línea y convierta en // comentario la línea de marcador de posición // "cuenta = cuenta". Thread.yield( ); // cede ante un subproceso de baja prioridad cuenta = cuenta; // marcador de posición que no hace nada } // } while(detener == false && cuenta < 100000000); detener = true; } } class DemoPrioridad { public static void main(String args[]) { SubpPrioridad ms2 = new SubpPrioridad("Baja prioridad", Thread.NORM_PRIORITY–1); SubpPrioridad ms1 = new SubpPrioridad("Alta prioridad", Thread.NORM_PRIORITY+1); try { ms1.subp.join( ); ms2.subp.join( ); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } System.out.println("\nEl subproceso de alta prioridad llegu00a2 a una cuenta de " + ms1.cuenta); System.out.println("El subproceso de baja prioridad llegu00a2 a una cuenta de " + ms2.cuenta); } } He aquí una ejecución de ejemplo: El subproceso de alta prioridad tiene una cuenta de 100000000 El subproceso de baja prioridad tiene una cuenta de 3622690 Como puede ver, el subproceso de alta prioridad recibió la mayor parte del tiempo de la CPU. www.fullengineeringbook.net 344 Java: Soluciones de programación Esté consciente de que los resultados especificados variarán, dependiendo de la versión del sistema del motor en tiempo de ejecución de Java que está usando, su sistema operativo, velocidad de procesador y carga de tarea. Para ver el efecto de yield( ), elimine los comentarios de la llamada a yield( ) en run( ) y convierta en comentario la instrucción count = count; (que es simplemente un marcador de posición). Por tanto, las instrucciones if anidadas dentro de run( ) deben tener este aspecto: if((cuenta % 10000) == 0) { if(subp.getPriority( ) > Thread.NORM_PRIORITY) // Para ver el efecto de yield( ), elimine la marca de // comentario de la siguiente línea y convierta en // comentario la línea de marcador de posición // "cuenta = cuenta". Thread.yield( ); // cede ante un subproceso de baja prioridad // cuenta = cuenta; // marcador de posición que no hace nada } Debido a que el subproceso de alta prioridad suele ceder la CPU, el subproceso de baja prioridad obtendrá más acceso a la CPU. Aunque variarán los resultados específicos, he aquí los resultados de una ejecución de ejemplo: El subproceso de alta prioridad tiene una cuenta de 46250000 El subproceso de baja prioridad tiene una cuenta de 100000000 Observe que, debido a las llamadas a yield( ), ¡en realidad, el subproceso de baja prioridad termina primero! Esto ilustra gráficamente el hecho de que el establecimiento de la prioridad de un subproceso es sólo uno de varios factores que afecta la cantidad de acceso a la CPU que recibe un subproceso. Opciones Aunque yield( ) es una buena manera para que un subproceso ceda la CPU, no es la única. Por ejemplo, sleep( ), wait( ) y join( ) causan la invocación al subproceso para suspender la ejecución. Por lo general, la suspensión de un subproceso implícitamente cede la CPU. Debido a las muchas variaciones que pueden ocurrir en los entornos en que podría ejecutarse un programa de Java, no es posible generalizar el comportamiento específico de un programa de multiprocesamiento experimentado en un entorno. Por tanto, cuando se establecen las prioridades de un subproceso, las características de ejecución de su programa en una situación no podrían ser las mismas que en otra. No debe depender de prioridades de subproceso para alcanzar un flujo Monitoree el estado de un subproceso Componentes clave Clases Métodos java.lang.Thread Thread.State getState( ) final boolean isAlive( ) www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 345 deseado de ejecución. En cambio, debe usar comunicación entre subprocesos vía wait( ) y notify( ). Recuerde que la prioridad de un subproceso afecta a su posible acceso a la CPU pero no garantiza ninguna especia de sincronización entre subprocesos. La clase Thread proporciona dos métodos que le permiten obtener información acerca del estado de un subproceso: isAlive( ) y getState( ). Como se explicó en Espere a que termine un subproceso, isAlive( ) devuelve verdadero si el subproceso está activo en el sistema y falso si ha terminado. Aunque isAlive( ) es útil, a partir de Java 5, con el método getState( ) obtendrá información muy fina acerca del estado de un subproceso, incluido si es ejecutable, o si está en espera o bloqueado. Empleando isAlive( ) y getState( ), es posible monitorear por completo el estado de un subproceso. En esta solución se demuestra el proceso. Paso a paso Para obtener información de estado acerca de un subproceso se requieren los siguientes pasos: 1. Para determinar si un subproceso está vivo, llame a isAlive( ). 2. Para obtener el estado de un subproceso, llame a getState( ). Análisis Cuando se trabaja con subprocesos, a menudo necesitará saber si un subproceso está vivo, o si ha terminado. Para hacer esta determinación, llame a isAlive( ), que se muestra aquí: final boolean isAlive( ) Devuelve verdadero si el subproceso que invoca está vivo. Un subproceso está vivo si se ha llamado a su método run( ) pero no ha regresado aún. (Un subproceso que se ha creado pero que aún no está vivo). Devuelve falso si el subproceso ha terminado. El método isAlive( ) tiene muchos usos. Por ejemplo, podría usar isAlive( ) para confirmar que un subproceso está activo antes de esperar un recurso producido por ese subproceso. Puede obtener información muy detallada acerca del estado de un subproceso al llamar a getState( ), que se muestra aquí: Thread.State getState( ) Valor de estado Significado BLOCKED El subproceso está bloqueado, lo que significa que está esperando acceso a un código synchronized. NEW Se ha creado el subproceso, pero aún no se ha llamado a su método start( ). RUNNABLE El subproceso se está ejecutando o se ejecutará en cuanto obtenga acceso a la CPU. TERMINATED El subproceso ha finalizado. Un subproceso termina cuando regresa su método run( ), o cuando el subproceso se detiene mediante una llamada a stop( ). (Observe que stop( ) es obsoleto y no debe usarse). TIMED_WAITING El subproceso está suspendido, a la espera de otro subproceso por un periodo específico. Esto puede ocurrir debido a una llamada a las versiones con tiempo límite de espera de sleep( ), wait( ) o join( ), por ejemplo. WAITING El subproceso está suspendido, esperando a otro subproceso. Esto puede ocurrir debido a una llamada a las versiones con tiempo límite de espera de wait( ) o join( ), por ejemplo. www.fullengineeringbook.net 346 Java: Soluciones de programación Devuelve un objeto de State que representa el estado actual del subproceso. State es una enumeración definida por Thread que tiene los siguientes valores: Aunque normalmente no será necesario monitorear el estado de un subproceso en código liberado, puede ser muy útil durante el desarrollo, la depuración y la afinación del rendimiento. También puede ser útil para crear instrumentación personalizada que monitoree el estado de los subprocesos en una aplicación de multiprocesamiento. Como se mencionó, getState( ) se agregó en Java 5, de modo que se necesita una versión moderna de Java para usarlo. Ejemplo En el siguiente ejemplo se crea un método llamado mostrarEstatusSubproceso( ) que usa isAlive( ) y getState( ) para desplegar el estatus de un subproceso. En el programa se ilustra mostrarEstatusSubproceso( ) al crear un subproceso que luego hace que el subproceso ingrese en varios estados. Se despliega cada estado. // Monitorea el estatus de un subproceso. class MiSubproceso implements Runnable { int cuenta; boolean contenedor; boolean listo; MiSubproceso( ) { cuenta = 0; contenedor = true; listo = false; } // Punto de entrada del subproceso. public void run( ) { // Obtiene el nombre de este subproceso. String nombreSubp = Thread.currentThread( ).getName( ); System.out.println("Iniciando " + nombreSubp); // Quema tiempo de la CPU. System.out.println(nombreSubp + " est\u00a0 usando la CPU."); while(contenedor) ; // no hace nada // Ahora, entra en espera mediante una llamada a wait( ). System.out.println("esperando..."); w( ); // ejecuta una llamada a wait( ) en este subproceso. // Luego, entra en estado de espera cronometrada // mediante una llamada a sleep( ). try { System.out.println("En inactividad..."); Thread.sleep(1000); } catch(InterruptedException exc) { System.out.println(nombreSubp + " interrumpido."); } www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 347 System.out.println(nombreSubp + " terminando."); } // Ejecuta una llamada a wait( ). synchronized void w( ) { try { while(!listo) wait( ); } catch(InterruptedException exc) { System.out.println("wait( ) interrumpido"); } } // Ejecuta una llamada a notify( ). synchronized void n( ) { listo = true; notify( ); } } class DemoEstadoSubproceso { public static void main(String args[]) { try { // Construye un objeto de MiSubproceso. MiSubproceso ms = new MiSubproceso( ); Thread subp = new Thread(ms, "MiSubproceso #1"); // Muestra el estado de un subproceso recién creado. System.out.println("MiSubproceso #1 creado pero no iniciado."); mostrarEstatusSubproceso(subp); // Muestra el estado del subproceso que se está ejecutando. System.out.println("Llamando a start( ) en MiSubproceso #1."); subp.start( ); Thread.sleep(50); // deja que se ejecute el subproceso MiSubproceso #1 mostrarEstatusSubproceso(subp); // Muestra el estado de un subproceso que espera en wait( ). ms.contenedor = false; // deja que MiSubproceso #1 entre a llamar a wait( ) Thread.sleep(50); // deja que se ejecute el subproceso MiSubproceso #1 mostrarEstatusSubproceso(subp); // Deja que MiSubproceso #1 siga adelante al llamar a notify( ). // Esto deja que MiSubproceso #1 entre en inactividad. ms.n( ); Thread.sleep(50); // deja que se ejecute el subproceso MiSubproceso #1 // Ahora, muestra el estado de un subproceso inactivo. mostrarEstatusSubproceso(subp); www.fullengineeringbook.net 348 Java: Soluciones de programación // Espera a que finalice un subproceso. while(subp.isAlive( )) ; // Muestra el estatus final. mostrarEstatusSubproceso(subp); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); } } // Muestra el estatus de un subproceso. static void mostrarEstatusSubproceso(Thread subp) { System.out.println("Estatus de " + subp.getName( ) + ":"); if(subp.isAlive( )) System.out.println(" else System.out.println(" Vivo"); System.out.println(" No vivo"); El estado es " + subp.getState( )); System.out.println( ); } } Aquí se muestra la salida: MiSubproceso #1 creado pero no iniciado. Estatus de MiSubproceso #1: No vivo El estado es NEW Llamando a start( ) en MiSubproceso #1. Iniciando MiSubproceso #1 MiSubproceso #1 está usando la CPU. Estatus de MiSubproceso #1: Vivo El estado es RUNNABLE esperando... Estatus de MiSubproceso #1: Vivo El estado es WAITING En inactividad... Estatus de MiSubproceso #1: Vivo El estado es TIMED_WAITING MiSubproceso #1 terminando. www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 349 Estatus de MiSubproceso #1: No vivo El estado es TERMINATED Ejemplo adicional: un monitor de subprocesos en tiempo real Empleando las capacidades de Thread, es posible crear un monitor de subprocesos que despliegue en tiempo real el estatus de un subproceso, incluido su nombre, estado, prioridad y si está vivo o muerto. El monitor se ejecuta en su propio subproceso, de modo que es plenamente independiente del subproceso bajo escrutinio. Al monitor se le pasa una referencia al subproceso que se monitoreará. Luego se usa un cronómetro para actualizar, a intervalos regulares, el despliegue del estatus del subproceso dentro de una ventana de Swing. La velocidad de actualización suele establecerse al principio en 100 milisegundos, pero puede ajustarse empleando un contador. Por tanto, el estatus del subproceso puede vigilarse durante su ejecución. Un monitor de subprocesos puede ser una herramienta muy útil cuando se depura y afinan aplicaciones de multiprocesamiento, porque permite ver lo que está haciendo un subproceso durante la ejecución. En otras palabras, un monitor de subprocesos proporciona una ventana al perfil de ejecución del subproceso que se está monitoreando. Debido a que los cambios al estado del subproceso pueden verse en tiempo real, es posible detectar algunos tipos de cuellos de botella, estados de espera inesperados y puntos muertos a medida que se presentan. Aunque el monitor de subprocesos que se muestra aquí es muy simple, aún es útil y puede expandirse y mejorarse fácilmente para adecuarse mejor a sus necesidades. El monitor de subprocesos se define con la clase MonitorSubprocesos que se muestra aquí: // Un monitor de subprocesos en tiempo real. import import import import javax.swing.*; javax.swing.event.*; java.awt.*; java.awt.event.*; class MonitorSubprocesos { JSpinner jcVelMuestra; JLabel jetqNombre; // muestra el nombre del subproceso JLabel jetqEstado; // muestra el estado del subproceso JLabel jetqVivo; // muestra el resultado de isAlive( ) JLabel jetqPri; // muestra la prioridad JLabel jetqVel; // etiqueta del contador de velocidad de muestra Thread subp; // referencia al subproceso que se está monitoreando Timer cronoSubp; // cronómetro para actualizar el estatus del subproceso // Pasa en el subproceso que habrá de monitorearse. MonitorSubprocesos(Thread s) { // Crea un nuevo contenedor de JFrame. JFrame jfrm = new JFrame("Monitor de subprocesos"); // Especifica el administrador de FlowLayout. jfrm.getContentPane( ).setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jfrm.setSize(300, 160); www.fullengineeringbook.net 350 Java: Soluciones de programación // Termina el programa cuando el usuario cierra la aplicación. // Elimine o cambie esto si lo desea. jfrm.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Almacena la referencia al subproceso. subp = s; // Crea un modelo de contador de enteros. SpinnerNumberModel spm = new SpinnerNumberModel(100, 1, 5000, 10); // Crea un JSpinner empleando el modelo. jcVelMuestra = new JSpinner(spm); // Establece el tamaño preferido del contador. jcVelMuestra.setPreferredSize(new Dimension(50, 20)); // Agrega un escucha de cambios para el contador Velocidad de muestra. jcVelMuestra.addChangeListener(new ChangeListener( ) { public void stateChanged(ChangeEvent ce) { cronoSubp.setDelay((Integer)jcVelMuestra.getValue( )); } }); // Elabora e inicializa las etiquetas. jetqNombre = new JLabel("Nombre del subproceso: " + subp.getName( )); jetqNombre.setPreferredSize(new Dimension(260, 22)); jetqEstado = new JLabel("Estado actual: " + subp.getState( )); jetqEstado.setPreferredSize(new Dimension(260, 22)); jetqVivo = new JLabel("Subproceso vivo: " + subp.isAlive( )); jetqVivo.setPreferredSize(new Dimension(260, 22)); jetqPri = new JLabel("Prioridad actual: " + subp.getPriority( )); jetqPri.setPreferredSize(new Dimension(260, 22)); jetqVel = new JLabel("Velocidad de muestra: "); // Crea un escucha de acción para el cronómetro. // Cada vez que el cronómetro se agota, actualiza el // despliegue del monitor. ActionListener cronoAL = new ActionListener( ) { public void actionPerformed(ActionEvent ae) { updateStatus( ); } }; // Crea el tiempo de actualización usando cronoAL. cronoSubp = new Timer(100, cronoAL); // Usa dos cuadros para contener los componentes. Box cuadrover = Box.createVerticalBox( ); cuadrover.add(jetqNombre); cuadrover.add(jetqPri); cuadrover.add(jetqEstado); cuadrover.add(jetqVivo); www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 351 jfrm.add(cuadrover); Box cuadrohor = Box.createHorizontalBox( ); cuadrohor.add(jetqVel); cuadrohor.add(jcVelMuestra); jfrm.add(cuadrohor); // Despliega el marco. jfrm.setVisible(true); // Inicia el cronómetro. Cada vez que se agota // se actualiza el despliegue del monitor. cronoSubp.start( ); } // Actualiza la información del subproceso. void updateStatus( ) { jetqNombre.setText("Nombre del subproceso: " + subp.getName( )); jetqEstado.setText("Estado actual: " + subp.getState( )); jetqVivo.setText("Subproceso vivo: " + subp.isAlive( )); jetqPri.setText("Prioridad actual: " + subp.getPriority( )); } } El código es simple y en los comentarios se describe cada paso. He aquí algunos hechos destacados. Cuando se crea un MonitorSubprocesos, debe pasarse una referencia al subproceso que habrá de monitorearse. Esta referencia se almacena en la variable de instancia subp. A continuación, se crea la GUI del monitor. Observe que se usa un contador para establecer la velocidad de muestreo. Es la velocidad a la que se actualizará el despliegue. Esta velocidad es, inicialmente, de 100 milisegundos, pero puede cambiar la velocidad durante la ejecución. El rango está limitado a 10 a 5 000 milisegundos, pero puede expandirlo si lo desea. La velocidad de muestreo se usa para establecer el periodo de demora de un cronómetro de Swing, que es una instancia de javax.swing.Timer. Cada vez que el cronómetro se agota, un suceso de acción se envía a todos los escuchas de acción registrados del cronómetro. En este caso, sólo hay un escucha. Maneja el suceso de acción del cronómetro al llamar a updateStatus( ), que actualiza la GUI para reflejar el estado actual del subproceso. NOTA Para conocer información acerca de Swing, y de soluciones que lo usen, consulte el capítulo 8. Puede poner en acción el monitor de subprocesos al sustituir con esta versión el DemoEstadoSubproceso del programa del ejemplo anterior. (También necesita importar javax. swing.*). En lugar de mostrar el estatus de un subproceso en la consola, lo muestra en tipo real, en la ventana del monitor. // Esta versión de DemoEstadoSubproceso usa un MonitorSubprocesos // para reportar el nombre, el estado y la prioridad en tiempo real de un subproceso. class DemoEstadoSubproceso { www.fullengineeringbook.net 352 Java: Soluciones de programación public static void main(String args[]) { try { // Construye un objeto de MiSubproceso. MiSubproceso ms = new MiSubproceso( ); final Thread subp = new Thread(ms, "MiSubproceso #1"); // Crea el monitor de subproceso. Como MonitorSubprocesos crea // una GUI de Swing, debe crearse una instancia de MonitorSubprocesos // en el suceso que despacha el subproceso. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new MonitorSubprocesos(subp); } }); // Usa sleep( ) aquí y en todos los lugares para hacer // más lenta la ejecución y permitir que se vean los // estados de los diversos subprocesos. Thread.sleep(3000); // Inicia el subproceso. subp.start( ); Thread.sleep(3000); // Muestra el estado de un subproceso que espera a wait( ). ms.contenedor = false; // deja que MiSubproceso #1 ingrese la llamada a wait( ) Thread.sleep(3000); // Cambia la prioridad del subproceso. System.out.println("Cambiando la prioridad del subproceso."); subp.setPriority(Thread.NORM_PRIORITY–2); Thread.sleep(3000); // Cambia el nombre del subproceso por MiSubproceso ALFA. System.out.println("Cambiando el nombre a MiSubproceso ALFA."); subp.setName("MiSubproceso ALFA"); Thread.sleep(3000); // Deja que el subproceso siga adelante al llamar a notify( ). // Esto permite que MiSubproceso #1 entre en inactividad. ms.n( ); Thread.sleep(3000); System.out.println("Terminando subproceso principal."); } catch(InterruptedException exc) { System.out.println("Subproceso principal interrumpido."); www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 353 } } } Aquí se muestra un ejemplo de la ventana del monitor: He aquí algunas mejoras que tal vez quiera agregar. En primer lugar, agregue un contador que le permita establecer la prioridad del subproceso que se está monitoreando. Por tanto, además de desplegar la prioridad del subproceso, puede establecerla. Esta característica le permitiría experimentar con diferentes prioridades, ayudándole a afinar las características de ejecución del subproceso. En segundo lugar, trate de agregar botones que suspendan, reanuden y detengan el subproceso. En tercer lugar, tal vez quiera desplegar cuando el subproceso se interrumpe. Por último, tal vez quiera indicar si el subproceso es de usuario o de daemon. Opciones Aunque no suele ser necesario, hay otra pieza de información relacionada con los subprocesos que puede ser útil en algunos casos: el estado de bloqueo de un objeto. Por ejemplo, cuando depura código de multiprocesamiento, en ocasiones es útil saber si un subproceso contiene el bloqueo sobre algún objeto. Esta pregunta puede responderse al llamar a holdsLock( ), que se muestra aquí: Use un grupo de subprocesos Componentes clave Clases Métodos java.lang.Thread java.lang.ThreadGroup int activeCount( ) int enumerate(Thread[] subps) final void interrupt( ) static boolean holdsLock(Object obj) El objeto en cuestión se pasa en obj. Si el subproceso que llama contiene ese bloqueo de objeto, entonces holdsLock( ) devuelve verdadero. De otra manera, devuelve falso. En una aplicación grande no es poco común tener varios subprocesos de ejecución separados, pero www.fullengineeringbook.net 354 Java: Soluciones de programación relacionados. En tales casos, en ocasiones es útil tratar de manera colectiva con estos subprocesos, como un grupo, en lugar de hacerlo individualmente. Por ejemplo, considere una situación en que varios usuarios están accediendo a una base de datos. Una implementación posible es crear un subproceso separado para cada usuario. De esta manera, las consultas de la base de datos pueden manejarse con facilidad de manera asincrónica e independiente. Si estos subprocesos se administran como grupo, entonces algún suceso que afecta a todos ellos, como la pérdida de una conexión de red, puede manejarse fácilmente al interrumpir todos los subprocesos del grupo. Para manejar grupos de subprocesos, Java proporciona la clase ThreadGroup, que está empaquetada en java. lang. En esta solución se demuestra su uso. Paso a paso El uso de un grupo de subprocesos incluye los pasos siguientes: 1. Cree un ThreadGroup. 2. Cree cada subproceso que será parte del grupo, empleando uno de los constructores de Thread que permiten que se especifiquen grupos de subproceso. 3. Para obtener una lista de los subprocesos en el grupo, llame a enumerate( ). 4. Para obtener un estimado del número de subprocesos activos en el grupo, llame a activeCount( ). 5. Para interrumpir todos los subprocesos de un grupo, llame a interrupt( ) en la instancia de ThreadGroup. Análisis ThreadGroup define dos constructores. Aquí se muestra el usado en esta solución: ThreadGroup(String nombre) El nombre del ThreadGroup se pasa en nombre. Los subprocesos se agregan a un grupo cuando se crean. Thread proporciona varios constructores que toman un argumento de ThreadGroup. El usado en esta solución se muestra a continuación: Thread(ThreadGroup tg, Runnable objSubp, String nombre) El subproceso se volverá parte del ThreadGroup especificado por tg. Una referencia a una instancia de una clase que implementa Runnable se pasa en objSubp. El nombre del subproceso se pasa en NombreSubp. (Consulte Fundamentos del multiprocesamiento para conocer detalles de los otros constructores de Thread.) Puede obtener una lista de todos los subprocesos de un grupo al usar esta forma de enumerate( ): int enumerate(Thread[] subps, boolean todos) Se devuelve todos los subprocesos que invocan a ThreadGroup en la matriz aludida por subps. Se devuelve el número de subprocesos almacenados actualmente en subps. Sin embargo, tenga cuidado. No se reportan errores si subps no es lo suficientemente grande para contener todos los subprocesos. Por tanto, la longitud de subps debe ser suficiente para contener todos los subprocesos activos con el fin de evitar problemas. Puede obtener el número de subprocesos activos (no terminados) en el grupo al llamar a www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 355 activeCount( ), que se muestra aquí. int activeCount( ) Se devuelve el número de subprocesos activos en el ThreadGroup (y en cualquier grupo secundario del grupo que invoca como principal). Sin embargo, esté consciente de que si los subprocesos están agregándose al grupo o se están terminando, entonces el número devuelto podría ser inexacto para el momento en que lo recibe.Puede interrumpir todos los subprocesos de un grupo al llamar a interrupt( ), que se muestra a continuación: final void interrupt( ) La llamada a interrupt( ) en una instancia de ThreadGroup da como resultado que una versión de interrupt( ) de Thread se llame en cada subproceso del grupo. Consulte Interrumpa un subproceso para conocer más detalles. Ejemplo En el siguiente ejemplo se demuestra un grupo de subprocesos. Se crean cuatro subprocesos, se despliega la cuenta activa, se enumeran los subprocesos, se detiene un subproceso, se despliega la cuenta activa actualizada y luego se interrumpe el resto de los subprocesos. // Usa un ThreadGroup. class MiSubproceso implements Runnable { private volatile boolean detenido; MiSubproceso( ) { detenido = false; } // Ejecuta el subproceso. public void run( ) { String nombreSubp = Thread.currentThread( ).getName( ); System.out.println("Iniciando " + nombreSubp); try { for(int i = 1; i < 1000; i++) { // Despliega puntos System.out.print("."); Thread.sleep(250); synchronized(this) { if(detenido) break; } } } catch (InterruptedException exc) { System.out.println(nombreSubp + " interrumpido."); } System.out.println("Saliendo de " + nombreSubp); } // Detiene el subproceso. www.fullengineeringbook.net 356 Java: Soluciones de programación synchronized void miDetenido( ) { detenido = true; } } // Demuestra la interrupción del subproceso. class DemoTG { public static void main(String args[]) { MiSubproceso ms = new MiSubproceso( ); MiSubproceso ms2 = new MiSubproceso( ); MiSubproceso ms3 = new MiSubproceso( ); MiSubproceso ms4 = new MiSubproceso( ); // Crea un grupo de subprocesos. ThreadGroup tg = new ThreadGroup("Mi grupo"); // Coloca cada subproceso en el grupo. Thread subp = new Thread(tg, ms, "MiSubproceso #1"); Thread subp2 = new Thread(tg, ms2, "MiSubproceso #2"); Thread subp3 = new Thread(tg, ms3, "MiSubproceso #3"); Thread subp4 = new Thread(tg, ms4, "MiSubproceso #4"); // Inicia cada subproceso. subp.start( ); subp2.start( ); subp3.start( ); subp4.start( ); try { // Deja que los otros subprocesos se ejecuten por un rato. Thread.sleep(1000); // Despliega los subprocesos activos en el grupo. System.out.println("\nHay " + tg.activeCount( ) + " subprocesos en tg."); // Enumera los subprocesos y despliega sus nombres. System.out.println("Estos son sus nombres: "); Thread subps[] = new Thread[tg.activeCount( )]; tg.enumerate(subps); for(Thread s : subps) System.out.println(s.getName( )); System.out.println( ); // Detiene subp2. System.out.println("\nDeteniendo MiSubproceso #2"); ms2.miDetenido( ); www.fullengineeringbook.net Capítulo 7: Multiprocesamiento 357 Thread.sleep(1000); // Deja que se ejecute el subproceso. System.out.println("\nHay ahora " + tg.activeCount( ) + " subprocesos en tg."); // Interrumpe todos los subprocesos restantes. System.out.println("\nInterrumpiendo todos los subprocesos " + "restantes en el grupo."); tg.interrupt( ); } catch (InterruptedException e) { System.out.println("Subproceso principal interrumpido"); } } } Aquí se muestra una salida de ejemplo. (Su salida exacta puede variar.) Iniciando MiSubproceso #1 .Iniciando MiSubproceso #2 .Iniciando MiSubproceso #3 .Iniciando MiSubproceso #4 ............. Hay 4 subprocesos en tg. Estos son sus nombres: MiSubproceso #1 MiSubproceso #2 MiSubproceso #3 MiSubproceso #4 Deteniendo MiSubproceso #2 .Saliendo de MiSubproceso #2 ........... Hay ahora 3 subprocesos en tg. Interrumpiendo todos los subprocesos restantes en el grupo. MiSubproceso #1 interrumpido. Saliendo de MiSubproceso #1 MiSubproceso #3 interrumpido. Saliendo de MiSubproceso #3 MiSubproceso #4 interrumpido. Saliendo de MiSubproceso #4 Opciones Un ThreadGroup puede contener otros grupos de subprocesos. En esta situación, al ThreadGroup que contiene los otros grupos se le llama principal. A un ThreadGroup se le asigna un principal cuando se crea. Como opción predeterminada, el grupo principal es el que usa el subproceso que crea el ThreadGroup. Sin embargo, puede especificar a cuál grupo principal pertenece un grupo secundario al usar esta forma del constructor ThreadGroup. www.fullengineeringbook.net 358 Java: Soluciones de programación ThreadGroup(ThreadGroup principal, String nombre) Aquí, principal es el principal del objeto que se está creando. El nombre del ThreadGroup secundario se pasa en nombre. Puede obtener el principal de un ThreadGroup al llamar a getParent( ), que se muestra a continuación: final ThreadGroup getParent( ) Si el ThreadGroup que invoca no tiene principal, entonces se devuelve null. Puede obtener una lista de todos los subprocesos de un grupo, incluidos todos los de los grupos secundarios, al usar esta forma de enumerate( ): int enumerate(Thread[] subps, boolean todos) Obtiene todos los subprocesos en el ThreadGroup que invoca y los devuelve en subps. Si todos es verdadero, entonces se obtienen todos los subprocesos en todos los grupos secundarios. Se devuelve el número de subprocesos almacenados en subps. Sin embargo, sea cuidadoso. No se reportan errores si subps no es lo suficientemente grande para contener todos los subprocesos. Por tanto, la longitud de subps debe ser suficiente para contener todos los subprocesos activos. Puede obtener una lista de todos los grupos de subprocesos en un grupo al usar esta forma de enumerate( ): int enumerate(ThreadGroup[] gruposSubp) Se obtienen todos los grupos de subprocesos en el ThreadGroup que invoca y se devuelve en subps. Se devuelve el número de grupos de subprocesos almacenados en realidad en gruposSubp. Sin embargo, sea cuidadoso. No se reportan errores si gruposSubp no es lo suficientemente grande para contener todos los grupos de subprocesos. Por tanto, la longitud de gruposSubp debe ser suficiente para contener todos los subprocesos activos. Puede obtener una cuenta de los grupos al llamar a activeGroupCount( ) en el grupo de subprocesos. Puede obtener una lista de todos los grupos de subprocesos en un grupo, incluidos todos los grupos secundarios, al usar esta forma de enumerate( ): www.fullengineeringbook.net 8 CAPÍTULO Swing E n este capítulo se presenta una serie de soluciones que demuestran Swing, el kit de herramientas de GUI más importante de Java. Definido por un rico conjunto de componentes visuales y una arquitectura estrechamente integrada y muy adaptativa, Swing permite la creación de interfaces de usuario complejas pero funcionales. El usuario moderno exige una experiencia visual de alta calidad y Swing es el marco conceptual que usará para proporcionarla. Swing es un tema muy largo, y se necesitaría un libro completo para describir todas sus características. Por tanto, no es posible atender todos los aspectos de Swing en este capítulo. Por ejemplo, Swing permite un nivel elevado de personalización y soporta muchas características avanzadas que permiten adecuar aspectos de su funcionamiento interno. Aunque son importantes, casi ningún programador utiliza estas características de manera cotidiana, y no son el eje de este capítulo. En cambio, con las soluciones presentadas aquí se ilustran técnicas fundamentales y componentes de uso común. Con las soluciones también se responde a muchas preguntas frecuentes del tipo "¿cómo hacer?" acerca de Swing. En esencia, el objetivo es mostrar las piezas claves del conjunto de componentes de Swing en acción, tal como se usan en la programación día a día. Por supuesto, puede adaptar las soluciones y usarlas como capas para añadir funcionalidad adicional, conforme lo necesite su aplicación. He aquí las soluciones de este capítulo: • • • • • • • • • • • • • Cree una aplicación simple de Swing. Establezca el administrador de diseño del panel de contenido. Trabaje con JLabel. Cree un botón simple. Use iconos, HTML y mnemotécnica con JButton. Cree un botón interruptor. Cree casillas de verificación. Cree botones de opción. Ingrese texto con JTextField. Trabaje con JList. Use una barra de desplazamiento. Use JScrollPane para manejar el desplazamiento. Despliegue datos en una JTable. www.fullengineeringbook.net 359 360 Java: Soluciones de programación • • • Maneje sucesos de JTable. Despliegue datos en un JTree. Cree un menú principal. NOTA Para conocer una introducción completa a Swing, consulte mi libro Swing: A Begginer’s Guide publicado por McGraw-Hill, 2007. La revisión general siguiente y muchos de los análisis de este capítulo son adaptaciones de ese libro. Revisión general de Swing Swing no existía en los primeros días de Java. En cambio, fue una respuesta a las deficiencias presentes en el subsistema de la GUI original de Java: el kit de herramientas de ventana abstracta (AWT, Abstract Window Toolkit). AWT define un conjunto básico de componentes que dan soporte a una interfaces gráfica útil, pero limitada. Una razón para la naturaleza limitada de AWT es que traduce sus diversos componentes visuales en sus equivalentes correspondientes, específicos de una plataforma, o colegas. Esto significa que el aspecto de un componente de AWT está definido por la plataforma, no por Java. Debido a que los componentes de AWT son recursos de código nativos, se les considera pesados. El uso de colegas nativos lleva a varios problemas. En primer lugar, debido a diferencias entre sistemas operativos, un componente podría parecer, o incluso actuar, de manera diferente en distintas plataformas. Esta posible variabilidad amenazaba la filosofía abarcadora de Java: se escribe una vez, se ejecuta en todo lugar. En segundo lugar, el aspecto de cada componente era fijo (porque está definido por la plataforma) y no podría cambiarse (fácilmente). En tercer lugar, el uso de componentes pesados causaba algunas restricciones frustrantes. Por ejemplo, un componente pesado es siempre rectangular y opaco. No mucho después del lanzamiento original de Java, se volvió evidente que las limitaciones y restricciones presentes en AWT eran tan importantes que se necesitaba un mejor método. La solución fue Swing. Introducido en 1997, Swing se incluyó como parte de las clases básicas de Java (JFC, Java Foundation Classes). Swing estuvo inicialmente disponible para usarse con Java 1.1 como una biblioteca separada. Sin embargo, a partir de Java 1.2, Swing (y el resto de las JFC) se integró totalmente en Java. Swing atiende las limitaciones asociadas con los componentes de AWT mediante el uso de dos características clave: componentes ligeros y un aspecto integrable. Aunque son muy transparentes para el programador, estas dos características son la base de la filosofía de diseño de Swing y la razón de gran parte de su capacidad y flexibilidad. Echemos un vistazo a cada uno. Con algunas excepciones, los componentes de Swing son ligeros. Esto significa que un componente está escrito totalmente en Java. No depende de colegas específicos de la plataforma. Los componentes ligeros tienen algunas ventajas importantes, incluidas la eficiencia y flexibilidad. Por ejemplo, un componente ligero puede ser transparente, lo que permite formas no rectangulares. Más aún, debido a que los componentes ligeros no se traducen en compañeros específicos de la plataforma, el aspecto de cada componente está determinado por Swing, no por el sistema operativo. Esto significa que cada componente funcionará de manera consistente entre todas las plataformas. Debido a que cada componente de Swing es representado por código de Java en lugar de colegas específicos de plataforma, es posible separar el aspecto de un componente de su lógica, y esto es lo que hace Swing. La separación del aspecto proporciona una ventaja importante: se vuelve www.fullengineeringbook.net Capítulo 8: Swing 361 posible cambiar la manera en que se representa un componente sin afectar ninguno de sus aspectos. En otras palabras, es posible "integrar" un nuevo aspecto para cualquier componente determinado sin crear ningún efecto colateral en el código que usa el componente. Java proporciona aspectos, como metal y Motif, que están disponibles para todos los usuarios de Swing. Al aspecto metal también se le llama aspecto de Java. Es un aspecto independiente de la plataforma, que está disponible en todos los entornos de ejecución de Java. También es el aspecto predeterminado. Por esto, se usa en los ejemplos de este capítulo. El aspecto integrable de Swing es posible porque Swing usa una versión modificada de la clásica arquitectura modelo-vista-controlador (MVC, Model-View-Controller). En la terminología de MVC, el modelo corresponde a la información de estado asociada con el componente. Por ejemplo, en el caso de una casilla de verificación, el modelo contiene un campo que indica si la casilla está marcada o no. La vista determina cómo se despliega el componente en la pantalla, incluido cualquier aspecto de la vista afectado por el estado actual del modelo. El controlador determina cómo reacciona el componente ante el usuario. Por ejemplo, cuando el usuario hace clic en la casilla de verificación, el controlador reacciona al cambiar el modelo para que refleje la elección del usuario (marcada o desmarcada). Esto luego da como resultado la actualización de la vista. Al separar un componente en un modelo, una vista y un controlador, la implementación específica de cada una puede cambiarse sin afectar a las otras dos. Por ejemplo, diferentes implementaciones de vistas puede representar el mismo componente de distintas maneras sin afectar al modelo o el controlador. Aunque la arquitectura MVC y los principios detrás de ella son conceptualmente sólidos, el alto nivel de separación entre la vista y el controlador no era benéfico para los componentes de Swing. En cambio, Swing usa una versión modificada de MVC que combina la vista y el controlador en una sola entidad lógica llamada delegado UI. Por esta razón, al método de Swing se le llama arquitectura modelo-delegado o arquitectura de modelo separable. Por tanto, aunque la arquitectura de componentes de Swing esté basada en MVC, no usa una implementación clásica. Aunque las soluciones de este capítulo no funcionan directamente con modelos de delegados UI, están, no obstante, presentes tras bambalinas. Un último tema: aunque Swing elimina varias de las limitaciones presentes en AWT, no lo reemplaza. En cambio, se construye a partir de las bases proporcionadas por AWT. Swing también usa el mismo mecanismo de manejo de sucesos que AWT. Por tanto, AWT aún es una parte crucial de Java. Componentes y contenedores Una GUI de Swing consta de dos elementos clave: componentes y contenedores. Sin embargo, esta distinción es principalmente conceptual, porque todos los contenedores son componentes. La distribución entre los dos se encuentra en su propósito. Como el término se usa de manera común, un componente es un control visual independiente, como un botón o un campo de texto. Un contenedor alberga un grupo de componentes. Por tanto, un contenedor es un tipo especial de componente que está diseñado para incluir otros componentes. Más aún, para que un componente se despliegue, debe encontrarse dentro de un contenedor. Por tanto, todas las GUI de Swing tendrán por lo menos un contenedor. Debido a que los contenedores son componentes, un contenedor puede albergar también otros contenedores. Esto permite a Swing definir lo que se llama una jerarquía de contención, en cuya parte superior debe estar un contenedor de alto nivel. www.fullengineeringbook.net 362 Java: Soluciones de programación Componentes En general, los componentes de Swing se derivan de la clase JComponent. (Las únicas excepciones son los cuatro contenedores de nivel superior, descritos en la siguiente sección). JComponent proporciona la funcionalidad que es común a todos los componentes. Por ejemplo, JComponent soporta el aspecto integrable. JComponent hereda las clases AWT Container y Component. Por tanto, un componente de Swing está integrado con un componente de AWT y es compatible con él. Todos los componentes de Swing están representados por clases definidas dentro del paquete javax.swing. En la siguiente tabla se muestran los nombres de clase para los componentes de Swing (incluidos los usados como contenedores): JApplet JButton JCheckBox JCheckBoxMenuItem JColorChooser JComboBox JComponent JDesktopPane JDialog JEditorPane JFileChooser JFormattedTextField JFrame JInternalFrame JLabel JLayeredPane JList JMenu JMenuBar JMenuItem JOptionPane JPanel JPasswordField JPopupMenu JProgressBar JRadioButton JRadioButtonMenuItem JRootPane JScrollBar JScrollPane JSeparator JSlider JSpinner JSplitPane JTabbedPane JTable JTextArea JTextField JTextPane JToggleButton JToolBar JToolTip JTree JViewport JWindow Observe que todas las clases de componente empiezan con la letra J. Por ejemplo, la clase de una etiqueta es JLabel, la de un botón es JButton y la de una casilla de verificación es JCheckBox. Contenedores Swing define dos tipos de contenedores. Los primeros son de nivel superior: JFrame, JApplet, JWindow y JDialog. Estos contenedores no heredan JComponent. Sin embargo, sí heredan las clases AWT Component y Container. A diferencia de otros componentes de Swing, que son ligeros, los contenedores de alto nivel son pesados. Esto hace que los contenedores de alto nivel sean un caso especial en la biblioteca de componentes de Swing. Como su nombre lo indica, un contenedor de alto nivel debe estar en la parte superior de la jerarquía de contención. Un contenedor de alto nivel no está contenido dentro de ningún otro contenedor. Más aún, toda la jerarquía de contención debe empezar con un contenedor de alto nivel. El de uso más común para aplicaciones es JFrame. El usado para applets es JApplet. El segundo tipo de contenedores soportado por Swing son los ligeros, los cuales sí heredan JComponent. Ejemplos de contenedores ligeros son JPanel, JScrollPane y JRootPane. Los contenedores ligeros suelen usarse para organizar y administrar colectivamente grupos de componentes relacionados porque un contenedor ligero puede estar dentro de otro contenedor. Por www.fullengineeringbook.net Capítulo 8: Swing 363 tanto, puede usar contenedores ligeros para crear subgrupos de controles relacionados que están contenidos en un contenedor externo. Los paneles de contenedor de nivel superior Cada contenedor de nivel superior define un conjunto de paneles. En la parte superior de la jerarquía se encuentra una instancia de JRootPane. Éste es un contenedor ligero cuyo objetivo es administrar los demás paneles. También ayuda a administrar la barra de menús opcional. Los paneles que abarcan el panel raíz se les denomina panel de cristal, panel de contenido y panel de capas. El panel de cristal es el panel de nivel superior. Se asienta por encima de todos los demás paneles y los cubre por completo. El panel de cristal le permite administrar sucesos de ratón que afectan a todo el contenedor (en lugar de un control individual) o pintar sobre cualquier otro componente, por ejemplo. En casi todos los casos, no necesitará usar el panel de cristal directamente. El panel de capas permite que se dé a los componentes un valor de profundidad. Este valor determina qué componente se coloca sobre cuál otro. (Por tanto, el panel de capas le permite especificar un orden Z para un componente, aunque esto no es algo que, por lo general, necesitará hacer). El panel de capas contiene el panel de contenido y la barra de menús (opcional). Aunque el panel de cristal y el panel de capas están integrados en la operación de un contenedor de nivel superior y tiene fines importantes, mucho de lo que proporcionan ocurre tras bambalinas. El panel con el que interactuará su aplicación casi siempre es el de contenido, porque es el panel al que agregará componentes visuales. En otras palabras, cuando agrega un componente, como un botón, a un contenedor de nivel superior, lo agregará al panel de contenido. Por tanto, éste contiene los componentes con los que interactúa el usuario. Como opción predeterminada, el panel de contenido es una instancia opaca de JPanel (que es uno de los contenedores ligeros de Swing). Revisión general del administrador de diseño En Java, la colocación de los componentes dentro de un contenedor es controlada por un administrador de diseño. Java ofrece varios administradores de diseño. Muchos son proporcionados por el AWT (dentro de java.awt), pero Swing agrega unos cuantos propios en javax.swing. Todos los administradores de diseño son instancias de una clase que implementa la interfaz LayoutManager. (Algunos también implementarán la interfaz LayoutManager2). He aquí la lista de los administradores de diseño usados en los ejemplos de este capítulo: java.awt.FlowLayout Un diseño simple que coloca los componentes de izquierda a derecha, de arriba hacia abajo. (Coloca componentes de derecha a izquierda en algunas configuraciones de otros idiomas). java.awt.BorderLayout Coloca componentes dentro del centro o los bordes del contenedor. Es el diseño predeterminado de un panel de contenido. java.awt.GridLayout Dispone los componentes dentro de una cuadrícula. javax.swing.BoxLayout Dispone los componentes vertical u horizontalmente dentro de un cuadro. Está más allá del alcance de este capítulo describir estos administradores de diseño en detalle, pero en el siguiente análisis se presenta una breve revisión general. (En las soluciones también se encuentra información adicional acerca de estos administradores de diseño). www.fullengineeringbook.net 364 Java: Soluciones de programación BorderLayout es el administrador de diseño predeterminado para el panel de contenido. Implementa un estilo de diseño que define cinco ubicaciones a los que puede agregarse un componente. El primero es el centro. Los otros cuatro son los lados (es decir, los bordes), que se denominan norte, sur, este y oeste. Como opción predeterminada, cuando agrega un componente al panel de contenido, está agregando el componente al centro. Para agregar un componente a una de las otras regiones, especifique su nombre. Aunque el diseño de un borde es útil en algunas situaciones, a menudo se necesita otro administrador de diseño más flexible. Uno de los más simples es FlowLayout. Un diseño de flujo dispone los componentes de fila en fila, de arriba abajo. Cuando una fila está llena, el diseño avanza a la siguiente fila. Aunque este esquema le da poco control sobre la colocación de los componentes, es muy simple de usar. Sin embargo, esté consciente de que si cambia el tamaño del marco, la colocación de los componentes se modificará. Debido a su simplicidad, el diseño de flujo se usa en varios de los ejemplos. GridLayout crea una cuadrícula de células rectangulares en que se coloca cada componente individual. El tamaño de cada celda en la cuadrícula es el mismo, y un componente colocado en una celda tiene un tamaño suficiente para llenar las dimensiones de la celda. Las dimensiones de la cuadrícula se especifican cuando crea una instancia de GridLayout. Por ejemplo, esta expresión new GridLayout(5, 2) crea una cuadrícula que tiene 5 filas y 2 columnas. BoxLayout le proporciona una manera fácil de crear grupos de componentes que están organizados en cuadros. Por lo general, no usará BoxLayout como administrador de diseño para el propio panel de contenido. En cambio, normalmente creará uno o más paneles que usan BoxLayout, agregará componentes a los paneles y luego agregará los paneles al panel de contenido. De esta manera, puede crear fácilmente grupos de componentes que se disponen como unidad. Aunque puede implementar este método manualmente (al usar JPanel), Swing ofrece un método más conveniente. La clase Box puede usarse para crear un contenedor que usa automáticamente BoxLayout. Este método se usa en uno de los ejemplos. Manejo de sucesos Otra parte importante de la mayoría de los programas de Swing es el manejo de sucesos. Casi todos los componentes de Swing responden a la entrada del usuario, y es necesario manejar los sucesos generados por estas interacciones. Por ejemplo, se generará un suceso cuando el usuario hace clic en un botón, oprime una tecla en el teclado o selecciona un elemento de una lista. Los sucesos también se generan de maneras que no están relacionadas directamente con la entrada del usuario. Por ejemplo, un suceso se genera cuando un cronómetro se agota. Cualquiera que sea el caso, el manejo de sucesos es una parte grande de cualquier programa que usa Swing. El mecanismo de manejo de sucesos usados por Swing es llamado modelo de sucesos de delegación. Su concepto es muy simple. Un origen genera un suceso y lo envía a otros escuchas. En este esquema, el escucha simplemente espera hasta que recibe un suceso. Una vez que llega un suceso, el escucha lo procesa y luego regresa. La ventaja de este diseño es que la lógica de la aplicación que procesa sucesos está claramente separada de la lógica de la interfaz de usuario que genera los sucesos. Un elemento de interfaz de usuario puede "delegar" el procesamiento de un suceso a una pieza separada de código. En el modelo de suceso de delegación, debe registrarse un escucha con un origen para recibir la notificación de un suceso. www.fullengineeringbook.net Capítulo 8: Swing 365 Sucesos En el modelo de delegación, un suceso es un objeto que describe un cambio de estado en un origen. Puede generarse como consecuencia de la interacción de una persona con un elemento en una interfaz gráfica de usuario o puede generarse bajo control del programa. La superclase de todos los sucesos es java.util.EventObject. Muchos sucesos se declaran en java.awt.event. Éstos son todos los sucesos definidos por AWT. Aunque Swing usa estos sucesos, también define varios propios. Éstos se encuentran en javax.swing.event. Orígenes de sucesos El origen de un suceso es un objeto que genera un suceso. Cuando un origen genera un suceso, debe enviar ese suceso a todos los escuchas registrados. Por tanto, para que todos los escuchas reciban un suceso, debe registrarse con el origen de ese suceso. Los escuchas se registran con un origen al llamar a un método addTipoListener( ) en el objeto de origen del suceso. Cada tipo de suceso tiene su propio método de registro. He aquí la forma general: public void addTipoListener(TipoListener es) Aquí, Tipo es el nombre de un suceso y es es una referencia al escucha del suceso. Por ejemplo, el método que registra un escucha de suceso del teclado es addKeyListener( ). El método que registra un escucha de movimiento del ratón es addMouseMotionListener( ). Cuando ocurre un suceso, se notifica a todos los escuchas registrados. Un origen también debe proporciona un método que permita a un escucha dejar de registrar el interés de un tipo específico de suceso. La forma general de este método es: public void removeTipoListener(TipoListener es) Aquí, Tipo es el nombre del suceso y es es una referencia al escucha del suceso. Por ejemplo, para eliminar un escucha de teclado, llamaría a removeKeyListener( ). Los métodos que agregan o eliminan escuchas son proporcionados por el origen que genera los sucesos. Por ejemplo, la clase JButton proporciona un método llamado addActionListener( ) que agrega un escucha de acción, que maneja el suceso de acción generado cuando se oprime el botón. Escuchas de sucesos Un escucha es un objeto al que se le notifica cuando ocurre un suceso. Tiene dos requisitos importantes. En primer lugar, debe haberse registrado con uno o más orígenes para recibir notificaciones acerca de un tipo específico de suceso. En segundo lugar, debe implementarse un método para recibir y procesar ese suceso. Los métodos que reciben y procesan sucesos están definidos en un conjunto de interfaces que se encuentran en java.awt.event y javax.swing.event. Por ejemplo, la interfaz ActionListener define un método que recibe una notificación cuando tiene lugar una acción, como hacer clic en un botón. Cualquier objeto puede recibir y procesar este suceso si proporciona una implementación de la interfaz ActionListener. Hay un principio general que se aplica a los manejadores de sucesos. Un manejador debe hacer su trabajo rápidamente y luego regresar. No debe enfrascarse en una operación larga, porque al hacerlo hace más lenta a toda la aplicación. Si se requiere una operación que consume tiempo, entonces generalmente se creará un subproceso para este fin. www.fullengineeringbook.net 366 Java: Soluciones de programación Cree una aplicación simple de Swing Componentes clave Clases Métodos javax.swing.JFrame void setSize(int ancho, int alto) void setDefaultCloseOperation(int que) Component add(Component comp) Void setVisible(boolean mostrar) javax.swing.JLabel javax.swing.SwingUtilities static void invokeLater(Runnable obj) Hay dos tipos de programas de Java en que suele usarse Swing. El primero es la applet. La creación de una applet de Swing se describe en el capítulo 6, en las soluciones Cree un esqueleto de applet basado en Swing y Cree una GUI y maneje sucesos en una applet de Swing. El segundo programa común de Swing es la aplicación de escritorio. Es el tipo de programa de Swing descrito en esta solución. Aunque Swing es fácil de usar, los programas de Swing difieren de los de consola y de los de GUI basados en AWT. Por ejemplo, un programa de Swing usa el conjunto de componentes de Swing para manejar la interacción con el usuario. Por tanto, la E/S no es manejada por System. in ni System.out como en una aplicación de consola, sino por controles visuales, como botones, contadores y barras de desplazamiento. Además, Swing tiene requisitos especiales que se relacionan con los subprocesos. En esta solución se muestran los pasos necesarios para crear una aplicación mínima de Swing. También introduce el componente más simple de Swing: JLabel. Paso a paso Para crear una aplicación de escritorio de Swing, se requieren los siguientes pasos: 1. Cree un contenedor de nivel superior para el programa. Por lo general, será una instancia de JFrame. 2. Establezca el tamaño del marco al llamar a setSize( ). 3. Establezca la operación de cierre predeterminada al llamar a setDefaultCloseOperation( ). 4. Cree uno o más componentes. 5. Agregue los componentes al panel de contenido del marco al llamar a add( ). 6. Despliegue el marco al llamar a setVisible( ). 7. En todos los casos, la GUI de Swing debe crearse en el subproceso que despacha el suceso mediante el uso de invokeLater( ). Por tanto, el subproceso que despacha el suceso debe ejecutar los pasos anteriores. www.fullengineeringbook.net Capítulo 8: Swing 367 Análisis Como se explicó en Revisión general de Swing, las clases e interfaces de Swing están empaquetadas en java.swing. Por tanto, cualquier programa que use Swing debe importar este paquete. Todas las aplicaciones de Swing deben tener un contenedor pesado en la parte superior de la jerarquía de contención. El contenedor de nivel superior incluye todos los contenedores y componentes asociados con la aplicación. Una aplicación de Swing por lo general usará una instancia de JFrame como contenedor de nivel superior. JFrame hereda las siguientes clases de AWT: Component, Container, Window y Frame. Define varios constructores. Aquí se muestra el usado en esta solución: JFrame(String nombre) El título de la ventana se pasa en nombre. El tamaño del marco puede establecerse al llamar a setSize( ), que se muestra a continuación: void setSize(int ancho, int alto) Los parámetros ancho y alto especifican el ancho y el alto de la ventana, en píxeles. Como opción predeterminada, cuando se cierra una ventana de nivel superior (como cuando el usuario hace clic en el cuadro de cierre), se elimina la ventana de la pantalla, pero no se termina la aplicación. Aunque este comportamiento predeterminado es útil en algunas situaciones, no es lo que se necesita en la mayor parte de las aplicaciones. En cambio, por lo general querrá que toda la aplicación termine cuando se cierra su ventana de nivel superior. Hay un par de maneras de lograr esto. La más fácil consiste en llamar a setDefaultCloseOperation( ). Aquí se muestra su forma general: void setDefaultCloseOperation(int que) El valor pasado en que determina lo que pasa cuando se cierra una ventana. He aquí los valores válidos: JFrame.DISPOSE_ON_CLOSE JFrame.EXIT_ON_CLOSE JFrame.HIDE_ON_CLOSE JFrame.DO_NOTHING_ON_CLOSE Estas constantes se declaran en WindowConstants, que es una interfaz declarada en javax.swing, que es implementada por JFRame y que define muchas constantes relacionadas con Swing. Para que el programa termine cuando se cierre su ventana de nivel superior, use EXIT_ON_CLOSE. Puede crear un componente de Swing al crear una instancia de una de sus clases de componentes. Swing define muchas clases de componentes que soportan botones, casillas de verificación, campos de texto, etc. En esta solución sólo se usa una de esas clases: JLabel. Es el componente más simple de Swing, porque no acepta entrada del usuario. En cambio, simplemente despliega información que puede constar de texto, un icono, o una combinación de ambos. La etiqueta empleada por esta solución sólo contiene texto. JLabel define varios constructores. El usado aquí es: JLabel(String cad) Esto crea una etiqueta que despliega la cadena pasada en cad. (Consulte Trabaje con JLabel para conocer más información acerca de las etiquetas). www.fullengineeringbook.net 368 Java: Soluciones de programación Después de que haya creado un componente, debe agregarlo a un contenedor. En este caso, se agregará al contenedor de nivel superior de la aplicación. Todos los contenedores de nivel superior tienen un panel de contenido en que se almacenan los componentes. Por tanto, para agregar un componente a un marco, debe agregarlo al panel de contenido del marco. A partir de Java 5, esto se logra al llamar a add( ) en la referencia a JFrame. Esto causa que el componente se agregue al panel de contenido asociado con JFrame. (Consulte la nota histórica que se encuentra a continuación). El método add( ) tiene varias versiones. Aquí se muestra la usada en el programa: Component add(Component comp) Cuando el marco se haga visible, comp también se desplegará. Su posición estará determinada por el administrador de diseño. Como opción predeterminada, el panel de contenido asociado con JFrame usa un diseño de bordes. Esta versión de add( ) agrega el componente a la posición central. Otras versiones de add( ) le permiten especificar una de las regiones del borde. Cuando se agrega un componente al centro, su tamaño se ajusta automáticamente para adecuarse al tamaño del centro. Para desplegar el marco (y el componente que contiene), debe llamar a setVisible( ), que se muestra aquí: Void setVisible(boolean mostrar) Si mostrar es verdadero, se despliega el marco; si es falso, se oculta. Como opción predeterminada, un JFrame es invisible, de modo que debe llamarse a setVisible(true) para desplegarlo. Hay una restricción muy importante a la que debe adherirse cuando use Swing. Toda interacción con los componentes visuales de Swing debe tener lugar a través del subproceso que despacha los sucesos en lugar del subproceso principal de la aplicación. Esto incluye la construcción inicial de la GUI. He aquí por qué: en general, los programas de Swing están orientados a sucesos. Por ejemplo, cuando un usuario interactúa con un componente, se genera un suceso. Se pasa un suceso a la aplicación al llamar a un manejador de sucesos definido por la aplicación. Esto significa que el manejador se ejecuta en el subproceso que despacha los sucesos proporcionado por Swing y no en el subproceso principal de la aplicación. Por tanto, aunque los manejadores de sucesos están definidos por su programa, se le llaman en un subproceso que no fue creado por su programa. Para evitar problemas (como el hecho de que dos subprocesos diferentes traten de actualizar el mismo componente al mismo tiempo), todos los componentes de la GUI de Swing deben crearse y actualizarse a partir del subproceso que despacha sucesos, no del subproceso principal de la aplicación. Sin embargo, main( ) se ejecuta en el subproceso principal. Por tanto, no puede crearse una instancia directa de componentes de GUI. En cambio, el programa debe crear un objeto Runnable que se ejecute en el subproceso que despacha sucesos, y hacer que este objeto cree la GUI. Para permitir que el código de la GUI se cree en el subproceso que despacha los sucesos, debe usar uno de los dos métodos definidos por la clase SwingUtilities. Estos métodos son invokeLater( ) e invokeAndWait( ). Se muestran a continuación: static void invokeLater(Runnable obj) static void invokeAndWait(Runnable obj) throws InterruptedException, InvocationTargetException Aquí, obj es un objeto de Runnable, cuyo método run( ) será llamado por el subproceso que despacha los sucesos. La diferencia entre los dos métodos es que invokeLater( ) regresa de inmediato, pero invokeAndWait( ) espera hasta que regresa obj.run( ). Puede usar estos métodos para llamar a un método que construya la GUI para su aplicación de Swing, o cada vez que necesite www.fullengineeringbook.net Capítulo 8: Swing 369 modificar el estado de la GUI a partir de código no ejecutado por el subproceso que despacha los sucesos. Por lo general, querrá usar invokeLater( ), como en el siguiente ejemplo. Sin embargo, cuando construya la GUI inicial para una applet, querrá usar invokeAndWait( ). (Consulte Cree un esqueleto de applet basado en Swing, en el capítulo 6). Nota histórica: getContentPane( ) Antes de Java 5, cuando se agregaba o eliminaba un componente, o se establecía el administrador de diseño para el panel de contenido de un contenedor de nivel superior, como JFrame, tenía que obtener explícitamente una referencia al panel de contenido al llamar a getContentPane( ). Por ejemplo, suponiendo la existencia de una instancia de JLabel llamada jetq y una de JFrame llamada jmarco, en el pasado, tenía que usar la siguiente instrucción para agregar jetq a jmarco: jmarco.getContentPane( ).add(jetq); // al viejo estilo A partir de Java 5, la llamada a getContentPane( ) ya no es necesaria porque las llamadas a add( ), remove( ) y setLayout( ) en JFrame son dirigidas automáticamente al panel de contenido. Por esta razón, en las soluciones de este libro no se hacen llamadas a getContentPane( ). Sin embargo, si quiere escribir código que pueda compilarse en versiones anteriores de Java, entonces necesitará agregar las llamadas a getContentPane( ) donde sea apropiado. Ejemplo Con el siguiente programa se muestra una manera de escribir una aplicación de Swing. En el proceso, se demuestran varias características claves de Swing. Usa dos componentes de éste: JFrame y JLabel. JFrame es un contenedor de nivel superior que suele usarse para aplicaciones de Swing. JLabel es el componente de Swing que crea una etiqueta, que es un componente que despliega información. La etiqueta es el componente más simple de Swing porque es pasivo. Es decir, una etiqueta no responde a la entrada del usuario. Sólo despliega la salida. El programa usa un contenedor de JFrame para contener una instancia de JLabel. La etiqueta despliega un mensaje de texto corto: // Un programa simple de Swing. import javax.swing.*; import java.awt.*; class DemoSwing { DemoSwing( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Una aplicación simple de Swing"); // Da al marco un tamaño inicial. jmarco.setSize(275, 100); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); www.fullengineeringbook.net 370 Java: Soluciones de programación // Crea una etiqueta de texto. JLabel jetq = new JLabel(" Ésta es una etiqueta de texto."); // Agrega la etiqueta al panel de contenido. jmarco.add(jetq); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco de un subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoSwing( ); } }); } } Los programas de Swing se compilan y ejecutan de la misma manera que otras aplicaciones de Java. Por tanto, para compilar este programa, se usa esta línea de comandos: javac DemoSwing.java Cuando el programa se ejecute, producirá la ventana que se muestra aquí: Preste especial atención a estas líneas de código dentro de main( ): SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoSwing( ); } }); Como se explicó, toda la interacción con los componentes visuales de Swing, incluida la construcción inicial de la GUI, debe tener lugar en el subproceso que despacha los sucesos. Aquí, el método invokeLater( ) se usa para crear una instancia de DemoSwing en ese subproceso. www.fullengineeringbook.net Capítulo 8: Swing 371 Opciones En el ejemplo se usa el administrador de diseños predeterminado para el panel de contenido de un JFrame, que es BorderLayout. Implementa un estilo de diseño que define cinco ubicaciones a las que puede agregarse un componente. La primera es el centro. Las otras cuatro son los lados (es decir, los bordes), que se denominan norte, sur, este y oeste. Como opción predeterminada, cuando agrega un componente al panel de contenido, está agregando el componente al centro. Para especificar una de las demás ubicaciones, use esta forma de add( ). void add(Component componente, Object ubi) Aquí, componente es el componente que se agrega y ubi especifica la ubicación a la que se agrega. El valor debe ser uno de los siguientes: BorderLayout.CENTER BorderLayout.EAST BorderLayout.SOUTH BorderLayout.WEST BorderLayout.NORTH Para ver el efecto de colocar un componente en cada ubicación, cambie el programa de ejemplo para que cree cinco etiquetas y agregue una a cada ubicación, como se muestra aquí: // Crea cinco etiquetas, una para cada ubicación de BorderLayout. JLabel jetqC = new JLabel("Centro", SwingConstants.CENTER); JLabel jetqO = new JLabel("Oeste", SwingConstants.CENTER); JLabel jetqE = new JLabel("Este", SwingConstants.CENTER); JLabel jetqN = new JLabel("Norte", SwingConstants.CENTER); JLabel jetqS = new JLabel("Sur", SwingConstants.CENTER); // Agrega las etiquetas al panel de contenido, en las ubicaciones especificadas. jmarco.add(jetqC); jmarco.add(jetqE, BorderLayout.EAST); jmarco.add(jetqO, BorderLayout.WEST); jmarco.add(jetqN, BorderLayout.NORTH); jmarco.add(jetqS, BorderLayout.SOUTH); Observe que las etiquetas especifican una alineación central. Como opción predeterminada, el contenido de una etiqueta está alineado a la izquierda. El uso de la alineación central facilita ver cada ubicación de BorderLayout. (Consulte Trabaje con JLabel para conocer detalles sobre alineación). Después de hacer estos cambios, la ventana tendrá este aspecto: Como verá, cada una de las cinco ubicaciones está ocupada por la etiqueta apropiada. En general, BorderLayout es más útil cuando está creando un JFrame que contiene un componente centrado que tiene asociado un componente de encabezado o pie de página. (A menudo, el componente que se está centrando es un grupo de componentes dentro de contenedores www.fullengineeringbook.net 372 Java: Soluciones de programación ligeros de Swing, como JPanel). Puede especificar un administrador de diseño diferente al llamar a setLayout( ). Esto se ilustra en la siguiente solución. Un aspecto clave de la mayor parte de los programa de Swing es el manejo de sucesos. Sin embargo, el programa del ejemplo anterior no responde a ningún suceso porque JLabel es un componente pasivo. En otras palabras, una JLabel no genera ningún suceso basado en interacción con el usuario. Como resultado, en el programa anterior no se incluye ningún manejador de sucesos. La mayor parte de los componentes de Swing sí generan sucesos. Varios de estos sucesos, junto con su manejo apropiado, se demuestran en las soluciones de este capítulo. Establezca el administrador de diseño del panel de contenido Componentes clave Clases Métodos java.awt.FlowLayout javax.swing.JFrame void setLayout(LayoutManager lm) Como opción predeterminada, el panel de contenido asociado con un JFrame o JApplet usa un diseño de bordes, encapsulado por el administrador de diseño BorderLayout. Este diseño se demuestra en la solución anterior. Aunque un diseño de borde es útil para muchas aplicaciones, hay ocasiones en que uno de los otros administradores de diseño será lo más conveniente. Cuando éste es el caso, puede cambiarse el administrador de diseño al llamar a setLayout( ). En esta solución se muestra el procedimiento. También se demuestra uno de los administradores de diseño más comunes: FlowLayout. Paso a paso Para cambiar el administrador de diseño, se requieren estos pasos: 1. Si es necesario, importe java.awt para obtener acceso al administrador de diseño deseado. 2. Cree una instancia del nuevo administrador de diseño. 3. Llame a setLayout( ) en la instancia de JFrame o JApplet, pasándola en el nuevo administrador de diseño. Análisis La mayor parte de los administradores generales de diseño están empaquetados en java.awt o javax. swing. Si estará usando un administrador de diseño almacenado en java.awt, entonces necesitará importar java.awt en su programa. Debe tener conciencia de que los administradores de diseño de java.awt son perfectamente aceptables para uso con Swing. Simplemente anteceden a la creación de Swing. Por ejemplo, la clase BorderLayout está empaquetada en java.awt. Swing también www.fullengineeringbook.net Capítulo 8: Swing 373 proporciona varios administradores de diseño propios, como BorderLayout y SpringLayout. Uno de los administradores de diseño más populares es FlowLayout. Es muy simple de usar, lo que lo hace especialmente conveniente cuando experimenta o cuando crea programas de ejemplo. Por esta razón se usa en varios ejemplos de este capítulo. FlowLayout diseña automáticamente los componentes de fila en fila, de arriba abajo. Cuando una fila está llena, el diseño pasa a la siguiente fila. Aunque es muy fácil de usar, FlowLayout tiene dos desventajas. En primer lugar, le da poco control sobre la ubicación precisa de los componentes. En segundo lugar, el cambio de tamaño del marco puede dar como resultado un cambio en la posición de los componentes. A pesar de sus limitaciones, FlowLayout suele ser apropiado para GUI simples. FlowLayout proporciona tres constructores. El usado en esta solución se muestra a continuación: FlowLayout( ) Crea un diseño de flujo que centra automáticamente los componentes en la línea y separa cada componente del siguiente con cinco píxeles en las cuatro direcciones. Para establecer el administrador de diseño, llame a setLayout( ), que se muestra aquí: void setLayout(LayoutManeger lm) El nuevo administrador de diseño se pasa en lm. Desde el lanzamiento de Java 5, al llamar a setLayout( ) en cualquier contenedor de nivel superior, incluidos JFrame y JApplet, se establece el diseño del panel de contenido. Versiones anteriores de Java requerían que explícitamente obtuviera una referencia al panel de contenido al llamar a getContentPane( ) en el contenedor de nivel superior, pero esto no se requiere en el nuevo código. (Consulte la Nota histórica, en Cree una aplicación simple de Swing). Ejemplo En el siguiente ejemplo se crea una aplicación de Swing que establece el administrador de diseño del panel de contenido de la instancia de JFrame para el diseño de flujo. Luego crea dos botones (que son instancias de JButton) y una etiqueta, y las agrega al marco. En este ejemplo, los botones no tienen uso alguno, más allá de estar presenten en el despliegue. (Consulte Cree un botón simple para conocer una solución en que se demuestran los botones). // Cambia el administrador de diseño del panel de contenido. import javax.swing.*; import java.awt.*; class CambiarDiseno { CambiarDiseno( ) { // Crea un nuevo contenedor JFrame. JFrame jmarco = new JFrame("Uso de FlowLayout"); // Da un tamaño inicial al marco. jmarco.setSize(275, 100); www.fullengineeringbook.net 374 Java: Soluciones de programación // Establece el uso del administrador de diseño FlowLayout. jmarco.setLayout(new FlowLayout( )); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta. JLabel jetq = new JLabel("Un ejemplo de diseño de flujo."); // Crea botones que no hacen nada. JButton jbtnA = new JButton("Alfa"); JButton jbtnB = new JButton("Beta"); // Agrega los botones y la etiqueta al panel de contenido. jmarco.add(jbtnA); jmarco.add(jbtnB); jmarco.add(jetq); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new CambiarDiseno( ); } }); } } Aquí se muestra la salida: Como se afirmó, cuando se usa el diseño de flujo, la posición de los componentes puede transformarse cuando se cambia el tamaño del marco. Por ejemplo, al hacer más ancho el marco causa que los botones y la etiqueta se alineen, como se muestra aquí: www.fullengineeringbook.net Capítulo 8: Swing 375 Opciones Como opción predeterminada, FlowLayout centra los componentes dentro de una línea y separa cada uno del siguiente con cinco píxeles. Puede modificar estos valores predeterminados al usar uno de los siguientes constructores de FlowLayout: FlowLayout(int alinHoriz) FlowLayout(int alinHoriz, int sepHoriz, int sepVert) La alineación horizontal de los componentes dentro de una línea se especifica con alinHoriz. Debe ser uno de estos valores: FlowLayout.CENTER FlowLayout.LEADING FlowLayout.RIGHT FlowLayout.TRAILING FlowLayout.LEFT Como opción predeterminada, los valores de LEADING y TRAILING son los mismos que LEFT y RIGHT. Se invertirán en contenedores cuya orientación está configurada para idiomas que se leen de derecha a izquierda, en lugar de izquierda a derecha. El espaciado horizontal y vertical entre componentes se especifica en píxeles con sepHoriz y sepVert, respectivamente. Para ver el efecto de cambiar la alineación y el espaciado, sustituya esta llamada a setLayout( ) en el ejemplo. jmarco.setLayout(new FlowLayout(FlowLayout.RIGHT, 20, 5)); Esto justifica a la derecha cada línea y usa un espaciado horizontal de 20 píxeles. Además de BorderLayout y FlowLayout, Java proporciona otros administradores de diseño de uso común. Tres de ellos con los que querremos experimentar son GridLayout, GridBagLayout y BoxLayout. Cada uno se describe brevemente aquí. GridLayout crea una cuadrícula de celdas rectangulares en que se colocan componentes individuales. El número de filas y columnas en la cuadrícula se especifica cuando se crea el diseño. El tamaño de cada celda en la cuadrícula es el mismo, y un componente puesto en una celda se cambia de tamaño para llenar las dimensiones de ésta. GridLayout está empaquetado en java.awt. GridBagLayout es, en esencia, una colección de cuadrículas. Con GridBagLayout puede especificar la ubicación relativa de los componentes al determinar sus posiciones dentro de las celdas de cada cuadrícula. La clave es que cada componente puede tener un tamaño diferente y cada fila de la cuadrícula puede tener un número distinto de columnas. Este esquema le da un control considerable sobre la manera en que se organizan los componentes dentro de un contenedor. Aunque GridBagLayout requiere un poco de trabajo de configuración, suelen valer la pena el esfuerzo cuando se crea un marco que contiene varios controles. GridBagLayout también está empaquetado en java.awt. Una opción a GridBagLayout que es más fácil de usar en algunos casos es BoxLayout. Le deja crear con facilidad un grupo de componentes que se distribuyen vertical u horizontalmente como unidad. BoxLayout no suele usarse como administrador de diseño para el panel de contenido. En cambio, se usa como administrador de diseño de uno o más paneles (como instancias de JPanel). También se agregan componentes a esos paneles, y luego éstos se añaden al panel de contenido. (La manera más fácil de obtener un contenedor que usa BoxLayout consiste en crear un Box. Consulte Use JScrollPane para manejar el desplazamiento con el fin de conocer un ejemplo que use Box). www.fullengineeringbook.net 376 Java: Soluciones de programación Aunque este ejemplo es una aplicación de Swing, el mismo procedimiento básico usado para establecer el administrador de diseño también se aplica a applets de Swing. Por ejemplo, a fin de cambiar el administrador de diseño para el panel de contenido asociado con un contenedor de JApplet, simplemente llame a setLayout( ) en la instancia de JApplet. Trabaje con JLabel Componentes clave Clases e interfaces Métodos javax.swing.border.BorderFactory static Border createLineBorder(color colorLinea) javax.swing.JLabe String getText( ) void setBorder(Border borde) void SetDisabledIcon(Icon desIcono) void Enabled(boolean estado) void setHorizontalAlignment(int alinHoriz) void setVerticalAlignment(int alinVert) void setText(String msj) javax.swing.Icon javax.swing.ImageIcon JLabel crea una etiqueta de Swing. Es el componente más simple de Swing porque no responde a interacción con el usuario. Aunque JLabel es muy fácil de usar, incluye muchas características y permite una cantidad importante de personalización, lo que le hace posible crear etiquetas muy complejas. Por ejemplo, el contenido de una etiqueta puede alinearse horizontal o verticalmente, una etiqueta puede usar HTML, puede deshabilitarse y puede contener un icono. Una etiqueta también puede incluir un borde. Francamente, la simplicidad inherente de JLabel hace que resulte fácil omitir sus características más sutiles. En esta solución se demuestra cómo crear y administrar varios tipos de etiquetas de JLabel. Paso a paso Para crear y administrar una etiqueta de Swing, se requieren estos pasos: 1. Cree una instancia de JLabel, especificando el texto, el icono, o ambos, que se desplegará dentro de la etiqueta. También puede especificar la alineación horizontal, si lo desea. 2. Para poner un borde alrededor de una etiqueta, llame a setBorder( ). 3. Para alinear el contenido de la etiqueta verticalmente, llame a setVerticalAlignment( ). Para establecer la alineación horizontal después de que se ha construido la etiqueta, llame a setHorizontalAlignment( ). 4. Para deshabilitar o habilitar una etiqueta, llame a setEnabled( ). www.fullengineeringbook.net Capítulo 8: Swing 377 5. Para cambiar el texto de una etiqueta, llame a setText( ). A fin de obtener el texto de una etiqueta, llame a getText( ). 6. Para usar HTML dentro de una etiqueta, empiece el texto con <html>. Análisis JLabel define varios constructores. Aquí se muestran: JLabel( ) JLabel(Icon, icono) JLabel(String cad) JLabel(Icon icono, int alinHoriz) JLabel(String cad, int alinHoriz) JLabel(String cad, Icon icono, int alinHoriz) Aquí, cad e icono son el texto y el icono usados por la etiqueta. Como opción predeterminada, el texto y el icono de una etiqueta se alinean a la izquierda. Puede cambiar la alineación horizontal al especificar el parámetro alinHoriz. Debe ser uno de los siguientes valores: SwingConstants.LEFT SwingConstants.RIGHT SwingConstants.CENTER SwingConstants.LEADING SwingConstants.TRAILING La interfaz SwingConstants define varias constantes que se relacionan con Swing. Esta interfaz se implementa con JLabel (y varios otros componentes). Por tanto, también puede hacer referencia a esas constantes mediante JLabel, como JLabel.RIGHT. Observe que los iconos son especificados por objetos de tipo Icon, que es una interfaz definida por Swing. La manera más fácil de obtener un icono consiste en usar la clase ImageIcon, que implementa Icon y encapsula una imagen. Por tanto, un objeto de tipo ImageIcon puede pasarse como argumento al parámetro Icon del constructor de JLabel. Hay varias maneras de proporcionar la imagen, incluida su lectura de un archivo o su descarga de un URL. He aquí el constructor ImageIcon usado en esta solución: ImageIcon(String nombrearch) Obtiene la imagen del archivo llamado nombrearch. Puede colocar un borde alrededor de una etiqueta. Los bordes son útiles cuando quiere mostrar claramente la extensión de la etiqueta. Todos los bordes de Swing son instancias de la interfaz javax.swing.border.Border. Aunque le es posible definir sus propios bordes, por lo general no necesitará hacerlo porque Swing proporciona varios estilos de bordes predefinidos, que están disponibles mediante javax.swing.BorderFactory. Esta clase define varios métodos de fábrica que crean varios tipos de bordes, que van de simples bordes de línea a biselados, enmarcados o mate. También puede crear bordes de título, que incluyen una leyenda corta incrustada en el borde, o un borde vacío, que es un borde invisible. Los bordes vacíos son útiles cuando se desea un hueco alrededor de un componente. www.fullengineeringbook.net 378 Java: Soluciones de programación NOTA Es posible agregar un borde casi a cualquier componente de Swing, pero no suele ser una buena idea. Casi todos los componentes de Swing, como botones, campos de texto y listas, dibujan sus propios bordes. La especificación de otro borde causará un conflicto. Las dos excepciones a esta regla son JLabel y JPanel. En esta solución se usa un tipo de borde: el de línea. Para crear un borde de línea, use el siguiente método de fábrica: static Border createLineBorder(Color colorLinea) Aquí, colorLinea especifica el color de la línea usada como borde. Por ejemplo, para dibujar un borde negro, pase Color.BLACK. Este método crea un borde de línea con el grosor predeterminado. Una vez que haya creado un borde, puede asignarlo a una etiqueta al llamar al método setBorder( ). Aquí se muestra: void setBorder(Border borde) Aquí, borde especifica el borde que habrá de usarse. Algo que hay que comprender es que el mismo borde puede usarse para varios componentes. Es decir, no necesita crear un nuevo objeto de Border para cada etiqueta a la que asignará el borde. Aunque la especificación de la alineación horizontal cuando se construye una etiqueta suele ser el método más fácil, Swing proporciona un opción. Puede llamar al método setHorizontalAlignment( ) en la etiqueta después de que se ha construido. Aquí se muestra: void setHorizontalAlignment(int alinHoriz) Aquí, alinHoriz debe ser una da las constantes de alineación horizontal que acabamos de describir. También puede establecer la alineación vertical de una etiqueta. Para ello, llame al método setVerticalAlignment( ) en la etiqueta. Aquí se muestra: void setVerticalAlignment(int alinVert) El valor pasado a alinVert debe ser una de estas constantes de alineación vertical: SwingConstants.TOP SwingConstants.CENTER SwingConstants.BOTTOM Por supuesto, el texto está centrado de arriba abajo, como opción predeterminada, de modo que sólo usaría CENTER si está regresando la alineación vertical a su valor predeterminado. Hay algo importante que debe comprender cuando establece la alineación de una etiqueta: no será necesario tener un efecto. Por ejemplo, cuando usa FlowLayout, la etiqueta tendrá un tamaño adecuado para su contenido. En este caso, no hay diferencia entre alinear arriba o abajo de la etiqueta. En general, la alineación de ésta sólo afecta a las etiquetas que tienen un tamaño mayor que su contenido. Una manera en que esto puede ocurrir es cuando usa un administrador de diseño, como GridLayout, que ajusta automáticamente el tamaño de una etiqueta para adecuarse al espacio disponible. También puede suceder cuando especifica un tamaño de componente preferido que es mayor del que necesita para incluir su contenido. Para establecer el tamaño preferido de un componente, llame a setPreferredSize( ). www.fullengineeringbook.net Capítulo 8: Swing 379 Puede deshabilitar una etiqueta al llamar setEnabled( ), que se muestra aquí: void setEnabled(boolean estado) Cuando estado es falso, la etiqueta está deshabilitada. Para habilitarla, pase true. Cuando una etiqueta está deshabilitada, aparece en gris. Puede obtener o cambiar el contenido de una etiqueta en tiempo de ejecución. Por ejemplo, para establecer el texto, llame a setText( ). Para obtener el texto, llame a getText( ). Aquí se muestran: void setText(String nuevoMsj) String getText( ) La cadena pasada en nuevoMsj se despliega dentro de la etiqueta, reemplazando la cadena anterior. Puede usar una cadena que contiene HTML como texto que habrá de desplegarse en una etiqueta. Para ello, empiece la cadena con <html>. Cuando se hace esto, el texto se forma automáticamente como lo especifica el marcado. El empleo de HTML ofrece una ventaja importante: le permite desplegar texto que abarca dos o más líneas. Ejemplo En el siguiente ejemplo se ilustran varias características de JLabel. Observe que el botón Cambiar le permite experimentar con varias opciones de alineación. Cada vez que se oprime el botón, cambia la alineación del texto de la etiqueta. // Demuestra JLabel. import import import import javax.swing.*; java.awt.*; java.awt.event.*; javax.swing.border.*; class DemoEtiqueta { JLabel JLabel JLabel JLabel JLabel JLabel jetqSimple; jetqBorde; jetqIcono; jetqHTML; jetqDes; jetqAlinear; JButton jbtnCambiar; int next; DemoEtiqueta( ) { next = 0; // Crea un nuevo contenedor JFrame. JFrame jmarco = new JFrame("Demo etiqueta"); www.fullengineeringbook.net 380 Java: Soluciones de programación // Establece el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(200, 360); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una instancia de Border para un borde de línea. Border borde = BorderFactory.createLineBorder(Color.BLACK); // Crea una etiqueta predeterminada que centra su texto. jetqSimple = new JLabel("Una etiqueta predeterminada"); // Crea una etiqueta con un borde. jetqBorde = new JLabel("Esta etiqueta tiene un borde"); jetqBorde.setBorder(borde); // Crea una etiqueta que incluye un icono. ImageIcon miIcono = new ImageIcon("miIcono.gif"); jetqIcono = new JLabel("Texto con icono.", miIcono, JLabel.LEFT); // Crea una etiqueta que despliega HTML y lo rodea // con un borde de línea. jetqHTML = new JLabel("<html>Usa HTML para crear un<br>" + " mensaje de varios renglones." + "<br>Uno<br>Dos<br>Tres"); jetqHTML.setBorder(borde); // Deshabilita una etiqueta. jetqDes= new JLabel("Esta etiqueta está deshabilitada."); jetqDes.setEnabled(false); // Crea una etiqueta que le permite experimentar con varias // opciones de alineación. Esta etiqueta tiene un borde para // que la alineación de su contenido sea más fácil de ver. jetqAlinear = new JLabel("Centrado", JLabel.CENTER); jetqAlinear.setBorder(borde); // Establece el tamaño preferido para la etiqueta alineada. jetqAlinear.setPreferredSize(new Dimension(150, 100)); // Crea el botón Cambiar. Al oprimir este botón // cambia la alineación del texto dentro de jetqAlinear. jbtnCambiar = new JButton("Cambiar alineación "); // Agrega un escucha de acción al botón Cambiar. jbtnCambiar.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { next++; www.fullengineeringbook.net Capítulo 8: if(next > 4) next = 0; switch(next) { case 0: jetqAlinear.setText("Centrado"); jetqAlinear.setHorizontalAlignment(JLabel.CENTER); jetqAlinear.setVerticalAlignment(JLabel.CENTER); break; case 1: jetqAlinear.setText("Arriba a la izquierda"); jetqAlinear.setHorizontalAlignment(JLabel.LEFT); jetqAlinear.setVerticalAlignment(JLabel.TOP); break; case 2: jetqAlinear.setText("Abajo a la derecha"); jetqAlinear.setHorizontalAlignment(JLabel.RIGHT); jetqAlinear.setVerticalAlignment(JLabel.BOTTOM); break; case 3: jetqAlinear.setText("Arriba a la derecha"); jetqAlinear.setHorizontalAlignment(JLabel.RIGHT); jetqAlinear.setVerticalAlignment(JLabel.TOP); break; case 4: jetqAlinear.setText("Abajo a la izquierda"); jetqAlinear.setHorizontalAlignment(JLabel.LEFT); jetqAlinear.setVerticalAlignment(JLabel.BOTTOM); break; } } }); // Agrega los componentes al panel de contenido. jmarco.add(jetqSimple); jmarco.add(jetqBorde); jmarco.add(jetqIcono); jmarco.add(jetqHTML); jmarco.add(jetqDes); jmarco.add(jetqAlinear); jmarco.add(jbtnCambiar); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoEtiqueta( ); } }); } } www.fullengineeringbook.net Swing 381 382 Java: Soluciones de programación Aquí se muestra la salida de ejemplo: Opciones Además de establecer u obtener el texto de una etiqueta, puede obtener u establecer el icono usando estos métodos: Icon getIcon( ) void setIcon(Icon icono) Aquí, icono especifica el icono que se desplegará dentro de la etiqueta. Debido a que el icono puede especificarse cuando se crea la etiqueta, sólo necesita usar setIcon( ) si quiere cambiar el icono después de que se ha creado la etiqueta. Cuando se deshabilita una etiqueta, su contenido se muestra automáticamente en gris; esto incluye su icono. Sin embargo, puede especificar una imagen separada que habrá de usarse cuando la etiqueta está deshabilitada al llamar a setDisabledIcon( ), que se muestra a continuación: void setDisabledIcon(Icon icono) Aquí, icono es la imagen mostrada cuando la etiqueta está deshabilitada. Cuando una etiqueta contiene un icono y texto, como opción predeterminada el icono se despliega a la izquierda. Esto puede cambiarse al llamar a uno de los métodos siguientes, o a ambos: void setVerticalTextPosition(int ubi) void setHorizontalTextPosition(int ubi) En el caso de setVerticalTextPosition, ubi debe ser una de las constantes de alineación vertical descritas antes. Para setHorizontalTextPosition, ubi debe ser una de las constantes de alineación horizontal. Por ejemplo, en el programa anterior, puede colocar el texto de jetqIcono arriba de su icono, al incluir las siguientes instrucciones: jetqIcono.setVerticalTextPosition(JLabel.TOP); jetqIcono.setHorizontalTextPosition(JLabel.CENTER); www.fullengineeringbook.net Capítulo 8: Swing 383 En ocasiones, una etiqueta describe el propósito o significado de otro componente, como un campo de texto. Por ejemplo, un campo de texto que acepta un nombre podía antecederse con una etiqueta que diga "Nombre". En esta situación, también es común que la etiqueta despliegue un texto mnemotécnico de teclado que actúa como teclas de método abreviado que causará que el enfoque de entrada pase al otro componente. Por tanto, para el campo Nombre, la opción mnemotécnica podría ser N. cuando se especifica una opción, al oprimir la tecla junto con alt se hace que el enfoque de entrada se mueva al campo de texto. Para agregar una combinación mnemotécnica a una etiqueta se requieren dos pasos. En primer lugar, debe especificar el carácter mnemotécnico al llamar a setDisplayedMnemonic( ). En segundo lugar, debe vincular el componente que recibirá el enfoque con la etiqueta al llamar a setLabelFor( ). Ambos métodos se definen con JLabel. El método setDisplayedMnemonic( ) tiene dos versiones. He aquí una de ellas: void setDisplayedMnemonic(char car) Aquí, car especifica el carácter que se mostrará como método abreviado de teclado. Por lo general, esto significa que el carácter está subrayado. Si existe más de uno de los caracteres especificados en el texto de la etiqueta, entonces se subraya su primera aparición. El carácter que se pasa vía car puede estar en mayúsculas o minúsculas. Después de que establezca el elemento mnemotécnico, debe vincular la etiqueta con el componente que recibirá el enfoque cuando se oprima la tecla de método abreviado. Para esto, use el método setLabelFor( ): void setLabelFor(Component comp) Aquí, comp es una referencia al componente que obtendrá el enfoque cuando se oprima la tecla mnemotécnica junto con alt. Cree un botón simple Componentes clave Clases e interfaces Métodos java.awt.event.ActionEvent String getActionCommand( ) java.awt.event.ActionListener void actionPerformed(ActionEvent ae) javax.swing.JButton void addActionListener(ActionListener al) void setEnabled(boolean estado) boolean isEnabled( ) Quizás el control de GUI de uso más común sea el botón. Un botón es una instancia de JButton, que hereda la clase abstracta AbstractButton. Ésta define la funcionalidad común a todos los botones. El modelo que usa JButton es ButtonModel. www.fullengineeringbook.net 384 Java: Soluciones de programación Los botones de Swing permiten una amplia gama de funciones. He aquí algunos ejemplos. Un JButton puede contener texto, una imagen, o ambos. El botón puede habilitarse o deshabilitarse bajo control del programa. El icono puede cambiarse dinámicamente con base en el estado del botón. Por ejemplo, el botón puede desplegar un icono cuando se pasa el ratón sobre él y otro cuando se oprime o cuando está deshabilitado. Debido a la importancia de los botones, se usan dos soluciones para describirlos. En ésta se muestra cómo crear y administrar un botón básico. En la siguiente solución se muestra cómo agregar iconos, usar HTML y definir un botón predeterminado. Paso a paso Para crear y administrar un botón, se requieren dos pasos: 1. Cree una instancia de JButton. 2. Defina un ActionListener para el botón. Este escucha manejará los sucesos de opresión del botón en su método actionPerformed( ). 3. Agregue la instancia de ActionListener al botón al llamar a addActionListener( ). 4. Una manera de identificar cuál botón ha generado un ActionEvent consiste en llamar a getActionCommand( ). Devuelve la cadena de comandos de acción asociada con el botón. 5. Para deshabilitar o habilitar un botón, llame a setEnabled( ). 6. Para determinar si un botón está habilitado o deshabilitado, llame a isEnabled( ). Análisis Para crear un botón, cree una instancia de JButton. Define varios constructores. El usado aquí es: JButton(String msj) Aquí, msj especifica el mensaje desplegado dentro del botón. Cuando se oprime un botón, genera un ActionEvent, que está empaquetado en java.awt.event. Para escuchar este suceso, primero debe crear una implementación de la interfaz ActionListener (también empaquetada en java.awt.event) y luego registrar este escucha con el botón. Después de hacer esto, se pasa al escucha de registro un ActionEvent cada vez que se oprime el botón. La interfaz ActionListener sólo define un método: actionPerformed( ). Aquí se muestra: void actionPerformed(ActionEvent ae) A este método se le llama cuando se oprime un botón. En otras palabras, es el manejador de ese tipo de sucesos. Para registrar un escucha de acción para un botón, use el método addActionListener( ) proporcionado por JButton. Aquí se muestra: void addActionListener(ActionListener al) El objeto pasado en al recibirá notificaciones de sucesos. Este objeto debe ser una instancia de una clase que implemente la interfaz ActionListener, como se acaba de describir. www.fullengineeringbook.net Capítulo 8: Swing 385 Empleando el objeto ActionEvent pasado a actionPeformed( ), puede obtener varias piezas útiles de información relacionadas con el suceso de opresión del botón. El usado en esta solución es la cadena de comandos de acción asociada con el botón. Todos los botones tienen una cadena de comandos de acción asociada. Como opción predeterminada, es la cadena que se despliega dentro del botón. La cadena de comandos de acción se obtiene al llamar a getActionCommand( ) en el objeto de suceso. Se declara así: String getActionCommand( ) Cuando se usan dos o más botones dentro de la misma aplicación, la cadena de comandos de acción le da una manera fácil de identificar cuál botón generó el suceso. En otras palabras, puede usar la cadena de comandos de acción para determinar cuál botón se oprimió. Puede deshabilitar o habilitar un botón bajo control del programa al llamar a setEnabled( ). Aquí se muestra: void setEnabled(Boolean estado) Si estado es falso, el botón está deshabilitado. Esto significa que no es posible oprimir el botón y que se muestra en gris. Si estado es verdadero, el botón está habilitado. Para determinar el estado habilitado o deshabilitado de un botón, llame a isEnabled( ): boolean isEnabled( ) Devuelve verdadero si el botón está habilitado y falso si no. Ejemplo En el siguiente ejemplo se muestra JButton en acción. El programa crea dos botones y una etiqueta. Cada vez que se oprime un botón, el hecho se reporta en la etiqueta. Los botones se denominan Alfa y Beta. Por tanto, "Alfa" y "Beta" son las cadenas de comando de acción para los botones. El escucha de acción usa estas cadenas para identificar cuál botón se oprimió. Cada vez que se oprime Alfa, se intercambia el estado de habilitado o deshabilitado. // Demuestra JButton. import java.awt.*; import java.awt.event.*; import javax.swing.*; class DemoBoton implements ActionListener { JLabel jetq; JButton jbtnA; JButton jbtnB; DemoBoton( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Un ejemplo de botón"); // Establece el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); www.fullengineeringbook.net 386 Java: Soluciones de programación // Da al marco un tamaño inicial. jmarco.setSize(220, 90); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta. jetq = new JLabel("Oprima un botón."); // Genera dos botones. jbtnA = new JButton("Alfa"); jbtnB = new JButton("Beta"); // Agrega escuchas de acción. jbtnA.addActionListener(this); jbtnB.addActionListener(this); // Agrega los botones y la etiqueta al panel de contenido. jmarco.add(jbtnA); jmarco.add(jbtnB); jmarco.add(jetq); // Despliega el marco. jmarco.setVisible(true); } // Maneja sucesos de botón. public void actionPerformed(ActionEvent ae) { String ac = ae.getActionCommand( ); // Ve cuál botón se oprimió. if(ac.equals("Alfa")) { // Cambia el estado de Beta cada vez que se oprime Alfa. if(jbtnB.isEnabled( )) { jetq.setText("Alfa oprimido. Beta deshabilitado."); jbtnB.setEnabled(false); } else { jetq.setText("Alfa oprimido. Beta habilitado."); jbtnB.setEnabled(true); } } else if(ac.equals("Beta")) jetq.setText("Beta oprimido."); } public static void main(String args[ ]) { // Crea el marco del subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoBoton( ); } }); } } www.fullengineeringbook.net Capítulo 8: Swing 387 He aquí una salida de ejemplo. En la primera ventana se muestran ambos botones habilitados. En la segunda, el botón Beta está deshabilitado, lo que significa que está atenuado. Opciones JButton proporciona constructores que le permiten especificar un icono, o icono y texto dentro del botón. Puede especificar iconos adicionales que indican cuando se pasa el ratón sobre él, cuando está deshabilitado y cuando está oprimido. También puede usar HTML dentro del texto mostrado en el botón. Estas características se describen en la solución siguiente. Puede establecer el texto dentro de un botón después de que se ha creado al llamar a setText( ). Puede obtener el texto dentro de un botón al llamar a getText( ). Aquí se muestran estos métodos: void setText(String msj) String getText( ) Si se ha establecido de manera específica la acción del botón, entonces el establecimiento del texto no afectará el comando de acción. De otra manera, al cambiar el texto, también se transformará el comando de acción. Como opción predeterminada, la cadena del comando de acción asociada con un botón es la que se despliega en éste. Sin embargo, es posible establecer el comando de acción en otro cadena al llamar a setActionCommand( ), que se muestra aquí: void setActionCommand(String nuevoCmd) La cadena pasada en nuevoCmd se vuelve el comando de acción para el botón. El texto del botón no se ve afectado. Por ejemplo, esto establece la cadena "Mi botón" para el comando de acción de jbtnA en el ejemplo: jbtnA.setActionCommand("Mi botón"); Después de hacer este cambio, el nombre dentro del botón aún es Alfa, pero "Mi botón" es la cadena del comando de acción. El establecimiento del comando de acción es particularmente útil cuando dos componentes diferentes usan el mismo nombre. El cambio de las cadenas de los comandos de acción le permite distinguirlos. Otra manera de determinar cuál componente generó un suceso de acción (o cualquier otro tipo de suceso) consiste en llamar a getSource( ) en el objeto del suceso. Este método está definido por EventObject, que es la superclase de todas las clases de sucesos. Devuelve una referencia al objeto que generó el suceso. Por ejemplo, he aquí otra manera de escribir el método actionPerformed( ) en el programa de ejemplo: // Usa getSource( ) para determinar el origen del suceso. public void actionPerformed(ActionEvent ae) { // Ve cuál botón se oprimió al llamar a getSource( ). if(ae.getSource( ) == jbtnA) { www.fullengineeringbook.net 388 Java: Soluciones de programación // Cambia el estado de Beta cada vez que se oprime Alfa. if(jbtnB.isEnabled( )) { jetq.setText("Alfa oprimido. Beta deshabilitado."); jbtnB.setEnabled(false); } else { jetq.setText("Alfa oprimido. Beta habilitado."); jbtnB.setEnabled(true); } } else if(ae.getSource( ) == jbtnB) jetq.setText("Beta oprimido."); } A muchos programadores les gusta más este método que usar la cadena de comandos de acción, porque evita la sobrecarga de la comparación de cadenas. Por supuesto, implica que el manejador tiene acceso a la referencia del componente original. Esto no siempre resulta conveniente, o posible. En esos casos, la siguiente opción puede ser adecuada. En el ejemplo, la clase DemoBoton implementó la interfaz ActionListener, proporcionando el método actionPerformed( ). Aunque no hay nada erróneo en esto, no es la única manera de manejar sucesos. Suelen usarse otros dos métodos. En primer lugar, puede implementar clases de escucha separadas. Por tanto, diferentes clases pueden manejar diferentes sucesos y estas clases estarían separadas de la clase principal de la aplicación. En segundo lugar, puede implementar escuchas mediante el uso de clases internas anónimas. Las clases internas anónimas son clases internas que no tienen un nombre. En cambio, una instancia de la clase simplemente se genera "al vuelo", cuando es necesaria. Las clases internas anónimas implementan algunos tipos de manejadores de sucesos de manera mucho más fácil. Por ejemplo, los manejadores de sucesos de acción para jbtnA en el ejemplo anterior se implementarían usando la clase interna anónima, como se muestra aquí: jbtnA.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { if(jbtnB.isEnabled( )) { jetq.setText("Alfa oprimido. Beta deshabilitado."); jbtnB.setEnabled(false); } else { jetq.setText("Alfa oprimido. Beta habilitado."); jbtnB.setEnabled(true); } } }); En este método, se crea una clase anónima interna que implementa la interfaz ActionListener. Preste especial atención a la sintaxis. El cuerpo de la clase interna empieza después de la { que sigue a new ActionListener( ). Además, tome nota de que la llamada a addActionListener( ) termina con una ) y ;, como sería normal. La misma sintaxis y el mismo método básicos se usan para crear una clase interna anónima para cualquier manejador de sucesos. Por supuesto, para diferentes sucesos, debe especificar distintos escuchas e implementar diferentes métodos. Una ventaja de usar una clase interna anónima es que ya se conoce el componente que invoca los métodos de la clase. No es necesario llamar a getActionCommand( ), por ejemplo, para determinar cuál botón generó el suceso, porque cada implementación de actionPerformed( ) está relacionada con un solo botón: el que generó el suceso. www.fullengineeringbook.net Capítulo 8: Swing 389 He aquí el aspecto del programa del ejemplo anterior cuando se retrabaja para usar clases internas anónimas que manejen sucesos de acción de botón. // Usa clases internas anónimas para manejar sucesos // de acción de JButton. Observe que DemoBoton ya no // implementa ActionListener. import java.awt.*; import java.awt.event.*; import javax.swing.*; class DemoBoton { JLabel jetq; JButton jbtnA; JButton jbtnB; DemoBoton( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Un botón de ejemplo"); // Establecer el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(220, 90); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta. jetq = new JLabel("Oprima un botón."); // Genera dos botones. jbtnA = new JButton("Alfa"); jbtnB = new JButton("Beta"); // Usa clases internas anónimas para manejar sucesos de botón. jbtnA.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { if(jbtnB.isEnabled( )) { jetq.setText("Alfa oprimido. Beta deshabilitado."); jbtnB.setEnabled(false); } else { jetq.setText("Alfa oprimido. Beta habilitado."); jbtnB.setEnabled(true); } } }); www.fullengineeringbook.net 390 Java: Soluciones de programación jbtnB.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { jetq.setText("Beta oprimido."); } }); // Agrega los botones y etiquetas al panel de contenido. jmarco.add(jbtnA); jmarco.add(jbtnB); jmarco.add(jetq); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoBoton( ); } }); } } Este programa es funcionalmente equivalente a la primera versión. La diferencia está en que ahora cada botón está vinculado con su propio manejador de sucesos de acción. No es necesario que DemoBoton implemente ActionListener o que use getActionCommand( ) para determinar cuál botón se oprimió. Un último detalle es que puede crear un botón de dos estados al emplear JToggleButton. Consulte Cree un botón interruptor. Use iconos, HTML y mnemotécnica con JButton Componentes clave Clases Métodos javax.swing.JRootPane void setDefaultButton(JButton boton) javax.swing.JButton void void void void void setDisabledIcon(Icon iconoDeshabilitdo) setIcon(Icon iconoPredet) setMnemonic(int teclaMnemo) setPressedIcon(Icon iconoOprimido) setRolloverIcon(Icon iconoPasoRaton) www.fullengineeringbook.net Capítulo 8: Swing 391 Más allá de la funcionalidad básica descrita en la solución anterior, JButton permite muchas opciones y personalizaciones. En esta solución se examinan cuatro: • Agregar iconos a un botón • Usar HTML en un botón • Definir un botón predeterminado • Agregar una tecla mnemotécnica a un botón Estas características le ayudan a dar a su aplicación un aspecto distintivo y pueden mejorar su uso. Paso a paso Para agregar iconos a un botón, especificar un botón predeterminado, o usar HTML en un botón se requieren uno o más de los pasos siguientes: 1. Para especificar el icono predeterminado (que se muestra cuando el botón está habilitado), pase el icono al constructor JButton. También puede establecer el icono predeterminado al llamar a setIcon( ). 2. Para especificar un icono que habrá de desplegarse cuando se pase el ratón sobre el botón, llame a setRolloverIcon( ). 3. Para especificar un icono que se despliega cuando el botón está deshabilitado, llame a setDisabledicon( ). 4. Para especificar un icono que habrá de desplegarse cuando se oprima el botón, llame a setPressedIcon( ). 5. Para especificar una tecla mnemotécnica para un botón, llame a setMnemonic( ). 6. Para definir un botón predeterminado (un botón que se oprimirá cuando el usuario oprime enter), llame a setDefaultButton( ). 7. Para desplegar HTML dentro de un botón, empiece la cadena con <html>. Análisis Para crear un botón que contenga un icono, usará este constructor de JButton: JButton(Icon icono) Aquí, icono especifica el icono que habrá de usarse para el botón. Para crear un botón que contenga un icono y texto, use este constructor: JButton(String cad, Icon icono) Cuando están presentes texto e icono, éste se encuentra al principio y el texto al final. Sin embargo, puede cambiar las posiciones relativas de imagen y texto. El especificado en estos constructores es el icono predeterminado. Es el que se usará para todos los fines si no se especifica algún otro. www.fullengineeringbook.net 392 Java: Soluciones de programación El icono predeterminado también puede especificarse o cambiarse después de que se ha creado, al llamar a setIcon( ). Aquí se muestra: void setIcon(Icon iconoPredet) El icono predeterminado está especificado con iconoPredet. JButton también le permite especificar iconos que están desplegados cuando el botón se encuentra deshabilitado, cuando se oprime y cuando se pasa el ratón sobre él. Para establecer estos iconos, usará los siguientes métodos: void setDisabledIcon(Icon iconoDeshabilitdo) void setPressedIcon(Icon iconoOprimido) void setRolloverIcon(Icon iconoPasoRaton) Un vez que se ha establecido el icono especificado, se desplegará cada vez que ocurra uno de los sucesos. Sin embargo, tenga en cuenta que el icono que se muestra al pasar el ratón sobre el botón tal vez no tenga soporte en todos los aspectos. Puede determinar si el icono está habilitado al llamar a isRolloverEnabled( ). Al establecerlo, este icono se habilita automáticamente. Puede establecer de manera explícita la propiedad de habilitación de despliegue cuando se pasa el ratón mediante una llamada a setRolloverEnabled(true). Puede definir un botón que se "oprimirá" automáticamente cuando el usuario oprima enter en el teclado. A éste se le denomina botón predeterminado. Para crear un botón predeterminado, llame a setDefaultButton( ) en el objeto de JRootPane que contiene el botón. Aquí se muestra este método: void setDefaultButton(JButton boton) Aquí, boton es el botón que se seleccionará como predeterminado. Recuerde que esté método está definido por JRootPane y, por tanto, debe llamarse en el panel raíz. Obtendrá una referencia al panel raíz al llamar a getRootPane( ) en el contenedor de nivel superior. Puede agregar una tecla mnemotécnica al texto desplegado dentro de un botón al llamar a setMnemonic( ). Cuando está tecla se oprime junto con alt, el botón se oprimirá. Aquí se muestra este método: void setMnemonic(int teclaMnemo) Aquí, teclaMnemo especifica la tecla mnemotécnica. Debe ser una de las constantes definidas en java.awt.event.KeyEvent, como VK_A, VK_X, o VK_S. La clase KeyEvent define constantes VK_ para todas las teclas del teclado. Por tanto, suponiendo un JButton llamado jbtn, puede asignar la tecla mnemotécnica T con esta instrucción: jbtn.setMnemonic(KeyEvent.VK_T) Después de que se ejecute está instrucción, es posible oprimir el botón al escribir alt+t. NOTA Hay otra versión de setMnemonic( ) que toma un argumento car, pero se considera obsoleta. Puede usar una cadena que contenga HTML como texto que se desplegará dentro de un botón. Para ello, empiece la cadena con <html>. Cuando se haga esto, el texto se formará automáticamente como lo especificó el marcado. Esto le permite crear botones con títulos que abarcan dos o más www.fullengineeringbook.net Capítulo 8: Swing 393 líneas. Pero tenga cuidado: esto puede llevar a botones excesivamente grandes, lo que a veces tiene un efecto desagradable. Otro detalle que debe tomarse en cuenta es que cuando se usa HTML, no se desplegará el elemento mnemotécnico asociado con un botón. Ejemplo En el siguiente ejemplo se expande el presentado en la solución anterior al agregar iconos y teclas mnemotécnicas, y al establecer jbtnA como botón predeterminado. Cuando pruebe el programa, notará que se despliega el icono que se mostrará cuando se pase el ratón encima de él. El icono oprimido se desplegará cuando se oprima el botón. Cada vez que oprima jbtnA, jbtnB pasa de habilitado a deshabilitado. Cuando el botón está deshabilitado, se despliega el icono de deshabilitado. Cuando se oprime cualquiera de los botones, se despliega el icono de oprimido. Observe que las teclas mnemotécnicas para jbtnA y jbtnB son A y B, respectivamente. Además, observe que jbtnA está establecido como el botón predeterminado. Esto significa que se oprime cuando teclea enter. // Demuestra iconos de botón, un botón predeterminado, HTML en un botón, // y teclas mnemotécnicas en un botón. import java.awt.*; import java.awt.event.*; import javax.swing.*; class PersonalizarBotones { JLabel jetq; JButton jbtnA; JButton jbtnB; PersonalizarBotones( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Personalizar botones"); // Establece el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(220, 100); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta. jetq = new JLabel("Oprima un botón."); // Carga los iconos. ImageIcon iconoA = new ImageIcon("IconoA.gif"); ImageIcon iconoADes = new ImageIcon("IconoADes.gif"); ImageIcon iconoARS = new ImageIcon("IconoARS.gif"); ImageIcon iconoAO = new ImageIcon("IconoAOprimido.gif"); www.fullengineeringbook.net 394 Java: Soluciones de programación ImageIcon ImageIcon ImageIcon ImageIcon iconoB = new ImageIcon("IconoB.gif"); iconoBDes = new ImageIcon("IconoBDes.gif"); iconoBRS = new ImageIcon("IconoBRS.gif"); iconoBO = new ImageIcon("IconoBOprimido.gif"); // Especifica el icono predeterminado cuando se construyen los botones. jbtnA = new JButton("Alfa", iconoA); jbtnB = new JButton("Beta", iconoB); // Establece los iconos que se despliegan cuando se pasa el ratón. jbtnA.setRolloverIcon(iconoARS); jbtnB.setRolloverIcon(iconoBRS); // Establece iconos de oprimido. jbtnA.setPressedIcon(iconoAO); jbtnB.setPressedIcon(iconoBO); // Establece iconos deshabilitados. jbtnA.setDisabledIcon(iconoADes); jbtnB.setDisabledIcon(iconoBDes); // Establece jbtnA como botón predeterminado. jmarco.getRootPane( ).setDefaultButton(jbtnA); // Establece teclas mnemotécnicas para los botones. jbtnA.setMnemonic(KeyEvent.VK_A); jbtnB.setMnemonic(KeyEvent.VK_B); // Maneja sucesos de botón. jbtnA.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { if(jbtnB.isEnabled( )) { jetq.setText("Alfa oprimido. Beta deshabilitado."); jbtnB.setEnabled(false); } else { jetq.setText("Alfa oprimido. Beta habilitado."); jbtnB.setEnabled(true); } } }); jbtnB.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { jetq.setText("Beta oprimido."); } }); // Agrega los botones y etiquetas al panel de contenido. jmarco.add(jbtnA); jmarco.add(jbtnB); jmarco.add(jetq); www.fullengineeringbook.net Capítulo 8: Swing 395 // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new PersonalizarBotones( ); } }); } } Aquí se muestra la salida de ejemplo, pero para apreciar verdaderamente el efecto de los iconos, las teclas mnemotécnicas y el botón predeterminado, necesitará ejecutar el programa. Opciones Para ver el efecto del uso de HTML dentro de un botón, cambie las declaraciones de jbtnA y jbtnB, como se muestra aquí: jbtnA = new JButton("<html>Alfa<br>Oprímeme", iconoA); jbtnB = new JButton("<html>Beta<br>¡También oprímeme!", iconoB); Ahora el botón tendrá este aspecto: Observe que las teclas mnemotécnicas ya no se despliegan. Sin embargo, aún funcionarán. Puede determinar si un botón es el predeterminado al llamar a isDefaultButton( ), que se muestra aquí: boolean isDefaultButton( ) Puede indicar que un botón no debe usarse como el predeterminado al llamar a setDefaultCapable( ), que se muestra aquí: void setDefaultCapable(boolean on) www.fullengineeringbook.net 396 Java: Soluciones de programación Si on es verdadero, el botón puede usarse como predeterminado. Si es falso, no debe usarse de esa manera. Puede determinar si un botón tiene opción de ser predeterminado al llamar a isDefaultCapable( ), que se muestra a continuación: boolean isDefaultCapable( ) Devuelve verdadero si el botón debe usarse como predeterminado, y falso de otra manera. Como regla general, los botones tienen la opción de ser los predeterminados, pero esta propiedad puede establecerse como falsa si su apariencia y percepción no soportan botones predeterminados. Debe tener cuidado cuando especifique un botón predeterminado, porque el usuario podría oprimirlo inadvertidamente. La regla que sigo es simple: un botón predeterminado no debe ser dañino. Si el botón predeterminado cambiará un archivo, por ejemplo, entonces necesita tener una medida de seguridad que evite que el usuario sobrescriba por accidente el archivo que ya existe. La adición de los iconos predeterminados, para indicar posición del ratón, deshabilitado y oprimido agrega atractivo visual a su interfaz. Sin embargo, la adición de estos iconos aumentará el tiempo de descarga. Por tanto, debe buscar un equilibrio entre los beneficios y el costo. Cree un botón interruptor Componentes clave Clases e interfaces Métodos javax.swing.event.ItemEvent Object getItem( ) int getStateChange( ) javax.swing.event.ItemListener void itemStateChanged(ItemEvent ie) javax.swing.JToggleButton void addItemListener(ItemListener il) boolean isSelected( ) void setselected(boolean on) En ocasiones, querrá usar un botón para habilitar o deshabilitar alguna función. Por ejemplo, imagine una aplicación de escritorio que controla una cinta transportadora. La GUI para esta aplicación podría usar un botón llamado Correr para encenderlo y apagarlo. La primera vez que el botón se oprime, encenderá la cinta. Cuando se vuelve a oprimir, se apaga. Aunque este tipo de funcionalidad puede implementarse usando JButton, Swing ofrece una mejor opción: JToggleButton. En esta solución se muestra cómo usarlo. JToggleButton tiene el aspecto de cualquier botón, pero actúa diferente, porque tiene dos estados: oprimido y sin oprimir. Cuando oprime un botón interruptor, permanece oprimido en lugar de regresar a su posición, como otros botones. Cuando oprime el botón por segunda ocasiones, se libera (salta hacia arriba). Por tanto, cada vez que se oprime un botón interruptor, cambia entre dos estados. www.fullengineeringbook.net Capítulo 8: Swing 397 Aunque es útil por cuenta propia, JToggleButton resulta importante por otra razón: es una superclase para otros dos componentes de Swing que también representan controles de dos estados: JCheckBox y JRadioButton, que se describen en Cree casillas de verificación y Cree botones de opción. Por tanto, JToggleButton define la funcionalidad básica de todos los componentes de dos estados. Paso a paso Para usar un botón interruptor, se requieren estos pasos: 1. Cree una instancia de JToggleButton. 2. Registre un ItemListener para el botón y maneje sucesos de elementos generados por el botón. 3. Para determinar si el botón está encendido o apagado, llame a isSelected( ) en la instancia de JButton. Si el botón está oprimido, el método devolverá verdadero. Como opción, llame a getStateChange( ) en la instancia de ItemEvent. Devuelve el estado actual del botón. 4. Puede seleccionar (es decir, oprimir) un botón interruptor bajo control del programa al llamar a setSelected( ). Análisis Los botones interruptores son objetos de la clase JToggleButton, que extiende AbstractButton. (Aunque está relacionada con un botón, JToggleButton no extiende JButton). JToggleButton define varios constructores, que le permiten especificar el texto o la imagen (o ambos) que se despliegan dentro del botón. También puede establecer un estado inicial. Aquí se muestra el constructor usado en la solución: JToggleButton(String cad, boolean estado) Esto crea un botón interruptor que contiene el texto pasado en cad. Si estado es verdadero, el botón está oprimido inicialmente (seleccionado). De otra manera, está liberado (sin seleccionar). JToggleButton genera un suceso de acción cada vez que se oprime. También genera un suceso de elemento, que es un objeto de tipo ItemEvent, usado por los componentes que permiten el concepto de selección. Cuando un JToggleButton se oprime, está seleccionado. Cuando permanece sin oprimir, no lo está. Aunque puede manejar un botón interruptor mediante sus sucesos de acción, suele manejarse mediante sus sucesos de elemento. Los sucesos de elemento se manejan al implementar la interfaz ItemListener. Esta especifica sólo un método: itemStateChanged( ), que se muestra aquí: void itemStateChanged(ItemEvent ie) El suceso de elemento se recibe en ie. Para recibir una referencia al elemento que cambio, llame a getItem( ) en el objeto de ItemEvent. Aquí se muestra este método: Object getItem( ) La referencia devuelta debe convertirse a la clase del componente que se está manejando, que en este caso es JToggleButton. El método getItem( ) es particularmente útil en casos en que dos o más www.fullengineeringbook.net 398 Java: Soluciones de programación componentes comparten el mismo manejador ItemEvent, porque le da una manera de identificar cuál componente generó el suceso. Cuando ocurre un suceso de elemento, el componente estará en uno de dos estados: seleccionado y no seleccionado. La clase ItemEvent define las siguientes constantes static int que representan esos dos estados. ItemEvent.SELECTED ItemEvent.DESELECTED Para obtener el nuevo estado, llame al método getStateChange( ) definido por ItemEvent. Aquí se muestra: int getStateChange( ) Devuelve ItemEvent.SELECTED o ItemEvent.DESELECTED. También puede determinar el estado seleccionado o no de un botón interruptor al llamar a isSelected( ). Suele ser el método más fácil. Aquí se muestra: boolean isSelected( ) Devuelve verdadero si el botón interruptor está oprimido y falso si no lo está. Puede seleccionar o dejar de seleccionar un botón interruptor (es decir, hacer que esté oprimido o no) al llamar a setSelected( ), que se muestra aquí: void setSelected(boolean on) Si on es verdadero, el botón está oprimido. Si es falso, no lo está. Ejemplo En el siguiente ejemplo se demuestra un botón interruptor al mostrar cómo puede usarse para controlar una cinta transportadora. Observe cómo funciona el escucha de elementos. Simplemente llama a isSelected( ) para determinar el estado del botón. // Demuestra un JToggleButton. import java.awt.*; import java.awt.event.*; import javax.swing.*; class DemoInterruptor { JLabel jetq; JLabel jetq2; JToggleButton jbtnint; DemoInterruptor( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Demuestra JToggleButton"); www.fullengineeringbook.net Capítulo 8: Swing // Establece el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(280, 90); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta. jetq = new JLabel("Control de la cinta: "); jetq2 = new JLabel("Cinta detenida"); // Crea un botón interruptor. jbtnint = new JToggleButton("Correr / Detener", false); // Agrega un escucha de elemento a jbtnint. jbtnint.addItemListener(new ItemListener( ) { public void itemStateChanged(ItemEvent ie) { if(jbtnint.isSelected( )) jetq2.setText("Cinta corriendo"); else jetq2.setText("Cinta detenida"); } }); // Agrega un botón interruptor y una etiqueta al panel de contenido. jmarco.add(jetq); jmarco.add(jbtnint); jmarco.add(jetq2); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoInterruptor( ); } }); } } Aquí se muestra la salida de ejemplo www.fullengineeringbook.net 399 400 Java: Soluciones de programación Opciones Como un JButton, un JToggleButton puede desplegar un icono o una combinación de icono y texto. Para agregar el icono predeterminado, puede usar esta forma del constructor de JToggleButton: JToggleButton(String cad, Icon icono) Se crea un botón interruptor que contiene el texto pasado en cad y la imagen pasada en icono. Además, como JButton, le permite especificar iconos que indican cuando el botón está oprimido o liberado, o cuando se ha pasado el ratón sobre él. Para agregar los otros iconos, use estos métodos: setDisabledIcon( ), setPressedIcon( ) y setRolloverIcon( ). Debido a que JToggleButton tiene dos estados (oprimido y liberado), también le permite agregar iconos que describen estos dos estados al llamar a setRolloverSelectedIcon( ) y setSelectedIcon( ). Tenga en cuenta que debe proporcionar un icono predeterminado para que se puedan usar los otros iconos. Se especifica la posición del texto en relación con un icono al llamar a setVerticalTextPosition( ) o setHorizontalTextPosition( ). Cuando se usa un botón interruptor, a veces resulta útil desplegar un mensaje diferente cuando el botón se oprime y cuando está liberado. Para ello, simplemente establezca el texto cada vez que el estado del botón cambie. [El texto dentro del botón puede establecerse al llamar a setText( )]. Puede agregar una tecla mnemotécnica al texto desplegado dentro de un JToggleButton al llamar a setMnemonic( ). Puede habilitar o deshabilitar un botón interruptor al llamar a setEnabled( ). Esto funciona de la misma manera para un botón interruptor que para los botones comunes. Cree casillas de verificación Componentes clave Clases e interfaces Métodos javax.swing.JCheckBox void addItemListener(ItemListener il) boolean isSelected( ) void setSelected(bolean on) javax.swing.event.ItemEvent Object getItem( ) int getStateChange( ) javax.swing.event.ItemListener void itemStateChanged(ItemEvent ie) En esta solución se demuestra la casilla de verificación, que es un objeto de tipo JCkeckBox. Una casilla de verificación suele usarse para seleccionar una opción. Por ejemplo, un IDE podría usar casillas de verificación para seleccionar varias opciones de compilador, como niveles de advertencia, optimización de código y modo de depuración. Si se marca una casilla, la opción queda seleccionada. Si se quita la marca, se ignora la opción. Cualquiera que sea su uso, las casillas de verificación son un componente principal de muchas GUI. www.fullengineeringbook.net Capítulo 8: Swing 401 Paso a paso Para usar una casilla de verificación se requieren estos pasos: 1. Cree una interfaz de JCheckBox. 2. Registre un ItemListener para la casilla de verificación y maneje sucesos de elementos generados por la casilla. 3. Para determinar si la casilla de verificación está seleccionada, llame a isSelected( ). Si la casilla está marcada, se devuelve verdadero. Si la casilla se limpia, se devuelve falso. Como opción, llame a getStateChange( ) en la instancia de ItemEvent. Devuelve el estado actual de la casilla de verificación. 4. Puede seleccionar una casilla de verificación bajo control del programa al llamar a setSelected( ). Análisis JCheckBox define varios constructores. El usado aquí es: JCheckBox(String cad) Esto crea una casilla de verificación que está relacionada con el texto especificado por cad. En Swing, una casilla de verificación es un tipo especial de botón de dos estados. Como resultado, JCheckBox hereda AbstractButton y JToggleButton. Por tanto, las mismas técnicas que manejan un botón interruptor también aplican a una casilla de verificación. Consulte Cree un botón interruptor para conocer los detalles. Aquí se presenta una breve revisión. Cuando se selecciona o se deja de seleccionar una casilla de verificación, se genera un suceso de elemento. Esto es manejado por itemStateChanged( ). Dentro de éste, el método getItem( ) puede usarse para obtener una referencia al objeto de JCheckBox que generó el suceso. A continuación, puede llamar a getStateChange( ) para determinar si la casilla fue seleccionada o desmarcada. Si se seleccionó, se devuelve ItemEvent.SELECTED. De otra manera, se devuelve ItemEvent. DESELECTED. Como opción, puede llamar a isSelected( ) en la casilla de verificación para determinar si está seleccionada. Puede establecer el estado de una casilla de verificación al llamar a setSelected( ). Las casillas de verificación generan un suceso de elemento cada vez que cambia el estado de una casilla. También generan sucesos de acción cuando cambia una selección, pero suele ser más fácil de usar un ItemListener porque le da acceso directo al método getStateChange( ). También le da acceso al método getItem( ). Ejemplo En el siguiente programa se demuestran las casillas de verificación. Se definen cuatro casillas de verificación que permiten la traducción a idiomas extranjeros. A la primera casilla de verificación se le llama Traducir. Está habilitada como opción predeterminada, pero desmarcada. A las tres restantes se les llama Francés, Alemán y Chino. Están deshabilitadas como opción predeterminada. Cuando la casilla de verificación Traducir está marcada, causa que las otras tres casillas de verificación estén habilitadas, lo que permite al usuario seleccionar uno o más idiomas. Los idiomas seleccionados se despliegan en jetqQue. Cada vez que cambia una casilla de verificación, la acción actual se despliega en jetqCambiar. www.fullengineeringbook.net 402 Java: Soluciones de programación // Demuestra casillas de verificación. import java.awt.*; import java.awt.event.*; import javax.swing.*; class DemoCV implements ItemListener { JLabel jetqTraducirA; JLabel jetqQue; JLabel jetqCambiar; JCheckBox jcvTraducir; JCheckBox jcvFrances; JCheckBox jcvAleman; JCheckBox jcvChino; DemoCV( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Demo de casilla de verificación"); // Especifica una cuadrícula de 1 columna y 7 filas. jmarco.setLayout(new GridLayout(7, 1)); // Da el marco un tamaño inicial. jmarco.setSize(280, 160); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea etiquetas. jetqTraducirA = new JLabel("Traducir a:"); jetqCambiar = new JLabel(""); jetqQue = new JLabel("Idioma seleccionado: ninguno"); // Crea las casillas de verificación. jcvTraducir = new JCheckBox("Traducir"); jcvFrances = new JCheckBox("Francés"); jcvAleman = new JCheckBox("Alemán"); jcvChino = new JCheckBox("Chino"); // Deshabilita inicialmente las casillas de verificación de // idioma y la etiqueta Traducir a y los idiomas seleccionados. jetqTraducirA.setEnabled(false); jetqQue.setEnabled(false); jcvFrances.setEnabled(false); jcvAleman.setEnabled(false); jcvChino.setEnabled(false); // Agrega el escucha de elementos para jcvTraducir. jcvTraducir.addItemListener(new ItemListener( ) { // Cambia el estado habilitar/deshabilitar de las // casillas de verificación de idioma y etiquetas. www.fullengineeringbook.net Capítulo 8: // relacionadas. Además, reporta el estado de // jcvTraducir. public void itemStateChanged(ItemEvent ie) { if(jcvTraducir.isSelected( )) { jetqTraducirA.setEnabled(true); jcvFrances.setEnabled(true); jcvAleman.setEnabled(true); jcvChino.setEnabled(true); jetqQue.setEnabled(true); jetqCambiar.setText("Traducción habilitada."); } else { jetqTraducirA.setEnabled(false); jcvFrances.setEnabled(false); jcvAleman.setEnabled(false); jcvChino.setEnabled(false); jetqQue.setEnabled(false); jetqCambiar.setText("Traducción deshabilitada."); } } }); // Los cambios a las casillas de verificación de // idioma se manejan en común con el método // itemStateChanged( ) implementado por DemoCV. jcvFrances.addItemListener(this); jcvAleman.addItemListener(this); jcvChino.addItemListener(this); // Y casillas de verificación y etiquetas al panel de contenido. jmarco.add(jcvTraducir); jmarco.add(jetqTraducirA); jmarco.add(jcvFrances); jmarco.add(jcvAleman); jmarco.add(jcvChino); jmarco.add(jetqCambiar); jmarco.add(jetqQue); // Despliega el marco. jmarco.setVisible(true); } // Esto maneja todas las casillas de verificación de idioma. public void itemStateChanged(ItemEvent ie) { String ops = ""; // Obtiene una referencia a la casilla de verificación que // causó el suceso. JCheckBox cv = (JCheckBox) ie.getItem( ); // Indica al usuario lo que hicieron. www.fullengineeringbook.net Swing 403 404 Java: Soluciones de programación if(ie.getStateChange( ) == ItemEvent.SELECTED) jetqCambiar.setText("Cambio en la selección: " + cv.getText( ) + " seleccionado."); else jetqCambiar.setText("Cambio en la selección: " + cv.getText( ) + " desmarcado."); // Construye una cadena que contiene todos los idiomas seleccionados. if(jcvFrances.isSelected( )) ops += "Francés "; if(jcvAleman.isSelected( )) ops += "Alemán "; if(jcvChino.isSelected( )) ops += "Chino "; // Muestra "Ninguno" si no hay un idioma seleccionado. if(ops.equals("")) ops = "Ninguno"; // Despliega las opciones seleccionadas. jetqQue.setText("Traducir a: " + ops); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoCV( ); } }); } } Aquí se muestra la salida: Como se mencionó, puede seleccionar una casilla de verificación bajo el control del programa al llamar a setSelected( ). Para probar esto, agregue esta línea de código después de agregar el escucha de elemento para jcvTraducir: jcvTraducir.setSelected(true); Esto causa que la casilla de verificación Traducir esté inicialmente seleccionada. También hace que se dispare un ItemEvent. Esto da como resultado que las opciones de idioma se habiliten cuando se despliega la ventana por primera vez. (Recuerde que un ItemEvent se dispara cada vez que la casilla de verificación cambia, esté bajo el control del programa o del usuario). www.fullengineeringbook.net Capítulo 8: Swing 405 Opciones Como los botones de Swing, las casillas de verificación ofrecen un rico conjunto de opciones y características que le permiten crear fácilmente un aspecto personalizado. Por ejemplo, puede sustituir un icono que reemplazará a la pequeña casilla que contiene la marca de verificación. También puede especificar un icono y una cadena. Aquí se presentan los constructores de JCheckBox que soportan estas opciones: JCheckBox(Icon icono) JCheckBox(String cad, Icon icono) Aquí, el icono predeterminado empleado por la casilla de verificación está especificado por icono. Por lo general, este icono sirve como icono de desmarcado. Es decir, es el icono mostrado cuando la casilla no está marcada. Por lo general, cuando se especifica este icono, también necesitará determinar el icono de marcado. Para especificar el icono de marcado, se llama a setSelectedIcon( ), que se muestra aquí: void setSelectedIcon(Icon icono) El icono que se pasa vía icono especifica la imagen que se desplegará cuando la casilla de verificación esté marcada. Debido a que los iconos de marcado y desmarcado funcionan en conjunto como un par, casi siempre necesita especificar ambos cuando está usando un icono con una casilla de verificación. Puede especificar un estado inicial para la casilla de verificación con estos constructores: JCheckBox(String cad, boolean estado) JCheckBox(Icon icono, boolean estado) JCheckBox(String cad, Icon icono, boolean estado) Si estado es verdadero, la casilla de verificación está inicialmente marcada. De otra manera, está inicialmente desmarcada. Puede establecer la alineación de la casilla de verificación y del texto en relación con el icono. Los métodos que manejan esto están definidos por AbstractButton y son setVerticalAlignment( ), setHorizontalAlignment( ), setVerticalTextPosition( ) y setHorizontalTextPosition( ). Puede establecer una tecla mnemotécnica que se muestra en la etiqueta de la casilla de verificación al llamar a setMnemonic( ), que también está definido por AbstractButton. Cree botones de opción Componentes clave Clases e interfaces Métodos java.awt.event.ActionEvent String getActionCommand( ) java.awt.event.ActionListener void actionPerformed(ActionEvent ae) javax.swing.ButtonGroup void add(AbstractButton boton) javax.swing.JRadioButton void addActionListener(ActionListener al) boolean isSelected( ) void setSelected(boolean on) www.fullengineeringbook.net 406 Java: Soluciones de programación En esta solución se muestra cómo crear y administrar botones de opción. Suelen usarse para desplegar un grupo de botones mutuamente excluyentes, en que sólo puede seleccionarse un botón a la vez. Por tanto, proporcionan un medio para que el usuario sólo seleccione una de dos o más opciones. Por ejemplo, cuando se compra una computadora en una tienda en línea, los botones de opción pueden desplegarse para permitirle seleccionar entre una laptop, una handheld o una torre. Los botones de opción están soportados por la clase JRadioButton, que extiende AbstractButton y JToggleButton. Los botones de opción suelen organizarse en grupos, que son instancias de ButtonGroup. Como resultado, en esta solución se explica cómo usar JRadioButton y ButtonGroup. Paso a paso Para crear y administrar botones de opción se requieren estos pasos: 1. Cree una instancia de ButtonGroup. 2. Cree una instancia de JRadioButton. 3. Agregue cada instancia de JRadioButton a la instancia de ButtonGroup. 4. Registre un ActionListener para cada botón de opción y maneje los sucesos de acción generados por los botones. 5. Para determinar si un botón de opción está seleccionado, llame a isSelected( ). Si el botón está seleccionado, se devuelve verdadero. De otra manera, se devuelve falso. Recuerde que sólo puede seleccionarse un botón en cualquier grupo determinado en un momento específico. 6. Puede seleccionar un botón de opción bajo control del programa al llamar a setSelected( ). Análisis JRadioButton proporciona varios constructores. Aquí se muestran los dos usados en esta solución: JRadioButton(String cad) JRadioButton(String cad, boolean estado) Aquí, cad es la etiqueta del botón. El primer constructor crea un botón que está desmarcado, como opción predeterminada. Para el segundo constructor, si estado es verdadero, el botón está seleccionado. De otra manera, está desmarcado. Para que los botones sean mutuamente excluyentes, deben configurarse en un grupo. Una vez que se hace, sólo puede seleccionarse uno de los botones del grupo a la vez. Por ejemplo, si un usuario selecciona un botón de opción que está en un grupo, cualquier botón previamente seleccionado en el grupo se deja de seleccionar de manera automática. Por supuesto, cada grupo de botones está separado del siguiente. Por tanto, puede tener dos grupos diferentes de botones de opción, cada uno con un botón seleccionado. Un grupo de botones se crea con la clase ButtonGroup. Su constructor predeterminado es invocado con este fin. Se agregan elementos al grupo de botones mediante el método siguiente: void add(AbstractButton ab) Aquí, ab es una referencia al botón que se agregará al grupo. www.fullengineeringbook.net Capítulo 8: Swing 407 Un JRadioButton genera sucesos de acción, de elemento y de cambio cada vez que cambia la selección del botón. Con más frecuencia se maneja el suceso de acción, lo que significa que normalmente implementa la interfaz ActionListener. Los sucesos de acción y los escuchas de acción se describieron de manera detallada en Cree un botón simple. Como se explicó allí, el único método definido por ActionListener es actionPerformed( ). Dentro de este método, puede usar varias maneras diferentes para determinar cuál botón está seleccionado. En primer lugar, puede revisar la cadena de comandos de acción con el suceso de acción al llamar a getActionCommand( ). Como opción predeterminada, el comando de acción es el mismo que la etiqueta del botón, pero puede asignar al comando otra acción al llamar a setActionCommand( ) en el botón de opción. En segundo lugar, puede llamar a getSource( ) en el objeto de ActionEvent y revisar esa referencia contra los botones. Por último, puede simplemente marcar cada botón de opción para encontrar cuál está seleccionado al llamar a isSelected( ) en cada botón. Recuerde que cada vez que ocurre un suceso de acción, significa que el botón que se está seleccionando ha cambiado, y que sólo se seleccionará uno y sólo un botón. Cuando se usan botones de opción, normalmente querrá seleccionar al principio uno de los botones. Esto se hace al llamar a setselected( ). Para conocer detalles acerca de isSelected( ) y setSelected( ), consulte Cree un botón interruptor. Ejemplo En el siguiente ejemplo se vuelve a trabajar el caso de la casilla de verificación de la solución anterior, pero usando ahora botones de opción. // Demuestra los botones de opción. import java.awt.*; import java.awt.event.*; import javax.swing.*; class DemoBO implements ActionListener { JLabel jetqTraducirA; JLabel jetqQue; JLabel jetqCambiar; JCheckBox jcvTraducir; JRadioButton jboFrances; JRadioButton jboAleman; JRadioButton jboChino; ButtonGroup bg; DemoBO( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Demo de botones de opción"); // Especifica una cuadrícula de 1 columna y 7 filas. jmarco.setLayout(new GridLayout(7, 1)); www.fullengineeringbook.net 408 Java: Soluciones de programación // Da el marco un tamaño inicial. jmarco.setSize(260, 160); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea las etiquetas. jetqTraducirA = new JLabel("Traducir a:"); jetqCambiar = new JLabel(""); jetqQue = new JLabel("Traducir al francés"); // Crea una casilla de verificación. jcvTraducir = new JCheckBox("Traducir"); // Crea el grupo de botones. bg = new ButtonGroup( ); // Crea los botones de opción. Selecciona el primero. jboFrances = new JRadioButton("Francés", true); jboAleman = new JRadioButton("Alemán"); jboChino = new JRadioButton("Chino"); // Agrega los botones de opción al grupo. bg.add(jboFrances); bg.add(jboAleman); bg.add(jboChino); // Deshabilita inicialmente los botones de // idioma y la etiqueta Traducir a. jetqTraducirA.setEnabled(false); jetqQue.setEnabled(false); jboFrances.setEnabled(false); jboAleman.setEnabled(false); jboChino.setEnabled(false); // Agrega el escucha de elemento para jcvTraducir. jcvTraducir.addItemListener(new ItemListener( ) { // Cambia el estado de habilitado/deshabilitado de // los botones de opción de idioma y las etiquetas. // relacionadas. Además, reporta el estado de jcvTraducir. public void itemStateChanged(ItemEvent ie) { if(jcvTraducir.isSelected( )) { jetqTraducirA.setEnabled(true); jboFrances.setEnabled(true); jboAleman.setEnabled(true); jboChino.setEnabled(true); jetqQue.setEnabled(true); jetqCambiar.setText("Traducción habilitada."); } else { jetqTraducirA.setEnabled(false); www.fullengineeringbook.net Capítulo 8: jboFrances.setEnabled(false); jboAleman.setEnabled(false); jboChino.setEnabled(false); jetqQue.setEnabled(false); jetqCambiar.setText("Traducción deshabilitada."); } } }); // Los cambios a los botones de opción de idioma // son manejados en común por el método // actionPerformed( ) implementado por DemoBO. jboFrances.addActionListener(this); jboAleman.addActionListener(this); jboChino.addActionListener(this); // Agrega los componentes al panel de contenido. jmarco.add(jcvTraducir); jmarco.add(jetqTraducirA); jmarco.add(jboFrances); jmarco.add(jboAleman); jmarco.add(jboChino); jmarco.add(jetqCambiar); jmarco.add(jetqQue); // Despliega el marco. jmarco.setVisible(true); } // Esto maneja todos los botones de opción de idioma. public void actionPerformed(ActionEvent ie) { // Sólo se seleccionará un botón a la vez. if(jboFrances.isSelected( )) jetqQue.setText("Traducir al francés "); else if(jboAleman.isSelected( )) jetqQue.setText("Traducir al alemán"); else jetqQue.setText("Traducir al chino"); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoBO( ); } }); } } www.fullengineeringbook.net Swing 409 410 Java: Soluciones de programación Aquí se muestra la salida: Opciones Los botones de opción soportan varias opciones. Aquí se mencionan unas cuantas. Por ejemplo, puede sustituir un icono que reemplazará el círculo pequeño que indica la selección. También puede especificar el icono y la cadena. He aquí los constructores de JRadioButton que soportan estas opciones: JRadioButton(Icon icono) JRadioButton(String cad, Icon icono) El icono predeterminado usado por el botón de opción está especificado por icono. Por lo general, este icono sirve como el icono de no seleccionado. Es decir, es el icono que se muestra cuando el botón no está seleccionado. Por lo general, cuando se especifica este icono, también necesitará especificar el icono de seleccionado. Para especificar el icono de seleccionado, llame a setSelectedIcon( ), que se muestra aquí: void setSelectedIcon(Icon icono) El icono que se pasa mediante icono especifica la imagen que se desplegará cuando el botón de opción está seleccionado. Debido a que los iconos de seleccionado y de no seleccionado funcionan como un par, casi siempre necesita especificar ambos cuando está usando un icono. JRadioButton también proporciona constructores que le permiten especificar un icono y un estado inicial, como se muestra aquí: JRadioButton(Icon icono, boolean estado) JRadioButton(String cad, Icon icono, boolean estado) Si estado es verdadero, el botón de opción está seleccionado inicialmente. De lo contrario, no lo está. Puede establecer la alineación del botón de opción y del texto, en relación con el icono. Los métodos que manejan esto están definidos por AbstractButton y son setVerticalAlignment( ), setHorizontalAlignment( ), setVerticalTextPosition( ) y setHorizontalTextPosition( ). Puede establecer una tecla mnemotécnica que se muestra en la etiqueta del botón de opción al llamar a setMnemonic( ). www.fullengineeringbook.net Capítulo 8: Swing 411 Ingrese texto con JTextField Componentes clave Clases e interfaces Métodos java.awt.event.ActionListener void actionPerformed(ActionEvent ae) java.awt.event.ActionEvent String getActionCommand( ) javax.swing.event.CaretListener void caretUpdate(CaretEvent ce) javax.swing.event.CaretEvent javax.swing.JTextField void addActionListener(ActionListener al) void addCaretListener(CaretListener cl) void cut( ) void copy( ) String getSelectedText( ) String getText( ) void paste( ) void setActionCommand(String nuevoCmd) void setText(String texto) Swing proporciona soporte amplio al ingreso de texto, al proporcionar varios componentes para este fin. En esta solución se revisa el que es quizás el componente de texto de uso más común: JTextField. Ofrece un servicio simple, pero muy útil: le permite al usuario ingresar una sola línea de texto. Por ejemplo, podría usar JTextField para obtener un nombre de usuario y una dirección de correo electrónico, un nombre de archivo o un número telefónico. A pesar de su simplicidad, hay muchas situaciones de ingreso de texto en que JTextField es exactamente el componente correcto. Es una solución fácil de usar, pero efectiva, para una amplia variedad de tareas de ingreso de texto. Aunque JTextField es sólo uno de los varios componentes de texto, casi todas las técnicas que se aplican a él también se aplican a los demás. Por tanto, gran parte de lo que se presenta en esta solución puede adaptarse para su uso con JTextArea, JFormattedTextField o JPasswordField, por ejemplo. Es necesario establecer que los componentes de texto de Swing constituyen un tema muy amplio. Las técnicas mostradas en esta solución representan un uso típico de un componente. Son posibles aplicaciones más complejas. Paso a paso Para usar JTextField para ingresar una línea de texto se requieren estos pasos: 1. Cree una instancia de JTextField. Asegúrese de que el componente es lo suficientemente amplio como para manejar una entrada típica. 2. Si lo desea, maneje sucesos de acción al registrar un ActionListener para el campo de texto. Los sucesos de acción se generan cuando el usuario oprime enter una vez que el campo de texto tiene el enfoque de entrada. www.fullengineeringbook.net 412 Java: Soluciones de programación 3. Si lo desea, maneje los sucesos de cursor al registrar un CaretListener para el campo de texto. Estos sucesos se generan cada vez que el cursor cambia de posición. 4. Para obtener el texto desplegado actualmente en el campo de texto, llame a getText( ). 5. Puede establecer el texto al llamar a setText( ). Puede usar este método para restablecer el texto, por ejemplo, si el usuario comete un error. 6. Puede obtener texto seleccionado al llamar a getSelectedText( ). 7. Puede cortar texto seleccionado al llamar a cut( ). Este método elimina el texto y también coloca el texto cortado en el portapapeles. Puede copiar texto seleccionado, pero no eliminarlo, al llamar a copy( ). 8. Puede copiar cualquier texto que esté en el portapapeles en el campo de texto en la ubicación del cursor actual al llamar a paste( ). Análisis JTextField hereda la clase abstracta javax.swing.text.JTextComponent, que es la superclase de todos los componentes de texto. JTextComponent define la funcionalidad común a todos los componentes de texto, incluido JTextField. Por ejemplo, los métodos cut( ), copy( ) y paste( ) están definidos por JTextComponent. El modelo para JTextField (y todos los demás componentes de texto) es javax.swing.text.Document. JTextField define varios constructores. Los dos usados aquí son: JTextField(int cols) JTextField(int cad, int cols) Aquí, cols especifica el ancho del campo de texto en columnas. Es importante comprender que puede ingresar una cadena que es más larga que el número de columnas. Sólo significa que el tamaño físico del campo de texto en la pantalla será de cols columnas de ancho. El segundo constructor le permite inicializar el campo de texto con la cadena pasada en cad. Al oprimir enter cuando un campo de texto tiene el enfoque se genera un ActionEvent. Si quiere manejar este suceso, debe registrar un ActionListener para el campo de texto. La interfaz ActionListener define sólo un método actionperformed( ). Este método es llamado cada vez que el campo de texto genera un suceso de acción. Para conocer detalles sobre el manejo de los sucesos de acción, consulte Cree un botón simple. Un JTextField tiene una cadena de comandos de acción asociada. Como opción predeterminada, el comando de acción es el contenido actual del campo de texto. Por tanto, la cadena de comandos de acción cambia cada vez que lo hace el contenido del campo de texto. Aunque esto puede ser útil en algunas situaciones, imposibilita el uso de esta cadena como medio para determinar el origen de un suceso de acción. Si quiere usar realmente la cadena de comandos de acción para identificar un campo de texto, entonces debe asignar al comando de acción un valor fijo de su propia elección al llamar al método setActionCommand( ), que se muestra aquí. void setActionCommand(String nuevoCmd) La cadena pasada en nuevoCmd se vuelve el nuevo comando de acción. El texto en el campo de texto queda sin afectación. Una vez que establece la cadena de comandos de acción, permanece igual, sin importar lo que haya ingresado en el campo de texto. www.fullengineeringbook.net Capítulo 8: Swing 413 Cada vez que el cursor de un campo de texto cambia de ubicación, como cuando escribe un carácter, se genera un CaretEvent. Puede escuchar estos sucesos al implementar un CaretListener. Al manejar los sucesos de cursor, su programa puede responder a cambios en el campo de texto a medida que ocurren, sin esperar a que el usuario oprima enter. CaretListener está empaquetado en javax.swing.event. Define un solo método, llamado caretUpdate( ), que se muestra aquí: void caretUpdate(CaretEvent ce) Recuerde que un suceso de cursor se genera cada vez que el cursor cambia de posición. Esto incluye los cambios causados por selección, corte o pegado de texto o reubicación del cursor dentro del texto. Por tanto, el manejo de los sucesos de cursor le permite vigilar los cambios en el texto en tiempo real. Para obtener la cadena que se está desplegando en el campo de texto, llame a getText( ) en la instancia de JTextField. Se declara como se muestra aquí: String getText( ) Puede establecer el texto en un JTextField al llamar a setText( ), como se muestra a continuación. void setText(String texto) Aquí, texto es la cadena que se colocará en el campo de texto. El usuario puede seleccionar un subconjunto de los caracteres dentro de un campo de texto, o se puede hacer bajo control del programa. Puede obtener la parte del texto que se ha seleccionado al llamar a getSelectedText( ), que se muestra aquí: String getSelectedText( ) Si no se ha seleccionado texto, entonces se devuelve null. Aunque los procedimientos precisos pueden diferir en distintos entornos, JTextField soporta automáticamente los comandos de edición estándar de corte, copia y pegado que le permiten mover texto entre un campo de texto y el portapapeles. (Por ejemplo, en Windows puede usar ctrl+x para cortar, ctrl+v para pegar y ctrl+c para copiar). También puede realizar estas acciones bajo control del programa al usar los métodos cut( ), copy( ) y paste( ) que se muestran aquí: void cut( ) void copy( ) void paste( ) El método cut( ) elimina cualquier texto seleccionado dentro del campo de texto y lo copia en el portapapeles. El método copy( ) copia, pero no elimina, el texto seleccionado. El método paste( ) copia cualquier texto que esté en el portapapeles y lo pega en el campo de texto. Si este campo tiene texto seleccionado, entonces éste es reemplazado con el que se encuentra en el portapapeles; de otra manera, el texto del portapapeles se inserta inmediatamente antes de la posición actual del cursor. Ejemplo Con el siguiente programa se demuestra JTextField. Maneja sucesos de acción y de cursor generados por el campo de texto. Recuerde que un suceso de acción se genera cada vez que el usuario oprime enter cuando el campo de texto tiene el enfoque del teclado. Cuando esto ocurre, se obtiene el contenido actual del campo de texto y se despliega en una etiqueta. Cada vez que se genera un suceso de cursor, también se obtiene el contenido actual del campo de texto y se despliega en una segunda etiqueta. Debido a que un suceso de cursor se genera cada vez que se mueve éste (lo que www.fullengineeringbook.net 414 Java: Soluciones de programación ocurrirá cuando se escriben los caracteres), la segunda etiqueta siempre contendrá el contenido actual del campo de texto. Por último, hay un botón llamado Obtener texto en mayúsculas. Cuando se oprime, genera un suceso de acción. El manejador de sucesos de acción responde al botón al obtener el texto del campo y desplegarlo en mayúsculas. // // // // // // // // // Demuestra JTextField. Para fines de demostración, DemoCT implementa ActionListener. Este manejador se usa para el botón y para el campo de texto. Observe que las cadenas de comandos de acción para el campo de texto y el botón están establecidas explícitamente para que cada una pueda ser reconocida por el manejador de sucesos de acción. En una aplicación real, suele ser más fácil usar clases anónimas internas para manejar sucesos de acción. import import import import java.awt.*; java.awt.event.*; javax.swing.*; javax.swing.event.*; // Observe que DemoCT implementa ActionListener. class DemoCT implements ActionListener { JTextField jct; JButton jbtnObtenerTextoMayus; JLabel jetqIndicador; JLabel jetqContenido; JLabel jetqTiempoReal; DemoCT( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame(«Demuestra un campo de texto»); // Especifica FlowLayout para el administrador de diseño. jmarco.setLayout(new FlowLayout( )); // Da el marco un tamaño inicial. jmarco.setSize(240, 140); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea un campo de texto. jct = new JTextField(10); // Establece el comando de acción para el campo de texto. // Esto permite que el campo de texto sea identificado por su // cadena de comandos de acción cuando ocurre un suceso de // acción. jct.setActionCommand("CT"); // Crea el botón. JButton jbtnObtenerTextoMayus = new JButton("Obtener texto en mayúsculas"); www.fullengineeringbook.net Capítulo 8: Swing // Agrega esta instancia como escucha de acción. jct.addActionListener(this); jbtnObtenerTextoMayus.addActionListener(this); // Agrega un escucha de cursor. jct.addCaretListener(new CaretListener( ) { public void caretUpdate(CaretEvent ce) { jetqTiempoReal.setText("Texto en tiempo real: " + jct.getText( )); } }); // Crea las etiquetas. jetqIndicador = new JLabel("Ingrese texto: "); jetqContenido = new JLabel("Esperando al suceso de acción."); jetqTiempoReal = new JLabel("Texto en tiempo real: "); // Agrega los componentes al panel de contenido. jmarco.add(jetqIndicador); jmarco.add(jct); jmarco.add(jbtnObtenerTextoMayus); jmarco.add(jetqTiempoReal); jmarco.add(jetqContenido); // Despliega el marco. jmarco.setVisible(true); } // Maneja los sucesos de acción para el campo de // texto y el botón. public void actionPerformed(ActionEvent ae) { if(ae.getActionCommand( ).equals("CT")) { // Enter se oprimió mientras se tenía el enfoque // en el campo de texto. jetqContenido.setText("Tecla ENTER oprimida: " + jct.getText( )); } else { // Se oprimió el botón Obtener texto en mayúsculas. String cad = jct.getText( ).toUpperCase( ); jetqContenido.setText("Botón oprimido: " + cad); } } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoCT( ); } }); } } www.fullengineeringbook.net 415 416 Java: Soluciones de programación Aquí se muestra una salida de ejemplo: Aunque la mayor parte del programa es muy sencillo, unas cuantas partes merecen una atención especial. En primer lugar, observe que el comando de acción relacionado con jct (el campo de texto) se establece en "CT" en la siguiente línea: jct.setActionCommand("CT"); Después de la ejecución de esta línea, la cadena de comandos de acción para jct será "CT" sin importar el texto que contenga. Esto permite que la cadena de comandos de acción se use para identificar el campo de texto y evitar posibles conflictos con otros componentes que generan comandos de acción (como el botón, en este caso). Como ya se explicó, si la cadena de comandos de acción no se establece, entonces la cadena sería el contenido actual del campo de texto en el momento en que se generó el suceso, lo que llevaría a conflictos con la cadena de comandos de acción asociada con otro componente. Para esta demostración, la clase DemoCT implementa ActionListener y este manejador se usa para el botón y el campo de texto. Debido a que la cadena de comandos de acción de jct se establece explícitamente, puede usarse para identificar el campo de texto. Si éste no fuera el caso, entonces debería usarse otro método para determinar el origen del suceso, como getSource( ). Francamente, en una aplicación real, por lo general sería más fácil usar clases internas anónimas que aplicar un solo manejador para manejar los sucesos de acción de cada componente por separado. Como ya se estableció, en este ejemplo se usa un solo manejador de sucesos de acción para fines de demostración. Ejemplo adicional: cortar, copiar y pegar JTextField soporta las acciones estándar de portapapeles de cortar, pegar y copiar. Éstas pueden utilizarse mediante comandos de edición de teclado estándar, como ctrl+x, ctrl+v y ctrl+c en el entorno de Windows. También pueden aplicarse bajo el control del programa, como se ilustra en este ejemplo. El programa despliega un campo de texto, tres botones y dos etiquetas. A los botones se les denomina Cortar, Pegar y Copiar, y realizan la función indicada por sus nombres. Por ejemplo, si se ha seleccionado un texto en el campo de texto, al oprimir Cortar se hace que el texto seleccionado se elimine y se ponga en el portapapeles. Al oprimir Pegar se hace que cualquier texto que se encuentre en el portapapeles se copie en el campo de texto, en la ubicación actual del cursor. Al oprimir Copiar se copia el texto seleccionado en el portapapeles pero no se elimina. Las dos etiquetas despliegan el contenido actual del campo de texto y el texto seleccionado, si lo hay. // Corta, pega y copia en un JTextField bajo control del programa. import import import import java.awt.*; java.awt.event.*; javax.swing.*; javax.swing.event.*; www.fullengineeringbook.net Capítulo 8: class CortarCopiarPegar { JLabel jetqTodo; JLabel jetqSeleccionado; JTextField jct; JButton jbtnCortar; JButton jbtnPegar; JButton jbtnCopiar; public CortarCopiarPegar( ) { // Crea un contenedor de JFrame. JFrame jmarco = new JFrame("Cortar, copiar y pegar"); // Especifica FlowLayout como el administrador de diseño. jmarco.setLayout(new FlowLayout( )); // Da el marco un tamaño inicial. jmarco.setSize(250, 150); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea las etiquetas. jetqTodo = new JLabel("Todo el texto: "); jetqSeleccionado = new JLabel("Texto seleccionado: "); jetqTodo.setPreferredSize(new Dimension(200, 20)); jetqSeleccionado.setPreferredSize(new Dimension(200, 20)); // Crea el campo de texto. jct = new JTextField(15); // Crea los botones Cortar, Pegar y Copiar. jbtnCortar = new JButton("Cortar"); jbtnPegar = new JButton("Pegar"); jbtnCopiar = new JButton("Copiar"); // Agrega un escucha de acción para el botón Cortar. jbtnCortar.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent le) { // Corta cualquier texto seleccionado y lo // coloca en el portapapeles. jct.cut( ); update( ); } }); // Agrega un escucha de acción para el botón Pegar. jbtnPegar.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent le) { // Pega el texto del portapapeles en el // campo de texto. www.fullengineeringbook.net Swing 417 418 Java: Soluciones de programación jct.paste( ); update( ); } }); // Agrega un escucha de acción para el botón Copiar. jbtnCopiar.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent le) { // Pega el texto del portapapeles en el // campo de texto. jct.copy( ); update( ); } }); // Agrega un escucha de cursor. Esto deja que la aplicación // responda en tiempo real a cambios en el campo de texto. jct.addCaretListener(new CaretListener( ) { public void caretUpdate(CaretEvent ce) { update( ); } }); // Agrega los componentes al panel de contenido. jmarco.add(jct); jmarco.add(jbtnCortar); jmarco.add(jbtnPegar); jmarco.add(jbtnCopiar); jmarco.add(jetqTodo); jmarco.add(jetqSeleccionado); // Despliega el marco. jmarco.setVisible(true); } // Muestra los textos completo y seleccionado en jct. private void update( ) { jetqTodo.setText("Todo el texto: " + jct.getText( )); if(jct.getSelectedText( ) != null) jetqSeleccionado.setText("Texto seleccionado: " + jct.getSelectedText( )); else jetqSeleccionado.setText("Texto seleccionado: "); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new CortarCopiarPegar( ); } }); } } www.fullengineeringbook.net Capítulo 8: Swing 419 Aquí se muestra la salida de ejemplo: Opciones Como ya se explicó, cada vez que el cursor cambia de posición, se genera un suceso de cursor, lo que causa que se llame al método caretUpdate( ) especificado por CaretListener. A este método se le pasa un objeto de CaretEvent, que encapsula el suceso. CaretEvent define dos métodos que pueden ser útiles en algunas situaciones de ingreso de texto. Se muestran a continuación: int getDot( ) int getMark( ) El método getDot( ) devuelve la ubicación actual del cursor. A esto se le llama punto (o dot). El método getMark( ) devuelve el punto de inicio de la selección. A esto se le llama la marca (mark). Por tanto, una selección está incluida entre la marca y el punto. Si no se ha hecho ninguna selección, entonces ambos tendrán el mismo valor. Puede colocar el cursor bajo control del programa al llamar a setCaretPosition( ) en el campo de texto. Aquí se muestra: void setCaretPosition(int nuevaUbi) Puede seleccionar una parte del texto bajo control del programa al llamar a moveCaretPosition( ) en el campo de texto. Aquí se muestra: void moveCaretPosition(int nuevaUbi) El texto entre la ubicación original del cursor y la nueva posición estará seleccionado. Hay una subclase muy útil de JTextField llamada JFormattedTextField. Le permite imponer un formato específico para el texto que se está ingresando. Por ejemplo, puede crear formatos para fecha, hora y valores numéricos. También puede crear formatos personalizados. Si quiere proporcionar un campo de texto en que el usuario ingrese una contraseña, entonces debe usar JPasswordField. Está diseñado expresamente para obtener contraseñas porque los caracteres ingresados por el usuario no reciben eco. En cambio, se despliega un carácter que actúa como marcador de posición. Además, las funciones de edición estándar de copiar y pegar están deshabilitadas en JPasswordField. JTextField sólo permite el ingreso de una línea de texto. Para permitir el ingreso de varias líneas, debe emplear un componente de texto diferente. El componente de texto de varias líneas más fácil de usar es JTextArea. Funciona de manera muy parecida a JTextField pero permite varias líneas de texto. Hay otros dos componentes que le resultarán interesantes a algunos lectores: JEditorPane y JTextPane. Son controles sustancialmente más complejos porque permiten la edición de documentos con estilo, como los que usan HTML o RTF. También pueden contener imágenes y otros componentes. www.fullengineeringbook.net 420 Java: Soluciones de programación Trabaje con JList Componentes clave Clases e interfaces Métodos javax.swing.event. ListSelectionEvent javax.swing.event. ListSelectionListener void valueChanged(ListSelectionEvent le) javax.swing.JList void addListSelectionListener(ListSelectionListener lsl) int getSelectedIndex( ) int[ ] getSelectedIndices( ) void setselectionMode(int modo) JList es una clase de listas básica de Swing. Permite la selección de uno o más elementos de una lista. Aunque la lista suele constar de cadenas, es posible crear una lista con casi cualquier objeto que pueda desplegarse. JList se usa de manera tan amplia en las GUI de Swing que es muy poco probable que no las haya visto antes. En esta solución se muestra cómo crear y administrar una. Paso a paso El uso de una JList requiere estos pasos: 1. Cree una instancia de JList. 2. Si es necesario, establezca el modo de selección al llamar a setSelectionMode( ). 3. Maneje los sucesos de selección de lista al registrar un ListSelectionListener para la lista. Los sucesos de selección de listas se generan cada vez que el usuario hace una selección, o que la cambia. 4. En el caso de listas de selección única, obtenga el índice de una selección al llamar a getSelectedIndex( ). Para listas de selección múltiple, obtenga una matriz que contenga el índice de todos los elementos seleccionados al llamar a getSelectedIndices( ). Análisis JList proporciona varios constructores. El usado aquí es: JList(Object[ ] elementos) Esto crea una JList que contiene los elementos de la matriz especificada por elementos. En esta solución, los elementos son instancias de String pero pueden especificarse otros objetos. JList usa javax.swing.ListModel como modelo de datos. javax.swing.ListSelectionModel es el modelo que rige las selecciones de listas. Aunque una JList funcionará de manera apropiada por sí sola, casi todo el tiempo envolverá una JList dentro de un JScrollPane, que es un contenedor que proporciona automáticamente desplazamiento para su contenido. www.fullengineeringbook.net Capítulo 8: Swing 421 (Consulte Use JScrollPane para manejar el desplazamiento para conocer más detalles). He aquí el constructor de JScrollPane usado en esta solución: JScrollPane(Component comp) Aquí, comp especifica el componente que habrá de desplazarse, que en este caso será JList. Al envolver una JList en un JScrollPane, las listas largas se desplazarán automáticamente. Esto simplifica el diseño de las GUI. También facilita el cambio del número de entradas en una lista sin tener que cambiar el tamaño del componente de JList. Una JList genera un ListSelectionEvent cuando el usuario hace o cambia una selección. Este suceso también se genera cuando el usuario deja de seleccionar un elemento. Se maneja al implementar ListSelectionListener, que está empaquetado en javax.swing.event. Este escucha especifica sólo un método, llamado valueChanged( ), que se muestra aquí: void valueChanged(ListSelectionEvent le) Aquí, le es una referencia al objeto que generó el suceso. ListSelectionEvent también está empaquetado en javax.swing.event. Aunque ListSelectionEvent proporciona algunos métodos propios, por lo general obtendrá información acerca de una selección de lista al usar métodos definidos por JList. Como opción predeterminada, una JList permite al usuario seleccionar varios rangos de elementos dentro de la lista, pero puede cambiar este comportamiento al llamar a setSelectionMode( ), que está definido por JList. Aquí se muestra: void setSelectionMode(int modo) Aquí, modo especifica el modo de selección. Debe ser uno de los valores definidos por su modelo, que está definido por javax.swing.ListSelectionModel. Aquí se muestran: SINGLE_SELECTION SINGLE_INTERVAL_SELECTION MULTIPLE_INTERVAL_SELECTION La selección predeterminada, de intervalos múltiples permite al usuario seleccionar varios rangos de elementos dentro de una lista. Con la selección de un solo intervalo, el usuario puede seleccionar un rango de elementos. Con una sola selección, el usuario puede seleccionar sólo un elemento. Por supuesto, también puede seleccionar un solo elemento en los otros dos modos. Sólo que además le permiten seleccionar un rango. Puede obtener el índice del primer elemento seleccionado, que también será el índice del único elemento seleccionado cuando se usa el modo de una sola selección, al llamar a getSelectedIndex( ), que se muestra aquí: int getSelectedIndex( ) El indizamiento empieza en cero. De modo que si está seleccionado el primer elemento, este método devolverá 0. Si no hay elemento alguno seleccionado, se devolverá –1. Puede obtener una matriz que contiene todos los elementos seleccionados al llamar a getSelectedIndices( ), que se muestra en seguida: int[ ] getSelectedIndices( ) En la matriz devuelta, los índices están ordenados de menor a mayor. Si se devuelve una matriz de longitud cero, significa que no se ha seleccionado ningún elemento. www.fullengineeringbook.net 422 Java: Soluciones de programación Ejemplo Con el siguiente programa se demuestra una JList de una sola selección. Presenta una lista de idiomas de cómputo, en la que el usuario puede seleccionar uno. Cada vez que se hace o cambia una selección, se genera un ListSelectionEvent, que es manejado por el método valueChanged( ) definido por ListSelectionListener. Responde al obtener el índice del elemento seleccionado y desplegando la selección. // Demuestra una JList de una sola selección. import import import import javax.swing.*; javax.swing.event.*; java.awt.*; java.awt.event.*; class DemoLista { JList jlst; JLabel jetq; JScrollPane jpandez; // Crea una matriz de lenguajes de cómputo. String lenguajes[ ] = { "Java", "Perl", "Python", "C++", "Basic", "C#" }; DemoLista( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Uso de JList"); // Establece el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); // Da el marco un tamaño inicial. jmarco.setSize(200, 160); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una JList. jlst = new JList(lenguajes); // Establece el modo de selección de lista en selección única. jlst.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Agrega la lista a un panel de desplazamiento. jpandez = new JScrollPane(jlst); // Establece el tamaño preferido del panel de desplazamiento. jpandez.setPreferredSize(new Dimension(100, 74)); // Crea una etiqueta que despliega la selección. jetq = new JLabel("Elija un lenguaje"); www.fullengineeringbook.net Capítulo 8: Swing 423 // Agrega un manejador de selección de lista. jlst.addListSelectionListener(new ListSelectionListener( ) { public void valueChanged(ListSelectionEvent le) { // Obtiene el índice del elemento cambiado. int ind = jlst.getSelectedIndex( ); // Despliega una selección, si se seleccionó un elemento. if(ind != –1) jetq.setText("Selección actual: " + lenguajes[ind]); else // De otra manera, vuelve a preguntar. jetq.setText("Por favor, elija un lenguaje."); } }); // Agrega la lista y la etiqueta al panel de contenido. jmarco.add(jpandez); jmarco.add(jetq); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoLista( ); } }); } } Aquí se muestra la salida: Echemos un vistazo de cerca a unos cuantos aspectos de este programa. En primer lugar, observe la matriz lenguajes casi cerca de la parte superior del programa. Está inicializada en una lista de cadenas que contiene los nombres de varios lenguajes de cómputo. Dentro de DemoLista( ), se construye una JList llamada jlst usando la matriz lenguajes. Esto hace que la lista se inicialice en las cadenas contenidas en la matriz. Un segundo punto de interés es la llamada a setSelectionMode( ). Como se explicó JList permite, como opción predeterminada, varias selecciones. Debe establecerse explícitamente para permitir sólo selecciones únicas. A continuación, jlst se envuelve dentro de un JScrollPane y se establece el tamaño preferido del panel de desplazamiento en 100 por 74. El método setPreferredSize( ) establece el tamaño deseado de un componente. Sin embargo, esté consciente de que algunos administradores de diseño ignorarán este requisito. Debido a que la lista sólo contiene unas cuantas entradas, sería posible en este caso www.fullengineeringbook.net 424 Java: Soluciones de programación evitar el uso de un panel de desplazamiento. Sin embargo, el uso de éste permite que la lista se presente en forma compacta. También permite que se agreguen entradas a la lista sin afectar el diseño de la GUI. Opciones Otro constructor de JList de uso común le permite especificar los elementos de la lista en la forma de un Vector. Aquí se muestra: JList(Vector<?> elementos) Los elementos contenidos en elementos se mostrarán en la lista. Como ya se explicó, una JList permite, como opción predeterminada, que varios elementos se seleccionen de la lista. Cuando se usa una selección múltiple, necesitará usar getSelectedIndices( ) para obtener una matriz de los elementos seleccionados. Por ejemplo, he aquí el manejador valueChanged del ejemplo, reescrito para soportar selección múltiple: // Manejador para selecciones múltiples. jlst.addListSelectionListener(new ListSelectionListener( ) { public void valueChanged(ListSelectionEvent le) { String lengs = "Selecciones actuales: "; // Obtiene los índices de los elementos seleccionados. int indices[ ] = jlst.getSelectedIndices( ); // Despliega la selección, si se seleccionaron uno o más elementos. if(indices.length != 0) { for(int i = 0; i < indices.length; i++) lengs += lenguajes[indices[i]] + " "; jetq.setText(lengs); } else // De otra manera, vuelve a preguntar. jetq.setText("Por favor, elija un lenguaje."); } }); Para probar este manejador, también tiene que convertir en comentario la llamada a setSelectionMode( ) en el ejemplo. Puede seleccionar un elemento bajo control del programa al llamar a setSelectedIndex( ), que se muestra aquí: void setSelectedIndex(int ind) Aquí, ind es el índice del elemento que habrá de seleccionar. Una razón por la que querrá usar setSelectedIndex( ) es preseleccionar un elemento cuando se despliega la lista por primera vez. Por ejemplo, haga la prueba de agregar esta línea al programa anterior después de que se ha agregado jlst al panel de contenido: jlst.setSelectedIndex(0); Después de hacer esta adición, el primer elemento de la lista, que es Java, estará seleccionado cuando se inicie el programa. www.fullengineeringbook.net Capítulo 8: Swing 425 Si la lista soporta selección múltiple, entonces puede seleccionar más de un elemento al llamar a setSelectedIndices( ), que se muestra aquí: void setSelectedIndices(int[ ] indices) La matriz pasada mediante indices contiene los índices de los elementos que habrán de seleccionarse. En una lista de selección múltiple, puede seleccionar un rango de elementos al llamar a setSelectionInterval( ), que se muestra aquí: void setSelectionInterval(int indInicio, int indDetener) El rango seleccionado es incluyente. Por tanto, se habrán de seleccionar indInicio e indDetener y todos los elementos intermedios. Puede seleccionar un elemento por valor en lugar de hacerlo por índice, si llama a setSelectedValue( ): void setSelectedValue(Object elem, boolean DezpAElem) El elemento que se seleccionará se pasa vía elem. Si DezpAElem es verdadero y el elemento seleccionado no es visible, se desplaza hacia la vista. Por ejemplo, la instrucción jlst.setSelectedValue("C#", true) selecciona C# y lo desplaza hacia la vista. Puede dejar de seleccionar todas las selecciones al llamar a clearSelection( ), que se muestra aquí: void clearSelection( ) Después de que se ejecuta este método, se limpian todas las selecciones. Puede determinar si una selección está disponible al llamar a isSelectionEmpty( ), que se muestra aquí: boolean isSelectionEmpty( ) Devuelve verdadero si no se han hecho selecciones; de lo contrario, devuelve falso. Cuando se usa una lista que soporta selección múltiple, en ocasiones querrá saber cuál elemento se seleccionó al principio y cuál al final. En el lenguaje de JList, al primer elemento se le denomina ancla. Al último elemento se le denomina guía. Puede obtener estos índices al llamar a getAnchorSelectionIndex( ) y getLeadSelectionIndex( ), que se muestran aquí: void getAnchorSelectionIndex( ) void getLeadSelectionIndex( ) Ambos devuelven –1, si no se ha hecho ninguna selección. Puede establecer los elementos de una JList al llamar a setListData( ). Tiene las dos formas siguientes: void setListData(Object[ ] elems) void setListData(Vector<?> elems) Aquí, elems es una matriz o un vector que contiene los elementos que quiere que se desplieguen en la lista que invoca. Una JList puede generar varios sucesos de selección de listas cuando el usuario está en el proceso de seleccionar o dejar de seleccionar uno o más elementos. A menudo no querrá www.fullengineeringbook.net 426 Java: Soluciones de programación responder a selecciones hasta que el usuario haya completado el proceso. Puede usar el método getValueIsAdjusting( ) para determinar cuando ha terminado el proceso de selección. Aquí se muestra: boolean getValueIsAdjusting( ) Devuelve verdadero si el proceso de selección es continuo y falso cuando el usuario detiene el proceso de selección. Al usar este método, puede esperar hasta que el usuario haya terminado antes de procesar las selecciones. Además de las opciones recién descritas. JList da soporte a otras varias. Hay otras dos opciones más avanzadas que podrían ser de interés. En primer lugar, en lugar de crear una lista al especificar una matriz o vector de los elementos cuando se construye la JList, puede crear primero una implementación de ListModel. A continuación, llene el modelo y luego úselo para construir una Jlist. Esto tiene dos ventajas: puede crear modelos de lista personalizada, si lo desea, y puede modificar el contenido de la lista en tiempo de ejecución al agregar o eliminar elementos del modelo. La segunda opción avanzada consiste en crear un generador personalizado de celdas para la lista. Un generador personalizado de celdas determina cómo se dibuja cada entrada de la lista. Debe ser un objeto de una clase que implemente la interfaz ListCellRenderer. Para los casos en que quiera combinar un campo de edición con una lista, use JComboBox. Una opción muy útil a JList en algunos casos es JSpinner. Crea un componente que incorpora una lista con un conjunto de flechas que desplazan la lista. La ventaja de JSpinner es que crea un componente muy compacto que proporciona una manera conveniente para que el usuario seleccione entre una lista de valores. Use una barra de desplazamiento Componentes clave Clases e Interfaces Métodos java.awt.event.AdjustmentEvent Adjustable getAdjustable( ) int getValue( ) boolean getValueAdjusting( ) java.awt.event.AdjustmentListener void adjustmentValueChanged (AdjustmentEvent ae javax.swing.JScrollBar void addAdjustmentListener (AdjustmentListener al) int getValue( ) boolean getValueIsAdjusting( ) Una barra de desplazamiento es una instancia de JScrollBar. A pesar de sus muchas opciones, es sorprendentemente fácil programar las barras de desplazamiento. Más aún, con frecuencia éstas proporcionan precisamente la funcionalidad correcta. Hay dos variedades básicas de barras de desplazamiento: verticales y horizontales. www.fullengineeringbook.net Capítulo 8: Swing 427 Aunque sus orientaciones difieren, ambas se manejan de la misma manera. En esta solución se muestran las técnicas básicas necesarias para crear y usar una barra de desplazamiento. Paso a paso Para usar una barra de desplazamiento se requieren estos pasos: 1. Cree una instancia de JScrollBar. 2. Agregue un AdjustmentListener para manejar los sucesos generados cuando se mueve el deslizador de la barra de desplazamiento. 3. Maneje los sucesos de ajuste al implementar adjustmentValueChanged( ). 4. Si es apropiado para su aplicación, use getValueIdAdjusting( ) para esperar hasta que el usuario haya dejado de mover la barra de desplazamiento. 5. Obtenga el valor de la barra de desplazamiento al llamar a getValue( ). Análisis Resulta útil empezar a revisar aspectos clave de las barras de desplazamiento. Están en realidad compuestas por varias partes individuales. En cada extremo hay flechas en que puede hacer clic para cambiar el valor actual de la barra de desplazamiento una unidad en dirección de la flecha. El valor actual, en relación con sus valores mínimo y máximo, está indicado por la posición del pulgar. El usuario puede arrastrar el pulgar (o el cuadro de la barra) a una nueva posición. Esta nueva posición se convierte entonces en el valor actual de la barra de desplazamiento. El modelo de ésta reflejará entonces este valor. Al hacer clic en la barra (a la que también se le denomina área de paginación) se hace que el pulgar salte en esa dirección en un incremento que suele ser mayor de 1. Por lo general, esta acción se traduce en alguna forma de desplazamiento de la página hacia arriba o hacia abajo. En Swing, una barra de desplazamiento es uno de tres componentes relacionados: JScrollBar, JSlider y JProgressBar. Todos estos componentes comparten un tema central: un indicador visual que se mueve por un rango predefinido. Debido a esto, estos componentes estarán basados en el mismo modelo, que está encapsulado por la interfaz BoundedRangeModel. Aunque no necesitamos usar el modelo directamente, define la operación básica de los tres componentes. Un aspecto clave de BoundedRangeModel es que define cuatro valores importantes: • Mínimo • Máximo • Actual • Extendido Los valores mínimo y máximo definen los extremos del rango sobre el que puede operar un componente basado en el BoundedRangeModel. El valor actual del componente estará dentro de ese rango. En general, la extensión representa el "ancho" conceptual de un elemento deslizable que se mueve entre los extremos del componente. Por ejemplo, en la barra de desplazamiento, la extensión corresponde al ancho (o "grosor") del cuadro de la barra de desplazamiento. BoundedRangeModel impone una relación entre estos cuatro valores. En primer lugar, el mínimo debe ser menor o igual al máximo. El valor actual debe ser mayor o igual al mínimo. El www.fullengineeringbook.net 428 Java: Soluciones de programación valor actual más la extensión debe ser menor o igual que el valor máximo. Por tanto, si especifica un valor máximo de 100 y una extensión de 20, entonces el valor actual nunca podrá ser mayor de 80 (que es 100 – 20). Puede especificar una extensión de 0, que permite que el valor actual esté en cualquier lugar dentro del rango especificado por el máximo y el mínimo, incluidos. JScrollBar también implementa la interfaz Adjustable. Esta interfaz está definida por AWT. JScrollBar define tres constructores. El primero crea una barra de desplazamiento predeterminada: JScrollBar( ) Esto crea una barra de desplazamiento vertical que usa los valores predeterminados para el valor inicial, la extensión, el mínimo y el máximo. Son: Valor inicial 0 Extensión 10 Mínimo 0 Máximo 100 El siguiente constructor le permite especificar la orientación de la barra de desplazamiento: JScrollBar(int VoH) La barra de desplazamiento está orientada de acuerdo con lo especificado por VoH. El valor de VoH debe ser JScrollBar.VERTICAL o JScrollBar.HORIZONTAL. Se usan los valores predeterminados. El constructor final le permite especificar la orientación y los valores: JScrollBar(int VoH, int valorInicial, int extens, int min, int max) Esto crea una barra de desplazamiento orientada de acuerdo con lo especificado en VoH, con el valor inicial, la extensión y los valores mínimo y máximo. En el caso de las barras de desplazamiento, la extensión especifica el tamaño del pulgar. Sin embargo, el tamaño físico de éste en la pantalla nunca caerá debajo de cierto punto, porque debe ser lo suficientemente grande para arrastrarse. Es importante comprender que el valor más grande que la barra de desplazamiento puede tener es igual al valor máximo menos la extensión. Por tanto, como opción predeterminada, el valor de una barra de desplazamiento puede ir de 0 a 90 (100 – 10). JScrollBar genera un suceso de ajuste cada vez que cambia el deslizador. Los sucesos de ajuste son objetos de tipo java.awt.AdjustmentEvent. Para procesar un suceso de ajuste, necesitará implementar la interfaz AdjustmentListener. Sólo define un método, adjustmentValueChanged( ), que se muestra aquí: void adjustmentValueChanged(AdjustmentEvent ae) Se llama a este método cada vez que se hace un cambio al valor de la barra de desplazamiento. Puede obtener una referencia a la barra de desplazamiento que generó el suceso al llamar a getAdjustable( ) en el objeto de AdjustmentEvent. Aquí se muestra: Adjustable getAdjustable( ) www.fullengineeringbook.net Capítulo 8: Swing 429 Como se mencionó, JScrollBar implementa la interfaz Adjustable, y muchas de las propiedades soportadas por JScrollBar son definidas por Adjustable. Esto significa que puede trabajar directamente con esas propiedades a través de la referencia devuelta por este método en lugar de tener que usar explícitamente la referencia a la barra de desplazamiento. Cuando el usuario arrastra el pulgar a una nueva ubicación o ejecuta una serie de comandos para desplazarse una página hacia arriba o hacia abajo, se generará un flujo de AdjustmentEvents. Aunque esto podría ser útil en algunos casos, en otros todo lo que importa es el valor final. Puede usar getValueIsAdjusting( ) para ignorar sucesos hasta que el usuario complete la operación. Aquí se muestra: boolean getValueIsAdjusting( ) Devuelve verdadero si el usuario aún está en el proceso de mover el pulgar. Cuando ha terminado el ajuste, devuelve falso. Por tanto, puede usar este método para esperar hasta que el usuario haya dejado de cambiar la barra de desplazamiento antes de que responda a los cambios. Puede obtener el valor actual de la barra de desplazamiento al llamar a getValue( ) en la instancia de JScrollBar. Aquí se muestra: int getValue( ) Ejemplo En el siguiente ejemplo se muestran las barras de desplazamiento vertical y horizontal. En el programa se despliega el valor de ambas barras. El valor de la barra horizontal se actualiza en tiempo real, mientras se mueve el pulgar. El valor de la barra vertical se actualiza sólo a la conclusión de cada ajuste. Esto se logra mediante el uso del método getValueIsAdjusting( ). // Demuestra JScrollBar. import import import import java.awt.*; java.awt.event.*; javax.swing.*; javax.swing.event.*; class DemoBD { JLabel jetqVert; JLabel jetqHoriz; JScrollBar jbdVert; JScrollBar jbdHoriz; DemoBD( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Demuestra JScrollBar"); // Establece el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); www.fullengineeringbook.net 430 Java: Soluciones de programación // Da el marco un tamaño inicial. jmarco.setSize(320, 300); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Despliega los valores actuales de la barra de desplazamiento. jetqVert = new JLabel("Valor de la barra de desplazamiento vertical: 0"); jetqHoriz = new JLabel("Valor de la barra de desplazamiento horizontal: 50"); // Crea una barra de desplazamiento vertical y horizontal predeterminada. jbdVert = new JScrollBar( ); // vertical, como opción predeterminada jbdHoriz = new JScrollBar(Adjustable.HORIZONTAL); // Establece el tamaño preferido de las barras de desplazamiento. jbdVert.setPreferredSize(new Dimension(20, 200)); jbdHoriz.setPreferredSize(new Dimension(200, 20)); // Establece el valor del pulgar de la barra de desplazamiento horizontal. jbdHoriz.setValue(50); // Agrega escuchas de ajuste para las barras de desplazamiento. // La barra de desplazamiento vertical espera hasta que el // usuario deja de cambiar el valor de la barra de desplazamiento // antes de que responda. jbdVert.addAdjustmentListener(new AdjustmentListener( ) { public void adjustmentValueChanged(AdjustmentEvent ae) { // Si la barra de desplazamiento está en el proceso // de cambio, simplemente se regresa. if(jbdVert.getValueIsAdjusting( )) return; // Depliega el nuevo valor. jetqVert.setText("Valor de la barra de desplazamiento vertical: " + ae.getValue( )); } }); // El manejador de la barra de desplazamiento horizontal // responde a todos los sucesos de ajuste, incluidos los // generados mientras la barra está cambiando. jbdHoriz.addAdjustmentListener(new AdjustmentListener( ) { public void adjustmentValueChanged(AdjustmentEvent ae) { // Despliega el nuevo valor. jetqHoriz.setText("Valor de la barra de desplazamiento horizontal: " + ae.getValue( )); } }); www.fullengineeringbook.net Capítulo 8: Swing 431 // Agrega componentes al panel de contenido. jmarco.add(jbdVert); jmarco.add(jbdHoriz); jmarco.add(jetqVert); jmarco.add(jetqHoriz); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoBD( ); } }); } } Aquí se muestra la salida: Opciones Como ya se explicó, cuando cambia una barra de desplazamiento, se genera un AdjustmentEvent, que define sus propias versiones de los métodos getValue( ) y getValueIsAdjusting( ). Estos métodos pueden ser más convenientes en algunos casos que los definidos por JScrollBar, porque no tiene que obtener explícitamente una referencia a la barra de desplazamiento. Además de los sucesos de ajuste, una JScrollBar también generará sucesos de cambio cuando su valor cambia. Los sucesos de cambio son objetos de tipo javax.swing.ChangeEvent y se manejan al implementar un javax.swing.ChangeListener. Este escucha es registrado con el modelo de la barra de desplazamiento, que se obtiene al llamar a getModel( ) en la instancia de JScrollBar. www.fullengineeringbook.net 432 Java: Soluciones de programación JScrollBar define varias propiedades que determinan su comportamiento. En primer lugar, acepta las propiedades de valor mínimo, máximo, extensión y actual definidos por BoundedRangeModel. Es posible acceder a estos valores mediante los métodos siguientes: int getMinimum( ) void setMinimum(int val) int getMaximum( ) void setMaximum(int val) int getVisibleAmount( ) void setVisibleAmount(int val) int getValue( ) void setValue(int val) Observe que los métodos que obtienen o establecen la extensión usan el nombre VisibleAmount. Esto se debe a que estos métodos son definidos por la interfaz Adjustable (que antecede a Swing). Sin embargo, aún establecen la propiedad de extensión definida por BoundedRangeModel. JScrollBar también define un método de conveniencia llamado setValues( ), que le permite establecer el valor, la cantidad visible (extensión), y los valores mínimos y máximos en una sola llamada. Aquí se muestra: void setValues(int valor, int cantVisible, int min, int max) Además de arrastrar el pulgar, puede cambiar la posición de la barra de desplazamiento al hacer clic en las flechas de sus extremos, o en el área de paginación de la barra. Cuando se hace clic en las flechas, la barra de desplazamiento cambia su posición en el valor contenido en la propiedad de incremento de unidad. Aquí se muestran sus elementos de acceso: int getUnitIncrement( ) void setUnitIncrement(int val) Cuando se hace clic en el área de paginación, la posición del pulgar cambia en el valor contenido en la propiedad de incremento de bloque. Se tiene acceso a esta propiedad mediante los métodos getBlockIncrement( ) y setBlockIncrement( ). He aquí las versiones de uso común de esos métodos: int getBlockIncrement( ) void setBlockIncrement(int val) Una opción agradable a una barra de desplazamiento en algunos casos es el deslizador, que es una instancia de JSlider. Presenta un control que permite que una perilla se mueva en un rango. Los deslizadores suelen usarse para establecer valores. Por ejemplo, podría usarse un deslizador para establecer el volumen de un reproductor de audio. Aunque las barras de desplazamiento independientes son importantes, suele haber un método alterno que es más fácil de usar y más poderoso: el panel de desplazamiento. Los paneles de desplazamiento son instancias de JScrollPane. Un panel de desplazamiento es un contenedor especializado que proporciona automáticamente desplazamiento para el componente que contiene. JScrollPane se describe en la siguiente solución. www.fullengineeringbook.net Capítulo 8: Swing 433 Otro componente que puede usarse en lugar de una barra de desplazamiento en algunos casos es JSpinner. Crea un componente que incorpora una lista con un conjunto de flechas que lo desplaza por una lista. Use JScrollPane para manejar el desplazamiento Componentes clave Clases Métodos javax.swing.JScrollPane JScrollPane es un componente especializado que maneja automáticamente el desplazamiento de otro componente. El componente que se está desplazando puede ser individual, como una tabla, o un grupo de componentes contenidos dentro de un contenedor ligero, como JPanel. Debido a que JScrollPane hace automático el desplazamiento, suele eliminar la necesidad de administrar barras de desplazamiento individuales. En esta solución se muestra cómo poner en acción a JScrollPane. Al área visible de un panel de desplazamiento se le llama puerto de vista. Es una ventana en que se despliega el componente que se está desplazando. Por tanto, el puerto de vista despliega la parte visible del componente que se está desplazando. Las barras desplazan el componente por el puerto de vista. En su comportamiento predeterminado, un JScrollPane agregará o eliminará dinámicamente una barra de desplazamiento a medida que se necesite. Por ejemplo, si el componente está más arriba que el puerto de vista, se agrega una barra de desplazamiento vertical. Si el componente cabrá por completo dentro del puerto de vista, se eliminan las barras de desplazamiento. Paso a paso He aquí los pasos que habrán de seguirse para usar un panel de desplazamiento: 1. Cree el componente que habrá de desplazarse. 2. Cree una instancia de JScrollPane, pasándole el objeto que habrá de desplazarse. 3. Agregue el panel de desplazamiento al panel de contenido. Análisis JScrollPane define varios constructores. Aquí se muestra el usado en esta solución: JScrollPane(Component comp) El componente que habrá de desplazarse está especificado por comp. Las barras de desplazamiento se despliegan automáticamente cuando el contenido del panel excede las dimensiones del puerto de vista. Debido a que JScrollPane hace automático el desplazamiento por el componente especificado, no es necesario proporcionar otras acciones a su programa. Ejemplo Con el siguiente ejemplo se demuestra JScrollPane. Para ello, se usa un panel de desplazamiento que manejará dos situaciones comunes. En primer lugar, usa un panel de desplazamiento para www.fullengineeringbook.net 434 Java: Soluciones de programación recorrer el contenido de una etiqueta que contiene una cadena larga, de varias líneas. En segundo lugar, usa un panel de desplazamiento para recorrer el contenido de un objeto de Box, que es un contenedor ligero que usa BoxLayout. // Demuestra JScrollPane. import javax.swing.*; import java.awt.*; class DemoPD { DemoPD( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Uso de JScrollPane"); // Establece el administrador de diseño en FlowLayout. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(240, 250); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta larga, basada en HTML. JLabel jetq = new JLabel("<html>JScrollPane proporciona una manera fácil<br>" + "para manejar situaciones en que un componente es<br>" + "(o podría ser) excesivo para el espacio disponible.<br>" + "Envolver el componente en un panel de desplazamiento<br>" + "es una solución conveniente.<br><br>" + "JScrollPane es especialmente útil para el desplazamiento<br>" + "de tablas, listas, imágenes o el contenido de contenedores<br>" + "ligeros, como JPanel o Box."); // Crea un panel de desplazamiento y hace que se recorra la etiqueta. JScrollPane jpandespEtiqueta = new JScrollPane(jetq); // Establece el tamaño preferido del panel de desplazamiento // que contiene la etiqueta. jpandespEtiqueta.setPreferredSize(new Dimension(200, 100)); // Luego, usa un panel de desplazamiento para recorrer el contenido // de un Box, que es un contenedor ligero que usa BoxLayout. // Primero, crea algunos componentes que estarán contenidos // dentro del cuadro. JLabel jetqSeleccion = new JLabel("Seleccione idiomas"); JCheckBox jcvIn = new JCheckBox("Inglés"); JCheckBox jcvFr = new JCheckBox("Francés"); JCheckBox jcvAl = new JCheckBox("Alemán"); JCheckBox jcvCh = new JCheckBox("Chino"); JCheckBox jcvEs = new JCheckBox("Español"); www.fullengineeringbook.net Capítulo 8: // No se necesitan manejadores de sucesos para este ejemplo, // pero puede tratar de agregar los propios, si lo desea. // Crea un contenedor Box para incluir las opciones de idioma. Box cuadro = Box.createVerticalBox( ); // Agrega componentes al cuadro. cuadro.add(jetqSeleccion); cuadro.add(jcvIn); cuadro.add(jcvFr); cuadro.add(jcvAl); cuadro.add(jcvCh); cuadro.add(jcvEs); // Crea un panel de desplazamiento que contendrá el cuadro. JScrollPane jpandespCuadro = new JScrollPane(cuadro); // Establece el tamaño del panel de desplazamiento // que contiene el cuadro. jpandespCuadro.setPreferredSize(new Dimension(140, 90)); // Agrega los paneles de desplazamiento al panel de contenido. jmarco.add(jpandespEtiqueta); jmarco.add(jpandespCuadro); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoPD( ); } }); } } Aquí se muestra la salida: www.fullengineeringbook.net Swing 435 436 Java: Soluciones de programación JScrollPane suele usarse para proporcionar desplazamiento para una JList o una JTable. Consulte Trabaje con JList y Despliegue datos en una JTable para conocer ejemplos adicionales. Opciones Como la mayor parte de los componentes de Swing, JScrollPane permite muchas opciones y personalizaciones. Tal vez la mejora más común es la adición de encabezados. JScrollPane permite encabezados de fila y de columna. Puede usar unos o ambos. Un encabezado puede incluir cualquier tipo de componente. Esto significa que no está limitado a etiquetas pasivas; un encabezado puede contener controles activos, como un botón. La manera más fácil de establecer un encabezado de fila consiste en llamar a setRowHeaderView( ), que se muestra aquí: void setRowHeaderView(Component comp) Aquí, comp es el componente que se usará como encabezado. La manera más fácil de establecer un encabezado de columna consiste en llamar a setColumnHeaderView( ), que se muestra a continuación: void setColumnHeaderView(Component comp) Aquí, comp es el componente que se usará como encabezado. Cuando se usan encabezados, a veces es útil incluir un borde alrededor del puerto de vista. Esto se logra fácilmente al llamar a setViewportBorder( ), que está definido por JScrollPane. Aquí se muestra: void setViewportBorder(Border borde) Aquí, borde especifica el borde. Pueden crearse bordes estándar al usar la clase BorderFactory. Para establecer el efecto de encabezados y un borde, haga la prueba de agregar el siguiente código al ejemplo. Agrega encabezados de fila y columna al panel de desplazamiento jpandespCuadro y coloca un borde alrededor de su puerto de vista. Inserte el código inmediatamente después de que se cree jpandespCuadro. jpandespCuadro.setColumnHeaderView( new JLabel("Internacionalización", JLabel.CENTER)); jpandespCuadro.setRowHeaderView(new JLabel(" ")); jpandespCuadro.setViewportBorder( BorderFactory.createLineBorder(Color.BLACK)); Observe que los encabezados de fila y columna son instancias de JLabel; sin embargo, podría usarse otro tipo de componente, como JButton. Además, observe que la etiqueta para el encabezado de fila es simplemente una cadena que contiene espacios. Se usa para equilibrar el aspecto visual del panel de desplazamiento. Después de hacer estas adiciones, ahora jpandespCuadro tiene este aspecto: www.fullengineeringbook.net Capítulo 8: Swing 437 Otra personalización popular consiste en usar las esquinas de un JScrollPane. Estas esquinas se crean cuando las barras de desplazamiento se intersecan entre sí o lo hacen con los encabezados de fila o columna. Estas esquinas pueden contener cualquier componente, como una etiqueta o un botón. Sin embargo, esté consciente de los dos problemas relacionados con el uso de estas esquinas. En primer lugar, el tamaño de las esquinas depende por completo del grosor de las barras de desplazamiento o los encabezados. Por tanto, suelen ser muy pequeñas. En segundo lugar, las esquinas sólo serán visibles si las barras de desplazamiento y los encabezados se muestran. Por ejemplo, sólo habrá una esquina inferior derecha si se muestran las barras horizontal y vertical. Si quiere colocar un control en esa esquina, entonces las barras de desplazamiento necesitan permanecer visibles todo el tiempo. Para ello, necesitará establecer la directiva de barra de desplazamiento, como se describe en breve. Para colocar un componente en una esquina, llame a setCorner( ) que se muestra aquí: void setCorner(String cual, Component comp) Aquí, cual especifica la esquina. Debe ser una de las siguientes constantes definidas por la interfaz ScrollPaneConstants, que está implementada por JScrollPane: LOWER_LEADING_CORNER LOWER_LEFT_CORNER LOWER_RIGHT_CORNER LOWER_TRAILING_CORNER UPPER_LEADING_CORNER UPPER_LEFT_CORNER UPPER_RIGHT_CORNER UPPER_TRAILING_CORNER Puede establecer la directiva de barras de desplazamiento que usará el panel de desplazamiento. Esta directiva determina cuándo se muestran las barras de desplazamiento. Como opción predeterminada, un JScrollPane sólo despliega una barra de desplazamiento cuando es necesario. Por ejemplo, si la información que se está desplazando es demasiado grande para el puerto de vista, una barra de desplazamiento vertical se despliega automáticamente para permitir que el puerto de vista se despliegue hacia arriba o hacia abajo. Cuando toda la información cae dentro del puerto de vista, entonces no se muestran barras de desplazamiento. Aunque la directiva predeterminada de barras de desplazamiento es apropiada para muchos paneles de desplazamiento, podría ser un problema cuando se usan las esquinas, como se acaba de describir. Un tema adicional: cada barra de desplazamiento tiene su propia directiva. Esto le permite especificar una directiva separada para las barras de desplazamiento vertical y horizontal. Puede especificar directivas de barra de desplazamiento cuando se crea un JScrollPane al pasar la directiva usando uno de estos constructores. www.fullengineeringbook.net 438 Java: Soluciones de programación JScrollPane(int pbdVert, int pbdHoriz) JScrollPane(Component comp, int pbdVert, int pbdHoriz) Aquí, pbdVert especifica la directiva para la barra de desplazamiento vertical y pbdHoriz para la horizontal. Las directivas de barras de desplazamiento se especifican empleando constantes definidas por la interfaz ScrollPaneConstants. Aquí se muestran las directivas de barras de desplazamiento: Constante Directiva HORIZONTAL_SCROLLBAR_AS_NEEDED Barra de desplazamiento horizontal que se muestra cuando se necesita. Es la opción predeterminada. HORIZONTAL_SCROLLBAR_NEVER Barra de desplazamiento horizontal nunca mostrada. HORIZONTAL_SCROLLBAR_ALWAYS Barra de desplazamiento horizontal siempre mostrada. VERTICAL_SCROLLBAR_AS_NEEDED Barra de desplazamiento vertical que se muestra cuando se necesita. Es la opción predeterminada. VERTICAL_SCROLLBAR_NEVER Barra de desplazamiento vertical nunca mostrada. VERTICAL_SCROLLBAR_ALWAYS Barra de desplazamiento vertical siempre mostrada. Puede establecer las directivas de barras de desplazamiento después de un panel de desplazamiento que se ha creado al llamar estos métodos: void setVerticalScrollBarPolicy(int pbdVert) void setHorizontalScrollBarPolicy(int pbdHoriz) Aquí, pbdVert y pbdHoriz especifican la directiva de barras de desplazamiento. Aunque un panel de desplazamiento es una buena manera de manejar contenedores que contienen más información de la que se puede desplegar a la vez, hay otra opción que puede encontrar más apropiada en algunos casos: JTabbedPane. Maneja un conjunto de componentes al vincularlos con fichas. La selección de una ficha causa que el componente asociado con esa ficha pase al frente. Por tanto, con el uso de JTabbedPane, puede organizar información en paneles de componentes relacionados y luego dejar que el usuario elija cuál panel desplegar. Despliegue datos en una JTable Componentes clave Clases Métodos javax.swing.JScrollPane javax.swing.JTable void setCellSelectionEnabled(boolean on) void setColumnSelectionAllowed(boolean habilitado) void setPreferredScrollableViewportSize( Dimension dimensión) void setRowSelectionAllowed(boolean habilitado) void setSelectionMode(int modo) www.fullengineeringbook.net Capítulo 8: Swing 439 JTable crea, despliega y administra tablas de información. Se argumenta que es el componente más poderoso de la biblioteca de Swing. También es uno de los que resulta más desafiante usar a su pleno potencial, porque proporciona una gran cantidad de funciones (en ocasiones, muy complicadas). También soporta muchas personalizaciones sofisticadas. Dicho esto, JTable también es uno de los más importantes componentes de Swing, sobre todo en aplicaciones empresariales, porque las tablas suelen usarse para desplegar entradas de base de datos. Debido a la complejidad de JTable, se usan dos soluciones para demostrarla. En ésta, se describe cómo crear una tabla que desplegará información en forma tabular. En la siguiente solución se muestra cómo manejar sucesos de tabla. Como los otros componentes de Swing, JTable está empaquetada dentro de javax.swing. Sin embargo, muchas de sus clases e interfaces de soporte se encuentran en javax.swing.table. Se usa un paquete separado debido al gran número de interfaces y clases que están relacionadas con tablas. En su núcleo, JTable es conceptualmente simple. Se trata de un componente que consta de una o más columnas de información. En la parte superior de cada columna se encuentra un encabezado. Además de describir los datos en una columna, el encabezado también proporciona el mecanismo mediante el cual el usuario puede cambiar el tamaño de una columna y la ubicación de ésta dentro de una tabla. La información dentro de la tabla está contenida en celdas. Cada celda tiene asociados un generador de celdas, que determina la manera en que se despliega la información, y un editor de celdas, que determina la manera en que el usuario modifica la información. JTable proporciona generadores y editores de celda predeterminados que son adecuados para muchas aplicaciones. Sin embargo, es posible especificar sus propios generadores y editores personalizados, si lo desea. JTable depende de tres modelos. El primero es el modelo de tabla, que está definido por la interfaz TableModel. Este modelo define los elementos relacionados con el despliegue de datos en un formato de dos dimensiones. El segundo es el modelo de columnas de tabla, que está representado por TableColumnModel; JTable está definida a partir de columnas, y es TableColumnModel el que especifica las características de una columna. Estos dos modelos están empaquetados en javax.swing.table. El tercer modelo es ListSelectionMode. Determina la manera en que se seleccionan los elementos. Está empaquetado en javax.swing. Paso a paso Para desplegar datos en una JTable se requieren estos pasos: 1. Cree una matriz de los datos que habrán de desplegarse. 2. Cree una matriz de los encabezados de columna. 3. Cree una instancia de JTable, especificando los datos y los encabezados. 4. En casi todos los casos, querrá establecer el tamaño del puerto de vista desplazable. Esto se hace al llamar a setPreferredScrollableViewportSize( ). 5. Cambie el modo de selección al llamar a setSelectionMode( ), si lo desea. 6. Como opción predeterminada, el usuario puede seleccionar una fila. Para permitir selecciones de columna o celda, use setColumnSelectionAllowed( ), setRowSelectionAllowed( ), setCellSelectionAllowed( ) o una combinación de ellos. 7. Cree un JScrollPane, especificando la JTable como el componente que habrá de desplazarse. www.fullengineeringbook.net 440 Java: Soluciones de programación Análisis JTable proporciona varios constructores. El usado en esta solución se muestra a continuación: JTable(Object[ ][ ] datos, Object[ ] nombresEncabez) Este constructor crea automáticamente una tabla que cubre los datos especificados en datos y tiene los nombres de encabezados especificados por nombresEncabez. La tabla usará los generadores y editores de celdas predeterminados, que son adecuados para muchas aplicaciones con tablas. La matriz datos es bidimensional: la primera dimensión especifica el número de filas en la tabla y, la segunda, el número de elementos en cada fila. En todos los casos, la longitud de cada fila debe ser igual a la de nombresEncabez. JTable no proporciona capacidad de desplazamiento. En cambio, una tabla suele estar envuelta en un JScrollPane. Esto también hace que el encabezado de columna se despliegue automáticamente. Si no envuelve JTable en un JScrollPane, entonces debe desplegar explícitamente la tabla y el encabezado. Cuando se usa un panel de desplazamiento para desplegar la tabla, por lo general querrá establecer el tamaño preferido del puerto de vista desplazable de la tabla. Este puerto define una región dentro de la tabla en que se despliegan y desplazan los datos. No incluye los encabezados de columna. Si no establece el tamaño, se usará uno predeterminado, que puede ser o no apropiado. Por tanto, suele ser mejor establecer explícitamente el tamaño de puerto de vista desplazable. Para ello, use setPreferredScrollableViewportSize( ), que se muestra aquí: void setPreferredScrollableViewportSize(Dimension dim) Aquí, dim especifica el tamaño deseado del área desplazable. Como opción predeterminada, cuando el usuario hace clic en una entrada en la tabla, se selecciona toda la fila. Aunque este comportamiento suele ser el deseado, puede cambiarse para permitir la selección de una columna, o de una celda individual. Estas opciones pueden ser más apropiadas que la selección de fila para algunas aplicaciones. La habilitación de la selección de instancia columna incluye dos pasos. En primer lugar, habilitar la selección de columna. En segundo lugar, deshabilitar la selección de fila. Este proceso se realiza al usar setColumnSelectionAllowed( ) y setRowSelectionAllowed( ). El método setColumnSelectionAllowed( ) se muestra aquí. void setColumnSelectionAllowed(boolean habilitado) Cuando habilitado es verdadero, está habilitada la selección de columnas. Cuando es falso, está deshabilitada. Aquí se muestra el método setRowSelectionAllowed( ): void setRowSelectionAllowed(boolean habilitado) Cuando habilitado es verdadero, está habilitada la selección de filas. Cuando es falso, está deshabilitada. Después de habilitar la selección de columnas y deshabilitar la de filas, cuando hace clic en la tabla se selecciona una columna (en lugar de una fila). Hay dos maneras diferentes de habilitar la selección de celdas individuales. En primer lugar, puede habilitar la selección de columnas y de filas. Debido a que la selección de filas está habilitada como opción predeterminada, esto significa que si simplemente habilita la selección de columnas, ambas estarán habilitadas. Esta condición hace que quede habilitada la selección de celdas. www.fullengineeringbook.net Capítulo 8: Swing 441 La segunda manera de habilitar la selección de celdas es llamar a setCellSelectionEnabled( ), que se muestra aquí: void setCellSelectionEnabled(boolean on) Si on es verdadero, entonces la celda está habilitada. Si on es falso, entonces está deshabilitada. Este método se traduce en llamadas a setRowSelectionAllowed( ) y setColumnSelectionAllowed( ) con on pasado como argumento. Por tanto, necesita tener un poco de cuidado cuando use este método para deshabilitar la selección de celdas. Dará como resultado que la selección de filas y columnas se deshabilite, ¡lo que significa que nada puede seleccionarse! Por tanto, la mejor manera de deshabilitar la selección de celdas es deshabilitar tanto la selección de columnas como de filas. JTable define un modo de selección, que determina cuántos elementos pueden seleccionar a la vez. Como opción predeterminada, JTable permite que el usuario seleccione varios elementos. Puede cambiar este comportamiento al llamar a setSelectionMode( ). Se muestra aquí: void setSelectionMode(int modo) Aquí, modo especifica el modo de selección. Debe ser uno de los siguientes valores, que están definidos por ListSelectionModel: SINGLE_SELECTION Puede seleccionarse una fila, columna o celda. SINGLE_INTERVAL_SELECTION Puede seleccionarse un rango de filas, columnas o celdas. MULTIPLE_INTERVAL_SELECTION Pueden seleccionarse varios rangos de filas, columnas o celdas. Es la opción predeterminada. Observe que MULTIPLE_INTERVAL_SELECTION es el modo predeterminado. Ejemplo En el siguiente ejemplo se demuestra JTable. Usa una tabla para desplegar información de estado acerca de pedidos de artículos comprados en una tienda en línea. Aunque el programa no maneja sucesos de tabla (consulte la siguiente solución para conocer detalles sobre los sucesos de tabla), se trata de una tabla plenamente funcional. Por ejemplo, puede ajustarse el ancho de las columnas y puede cambiarse la posición de una columna en relación con las demás. El programa también incluye botones de opción que le permiten intercambiar entre selección de fila, de columna y de celda. También puede cambiar entre el modo de selección único y múltiple al marcar o desmarcar una casilla de verificación. // // // // // Demuestra JTable. Este programa simplemente despliega una tabla. No maneja sucesos de tabla. Consulte "Maneje sucesos de tabla" para conocer información acerca de los sucesos de tabla. import import import import import java.awt.*; java.awt.event.*; javax.swing.*; javax.swing.event.*; javax.swing.table.*; www.fullengineeringbook.net 442 Java: Soluciones de programación class DemoTabla implements ActionListener { String[ ] encabezados = { "Nombre", "ID cliente", "Pedido #", "Estatus" }; Object[ ][ ] datos = { { "Tomás", new Integer(34723), "T–01023", "Enviado" }, { "Carla", new Integer(67263), "W–43Z88", "Enviado" }, { "Saúl", new Integer(97854), "S–98301", "Devuelto" }, { "Ángel", new Integer(70851), "A–19287", "Pendiente" }, { "Luis", new Integer(40952), "L–18567", "Enviado" }, { "Marcos", new Integer(88992), "M–22345", "Cancelado" }, { "Teresa", new Integer(67492), "T–18269", "Devuelto" } }; JTable jtabPedidos; JRadioButton jboFilas; JRadioButton jboColumnas; JRadioButton jboCeldas; JCheckBox jcvUnica; DemoTabla( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame(«Demo de JTable»); // Especifica FlowLayout como administrador de diseño. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(460, 180); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una tabla que despliega datos de pedido. jtabPedidos = new JTable(datos, encabezados); // Envuelve la tabla en un panel de desplazamiento. JScrollPane jpandez = new JScrollPane(jtabPedidos); // Establece el tamaño del puerto de vista desplazable. jtabPedidos.setPreferredScrollableViewportSize( new Dimension(420, 62)); // Crea los botones de opción que determinan // el tipo de selecciones permitidas. jboFilas = new JRadioButton("Seleccionar filas", true); jboColumnas = new JRadioButton("Seleccionar columnas"); jboCeldas = new JRadioButton("Seleccionar celdas"); www.fullengineeringbook.net Capítulo 8: // Agrega los botones de opción a un grupo. ButtonGroup bg = new ButtonGroup( ); bg.add(jboFilas); bg.add(jboColumnas); bg.add(jboCeldas); // Los sucesos de botón de opción se manejan en común con el // método actionPerformed( )implementado por DemoTabla. jboFilas.addActionListener(this); jboColumnas.addActionListener(this); jboCeldas.addActionListener(this); // Crea la casilla de verificación Modo de selección única. // Cuando se marca, sólo se permiten selecciones únicas. jcvUnica = new JCheckBox("Modo de selección único"); // Agrega un escucha de elemento para jcvUnica. jcvUnica.addItemListener(new ItemListener( ) { public void itemStateChanged(ItemEvent ie) { if(jcvUnica.isSelected( )) // Permite selecciones únicas. jtabPedidos.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); else // Permite selecciones múltiples. jtabPedidos.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); } }); // Agrega los componentes al panel de contenido. jmarco.add(jpandez); jmarco.add(jboFilas); jmarco.add(jboColumnas); jmarco.add(jboCeldas); jmarco.add(jcvUnica); // Despliega el marco. jmarco.setVisible(true); } // Esto maneja los botones de selección de fila, columna y celda. public void actionPerformed(ActionEvent ie) { // Ve cuál botón está seleccionado. if(jboFilas.isSelected( )) { // Habilita la selección de filas. jtabPedidos.setColumnSelectionAllowed(false); jtabPedidos.setRowSelectionAllowed(true); } else if(jboColumnas.isSelected( )) { // Habilita la selección de columnas. www.fullengineeringbook.net Swing 443 444 Java: Soluciones de programación jtabPedidos.setColumnSelectionAllowed(true); jtabPedidos.setRowSelectionAllowed(false); } else { // Habilita la selección de celdas. jtabPedidos.setCellSelectionEnabled(true); } } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoTabla( ); } }); } } Aquí se muestra la salida: Opciones JTable es un componente muy complejo y con gran cantidad de características. Está más allá del alcance de este libro examinar todas sus opciones y personalizaciones. Sin embargo, aquí se mencionan algunas de las más comunes. Si habrá de usar las tablas de manera constante, entonces querrá estudiar las capacidades del componente a profundidad. (Para conocer más información sobre el manejo de sucesos de tabla, consulte la siguiente solución.) Si está construyendo una tabla a partir de un conjunto fijo de datos que pueden almacenarse fácilmente en una matriz, entonces el constructor usado en el ejemplo es la manera más fácil de crear una JTable. Sin embargo, en casos en que el tamaño del conjunto de datos está sujeto a cambio, o cuando no se conoce por anticipado su tamaño, como cuando los datos se están obteniendo de una colección, tal vez sea más fácil usar el siguiente constructor: JTable(Vector datos, Vector nombresEncabez) En este caso, datos es un Vector de vectores. Cada Vector debe contener la misma cantidad de elementos que los nombres de encabezado especificados por nombresEncabez. Debido a que las instancias de Vector son estructuras de datos dinámicas, no es necesario que se conozca por anticipado el tamaño de los datos. www.fullengineeringbook.net Capítulo 8: Swing 445 JTable soporta varios modos de cambio de tamaño, que controlan la manera en que cambia el ancho de las columnas cuando el usuario ajusta el tamaño de una columna al arrastrar el borde de su encabezado. Como opción predeterminada, cuando cambia el ancho de una columna, se ajusta el ancho de todas las columnas subsecuentes (es decir, todas las que se encuentran a la derecha de la que se está cambiando) de modo que el ancho general de la tabla queda sin cambio. Cualquier columna antes de la que se cambió queda tal como estaba. Sin embargo, este comportamiento predeterminado es sólo uno de los cinco modos diferentes de cambio de tamaño que puede seleccionar. Para cambiar el modo de cambio de tamaño, use el método setAutoResizeMode( ), que se muestra aquí: void setAutoResizeMode(int como) Aquí, como debe ser una de las cinco constantes definidas por JTable. Se muestran a continuación: AUTO_RESIZE_ALL_ COLUMNS El ancho de todas las columnas se ajusta cando cambia el ancho de una columna. AUTO_RESIZE_LAST_ COLUMN Se ajusta sólo el ancho de la columna del extremo derecho cuando cambia una columna. AUTO_RESIZE_NEXT_ COLUMN Se ajusta el ancho sólo de la columna siguiente cuando cambia una columna. AUTO_RESIZE_OFF No se hace ningún ajuste a las columnas. En lugar de eso, cambia el ancho de la tabla. Si la tabla está envuelta en un panel de desplazamiento y el ancho de la tabla se expande más allá de los límites del puerto de vista, entonces éste permanece del mismo tamaño y se agrega una barra de desplazamiento horizontal que permite que la tabla se desplace a la izquierda y la derecha para traer las otras columnas a la vista. Cuando se crea la tabla, las columnas no necesariamente tienen el tamaño necesario para llenar el ancho de la tabla. Por tanto, si deshabilita la función de cambio de tamaño automático, tal vez necesite especificar manualmente el ancho de las columnas. AUTO_RESIZE_SUBSEQUENT_ COLUMNS Se ajusta el ancho de las columnas a la derecha de la que se está cambiando. Ésta es la configuración predeterminada. La mejor manera de comprender los efectos de los varios modos de cambio de tamaño es experimentar. Un tema adicional: cuando usa AUTO_RESIZE_OFF, debe tomar una cantidad importante de control manual sobre la tabla. Como regla general, debe usar esta opción sólo como último recurso. Suele ser mejor dejar que JTable maneje el cambio de tamaño por usted automáticamente. Una opción que le resultará muy útil es la capacidad de JTable para imprimir una tabla. Esto se logra al llamar a print( ). Este método tiene varias formas y se agregó en Java 5. Aquí se muestra su forma más simple: boolean print( ) throws java.awt.print.PrinterException Devuelve verdadero si la tabla se imprime y falso si el usuario cancela la impresión. Este método causa que se despliegue el cuadro de diálogo Imprimir estándar. Por ejemplo, para imprimir la tabla jtabPedidos usada en el ejemplo, puede usar la siguiente secuencia: www.fullengineeringbook.net 446 Java: Soluciones de programación try { jtabPedidos.print( ) } catch (java.awt.print.PrinterException excepción) { // ... } Otras versiones de print( ) le permiten especificar varias opciones, incluidos un encabezado y un pie de página. Puede controlar si la tabla despliega o no líneas de cuadrícula (que son las líneas entre las celdas) al llamar a setShowGrid( ). Como opción predeterminada, se muestran las líneas de cuadrícula. Si pasa falso a este método, no se muestran las líneas de cuadrícula. Como opción, puede controlar el despliegue de líneas horizontal y vertical de manera independiente al llamar a setShowVerticalLines( ) y setShowHorizontalLines( ). He aquí otras tres personalizaciones que pueden resultar valiosas. En primer lugar, puede crear su propio modelo de tabla, que le permita controlar la manera en que se obtienen los datos de la tabla. Esto le permite manejar fácilmente casos en que los datos se obtienen o calculan de manera dinámica. En segundo lugar, puede crear sus propios generadores de celdas, que permiten especificar la manera en que deben desplegarse los datos de la tabla. En tercer lugar, puede definir sus propios editores de celdas. Un editor de celdas es el componente invocado cuando el usuario edita los datos dentro de una celda. Por tanto, un editor de celdas determina la manera en que puede editarse una celda. Maneje sucesos de JTable Componentes clave Clases e interfaces Métodos javax.swing.event.ListSelectionEvent javax.swing.event.ListSelectionListener void valueChanged(ListSelectionEvent le) javax.swing.ListSelectionModel void addListSelectionListener( ListSelectionListener lsl) javax.swing.TableModelEvent int getColumn( ) int getFirstRow( ) int getType( ) javax.swing.TableModelListener void tableChanged(TableModelEvent tme) javax.swing.JTable TableColumnModel getColumnModel( ) int getSelectedColumn( ) int[ ] getSelectedColumns( ) int getSelectedRow( ) int[ ] getSelectedRows( ) TableModel getModel( ) ListSelectionModel getSelectionModel( ) javax.swing.table.TableModel void addTableModelListener( TableModelListener tml) Object getValueAt(int fila, int columna) www.fullengineeringbook.net Capítulo 8: Swing 447 En la solución anterior se describen las técnicas básicas requeridas para crear una JTable y usarla para desplegar datos. En esta solución se muestra cómo manejar dos sucesos importantes generados por la tabla: ListSelectionEvent y TableModelEvent. Se genera un ListSelectionEvent cuando el usuario selecciona algo en la tabla. Un TableModelEvent se dispara cuando una tabla cambia de alguna manera. Al manejar estos sucesos, puede responder cuando el usuario interactúa con la tabla. Aunque ninguno de esos sucesos es difícil de manejar, ambos requieren un poco más de trabajo que podría esperar debido a los varios modelos que usa JTable. Paso a paso El manejo de un ListSelectionEvent generado cuando una fila está seleccionada requiere dos pasos: 1. Registre un ListSelectionListener con el ListSelectionModel usado por la tabla. Este modelo se obtiene al llamar a getSelectionModel( ) en la JTable. 2. Puede determinar cuál fila o cuáles filas están seleccionadas al llamar a getSelectedRow( ) o getSelectedRows( ) en la JTable. El manejo de un ListSelectionEvent generado cuando una columna está seleccionada requiere dos pasos: 1. Registre un ListSelectionListener con el ListSelectionModel usado por el modelo de columna de la tabla. Este modelo se obtiene al llamar primero a getColumnModel( ) en la JTable y luego llamando a getSelectionModel( ) en la referencia al modelo de columna. 2. Puede determinar cuál columna o cuáles columnas están seleccionadas al llamar a getSelectedColumn( ) o getSelectedColumns( ) en la JTable. El manejo de un TableModelEvent generado cuando los datos de una tabla cambian requiere estos pasos: 1. Registre un TableModelListener con el TableModel de la tabla. Se obtiene una referencia a TableModel al llamar a getModel( ) en la instancia de JTable. 2. Puede determinar la naturaleza del cambio al llamar a getType( ) en la instancia de TableModelEvent. 3. Puede determinar los índices de la celda cambiada al llamar a getFirstRow( ) y getColumn( ) en la instancia de TableModelEvent. 4. Puede obtener el valor de la celda que ha cambiado al llamar a getValueAt( ) en la instancia de TableModelEvent. Análisis Cuando está seleccionada una fila, columna o celda dentro de una tabla, se dispara un ListSelectionEvent. Para manejar este suceso, debe registrar un ListSelectionListener. ListSelectionEvent y ListSelectionListener se describen en Trabaje con JList, y ese análisis no se repetirá aquí. Sin embargo, recuerde que ListSelectionListener especifica sólo un método, llamado valueChanged( ), que se muestra aquí: void valueChanged(ListSelectionEvent le) www.fullengineeringbook.net 448 Java: Soluciones de programación Aquí, le es una referencia al objeto que generó el suceso. Aunque ListSelectionEvent proporciona algunos métodos propios, a menudo interrogará al objeto de JTable o sus modelos para determinar lo que ha ocurrido. Para manejar sucesos de selección de filas, debe registrar un ListSelectionListener. Este escucha no se agrega a la instancia de JTable. En cambio, se agrega al modelo de selección de listas. Se obtiene una referencia a este modelo al llamar a getSelectionModel( ) en la instancia de JTable. Aquí se muestra: ListSelectionModel getSelectionModel( ) La interfaz ListSelectionModel define varios métodos que le permiten definir el estado del modelo. Gran parte de su funcionalidad está directamente disponible en JTable. Sin embargo, hay un método definido por ListSelectionModel que es útil: getValueIsAdjusting( ). Devuelve verdadero si el proceso de selección aún está dándose y falso cuando ha terminado. (Funciona de manera similar al método del mismo nombre usado por JList y JScrollBar, descrito en Trabaje con JList y Use una barra de desplazamiento). Una vez que se ha recuperado un ListSelectionEvent, puede determinar cuál fila se ha seleccionado al llamar a getSelectedRow( ) o getSelectedRows( ). Se muestran aquí: int getSelectedRow( ) int[ ] getSelectedRows( ) El método getSelectedRow( ) devuelve el índice de la primera fila seleccionada, que será la única seleccionada si se está usando un modo de selección único. Si no se ha seleccionado fila alguna, entonces se devuelve –1. Cuando está habilitada la selección múltiple (que es la opción predeterminada), entonces debe llamar a getSelectedRows( ) para obtener una lista de los índices de todas las filas seleccionadas. Si no hay filas seleccionadas, la matriz devuelta tendrá una longitud de cero. Si sólo está seleccionada una fila, entonces la matriz tendrá exactamente una longitud de un elemento. Por tanto, puede usar getSelectedRows( ) aunque esté usando un modo de selección única. Para manejar sucesos de selección de columnas, también debe registrar un ListSelectionListener. Sin embargo, este escucha no está registrado con el modelo de selección de listas proporcionado por JTable. En cambio, debe registrarse con el modelo usado por el modelo de columna de la tabla. (El modelo para cada columna está especificado por la implementación de TableColumnModel). Obtendrá una referencia al modelo al llamar a getColumnModel( ) en la instancia de JTable. Aquí se muestra: TableColumnModel getColumnModel( ) Empleando la referencia devuelta, puede obtener una referencia al ListSelectionModel usado por las columnas. Por tanto, suponiendo un JTable llamada jtabla, usará una instrucción como ésta para obtener el modelo de selección de listas para una columna: ListSelectionModel modSelecCols = jtabla.getColumnModel( ).getSelectionModel( ); Una vez que se ha recibido un ListSelectionEvent, puede determinar cuál columna se ha seleccionado al llamar a getSelectedColumn( ) o getSelectedColumns( ) en la JTable. Aquí se muestran: int getSelectedColumn( ) int[ ] getSelectedColumns( ) www.fullengineeringbook.net Capítulo 8: Swing 449 El método getSelectedColumn( ) devuelve el índice de la primera columna seleccionada, que será la única columna seleccionada si se está usando un modo de selección única. Si no se ha seleccionado una columna, entonces se devolverá –1. Cuando está habilitada la selección múltiple (que es la opción predeterminada), entonces llame a getSelectedColumns( ) para obtener una lista de los índices de todas las columnas seleccionadas. Si no hay columnas seleccionadas, la matriz devuelta tendrá una longitud de cero. Si sólo está seleccionada una columna, entonces la matriz tendrá exactamente una longitud de un elemento. Por tanto, puede usar getSelectedColumns( ) aunque esté usando un modo de selección única. Un hecho que debe tener firmemente en cuenta es que los índices devueltos por estos métodos están en relación con la vista (en otras palabras, las posiciones de las columnas desplegadas en la pantalla). Debido a que el usuario puede reubicar las columnas, estos índices podrían diferir de los del modelo de tabla, que son fijos. Para manejar la selección de celdas, registre escuchas para los sucesos de selección de filas y columnas. Luego, determine cuál celda está seleccionada al obtener el índice de la fila y la columna. Puede estar a la escucha de cambios a los datos de la tabla (como cuando el usuario cambia el valor de una celda) al registrar un TableModelListener con el modelo de la tabla. Este modelo se obtiene al llamar a getModel( ) en la instancia de JTable. Aquí se muestra: TablaModel getModel( ) TableModel define el método addTableModelListener( ), que se usa para agregar un escucha para TableModelEvents. Aquí se muestra: void addTableModelListener(TableModelListener tml) TableModelListener define sólo un método, tableChanged( ). Aquí se muestra: void tableChanged(TableModelEvent tme) A este método se le llama cada vez que cambia el modelo de tabla, que incluye cambios a los datos y los encabezados, e inserciones o eliminaciones de columnas. TableModelEvents define los métodos mostrados aquí: Método Descripción int getColumn( ) Devuelve el índice basado en cero de la columna en que ocurrió el suceso. Un valor devuelto de TableModelEvent.ALL_COLUMNS significa que fueron seleccionadas todas las columnas dentro de la fila seleccionada. int getFirstRow( ) Devuelve el índice basado en cero de la primera fila en que ocurrió el suceso. Si el encabezado cambió, el valor devuelto es TableModelEvent.HEADER_ROW. int getLastRow( ) Devuelve el índice basado en cero de la primera fila en que ocurrió el suceso. int getType( ) Devuelve un valor que indica qué tipo de cambio ocurrió. Será uno de estos valores: TableModelEvent.DELETE, TableModelEvent.INSERT o TableModelEvent.UPDATE. Los métodos getFirstRow( ), getLastRow( ) y getColumn( ) devuelven el índice de la fila (o las filas) y columnas que han cambiado. Esta misma información puede obtenerse al llamar a métodos definidos por JTable (como se describió antes). Pero en ocasiones es más conveniente obtenerlo del objeto del suceso. Sin embargo, necesitará ser cuidadoso porque getColumn( ) devuelve el índice de columnas como se mantuvo en el modelo de la tabla. Si el usuario reorganiza las columnas, entonces diferirá del índice de columnas devuelto por un método como getSelectedColumn( ) de JTable, que refleja la posición de las columnas en la vista actual. Por tanto, no mezcle los dos índices. www.fullengineeringbook.net 450 Java: Soluciones de programación Además, observe getType( ). Devuelve un valor que indica qué tipo de cambio ha tenido lugar. Los valores de devolución posibles, definidos por TableModelEvent, se muestran a continuación: DELETE Se ha eliminado una fila o columna. INSERT Se ha agregado una fila o columna. UPDATE Han cambiado los datos de la celda. A menudo, si los datos de la celda han cambiado, su aplicación necesitará actualizar el origen de los datos para reflejar este cambio. Por tanto, si su tabla permite la edición de celdas, resulta especialmente importante vigilar los sucesos UPDATE del modelo de tabla. Cuando cambian los datos dentro de una celda, puede obtener el nuevo valor al llamar a getValueAt( ) en la instancia del modelo. Aquí se muestra: Object getValueAt(int fila, int columna) Aquí, fila y columna especifican las coordenadas de la celda que cambió. Ejemplo En el siguiente ejemplo se muestran los pasos necesarios para manejar los sucesos de selección de listas y modelo de tablas. Cada vez que selecciona una nueva fila, columna o celda (dependiendo de cual opción de selección esté elegida), se despliegan los índices del elemento seleccionado. Si cambia el contenido de una celda, entonces el contenido actualizado se despliega junto con los índices de la celda que cambió. // Maneja los sucesos de selección y cambio de modelo para una JTable. import import import import import java.awt.*; java.awt.event.*; javax.swing.*; javax.swing.event.*; javax.swing.table.*; class DemoSucesosTabla implements ActionListener, ListSelectionListener { String[ ] encabezados = { «Nombre», «ID cliente», «Pedido #», «Estatus» }; Object[ ][ ] datos = { { «Tomás», new Integer(34723), «T–01023», «Enviado» }, { «Carla», new Integer(67263), «W–43Z88», «Enviado» }, { «Saúl», new Integer(97854), «S–98301», «Devuelto» }, { «Ángel», new Integer(70851), «A–19287», «Pendiente» }, { «Luis», new Integer(40952), «L–18567», «Enviado» }, { «Marcos», new Integer(88992), «M–22345», «Cancelado» }, { «Teresa», new Integer(67492), «T–18269», «Devuelto» } }; JTable jtabPedidos; www.fullengineeringbook.net Capítulo 8: Swing JRadioButton jboFilas; JRadioButton jboColumnas; JRadioButton jboCeldas; JCheckBox jcvUnica; JLabel jetq; JLabel jetq2; TableModel tm; DemoSucesosTabla( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("JTable Event Demo"); // Especifica FlowLayout para el administrador de diseño. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(460, 230); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta que desplegará una selección. jetq = new JLabel( ); jetq.setPreferredSize(new Dimension(400, 20)); // Crea una etiqueta que desplegará cambios a una celda. jetq2 = new JLabel( ); jetq2.setPreferredSize(new Dimension(400, 20)); // Crea una tabla que despliega datos de pedidos. jtabPedidos = new JTable(datos, encabezados); // Envuelve la tabla en un panel de desplazamiento. JScrollPane jpandez = new JScrollPane(jtabPedidos); // Establece el tamaño del puerto de vista desplazable. jtabPedidos.setPreferredScrollableViewportSize( new Dimension(420, 62)); // Obtiene el modelo de selección de listas para filas. ListSelectionModel modSelecFilas = jtabPedidos.getSelectionModel( ); // Obtiene el modelo de selección de listas para columnas. ListSelectionModel modSelecCols = jtabPedidos.getColumnModel( ).getSelectionModel( ); // Escucha sucesos de selección. modSelecFilas.addListSelectionListener(this); modSelecCols.addListSelectionListener(this); www.fullengineeringbook.net 451 452 Java: Soluciones de programación // Obtiene el modelo de tabla. tm = jtabPedidos.getModel( ); // Agrega un escucha de modelo de tabla. Escucha // cambios a los datos de una celda. tm.addTableModelListener(new TableModelListener( ) { public void tableChanged(TableModelEvent tme) { // Si cambian los datos de una celda, lo reporta. if(tme.getType( ) == TableModelEvent.UPDATE) { jetq2.setText("La celda " + tme.getFirstRow( ) + ", " + tme.getColumn( ) + " cambió." + " El nuevo valor es: " + tm.getValueAt(tme.getFirstRow( ), tme.getColumn( ))); } } }); // Crea los botones de opción que determinan // qué tipo de selecciones se permiten. jboFilas = new JRadioButton("Seleccionar filas", true); jboColumnas = new JRadioButton("Seleccionar columnas"); jboCeldas = new JRadioButton("Seleccionar celdas"); // Agrega los botones de opción a un grupo. ButtonGroup bg = new ButtonGroup( ); bg.add(jboFilas); bg.add(jboColumnas); bg.add(jboCeldas); // Los sucesos de botón de opción se manejan en común con el // método actionPerformed( )implementado por DemoSucesosTabla. jboFilas.addActionListener(this); jboColumnas.addActionListener(this); jboCeldas.addActionListener(this); // Crea la casilla de verificación Modo de selección único. // Cuando se marca, sólo se permiten selecciones únicas. jcvUnica = new JCheckBox("Modo de selección única"); // Agrega un escucha de elemento para jcvUnica. jcvUnica.addItemListener(new ItemListener( ) { public void itemStateChanged(ItemEvent ie) { if(jcvUnica.isSelected( )) // Permite selecciones únicas. jtabPedidos.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); else // Regresa al modo de selección predeterminado, que // permite selecciones de varios intervalos. www.fullengineeringbook.net Capítulo 8: jtabPedidos.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); } }); // Agrega los componentes al panel de contenido. jmarco.add(jpandez); jmarco.add(jboFilas); jmarco.add(jboColumnas); jmarco.add(jboCeldas); jmarco.add(jcvUnica); jmarco.add(jetq); jmarco.add(jetq2); // Despliega el marco. jmarco.setVisible(true); } // Esto maneja los botones de selección de fila, columna y celda. public void actionPerformed(ActionEvent ie) { // Ve cuál botón está seleccionado. if(jboFilas.isSelected( )) { // Habilita la selección de filas. jtabPedidos.setColumnSelectionAllowed(false); jtabPedidos.setRowSelectionAllowed(true); } else if(jboColumnas.isSelected( )) { // Habilita la selección de columnas. jtabPedidos.setColumnSelectionAllowed(true); jtabPedidos.setRowSelectionAllowed(false); } else { // Habilita la selección de celdas. jtabPedidos.setCellSelectionEnabled(true); jcvUnica.setSelected(true); jtabPedidos.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); } } // Maneja la selección de sucesos al desplegar los índices // de los elementos seleccionados. Todos los índices están // relacionados con la vista. (Consulte Opciones para conocer // los detalles sobre la conversión de índices de vista a índices // de modelo.) public void valueChanged(ListSelectionEvent le) { String cad; // Determina lo que se ha seleccionado. if(jboFilas.isSelected( )) { www.fullengineeringbook.net Swing 453 454 Java: Soluciones de programación cad = "Filas seleccionadas: "; // Obtiene una lista de todas las filas seleccionadas. int[ ] filas = jtabPedidos.getSelectedRows( ); // Crea una cadena que contiene los índices de las filas seleccionadas. for(int i=0; i < filas.length; i++) cad += filas[i] + " "; } else if(jboColumnas.isSelected( )) { cad = "Columnas seleccionadas: "; // Obtiene una lista de todas las columnas seleccionadas. int[ ] cols = jtabPedidos.getSelectedColumns( ); // Crea una cadena que contiene los índices de las columnas seleccionadas. for(int i=0; i < cols.length; i++) cad += cols[i] + " "; } else { cad = "Celda seleccionada: (en relación con la vista) " + jtabPedidos.getSelectedRow( ) + ", " + jtabPedidos.getSelectedColumn( ); } // Despliega los índices. jetq.setText(cad); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoSucesosTabla( ); } }); } } Aquí se muestra la salida: www.fullengineeringbook.net Capítulo 8: Swing 455 Opciones Debido a que el orden de las columnas desplegadas en la pantalla puede cambiar, como cuando el usuario arrastra una columna a una nueva ubicación, el índice de la columna devuelto por métodos como getSelectedColumn( ) o getSelectedColumns( ) puede diferir del índice usado por el modelo. Esto podría ser un problema en casos en que quiera usar el índice para acceder a los datos del modelo. Para manejar esta situación, JTable proporciona el método convertColumnIndexToModel( ). Se muestra aquí: int convertColumnIndexToModel(int indiceVista) Devuelve el índice del modelo de la columna que corresponde al índice de la vista pasado en indiceVista. También puede realizar la operación inversa, convirtiendo un índice de columna del modelo en uno de la vista, al llamar a convertColumnIndexToView( ). Se muestra aquí: int convertColumnIndexToView(int indiceModelo) Devuelve el índice de la vista de la columna que corresponde al índice del modelo pasado en indiceModelo. Por ejemplo, en el listado anterior, puede desplegar los índices de modelo en lugar de los de vista de las columnas seleccionadas, al sustituir esta secuencia en la parte del método valueChanged( ) que despliega las selecciones de columna: // Crea una cadena que contiene los índices del modelo de las columnas seleccionadas. for(int i=0; i < cols.length; i++) cad += jtabPedidos.convertColumnIndexToModel(cols[i]) + " "; A partir de Java 6, es fácil permitir que el usuario ordene los datos en una tabla con base en las entradas de una columna. El ordenamiento da como resultado la reorganización de la filas, lo que puede llevar a que el índice de cualquier fila determinada de la vista sea diferente de su índice en el modelo. Debido a esto, Java 6 agregó los métodos convertRowIndexToModel( ) y convertRowIndexToView( ). Funcionan como sus contrapartes orientadas a columnas. Para permitir que el usuario ordene los datos, debe establecer el ordenador de las filas al llamar a setRowSorter( ). Este método se agregó en Java 6 y se muestra aquí: void setRowSorter(RowSorter<? extends TableModel> ordenadorFila) El ordenador de filas se especifica con ordenadorFila, que es una instancia de la clase RowSorter. Para el ordenamiento de filas en una JTable, puede tratarse de una instancia de javax.swing.table. TableRowSorter. Por ejemplo, para permitir el ordenamiento de la tabla jTabPedidos que se usó en el ejemplo, agregue esta línea: jtabPedidos.setRowSorter(new TableRowSorter<TableModel>(tm)); Después de hacer este cambio, las filas pueden ordenarse por entradas de columna con sólo hacer clic en el encabezado de columna deseado. Puede establecer los datos de una celda bajo control del programa al llamar a setValueAt( ) en el modelo de la tabla. Aquí se muestra: void setValueAt(Object val, int fila, int columna) Asigna el valor val a la celda localizada en fila y columna. Para ambos métodos, los valores de fila y columna se relacionan con el modelo, no con la vista. www.fullengineeringbook.net 456 Java: Soluciones de programación Despliegue datos en un JTree Componentes clave Clases e interfaces Métodos javax.swing.JTree void addTreeExpansionListener( TreeExpansionListener tel) void addTreeSelectionListener( TreeSelectionListener tsl) TreeModel getModel( ) TreeSelectionModel getSelectionModel( ) void setEditable(boolean on) javax.swing.tree.DefaultMutableTreeNode void addMutableTreeNode secund javax.swing.tree.TreeModel void addTreeModelListener( TreeModelListener tml) javax.swing.tree.TreeExpansionEvent TreePath getPath( ) javax.swing.tree.TreeExpansionListener void treeCollapsed( TreeExpansionEvent tee) voidtreeExpanded( TreeExpansionEvent tee) javax.swing.tree.TreeModelEvent Object[ ] getChildren( ) TreePath getTreePath javax.swing.tree.TreeModelListener void treeNodesChanged( TreeModelEvent tme) void treeStructureChanged( TreeModelEvent tme) void treeNodesInserted( TreeModelEvent tme) void treeNodesRemoved( TreeModelEvent tme) javax.swing.tree.TreePath Object getLastPathComponent( ) Object[ ] getPath( ) javax.swing.tree.TreeSelectionEvent TreePath getPath( ) javax.swing.tree.TreeSelectionListener void valueChanged( TreeSelectionEvent tse) javax.swing.tree.TreeSelectionModel void setSelectionMode(int como) www.fullengineeringbook.net Capítulo 8: Swing 457 JTree presenta una vista jerárquica de datos en un formato de árbol. Esto hace que JTree sea más adecuado para desplegar datos que pueden ordenarse en categorías y subcategorías. Por ejemplo, un árbol suele usarse para desplegar el contenido de un sistema de archivos. En este caso, los archivos individuales están subordinados al directorio que los contiene, que podría estar subordinado a otro directorio, de nivel superior. En el árbol, las ramas pueden expandirse o contraerse según lo solicite el usuario. Esto permite que el árbol presente datos jerárquicos en una forma compacta, pero expansible. JTree está empaquetado dentro de javax.swing, pero muchas de sus clases e interfaces de soporte se encuentran en javax.swing.tree. Se usa un paquete separado porque un gran número de interfaces y clases están relacionadas con los árboles. JTree soporta muchas personalizaciones y opciones (muchas más de las que pueden describirse en esta solución). Por fortuna, las opciones predeterminadas proporcionadas por JTree son suficientes para una gran cantidad de aplicaciones. Con toda franqueza, dada la sofisticación de JTree, es sorprendentemente fácil trabajar con él. JTree se basa en una estructura de datos conceptualmente simple, basada en un árbol. Un árbol empieza con un solo nodo raíz que indica el inicio del árbol. Bajo la raíz hay uno o más nodos secundarios. Hay dos tipos de nodos secundarios: nodos de hoja (también denominados nodos terminales), que no tienen secundarios, y nodos de rama, que forman los nodos raíz de los subárboles. Un subárbol es simplemente un árbol que es parte de un árbol más grande. A la secuencia de nodos que llevan de la raíz a un nodo especifico se le denomina ruta. Cada nodo tiene asociado un generador de celdas, que determina la manera en que se despliega la información, y un editor de celdas, que determina la manera en que el usuario edita la información. JTree proporciona generadores y editores de celdas predeterminados que son adecuados para muchas aplicaciones (tal vez casi todas). Como podría esperar, es posible especificar sus propios generadores y editores de celdas, si lo desea. JTree no proporciona capacidades de desplazamiento propias, pero casi siempre necesitará proporcionarlas. Como pronto lo verá cuando trabaje con JTree, sólo se requiere una pequeña cantidad de datos para producir un árbol que sea demasiado largo cuando lo expande por completo. En lugar de tratar de crear un JTree lo suficientemente largo para que contenga un árbol por completo expandido (lo que en muchos casos ni siquiera será posible), es mejor envolver un JTree en un JScrollPane. Esto permite que el usuario recorra el árbol, trayendo a la vista la parte que se desee ver. JTree depende de dos modelos: TreeModel y TreeSelectionModel. La interfaz TreeModel define lo relacionado con el despliegue de datos en un formato de árbol. Swing proporciona una implementación predeterminada de este modelo llamado DefaultTreeModel. Tanto TreeModel como DefaultTreeModel están empaquetados en javax.swing.tree. La interfaz TreeSelectionModel determina la manera en que se seleccionan los elementos. También está empaquetado en javax. swing.tree. JTree puede generar diversos sucesos, pero tres se relacionan directamente con los árboles: TreeSelectionEvent, TreeExpansionEvent y TreeModelEvent. Estos sucesos se generan cuando se selecciona un suceso de un árbol, cuando el árbol está expandido o contraído y cuando cambian los datos del árbol, respectivamente. Están empaquetados en javax.swing.event. Cada nodo de un árbol es una instancia de la interfaz TreeNode. Por tanto, JTree es una colección de objetos de TreeNode. La interfaz MutableTreeNode extiende TreeNode y define el comportamiento de los nodos que son mutables (es decir, que pueden cambiarse). Swing proporciona una implementación predeterminada de MutableTreeNode llamada DefaultMutableTreeNode. Esta clase es apropiada para una amplia variedad de nodos de árbol, y se usa para crear los nodos de esta solución. www.fullengineeringbook.net 458 Java: Soluciones de programación Paso a paso Para desplegar datos en un JTree se requieren estos pasos: 1. Cree los nodos que se desplegarán. En estas soluciones se usan nodos del tipo DefaultMutableTreeNode. 2. Cree la jerarquía de árbol deseada al agregar un nodo a otro. Haga que uno de estos nodos sea el raíz. 3. Cree una instancia de JTree, pasándola en el nodo raíz. 4. Cuando el usuario selecciona un elemento del archivo, se genera un TreeSelectionEvent. Para escuchar este suceso, registre un TreeSelectionListener con el JTree. 5. Cuando el árbol está expandido o contraído, se genera un TreeExpansionEvent. Para escuchar este suceso, registre un TreeExpansionListener con el JTree. 6. Cuando cambien los datos o la estructura del árbol, se genera un TreeModelEvent. Para escuchar este suceso, registre un TreeModelListener con el modelo del árbol. Para obtener una referencia al modelo, llame a getModel( ) en la instancia de JTree. Empleando el objeto de suceso, puede obtener información acerca de la ruta que cambió. 7. Puede cambiar el modo de selección al llamar a setSelectionMode( ) en el modelo de selección del árbol. (El modelo de selección del árbol se obtiene al llamar a getSelectionModel( ) en la instancia de JTree.) 8. Puede permitir que un nodo de árbol se edite al llamar a setEditable( ) en la instancia de JTree. Análisis JTree define muchos constructores. Aquí se muestra el usado en esta solución: JTree(TreeNode tn) Esto construye un árbol que tiene tn como raíz. Un JTree contiene una colección de objetos que representan el árbol. Estos objetos deben ser instancias de la interfaz TreeNode. Declara métodos que encapsulan información acerca del nodo de un árbol. Por ejemplo, es posible obtener una referencia al nodo principal o una enmeración de los nodos secundarios. También puede determinar si un nodo es una hoja. La interfaz MutableTreeNode extiende TreeNode. Define un nodo que puede tener cambiados sus datos, o tener nodos secundarios agregados o eliminados. Swing proporciona una implementación predeterminada de MutableTreeNode llamada DefaultMutableTreeNode. Es la clase que se usa en esta solución para construir nodos para el árbol. DefaultMutableTreeNode define tres constructores. El usado en esta solución es DefaultMutableTreeNode(Object obj) Aquí obj es el objeto que se incluirá en el nodo del árbol. Para crear una jerarquía de nodos de árbol, se agrega un nodo a otro. Esto se logra al llamar al método add( ) de DefaultMutableTreeNode. Aquí se muestra: void add(MutableTreeNode secun) www.fullengineeringbook.net Capítulo 8: Swing 459 Aquí, secun es un nodo de árbol mutable que se agregará como secundario al nodo que invoca. Por tanto, para construir un árbol, creará un nodo raíz y luego le agregará nodos subordinados. Para escuchar a un TreeSelectionEvent, debe implementar la interfaz TreeSelectionListener y registrar el escucha con la instancia de JTree. TreeSelectionListener define el siguiente método: void valueChanged(TreeSelectionEvent tse) Aquí, tse es el suceso de selección. TreeSelectionEvent define varios métodos. Uno de interés especial es getPath( ), porque se usa en esta solución. Aquí se muestra: TreePath getPath( ) La ruta que lleva a la selección es devuelta como un objeto de TreePath. TreePath es una clase empaquetada en javax.swing.tree. Encapsula una ruta que lleva de la raíz del árbol al nodo seleccionado. TreePath define varios métodos. Dos son de interés especial: Object[ ] getPath( ) Object getLastPathComponent( ) El método getPath( ) devuelve una matriz de objetos que representan todos los nodos de la ruta. El getLastPathComponent( ) devuelve una referencia al último nodo de la ruta. Para escuchar un TreeExpansionEvent, debe implementar la interfaz TreeExpansionListener y registrar el escucha con la instancia de JTree. TreeExpansionListener define los dos métodos siguientes: void treeCollapsed(TreeExpansionEvent tee) void treeExpanded(TreeExpansionEvent tee) Aquí, tee es el suceso de expansión del árbol. Al primer método se le llama cuando un subárbol está oculto, y al segundo cuando un subárbol se vuelve visible. Como tema interesante, JTree le permite escuchar dos tipos de tres notificaciones de expansión de árbol. En primer lugar, es posible recibir notificación justo antes de que se presente un suceso de expansión al registrar un TreeWillExpandListener. En segundo lugar, puede notificarse después del suceso de expansión al registrar un TreeExpansionListener. Por lo general, registrará un TreeExpansionListener, como en esta solución, pero Swing le da la opción. TreeExpansionEvent define sólo un método, getPath( ), que se muestra aquí: TreePath getPath( ) Este método devuelve un objeto de TreePath, que contiene la ruta al nodo que se expandió o contrajo, o que se expandirá o contraerá. Para escuchar a un TreeModelEvent, debe implementar la interfaz TreeModelListener y registrar el escucha con el modelo del árbol. TreeModelListener define los siguientes métodos: void treeNodesChanged(TreeModelEvent tme) void treeStructureChanged(TreeModelEvent tme) void treeNodesInserted(TreeModelEvent tme) void treeNodesRemoved(TreeModelEvent tme) www.fullengineeringbook.net 460 Java: Soluciones de programación Aquí, tme es el suceso de modelado de árbol. Los nombres implican el tipo de suceso de modelado que ha ocurrido. De éstos, sólo treeNodesChanged( ) se usa en esta solución. TreeModelEvent define varios métodos. Se usan dos en esta solución. El primero se muestra aquí: TreePath getThreePath( ) Este método devuelve la ruta al principal del nodo en cuyo punto ocurrió el cambio. El segundo es Object[ ] getChildren( ) Este método devuelve una matriz que contiene referencias a los nodos que cambiaron. Estos nodos serán secundarios del último nodo en la ruta devuelta por getTreePath( ). Si el valor devuelto es null, entonces el que cambió es el nodo raíz. De otra manera, el nodo o los nodos cambiados se devuelven en una matriz. En el siguiente ejemplo, sólo puede cambiarse un nodo a la vez, de modo que una referencia al nodo cambiado se encuentra en el primer lugar de la matriz devuelta. Como se mencionó, un escucha de modelo de árbol debe registrarse con el modelo del árbol, no con la instancia de JTree. El modelo de árbol se obtiene al llamar a getModel( ) en la instancia de JTree. Aquí se muestra: TreeModel getModel( ) El TreeModel predeterminado es DefaultTreeModel. Es el modelo que se usa si no especifica un modelo propio. JTree soporta tres modos de selección, que se definen con TreeSelectionModel. Aquí se muestran: CONTIGUOUS_TREE_SELECTION DISCONTIGUOUS_TREE_SELECTION SINGLE_TREE_SELECTION Como opción predeterminada, los árboles permiten selección no contigua. Puede cambiar el modo de selección al llamar a setSelectionMode( ) en el modelo de selección del árbol, pasándolo en el nuevo nodo. El modelo se obtiene al llamar a getSelectionModel( ) en la instancia de JTree. Ambos métodos se muestran aquí: TreeSelectionModel getSelectionModel( ) void setSelectionMode(int como) Aquí, como debe ser uno de los valores recién mencionados. Como opción predeterminada, un árbol no es editable. Es decir, el usuario no puede editar el contenido de sus nodos. Para habilitar la edición, llame a setEditable( ), que se muestra aquí: void setEditable(bolean on) Para habilitar la edición, pase verdadero a on. www.fullengineeringbook.net Capítulo 8: Swing 461 Ejemplo En el siguiente ejemplo se demuestra JTree. Crea un pequeño árbol que muestra la derivación de varios componentes de Swing. En este ejemplo, el nodo que representa JComponent es la raíz. Luego se agregan los subárboles que representan los botones y los componentes de texto. // Demuestra JTree. import import import import java.awt.*; javax.swing.*; javax.swing.event.*; javax.swing.tree.*; class DemoArbol { JLabel jetq; DemoArbol( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame("Demo de JTree"); // Especifica FlowLayout como administrador de diseño. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(260, 240); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una etiqueta que desplegará la selección de árbol // y establece su tamaño. jetq = new JLabel( ); jetq.setPreferredSize(new Dimension(230, 50)); // Crea un árbol que muestra la relación entre varias // clases de Swing. // Primero, crea el nodo raíz del árbol. DefaultMutableTreeNode raiz = new DefaultMutableTreeNode("JComponent"); // Luego, crea dos árboles secundarios. Uno empieza con // AbstractrButton y el otro con JTextComponent. // Crea la raíz del subárbol AbstractButton. DefaultMutableTreeNode nodoBtnAbst = new DefaultMutableTreeNode("AbstractButton"); raiz.add(nodoBtnAbst); // agrega al nodo AbstractButton al árbol. // // // // El subárbol AbstractButton tiene dos nodos: JButton y JToggleButton. Bajo JToggleButton están JCheckBox y JRadioButton. Estos nodos se crean con las siguientes instrucciones. www.fullengineeringbook.net 462 Java: Soluciones de programación // Crea los nodos bajo AbstractButton. DefaultMutableTreeNode nodoJbtn = new DefaultMutableTreeNode("JButton"); nodoBtnAbst.add(nodoJbtn); // agrega JButton a AbstractButton DefaultMutableTreeNode nodoJbtnint = new DefaultMutableTreeNode("JToggleButton"); nodoBtnAbst.add(nodoJbtnint); // agrega JToggleButton a AbstractButton // Agrega un subárbol bajo JToggleButton nodoJbtnint.add(new DefaultMutableTreeNode("JCheckBox")); nodoJbtnint.add(new DefaultMutableTreeNode("JRadioButton")); // Ahora, crea un subárbol de JTextComponent. DefaultMutableTreeNode nodoJtextComp = new DefaultMutableTreeNode("JTextComponent"); raiz.add(nodoJtextComp); // agrega JTextComponent a la raíz // Habilita el subárbol de JTextComponent. DefaultMutableTreeNode nodoJcampoTexto = new DefaultMutableTreeNode("JTextField"); nodoJtextComp.add(nodoJcampoTexto); nodoJtextComp.add(new DefaultMutableTreeNode("JTextArea")); nodoJtextComp.add(new DefaultMutableTreeNode("JEditorPane")); // Crea un subárbol bajo JTextField. nodoJcampoTexto.add(new DefaultMutableTreeNode("JFormattedTextField")); nodoJcampoTexto.add(new DefaultMutableTreeNode("JPasswordField")); // Ahora, crea un JTree que usa la estructura // definida por las instrucciones anteriores. JTree jarbol = new JTree(raiz); // Permite que el árbol se edite de modo que puedan // generarse los sucesos del modelo. jarbol.setEditable(true); // Establece el modo de selección del árbol en selección única. TreeSelectionModel tsm = jarbol.getSelectionModel( ); tsm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); // Envuelve el árbol en un panel de desplazamiento. JScrollPane jpandez = new JScrollPane(jarbol); // Establece el tamaño preferido del panel de desplazamiento. jpandez.setPreferredSize(new Dimension(160, 140)); // Escucha sucesos de expansión del árbol. jarbol.addTreeExpansionListener(new TreeExpansionListener( ) { public void treeExpanded(TreeExpansionEvent tee) { // Obtiene la ruta al punto de expansión. TreePath tp = tee.getPath( ); www.fullengineeringbook.net Capítulo 8: Swing // Despliega el nodo jetq.setText("Expansión: " + tp.getLastPathComponent( )); } public void treeCollapsed (TreeExpansionEvent tee) { // Obtiene la ruta al punto de expansión. TreePath tp = tee.getPath( ); // Despliega el nodo jetq.setText("Contracción: " + tp.getLastPathComponent( )); } }); // Escucha tres sucesos de selección. jarbol.addTreeSelectionListener(new TreeSelectionListener( ) { public void valueChanged(TreeSelectionEvent tse) { // Obtiene la ruta a la selección. TreePath tp = tse.getPath( ); // Despliega el nodo seleccionado. jetq.setText("Suceso de selección: " + tp.getLastPathComponent( )); } }); // Escucha sucesos de modelo de árbol. Observa que el // escucha está registrado con el modelo de árbol. jarbol.getModel( ).addTreeModelListener(new TreeModelListener( ) { public void treeNodesChanged(TreeModelEvent tme) { // Obtiene la ruta al principal del nodo que cambió. TreePath tp = tme.getTreePath( ); // Obtiene el secundario del principal del nodo que cambió. Object[ ] secundario = tme.getChildren( ); DefaultMutableTreeNode changedNode; if(secundario != null) changedNode = (DefaultMutableTreeNode) secundario[0]; else changedNode = (DefaultMutableTreeNode) tp.getLastPathComponent( ); // Despliega la ruta. jetq.setText("<html>Model change path: " + tp + "<br>" + "New datos: " + changedNode.getUserObject( )); } www.fullengineeringbook.net 463 464 Java: Soluciones de programación // Vacía implementaciones de los métodos restantes de // TreeModelEvent. Implementa éstas si su aplicación // necesita manejar estas acciones. public void treeNodesInserted(TreeModelEvent tse) {} public void treeNodesRemoved(TreeModelEvent tse) {} public void treeStructureChanged(TreeModelEvent tse) {} }); // Agrega y el árbol y la etiqueta al panel de contenido. jmarco.add(jpandez); jmarco.add(jetq); // Despliega el marco. jmarco.setVisible(true); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new DemoArbol( ); } }); } } Aquí se muestra la salida: Opciones Como JTable, JTree es un control muy complejo que soporta muchas opciones y permite una cantidad importante de personalización (tantas opciones, que no es posible analizarlas todas). Sin embargo, aquí se mencionan algunas de las más populares. Puede hacer que un nodo se vuelva visible (es decir, se traiga a la vista) al llamar a makeVisible( ) en la instancia de JTree. Se muestra aquí: void makeVisible(TreePath rutaANodo) www.fullengineeringbook.net Capítulo 8: Swing 465 La ruta al nodo deseado es especificada por rutaANodo. Por ejemplo, si agrega esta línea al programa anterior inmediatamente después de que se declara jarbol, entonces el nodo JTextField será visible cuando el árbol se despliegue por primera vez: jarbol.makeVisible(new TreePath(nodoJcampoTexto.getPath( ))); En general, un uso común de makeVisible( ) es hacer que el nodo se muestre cuando se despliega por primera vez el árbol. Por ejemplo, si se usa un árbol para presentar información de ayuda, entonces el tema de ayuda solicitado se hace visible en el árbol. Dependiendo del aspecto, el nodo raíz puede o no tener manejadores asociados. (Un manejador es un pequeño icono que indica si una rama está expandida o contraída.) Puede habilitar o deshabilitar los manejadores raíz al llamar a setShowsRootHandles( ), que se muestra aquí: void setShowsRootHandles(boolean on) Si on es verdadero, se muestran los manejadores raíz. De otra manera, no se muestran. Puede modificar el contenido de un árbol al usar uno o más de los métodos proporcionados por DefaultMutableTreeNode. Por ejemplo, puede cambiar el contenido de un nodo al llamar a setUserObject( ), que se muestra aquí: void setUserObject(Object obj) Aquí, obj se vuelve el nuevo objeto asociado con el nodo que invoca. Puede eliminar un nodo al llamar a remove( ) en el nodo principal. Aquí se muestra una forma: void remove(MutableTreeNode nodo) Aquí, nodo debe ser un secundario del nodo que invoca. Puede determinar si un nodo es una hoja al llamar a isLeaf( ) en el nodo. Aquí se muestra: boolean isLeaf( ) Devuelve verdadero si el nodo que invoca es terminal (uno que no tiene nodos secundarios). De lo contrario, devuelve falso. Puede determinar si un nodo es el raíz al llamar a isRoot( ) en el nodo. Aquí se muestra: boolean isRoot( ) Devuelve verdadero si el nodo que invoca es el raíz. De lo contrario, devuelve falso. DefaultMutableTreeNode también definen este conjunto de métodos que realiza varios cortes transversales de los nodos: Enumeration breadthFirstEnumeration( ) Enumeration depthFirstEnumeration( ) Enumeration postorderEnumeration( ) Enumeration preorderEnumeration( ) Cada uno devuelve la enumeración especificada. A partir de la raíz, un corte transversal de primera generación visita todos los nodos en el mismo nivel antes de pasar al siguiente. Un corte transversal de primer nivel (que es el mismo que el posorden transversal), visita primero las hojas y luego la www.fullengineeringbook.net 466 Java: Soluciones de programación raíz de cada subárbol. Un corte transversal de preorden visita la raíz seguido por las hojas de cada subárbol. También puede definir generadores y editores de celdas personalizados para un árbol. Esto le da control sobre la manera en que los elementos del árbol se despliegan y se editan. Cuando se trabaja con árboles, querrá explorar no sólo JTree, sino TreeNode, DefaultMutableTreeNode y TreePath, junto con TreeModel y TreeSelectionModel, para aprovechar por completo las personalizaciones y opciones disponibles. Cree un menú principal Componentes clave Clases e interfaces Métodos java.awt.event.ActionEvent String getActionCommand( ) java.awt.event.ActionListener void actionPerformed(ActionEvent ae) javax.swing.JFrame void setJMenuBar(JMenuBar mb) javax.swing.JMenu JMenuItem add(JMenuItem elemento) void addSeparator( ) javax.swing.JMenuBar JMenu add(JMenu menu) javax.swing.JMenuItem Swing soporta un subsistema extenso dedicado a menús. Por ejemplo, puede crear un menú principal, uno emergente y una barra de herramientas. También puede agregar aceleradores de teclado, teclas mnemotécnicas e imágenes, y puede usar elementos de menú de estilo de botón y casilla de verificación. Francamente, podría dedicarse un libro completo al subsistema de menús de Swing. Por tanto, no es posible detallarlos aquí. Sin embargo, en esta solución se muestra cómo crear uno de los menús más importantes: el menú principal. Muchas de las técnicas descritas aquí también son aplicables a otros tipos de menús. Como la mayoría de los lectores sabe, el principal menú de una aplicación se despliega en la barra de menús del marco principal de la aplicación. Suele desplegarse a lo largo de la parte superior del marco de la aplicación, y es el menú que define toda (o casi toda) la funcionalidad de una aplicación. Por ejemplo, el menú principal incluirá elementos como Archivo, Edición y Ayuda. Swing define varias clases relacionadas con menús. Aquí se muestran las usadas en esta solución: JMenuBar Un objeto que contiene el menú de nivel superior para la aplicación. JMenu Un menú estándar. Un menú consta de uno o más elementos de JMenuItem. JMenuItem Un objeto que llena los menús. www.fullengineeringbook.net Capítulo 8: Swing 467 Estas clases funcionan de manera conjunta para forma el menú principal de la aplicación. JMenuBar es, hablando de manera general, un contenedor de menús. Una instancia de JMenuBar tiene una o más instancias de JMenu. Cada objeto de JMenu define un menú. Es decir, cada objeto de JMenu contiene uno o más elementos seleccionables. Los menús desplegados por JMenu son objetos de JMenuItem. Por tanto, un JMenuItem define una selección que puede ser elegida por un usuario. Otro elemento clave es que JMenuItem es una superclase de JMenu. Esto permite la creación de submenús, que son, en esencia, menús dentro de menús. Para crear un submenú, primero cree y llene un objeto de JMenu y luego agréguelo a otro. El sistema de menús de Swing también depende de dos interfaces importantes: SingleSelectionModel y MenuElement. SingleSelectionModel determina las acciones de un componente que contiene varios elementos, pero de los cuales sólo puede seleccionarse uno y sólo uno en cualquier momento. Este modelo lo usa JMenuBar. Hay una implementación predeterminada llamada DefaultSingleSelectionModel. La interfaz MenuElement define la naturaleza de un elemento de menú. En general, no necesitará interactuar con estas interfaces directamente a menos que este personalizando el sistema de menús. Paso a paso Para crear y usar una barra de menús principal se requieren los siguientes pasos: 1. Cree una instancia de JMenuBar. 2. Cree una instancia de JMenu para cada menú de nivel superior de la barra. 3. Cree una instancia de JMenuItem que representa los elementos de los menús. 4. Agregue instancias de JMenuITem a una de JMenu al llamar a add( ) en el JMenu. Si lo desea, separe un elemento del siguiente al llamar a addseparator( ) en la instancia de JMenu. 5. Repita del paso 2 al 4 hasta que se hayan creado los menús de nivel superior. 6. Agregue cada instancia de JMenu a la barra de menús al llamar a add( ) en la instancia de JMenuBar. 7. Agregue la barra de menús al marco, al llamar a setMenuBar( ) en la instancia de JFrame. 8. Se genera un ActionEvent cada vez que el usuario selecciona un elemento de un menú. Para manejar estos sucesos, registre un escucha de acción para cada elemento de menú. Análisis JMenuBar es, en esencia, un contenedor de menús. Como todos los componentes, hereda JComponent (que hereda Container y Component). Sólo tiene un constructor, que es el constructor predeterminado. Por tanto, al principio la barra de menús estará vacía y necesitará llenarla con menús antes de usarla. Cada aplicación tiene una sola barra de menús. JMenuBar define varios métodos, pero por lo general sólo necesitará usar uno: add( ). El método add( ) agrega un JMenu a la barra de menús. Aquí se muestra: JMenu add(JMenu menu) Aquí, menu es una instancia de JMenu que se agrega a la barra de menús. Se devuelve una referencia al menú. Los menús se ubican en la barra de izquierda a derecha, en el orden en que se agregan. www.fullengineeringbook.net 468 Java: Soluciones de programación JMenu encapsula un menú, que es llenado con elementos de JMenuItem. Se deriva de JMenuItem. Esto significa que un JMenu puede ser una selección de otro JMenu. Esto permite que un submenú sea submenú de otro. JMenu define varios constructores. El usado aquí es JMenu(String nombre) Crea un menú que tiene el título especificado por nombre. Para agregar un elemento al menú, use el método add( ). Tiene varias formas. Aquí se muestra la usada en esta solución: JMenuItem add(jMenuItem elemento) Aquí, elemento es el elemento del menú que habrá de agregarse. Se agrega al final del menú. Puede agregar un separador (un objeto de tipo JSeparator) a un menú al llamar a addSeparator( ), que se muestra aquí: void addSeparator( ) El separador se agrega al final del menú. Como opción predeterminada, un separador es una barra horizontal. JMenuItem encapsula un elemento en un menú. Este elemento puede ser una selección vinculada a alguna acción de programa, como Guardar o Cerrar, o puede causar que se despliegue un submenú. JMenuItem define varios constructores. Aquí se muestra el usado en esta solución: JMenuItem(String nombre) Crea un elemento de menú con el nombre especificado por nombre. Una clave es que JMenuItem extiende AbstractButton. Recuerde que ésta también es la superclase de todos los componentes de botón de Swing, como JButton. Por tanto, aunque tienen un aspecto distinto, todos los elementos de menú son, en esencia, botones. Por ejemplo, al seleccionar un elemento de menú se genera un suceso de acción de la misma manera que al presionar un botón. Una vez que se ha creado y llenado una barra de menús. Se agrega a un JFrame al llamar a setJMenuBar( ) en la instancia de JFrame. (Las barras de menú no se agregan al panel de contenido.) aquí se muestra el método setJMenuBar( ): void setJMenuBar(JMenuBar mb) Aquí, mb es una referencia a la barra de menús. Ésta se desplegará en una posición determinada por el aspecto y percepción. Por lo general, será en la parte superior de la ventana. Cuando se selecciona un elemento de menú, se genera un suceso de acción. El ActionEvent generado por un elemento de menú es similar al generado por un JButton. Para conocer detalles sobre el manejo de sucesos de acción, consulte Cree un botón simple. La cadena de comandos de acción asociada con ese suceso de acción será, como opción predeterminada, el nombre de la selección. (La cadena de comandos de acción puede obtenerse al llamar a getActionCommand( ) en el objeto de sucesos de acción). Por tanto, puede determinar cuál elemento fue seleccionado al examinar la cadena de comandos de acción. Por supuesto, también puede reconocer el origen de un suceso al llamar a getSource( ) en el objeto del suceso. También puede usar una clase interna anónima separada para manejar los sucesos de acción de cada elemento de menú. Sin embargo, esté consciente de que los sistemas de menús tienden a ser muy grandes. El uso de una clase separada a fin de manejar sucesos para cada elemento de menú puede causar que se cree una gran cantidad de clases, lo que puede llevar a ineficacias en tiempo de ejecución. www.fullengineeringbook.net Capítulo 8: Swing 469 Ejemplo He aquí un programa que crea una barra de menús simple que contiene tres menús. El primero es un menú Archivo estándar que incluye las selecciones Abrir, Cerrar, Guardar y Salir. El segundo menú recibe el nombre Opciones y contiene dos submenús denominados Id y Destino. El tercer menú es Ayuda y tiene un elemento: Acerca de. Cuando se selecciona un elemento, se despliega el nombre de la selección en una etiqueta en el panel de contenido. // Crea un menú principal. import java.awt.*; import java.awt.event.*; import javax.swing.*; class CrearMenuPrincipal implements ActionListener { JLabel jetq; CrearMenuPrincipal( ) { // Crea un nuevo contenedor de JFrame. JFrame jmarco = new JFrame(«Demo de menús»); // Especifica FlowLayout como administrador de diseño. jmarco.setLayout(new FlowLayout( )); // Da al marco un tamaño inicial. jmarco.setSize(220, 200); // Termina el programa cuando el usuario cierra la aplicación. jmarco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Crea una nueva etiqueta que desplegará la selección de menú. jetq = new JLabel( ); // Crea la barra de menús. JMenuBar jbm = new JMenuBar( ); // Crea el menú Archivo. JMenu jmArchivo = new JMenu("Archivo"); JMenuItem jemAbrir = new JMenuItem("Abrir"); JMenuItem jemCerrar = new JMenuItem("Cerrar"); JMenuItem jemGuardar = new JMenuItem("Guardar"); JMenuItem jemSalir = new JMenuItem("Salir"); jmArchivo.add(jemAbrir); jmArchivo.add(jemCerrar); jmArchivo.add(jemGuardar); jmArchivo.addSeparator( ); jmArchivo.add(jemSalir); jbm.add(jmArchivo); // Crea el menú Opciones. JMenu jmOpciones = new JMenu("Opciones"); www.fullengineeringbook.net 470 Java: Soluciones de programación // Crea el submenú Lenguajes. JMenu jmLenguaje = new JMenu("Lenguaje"); JMenuItem jemJava = new JMenuItem("Java"); JMenuItem jemCpp = new JMenuItem("C++"); JMenuItem jemCsharp = new JMenuItem("C#"); jmLenguaje.add(jemJava); jmLenguaje.add(jemCpp); jmLenguaje.add(jemCsharp); jmOpciones.add(jmLenguaje); // Crea el submenú Destino. JMenu jmDestino = new JMenu("Destino"); JMenuItem jmDepurar = new JMenuItem("Depurar"); JMenuItem jmDesplegar = new JMenuItem("Despliegue"); jmDestino.add(jmDepurar); jmDestino.add(jmDesplegar); jmOpciones.add(jmDestino); // Por último, agrega el menú Opciones completo a la barra de menús. jbm.add(jmOpciones); // Crea el menú Ayuda. JMenu jmAyuda = new JMenu("Ayuda"); JMenuItem jemAcerca = new JMenuItem("Acerca de"); jmAyuda.add(jemAcerca); jbm.add(jmAyuda); // Agrega escuchas de acción a los elementos de menú. // Usa un manejador para todos los sucesos de menú. jemAbrir.addActionListener(this); jemCerrar.addActionListener(this); jemGuardar.addActionListener(this); jemSalir.addActionListener(this); jemJava.addActionListener(this); jemCpp.addActionListener(this); jemCsharp.addActionListener(this); jmDepurar.addActionListener(this); jmDesplegar.addActionListener(this); jemAcerca.addActionListener(this); // Agrega la etiqueta al panel de contenido. jmarco.add(jetq); // Agrega la barra de menús al marco. jmarco.setJMenuBar(jbm); // Despliega el marco. jmarco.setVisible(true); } // Maneja sucesos de acción de elementos de menú. public void actionPerformed(ActionEvent ae) { www.fullengineeringbook.net Capítulo 8: Swing 471 // Obtiene el comando de acción de la selección de menú. String cadComds = ae.getActionCommand( ); // Si el usuario elige Salir, entonces sale del programa. if(cadComds.equals("Salir")) System.exit(0); // De otra manera, despliega la selección. jetq.setText(cadComds + " Seleccionado"); } public static void main(String args[ ]) { // Crea el marco en el subproceso que despacha sucesos. SwingUtilities.invokeLater(new Runnable( ) { public void run( ) { new CrearMenuPrincipal( ); } }); } } La salida de ejemplo se muestra aquí. Opciones Como se estableció al principio de la solución, el tema de los menús es muy extenso. Permiten muchas opciones, tienen muchas variaciones y ofrecen muchas oportunidades para la personalización. Está más allá del alcance de esta solución analizarlos todos. Sin embargo, aquí se mencionan algunas de las opciones de uso más común. Además de los elementos de menú "estándar", también puede incluir casillas de verificación y botones de opción en un menú. Un elemento de menú de casilla de verificación se crea con JCheckBoxMenuItem. Un menú de botón de opción se crea con JRadioButtonmenuItem. Ambas clases extienden JMenuItem. JToolBar crea un componente independiente que está relacionado con el menú. Suele usarse para proporcionar acceso a la funcionalidad contenida dentro de los menús de la aplicación. Por ejemplo, una barra de herramientas podría proporcionar acceso rápido a los comandos de formato soportados por un procesador de palabras. Como se describió antes, como opción predeterminada, cuando agrega JMenuItem a JMenu, el elemento se incluye al final del menú. Sin embargo, al usar la siguiente versión de add( ) puede agregar un elemento de menú a un menú en una ubicación específica. Component add(Component elementoMenu, int ind) www.fullengineeringbook.net 472 Java: Soluciones de programación Aquí, se agrega en el índice especificado por ind. La indización empieza en 0. Por ejemplo, en el programa anterior, la siguiente instrucción hace que Despliegue sea la primera entrada del menú Destino: jmDestino.add(jemDesplegar, 0); Aunque elementoMenu se declara como un Component, por lo general agregará elementos de JMenuItem a un menú. (Esta versión de add( ) se hereda de Component. Como se mencionó al principio de este capítulo, todos los componentes de Swing heredan JComponent, que hereda Component.) En algunos casos tal vez quiera eliminar un elemento de menú que ya no se necesite. Puede hacer esto al llamar a remove( ) en la instancia de JMenu. Tiene estas dos formas. void remove(JMenuItem elementoMenu) void remove(int ind) Aquí, elementoMenu es una referencia al elemento de menú que habrá de eliminarse e ind es el índice el menú que se habrá de eliminar. La indización empieza en 0. JMenuBar también soporta versiones de add( ) y remove( ) que le permiten agregar un objeto de JMenu a la barra en un índice específico o eliminar una instancia de JMenu de la barra. En ocasiones es útil saber cuántos elementos se encuentran en la barra de menús, para obtener esta cuenta de los elementos de la barra principal, llame a getMenuCount( ), de la instancia de JMenuBar. Aquí se muestra: int getMenuCount( ) Devuelve el número de elementos contenidos dentro de la barra de menús. Puede determinar cuántos elementos hay en un JMenu al llamar a getMenuComponentCount( ), que se muestra aquí: int getMenuComponentCount( ) Se devuelve la cuenta. Puede recuperar una matriz de los elementos en el menú al llamar a getMenuComponents( ) en la instancia de JMenu. Se muestra a continuación: Component[ ] getMenuComponents( ) Se devuelve una matriz que contiene los componentes. Debido a que los elementos de menú heredan AbstractButton, tiene acceso a la funcionalidad proporcionada por AbstractButton. Por ejemplo, puede habilitar/deshabilitar un elemento de menú al llamar a setEnabled( ), que se muestra aquí: void setEnabled(boolean habilitar) Si habilitar es verdadero, el elemento de menú está habilitado. Si es falso, estará deshabilitado y no podrá seleccionarse. El menú principal no es el único tipo de menú que puede crearse. También pueden crearse menús independientes, emergentes. Se trata de menús que no descienden de una barra de menús. Más bien, son activados de manera independiente, por lo general con el botón derecho del ratón. Los menús emergentes son instancias de JPopupMenu. www.fullengineeringbook.net 9 CAPÍTULO Miscelánea U no de los problemas con la escritura de un libro de este tipo, es encontrar un punto apropiado para detenerse. Hay un universo casi ilimitado de temas para elegir, cualquier cantidad de ellos merece su inclusión. Es difícil encontrar dónde trazar la línea. Por supuesto, todos los libros deben terminar. Por tanto, siempre es obligatorio un punto de llegada, sea fácil encontrarlo o no. Este libro, por supuesto, no es la excepción. En este, el capítulo final del libro, he decidido concluir con una serie de soluciones que abarcan diversos temas. Estas soluciones representan técnicas que quise incluir en el libro, pero para las cuales un capítulo completo no era posible, por una razón u otra. Sin embargo, las soluciones cumplen los requisitos que establecí cuando empecé este libro: responden una pregunta frecuente y son aplicables a un amplio rango de programadores. Más aún, todas describen conceptos clave que puede adaptar y mejorar fácilmente. He aquí las soluciones contenidas en este capítulo: • Acceda a un recurso mediante una conexión HTTP. • Use un semáforo. • Devuelva un valor de un subproceso. • Use reflexión para obtener información acerca de una clase en tiempo de ejecución. • Use reflexión para crear dinámicamente un objeto y llamar métodos. • Cree una clase personalizada de excepción. • Calendarice una tarea para ejecución futura. 473 www.fullengineeringbook.net 474 Java: Soluciones de programación Acceda a un recurso mediante una conexión HT