Uploaded by OMAR ALEJANDRO BERMUDEZ CACERES

Java Soluciones de programación - Herbert Schildt

advertisement
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
Download