Java: Manual De Referencia, 7ma Edición Java Herbert Schildt FREELIBROS.ORG
User Manual:
Open the PDF directly: View PDF
.
Page Count: 1059
| Download | |
| Open PDF In Browser | View PDF |
Java
Manual de referencia
www.detodoprogramacion.com
Acerca del autor
Herbert Schildt Es la máxima autoridad en los
lenguajes de programación Java, C, C++ y C#.
Sus libros de programación han vendido más de
3.5 millones de copias en todo el mundo y se han
traducido a la mayoría de los idiomas. Es autor de
Manual de referencia de C#, Manual de referencia de C,
El arte de programar en Java, Fundamentos de Java y
Java 2 Manual de referencia entre otros best sellers.
Schildt es egresado y posgraduado de la University of
Illinois. Se le puede contactar en la oficina de su
consultoría, (217) 586-4683. Su sitio Web es:
www.HerbSchildt.com.
www.detodoprogramacion.com
Java
Manual de referencia,
Séptima edición
Herbert Schildt
Traducción
Javier González Sánchez
Tecnológico de Monterrey Campus Guadalajara
Rosana Ramos Morales
Universidad de Guadalajara
MÉXICO BOGOTÁ BUENOS AIRES CARACAS GUATEMALA
LISBOA MADRID NUEVA YORK SAN JUAN SANTIAGO
AUCKLAND LONDRES MILÁN SÃO PAULO MONTREAL NUEVA DELHI
SAN FRANCISCO SINGAPUR SAN LUIS SIDNEY TORONTO
www.detodoprogramacion.com
Director Editorial: Fernando Castellanos Rodríguez
Editor de desarrollo: Miguel Ángel Luna Ponce
Supervisora de producción: Jacqueline Brieño Álvarez
Formación: Overprint, S.A. de C.V.
Java Manual de referencia
Séptima edició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 séptima 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
ISBN 13: 978-970-10-6288-3
ISBN 10: 970-10-6288-4
Translated from the 7th English edition of
Java: The Complete Reference
By: Herbert Schildt
Copyright © 2007 by The McGraw-Hill Companies. All rights reserved.
ISBN-10: 0-07-226385-7
ISBN-13: 978-0-07-226385-5
7890123456
8765432109
Impreso en México
Printed in Mexico
www.detodoprogramacion.com
Resumen del contenido
Parte I
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Parte II
15
16
17
18
19
20
21
22
23
24
25
26
27
El Lenguaje Java
Historia y evolución de Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducción a Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Tipos de dato, variables y arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Sentencias de control. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Métodos y clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Paquetes e interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Gestión de excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Programación multihilo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Enumeraciones, autoboxing y anotaciones (metadatos) . . . . . . . . . . . . . . . . . . . . . . .
E/S, applets y otros temas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Tipos parametrizados. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
15
33
57
77
105
125
157
183
205
223
255
285
315
La biblioteca de Java
Gestión de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Explorando java.lang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
java.util parte 1: colecciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
java.util parte 2: más clases de utilería . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Entrada/salida: explorando java.io . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Trabajo en red . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La clase Applet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Gestión de eventos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
AWT: Trabajando con ventanas, gráficos y texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
AWT: Controles, gestores de organización y menús . . . . . . . . . . . . . . . . . . . . . . . . . . .
Imágenes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Utilerías para concurrencia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
NES, expresiones regulares y otros paquetes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
359
385
437
503
555
599
617
637
663
701
755
787
813
v
www.detodoprogramacion.com
vi
Java:
Manual de referencia
Parte III Desarrollo de software utilizando Java
28
29
30
31
Java Beans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducción a Swing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Explorando Swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Servlets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
847
859
879
907
Parte IV Aplicaciones en Java
32
33
A
Applets y servlets aplicados en la solución de problemas . . . . . . . . . . . . . . . . . . . . . .
Creando un administrador de descargas en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Usando los comentarios de documentación de Java . . . . . . . . . . . . . . . . . . . . . . . . . . .
931
965
991
Índice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
997
www.detodoprogramacion.com
Contenido
Prefacio ...........................................................................................................................
Parte I
xxix
El lenguaje Java
1
Historia y evolución de Java .....................................................................................
Linaje de Java .................................................................................................................
El nacimiento de la programación moderna: C ..............................................
El Siguiente Paso: C++ ......................................................................................
Todo está dispuesto para Java ...........................................................................
La creación de Java ........................................................................................................
La conexión con C# ...........................................................................................
Cómo Java cambió al Internet ......................................................................................
Java applets ........................................................................................................
Seguridad ...........................................................................................................
Portabilidad ........................................................................................................
La magia de Java: el bytecode .......................................................................................
Servlets: Java en el lado del servidor ............................................................................
Las cualidades de Java ...................................................................................................
Simple.................................................................................................................
Orientado a objetos ...........................................................................................
Robusto ..............................................................................................................
Multihilo .............................................................................................................
Arquitectura neutral ..........................................................................................
Interpretado y de alto rendimiento ..................................................................
Distribuido .........................................................................................................
Dinámico ............................................................................................................
La evolución de Java ......................................................................................................
Java SE 6 .............................................................................................................
Una cultura de innovación ............................................................................................
3
3
3
5
6
6
8
8
8
9
9
9
10
11
11
11
11
12
12
12
12
13
13
14
14
2
Introducción a Java .....................................................................................................
Programación orientada a objetos ...............................................................................
Dos paradigmas .................................................................................................
Abstracción ........................................................................................................
Los tres principios de la programación orientada a objetos ..........................
Un primer programa sencillo........................................................................................
Escribiendo el programa ...................................................................................
Compilando el programa ..................................................................................
Análisis detallado del primer programa de prueba ........................................
Un segundo programa breve ........................................................................................
15
15
15
16
16
21
21
22
22
24
vii
www.detodoprogramacion.com
viii
Java:
3
Manual de referencia
Dos sentencias de control .............................................................................................
La sentencia if ....................................................................................................
El ciclo for ...........................................................................................................
Utilizando bloques de código .......................................................................................
Cuestiones de léxico ......................................................................................................
Espacios en blanco ............................................................................................
Identificadores ...................................................................................................
Literales ..............................................................................................................
Comentarios ......................................................................................................
Separadores........................................................................................................
Palabras clave de Java ........................................................................................
La biblioteca de clases de Java ......................................................................................
26
26
27
29
30
30
30
31
31
31
31
32
Tipos de dato, variables y arreglos ..........................................................................
Java es un lenguaje fuertemente tipificado ..................................................................
Los tipos primitivos .......................................................................................................
Enteros............................................................................................................................
byte .....................................................................................................................
short....................................................................................................................
int ........................................................................................................................
long .....................................................................................................................
Tipos con punto decimal ...............................................................................................
float .....................................................................................................................
double .................................................................................................................
Caracteres .......................................................................................................................
Booleanos .......................................................................................................................
Una revisión detallada de los valores literales.............................................................
Literales enteros ................................................................................................
Literales con punto decimal .............................................................................
Literales booleanos............................................................................................
Literales de tipo carácter ...................................................................................
Literales de tipo cadena ....................................................................................
Variables .........................................................................................................................
Declaración de una variable .............................................................................
Inicialización dinámica......................................................................................
Ámbito y tiempo de vida de las variables ........................................................
Conversión de tipos.......................................................................................................
Conversiones automáticas de Java ...................................................................
Conversión de tipos incompatibles ..................................................................
Promoción automática de tipos en las expresiones ....................................................
Reglas de la promoción de tipos ......................................................................
Arreglos ..........................................................................................................................
Arreglos unidimensionales ...............................................................................
Arreglos multidimensionales............................................................................
Sintaxis alternativa para la declaración de arreglos ........................................
33
33
33
34
34
35
35
35
36
36
36
37
38
39
39
40
40
40
40
41
41
42
42
45
45
45
47
47
48
48
51
55
www.detodoprogramacion.com
ix
Contenido
Unas breves notas sobre las cadenas ...........................................................................
Una nota para los programadores de C/C++ sobre los apuntadores ........................
55
56
4
Operadores ...................................................................................................................
Operadores aritméticos .................................................................................................
Operadores aritméticos básicos .......................................................................
El operador de módulo .....................................................................................
Operadores aritméticos combinados con asignación .....................................
Incremento y decremento .................................................................................
Operadores a nivel de bit ..............................................................................................
Operadores lógicos a nivel de bit .....................................................................
Desplazamiento a la izquierda .........................................................................
Desplazamiento a la derecha............................................................................
Desplazamiento a la derecha sin signo ...........................................................
Operadores a nivel de bit combinados con asignación ..................................
Operadores relacionales................................................................................................
Operadores lógicos booleanos .....................................................................................
Operadores lógicos en cortocircuito ................................................................
El operador de asignación.............................................................................................
El operador ? ..................................................................................................................
Precedencia de operadores ...........................................................................................
El uso de paréntesis .......................................................................................................
57
57
58
59
59
60
62
63
65
66
68
69
70
71
72
73
73
74
74
5
Sentencias de control..................................................................................................
Sentencias de selección ................................................................................................
If ..........................................................................................................................
switch .................................................................................................................
Sentencias de iteración ................................................................................................
while ...................................................................................................................
do-while .............................................................................................................
for........................................................................................................................
La versión for-each del ciclo for .......................................................................
Ciclos anidados ..................................................................................................
Sentencias de salto ........................................................................................................
break ...................................................................................................................
continue..............................................................................................................
return ..................................................................................................................
77
77
77
80
84
84
86
88
92
97
98
98
102
103
6
Clases ............................................................................................................................
Fundamentos de clases .................................................................................................
La forma general de una clase ..........................................................................
Una clase simple ................................................................................................
Declaración de objetos ..................................................................................................
El operador new ................................................................................................
Asignación de variables de referencia a objetos..........................................................
Métodos..........................................................................................................................
Adición de un método a la clase Caja ..............................................................
105
105
105
106
109
109
111
111
112
www.detodoprogramacion.com
x
Java:
Manual de referencia
Devolución de un valor .....................................................................................
Métodos con parámetros ..................................................................................
Constructores .................................................................................................................
Constructores con parámetros .........................................................................
La palabra clave this ......................................................................................................
Ocultando variables de instancia .....................................................................
Recolección automática de basura ..............................................................................
El método finalize( ) .....................................................................................................
Una clase stack ..............................................................................................................
114
115
117
119
120
120
121
121
122
7
Métodos y clases..........................................................................................................
Sobrecarga de métodos .................................................................................................
Sobrecarga de constructores .............................................................................
Uso de objetos como parámetros .................................................................................
Paso de argumentos .....................................................................................................
Devolución de objetos ...................................................................................................
Recursividad ...................................................................................................................
Control de acceso ..........................................................................................................
static ............................................................................................................................
final ............................................................................................................................
Más información sobre arreglos ...................................................................................
Introducción a clases anidadas y clases interiores ......................................................
La clase string ................................................................................................................
Argumentos en la línea de órdenes .............................................................................
Argumentos de tamaño variable ..................................................................................
Sobrecarga de métodos con argumentos de tamaño variable .......................
Argumentos de tamaño variable y ambigüedad .............................................
125
125
128
130
132
134
135
137
141
143
143
145
148
150
151
154
155
8
Herencia ........................................................................................................................
Fundamentos de la herencia.........................................................................................
Acceso a miembros y herencia .........................................................................
Un ejemplo más práctico ..................................................................................
Una variable de una superclase puede referenciar
a un objeto de tipo subclase .....................................................................
super ............................................................................................................................
Usando super para llamar a constructores de superclase ..............................
Un segundo uso de super .................................................................................
Creación de una jerarquía multinivel ...........................................................................
Cuándo son ejecutados los constructores ...................................................................
Sobrescritura de métodos .............................................................................................
Selección dinámica de métodos ...................................................................................
¿Por qué se sobrescriben los métodos? ...........................................................
Aplicación de la sobrescritura de métodos ......................................................
Clases abstractas ............................................................................................................
Uso del modificador final con herencia .......................................................................
Uso del modificador final para impedir la sobrescritura ................................
Uso del modificador final para evitar la herencia ...........................................
La clase object ................................................................................................................
157
157
159
160
www.detodoprogramacion.com
161
162
163
166
167
170
171
173
175
175
177
180
180
180
181
xi
Contenido
9
Paquetes e interfaces ..................................................................................................
Paquetes..........................................................................................................................
Definición de paquete .......................................................................................
Localización de paquetes y CLASSPATH .......................................................
Ejemplo de un paquete .....................................................................................
Protección de acceso .....................................................................................................
Ejemplo de acceso .............................................................................................
Importar paquetes .........................................................................................................
Interfaces ........................................................................................................................
Definición de una interfaz ................................................................................
Implementación de interfaces ..........................................................................
Interfaces anidadas ............................................................................................
Utilizando interfaces .........................................................................................
Variables en interfaces .......................................................................................
Las Interfaces se pueden extender ...................................................................
183
183
184
184
185
186
187
190
192
193
194
196
197
200
202
10
Gestión de excepciones ..............................................................................................
Fundamentos de la gestión de excepciones ................................................................
Tipos de excepciones .....................................................................................................
Excepciones no capturadas ...........................................................................................
Utilizando try y catch ....................................................................................................
Descripción de una excepción ..........................................................................
Cláusulas catch múltiples .............................................................................................
Sentencias try anidadas.................................................................................................
throw ............................................................................................................................
throws ............................................................................................................................
finally ............................................................................................................................
Excepciones integradas en Java ....................................................................................
Creando excepciones propias .......................................................................................
Excepciones encadenadas .............................................................................................
Utilizando excepciones .................................................................................................
205
205
206
206
207
209
209
211
213
214
216
217
219
221
222
11
Programación multihilo .............................................................................................
El modelo de hilos en Java ............................................................................................
Prioridades en hilo ............................................................................................
Sincronización ...................................................................................................
Intercambio de mensajes ..................................................................................
La clase Thread y la interfaz Runnable ............................................................
El hilo principal ..............................................................................................................
Creación de un hilo .......................................................................................................
Implementación de la interfaz Runnable ........................................................
Extensión de la clase Thread .............................................................................
Elección de una de las dos opciones ................................................................
Creación de múltiples hilos ..........................................................................................
Uso de isAlive( ) y join( )...............................................................................................
Prioridades de los Hilos ................................................................................................
Sincronización ...............................................................................................................
223
224
224
225
226
226
226
228
228
230
232
232
233
236
238
www.detodoprogramacion.com
xii
Java:
Manual de referencia
Métodos sincronizados .....................................................................................
La sentencia synchronized ...............................................................................
Comunicación entre hilos .............................................................................................
Bloqueos .............................................................................................................
Suspensión, reanudación y finalización de hilos ........................................................
Suspensión, reanudación y finalización de hilos
con Java 1.1 y versiones anteriores ..........................................................
La forma moderna de suspensión, reanudación
y finalización de hilos ...............................................................................
Programación multihilo ................................................................................................
239
241
242
247
249
12
Enumeraciones, autoboxing y anotaciones (metadatos) ......................................
Enumeraciones ..............................................................................................................
Fundamentos de las enumeraciones ...............................................................
Los métodos values ( ) y valuesOf ( )...............................................................
Las enumeraciones en Java son tipos de clase ................................................
Las enumeraciones heredan de la clase enum................................................
Otro ejemplo con enumeraciones ....................................................................
Envoltura de tipos ..........................................................................................................
Autoboxing.....................................................................................................................
Autoboxing y métodos ......................................................................................
Autoboxing en expresiones ..............................................................................
Autoboxing en valores booleanos y caracteres ...............................................
Autoboxing y la prevención de errores ............................................................
Una advertencia sobre el uso autoboxing........................................................
Anotaciones (metadatos) ..............................................................................................
Fundamentos de las anotaciones .....................................................................
Especificación de la política de retención ........................................................
Obtención de anotaciones en tiempo de ejecución........................................
La interfaz annotatedElement ..........................................................................
Utilizando valores por omisión ........................................................................
Anotaciones de marcado ..................................................................................
Anotaciones de un solo miembro ....................................................................
Anotaciones predefinidas en Java ....................................................................
Restricciones para las anotaciones ...................................................................
255
255
255
258
259
261
263
264
266
267
268
270
271
271
272
272
273
273
278
279
280
281
282
284
13
E/S, applets y otros temas .........................................................................................
Fundamentos de E/S ....................................................................................................
Flujos ..................................................................................................................
Flujos de bytes y flujos de caracteres ...............................................................
Flujos predefinidos ............................................................................................
Entrada por consola.......................................................................................................
Lectura de caracteres .........................................................................................
Lectura de cadenas ............................................................................................
Salida por consola .........................................................................................................
285
285
285
286
288
288
289
290
292
www.detodoprogramacion.com
249
251
254
Contenido
14
xiii
La clase PrintWriter .......................................................................................................
Lectura y escritura de archivos .....................................................................................
Fundamentos de Applets ..............................................................................................
Los modificadores transient y volatile .........................................................................
instanceof .......................................................................................................................
strictfp ............................................................................................................................
Métodos nativos ............................................................................................................
Problemas con los métodos nativos.................................................................
assert ............................................................................................................................
Opciones para activar y desactivar la aserción ................................................
Importación estática de clases e interfaces ..................................................................
Invocación de constructores sobrecargados con la palabra clave this( ) ...................
292
293
296
299
300
302
302
306
306
309
309
312
Tipos parametrizados .................................................................................................
¿Qué son los tipos parametrizados? ............................................................................
Un ejemplo sencillo con tipos parametrizados ...........................................................
Los tipos parametrizados sólo trabajan con objetos.......................................
Los tipos parametrizados se diferencian por el tipo de sus argumentos ......
Los tipos parametrizados son una mejora a la seguridad ..............................
Una clase con tipos parametrizados con dos tipos como parámetro ........................
La forma general de una clase con tipos parametrizados ..........................................
Tipos delimitados...........................................................................................................
Utilizando argumentos comodines ..............................................................................
Comodines delimitados ....................................................................................
Métodos con tipos parametrizados ..............................................................................
Constructores con tipos parametrizados .........................................................
Interfaces con tipos parametrizados ............................................................................
Compatibilidad entre el código de versiones anteriores
y los tipos parametrizados ......................................................................................
Jerarquía de clases con tipos parametrizados..................................................
Superclases con tipos parametrizados .............................................................
Subclases con tipos parametrizados ................................................................
Comparación de tipos en tiempo de ejecución ...............................................
Conversión de tipos ..........................................................................................
Sobrescritura de métodos en clases con tipos parametrizados .....................
Cómo están implementados los tipos parametrizados ..............................................
Métodos puente.................................................................................................
Errores de ambigüedad .................................................................................................
Restricciones de los tipos parametrizados ...................................................................
Los tipos parametrizados no pueden ser instanciados ..................................
Restricciones en miembros estáticos ...............................................................
Restricciones en arreglos con tipos parametrizados .......................................
Restricciones en excepciones con tipos parametrizados ................................
Comentarios adicionales sobre tipos parametrizados ................................................
315
316
316
320
320
320
323
324
324
327
329
334
336
337
www.detodoprogramacion.com
339
342
342
344
345
348
348
349
351
353
354
354
354
355
356
356
xiv
Java:
Parte II
15
Manual de referencia
La biblioteca de Java
Gestión de cadenas .....................................................................................................
Los constructores String ...............................................................................................
Longitud de una cadena ...............................................................................................
Operaciones especiales con cadenas ...........................................................................
Literales de cadena ............................................................................................
Concatenación de cadenas ...............................................................................
Concatenación de cadenas con otros tipos de datos ......................................
Conversión de cadenas y toString( ) ................................................................
Extracción de caracteres ................................................................................................
charAt( ) .............................................................................................................
getChars( ) .........................................................................................................
getBytes( ) ..........................................................................................................
toCharArray( )....................................................................................................
Comparación de cadenas ..............................................................................................
equals( ) y equalsIgnoreCase( ) ........................................................................
regionMatches( )................................................................................................
startsWith( ) y endsWith( )................................................................................
Comparando equals( ) con el Operador == ...................................................
compareTo( ) ......................................................................................................
Búsqueda en las Cadenas .............................................................................................
Modificación de una cadena .........................................................................................
substring( ) .........................................................................................................
concat( ) ..............................................................................................................
replace( ).............................................................................................................
trim( )..................................................................................................................
Conversión de datos mediante valueOf( )...................................................................
Cambio entre mayúsculas y minúsculas dentro de una cadena ................................
Otros métodos para trabajar con cadenas ...................................................................
StringBuffer ....................................................................................................................
Constructores StringBuffer ...............................................................................
length( ) y capacity( ).........................................................................................
ensureCapacity( )...............................................................................................
setLength( ) ........................................................................................................
charAt( ) y setCharAt( ) ....................................................................................
getChars( ) .........................................................................................................
append( ) ............................................................................................................
insert( ) ...............................................................................................................
reverse( ) .............................................................................................................
delete( ) y deleteCharAt( ) ................................................................................
replace( ).............................................................................................................
substring( ) .........................................................................................................
Otros métodos para trabajar con StringBuffer ................................................
StringBuilder ..................................................................................................................
www.detodoprogramacion.com
359
360
362
362
362
363
363
364
365
365
365
366
366
366
367
367
368
368
369
370
372
372
373
373
373
374
375
376
377
377
378
378
379
379
379
380
381
381
382
382
383
383
384
xv
Contenido
16
Explorando java.lang ..................................................................................................
Envoltura de tipos primitivos........................................................................................
Number ..............................................................................................................
Double y Float....................................................................................................
Byte, Short, Integer y Long ...............................................................................
Character ............................................................................................................
Adiciones recientes al tipo character para soporte de unicode......................
Boolean...............................................................................................................
Void .................................................................................................................................
La clase Process .............................................................................................................
La clase Runtime ...........................................................................................................
Administración de memoria .............................................................................
Ejecución de otros programas ..........................................................................
La clase ProcessBuilder .................................................................................................
La clase System..............................................................................................................
Uso de currentTimeMillis( ) ..............................................................................
Uso de arraycopy( ) ...........................................................................................
Propiedades del entorno ...................................................................................
La clase Object ...............................................................................................................
El método clone( ) y la interfaz Cloneable ..................................................................
Class ...............................................................................................................................
ClassLoader....................................................................................................................
Math ...............................................................................................................................
Funciones trascendentes ...................................................................................
Funciones exponenciales ..................................................................................
Funciones de redondeo.....................................................................................
Otros métodos en la clase Math ......................................................................
StrictMath.......................................................................................................................
Compiler.........................................................................................................................
Thread, ThreadGroup y Runnable ................................................................................
La interfaz Runnable .........................................................................................
Thread ................................................................................................................
ThreadGroup .....................................................................................................
ThreadLocal e InheritableThreadLocal ........................................................................
Package ...........................................................................................................................
RuntimePermission .......................................................................................................
Throwable ......................................................................................................................
SecurityManager............................................................................................................
StackTraceElement ........................................................................................................
Enum ............................................................................................................................
La interfaz CharSequence.............................................................................................
La interfaz Comparable ................................................................................................
La interfaz Appendable .................................................................................................
www.detodoprogramacion.com
385
386
386
386
390
398
401
402
403
403
404
405
406
407
409
410
411
412
412
413
415
418
418
418
419
419
420
422
422
422
422
422
424
429
429
431
431
431
431
432
433
433
434
xvi
Java:
17
Manual de referencia
La interfaz Iterable ........................................................................................................
La interfaz Readable ......................................................................................................
Los subpaquetes de java.lang .......................................................................................
java.lang.annotation ..........................................................................................
java.lang.instrument ..........................................................................................
java.lang.management ......................................................................................
java.lang.ref ........................................................................................................
java.lang.reflect ..................................................................................................
434
434
435
435
435
435
435
436
java.util parte 1: colecciones......................................................................................
Introducción a las colecciones ......................................................................................
Cambios recientes en las colecciones ..........................................................................
Los tipos parametrizados se aplican a las colecciones ...................................
El autoboxing facilita el uso de tipos primitivos .............................................
El ciclo estilo for-each .......................................................................................
Las interfaces de la estructura de colecciones .............................................................
La interfaz collection .........................................................................................
La interfaz List ...................................................................................................
La interfaz Set ....................................................................................................
La interfaz SortedSet.........................................................................................
La interfaz NavigableSet ...................................................................................
La interfaz Queue..............................................................................................
La interfaz Dequeue ..........................................................................................
Las clases de la estructura de colecciones ...................................................................
La clase ArrayList ..............................................................................................
La clase LinkedList............................................................................................
La clase HashSet ...............................................................................................
La clase LinkedHashSet....................................................................................
La clase TreeSet ..................................................................................................
La clase PriorityQueue ......................................................................................
La clase ArrayDequeue ....................................................................................
La clase EnumSet ..............................................................................................
Acceso a una colección por medio de un iterador ......................................................
Uso de un iterador .............................................................................................
for-each como alternativa de los iteradotes ....................................................
Almacenamiento de clases definidas por el usuario en colecciones .........................
La interfaz RandomAccess ...........................................................................................
Trabajo con mapas .........................................................................................................
Las interfaces de Map .......................................................................................
La interfaz NavigableMap ................................................................................
Las clases Map ...................................................................................................
Comparadores................................................................................................................
Uso de un comparador......................................................................................
Los algoritmos de la estructura de colecciones ...........................................................
437
438
439
439
440
440
440
441
442
444
444
444
446
447
448
449
452
454
455
455
457
457
458
459
460
462
463
464
464
464
466
468
473
474
476
www.detodoprogramacion.com
Contenido
18
xvii
Arrays ............................................................................................................................
¿Por qué colecciones con tipos parametrizados? ........................................................
Las clases e interfaces preexistentes ............................................................................
La interfaz Enumeration ...................................................................................
Vector ..................................................................................................................
Stack ...................................................................................................................
Dictionary ...........................................................................................................
Hashtable ...........................................................................................................
Properties ...........................................................................................................
Uso de store( ) y load( ).....................................................................................
Resumen de las colecciones.........................................................................................
481
485
488
488
488
492
494
495
498
501
502
java.util Parte 2: más clases de utilería ...................................................................
StringTokenizer ..............................................................................................................
BitSet ............................................................................................................................
Date ............................................................................................................................
Calendar .........................................................................................................................
GregorianCalendar ........................................................................................................
TimeZone .......................................................................................................................
SimpleTimeZone ...........................................................................................................
Locale ............................................................................................................................
Random ..........................................................................................................................
Observable .....................................................................................................................
La interfaz Observer..........................................................................................
Un ejemplo con la interfaz Observer ...............................................................
Timer y TimerTask ..........................................................................................................
Currency .........................................................................................................................
Formatter ........................................................................................................................
Constructores de la clase Formatter.................................................................
Métodos de la clase Formatter .........................................................................
Principios de formato ........................................................................................
Formato de cadenas y caracteres......................................................................
Formato de números .........................................................................................
Formato de horas y fechas ................................................................................
Los especificadores %n y %% ..........................................................................
Especificación del tamaño mínimo de un campo ...........................................
Especificación de precisión ...............................................................................
Uso de las banderas de formato .......................................................................
Justificado del texto de salida ...........................................................................
Las banderas de espacio, +, 0 y ( ......................................................................
La bandera del signo coma ...............................................................................
La bandera de # .................................................................................................
La opción mayúsculas .......................................................................................
Uso de índices de argumento ...........................................................................
El método printf( ) .............................................................................................
503
503
505
507
509
512
513
514
515
516
518
519
519
522
524
525
526
526
526
529
529
530
532
532
533
534
535
535
536
537
537
538
539
www.detodoprogramacion.com
xviii
Java:
19
Manual de referencia
Scanner ...........................................................................................................................
Constructores de la clase Scanner....................................................................
Funcionamiento de Scanner .............................................................................
Ejemplos con la clase Scanner ..........................................................................
Establecer los delimitadores a utilizar .............................................................
Características adicionales de la clase Scanner ...............................................
Las clases ResourceBundle, ListResourceBundle y PropertyResourceBundle ..........
Otras clases e interfaces de utilería ..............................................................................
Los subpaquetes de java.util .........................................................................................
Los paquetes java.util.concurrent, java.util.concurrent.atomic
y java.util.concurrent.lock.........................................................................
El paquete java.util.jar .......................................................................................
El paquete java.util.logging...............................................................................
El paquete java.util.prefs ...................................................................................
El paquete java.util.regex ..................................................................................
El paquete java.util.spi ......................................................................................
El paquete java.util.zip ......................................................................................
539
539
540
543
546
548
549
553
554
Entrada/salida: explorando java.io ..........................................................................
Las clases e interfaces de entrada/salida de Java ........................................................
File .................................................................................................................................
Directorios ..........................................................................................................
Uso de FilenameFilter .......................................................................................
La alternativa listFiles( ) ....................................................................................
Creación de directorios .....................................................................................
Las interfaces Closeable y Flushable............................................................................
Las clases Stream...........................................................................................................
Los flujos de Bytes .........................................................................................................
InputStream .......................................................................................................
OutputStream ....................................................................................................
FileInputStream .................................................................................................
FileOutputStream ..............................................................................................
ByteArrayInputStream ......................................................................................
ByteArrayOutputStream ...................................................................................
Flujos de Bytes Filtrados ...................................................................................
Flujos de Bytes con Búfer ..................................................................................
SequenceInputStream .......................................................................................
PrintStream ........................................................................................................
DataOutputStream y DataInputStream ..........................................................
RandomAccessFile ............................................................................................
Los flujos de caracteres .................................................................................................
Reader ................................................................................................................
Writer..................................................................................................................
FileReader ..........................................................................................................
FileWriter ...........................................................................................................
555
555
556
559
560
561
561
561
562
562
562
562
564
565
567
568
569
569
573
574
576
577
578
579
579
579
579
www.detodoprogramacion.com
554
554
554
554
554
554
554
Contenido
xix
CharArrayReader ...............................................................................................
CharArrayWriter ................................................................................................
BufferedReader ..................................................................................................
BufferedWriter ...................................................................................................
PushbackReader ................................................................................................
PrintWriter .........................................................................................................
La clase Console ............................................................................................................
Uso de flujos de E/S.......................................................................................................
Mejora de wc( ) mediante la clase StreamTokenizer ......................................
Serialización ...................................................................................................................
Serializable .........................................................................................................
Externalizable.....................................................................................................
ObjectOutput .....................................................................................................
ObjectOutputStream.........................................................................................
ObjectInput ........................................................................................................
ObjectInputStream ............................................................................................
Un ejemplo de serialización .............................................................................
Ventajas de los flujos .....................................................................................................
582
582
583
585
585
586
587
589
590
592
593
593
593
593
595
595
595
598
20
Trabajo en red ..............................................................................................................
Fundamentos del trabajo en red ..................................................................................
Las clases e interfaces para el trabajo en red ...............................................................
InetAddress ....................................................................................................................
Métodos de fábrica ............................................................................................
Métodos de instancia ........................................................................................
Inet4Address e Inet6Address .......................................................................................
Conectores TCP/lP para clientes ..................................................................................
URL ................................................................................................................................
URLConnection .............................................................................................................
HttpURLConnection .....................................................................................................
La clase URI ...................................................................................................................
Cookies ...........................................................................................................................
ConectoresTCP/lP para servidores ...............................................................................
Datagramas ....................................................................................................................
DatagramSocket ................................................................................................
DatagramPacket.................................................................................................
Un ejemplo utilizando Datagramas .................................................................
599
599
600
601
601
602
603
603
605
607
610
612
612
612
613
613
614
615
21
La clase Applet.............................................................................................................
Dos tipos de applets ......................................................................................................
Fundamentos de Applet................................................................................................
La clase Applet...................................................................................................
Arquitectura de un Applet ............................................................................................
Estructura de un Applet ................................................................................................
Comienzo y final de un Applet ........................................................................
Sobrescribir el método update( ) ......................................................................
617
617
617
618
620
621
622
623
www.detodoprogramacion.com
xx
Java:
22
Manual de referencia
Métodos sencillos de visualización de applets ............................................................
Repintar la pantalla .......................................................................................................
Un Applet sencillo .............................................................................................
Uso de la barra de estado..............................................................................................
La etiqueta APPLET de HTML.....................................................................................
Paso de parámetros a los Applets .................................................................................
Mejora del Applet que muestra una frase .......................................................
getDocumentBase( ) y getCodeBase( ) ........................................................................
AppletContext y showDocument( ) .............................................................................
La interfaz AudioClip ....................................................................................................
La interfaz AppletStub ..................................................................................................
Salida a consola .................................................................................................
623
625
626
628
629
630
631
633
634
635
635
636
Gestión de eventos......................................................................................................
Dos mecanismos para gestionar eventos ....................................................................
El modelo de delegación de eventos............................................................................
Eventos ...............................................................................................................
Fuentes de eventos ............................................................................................
Auditores de eventos.........................................................................................
Clases de eventos ..........................................................................................................
La clase ActionEvent .........................................................................................
La clase AdjustmentEvent ................................................................................
La clase ComponentEvent ................................................................................
La clase ContainerEvent ...................................................................................
La clase FocusEvent ..........................................................................................
La clase InputEvent ...........................................................................................
La clase ItemEvent ............................................................................................
La clase KeyEvent..............................................................................................
La clase MouseEvent.........................................................................................
La clase MouseWheelEvent..............................................................................
La clase TextEvent ..............................................................................................
La clase WindowEvent ......................................................................................
Fuentes de eventos ........................................................................................................
Las interfaces de auditores de eventos ........................................................................
La interfaz ActionListener ................................................................................
La interfaz AdjustmentListener........................................................................
La interfaz ComponentListener .......................................................................
La interfaz ContainerListener ..........................................................................
La interfaz FocusListener..................................................................................
La interfaz ItemListener....................................................................................
La interfaz KeyListener .....................................................................................
La interfaz MouseListener ................................................................................
La interfaz MouseMotionListener ...................................................................
La interfaz MouseWheelListener .....................................................................
La interfazTextListener......................................................................................
La interfaz WindowFocusListener ...................................................................
637
637
638
638
638
639
639
640
641
642
643
643
644
644
645
646
647
648
648
649
650
650
651
651
651
651
652
652
652
652
652
652
652
www.detodoprogramacion.com
Contenido
23
xxi
La interfaz WindowListener .............................................................................
Uso del modelo de delegación de eventos ..................................................................
La gestión de eventos de ratón ........................................................................
La gestión de eventos de teclado .....................................................................
Clases adaptadoras ........................................................................................................
Clases internas ...............................................................................................................
Clases internas anónimas .................................................................................
653
653
653
656
659
660
662
AWT: trabajando con ventanas, gráficos y texto ...................................................
Las clases de AWT .........................................................................................................
Fundamentos básicos de ventanas...............................................................................
Component ........................................................................................................
Container ...........................................................................................................
Panel ...................................................................................................................
Window ..............................................................................................................
Frame ..................................................................................................................
Canvas ................................................................................................................
Trabajo con ventanas de tipo Frame ............................................................................
Cómo establecer las dimensiones de una ventana .........................................
Ocultar y mostrar una ventana.........................................................................
Poner el título a una ventana ............................................................................
Cerrar una ventana de tipo frame ....................................................................
Crear una ventana de tipo frame en un Applet .........................................................
Gestión de eventos en una ventana de tipo Frame ........................................
Creación de un programa con ventanas ......................................................................
Visualización de información dentro de una ventana ................................................
Trabajo con gráficos .......................................................................................................
Dibujar líneas .....................................................................................................
Dibujar rectángulos ...........................................................................................
Dibujar elipses y círculos ..................................................................................
Dibujar arcos ......................................................................................................
Dibujar polígonos ..............................................................................................
Tamaño de los gráficos ......................................................................................
Trabajar con color ..........................................................................................................
Métodos de la clase Color.................................................................................
Establecer el color para los gráficos .................................................................
Un ejemplo de applet con colores ....................................................................
Establecer el modo de pintado .....................................................................................
Trabajo con tipos de letra ..............................................................................................
Determinación de los tipos de letra disponibles .............................................
Creación y selección de un tipo de letra ..........................................................
Información sobre los tipos de letra.................................................................
Gestión de la salida de texto utilizando FontMetrics .................................................
Visualización de varias líneas de texto .............................................................
Centrar el texto ..................................................................................................
Alineamiento de varias líneas de texto ............................................................
663
664
666
666
666
667
667
667
667
667
668
668
668
668
669
670
674
676
676
677
677
678
679
680
681
682
683
684
684
685
686
687
689
690
691
693
694
695
www.detodoprogramacion.com
xxii
Java:
Manual de referencia
24
AWT: controles, gestores de organización y menús .............................................
Conceptos básicos de los controles ..............................................................................
Añadir y eliminar controles ..............................................................................
Responder a los controles .................................................................................
La Excepción de tipo HeadlessException ........................................................
Label ..............................................................................................................................
Button ............................................................................................................................
Gestión de botones ...........................................................................................
Checkbox ........................................................................................................................
Gestión de Checkbox ........................................................................................
CheckboxGroup.............................................................................................................
Choice ............................................................................................................................
Gestión de Choice .............................................................................................
List .................................................................................................................................
Gestión de List...................................................................................................
Scrollbar..........................................................................................................................
Gestión de Scrollbar ..........................................................................................
TextField .........................................................................................................................
Gestión de TextField ..........................................................................................
TextArea ..........................................................................................................................
Gestores de organización..............................................................................................
FlowLayout ........................................................................................................
BorderLayout .....................................................................................................
Insets ..................................................................................................................
GridLayout .........................................................................................................
CardLayout ........................................................................................................
GridBagLayout...................................................................................................
Barras de menú y menús...............................................................................................
Cuadros de diálogo........................................................................................................
FileDialog .......................................................................................................................
Gestión de eventos extendiendo los componentes AWT...........................................
Extender Button .................................................................................................
Extender Checkbox ...........................................................................................
Extender CheckboxGroup ................................................................................
Extender Choice ................................................................................................
Extender List ......................................................................................................
Extender Scrollbar .............................................................................................
701
701
702
702
702
702
704
704
707
707
709
711
711
713
714
716
717
719
720
721
723
724
725
727
728
730
732
737
742
747
748
749
750
751
752
752
753
25
Imágenes .......................................................................................................................
Formatos de archivos ....................................................................................................
Conceptos básicos sobre imágenes: creación, carga y visualización .........................
Creación de un objeto imagen .........................................................................
Carga de una imagen ........................................................................................
Visualización de una imagen ............................................................................
755
755
756
756
756
757
www.detodoprogramacion.com
Contenido
xxiii
ImageObserver ..............................................................................................................
Doble almacenamiento en búferes ..............................................................................
MediaTracker..................................................................................................................
ImageProducer...............................................................................................................
MemoryImageSource ........................................................................................
ImageConsumer ............................................................................................................
PixelGrabber ......................................................................................................
ImageFilter .....................................................................................................................
CropImageFilter.................................................................................................
RGBImageFilter .................................................................................................
Animación de imágenes ...............................................................................................
Más clases para trabajo con imágenes .........................................................................
758
759
762
765
766
767
767
770
770
772
783
786
26
Utilerías para concurrencia .......................................................................................
El API para trabajo con concurrencia ...........................................................................
java.util.concurrent ............................................................................................
java.util.concurrent.atomic ...............................................................................
java.util.concurrent.locks ..................................................................................
Uso de objetos para sincronización..............................................................................
Semaphore .........................................................................................................
CountDownLatch..............................................................................................
CyclicBarrier .......................................................................................................
Exchanger ...........................................................................................................
Uso de executor .............................................................................................................
Un ejemplo simple de Executor .......................................................................
Uso de Callable y Future ...................................................................................
La enumeración de tipo TimeUnit ...............................................................................
Las colecciones concurrentes .......................................................................................
Candados .......................................................................................................................
Operaciones atómicas ...................................................................................................
Las utilerías de concurrencia frente a la programación tradicional de Java ..............
787
788
788
789
789
789
789
795
796
799
801
802
804
806
808
808
811
812
27
NES, expresiones regulares y otros paquetes ........................................................
El núcleo de los paquetes de Java.................................................................................
NES ................................................................................................................................
Fundamentos de NES .......................................................................................
Conjuntos de caracteres y selectores ...............................................................
Uso del NES .......................................................................................................
¿Es NES el futuro de la gestión de operaciones de E/S? ................................
Expresiones regulares ....................................................................................................
Pattem.................................................................................................................
Matcher ..............................................................................................................
Sintaxis de expresiones regulares .....................................................................
Ejemplos prácticos de expresiones regulares ..................................................
813
813
815
815
819
819
825
825
826
826
827
827
www.detodoprogramacion.com
xxiv
Java:
Manual de referencia
Dos opciones para el método matches( ) ........................................................
Explorando las expresiones regulares ..............................................................
Reflexión .........................................................................................................................
Invocación remota de métodos (RMI) .........................................................................
Una aplicación cliente/servidor sencilla utilizando RMI ................................
Formato de texto ............................................................................................................
La clase DateFormat..........................................................................................
La clase SimpleDateFormat ..............................................................................
Parte III
832
833
833
837
837
840
840
842
Desarrollo de software utilizando Java
28
Java Beans .....................................................................................................................
¿Qué es Java Beans? ......................................................................................................
Ventajas de los Java Beans.............................................................................................
Introspección..................................................................................................................
Patrones de diseño para propiedades ..............................................................
Patrones de diseño para eventos ......................................................................
Métodos y patrones de diseño .........................................................................
Uso de la interfaz BeanInfo ..............................................................................
Propiedades limitadas y restringidas ...........................................................................
Persistencia .....................................................................................................................
Customizers ...................................................................................................................
La Java Beans API ..........................................................................................................
Introspector ........................................................................................................
PropertyDescriptor ............................................................................................
EventSetDescriptor............................................................................................
MethodDescriptor .............................................................................................
Un ejemplo de programación de Java Beans ...............................................................
847
847
848
848
848
850
850
850
851
851
851
852
854
854
854
854
854
29
Introducción a Swing .................................................................................................
Los orígenes de Swing ..................................................................................................
Swing está construido sobre AWT ...............................................................................
Dos características clave de Swing ...............................................................................
Los componentes de Swing son ligeros ..........................................................
La apariencia de un componente es independiente
del componente mismo ............................................................................
El modelo MVC .............................................................................................................
Componentes y contenedores ......................................................................................
Componentes ....................................................................................................
Contenedores.....................................................................................................
Los contenedores raíz .......................................................................................
Los paquetes de Swing .................................................................................................
Una aplicación sencilla con Swing ...............................................................................
Gestión de eventos ........................................................................................................
Crear un applet con Swing ...........................................................................................
Dibujar en Swing ...........................................................................................................
859
859
860
860
860
www.detodoprogramacion.com
860
861
862
862
863
863
864
864
868
871
873
Contenido
xxv
Fundamentos de dibujo ....................................................................................
Calcular el área de dibujo ................................................................................
Un ejemplo con dibujos ....................................................................................
874
875
875
30
Explorando Swing.......................................................................................................
JLabel e ImageIcon ........................................................................................................
JTextField ........................................................................................................................
Los botones de Swing ...................................................................................................
JButton................................................................................................................
JToggleButton.....................................................................................................
JCheckBox ..........................................................................................................
JRadioButton ......................................................................................................
JTabbedPane ...................................................................................................................
JScrollPane......................................................................................................................
JList ................................................................................................................................
JComboBox ....................................................................................................................
JTree ..............................................................................................................................
JTable ..............................................................................................................................
Otras características para explorar de Swing...............................................................
879
879
881
883
883
885
887
889
891
893
895
898
900
904
906
31
Servlets ..........................................................................................................................
Introducción ...................................................................................................................
El ciclo de vida de un servlet.........................................................................................
Uso tomcat para el desarrollo de servlet ......................................................................
Un servlet sencillo .........................................................................................................
Crear y compilar el código fuente de un servlet .............................................
Arrancando el servidor web Tomcat .................................................................
Acceso al servlet con un navegador .................................................................
El servlet API ..................................................................................................................
El paquete javax.servlet .................................................................................................
La interfaz Servlet..............................................................................................
La interfaz ServletConfig ..................................................................................
La interfaz ServletContext ................................................................................
La interfaz ServletRequest ................................................................................
La interfaz ServletResponse .............................................................................
La clase GenericServlet .....................................................................................
La clase ServletInputStream .............................................................................
La clase ServletOutputStream ..........................................................................
La clase ServletException..................................................................................
Leyendo parámetros de un servlet ...............................................................................
El paquete javax.servlet.http .........................................................................................
La interfaz HttpServletRequest ........................................................................
La interfaz HttpServletResponse .....................................................................
La interfaz HttpSession ....................................................................................
La interfaz HttpSessionBindingListener .........................................................
La clase Cookie ..................................................................................................
907
907
908
908
910
910
911
911
911
911
912
912
913
913
913
914
915
915
915
915
917
917
917
918
919
919
www.detodoprogramacion.com
xxvi
Java:
Manual de referencia
La clase HttpServlet ..........................................................................................
La clase HttpSessionEvent ...............................................................................
La clase HttpSessionBindingEvent ..................................................................
Gestión de peticiones y respuestas de HTTP ..............................................................
Gestión de peticiones tipo GET .......................................................................
Gestión de peticiones tipo POST .....................................................................
Uso de Cookies ..............................................................................................................
Sesiones ..........................................................................................................................
Parte IV
921
921
922
923
923
924
925
927
Aplicaciones en Java
32
Applets y servlets aplicados en la solución de problemas .................................
Calcular los pagos de un préstamo ..............................................................................
Las variables de la clase ....................................................................................
El método init( ).................................................................................................
El método makeGUI( )......................................................................................
El método actionPerformed( ) ..........................................................................
El método compute( ) .......................................................................................
Calcular el valor futuro de una inversión.....................................................................
Calcular la inversión inicial requerida para alcanzar un valor futuro ........................
Calcular la inversión inicial necesaria para una anualidad deseada..........................
Calcular la anualidad máxima para una inversión dada ............................................
Calcular el balance restante un préstamo....................................................................
Crear servlets financieros ..............................................................................................
Convertir un Applet en un servlet ....................................................................
El servlet RegPayS..............................................................................................
Ejercicios recomendados ...............................................................................................
931
932
935
936
936
938
939
940
943
947
951
955
959
960
960
963
33
Creando un administrador de descargas en Java ..................................................
Introducción ...................................................................................................................
Descripción del administrador de descargas ...............................................................
La clase Download ........................................................................................................
Las variables de Download...............................................................................
El constructor Download ..................................................................................
El método download( ) .....................................................................................
El método run( ) ................................................................................................
El método stateChanged( ) ...............................................................................
Los métodos de acción y accesores ..................................................................
La clase ProgressRenderer ............................................................................................
La clase DownloadsTableModel ...................................................................................
El método addDownload( ) ..............................................................................
El método clearDownload( ) ............................................................................
El método getColumnClass( ) ..........................................................................
El método getValueAt( ) ....................................................................................
El método update( )...........................................................................................
965
966
966
967
971
971
971
971
975
975
975
976
978
979
979
979
980
www.detodoprogramacion.com
Contenido
A
xxvii
La clase DownloadManager .........................................................................................
Las variables de DownloadManager ...............................................................
El constructor DownloadManager ...................................................................
El método verifyUrl( ) .......................................................................................
El método tableSelectionChanged( ) ...............................................................
El método updateButtons( ) .............................................................................
Gestión de los eventos de acción .....................................................................
Compilar y ejecutar el administrador de descarga......................................................
Mejorando el administrador de descargas...................................................................
980
986
986
987
987
988
989
989
990
Usando los comentarios de documentación de Java ............................................
Las etiquetas de javadoc ...............................................................................................
@author ..............................................................................................................
{@code}...............................................................................................................
@deprecated ......................................................................................................
{@docRoot} ........................................................................................................
@exception .........................................................................................................
{@inheritDoc} ....................................................................................................
{@link} ................................................................................................................
{@linkplain}........................................................................................................
{@literal} .............................................................................................................
@param ..............................................................................................................
@return ...............................................................................................................
@see ....................................................................................................................
@serial ................................................................................................................
@serialData ........................................................................................................
@serialField ........................................................................................................
@since.................................................................................................................
@throws .............................................................................................................
{@value}..............................................................................................................
@version .............................................................................................................
Forma general de un comentario de documentación .................................................
Salida de javadoc ...........................................................................................................
Un ejemplo que utiliza comentarios de documentación............................................
991
991
992
992
992
993
993
993
993
993
993
993
993
994
994
994
994
994
994
995
995
995
995
995
Índice ............................................................................................................................
997
www.detodoprogramacion.com
www.detodoprogramacion.com
Prefacio
M
ientras escribo esto, Java está justo iniciando su segunda década. A diferencia de
muchos otros lenguajes de computadora cuya influencia comienza a disminuir con el
paso de los años, la influencia de Java ha crecido fuertemente con el paso del tiempo.
Java saltó a la fama como opción para programar aplicaciones en Internet con su primera
versión. Cada versión subsiguiente ha solidificado esa posición. Hoy día, Java sigue siendo la
primera y mejor opción para desarrollo de aplicaciones Web.
Una de las razones del éxito de Java es su agilidad. Java se ha adaptado rápidamente a
los cambios en el ambiente de desarrollo y a los cambios en la forma en que los programadores
programan. Y lo más importante, Java no sólo ha seguido las tendencias, ha ayudado a crearlas.
A diferencia de muchos lenguajes que tienen un ciclo de revisión de aproximadamente 10
años, en promedio los ciclos de revisión de Java son de alrededor de 1.5 años. La facilidad de
Java para adaptarse a los rápidos cambios en el mundo de la computación es una parte crucial
del porque ha permanecido a la vanguardia del diseño de lenguajes de programación. Con la
versión de Java SE 6, el liderazgo de Java es indiscutible. Si estamos realizando programas para
Internet, hemos seleccionado el lenguaje correcto. Java ha sido y continúa siendo el lenguaje
más importante para el desarrollo de aplicaciones en Internet
Como muchos lectores sabrán, ésta es la séptima edición del libro, el cual fue publicado por
primera vez en 1996. Esta edición ha sido actualizada para Java SE 6. También ha sido extendida
en muchas áreas clave, como ejemplo de ello podemos mencionar que ahora se incluye más
cobertura de Swing y una discusión más detallada de los paquetes de recursos. De principio a
fin hay muchos otros agregados y mejoras. En general, un gran número de páginas con material
nuevo han sido incorporadas.
Un libro para todos los programadores
Este libro es para todo tipo de programadores, principiantes y experimentados. Los principiantes
encontrarán discusiones cuidadosamente establecidas y ejemplos particularmente útiles. Para el
programador experimentado se ha realizado una cobertura profunda de las más avanzadas
características de Java y sus bibliotecas. Para ambos, este libro ofrece un recurso duradero y una
referencia fácil de utilizar.
Qué contiene
Este libro es una guía completa y detallada del lenguaje de programación Java, describe su
sintaxis, palabras clave y principios fundamentales de programación. Además de examinar
porciones significativas de las bibliotecas de Java. El libro está divido en cuatro partes, cada una
se enfoca en un aspecto diferente del ambiente de programación de Java.
xxix
www.detodoprogramacion.com
xxx
Java:
Manual de referencia
La primera parte presenta un tutorial detallado del lenguaje de programación Java.
Comienza con lo básico, incluyendo temas como tipo de datos, sentencias de control y clases.
La primera parte también trata el mecanismo de gestión de excepciones de Java, el subsistema
de multihilos, los paquetes y las interfaces. Por supuesto las nuevas características de Java, tales
como tipos parametrizados, anotaciones, enumeraciones y autoboxing son cubiertas a detalle.
La segunda parte examina aspectos clave de las bibliotecas estándares del API de Java. Los
temas que se incluyen son las cadenas de caracteres, la construcción de flujos de E/S, el trabajo
en red, las utilerías estándares, la estructura de colecciones, los applets, los controles basados en
interfaces gráficas de usuario, las imágenes y la concurrencia.
La tercera parte examina tres importantes tecnologías de Java: Java Beans, Swing y servlets.
La cuarta parte contiene dos capítulos que muestran ejemplos de Java en acción. En el
primer capítulo se desarrollan varios applets para realizar cálculos financieros comunes, tales
como calcular el pago regular de un préstamo o la inversión mínima necesaria para retirar
mensualmente una cantidad determinada. Este capítulo también muestra como convertir esos
applets en servlets. El segundo capítulo desarrolla un administrador de descarga de archivos que
supervisa dichas descargas. Esta aplicación tiene la habilidad de iniciar, detener, suspender y
continuar. Ambos capítulos son adaptaciones de textos tomados de mi libro The Art of Java, del
cual fui coautor junto con James Holmes.
El código está en la Web
Recuerde que el código fuente, de todos los ejemplos en este libro, está disponible sin costo en la
Web en la página www.mcgraw-hill-educacion.com.
Agradecimientos
Patrick Naughton merece una mención especial. Patrick fue uno de los creadores del lenguaje
Java, y colaboró en la primera edición de este libro. Gran parte del material de los capítulos
19, 20 y 25 fue proporcionado inicialmente por Patrick. Su perspicacia, experiencia y energía
contribuyeron en gran medida al gran éxito de este libro.
También agradezco a Joe O’Neil el haberme proporcionado los borradores iniciales de los
capítulos 27, 28, 30 y 31. Joe ha colaborado en varios de mis libros y, como siempre, su esfuerzo
es apreciado.
Finalmente, muchas gracias a James Holmes por proporcionar el capítulo 32. James es un
programador y autor extraordinario. Trabajó conmigo en la escritura de The Art of Java, es autor
de Struts The Complete Reference y uno de los coautores de JSF: The Complete Reference.
HERBERT SCHILDT
www.detodoprogramacion.com
Referencias adicionales
Este libro es la puerta de acceso a los libros de programación de la serie de Herb Schildt. Algunos
otros textos de interés se citan a continuación:
Para aprender más acerca de la programación en Java, recomendamos los siguientes:
Java: A Beginner’s Guide
Swing: A Beginner’s Guide
The Art of Java
Para aprender acerca de C++, encontrarás especialmente útiles los siguientes libros:
C++: The Complete Reference
C++: A Beginner’s Guide
The Art of C++
C++ From the Ground Up
STL Programming From the Ground Up
Para aprender acerca de C#, sugerimos los siguientes libros de Schildt:
C#: The Complete Reference
C#: A Beginner’s Guide
Para aprender acerca del lenguaje C, los siguientes títulos serán interesantes:
C: The Complete Reference
Teach Yourself C
Cuando necesite respuestas sólidas y rápidas, diríjase a Herbert Schildt,
la autoridad reconocida en el mundo de la programación.
www.detodoprogramacion.com
www.detodoprogramacion.com
I
PARTE
El lenguaje Java
CAPÍTULO 1
Historia y evolución de Java
CAPÍTULO 2
Introducción a Java
CAPÍTULO 3
Tipos de dato, Variables
y Arreglos
CAPÍTULO 4
Operadores
CAPÍTULO 5
Sentencias de control
CAPÍTULO 6
Clases
CAPÍTULO 7
Métodos y clases
CAPÍTULO 8
Herencia
CAPÍTULO 9
Paquetes e interfaces
CAPÍTULO 10
Gestión de excepciones
CAPÍTULO 11
Programación multihilo
CAPÍTULO 12
Enumeraciones, autoboxing
y anotaciones (metadatos)
CAPÍTULO 13
E/S, applets y otros temas
CAPÍTULO 14
Tipos parametrizados
www.detodoprogramacion.com
www.detodoprogramacion.com
1
CAPÍTULO
Historia y evolución
de Java
P
ara entender completamente Java, se deben entender las razones detrás de su creación,
las fuerzas que lo formaron y el legado que hereda. Java es una mezcla de los mejores
elementos de los lenguajes de programación exitosos. El resto de los capítulos de este
libro describirán los aspectos prácticos de Java incluyendo su sintaxis, bibliotecas principales y
aplicaciones. Este capítulo explica cómo y porqué surge Java, qué lo hace tan importante, y cómo se
ha desarrollado a través de los años.
Aunque ha sido fuertemente ligado a Internet, es importante recordar que Java es un lenguaje
de programación de uso general. Las innovaciones y desarrollo de los lenguajes de programación
ocurren por dos razones fundamentales:
• Para adaptarse a los cambios en ambientes y usos
• Para implementar refinamientos y mejoras en el arte de la programación
Como verá, el desarrollo de Java fue dirigido por ambos elementos en similar medida.
Linaje de Java
Java está relacionado con C++, que es un descendiente directo de C. Java hereda la mayor parte
de su carácter de estos dos lenguajes. De C, Java deriva su sintaxis y muchas de sus características
orientadas a objetos fueron consecuencia de la influencia de C++. Efectivamente, muchas de las
características de Java vienen de –o surgen como respuesta a– sus lenguajes predecesores. Más aún, la
creación de Java está profundamente arraigada en el proceso de refinamiento y adaptación, que en las
pasadas décadas ha ocurrido con los lenguajes de programación. Por estos motivos, en esta sección
revisaremos la secuencia de eventos y factores que condujeron a la creación de Java. Como se verá,
cada innovación en el diseño de un lenguaje de programación se debe a la necesidad de resolver un
problema al que no han podido dar solución los lenguajes precedentes. Java no es una excepción.
El Nacimiento de la programación moderna: C
El lenguaje C sacudió el mundo de la computación. Su repercusión no debería ser subestimada,
ya que cambió fundamentalmente la forma en que la programación era enfocada y concebida. La
creación de C fue un resultado directo de la necesidad de un lenguaje de alto nivel, estructurado,
eficiente y que pudiera reemplazar al código ensamblador en la creación de programas. Como
3
www.detodoprogramacion.com
4
Parte I:
El lenguaje Java
probablemente sabrá, cuando se diseña un lenguaje de programación se realiza una serie de
balances comparativos, tales como:
• Facilidad de uso frente a potencia
• Seguridad frente a eficiencia
• Rigidez frente a extensibilidad
Antes de la aparición de C, los programadores usualmente tenían que elegir entre lenguajes que
optimizaran un conjunto de características u otro. Por ejemplo, aunque FORTRAN podía utilizarse
para escribir programas muy eficientes en aplicaciones científicas, no resultaba muy bueno para
implementar aplicaciones de sistema. Y mientras BASIC era fácil de aprender, no era muy poderoso,
y su falta de estructura cuestionaba su utilidad en el desarrollo de programas grandes. El lenguaje
ensamblador se puede utilizar para generar programas muy eficientes, pero su aprendizaje y uso no
resultan muy sencillos. Además, la depuración del código ensamblador resulta bastante complicada.
Otro problema complejo fue que los primeros lenguajes de computadora como BASIC,
COBOL y FORTRAN no fueron diseñados en torno a los principios de la estructuración.
En lugar de eso, dependían del GOTO como forma más importante de control de flujo.
Como consecuencia, los programas escritos con estos lenguajes tendían a producir “código
spaghetti”: un código lleno de saltos enredados y ramificaciones condicionales que hacen
que la comprensión de un programa resulte virtualmente imposible. Por otro lado, lenguajes
estructurados, como Pascal, no fueron diseñados pensando en la eficiencia y fallaron al intentar
incluir ciertas características necesarias para hacerlos aplicables en una amplia gama de sistemas;
específicamente, dado los dialectos estándares de Pascal disponibles en ese entonces, no era
práctico considerar el uso de Pascal para elaborar aplicaciones de sistema.
Así, justo antes de la aparición de C, ningún lenguaje había conseguido reunir los atributos
que habían concentrado los primeros esfuerzos. Existía la necesidad de un nuevo lenguaje. A
principios de los años setenta tuvo lugar la revolución informática, y la demanda de software
superó rápidamente la capacidad de los programadores de producirlo. En los círculos académicos
se hizo un gran esfuerzo en un intento de crear un lenguaje de programación mejor que los
existentes. Pero y quizás lo más importante, una fuerza secundaria comenzaba a aparecer. El
hardware de la computadora se estaba convirtiendo en algo bastante común, de manera que se
estaba alcanzando una masa crítica. Por primera vez, los programadores tenían acceso ilimitado
a sus máquinas, y esto permitía la libertad de experimentar. Esto también consintió que los
programadores comenzaran a crear sus propias herramientas. En la víspera de la creación de C,
todo estaba preparado para dar un salto hacia adelante en los lenguajes de programación.
C fue inventado e implementado por primera vez por Dennis Ritchie en una DEC PDP-11
corriendo el sistema operativo UNIX. C fue el resultado del proceso de desarrollo que comenzó con
un lenguaje anterior llamado BCPL, desarrollado por Martin Richards. BCPL tenía influencia de un
lenguaje llamado B, inventado por Ken Thompson, que condujo al desarrollo de C en la década de
los años setenta. Durante muchos años, el estándar para C fue, de hecho, el que era suministrado
con el sistema operativo UNIX y descrito en “The C programming Language” por Brian Kernighan
y Dennis Ritchie (Prentice-Hall, 1978). C fue formalmente estandarizado en diciembre de 1989,
cuando se adoptó el estándar ANSI (American National Standards Institute) de C.
La creación de C es considerada por muchos como el comienzo de la era moderna en los
lenguajes de programación. Sintetizaba con éxito los conflictivos atributos que habían causado
tantos problemas a los anteriores lenguajes de programación. El resultado fue un lenguaje
poderoso, eficiente y estructurado cuyo aprendizaje era relativamente fácil. También tenía
www.detodoprogramacion.com
Capítulo 1:
Historia y evolución de Java
El Siguiente Paso: C++
Durante los últimos años de los setenta y principios de los ochenta, C se convirtió en el
lenguaje de programación dominante, y aún sigue siendo ampliamente utilizado. Aunque C
es un lenguaje exitoso y útil y que sin duda ha triunfado, se necesitaba algo más. A lo largo de
la historia de la programación, el aumento en la complejidad de los programas ha conducido
a la necesidad de mejorar las formas de manejar esa complejidad. C++ es la respuesta a esa
necesidad. Para entender mejor por qué la gestión de la complejidad de los programas ha dado
lugar a la creación de C++, consideremos lo siguiente.
La manera de programar ha cambiado dramáticamente desde que se inventó la computadora.
Por ejemplo, cuando se inventaron las primeras computadoras, la programación se hacia
manualmente conmutando las instrucciones binarias desde el panel frontal. Este enfoque sirvió
mientras los programas consistían de unos pocos cientos de instrucciones. Cuando los programas
fueron creciendo, surgió el lenguaje ensamblador, con el cual los programadores podían abordar
programas más grandes y cada vez más complejos usando representaciones simbólicas de las
instrucciones de la máquina. Conforme los programas continuaron creciendo, los lenguajes de
alto nivel fueron introducidos para dar al programador más herramientas con las cuales gestionar
la complejidad.
El primer lenguaje ampliamente utilizado fue, claro está, FORTRAN. Aunque FORTRAN
fue un impresionante primer paso, no es un lenguaje que anime a desarrollar programas claros
y fáciles de entender. En los años sesenta nació la programación estructurada. Este es
el método de programación que soportan lenguajes como C. El uso de los lenguajes
estructurados permite a los programadores escribir, por primera vez, programas de una
complejidad moderada con mayor facilidad. De cualquier forma, aún con los métodos de
programación estructurada, una vez que un proyecto alcanza cierto tamaño, su complejidad
excede la capacidad de manejo del programador. A principios de los ochenta, muchos de
los proyectos estaban llevando al enfoque estructurado más allá de sus límites. Para resolver
este problema, una nueva forma de programación surgió, llamada programación orientada a
objetos (POO). La programación orientada a objetos se discute en detalle después en este libro,
pero aquí está una breve definición: POO es una metodología de programación que ayuda a
organizar programas complejos mediante el uso de la herencia, encapsulación y polimorfismo.
En resumen, aunque C es uno de los mejores lenguajes de programación en el mundo, su
capacidad para gestionar la complejidad tiene un límite. Una vez que el tamaño del programa
excede un cierto punto, se vuelve demasiado complejo tanto así que es muy difícil abarcarlo en
su totalidad. Aunque el tamaño preciso en el cual ocurre esta diferencia depende de la naturaleza
del programa y del programador, siempre hay un límite en el cual un programa se vuelve
www.detodoprogramacion.com
PARTE I
otro aspecto casi intangible: era el lenguaje de los programadores. Antes de la invención de
C, los lenguajes de programación eran diseñados generalmente como ejercicios académicos
o por comités burocráticos. C es diferente. Fue diseñado, implementado y desarrollado por
programadores que reflejaron en él su forma de entender la programación. Sus características
fueron concebidas, probadas y perfeccionadas por personas que en realidad usaban el lenguaje.
El resultado fue un lenguaje que a los programadores les gustaba utilizar. En efecto, C consiguió
rápidamente muchos seguidores quienes tenían en él una fe casi religiosa, y como consecuencia
encontró una amplia y rápida aceptación en la comunidad de programadores. En resumen, C es
un lenguaje diseñado por y para programadores. Como se verá, Java ha heredado este legado.
5
6
Parte I:
El lenguaje Java
imposible de gestionar. C++ agrega características que permiten pasar estos límites, habilita a los
programadores a comprender y manejar programas más largos.
C++ fue inventado por Bjarne Stroustrup in 1979, mientras trabajaba en los Laboratorios
Bell en Murray Hill, New Jersey. Stroustrup llamó inicialmente al nuevo lenguaje “C con clases”.
Sin embargo, en 1983 el nombre fue cambiado a C++. C++ es una extensión de C en la que
se añaden las características orientadas a objetos. Como C++ se construye sobre la base de C,
incluye todas sus características, atributos y ventajas; ésta es la razón de su éxito como lenguaje
de programación. La invención de C++ no fue un intento de crear un lenguaje de programación
completamente nuevo. En lugar de eso, fue una ampliación de un lenguaje existente y exitoso.
Todo está dispuesto para Java
A finales de los años ochenta y principios de los noventa la programación orientada a objetos
usando C++ dominaba. De hecho, por un pequeño instante pareció que los programadores
finalmente habían encontrado el lenguaje perfecto. Como C++ había combinado la gran
eficiencia y el estilo de C con el paradigma de la programación orientada a objetos, era un
lenguaje que podía utilizar para crear una amplia gama de programas. Sin embargo, como en
el pasado, surgieron, una vez más, fuerzas que darían lugar a una evolución de los lenguajes de
programación. En pocos años, la World Wide Web e Internet alcanzaron una masa crítica. Este
evento precipitaría otra revolución en el mundo de la programación.
La creación de Java
Java fue concebido por James Gosling, Patrick Naughton, Chris Warth, Ed Frank, y Mike
Sheridan en Sun Microsystems, Inc. en 1991. Tomó 18 meses el desarrollo de la primera versión
funcional. Este lenguaje fue llamado inicialmente “Oak”, pero fue renombrado como “Java” en
1995. Entre la implementación inicial de Oak en el otoño de 1992 y el anuncio oficial de Java en
la primavera de 1995, muchas personas contribuyeron al diseño y evolución del lenguaje. Bill
Joy, Artur van Hoff, Jonathan Payne, Frank Yellin, y Tim Lindholm realizaron contribuciones clave
para la maduración del prototipo original.
Algo sorprendente es que el impulso inicial para Java no fue Internet, sino la necesidad de
un lenguaje de programación que fuera independiente de la plataforma (esto es, arquitectura
neutral) un lenguaje que pudiera ser utilizado para crear software que pudiera correr en
dispositivos electrodomésticos, como hornos de microondas y controles remoto. Como se
puede imaginar, existen muchos tipos diferentes de CPU que se utilizan como controladores.
El inconveniente con C y C++ (y la mayoría de los lenguajes) es que están diseñados para ser
compilados para un dispositivo específico. Aunque es posible compilar un programa de C++
para casi todo tipo de CPU, hacerlo requiere un compilador de C++ completo para el CPU
especificado. El problema es que los compiladores son caros y consumen demasiado tiempo al
crearse. Era necesaria una solución fácil y más eficiente. En un intento por encontrar tal solución,
Gosling y otros comenzaron a trabajar en el desarrollo de un lenguaje de programación portable,
que fuese independiente de la plataforma y que pudiera ser utilizado para producir código
capaz de ejecutarse en distintos CPU bajo diferentes entornos. Este esfuerzo condujo en última
instancia a la creación de Java.
Mientras se trabajaban distintos aspectos de Java, surgió un segundo, y definitivamente más
importante, factor, que jugaría un papel crucial en el futuro de Java. Este factor fue, naturalmente,
la World Wide Web. Si el mundo de la Web no se hubiese desarrollado al mismo tiempo que
www.detodoprogramacion.com
Capítulo 1:
Historia y evolución de Java
www.detodoprogramacion.com
PARTE I
Java estaba siendo implementado, Java podría haber sido simplemente un lenguaje útil para
programación de dispositivos electrónicos. Sin embargo, con la aparición de la World Wide
Web, Java fue lanzado a la vanguardia del diseño de lenguajes de programación, porque la Web
también demandaba programas que fuesen portables.
Aunque la búsqueda de programas eficientes, portables (independientes de la plataforma),
es tan antigua como la propia disciplina de la programación, ha ocupado un lugar secundario en
el desarrollo de los lenguajes, debido a problemas cuya solución era más urgente. Por otro parte,
la mayoría de las computadoras del mundo se dividen en tres grandes grupos: Intel, Macintosh y
UNIX. Por ello, muchos programadores han permanecido dentro de sus fronteras sin la urgente
necesidad de un código portable. Sin embargo, con la llegada de Internet y de la Web, el viejo
problema de portabilidad resurgió. Después de todo, Internet, consiste en un amplio universo
poblado por muchos tipos de computadoras, sistemas operativos y CPU. Incluso aunque muchos
tipos diferentes de plataformas se encuentran conectados a Internet, a todos los usuarios les
gustaría ejecutar el mismo programa. Lo que fue una vez un problema irritante pero de baja
prioridad se ha convertido en una necesidad que requiere máxima atención.
En 1993, para el equipo que estaba diseñando Java resultó obvio que el problema de la
portabilidad, que se encontraban con frecuencia cuando creaban código para los controladores,
era también el problema que se encontraban al crear código para Internet. Efectivamente, el
mismo problema que Java intentaba resolver a pequeña escala estaba también en Internet a gran
escala. Y esto hizo que Java cambiara su orientación pasando de ser aplicado a los dispositivos
electrónicos de consumo a la programación para Internet. Por esto, aunque la motivación inicial
fue la de proporcionar un lenguaje de programación independiente de la arquitectura, ha sido
Internet quien finalmente ha conducido al éxito de Java a gran escala.
Como se mencionó anteriormente, Java derivó muchas de sus características de C y C++.
Los diseñadores de Java sabían que utilizando la sintaxis de C y repitiendo las características
orientadas a objetos de C++ conseguirían que su nuevo lenguaje atrajese a las legiones de
programadores experimentados en C/C++. Además de las semejanzas evidentes a primera
vista, Java comparte con C y C++ algunos de los atributos que hicieron triunfar a C y C++. En
primer lugar, Java fue diseñado, probado y mejorado por programadores de trabajaban en el
mundo real. Java es un lenguaje que tiene sus fundamentos en las necesidades y la experiencia
de las personas que lo diseñaron. Por este motivo, Java es el lenguaje de los programadores.
En segundo lugar, Java es un lenguaje coherente y consistente lógicamente. En tercer lugar,
excepto por las restricciones que impone el ambiente de Internet, Java permite al programador
un control total. En otras palabras, Java no es un lenguaje de entrenamiento; es un lenguaje para
programadores profesionales.
Dadas las semejanzas entre Java y C++, se puede pensar que Java es simplemente “La
versión de C++ para Internet”; sin embargo, creer esto sería un gran error. Java tiene diferencias
prácticas y filosóficas con C++. Si bien es cierto que Java fue influido por C++, no es una
versión mejorada de C++. Por ejemplo, Java no es compatible de ninguna forma con C++. Las
semejanzas con C++ son evidentes, y si usted es un programador C++, con Java se sentirá como
en casa. Otro punto: Java no fue diseñado para sustituir a C++, sino para resolver un cierto tipo
de problemas diferentes a los que resolvía C++, y ambos coexistirán en los años venideros.
Como mencionamos al principio de este capítulo, la evolución de los lenguajes de
programación se debe a dos motivos: la adaptación a los cambios del entorno y la introducción
de mejoras en el arte de la programación. El cambio de entorno que dio lugar a la aparición de
Java fue la necesidad de programas independientes de la plataforma destinados a su distribución
7
8
Parte I:
El lenguaje Java
en Internet. Sin embargo, Java también incorpora cambios en la forma en que los programadores
plantean el desarrollo de sus programas. Por ejemplo, Java amplió y refinó el paradigma
orientado a objetos usado por C++, añadiendo soporte para multihilos, y proporcionando
una biblioteca que simplifica el acceso a Internet. En resumen, no fueron las características
individuales de Java las que lo hicieron tan notable, sino que fue el lenguaje en su totalidad. Java
fue la respuesta perfecta a las demandas del emergente universo de computación distribuida.
Java fue a la programación para Internet lo que C fue a la programación de sistemas: una
revolucionaria fuerza que cambió el mundo.
La conexión de C#
El alcance y poder de Java continúa presente en el mundo del desarrollo de los lenguajes de
programación. Muchas de sus características innovadoras, construcciones y conceptos se han
convertido en guía de referencia para cualquier nuevo lenguaje.
Quizás el más importante ejemplo de la influencia de Java es C#. Creado por Microsoft para
su plataforma .NET, C# está estrechamente relacionado con Java. Por ejemplo, ambos comparten
la misma sintaxis general, soportan programación distribuida y utilizan el mismo modelo de
objetos. Existen, claro está, diferencias entre Java y C#, pero en general la apariencia de esos
lenguajes es muy similar. Esta influencia de Java en C# es el testimonio más fuerte hasta la fecha
de que Java redefinió la forma en que pensamos y utilizamos los lenguajes de programación.
Cómo Java cambió al Internet
Internet ha ayudado a Java a situarse como líder de los lenguajes de programación, y Java
recíprocamente ha tenido un profundo efecto sobre Internet. Además de simplificar la
programación Web en general, Java innovó con un nuevo tipo de programación para la red
llamado applet que cambió la forma en que se concebía el contenido del mundo en línea. Java
también solucionó algunos de los problemas más difíciles asociados con el Internet: portabilidad
y seguridad. Veamos más de cerca cada uno de éstos.
Java applets
Un applet es un tipo especial de programa de Java que es diseñado para ser transmitido por
Internet y automáticamente ejecutado por un navegador compatible con Java. Un applet es
descargado bajo demanda, sin mayor interacción con el usuario. Si el usuario hace clic a una
liga que contiene un applet, el applet será automáticamente descargado y ejecutado en el
navegador. Los applets son pequeños programas comúnmente utilizados para desplegar datos
proporcionados por el servidor, gestionar entradas del usuario, o proveer funciones simples, tales
como una calculadora, que se ejecuta localmente en lugar de en el servidor. En esencia, el applet
permite a algunas funcionalidades ser movidas del servidor al cliente.
La creación de los applets cambió la programación para Internet porque expandió el
universo de objetos que pueden ser movidos libremente en el ciberespacio. En general, hay dos
muy amplias categorías de objetos que son transmitidos entre servidores y clientes: información
pasiva y programas activos. Por ejemplo, cuando usted lee su correo electrónico, usted está
viendo información pasiva. Incluso cuando usted descarga un programa, el código del programa
es sólo información pasiva hasta que usted lo ejecuta. En contraste, los applets son dinámicos,
ellos mismos se ejecutan.
www.detodoprogramacion.com
Capítulo 1:
Historia y evolución de Java
Seguridad
Como probablemente ya sabe, cada vez que transfiere un programa a su computadora corre un
riesgo, porque el código que usted está descargado podría contener virus, caballos de Troya o algún
otro código malicioso. El núcleo del problema es que ese código malicioso puede causar daños
porque está obteniendo acceso no autorizado a los recursos del sistema. Por ejemplo, un virus
podría obtener información privada, como los números de las tarjetas de crédito, estados de cuenta
bancarios y claves de acceso realizando una búsqueda en el sistema de archivos de su computadora.
Para garantizar que un applet de Java pueda ser descargado y ejecutado en la computadora del
cliente con seguridad, fue necesario evitar que un applet pudiera realizar ese tipo de acciones. Para
ello Java confina a los applets a ser ejecutados en un ambiente controlado sin permitirle el acceso a
los recursos completos de la computadora (pronto veremos cómo se logra esto). La posibilidad de
descargar applets con la certeza de que no harán ningún daño y que no producirán violaciones en
la seguridad del sistema es considerada por muchos la característica más innovadora de Java.
Portabilidad
La portabilidad es uno de los aspectos más importantes en Internet debido a la existencia
de muchos y diferentes tipos de computadoras y sistemas operativos conectados a ésta. Si
un programa de Java va a ser ejecutado sobre cualquier computadora conectada a la red, es
necesario que exista alguna forma de habilitar al programa para que se ejecute en diferentes
sistemas. Por ejemplo, en el caso de un applet, el mismo applet debe ser capaz de ser descargado
y ejecutado por una amplia variedad de CPU, sistemas operativos y navegadores conectados a
Internet. No es práctico tener diferentes versiones del applet para diferentes computadoras. El
mismo código debe funcionar en todas las computadoras. Es necesario generar código ejecutable
portable. Como veremos pronto, el mismo mecanismo que ayuda a garantizar la ejecución
segura del applet también ayuda a hacer del applet un código portable.
La magia de Java: el bytecode
La clave que permite a Java resolver ambos problemas, el de la seguridad y el de la portabilidad,
es que la salida del compilador de Java no es un código ejecutable, sino un bytecode. El bytecode
es un conjunto de instrucciones altamente optimizado diseñado para ser ejecutado por una
máquina virtual la cual es llamada Java Virtual Machine (JVM, por sus siglas en inglés). En
esencia, la máquina virtual original fue diseñada como un intérprete de bytecode. Esto puede
resultar un poco sorprendente dado que muchos lenguajes de programación modernos están
diseñados para ser compilados en código ejecutable pensando en lograr el mejor rendimiento.
No obstante, el hecho de que un programa en Java es ejecutado por la JVM ayuda a resolver los
problemas asociados con los programas basados en Web. Veamos por qué.
Traducir un programa Java en bytecode hace que su ejecución en una gran variedad de
entornos resulte mucho más sencilla, y la razón es que para cada plataforma, sólo es necesario
implementar el intérprete de Java. Una vez que el sistema de ejecución existe para un ambiente
www.detodoprogramacion.com
PARTE I
Si bien los programas dinámicos en la red son altamente deseados, también es cierto que
representan serios problemas en las áreas de seguridad y portabilidad. Un programa que se
descarga y ejecuta automáticamente en la computadora del cliente debe ser vigilado para evitar
que ocasioné daños. También debe ser capaz de correr sobre ambientes y sistemas operativos
diferentes y variados. Veamos un poco más de cerca estos puntos.
9
10
Parte I:
El lenguaje Java
determinado, cualquier programa de Java puede ejecutarse en esa plataforma. Recuerde que,
aunque los detalles de la JVM difieran de plataforma a plataforma, todas entienden el mismo
Java bytecode. Si Java fuera un lenguaje compilado a un código nativo, entonces versiones
diferentes del mismo programa deberían compilarse para cada tipo de CPU conectado al
Internet. Obviamente esa solución no es factible. Además, la ejecución del bytecode a través de
la JVM es la manera más fácil de crear código auténticamente portable.
El hecho de que Java sea interpretado también ayuda a hacerlo seguro. Como la ejecución de
cada programa de Java está bajo el control de la JVM, ésta puede contener al programa e impedir
que se generen efectos no deseados en el resto del sistema. Como se verá más adelante, ciertas
restricciones que existen en Java, sirven para mejorar la seguridad.
En general, cuando un programa es compilado a una forma intermedia y luego interpretado
por una máquina virtual, el programa se ejecuta más lento que si fuese compilado a código nativo;
sin embargo, en Java esta diferencia no es tan grande. El bytecode ha sido altamente optimizado
para habilitar a la JVM a ejecutar los programas más rápido de lo que se podría esperar.
Aunque Java fue diseñado como un lenguaje interpretado, no hay nada que impida la
compilación del bytecode en código nativo para incrementar el rendimiento. Por esta razón, Sun
comenzó a distribuir su tecnología HotSpot no mucho tiempo después del lanzamiento inicial
de Java. HotSpot proporciona un compilador de bytecode a código nativo denominado Just-inTime o simplemente JIT por sus siglas en inglés. Cuando un compilador JIT es parte de la JVM,
porciones de bytecode son compiladas en código ejecutable en tiempo real sobre un esquema
de pieza por pieza. Es importante entender que no es práctico compilar un programa de Java
completo en código ejecutable, todo de una sola vez, porque Java realiza varias revisiones en
tiempo de ejecución que no podrían ser realizados. Un compilador JIT compila código conforme
va siendo necesario, durante la ejecución. Incluso aplicando compilación dinámica al bytecode,
la portabilidad y las características de seguridad permanecen debido a que la JVM permanece a
cargo del ambiente de ejecución.
Servlets: Java en el lado del servidor
Los applets sin duda son de gran utilidad, sin embargo representan apenas la mitad de la
ecuación de los sistemas cliente/servidor. Poco tiempo después del lanzamiento inicial de Java
resultó obvio que Java también sería útil en el lado del servidor, para ello se crearon los servlets.
Un servlet es un pequeño programa que se ejecuta en el servidor. De la misma forma que los
applets extienden dinámicamente la funcionalidad del navegador Web, los servlets extienden
la del servidor Web. Con la aparición de los servlets, Java se posicionó como un lenguaje de
programación útil en ambos lados de los sistemas cliente/servidor.
Los servlets son utilizados para enviar al cliente contenido que es creado y generado
dinámicamente. Por ejemplo, una tienda en línea podría usar un servlet para buscar el precio
de un artículo en una base de datos. La información obtenida de la base de datos puede ser
utilizada para construir dinámicamente una página Web que es enviada al navegador del cliente
que solicitó la información. Si bien existen diversos mecanismos para generar contenido de
manera dinámica en el Web, tales como CGI (Common Gateway Interface), los servlets ofrecen
diversas ventajas, entre ellas un mejor rendimiento.
Los servlets son altamente portables debido a que como todos los programas de Java son
compilados a bytecode y ejecutados por una máquina virtual, esto garantiza que el mismo servlet
pueda ser utilizado en diferentes servidores. Los únicos requerimientos son que el servidor
cuente con una JVM y un contenedor de servlets.
www.detodoprogramacion.com
Capítulo 1:
Historia y evolución de Java
11
Las cualidades de Java
•
•
•
•
Simple
Seguro
Portable
Orientado a objetos
•
•
•
•
Robusto
Multihilos
Arquitectura neutral
Interpretado
• Alto rendimiento
• Distribuido
• Dinámico
Simple
Java fue diseñado con la finalidad de que su aprendizaje y utilización resultaran sencillos para el
programador profesional. Contando con alguna experiencia en programación es fácil dominar
Java. Si ya se comprenden los conceptos básicos de programación orientada a objetos, aprender
Java será aún más sencillo. Lo mejor de todo, si se tiene experiencia programando con C++,
cambiar a Java requiere sólo un poco de esfuerzo. La mayoría de los programadores de C/C++ no
tienen prácticamente ningún problema al aprender Java porque Java hereda la sintaxis y muchas
de las características orientadas a objetos de C++.
Orientado a objetos
Aunque influido por sus predecesores, Java no fue diseñado para tener un código compatible con
cualquier otro lenguaje. Esto dio la libertad al equipo de Java de partir de cero. Una consecuencia
de esto fue una aproximación clara, pragmática y aprovechable de los objetos. Java ha tomado
prestadas muchas ideas de entornos de orientación a objetos de las últimas décadas, logrando
un equilibrio razonable entre el modelo purista “todo es un objeto” y el modelo pragmático
“mantente fuera de mi camino”. El modelo de objetos en Java es sencillo y de fácil ampliación,
mientras que los tipos primitivos como los enteros, se mantienen como “no objetos” de alto
rendimiento.
Robusto
El ambiente multiplataforma de la Web es muy exigente con un programa, ya que éste debe
ejecutarse de forma fiable en una gran variedad de sistemas. Por este motivo, la capacidad para
crear programas robustos tuvo una alta prioridad en el diseño de Java. Para ganar fiabilidad, Java
restringe al programador en algunas áreas clave, con ello se consigue encontrar rápidamente los
errores en el desarrollo del programa. Al mismo tiempo, Java lo libera de tener que preocuparse
por las causas más comunes de errores de programación. Como Java es un lenguaje estrictamente
tipificado, comprueba el código durante la compilación. Sin embargo, también comprueba el
código durante la ejecución. De hecho en Java es imposible que se produzcan situaciones en las
que aparecen a menudo errores difíciles de localizar. Una característica clave de Java es que se
conoce que el programa se comportará de una manera predecible en diversas condiciones.
Para comprender la robustez de Java, consideremos dos de las causas de fallo de programa
más importantes: la gestión de memoria y las condiciones de excepción no controladas (errores
en tiempo de ejecución). La gestión de la memoria puede convertirse en una tarea difícil y
tediosa en los entornos de programación tradicionales. Por ejemplo en C/C++ el programador
www.detodoprogramacion.com
PARTE I
Ninguna discusión sobre la historia de Java está completa sin tener en cuenta las cualidades
que describen a Java. Aunque las razones fundamentales de la invención de Java fueron
la portabilidad y la seguridad, existen otros factores que también desempeñaron un papel
importante en el modelado de la forma final del lenguaje. Las consideraciones clave fueron
resumidas por el equipo de Java en la siguiente lista de términos:
12
Parte I:
El lenguaje Java
debe reservar y liberar la memoria dinámica en forma manual. Esto puede ocasionar problemas,
ya que en ocasiones los programadores olvidan liberar memoria que ha sido reservada
previamente o, peor aún, intentan liberar memoria que otra parte de su código todavía está
utilizando. Java elimina virtualmente este problema, ya que se encarga en lo interno tanto de
reservar la memoria como de liberarla. De hecho, la liberación es completamente automática,
ya que Java dispone del sistema de recolección de basura que se encarga de los objetos que ya
no se utilizan. En los entornos tradicionales, las excepciones surgen, a menudo, en situaciones
tales como la división entre cero, o “archivo no encontrado”, y se deben gestionar mediante
construcciones torpes y difíciles de leer. En esta área, Java proporciona la gestión de excepciones
orientada a objetos. En un programa de Java correctamente escrito, todos los errores de ejecución
pueden y deben ser gestionados por el programa.
Multihilo
Java fue diseñado para satisfacer los requisitos del mundo real, de crear programas en red
interactivos. Para ello, Java proporciona la programación multihilo que permite la escritura
de programas que hagan varias cosas simultáneamente. El intérprete de Java dispone de una
solución elegante y sofisticada para la sincronización de múltiples procesos que permiten
construir fácilmente sistemas interactivos. El método multihilo de Java, de utilización sencilla,
permite ocuparse sólo del comportamiento específico del programa, en lugar de pensar en el
sistema multitarea.
Arquitectura neutral
Una cuestión importante para los diseñadores de Java era la relativa a la longevidad y portabilidad
del código. Uno de los principales problemas a los que se enfrentan los programadores es que
no tienen garantía de que el programa que escriben hoy podrá ejecutarse mañana, incluso en la
misma máquina. Las actualizaciones de los sistemas operativos y los procesadores, y los cambios
en los recursos básicos del sistema, conjuntamente, pueden hacer que un programa funcione mal.
Los diseñadores de Java tomaron decisiones difíciles en el lenguaje y en el intérprete Java en un
intento de cambiar esta situación. Su meta fue “escribir una vez; ejecutar en cualquier sitio, en
cualquier momento y para siempre”. Ese objetivo se consiguió en gran parte.
Interpretado y de alto rendimiento
Como antes se ha descrito, Java permite la creación de programas que pueden ejecutarse
en diferentes plataformas por medio de la compilación en una representación intermedia
llamada código bytecode. Este código puede ser interpretado en cualquier sistema que tenga
un intérprete Java. Como ya se explicó el bytecode fue cuidadosamente diseñado para que
fuera fácil de traducir al código nativo y poder conseguir así un rendimiento alto utilizando la
característica de JIT. Los intérpretes de Java que proporcionan esta característica no pierden
ninguna de las ventajas de un código independiente de la plataforma.
Distribuido
Java fue ideado para el entorno distribuido de Internet, ya que gestiona los protocolos TCP/IP. De
hecho, acceder a un recurso utilizando un URL no es muy distinto a acceder a un archivo. Java
soporta invocación remota de métodos (RMI, por sus siglas en inglés). Esta característica permite a
un programa invocar métodos de objetos situados en computadoras diferentes a través de la red.
www.detodoprogramacion.com
Capítulo 1:
Historia y evolución de Java
13
Dinámico
La evolución de Java
La versión inicial de Java aún y cuando fue revolucionaria no marcó el fin de la era innovadora
de Java.
A diferencia de otros lenguajes de programación que normalmente se van estableciendo
a base de pequeñas mejoras incrementales, Java ha continuado evolucionando a un ritmo
explosivo. Poco después de la versión 1.0, los diseñadores ya habían creado la versión 1.1. Java
1.1 incorporaba muchos elementos nuevos en sus bibliotecas, redefinía la forma en que los
eventos eran gestionados y reconfiguraba muchas características de la biblioteca 1.0. También
declaraba obsoletas algunas de las características definidas por Java 1.0. Por lo tanto, Java 1.1
añadía y eliminaba atributos de su versión original.
La siguiente versión fue Java 2, donde el “2” indicaba “segunda generación”. La creación
de Java 2 fue un parte aguas que marcaba el comienzo de la “era moderna” de este lenguaje de
programación que evolucionaba rápidamente. La primera versión de Java 2 tenía asignado el
número de versión 1.2, cosa que puede resultar extraña. La razón es que inicialmente se refería
a las bibliotecas de Java, pero se generalizó como referencia al bloque completo. Con Java 2 la
empresa Sun re-etiquetó a Java como J2SE (Java 2 Plataform Standard Edition) y la numeración
de versiones continuó aplicándose ahora con este nombre de producto.
Java 2 añadía nuevas facilidades, tales como los componentes Swing y la estructura de
colecciones, además mejoraba la máquina virtual y varias herramientas de programación.
También declaraba obsoletos algunos elementos. Los más importantes afectaban a la clase
Thread, en la que se declaraban como obsoletos los métodos suspend( ), resume( ), y stop( ).
J2SE 1.3 fue la primera gran actualización de Java 2. En su mayor parte añade funcionalidad
y “estrecha” el entorno de desarrollo. En general, los programas escritos para la versión 1.2 y
los escritos para la versión 1.3 son compatibles. Aunque la versión 1.3 contiene un conjunto de
cambios más pequeño que las versiones anteriores, estos cambios son, no obstante, importantes.
La versión J2SE 1.4 trae consigo nuevas y modernas características. Esta versión contenía
varias actualizaciones, mejoras y adiciones importantes. Por ejemplo, agregó la nueva palabra
clave assert, excepciones encadenadas, y un subsistema basado en canales para E/S. También
realizó cambios a la estructura de colecciones y a las clases para trabajo en red. Así como
numerosos cambios pequeños realizados en todas partes. Aún con la significativa cantidad
de nuevas características, la versión 1.4 mantuvo casi 100 por ciento de compatibilidad con
versiones anteriores.
La siguiente versión de Java fue J2SE 5, y fue revolucionaria. De manera diferente a la
mayoría de las mejoras anteriores, que ofrecieron mejoras importantes, pero controladas, J2SE 5
fundamentalmente expandió el alcance, poder y rango de acción del lenguaje. Para apreciar la
magnitud de los cambios que J2SE 5 realizó a Java, veamos la siguiente lista de nuevas
características:
www.detodoprogramacion.com
PARTE I
Los programas de Java se transportan con cierta cantidad de información que se utiliza para
verificar y resolver el acceso a objetos en el tiempo de ejecución. Esto permite enlazar el código
dinámicamente de una forma segura y viable. Esto es crucial para la robustez del entorno de
Java, en el que pequeños fragmentos de bytecode pueden ser actualizados dinámicamente en un
sistema que está ejecutándose.
14
Parte I:
•
•
•
•
•
El lenguaje Java
Tipos parametrizados
Anotaciones
Autoboxing y auto-unboxing
Enumeraciones
Nueva estructura de control iterativa
•
•
•
•
Argumentos variables
Importación estática
E/S con formato
Utilerías para trabajo concurrente
Éstos no son anexos menores o actualizaciones. Cada una de estas características representa una
adición significativa al lenguaje. Los tipos parametrizados, la nueva estructura de control iterativa
y los argumentos variables introducen nuevos elementos en la sintaxis del lenguaje. Autoboxing y
auto-unboxing alteran la semántica del lenguaje. Mientras que las anotaciones añaden una nueva
dimensión a la programación. La repercución de estas nuevas características va más allá de sus
efectos directos. Estos elementos cambiaron la estructura (cualidades y características) distintivas
de Java.
El número de versión siguiente para Java habría sido normalmente 1.5. Sin embargo, las
nuevas características eran tan significativas que un cambio de 1.4 a 1.5 no habría expresado
la magnitud del cambio. Sun decidió aumentar el número de versión a 5 como una forma de
enfatizar que ocurría un acontecimiento importante. Así, la nueva versión de Java fue nombrada
J2SE 5, y las herramientas de desarrollo fueron nombradas JDK 5 (por las siglas en inglés de Java
Development Kit). Sin embargo, a fin de mantener la consistencia, Sun decidió utilizar 1.5 como
el número de versión interno, que también es conocido como el número de versión del
desarrollador en contraparte con el “5” en J2SE 5 que es conocido como el número de versión del
producto.
Java SE 6
El más reciente lanzamiento de Java se llama Java SE 6, el material en este libro ha sido
actualizado para cubrir esta versión. Con el lanzamiento de Java SE 6, Sun una vez más decidió
cambiar el nombre de Java. Primero nótese que el “2” ha sido eliminado, así que ahora el
nombre es Java SE y el nombre oficial del producto es Java Plataform, Standard Edition 6. Al igual
que con J2SE 5, el 6 en Java SE 6 es el número de versión del producto. El número de versión
interno o número de versión del desarrollador es 1.6.
Java SE 6 está construido sobre la base de J2SE 5 y añade algunas mejoras. Java SE 6 no
agrega ninguna característica impactante al lenguaje Java propiamente, sin embargo incrementa
la cantidad de bibliotecas en el API del lenguaje y realiza mejoras en el tiempo de ejecución.
En lo que respecta a este libro, los cambios en el núcleo de bibliotecas del lenguaje son los más
notables en Java SE 6. Muchos paquetes tienen nuevas clases y muchas de las clases tienen
nuevos métodos. Estos cambios se muestran a lo largo del libro. El lanzamiento de Java SE 6
contribuye a solidificar aún más los avances hechos por J2SE 5.
Una cultura de innovación
Desde sus inicios, Java ha estado en el centro de la innovación. Su versión original redefinió la
programación para Internet. La máquina virtual de Java (JVM) y el bytecode cambiaron la forma
en que concebimos la seguridad y la portabilidad. El applet (y después el servlet) le dieron vida al
Web. Los procesos de la comunidad Java (JCP por sus siglas en inglés) redefinió la forma en que
las nuevas ideas se asimilan e integran a un lenguaje. El mundo de Java siempre está en constante
movimiento y Java SE 6 es la versión más reciente producida en la dinámica historia de Java.
www.detodoprogramacion.com
2
CAPÍTULO
Introducción a Java
C
omo ocurre en otros lenguajes de programación, los elementos de Java no existen de forma
aislada, sino que trabajan conjuntamente para conformar el lenguaje como un todo. Sin
embargo, esta interrelación puede hacer difícil describir un aspecto de Java sin involucrar
a otros. A menudo, una discusión sobre una determinada característica implica un conocimiento
anterior de otra. Por esta razón, este capítulo presenta una descripción rápida de varias características
claves de Java. El material aquí descrito le proporcionará una base que le permitirá escribir y
comprender programas sencillos. La mayoría de los temas que se discuten se examinarán con más
detalle en el resto de los capítulos de la primera parte.
Programación orientada a objetos
La programación orientada a objetos (POO) es la base de Java. De hecho, todos los programas de
Java están por lo menos a un cierto grado orientados a objetos. POO es tan importante en Java que
es mejor entender sus principios básicos antes de empezar a escribir, incluso, programas sencillos en
Java. Por este motivo, este capítulo comienza con una discusión sobre aspectos teóricos de POO.
Dos paradigmas
Todos los programas consisten en dos elementos: código y datos. Además, un programa puede estar
conceptualmente organizado en torno a su código o en torno a sus datos, es decir, algunos programas
están escritos en función de “lo que está ocurriendo” y otros en función de “quién está siendo
afectado”. Éstos son los dos paradigmas que gobiernan la forma en que se construye un programa.
La primera de estas dos formas se denomina modelo orientado al proceso. Este enfoque describe un
programa como una serie de pasos lineales (es decir, un código). Se puede considerar al modelo
orientado al proceso como un código que actúa sobre los datos. Los lenguajes basados en procesos, como
C, emplean este modelo con un éxito considerable. Sin embargo, como se menciona en el Capítulo 1,
bajo este enfoque surgen problemas a medida que se escriben programas más largos y más complejos.
El segundo enfoque, denominado programación orientada a objetos, fue concebido para abordar esta
creciente complejidad. La programación orientada a objetos organiza un programa alrededor de sus
datos (es decir, objetos), y de un conjunto de interfaces bien definidas para esos datos. Un programa
orientado a objetos se puede definir como un conjunto de datos que controlan el acceso al código. Como se
verá, con este enfoque se pueden conseguir varias ventajas desde el punto de vista de la organización.
15
www.detodoprogramacion.com
16
Parte I:
El lenguaje Java
Abstracción
Un elemento esencial de la programación orientada a objetos es la abstracción. Los seres
humanos abordan la complejidad mediante la abstracción. Por ejemplo, no consideramos a un
coche como un conjunto de diez mil partes individuales, sino que pensamos en él como un
objeto correctamente definido y con un comportamiento determinado. Esta abstracción nos
permite utilizar el coche para ir al mercado sin estar agobiados por la complejidad de las partes
que lo forman. Podemos ignorar los detalles de cómo funcionan el motor, la transmisión o los
frenos, y, en su lugar, utilizar libremente el objeto como un todo.
Una forma adecuada de utilizar la abstracción es mediante el uso de clasificaciones
jerárquicas. Esto permitirá dividir en niveles la semántica de sistemas complejos,
descomponiéndolos en partes más manejables. Desde fuera, el coche es un objeto simple. Una
vez en su interior, se puede comprobar que está formado por varios subsistemas: la dirección,
los frenos, el equipo de sonido, los cinturones, la calefacción, el teléfono móvil, etc. A su vez,
cada uno de estos subsistemas está compuesto por unidades más especializadas. Por ejemplo,
el equipo de sonido está formado por un radio, un reproductor de CD y/o un reproductor de
cinta. La cuestión es controlar la complejidad del coche (o de cualquier otro sistema complejo)
mediante la utilización de abstracciones jerárquicas.
Las abstracciones jerárquicas de sistemas complejos se pueden aplicar también a los programas
de computadora. Los datos de los programas tradicionales orientados a proceso se pueden
transformar mediante la abstracción en objetos. La secuencia de pasos de un proceso se puede
convertir en una colección de mensajes entre estos objetos. Así, cada uno de esos objetos describe
su comportamiento propio y único. Se puede tratar estos objetos como entidades que responden a
los mensajes que les ordenan hacer algo. Ésta es la esencia de la programación orientada a objetos.
Los conceptos orientados a objetos forman el corazón de Java y la base de la comprensión
humana. Es importante comprender bien cómo se trasladan estos conceptos a los programas.
Como se verá, la programación orientada a objetos es un paradigma potente y natural para crear
programas que sobrevivan a los inevitables cambios que acompañan al ciclo de vida de cualquier
proyecto importante de software, incluida su concepción, crecimiento y envejecimiento. Por
ejemplo, una vez que se tienen objetos bien definidos e interfaces, para esos objetos, limpias y
fiables, se pueden extraer o reemplazar partes de un sistema antiguo sin ningún temor.
Los tres principios de la programación orientada a objetos
Todos los lenguajes orientados a objetos proporcionan los mecanismos que ayudan a
implementar el modelo orientado a objetos. Estos mecanismos son encapsulación, herencia y
polimorfismo. Veamos a continuación cada uno de estos conceptos.
Encapsulación
La encapsulación es el mecanismo que permite unir el código junto con los datos que manipula,
y mantiene a ambos a salvo de las interferencias exteriores y de un uso indebido. Una forma de
ver el encapsulado es como una envoltura protectora que impide un acceso arbitrario al código
y los datos desde un código exterior a la envoltura. El acceso al código y los datos en el interior
de la envoltura es estrictamente controlado a través de una interfaz correctamente definida.
Para establecer una semejanza con el mundo real, consideremos la transmisión automática de
un automóvil. Ésta encapsula cientos de bits de información sobre el motor, como por ejemplo
la aceleración, la superficie sobre la que se encuentra el coche y la posición de la palanca de
cambios. El usuario tiene una única forma de actuar sobre este complejo encapsulado: moviendo
www.detodoprogramacion.com
Capítulo 2:
Introducción a Java
Herencia
La herencia es el proceso por el cual un objeto adquiere las propiedades de otro objeto. Esto es
importante, ya que supone la base del concepto de clasificación jerárquica. Como se mencionó
anteriormente, una gran parte del conocimiento se trata mediante clasificaciones jerárquicas.
Por ejemplo, un labrador es parte de la clasificación de perros, que a su vez es parte de la
clasificación de mamíferos, que está contenida en una clasificación mayor, la clase animal. Sin la
utilización de jerarquías, cada objeto necesitaría definir explícitamente todas sus características.
Sin embargo, mediante el uso de la herencia, un objeto sólo necesita definir aquellas cualidades
que lo hacen único en su clase. Puede heredar sus atributos generales de sus padres. Por lo tanto,
el mecanismo de la herencia hace posible que un objeto sea una instancia específica de un caso
más general. Veamos este proceso con más detalle.
www.detodoprogramacion.com
PARTE I
la palanca de cambios. No se puede actuar sobre la transmisión utilizando las intermitentes o el
limpiaparabrisas. Por lo tanto, la palanca de cambios es una interfaz bien definida (de hecho la
única) para interactuar con la transmisión. Además, lo que ocurra dentro de la transmisión no
afecta a objetos exteriores a la misma. Por ejemplo, al cambiar de marcha no se encienden las luces.
Como la transmisión está encapsulada, docenas de fabricantes de coches pueden implementarla
de la forma que les parezca mejor. Sin embargo, desde el punto de vista del conductor, todas ellas
funcionan del mismo modo. Esta misma idea se puede aplicar a la programación. El poder del
código encapsulado es que cualquiera sabe cómo acceder al mismo y, por lo tanto, utilizarlo sin
preocuparse de los detalles de la implementación, y sin temor a efectos inesperados.
En Java, la base de la encapsulación es la clase. Aunque examinaremos con más detalle las
clases más adelante, una breve discusión sobre las mismas será útil ahora. Una clase define la
estructura y comportamiento (datos y código) que serán compartidos por un conjunto de objetos.
Cada objeto de una determinada clase contiene la estructura y comportamiento definidos por la
clase, como si se hubieran grabado en ella con un molde con la forma de la clase. Por este motivo,
algunas veces se hace referencia a los objetos como a instancias de una clase. Una clase es una
construcción lógica, mientras que un objeto tiene una realidad física.
Cuando se crea una clase, se especifica el código y los datos que constituyen esa clase.
En conjunto, estos elementos se denominan miembros de la clase. Específicamente, los datos
definidos por la clase se denominan variables miembro o variables de instancia. Los códigos
que operan sobre los datos se denominan métodos miembro o, simplemente, métodos (si está
familiarizado con C o C++, en Java un programador denomina método a lo que en C/C++ un
programador denomina función). En los programas correctamente escritos en Java, los métodos
definen cómo se pueden utilizar las variables miembro. Esto significa que el comportamiento y la
interfaz de una clase están definidos por los métodos que operan sobre sus datos de instancia.
Dado que el propósito de una clase es encapsular la complejidad, existen mecanismos para
ocultar la complejidad de la implementación dentro de una clase. Cada método o variable dentro
de una clase puede declararse como privada o pública. La interfaz pública de una clase representa
todo lo que el usuario externo necesita o puede conocer. A los métodos y datos privados sólo se
puede acceder por el código miembro de la clase. Por consiguiente, cualquier código que no sea
miembro de la clase no tiene acceso a un método o variable privado. Puesto que los miembros
privados de una clase sólo pueden ser accesados por otras partes del programa a través de los
métodos públicos de la clase, eso asegura que no ocurran acciones impropias. Evidentemente,
esto significa que la interfaz pública debe ser diseñada cuidadosamente para no exponer
demasiado los trabajos internos de una clase (véase la Figura 2.1).
17
18
Parte I:
El lenguaje Java
FIGURA 2.1
Encapsulación: se
pueden utilizar
métodos públicos
para proteger datos
privados.
Clase A
Variables
de instancia pública
(no recomendadas)
Métodos
públicos
Métodos
privados
Variables
de instancia privada
Para muchas personas es natural considerar que el mundo está compuesto por objetos
relacionados unos con otros de forma jerárquica, tal como los animales, los mamíferos y los
perros. Si se quisiera describir a los animales de forma abstracta, se diría que tienen ciertos
atributos, como tamaño, inteligencia y tipo de esqueleto. Los animales presentan también
aspectos relativos al comportamiento, comen, respiran y duermen. Esta descripción de atributos
y comportamiento es la definición de la clase de los animales.
Si se quisiera describir una clase más específica de animales, tales como los mamíferos, habría
que indicar sus atributos específicos, como el tipo de dientes y las glándulas mamarias. A esto se
denomina una subclase de animales, y la clase animal es una superclase de los mamíferos.
Como los mamíferos son simplemente unos animales especificados con más precisión,
heredan todos los atributos de los animales. Una subclase hereda todos los atributos de cada uno
de sus predecesores en la jerarquía de clases.
Animal
Mamífero
Canino
Doméstico
Retriever
Labrador
Reptil…
Felino…
Lupus…
Poodle…
Golden
www.detodoprogramacion.com
Capítulo 2:
Introducción a Java
Polimorfismo
El polimorfismo (del griego, “muchas formas”) es una característica que permite que una interfaz
sea utilizada por una clase general de acciones. La acción específica queda determinada por la
#
&
$
(
'
!
)
%
#
)
$
"
) *
! "
"
"#
FIGURA 2.2. La clase Labrador hereda los elementos encapsulados de todas sus superclases.
www.detodoprogramacion.com
PARTE I
La herencia interactúa también con la encapsulación. Si una determinada clase encapsula
determinados atributos, entonces cualquier subclase tendrá los mismos atributos más cualquiera
que añada como parte de su especialización (véase la Figura 2.2). Éste es un concepto clave que
permite a los programas orientados a objetos crecer en complejidad linealmente, en lugar de
geométricamente. Una nueva subclase hereda todos los atributos de todos sus predecesores.
Esto elimina interacciones impredecibles con gran parte del resto del código en el sistema.
19
20
Parte I:
El lenguaje Java
naturaleza exacta de la situación. Consideremos una pila (que es una lista en la que el último
elemento que entra es el primero que sale). Podríamos tener un programa que requiera tres
tipos distintos de pilas. Una para valores enteros, otra para valores en punto flotante, y la última
para caracteres. El algoritmo que implementa cada pila es el mismo, incluso aunque los datos
almacenados sean diferentes. En un lenguaje no orientado a objetos sería necesario crear tres
conjuntos diferentes de rutinas de pila, cada una con un nombre distinto. Sin embargo, gracias al
polimorfismo, en Java se puede especificar un conjunto general de rutinas de pila que compartan
los mismos nombres.
De manera más general, el concepto de polimorfismo se expresa a menudo mediante
la frase “una interfaz, múltiples métodos”. Esto significa que es posible diseñar una interfaz
genérica para un grupo de actividades relacionadas. Esto ayuda a reducir la complejidad
permitiendo que la misma interfaz sea utilizada para especificar una clase general de acciones. Es
tarea del compilador seleccionar la acción específica (esto es, el método) que corresponde a cada
situación. El programador no necesita hacer esta selección manualmente sólo recordar y utilizar
la interfaz general.
Continuando con el ejemplo del perro, el sentido del olfato es polimórfico. Si el perro huele
un gato, ladrará y correrá detrás de él. Si el perro huele comida, producirá saliva y correrá hacia
su plato. El mismo sentido del olfato está funcionando en ambas situaciones. La diferencia está
en lo que el perro huele, es decir, el tipo de dato sobre los que opera el olfato del perro. El mismo
concepto general se implementa en Java cuando se aplican métodos dentro de un programa Java.
Polimorfismo, encapsulación y herencia trabajan juntos
Cuando se aplican adecuadamente, el polimorfismo, la encapsulación y la herencia dan lugar a
un entorno de programación que facilita el desarrollo de programas más robustos y fáciles de
ampliar que el modelo orientado a procesos. Una jerarquía de clase correctamente diseñada es
la base que permite reutilizar un código en cuyo desarrollo y pruebas se han invertido tiempo y
esfuerzo. La encapsulación permite trasladar las implementaciones en el tiempo sin tener que
modificar el código que depende de las interfaces públicas de las clases. El polimorfismo permite
crear un código claro, razonable, legible y elástico.
De los dos ejemplos del mundo real presentados, el del automóvil ilustra de forma más
completa la potencia del diseño orientado a objetos. Es divertido pensar en los perros desde
el punto de vista de la herencia, pero los coches se parecen más a los programas. Todos los
conductores confían en la herencia para conducir diferentes tipos de vehículos (subclases). Tanto
si el vehículo es un autobús escolar, un Mercedes sedán, un Porsche o un coche familiar, todos
los conductores pueden encontrar y accionar más o menos el volante, los frenos o el acelerador.
Después de cierta práctica con el mecanismo de cambio de velocidades, la mayoría de las
personas pueden incluso superar la diferencia entre el cambio manual y el automático, ya que
fundamentalmente conocen su superclase común, la transmisión.
Los conductores interactúan constantemente con características encapsuladas del automóvil.
El freno y el acelerador son interfaces tremendamente sencillas sobre las que operan los
pies, cuando se tiene en cuenta toda la complejidad que se esconde detrás de las mismas. La
fabricación del motor, el estilo de los frenos y el tamaño de los neumáticos no tienen efecto
alguno en la forma en que interactuamos con la definición de la clase de los pedales.
El último atributo, el polimorfismo, se refleja claramente en la capacidad de los fabricantes de
coches de ofrecer una amplia gama de versiones del mismo vehículo básico. Por ejemplo, se puede
elegir entre un coche con sistema de frenos ABS o con los frenos tradicionales, dirección asistida o
normal y motor de 4, 6 u 8 cilindros. Cualquiera que sea el modelo elegido, habrá que presionar el
www.detodoprogramacion.com
Capítulo 2:
Introducción a Java
Un primer programa sencillo
Una vez discutidos los pilares básicos de la orientación a objetos de Java, veamos programas de
Java reales. Comencemos compilando y ejecutando el siguiente programa ejemplo, que, como se
verá, supone algo más de trabajo de lo que se podría imaginar.
/*
Este es un programa simple en Java.
Este archivo se llama "Ejemplo.java".
*/
class Ejemplo {
// El programa comienza con una llamada a main ().
public static void main(String args []) {
System.out.println("Este es un programa simple en Java.");
}
}
NOTA
Las descripciones que siguen utilizan las herramientas de desarrollo de Java versión 6 de la
empresa Sun Microsystems, (JDK 6, por sus siglas en inglés, Java Development Kit). Si se utiliza
un entorno de desarrollo de Java diferente, puede ser necesario seguir un procedimiento distinto
para compilar y ejecutar los programas de Java. En ese caso, habrá que consultar los manuales de
usuario del compilador utilizado.
Escribiendo el programa
En la mayor parte de los lenguajes de programación, el nombre del archivo que contiene el
código fuente de un programa es irrelevante. Sin embargo, en Java esto no es así. La primera
cuestión que hay que aprender en Java es que el nombre del archivo fuente es muy importante.
Para este ejemplo, el nombre del archivo fuente debe ser Ejemplo.java. Veamos por qué.
En Java, un archivo fuente se denomina oficialmente unidad de compilación. Es un archivo de
texto que contiene una o más definiciones de clase. El compilador Java requiere que el archivo
fuente utilice la extensión .java en el nombre del archivo.
Volviendo al programa, el nombre de la clase definida por el programa es también
Ejemplo. Esto no es una coincidencia. En Java, todo código debe residir dentro de una clase.
Por convención, el nombre de esa clase debe ser el mismo que el del archivo que contiene el
www.detodoprogramacion.com
PARTE I
freno para parar, girar el volante para cambiar de dirección y presionar el acelerador para comenzar
a moverse. La misma interfaz se puede utilizar para controlar distintas implementaciones.
Como se puede ver, mediante la aplicación del encapsulado, herencia y polimorfismo, las
partes individuales se transforman en el objeto que conocemos como un coche. Lo mismo se
puede decir de un programa de computadora. Por medio de la aplicación de los principios de
la orientación a objetos, las diferentes partes de un programa complejo se unen para formar un
todo cohesionado, robusto y sostenible.
Como se mencionó al comienzo de esta sección, todo programa en Java está orientado a
objetos. O, de una forma más precisa, cada programa en Java implica encapsulación, herencia
y polimorfismo. Aunque los ejemplos cortos que aparecen en el resto del capítulo y en los
próximos capítulos puede que no exhiban claramente estas características, sin embargo, éstas
están presentes. La mayor parte de las características de Java residen en sus bibliotecas de clases,
que utilizan de forma amplia la encapsulación, la herencia y el polimorfismo.
21
22
Parte I:
El lenguaje Java
programa. También es preciso asegurarse de que coinciden las letras mayúsculas y minúsculas
del nombre del archivo y de la clase.
La razón es que Java distingue entre mayúsculas y minúsculas. En este momento, la convención
de que los nombres de los archivos correspondan exactamente con los nombres de las clases puede
parecer arbitraria. Sin embargo, esto facilita el mantenimiento y organización de los programas.
Compilando el programa
Para compilar el programa Ejemplo, se ejecuta el compilador, javac, especificando el nombre del
archivo fuente en la línea de comandos, tal y como se muestra a continuación.
C:\>javac Ejemplo.java
El compilador javac crea un archivo llamado Ejemplo.class, que contiene la versión del programa
en bytecode. Como se dijo anteriormente, el bytecode es la representación intermedia del programa
que contiene las instrucciones que el intérprete o máquina virtual de Java ejecutará. Por tanto, el
resultado de la compilación con javac no es un código que pueda ser directamente ejecutado.
Para ejecutar el programa realmente, se debe utilizar el intérprete denominado java. Para
ello se pasa el nombre de la clase, Ejemplo, como argumento a la línea de comandos.
C:\>java Ejemplo
Cuando se ejecuta el programa, se despliega la siguiente salida:
Este es un programa simple en Java.
Cuando se compila código fuente, cada clase individual se almacena en su propio archivo,
con el mismo nombre de la clase y utilizando la extensión .class. Ésta es la razón por la que
conviene nombrar los archivos fuente con el mismo nombre que la clase que contienen, ya que
así el nombre del archivo fuente coincidirá con el nombre del archivo .class. Cuando se ejecute el
intérprete de java, se especificará realmente el nombre de la clase que se quiere que el intérprete
ejecute. El intérprete automáticamente buscará un archivo con ese nombre y con la extensión
.class. Si encuentra el archivo, ejecutará el código contenido en la clase especificada.
Análisis detallado del primer programa de prueba
Aunque Ejemplo.java es bastante corto, incluye varias de las características clave comunes a
todos los programas en Java. Examinemos con más detalle cada parte del programa.
El programa comienza con las siguientes líneas:
/*
Este es un programa simple en Java.
Este archivo se llama "Ejemplo.java".
*/
Esto es un comentario. Como en la mayoría de los lenguajes de programación, Java permite
introducir notas en el archivo fuente del programa. El contenido de un comentario es ignorado
por el compilador. Un comentario describe o explica la operación del programa a cualquiera que
esté leyendo el código fuente. En este caso, el comentario describe el programa y recuerda que el
archivo fuente debe llamarse Ejemplo.java. Naturalmente, en aplicaciones reales, los
comentarios generalmente explican cómo funcionan o qué hace alguna parte del programa.
www.detodoprogramacion.com
Capítulo 2:
Introducción a Java
class Ejemplo {
Esta línea utiliza la palabra clave class para declarar que se está definiendo una nueva clase.
Ejemplo es un identificador y el nombre de la clase. La definición completa de la clase,
incluyendo todos sus miembros, debe estar entre la llave de apertura ({) y la de cierre (}). De
momento, no nos preocuparemos más de los detalles de una clase, pero sí tendremos en cuenta
que todas las acciones de un programa ocurren dentro de una clase. Ésta es una razón por la que
todos los programas están orientados a objetos.
La siguiente línea del programa se muestra a continuación y es un comentario de una línea.
// El programa comienza con una llamada a main() .
Éste es el segundo tipo de comentarios que permite Java. Un comentario de una sola línea
comienza con un // y termina al final de la línea. Como regla general, los programadores utilizan
comentarios de múltiples líneas para notas más largas y comentarios de una sola línea para
descripciones breves. El tercer tipo de comentario, el comentario de documentación, será analizado
en la sección “Comentarios” más adelante en este capítulo.
A continuación se presenta la siguiente línea de código:
public static void main(String args[]) {
En esta línea comienza el método main( ). Tal y como sugiere el comentario anterior, en esta
línea comienza la ejecución del programa. Todos los programas de Java comienzan la ejecución
con la llamada al método main( ). El significado exacto de cada parte de esta línea no se
puede precisar en este momento, ya que supone un conocimiento detallado del concepto de
encapsulación en Java. Sin embargo, ya que en la mayoría de los ejemplos de la primera parte de
este libro se usa esta línea de código, veamos brevemente cada parte de esta línea.
La palabra clave public es un especificador de acceso que permite al programador controlar la
visibilidad de los miembros de una clase. Cuando un miembro de una clase va precedido por el
especificador public, entonces es posible acceder a ese miembro desde cualquier código fuera de
la clase en que se ha declarado (lo opuesto al especificador public es private, que impide el
acceso a un miembro declarado como tal desde un código fuera de su clase). En este caso, main( )
debe declararse como public, ya que debe ser llamado por un código que está fuera de su clase
cuando el programa comienza. La palabra clave static (estático) permite que se llame a main( )
sin tener que referirse a ninguna instancia particular de esa clase. Esto es necesario, ya que el
intérprete o máquina virtual de Java llama a main( ) antes de que se haya creado objeto alguno.
La palabra clave void simplemente indica al compilador que main( ) no devuelve ningún valor.
Como se verá, los métodos pueden devolver valores. No se preocupe si todo esto resulta un tanto
confuso. Todos estos conceptos se analizarán con más detalle en los capítulos siguientes.
Según lo indicado, main( ) es el primer método al que se llama cuando comienza una
aplicación Java. Hay que tener en cuenta que Java distingue entre mayúsculas y minúsculas, es
decir, que Main es distinto de main. Es importante comprender que el compilador Java compilará
www.detodoprogramacion.com
PARTE I
Java proporciona tres tipos de comentarios. El que aparece al comienzo de este programa
se denomina comentario multilínea. Este tipo de comentario debe comenzar con /* y terminar
con */. Cualquier cosa que se encuentre entre los dos símbolos de comentario es ignorada por el
compilador. Tal y como indica el nombre, un comentario multilínea puede tener varias líneas de
longitud.
A continuación se muestra la siguiente línea de código del programa:
23
24
Parte I:
El lenguaje Java
clases que no contengan un método main(), pero el intérprete de Java no puede ejecutar dichas
clases. Así, si se escribe Main en lugar de main, el compilador compilará el programa, pero el
intérprete de java enviará un mensaje de error al no poder encontrar el método main().
Cualquier información que sea necesaria pasar a un método se almacena en las variables
especificadas dentro de los paréntesis que siguen al nombre del método. A estas variables se las
denomina parámetros. Aunque un determinado método no necesite parámetros, es necesario
poner los paréntesis vacíos. En el método main( ) sólo hay un parámetro, aunque complicado.
String args[ ] declara un parámetro denominado args, que es un arreglo de instancias de la
clase String (los arreglos son colecciones de objetos similares). Los objetos del tipo String
almacenan cadenas de caracteres. En este caso, args recibe los argumentos que estén presentes
en la línea de comandos cuando se ejecute el programa. Este programa no hace uso de esta
información, pero otros programas que se presentan más adelante sí lo harán.
El último carácter de la línea es {. Este carácter señala el comienzo del cuerpo del método
main( ). Todo el código comprendido en un método debe ir entre la llave de apertura del método
y su correspondiente llave de cierre.
El método main( ) es simplemente un lugar de inicio para el programa. Un programa
complejo puede tener una gran cantidad de clases, pero sólo es necesario que una de ellas
tenga el método main( ) para que el programa comience. Cuando se comienza a crear applets
-programas Java incrustados en navegadores Web- no se utilizará el método main( ), ya que el
navegador Web utiliza un medio diferente para comenzar la ejecución de los applets.
A continuación se presenta la siguiente línea de código que está contenida dentro de main( ).
System.out.println ("Este es un programa simple en Java.");
Esta línea despliega la cadena “Este es un programa simple en Java”, seguida por una nueva
línea en la pantalla. La salida es efectuada realmente por el método println( ). En este caso, el
método println( ) despliega la cadena de caracteres que se le pasan como parámetro. También
se puede utilizar este método para visualizar información de otros tipos. La línea comienza
con System.out. Aunque su explicación resulta complicada en este momento, se puede decir
brevemente que System es una clase predefinida que proporciona acceso al sistema, y out es el
flujo de salida que está conectado a la consola.
Como probablemente ya habrá adivinado, la salida y entrada por consola no se utilizan con
frecuencia en los programas Java reales y applets. La mayor parte de las computadoras actuales
tienen entonos gráficos con ventanas, por este motivo la E/S por consola solamente se utiliza en
programas sencillos o de demostración. En capítulos posteriores se verán otras formas de generar
salidas con Java, pero, de momento, continuaremos utilizando los métodos de E/S por consola.
Observe también que la sentencia println( ) termina con un punto y coma. Todas las
sentencias de Java terminan con un punto y coma. La razón para que otras líneas del programa
no lo hagan así es que no son técnicamente sentencias.
La primera } del programa termina el método main( ), y la última } termina la definición de
la clase Ejemplo.
Un segundo programa breve
Uno de los conceptos fundamentales en cualquier lenguaje de programación es el de variable.
Una variable es un espacio de memoria con un nombre asignado, al que el programa puede
asignar un valor. El valor de la variable se puede cambiar durante la ejecución del programa. El
siguiente programa muestra cómo se declara una variable y cómo se le asigna un valor. Además,
www.detodoprogramacion.com
Capítulo 2:
Introducción a Java
/*
Este es otro ejemplo breve.
Este archivo se llama "Ejemplo2.java".
*/
class Ejemplo2 {
public static void main(String args[]) {
int num; // declara una variable llamada num
num = 100; // asigna a num el valor 100
System.out.println("Este es num: " + num);
num = num * 2;
System.out.print{"El valor de num * 2 es ");
System.out.println(num);
}
}
Al ejecutar este programa, se obtiene la siguiente salida:
Este es num: 100
El valor de num * 2 es 200
Veamos con más detalle cómo se produce esta salida. La primera línea nueva del programa es:
int num; // declara una variable llamada num
Esta línea declara una variable entera llamada num. Como muchos otros lenguajes, Java requiere
que las variables sean declaradas antes de utilizarlas.
La forma general de declaración de una variable es:
tipo nombre;
Donde tipo especifica el tipo de la variable declarada, y nombre es el nombre de la variable. Se
puede declarar más de una variable de un tipo determinado separando por comas los nombres
de las variables a declarar. Java define varios tipos de datos entre los que se pueden citar los
enteros, caracteres y punto flotante. La palabra clave int especifica un tipo entero.
En el programa, la línea
num = 100; // asigna a num el valor 100
asigna a num el valor 100. En Java, el operador de asignación es el signo igual. La siguiente línea
del código es la responsable de desplegar el valor de num precedido por la cadena de caracteres
“Esto es num:”.
System.out.println("Este es num: " + num);
En esta sentencia, el signo de suma hace que el valor de num sea añadido a la cadena que le
precede, y a continuación se despliega la cadena resultante. Lo que realmente ocurre es que num
se convierte en el carácter equivalente y después se concatena con la cadena que le precede.
Este proceso se describirá con más detalle más adelante. Este mecanismo se puede generalizar.
Utilizando el operador +, se pueden encadenar tantos elementos como se desee dentro de una
única sentencia println( ).
www.detodoprogramacion.com
PARTE I
también ilustra algunos aspectos nuevos de la salida por consola. Como indican los comentarios
de las primeras líneas, el archivo correspondiente debe llamarse Ejemplo2.java.
25
26
Parte I:
El lenguaje Java
La siguiente línea de código asigna a num el valor de num multiplicado por dos. Como en
otros lenguajes, Java utiliza el operador * para indicar multiplicación. Después de la ejecución de
esta línea, el valor almacenado en num será 200.
Las dos siguientes líneas de programa son:
System.out.print ("El valor de num * 2 es ");
System.out.println(num);
En estas dos líneas hay cosas que aparecen por primera vez. En primer lugar, el método print( )
se utiliza para presentar la cadena “El valor de num * 2 es”. Esta cadena no es seguida por una
nueva línea. Esto significa que cuando se genere una nueva salida, comenzará en la misma línea.
El método print( ) es como el método println( ), excepto que no pone el carácter de línea nueva
después de cada llamada. A continuación, en la llamada a println( ) se utiliza num para
imprimir el valor almacenado en la variable. Para la salida de valores de cualquier tipo en Java se
pueden utilizar ambos métodos, print( ) y println( ).
Dos sentencias de control
Aunque en el Capítulo 5 se examinan con más profundidad las sentencias de control, a
continuación se introducen brevemente dos de ellas para que se puedan utilizar en los
programas de ejemplo que aparecen en los capítulos 3 y 4. Además nos servirán para explicar un
importante aspecto de Java: los bloques de código.
La sentencia if
La sentencia if de Java actúa de la misma forma que la sentencia IF en cualquier otro lenguaje.
Además, es sintácticamente idéntica a las sentencias if de C, C++ y C#. A continuación se
presenta su forma más simple.
if(condición) sentencia;
donde condición es una expresión booleana. Si la condición es verdadera, entonces se ejecuta la
sentencia. Si la condición es falsa, entonces se evita la sentencia. A continuación se presenta un
ejemplo:
if(num < 100) println("num es menor que 100");
En este caso, si num contiene un valor menor que 100, la expresión condicional es
verdadera, y se ejecutará la sentencia println( ). Si num contiene un valor mayor o igual que
100, entonces no se ejecuta el método println( ).
Como se verá en el Capítulo 4, Java define un conjunto completo de operadores relacionales
que se pueden utilizar en expresiones condicionales. Algunos de éstos son:
Operador
Significado
<
Menor que
>
Mayor que
==
Igual a
Observe que para la prueba de igualdad se utiliza el doble signo igual.
www.detodoprogramacion.com
Capítulo 2:
Introducción a Java
27
El siguiente programa ejemplifica el uso de la sentencia if:
Este archivo se llama "EjemploIf.java".
*/
class EjemploIf {
public static void main(String args[]) {
int x, y;
x = 10;
y = 20;
if(x < y) System.out.println ("x es menor que y");
x = x * 2;
if(x == y) System.out.println("x es ahora igual que y");
x = x * 2;
if(x > y) System.out.println ("x es ahora mayor que y");
// Esto no desplegará nada
if(x == y) System.out.println ("esto no se verá");
}
}
La salida generada por este programa es la siguiente:
x es menor que y
x es ahora igual que y
x es ahora mayor que y
Observe otra cosa en este programa. La línea
int x, y;
declara dos variables, x e y, utilizando una lista con elementos separados por comas.
El ciclo for
Como es de sobra conocido, las sentencias de ciclos son una parte importante de prácticamente
cualquier lenguaje de programación, y Java no es una excepción. De hecho, tal y como se verá en
el Capítulo 5, Java facilita un potente surtido de construcciones de ciclos. Probablemente la más
versátil es el ciclo for. La forma más simple del ciclo for es la siguiente:
for(inicialización; condición; iteración) sentencia;
En su forma más habitual, la parte de inicialización del ciclo asigna un valor inicial a la
variable de control del ciclo. La condición es una expresión booleana que examina la variable
de control del ciclo. Si el resultado de la prueba es verdadero, el ciclo for continúa iterando. Si
es falso, el ciclo termina. La expresión de iteración determina cómo cambia la variable de control
cada vez que se recorre el ciclo. El siguiente programa sirve como ejemplo del ciclo for:
/*
Demostración del ciclo for.
www.detodoprogramacion.com
PARTE I
/*
Demostración de la sentencia if.
28
Parte I:
El lenguaje Java
Este archivo se llama "ForPrueba.java".
*/
class ForPrueba {
public static void main(String args[]) {
int x;
for(x = 0; x 9 es " + (10 > 9) );
}
La salida generada por este programa es la siguiente:
b es
b es
Esto
10 >
false
true
se ejecuta
9 es true
En este programa vale la pena observar tres cosas. En primer lugar, como se puede ver,
cuando un valor boolean es presentado por println(), lo que se imprime es “true” o “false”.
En segundo lugar, el valor de una variable del tipo boolean es suficiente por sí mismo, para el
control de la sentencia if. No es necesario escribir una sentencia if como la siguiente:
if ( b == true) …
En tercer lugar, el resultado de un operador relacional, tal como <, es un valor del tipo boolean.
Ésta es la razón de que la expresión 10 > 9 muestre el valor “true”. El conjunto de paréntesis que
encierran a 10 > 9 es necesario porque el operador + tiene prioridad sobre el operador >.
Una revisión detallada de los valores literales
En el Capítulo 2 se mencionaron brevemente los literales. Veámoslos con más detalle ahora
después de haber descrito los tipos de Java.
Literales enteros
El tipo más utilizado en los programas de Java es probablemente el de los enteros. Cualquier
valor numérico entero es un literal entero, como por ejemplo, 1, 2, 3 y 42. Éstos son valores
decimales, es decir, escritos en base 10. Existen otras dos bases que se pueden utilizar para
literales enteros, la octal (base 8) y la hexadecimal (base 16). En Java se indica que un valor es
octal porque va precedido por un 0. Por lo tanto, el valor aparentemente válido 09 producirá
un error de compilación, ya que 9 no pertenece al conjunto de dígitos utilizados en base 8 que
van de 0 a 7. Una base más utilizada por los programadores es la hexadecimal, que corresponde
claramente con las palabras de tamaño de módulo 8 tales como las de 8, 16, 32 y 64 bits. Una
constante hexadecimal se denota precediéndola por un cero-x (0x o 0X). Los dígitos que se
utilizan en base hexadecimal son del 0 al 9, y las letras de la A a la F (o de la a a la f), que
sustituyen a los números del 10 al 15.
Los literales enteros crean un valor int, que es un valor entero de 32 bits. Teniendo en cuenta
que Java es un lenguaje fuertemente tipificado, nos podríamos preguntar cómo es posible asignar
un literal entero a alguno de los otros tipos de enteros de Java byte o long, sin que se produzca
un error de incompatibilidad. Afortunadamente, estas situaciones se resuelven de forma sencilla.
Cuando se asigna una literal a una variable del tipo byte o short, no se genera ningún error si el
valor literal está dentro del rango del tipo de la variable.
Siempre es posible asignar un literal entero a una variable de tipo long. Sin embargo, para
especificar un literal de tipo long es preciso indicar de manera explícita a la computadora que el
valor literal es del tipo long. Esto se hace añadiendo la letra L mayúscula o minúscula al literal.
Por ejemplo, 0x7ffffffffffffffL o 9223372036854775807 es el mayor literal del tipo long. Un entero
www.detodoprogramacion.com
PARTE I
}
40
Parte I:
El lenguaje Java
puede ser asignado también a una variable de tipo char mientras se encuentre dentro del rango
establecido para char.
Literales con punto decimal
Los números con punto decimal representan valores decimales con un componente fraccional.
Se pueden representar utilizando las notaciones estándar o científica. La notación estándar
consiste en la parte entera, el punto decimal y la parte fraccional. Por ejemplo, 2.0, 3.14159 y
0.6667 son representaciones válidas en la notación estándar de números de punto flotante. La
notación científica utiliza además de la notación estándar un sufijo que especifica la potencia de
10 por la que hay que multiplicar el número. El exponente se indica mediante una E o e seguida
de un número decimal, que puede ser positivo o negativo; por ejemplo, 6.022E23, 3.14159E-05, y
2E+100.
Los literales de punto flotante, en Java, utilizan por omisión la precisión double.
Para especificar un literal de tipo float se debe añadir una F o f a la constante. También se
puede especificar explícitamente un literal de tipo double añadiendo una D o d. Hacerlo
así es, evidentemente, redundante. El tipo double por omisión consume 64 bits para el
almacenamiento, mientras que el tipo float es menos exacto y requiere únicamente 32 bits.
Literales booleanos
Los literales booleanos son sencillos. Existen sólo dos valores lógicos que puede tener un valor
del tipo boolean, que son los valores true y false. Estos valores no se convierten en ninguna
representación numérica. El literal true en Java no es igual a 1, ni el false igual a 0. En Java, estos
dos literales solamente se pueden asignar a variables declaradas como boolean, o utilizadas en
expresiones con operadores booleanos.
Literales de tipo carácter
Los caracteres de Java son índices dentro del conjunto de caracteres Unicode. Son valores de
16 bits que pueden ser convertidos en enteros y manipulados con operadores enteros como los
operadores de suma y resta. Un literal de carácter se representa dentro de una pareja de comillas
simples. Todos los caracteres ASCII visibles se pueden introducir directamente dentro de las
comillas, como por ejemplo, ‘a’, ‘z’, y ‘@’. Para los caracteres que resulta imposible introducir
directamente, existen varias secuencias de escape que permiten introducir al carácter deseado
como ‘\’’ para el propio carácter de comilla simple, y ‘\n’ para el carácter de línea nueva. También
existe un mecanismo para introducir directamente el valor de un carácter en base octal o
hexadecimal. Para la notación octal se utiliza la diagonal invertida seguida por el número de tres
dígitos. Por ejemplo, ‘\141’ es la letra ‘a’. Para la notación hexadecimal, se escribe la diagonal
invertida seguida de una u (\u), y exactamente cuatro dígitos hexadecimales. Por ejemplo, ‘\u0061’
es el carácter ISO-Latin-1 ‘a’, ya que el bit superior es cero. ‘\ua432’ es un carácter japonés
Katakana. La Tabla 3-1 muestra las secuencias de caracteres de escape.
Literales de tipo cadena
Los literales de tipo cadena en Java se especifican como en la mayoría de los lenguajes,
encerrando la secuencia de caracteres en una pareja de comillas dobles. Por ejemplo,
“Hola Mundo”
“dos \nlíneas”
“\”Esto está entre comillas\””
www.detodoprogramacion.com
Capítulo 3:
TABLA 3-1
Secuencias de escape
Tipos de dato, variables y arreglos
Descripción
\ddd
Carácter escrito en base octal (ddd)
\uxxxx
Carácter escrito utilizando su valor Unicode en
hexadecimal (xxxx)
\’
Comilla simple
\”
Comilla doble
\\
Diagonal
\r
Retorno de carro
\n
Nueva línea o salto de línea
\f
Comienzo de página
\t
Tabulador
\b
Retroceso
Las secuencias de escape y la notación octal/hexadecimal definidas para caracteres literales
funcionan del mismo modo en las cadenas de literales. Una cuestión importante, respecto a las
cadenas en Java, es que deben comenzar y terminar en la misma línea. No existe, como en otros
lenguajes, una secuencia de escape para continuación de la línea.
NOTA
Como sabrá, en la mayoría de los lenguajes, incluyendo C/C++, las cadenas se implementan
como arreglos de caracteres. Sin embargo, éste no es el caso en Java. Las cadenas son realmente un
tipo de objetos. Como se verá posteriormente, ya que Java implementa las cadenas como objetos,
incluye un extensivo conjunto de facilidades para manejo de cadenas que son, a la vez, potentes y
fáciles de manejar.
Variables
La variable es la unidad básica de almacenamiento en un programa Java. Una variable se define
mediante la combinación de un identificador, un tipo y un inicializador opcional. Además, todas
las variables tienen un ámbito que define su visibilidad y tiempo de vida. A continuación se
examinan estos elementos.
Declaración de una variable
En Java, se deben declarar todas las variables antes de utilizarlas. La forma básica de declaración
de una variable es la siguiente:
tipo identificador [ = valor][, identificador [= valor] ...];
El tipo es uno de los tipos de Java, o el nombre de una clase o interfaz. (Los tipos de clases e
interfaces se analizan más adelante, en la Parte 1 de este libro). El identificador es el nombre de
la variable. Se puede inicializar la variable mediante un signo igual seguido de un valor. Tenga
en cuenta que la expresión de inicialización debe dar como resultado un valor del mismo tipo (o
de un tipo compatible) que el especificado para la variable. Para declarar más de una variable del
tipo especificado, se utiliza una lista con los elementos separados por comas.
www.detodoprogramacion.com
PARTE I
Secuencia de escape
41
42
Parte I:
El lenguaje Java
A continuación se presentan ejemplos de declaraciones de variables de distintos tipos.
Observe cómo algunas de estas declaraciones incluyen una inicialización.
int a, b, c;
int d = 3, e, f = 5;
// declara tres enteros, a, b, y c.
// declara tres enteros más, inicializando d y f.
byte z = 22;
double pi = 3.14159;
char x = 'x';
// inicializa z.
// declara una aproximación de pi.
// la variable x tiene el valor 'x'.
Los identificadores elegidos no tienen nada intrínseco en sus nombres que indique su tipo.
Java permite que cualquier nombre correcto sea utilizado para declarar una variable de cualquier
tipo.
Inicialización dinámica
Aunque los ejemplos anteriores han utilizado únicamente constantes como inicializadores, Java
permite la inicialización dinámica de variables mediante cualquier expresión válida en el instante
en que se declara la variable.
A continuación se presenta un programa corto que calcula la longitud de la hipotenusa de
un triángulo rectángulo a partir de la longitud de los dos catetos.
// Ejemplo de inicialización dinámica.
class DynInit {
public static void main(String args[]) {
double a = 3.0, b = 4.0;
// Se inicializa c dinámicamente
double c = Math.sqrt(a * a + b * b);
System.out.println"La hipotenusa es " + c);
}
}
En este ejemplo se declaran tres variables locales, a, b y c. Las dos primeras, a y b, se han
inicializado mediante constantes; sin embargo, c se inicializa dinámicamente como la longitud de
la hipotenusa; calculada mediante el teorema de Pitágoras. El programa utiliza otro de los métodos
definidos en Java, sqrt( ), que es un miembro de la clase Math, para calcular la raíz cuadrada de
su argumento. El punto clave aquí es que la expresión de inicialización puede usar cualquier
elemento válido en el instante de la inicialización, incluyendo la llamada a métodos, otras variables,
o literales.
Ámbito y tiempo de vida de las variables
Hasta el momento, todas las variables que se han utilizado se han declarado al comienzo del
método main( ). Sin embargo, Java permite la declaración de variables dentro de un bloque. Tal
y como se vió en el Capítulo 2, un bloque comenzaba con una llave de apertura y terminaba con
una llave de cierre. Un bloque define un ámbito. Cada vez que se inicia un nuevo bloque, se está
creando un nuevo ámbito. Un ámbito determina qué objetos son visibles para otras partes del
programa. También determina el tiempo de vida de esos objetos.
La mayoría de lenguajes definen dos categorías generales de ámbitos: global y local. Sin
embargo, estos ámbitos tradicionales no se ajustan estrictamente al modelo orientado a objetos
de Java. En Java, los dos grandes ámbitos son el definido por las clases, y el definido por los
www.detodoprogramacion.com
Capítulo 3:
Tipos de dato, variables y arreglos
// Ejemplo de ámbito de un bloque.
class Ambito {
public static void main(String args[]) {
int x; // conocida para todo el código que está dentro de main
x = 10;
if(x == 10) ( // comienzo de un nuevo ámbito
int y = 20; // conocida solamente dentro de este bloque
// aquí, se conocen tanto x como y.
System.out.println("x e y: " + x + " " + y);
x = y * 2;
}
// y = 100; // Error! Aquí no se conoce y
// aquí todavía se conoce x.
System.out.println("x es " + x);
}
}
Como lo indican los comentarios, la variable x se declara al comienzo del ámbito del método
main( ) y es accesible para todo el código contenido dentro de main( ). Dentro del bloque if se
declara y. Como un bloque define un ámbito, y sólo es visible para el código que está dentro de
su bloque. Por ello, fuera de su bloque, la línea y = 100; tuvo que ser precedida por el símbolo
de comentario. Si se elimina este comentario, se producirá un error de compilación, ya que la
variable y no es visible fuera de su bloque. Sin embargo, dentro del bloque if se puede utilizar
la variable x, ya que dentro de un bloque (un ámbito anidado) se tiene acceso a las variables
declaradas en un ámbito exterior.
www.detodoprogramacion.com
PARTE I
métodos. Esta distinción es, de alguna manera, artificial. Sin embargo, esta distinción tiene
sentido, ya que el ámbito de la clase tiene ciertas propiedades y atributos que no se pueden
aplicar al ámbito definido por un método. Teniendo en cuenta estas diferencias, se deja para
el Capítulo 6, en el que se describen las clases, la discusión sobre el ámbito de las clases y las
variables declaradas dentro de una clase. De momento sólo examinaremos los ámbitos definidos
por un método o en un método.
El ámbito definido por un método comienza con la llave que inicia el cuerpo del método. Si
el método tiene parámetros, éstos también están incluidos en el ámbito del método. Aunque los
parámetros se analizan con más profundidad en el Capítulo 6, en este momento se puede decir
que son equivalentes a cualquier otra variable del método.
Como regla general, se puede decir que las variables declaradas dentro de un ámbito no
son visibles, es decir, accesibles, al código definido fuera de ese ámbito. Por tanto, cuando se
declara una variable dentro de un ámbito, se está localizando y protegiendo esa variable contra
un acceso no autorizado y/o modificación. Las reglas de ámbito proporcionan la base de la
encapsulación.
Los ámbitos pueden estar anidados. Por ejemplo, cada vez que se crea un bloque de código,
se está creando un nuevo ámbito anidado. Cuando esto sucede, los ámbitos exteriores encierran
al ámbito interior. Esto significa que los objetos declarados en el ámbito exterior son visibles
para el código dentro del ámbito interior. Sin embargo, no ocurre igual en el sentido opuesto, los
objetos declarados en el ámbito interior no son visibles fuera del mismo.
Para entender el efecto de los ámbitos anidados, consideremos el siguiente programa:
43
44
Parte I:
El lenguaje Java
Dentro de un bloque, las variables se pueden declarar en cualquier punto, pero sólo son
válidas después de ser declaradas. Por tanto, si se define una variable al comienzo de un
método, está disponible para todo el código contenido en el método. Por el contrario, si se
declara una variable al final de un bloque, ningún código tendrá acceso a la misma. El siguiente
fragmento de código no es válido, ya que no se puede usar la variable count antes de su
declaración:
// Este fragmento no es correcto!
count = 100; // No se puede utilizar count antes de declararla
int count;
Otro punto importante que se ha de tener en cuenta es el siguiente: las variables se crean
cuando la ejecución del programa alcanza su ámbito, y son destruidas cuando se abandona
su ámbito. Esto significa que una variable no mantiene su valor una vez que se ha salido de su
ámbito. Por tanto, las variables declaradas dentro de un método no mantienen sus valores entre
llamadas a ese método. Del mismo modo, una variable declarada dentro de un bloque pierde su
valor cuando se abandona el bloque. Es decir, el tiempo de vida de una variable está limitado por
su ámbito.
Si la declaración de una variable incluye un inicializador, entonces esa variable se reinicializa
cada vez que se entra en el bloque en que ha sido declarada. Consideremos el siguiente
programa:
// Ejemplo del tiempo de vida de una variable.
c1ass Duracion {
public static void main(String args[]) {
int x;
for(x = 0; x < 3; x++) {
int y = -1; // y se inicializa cada vez que se entra en el bloque
System.out.println("y es: " + y); // siempre se imprime -1
y = 100;
System.out.println("y es ahora: " + y);
}
}
}
La salida generada por este programa es la siguiente:
y
y
y
y
y
y
es: –1
es ahora: 100
es: –1
es ahora: 100
es: –1
es ahora: 100
Como puede verse, cada vez que se entra en el ciclo for interior, la variable y se reinicializa a –1.
Aunque a continuación se le asigne el valor 100, este valor se pierde.
Por último, aunque los bloques pueden estar anidados, no se puede declarar una variable
con el mismo nombre que otra que está en un ámbito exterior. El ejemplo que se presenta
a continuación muestra una declaración no válida de variables:
// Este programa no se compilará
class AmbitoErr {
www.detodoprogramacion.com
Capítulo 3:
Tipos de dato, variables y arreglos
}
}
Conversión de tipos
Si tiene cierta experiencia en programación, ya sabrá que es bastante común asignar un valor de
un tipo a una variable de otro tipo. Si los dos tipos son compatibles, Java realiza la conversión
automáticamente. Por ejemplo, siempre es posible asignar un valor del tipo int a una variable
del tipo long. Sin embargo, no todos los tipos son compatibles, y, por lo tanto, no cualquier
conversión está permitida implícitamente. Por ejemplo, la conversión de double a byte no está
definida. Afortunadamente, se puede obtener una conversión entre tipos incompatibles. Para
ello, se debe usar un cast, que realiza una conversión explícita entre tipos. Veamos ambos tipos
de conversión.
Conversiones automáticas de Java
Cuando datos de un tipo se asignan a una variable de otro tipo, tiene lugar una conversión
automática de tipo si se cumplen las siguientes condiciones:
• Los dos tipos son compatibles.
• El tipo destino es más grande que el tipo fuente.
Cuando se cumplen estas dos condiciones, se produce una conversión de ensanchamiento o
promoción. Por ejemplo, el tipo int siempre es lo suficientemente amplio para almacenar todos
los valores válidos del tipo byte, de manera que se realiza una conversión automática.
En este tipo de conversiones, los tipos numéricos, incluyendo los tipos enteros y de punto
flotante, son compatibles entre sí. Sin embargo, los tipos numéricos no son compatibles con los
tipos char o boolean. Además, char y boolean no son compatibles entre sí.
Como se mencionó anteriormente, Java también realiza una conversión automática de tipos
cuando se almacena una constante entera en variables del tipo byte, short, long o char.
Conversión de tipos incompatibles
Aunque la conversión automática de tipos es útil, no es capaz de satisfacer todas las necesidades.
Por ejemplo, ¿qué ocurre si se quiere asignar un valor del tipo int a una variable del tipo byte?
Esta conversión no se realiza automáticamente porque un valor del tipo byte es más pequeño
que un valor del tipo int. Esta clase de conversión se denomina en ocasiones estrechamiento, ya
que explícitamente se estrecha el valor para que se ajuste al tipo de destino.
Para realizar una conversión entre dos tipos incompatibles, se debe usar un cast. Un cast es
simplemente una conversión de tipos explícita, y tiene la siguiente forma genérica:
(tipo) valor
www.detodoprogramacion.com
PARTE I
public static void main(String args[]) {
int bar = 1;
{ // se crea un nuevo ámbito
int bar = 2; // Error de compilación, ¡la variable bar ya está definida!
}
45
46
Parte I:
El lenguaje Java
Donde tipo especifica el tipo al que se desea convertir el valor especificado. Por ejemplo, el
siguiente fragmento convierte un int en un byte. Si el valor del entero es mayor que el rango de
un byte, se reducirá al módulo (residuo de la división entera) del rango del tipo byte.
int a;
byte b;
// ...
b = (byte) a;
Una conversión diferente es la que tiene lugar cuando se asigna un valor de punto flotante
a un tipo entero. En este caso, se trunca la parte fraccionaria. Como ya se sabe, los enteros no
tienen componente fraccional. Por tanto, cuando se asigna un valor en punto flotante a un
entero, se pierde la componente fraccional. Por ejemplo, si se asigna a un entero el valor 1.23,
el valor resultante será simplemente 1, truncándose la parte fraccionaria, 0.23. Naturalmente,
si el tamaño de la componente numérica es demasiado grande para ajustarse al tipo entero de
destino, entonces ese valor se reducirá al módulo del rango del tipo de destino.
El siguiente programa ejemplifica algunas conversiones de tipo explícitas:
// Ejemplo de conversiones de tipo explícitas (cast)
class Conversion {
public static void main(String args[]) {
byte b;
int i = 257;
double d = 323.142;
System.out.println("\nConversión de int a byte.");
b = (byte) i;
System.out.println{"i y b " + i + " " + b);
System.out.println("\nConversión de double a int.");
i = (int) d;
System.out.println{"d y i " + d + " " + i);
System.out.println{"\nConversión de double a byte.");
b = (byte) d;
System.out.println("d y b " + d + " " + b);
}
}
La salida que produce este programa es:
Conversión de int a byte.
i y b 257 1
Conversión de double a int.
d y i 323.142 323
Conversión de double a byte.
d y b 323 .142 67
Veamos cada una de estas conversiones. Cuando se convierte el valor 257 a una variable
byte, el resultado es el residuo de la división entera de 257 entre 256 (el rango del tipo byte),
que en este caso es 1. Cuando se convierte d en int, se pierde su componente fraccional. Cuando
se convierte d a byte, se pierde su componente fraccionaria y se reduce su valor al módulo de
256, que en este caso es 67.
www.detodoprogramacion.com
Capítulo 3:
Tipos de dato, variables y arreglos
47
Promoción automática de tipos en las expresiones
byte a = 40;
byte b = 50;
byte e = 100;
int d = a * b / c;
El resultado del término intermedio a * b excede fácilmente el rango de sus operandos,
que son del tipo byte. Para resolver este tipo de problema, Java convierte automáticamente
cada operando del tipo byte o short al tipo int, al evaluar una expresión. Esto significa que la
subexpresión a * b se calcula utilizando tipos enteros, no bytes. Por tanto, el resultado de la
operación intermedia, 50 * 40, es válido aunque se hayan especificado a y b como del tipo byte.
Aunque las promociones automáticas son muy útiles, pueden dar lugar a errores confusos
en tiempo de compilación. Por ejemplo, este código, aparentemente correcto, ocasiona un
problema:
byte b = 50;
b = b * 2; / / Error, ¡no se puede asignar un int a un byte!
Este código intenta almacenar 50 * 2, un valor del tipo byte perfectamente válido,
en una variable byte. Sin embargo, cuando se evaluó la expresión, los operandos fueron
promocionados automáticamente al tipo int. Por tanto, el tipo de la expresión es ahora del tipo
int, y no se puede asignar al tipo byte sin utilizar la conversión explícita. Esto ocurre incluso si,
como en este caso, el valor que se intenta asignar está en el rango del tipo objetivo.
En casos como el siguiente, en que se prevén las consecuencias del desbordamiento, se
debería usar la conversión explícita,
byte b = 50;
b = (byte) (b * 2);
que conduce al valor correcto de 100.
Reglas de la promoción de tipos
Java define varias reglas para la promoción de tipos que se aplican a las expresiones. Estas reglas
son las siguientes. En primer lugar, los valores byte, short y char son promocionados al tipo
int, como se acaba de describir. Además, si un operando es del tipo long, la expresión completa
es promocionada al tipo long. Si un operando es del tipo float, la expresión completa es
promocionada al tipo float. Si cualquiera de los operandos es double, el resultado será double.
El siguiente programa muestra cómo se promociona cada valor en la expresión para
coincidir con el segundo argumento de cada operador binario:
class Promocion {
public static void main(String args[]) {
byte b = 42;
char c ='a';
short s = 1024;
www.detodoprogramacion.com
PARTE I
Las conversiones de tipo, además de ocurrir en la asignación de valores, pueden tener lugar en
las expresiones. Para ver cómo sucede esto, consideremos el siguiente caso. En una expresión,
puede ocurrir que la precisión requerida por un valor intermedio exceda el rango de cualquiera
de los operandos. Por ejemplo, en la siguiente expresión:
48
Parte I:
El lenguaje Java
int i = 50000;
float f = 5.67f;
double d = .1234;
double resultado = (f * b) + (i / e) - (d * s);
System.out.println((f * b) + " + " + (i / e) + " - " + (d * s));
System.out.println(“resultado = " + resultado);
}
}
Examinemos con más detalle todas las promociones de tipos que tienen lugar en esta línea
del programa:
double result = (f * b) + (i / c) - (d * s);
En la subexpresión, f * b, b es promocionado a float y el resultado de la subexpresión es del tipo
float. A continuación, en la subexpresión i / c, c es promocionado a int, y el resultado es del tipo
int. Luego, en d * s, el valor de s se promociona a double, y el tipo de la expresión es double.
Finalmente, considerando estos tres valores intermedios, float, int y double, el resultado de
float más un int es del tipo float. A continuación, el resultado de un float menos el último
double es promocionado a double, que es el tipo final del resultado de la expresión.
Arreglos
Un arreglo es un grupo de variables del mismo tipo al que se hace referencia por medio de un
nombre común. Se pueden crear arreglos de cualquier tipo, y pueden tener una dimensión
igual a uno o mayor. Para acceder a un elemento concreto de un arreglo se utiliza su índice.
Los arreglos ofrecen un medio conveniente para agrupar información relacionada.
NOTA Si está familiarizado con C/C++, debe tener cuidado. Los arreglos, en Java, funcionan de
forma diferente a como funcionan los arreglos en esos dos lenguajes.
Arreglos unidimensionales
Un arreglo unidimensional es, esencialmente, una lista de variables del mismo tipo. Para crear
un arreglo, primero se debe crear una variable arreglo del tipo deseado. La forma general de
declarar un arreglo unidimensional es:
tipo nombre [ ];
En donde, tipo declara el tipo base del arreglo, el cual determina el tipo de cada elemento que
conforma el arreglo. Por lo tanto, el tipo base determina qué tipo de datos almacenará el arreglo.
Por ejemplo, la siguiente línea declara un arreglo llamado dias_del_ mes con el tipo “arreglo de
int”:
int dias_del_mes[];
Aunque esta declaración establece que dias_del _mes es una variable de tipo arreglo,
todavía no existe realmente ningún arreglo. De hecho, el valor de dias_del_mes es null, null
www.detodoprogramacion.com
Capítulo 3:
Tipos de dato, variables y arreglos
nombre = new tipo[tamaño];
donde tipo especifica el tipo de datos almacenados en el arreglo, tamaño especifica el número de
elementos, el arreglo y nombre es la variable a la que se asigna el nuevo arreglo; es decir, al usar
new para reservar espacio para un arreglo, se debe especificar el tipo y número de elementos que
se van a almacenar. Al reservar espacio para los elementos del arreglo mediante new, todos los
elementos se inicializan a cero automáticamente. El siguiente ejemplo reserva espacio para un
arreglo de 12 elementos enteros y los asigna a dias_del_mes.
dias_del_mes = new int[12];
Cuando se ejecute esta sentencia, dias_del_mes hará referencia a un arreglo de 12 elementos
enteros. Además, todos los elementos del arreglo se inicializan a cero.
Resumiendo, la obtención de un arreglo es un proceso que consta de dos partes. En primer
lugar, se debe declarar una variable del tipo de arreglo deseado. En segundo lugar, se debe
reservar espacio de memoria para almacenar el arreglo mediante el operador new, y asignarlo
a la variable. En Java, la memoria necesaria para los arreglos se reserva dinámicamente. Si no le
resulta familiar el concepto de reserva dinámica, no se preocupe, se describirá con detalle más
adelante en este libro.
Una vez reservada la memoria para un arreglo, se puede acceder a un elemento concreto del
arreglo especificando su índice dentro de corchetes. Todos los índices de un arreglo comienzan
en cero. Por ejemplo, la siguiente sentencia asigna el valor 28 al segundo elemento de dias_del_
mes.
dias_del_mes[1] = 28;
La siguiente línea muestra el valor correspondiente al índice 3.
System.out.println(dias_del_mes[3]);
El siguiente programa resume las ideas anteriores, creando un arreglo con el número de días
de cada mes.
// Ejemplo de un arreglo unidimensional.
class Arreglo {
public static void main(String args[]) {
int dias_del_mes [];
dias_del_mes =new int[12];
dias_del_mes [0] = 31;
dias_del_mes [1] = 28;
dias_del_mes [2] = 31;
dias_del_mes [3] = 30;
dias_del_mes [4] = 31;
dias_del_mes [5] = 30;
www.detodoprogramacion.com
PARTE I
representa un arreglo que no tiene ningún valor. Para que dias_del_mes sea un verdadero
arreglo de enteros se debe reservar espacio utilizando el operador new y asignar este espacio a
dias_del_mes. new es un operador especial que reserva espacio de memoria.
Este operador se verá con más detalle en un capítulo posterior, pero es preciso utilizarlo
en este momento para reservar espacio para los arreglos. La forma general del operador new
cuando se aplica a arreglos unidimensionales es la siguiente:
49
50
Parte I:
El lenguaje Java
dias_del_mes [6] = 31;
dias_del_mes [7] = 31;
dias_del_mes [8] = 30;
dias_del_mes [9] = 31;
dias_del_mes [10] = 30;
dias_del_mes [11] = 31;
System.out.println("Abril tiene" + dias_del_mes [3] + " días.");
}
}
Cuando se ejecuta este programa, se imprime el número de días que tiene el mes de Abril. Como
se ha indicado, los índices de arreglos en Java, comienzan en cero, es decir, el número de días del
mes de Abril es dias_del_mes [3] o 30.
Es posible combinar la declaración de una variable de tipo arreglo con la reserva de memoria
para el propio arreglo, tal y como se muestra a continuación:
int dias_del_mes [] = new int [12];
Ésta es la declaración que normalmente se hace en los programas profesionales escritos en Java.
Los arreglos también pueden ser inicializados cuando se declaran. El proceso es el mismo
que cuando se inicializan tipos sencillos. Un inicializador de arreglo es una lista de expresiones
entre llaves separadas por comas. Las comas separan los valores de los elementos del arreglo.
Se creará un arreglo lo suficientemente grande para que pueda contener los elementos que
se especifiquen en el inicializador del arreglo. No es necesario utilizar new. Por ejemplo, el
siguiente código crea un arreglo de enteros para almacenar el número de días de cada mes:
// Versión mejorada del programa anterior.
class AutoArreglo (
public static void main(String args[]) {
int dias_del_mes [] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
System.out.println("Abril tiene" + dias_del_mes [3] +" días.");
}
}
Cuando se ejecuta este programa, se obtiene la misma salida que generó la versión anterior.
Java comprueba estrictamente que no se intente almacenar o referenciar accidentalmente
valores que estén fuera del rango del arreglo. El intérprete de Java comprueba que todos los
índices del arreglo están dentro del rango correcto. Por ejemplo, el intérprete de Java comprobará
el valor de cada índice en dias_del_mes para asegurar que se encuentran comprendidos entre
0 y 11. Si se intenta acceder a elementos que estén fuera del rango del arreglo (con índices
negativos o índices mayores que el rango del arreglo), se producirá un error en tiempo de
ejecución.
En el siguiente ejemplo se utiliza un arreglo unidimensional para calcular el valor promedio
de un conjunto de números.
// Promedia los valores de un arreglo
class Promedio (
public static void main(String args[]) {
doub1e nums[] = {10.1, 11.2, 12.3, 13.4, 14.5};
double resultado = 0;
int i;
www.detodoprogramacion.com
Capítulo 3:
Tipos de dato, variables y arreglos
51
for(i=0; i<5; i++)
resultado = resultado + nums[i];
}
Arreglos multidimensionales
En Java, los arreglos multidimensionales son realmente arreglos de arreglos. Tal y como se podría
esperar, se parecen a los arreglos multidimensionales y actúan como estos. Sin embargo,
como se verá, existen un par de sutiles diferencias. Para declarar una variable de arreglo
multidimensional, hay que especificar cada índice adicional utilizando otra pareja de corchetes.
Por ejemplo, la siguiente línea declara una variable de arreglo bidimensional denominada dosD.
int dosD[] [] = new int[4] [5];
Esta sentencia reserva espacio para un arreglo de dimensión 4 por 5, y la asigna a
dosD. Internamente esta matriz se implementa como un arreglo de arreglos del tipo int.
Conceptualmente, este arreglo se parece al mostrado en la Figura 3.1.
El siguiente programa asigna un valor numérico a cada elemento del arreglo de izquierda a
derecha y de arriba abajo, y a continuación despliega esos valores.
// Ejemplo de un arreglo bidimensional.
class ArregloDosD {
public static void main(String args[]) {
int dosD[] []= new int[4] [5];
int i, j, k = 0;
for(i=0; i<4; i++)
for{j=0; j<5; j++) {
dosD[i] [j] =k;
k++;
}
for(i=0; i<4; i++) {
for(j=0; j<5; j++)
System.out.print(dosD[i] [j] + " ");
System.out.println();
}
}
}
Este programa produce la siguiente salida:
0 1 2
5 6 7
10 11
15 16
3 4
8 9
12 13 14
17 18 19
Cuando se reserva memoria para un arreglo multidimensional, sólo es necesario especificar
la memoria para la primera dimensión, es decir, la que está más a la izquierda.
www.detodoprogramacion.com
PARTE I
System.out.println("La media es " + resultado / 5);
}
52
Parte I:
El lenguaje Java
El índice derecho determina las columnas
El índice
izquierdo
determina
las líneas
Dado: int dosD[][] = int nuevo [4][5];
FIGURA 3.1 Una vista conceptual de un arreglo bidimensional de 4 x 5.
Después se puede reservar la memoria para las restantes dimensiones. El siguiente código
reserva memoria para la primera dimensión de dosD cuando se declara la variable. El espacio
para la segunda dimensión se reserva manualmente.
int dosD[]
dosD [0] =
dosD [1] =
dosD [2] =
dosD [3] =
[] =new int[4] [];
new int[5];
new int[5];
new int[5];
new int[5];
En este caso no resulta ventajoso reservar espacio para la segunda dimensión
individualmente, sin embargo, en otros casos puede que sí lo sea. Por ejemplo, cuando se reserva
memoria para varias dimensiones de forma separada, no es necesario reservar el mismo número
de elementos para cada dimensión. Como se ha indicado anteriormente, al ser los arreglos
multidimensionales realmente arreglos de arreglos, la longitud de cada uno se puede establecer
de forma independiente. El siguiente programa crea un arreglo bidimensional en la que los
tamaños de la segunda dimensión no son iguales.
// Reserva de diferentes tamaños para la segunda dimensión.
class OtroDosD {
public static void main(String args[]) {
int dosD[] [] =new int[4][];.
dosD [0] = new int[l];
dosD [1] = new int[2];
dosD [2] = new int[3];
dosD [3] = new int[4];
int i, j, k = 0;
for(i=0; i<4; i++)
www.detodoprogramacion.com
Capítulo 3:
Tipos de dato, variables y arreglos
}
}
Este programa genera la siguiente salida:
0
1 2
3 4 5
6 7 8 9
El arreglo creado por este programa tiene una forma parecida a ésta:
En muchas aplicaciones no se recomienda el uso de arreglos multidimensionales irregulares,
porque no coincide con lo que mucha gente supone al encontrar un arreglo multidimensional.
Sin embargo, estos arreglos se pueden utilizar efectivamente en muchas situaciones. Por
ejemplo, si se necesita un arreglo bidimensional de gran tamaño pero que esté dispersamente
poblado, es decir, en el que no se van a utilizar todos sus elementos, un arreglo irregular puede
ser la solución ideal.
Se puede inicializar un arreglo multidimensional. Para ello, se encierra entre llaves el
inicializador de cada dimensión. El siguiente programa crea una matriz en la que cada elemento
contiene el producto de los índices de la fila y columna. Observe también que se pueden utilizar
expresiones y literales en los inicializadores de arreglos.
// Inicialización de un arreglo bidimensional.
class Matriz {
public static void main(String args[]) {
double m[] [] = {
{ 0*0, 1*0, 2*0, 3*0 },
{ 0*1, 1*1, 2*1, 3*1 },
{ 0*2, 1*2, 2*2, 3*2 },
{ 0*3, 1*3, 2*3, 3*3 }
www.detodoprogramacion.com
PARTE I
for(j=0; j>
Desplazamiento a la derecha
>>>
Desplazamiento a la derecha rellenando con ceros
<<
Desplazamiento a la izquierda
&=
AND a nivel de bit y asignación
|=
OR a nivel de bit y asignación
^=
OR exclusivo a nivel de bit y asignación
>>=
Desplazamiento a la derecha y asignación
>>>=
Desplazamiento a la derecha rellenando con ceros y asignación
<<=
Desplazamiento a la izquierda y asignación
Ya que los operadores a nivel de bit manipulan los bits en un entero, es importante
comprender qué efectos pueden tener esas manipulaciones sobre el valor. Concretamente, es
útil conocer cómo almacena Java los valores enteros y cómo representa números negativos. Así
que antes de continuar, revisemos brevemente estos dos temas.
Todos los tipos enteros se representan mediante números binarios con un número
distinto de bits. Por ejemplo, el valor 42, para el tipo byte, en binario es 00101010, donde
cada posición representa una potencia de dos, comenzando con 20 para el bit situado más a la
derecha. El siguiente bit a la izquierda sería 21, o 2, continuando hacia la izquierda con 22, o 4, y
seguidamente 8, 16, 32, etc. De esta forma, 42 tiene un valor 1 en los bits de las posiciones 1, 3
y 5 (contando desde 0 a partir de la derecha); así que 42 es la suma de 21 + 23 + 25, es decir, 2 + 8
+ 32.
Todos los tipos enteros excepto char son enteros con signo. Esto significa que pueden
representar tanto valores positivos como valores negativos. Java utiliza una codificación
denominada complemento a dos, lo que significa que los números negativos se representan
invirtiendo (cambiando los unos por ceros y viceversa) todos los bits en el valor y añadiendo un
1 al resultado. Por ejemplo, – 42 se representa invirtiendo todos los bits de 42, o 00101010, lo que
da lugar a 11010101, y añadiendo 1 se produce el resultado final 11010110, o – 42. Para
decodificar un número negativo, primero se invierten todos sus bits, y luego se suma 1.
Invirtiendo por ejemplo los bits de – 42, o 11010110, se obtiene 00101001, o 41, entonces
añadiendo 1 se obtiene 42.
La razón de que Java, (y la mayoría de los otros lenguajes de programación), utilice el
complemento a dos es fácil de entender cuando se considera la cuestión del cruce por cero. Con
valores del tipo byte, el cero se representa por 00000000. Utilizando el complemento a uno, es
www.detodoprogramacion.com
Capítulo 4:
Operadores
Operadores lógicos a nivel de bit
Los operadores lógicos a nivel de bit son &, |, ^ y ~. La siguiente tabla muestra el resultado de
cada operación. En la siguiente discusión, hay que tener en cuenta que los operadores a nivel de
bit se aplican a cada uno de los bits de los operandos.
A
B
A|B
A&B
A^B
~A
0
0
0
0
0
1
1
0
1
0
1
0
0
1
1
0
1
1
1
1
1
1
0
0
El operador NOT
El operador NOT unario, ~, también denominado complemento a nivel de bit, invierte todos los
bits de su operando. Por ejemplo, al aplicar el operador NOT al número 42, que tiene la siguiente
representación:
00101010
se obtiene
11010101
después de que el operador NOT es aplicado.
El operador AND
El operador AND, &, produce un bit 1 si ambos operandos son 1, y un 0 en el resto de los casos.
Como ejemplo:
00101010
& 00001111
42
15
00001010
10
www.detodoprogramacion.com
PARTE I
decir, invirtiendo simplemente todos los bits, se obtiene 11111111, que sería la representación
de un cero negativo. Pero un cero negativo no es válido en aritmética entera. Este problema
se resuelve usando el complemento a dos para representar números negativos. Al usar el
complemento a dos y añadir un 1 al complemento, se obtiene 100000000 como resultado.
Esto produce un bit 1 por la izquierda, el noveno bit, que no cabe en un valor del tipo byte, y
la representación que se obtiene para –0 es la misma que para 0. La representación para – 1
es 11111111. Aunque en el ejemplo anterior se ha utilizado un valor del tipo byte, los mismos
principios básicos siguen aplicando para cualquier tipo entero en Java.
Como Java utiliza el complemento a dos para representar números negativos, y dado
que todos los valores enteros en Java tienen signo —al aplicar los operadores a nivel de bit
se pueden producir con facilidad resultados inesperados— Por ejemplo, si se cambia el bit de
mayor orden de un valor, el resultado obtenido puede interpretarse como un número negativo,
independientemente de que éste fuera el objetivo o no. Para evitar sorpresas desagradables,
basta con recordar que el bit de mayor orden sólo se utiliza para determinar el signo.
63
64
Parte I:
El lenguaje Java
El operador OR
El operador OR, |, combina los bits de manera tal que si uno de los bits del operando es un 1,
entonces el bit resultante es un 1, tal y como se muestra a continuación:
00101010
| 00001111
42
15
00101111
47
El operador XOR
El operador XOR, ^, combina los bits de tal forma que si exactamente uno de los operandos
es 1, entonces el resultado es 1. En el resto de los casos el resultado es 0. El siguiente ejemplo
sirve para ver el efecto del operador ^. Este ejemplo también demuestra un atributo útil de
la operación XOR. Observe cómo se invierte el patrón de bits de 42 siempre que el segundo
operando tenga un bit 1. Sin embargo, si el segundo operando tiene un bit 0, el primer operando
no cambia. Esta propiedad es útil en determinadas manipulaciones de bits.
00101010
^ 00001111
42
15
00100101
37
Usando los operadores lógicos a nivel de bit
El siguiente programa demuestra el funcionamiento de los operadores lógicos a nivel de bit.
// Ejemplo de los operadores lógicos a nivel de bit.
class BitLogic {
public static void main(String args[]) {
String binary[] = {
"0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000v,
"1001", "1010", "1011", "1100", "1101", "1110", "1111"
};
int a = 3; // 0 + 2 + 1 ó 0011 en binario
int b = 6; // 4 + 2 + 0 ó 0110 en binario
int c = a | b;
int d = a & b;
int e = a ^ b;
int f = (~a & b) | (a & ~b);
int g = ~a & 0x0f ;
System.out.println("
a
System.out.println("
b
System.out.println("
a | b
System.out.println("
a & b
System.out.println("
a ^ b
System.out.println("~a & b | a & ~b
System.out.println("
~a
=
=
=
=
=
=
=
"
"
"
"
"
"
"
+
+
+
+
+
+
+
binary[a]);
binary[b]);
binary[c]);
binary[d]);
binary[e]);
binary[f]);
binary[g]);
}
}
En este ejemplo, se han elegido a y b de tal forma que tienen patrones de bit que
representan las cuatro combinaciones diferentes que se pueden tener con los dos dígitos
binarios: 0-0, 0-1, 1-0, y 1-1. En los resultados de c y d, se puede ver cómo operan los
www.detodoprogramacion.com
Capítulo 4:
Operadores
a
b
a | b
a & b
a ^ b
~a&b | a&~b
~a
=
=
=
=
=
=
=
0011
0110
0111
0010
0101
0101
1100
Desplazamiento a la izquierda
El operador de desplazamiento a la izquierda, <<, desplaza a la izquierda todos los bits de un
valor un determinado número de veces. Su forma general es:
valor << num
num especifica el número de posiciones que se desplazarán a la izquierda los bits de valor; es
decir, el operador << mueve todos los bits del valor especificado a la izquierda el número de
posiciones indicado por num. En cada desplazamiento a la izquierda el bit de mayor orden es
desplazado fuera (y perdido), y un cero incluido a la derecha. Esto significa que cuando se realiza
un desplazamiento a la izquierda en un operando del tipo int, se pierden los bits una vez que
son desplazados más allá de la posición 31. Si el operando es del tipo long, entonces los bits se
pierden después de la posición 63.
La promoción automática de tipos de Java da lugar a resultados inesperados cuando se
desplazan valores del tipo byte y short. Los valores del tipo byte y short son promocionados
a int cuando se evalúa una expresión. Además, el resultado de la misma expresión también es
del tipo int. Esto significa que el resultado de un desplazamiento a la izquierda en un valor del
tipo byte o short dará lugar a un valor int, y los bits desplazados a la izquierda no se perderán
mientras no vayan más allá de la posición 31. Asimismo, en un valor byte o short negativo el
signo se extiende cuando es promocionado a int, y los bits de mayor orden se rellenan con unos.
Por esta razón, al realizar un desplazamiento a la izquierda en un valor byte o short se debe
descartar los bits de mayor orden del resultado int. Por ejemplo, si se desplaza a la izquierda un
valor byte, ese valor será promocionado a int y después desplazado. Esto implica que se deben
descartar los tres bits más altos del resultado si lo que se quiere es el valor byte desplazado. La
manera más fácil de hacer esto es, simplemente, convertir el resultado al tipo byte. El siguiente
programa muestra este concepto:
// Desplazamiento a la izquierda en un valor byte.
class DesplazarByte {
public static void main(String args([]) {
byte a = 64, b;
int i;
i = a << 2;
b = (byte) (a << 2);
www.detodoprogramacion.com
PARTE I
operadores | y & en cada bit. Los valores asignados a e y f son iguales y muestran cómo
funciona el operador ^. El arreglo de tipo string llamado binary contiene la representación
binaria de los números del 0 al 15. En este ejemplo, se indexa el arreglo para mostrar la
representación binaria de cada resultado. El arreglo se ha construido de manera que la
representación como cadena de un valor binario n esté almacenada en binary[n]. Al valor de
~a se le hace un AND con 0x0f (0000 1111 en binario) para reducir su valor a un valor menor
a 16, y poder imprimirlo utilizando el arreglo binary. La salida de este programa es:
65
66
Parte I:
El lenguaje Java
System.out.println("Valor original de a: " + a);
System.out.println("i y b: " + i + " " + b);
}
}
La salida que produce este programa es:
Valor original de a: 64
i y b: 256 0
El valor de a es promocionado a int en la evaluación, y el desplazamiento hacia la izquierda
del valor 64 (0100 0000) dos veces da como resultado el valor 256 (1 0000 0000), que es
almacenado en i. Sin embargo, el valor contenido en b es 0 ya que, después del desplazamiento,
el bit de menor orden es 0, y su único 1 ha sido desplazado fuera.
Como el desplazamiento a la izquierda tiene el efecto de multiplicar por dos el valor original,
los programadores lo utilizan como una alternativa eficiente para realizar dicha multiplicación,
pero teniendo en cuenta que al desplazar un 1 en el bit de mayor orden (bit 31 o 63), el valor que
se obtiene es negativo. Esto se ejemplifica en el siguiente programa.
// El desplazamiento a la izquierda es una forma rápida de multiplicar por 2.
class MultPorDos {
public static void main(String args([]) {
int i;
int num = 0xFFFFFFE;
for (i=0; i<4; i++) {
num = num << 1;
System.out.println(num);
}
}
}
La salida que se obtiene es:
536870908
1073741816
2147483632
-32
El valor de partida fue elegido cuidadosamente para que después de desplazarlo cuatro
posiciones diera lugar a –32. Como puede verse, cuando se desplaza un bit 1 a la posición 31, el
número se interpreta como negativo.
Desplazamiento a la derecha
El operador de desplazamiento a la derecha, >>, desplaza todos los bits de un valor a la derecha
un número especificado de veces. Su forma general es:
valor >> num
num especifica el número de posiciones que se desplazarán a la derecha los bits de valor; es decir,
el operador >> mueve todos los bits del valor especificado el número de veces que indica num.
El siguiente fragmento de código desplaza dos posiciones a la derecha el valor 32, lo que da
por resultado un 8 que se almacena en la variable a:
www.detodoprogramacion.com
Capítulo 4:
Operadores
67
int a = 32;
a = a >> 2; // ahora a contiene 8
int a = 35;
a = a >> 2; / / ahora también se obtiene un 8 en a
Revisando la operación en binario, se ve más claramente lo que pasa:
00100011
>>2
00001000
35
8
Cada vez que se desplaza un valor a la derecha, se divide ese valor por dos y se pierde
el residuo. Esto se puede utilizar para realizar divisiones enteras entre dos con un mayor
rendimiento. Obviamente, hay que asegurar que no se pierdan bits por la derecha.
Cuando se realiza el desplazamiento a la derecha, los bits superiores (más a la izquierda)
se rellenan con el contenido previo del bit superior. A esto se denomina extensión de signo, y
sirve para preservar el signo de los números negativos cuando se realizan desplazamientos a la
derecha. Por ejemplo, –8 >> 1 es igual a –4, que expresado en binario es:
11111000
>>1
11111100
–8
–4
Es interesante observar que si se desplaza a la derecha –1, el resultado sigue siendo –1, ya
que la extensión de signo introduce un 1 en el bit de mayor orden.
En ocasiones, puede que no se quiera realizar la extensión de signo al realizar
desplazamientos a la derecha. El siguiente programa, por ejemplo, convierte un valor byte en
su representación hexadecimal tipo cadena. Observe que el valor desplazado es enmascarado
cuando se le aplica el operador AND con 0x0f para descartar cualquier bit de extensión de signo,
y este valor se puede utilizar como índice en la matriz de caracteres hexadecimales
// Enmascarando la extensión de signo.
class HexByte (
static public void main(String args[]) {
char hex[] = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
byte b = (byte) 0xf1;
System.out.println("b = 0x" + hex [ (b >> 4) & 0x0f] + hex [b & 0x0f]);
}
}
La salida de este programa es:
b = 0xf1
www.detodoprogramacion.com
PARTE I
Cuando un valor tiene bits que son desplazados fuera del rango de posiciones, esos bits se
pierden. Por ejemplo, en el siguiente fragmento de código se desplaza el valor 35 a la derecha
dos posiciones. Esto hace que los dos bits de orden más bajo se pierdan y el resultado final en a
sea 8.
68
Parte I:
El lenguaje Java
Desplazamiento a la derecha sin signo
Como hemos visto, el operador >> rellena automáticamente el bit de mayor orden con su
contenido previo cada vez que se realiza un desplazamiento, esto preserva el signo del valor.
Sin embargo, puede ser que algunas veces no se desee hacer esto; por ejemplo, si se desplaza
algo que no representa un valor numérico, entonces podría no desear la preservación del
signo. Esta situación es frecuente cuando se trabaja con valores basados en pixeles y gráficos.
En este caso interesa desplazar un cero en el bit de mayor orden, independientemente de cual
sea su valor inicial. Este desplazamiento se denomina desplazamiento sin signo. Para realizar
esta operación en Java se utiliza el operador de desplazamiento a la derecha sin signo, >>>,
que siempre desplaza ceros en el bit de mayor orden.
El siguiente fragmento de código muestra cómo funciona el operador >>>. Se asigna a a
el valor –1, que tiene una representación binaria con 32 unos. A continuación se desplaza a la
derecha 24 posiciones, rellenando los 24 bits más altos con ceros, ignorando la extensión de
signo normal. Esto hace que el valor final de a sea 255.
int a = -1;
a = a >>> 24;
Revisemos la misma operación en binario para ilustrar mejor como tiene lugar el
desplazamiento:
11111111 11111111 11111111 11111111
>>> 24
00000000 00000000 00000000 11111111
–1 en binario como un int
255 en binario como un int
A menudo, el operador >>> no es todo lo útil que puede parecer en un principio, ya que
sólo afecta a valores de 32 y 64 bits. Recuerde que los valores más pequeños son promocionados
automáticamente al tipo int. Esto significa que la extensión de signo y el desplazamiento
tienen lugar en un valor de 32 bits, en lugar de valores de 8 o 16 bits. Se puede suponer que un
desplazamiento a la derecha sin signo es un valor del tipo byte relleno con ceros a partir del bit
séptimo, pero no es así, ya que realmente es un valor de 32 bits el que se desplaza. El siguiente
programa ejemplifica este efecto:
// Desplazamiento sin signo en un valor byte.
class ByteUShift {
static public void main(String args[]) {
char hex[] = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
byte b = (byte) 0xf1;
byte c = (byte) (b >> 4);
byte d = (byte) (b >>> 4);
byte e = (byte) ((b & 0xff) >> 4);
System.out.println("
b = 0x"
+ hex[(b >> 4) & 0x0f] + hex[b & 0x0f]);
System.out.println("
b >> 4 = 0x"
+ hex[(c >> 4) & 0x0f] + hex[c & 0x0f]);
www.detodoprogramacion.com
Capítulo 4:
Operadores
}
}
La salida de este programa muestra cómo, aparentemente, el operador >>> no tiene
ningún efecto cuando opera sobre un valor byte. Para demostrar esto, se asigna arbitrariamente
a la variable b un valor negativo del tipo byte. A continuación se asigna a c el valor byte
que resulta al desplazar a la derecha cuatro posiciones b, que es 0xff teniendo en cuenta la
esperada extensión de signo. Después se asigna a d el valor byte de b desplazado a la derecha
sin signo cuatro posiciones, que se podría suponer igual a 0x0f, pero que realmente es 0xff
debido a la extensión de signo que se produce cuando se promociona b a un valor int antes del
desplazamiento. La última expresión asigna a e el valor byte de b enmascarado a 8 bits usando
el operador AND, y desplazando a la derecha cuatro posiciones, que da lugar al valor esperado
de 0x0f. Observe que para obtener d no se utiliza el operador de desplazamiento a la derecha sin
signo, ya que se conoce el estado del bit de signo después de AND.
b = 0xf1
b >> 4 = 0xff
b >>> 4 = 0xff
(b & 0xff) >> 4 = 0x0f
Operadores a nivel de bit combinados con asignación
Todos los operadores a nivel de bit binarios tienen una forma abreviada similar a la de los
operadores algebraicos, que combina la asignación con la operación a nivel de bit. Por ejemplo,
las dos sentencias siguientes, que desplazan el valor de a a la derecha en cuatro bits, son
equivalentes:
a = a >> 4;
a >>= 4;
Del mismo modo, mediante las dos sentencias siguientes, que son equivalentes, se asigna a
a la expresión a OR b a nivel de bit.
a = a | b;
a |= b;
El siguiente programa crea algunas variables enteras y utiliza la forma abreviada de los
operadores de asignación a nivel de bit para manipularlas:
class OpBitEquals {
public static void main(Stringargs[]) {
int a = 1;
int b = 2;
int c = 3;
a
b
c
a
|= 4;
>>= 1;
<<= 1;
^= c;
www.detodoprogramacion.com
PARTE I
System.out.println("
b >>> 4 = 0x"
+ hex[(d >> 4) & 0x0f] + hex[d & 0x0f]);
System.out.println("(b & 0xff) >> 4 = 0x"
+ hex[(e >> 4) & 0x0f] + hex[e & 0x0f]);
69
70
Parte I:
El lenguaje Java
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
}
}
La salida de este programa es la siguiente:
a = 3
b = 1
c = 6
Operadores relacionales
Los operadores relacionales determinan la relación que un operando tiene con otro.
Específicamente, determinan relaciones de igualdad y orden. A continuación se muestran los
operadores relacionales:
Operador
Resultado
==
Igual a
!=
Diferente de
>
Mayor que
<
Menor que
>=
Mayor o igual que
<=
Menor o igual que
El resultado de estas operaciones es un valor booleano. La aplicación más frecuente de los
operadores relacionales es en la obtención de expresiones que controlan la sentencia if y las
sentencias de ciclos.
En Java se pueden comparar valores de cualquier tipo, incluyendo los números enteros y
de punto flotante, los caracteres y valores booleanos, usando la prueba de igualdad, ==, y la
de desigualdad, !=. Observe que, la igualdad se indica utilizando dos signos igual, y no sólo
uno. (Recuerde que un signo igual es el operador de asignación). Sólo se pueden comparar
valores numéricos mediante los operadores de orden, es decir, solamente se pueden comparar
operandos enteros, de punto flotante y caracteres, para ver cuál es mayor o menor.
Como se mencionó, el resultado que produce un operador relacional es un valor de tipo
boolean. El siguiente fragmento de código, por ejemplo, es perfectamente válido:
int a = 4;
int b = 1;
boolean c = a < b;
En este caso, se almacena en c el resultado de a < b (que es la literal false).
Si usted tiene experiencia programando en C/C++ considere lo siguiente, estos dos tipos de
sentencias son muy comunes:
int done;
/ / ...
www.detodoprogramacion.com
Capítulo 4:
Operadores
71
if(!done) …//Válido en C/C++
if(done) ... //pero no en Java
PARTE I
En Java, estas dos sentencias deben ser escritas en la forma siguiente:
if (done == 0) ... // Este es el estilo de Java.
if (done != 0) ...
La diferencia está en que Java no define los valores verdadero y falso del mismo modo que
en C/C++, donde cualquier valor distinto de cero es verdadero, y el cero es falso. En Java los
valores true y false son valores no numéricos que no tienen relación con el cero o el distinto de
cero. Por esto, para comprobar si un valor es igual a cero o no, se deben emplear explícitamente
uno o más operadores relacionales.
Operadores lógicos booleanos
Los operadores lógicos booleanos que se muestran a continuación sólo operan sobre operandos
del tipo boolean. Todos los operadores lógicos binarios combinan dos valores boolean para dar
como resultado un valor boolean.
Operador
Resultado
&
AND lógico
|
OR lógico
^
XOR lógico (OR exclusivo)
||
OR en cortocircuito
&&
AND en cortocircuito
!
NOT lógico unario
&=
Asignación AND
|=
Asignación OR
^=
Asignación XOR
==
Igual a
!=
Diferente de
?:
if-then-else ternario
Los operadores lógicos booleanos, &, | , y ^, operan sobre valores del tipo boolean de
la misma forma que operan sobre los bits de un entero. El operador lógico !, invierte el estado
booleano: !true == false y !false== true. La siguiente tabla muestra el resultado de cada
operación lógica:
A
B
A|B
A&B
A^B
!A
false
false
false
false
false
true
true
false
true
false
true
false
false
true
true
false
true
true
true
true
true
true
false
false
www.detodoprogramacion.com
72
Parte I:
El lenguaje Java
El siguiente programa es casi igual que el ejemplo BitLogic mostrado anteriormente, pero
este utiliza valores lógicos del tipo boolean en lugar de bits:
// Ejemplo de los operadores lógicos booleanos.
class BoolLogic (
public static void main(String args[]) {
boolean a = true;
boolean b = false;
boolean c = a | b;
boolean d = a & b;
boolean e = a ^ b;
boolean f = (!a & b) | (a & !b);
boolean g = !a;
System.out.println("
a = " + a);
System.out.println("
b = " + b);
System.out.println("
a | b = " + c);
System.out.println("
a & b = " + d);
System.out.println("
a ^ b = " + e);
System.out.println(" !a & b | a & !b = " + f);
System.out.println("
!a = " + g);
}
}
Después de ejecutar este programa, se ve que las reglas lógicas se aplican a los valores de
tipo boolean del mismo modo que se aplicaron a los bits. La siguiente salida muestra que la
representación como cadena de los valores boolean en Java son las literales, true o false:
a
b
a | b
a & b
a ^ b
a & b | a & !b
!a
=
=
=
=
=
=
=
true
false
true
false
true
true
false
Operadores lógicos en cortocircuito
Java proporciona dos operadores lógicos booleanos que no se encuentran en muchos otros
lenguajes de programación. Se trata de versiones secundarias de los operadores AND y OR, y se
conocen como operadores lógicos en cortocircuito. En la tabla anterior se ve cómo el operador
OR da como resultado true cuando A es true, independientemente del valor de B. Del mismo
modo, el operador AND da como resultado false cuando A es false, independientemente
del valor de B. Cuando se utilizan las formas || y &&, en lugar de las formas | y & de estos
operadores, Java no evalúa el operando de la derecha si el resultado de la operación queda
determinado por el operando de la izquierda. Esto es útil cuando el operando de la derecha
depende de que el de la izquierda sea true o false. El siguiente fragmento de código, por ejemplo,
muestra cómo se puede utilizar de manera ventajosa la evaluación lógica en cortocircuito para
asegurar que la operación de división sea válida antes de evaluarla:
if (denom != 0 && num / denom > 10)
Al usar la forma en cortocircuito del operador AND (&&) no existe riesgo de que se
produzca una excepción en tiempo de ejecución si denom es igual a cero. Si esta línea de
www.detodoprogramacion.com
Capítulo 4:
Operadores
if (c == l & e++ < 100) d = 100;
Aquí, al utilizar un solo carácter, &, se asegura que la operación de incremento se aplicará a e
tanto si c es igual a 1 como si no lo es.
El operador de asignación
Aunque el operador de asignación se ha estado utilizando desde el Capítulo 2, en este momento
se puede analizar de manera más formal. El operador de asignación es un solo signo igual, =. Este
operador se comporta en Java del mismo modo que en otros lenguajes de programación. Tiene la
forma general:
var = expresión;
donde el tipo de la variable var debe ser compatible con el tipo de expresión.
El operador de asignación tiene un atributo interesante con el que puede que no esté
familiarizado: permite crear una cadena de asignaciones. Consideremos, por ejemplo,
este fragmento de código:
int x, y, z;
x = y = z = 100; // asigna a x, y, y z el valor 100
Este código, asigna a las variables x, y y z el valor 100 mediante una única sentencia. Y esto es así
porque el operador = es un operador que cede el valor de la expresión de la derecha. Por tanto,
el valor de z =100 es 100, que entonces se asigna a y, y que a su vez se asigna a x. Utilizar una
“cadena de asignaciones” es una forma fácil de asignar a un grupo de variables un valor común.
El operador ?
Java incluye un operador ternario especial que puede sustituir a ciertos tipos de sentencias ifthen-else. Este operador es ?. Puede resultar un tanto confuso en principio, pero el operador ?
resulta muy efectivo una vez que se ha practicado con él. El operador ? tiene la siguiente forma
general:
expresión1 ? expresión2 : expresión3
Donde expresión1 puede ser cualquier expresión que dé como resultado un valor del tipo boolean.
Si expresión1 genera como resultado true, entonces se evalúa la expresión2; en caso contrario se
evalúa la expresión3. El resultado de la operación ? es el de la expresión evaluada. Es necesario que
tanto la expresión2 como la expresión3 devuelvan el mismo tipo que no puede ser void.
A continuación se presenta un ejemplo de empleo del operador ?:
resultado = denom == 0 ? 0 : num / denom;
www.detodoprogramacion.com
PARTE I
código se escribiera utilizando la versión sencilla del operador AND, &, se deberían evaluar
ambos operandos, y se produciría una excepción cuando el valor de denom fuera igual a cero.
Es una práctica habitual el usar las formas en cortocircuito de los operadores AND y OR
en los casos de lógica booleana, y dejar la versión de un sólo carácter de estos operadores
exclusivamente para las operaciones a nivel de bit. Sin embargo, hay excepciones a esta regla.
Consideremos, por ejemplo, la siguiente sentencia:
73
74
Parte I:
El lenguaje Java
Cuando Java evalúa esta expresión de asignación, en primer lugar examina la expresión a la
izquierda de la interrogación. Si denom es igual a cero, se evalúa la expresión que se encuentra
entre la interrogación y los dos puntos y se toma como valor de la expresión completa. Si denom
no es igual a cero, se evalúa la expresión que está detrás de los dos puntos y se toma como
valor de la expresión completa. Finalmente, el resultado del operador ? se asigna a la variable
resultado.
El siguiente programa es un ejemplo del operador ?, que se utiliza para obtener el valor
absoluto de una variable.
// Ejemplo del operador ?
class Ternario {
public static void main(String args[]) {
int i. k;
i = 10;
k = i < 0 ? -i : i; // se obtiene el valor absoluto de i
System.out.print("Va1or absoluto de ");
System.out.println(i + " es " + k);
i = -10;
k = i < 0 ? -i : i; // se obtiene el valor absoluto de i
System.out.print("Valor absoluto de" );
System.out.println(i + " es " + k);
}
}
La salida que se obtiene es:
El valor absoluto de 10 es 10
El valor absoluto de -10 es 10
Precedencia de operadores
La Tabla 4.1 muestra el orden de precedencia de los operadores de Java, desde la más alta a la
más baja. Observe que la primera fila presenta elementos a los que normalmente no se considera
como operadores: paréntesis, corchetes y el operador punto. Técnicamente, éstos son llamados
separadores, pero ellos actúan como operadores en una expresión. Los paréntesis son usados
para alterar la precedencia de una operación. Después de haber visto los capítulos anteriores,
ya sabemos que los corchetes se utilizan para indexar arreglos. El operador punto se utiliza para
acceder a los elementos contenidos en un objeto y se discutirá más adelante.
El uso de paréntesis
Los paréntesis aumentan la prioridad de las operaciones en su interior. Esto es necesario para
obtener el resultado deseado en muchas ocasiones. Consideremos la siguiente expresión:
a >> b + 3
En esta expresión, en primer lugar se añaden 3 unidades a b y después se desplaza a a la
derecha tantas posiciones como el resultado de la suma anterior. Esta expresión se puede escribir
también utilizando paréntesis:
a >> (b + 3)
www.detodoprogramacion.com
Capítulo 4:
TABLA 4.1
Precedencia
más alta
()
[]
.
++
––
~
*
/
%
+
–
>>
>>>
<<
>
>=
<
==
!=
!
<=
&
^
|
&&
||
?:
=
op=
Sin embargo, si en primer lugar, se quiere desplazar a a la derecha las posiciones que
indique b, y después añadir 3 al resultado, será necesario utilizar paréntesis.
(a >> b) + 3
Además de cambiar la prioridad de un operador, los paréntesis se utilizan en algunas
ocasiones para hacer más claro el significado de una expresión. Para cualquiera que lea su
código, una expresión compleja puede ser difícil de entender. Añadir paréntesis puede ser
redundante, pero ayuda a que expresiones complejas resulten más claras, evitando posibles
confusiones posteriores. Por ejemplo, ¿cuál de las siguientes expresiones es más fácil de leer?
a | 4 + c >> b & 7
(a | ( ( (4 + c) >> b) & 7) )
Una cuestión más: los paréntesis, redundantes o no, no degradan el funcionamiento de un
programa. Por lo tanto, añadir paréntesis para reducir ambigüedades no afecta negativamente al
programa.
www.detodoprogramacion.com
75
PARTE I
Precedencia de
los Operadores en Java
Operadores
www.detodoprogramacion.com
5
CAPÍTULO
Sentencias de control
U
n lenguaje de programación utiliza sentencias de control para hacer que el flujo de ejecución
avance y se bifurque en función de los cambios de estado en el programa. Las sentencias de
control para programas en Java pueden ser clasificadas en las siguientes categorías: selección,
iteración y salto. Las sentencias de selección permiten al programa elegir diferentes caminos de
ejecución con base en el resultado de una expresión o en el estado de una variable. Las sentencias
de iteración permiten al programa ejecutar repetidas veces una o más sentencias (las sentencias de
iteración constituyen los ciclos). Finalmente, las sentencias de salto hacen posible que el programa se
ejecute de una forma no lineal. En este capítulo se examinan las sentencias de control de Java.
Sentencias de selección
Java admite dos sentencias de selección: if y switch. Estas sentencias permiten controlar el flujo de
ejecución del programa basado en función de condiciones conocidas únicamente durante el tiempo
de ejecución. Se sorprenderá gratamente de la potencia y flexibilidad de estas dos sentencias.
if
La sentencia if se introdujo en el Capítulo 2 y se examina con detalle en este capítulo, if es la
sentencia de bifurcación condicional de Java. Se puede utilizar para dirigir la ejecución del programa
hacia dos caminos diferentes. El formato general de la sentencia if es:
if (condición) sentencia1;
else sentencia2;
Cada sentencia puede ser una sentencia única o un conjunto de sentencias encerradas entre llaves,
es decir, un bloque. La condición es cualquier expresión que devuelva un valor booleano. La cláusula
else es opcional.
La sentencia if funciona del siguiente modo: Si la condición es verdadera, se ejecuta la sentencia1.
En caso contrario se ejecuta la sentencia2 (si es que existe). En ningún caso se ejecutarán ambas
sentencias. Las siguientes líneas muestran un ejemplo en el que se utiliza la sentencia if.
int a, b;
// ...
if (a < b) a = 0;
else b = 0;
77
www.detodoprogramacion.com
78
Parte I:
El lenguaje Java
Si a es menor que b, entonces a se hace igual a cero. En caso contrario, b se hace igual a cero. En
ningún caso se asignará a ambas variables el valor cero.
Con mucha frecuencia, la expresión que se utiliza para controlar la sentencia if involucrará
operadores relacionales. Sin embargo, esto no es técnicamente necesario. Es posible controlar la
sentencia if utilizando una sola variable booleana como se muestra en el siguiente fragmento
de código:
boolean datosDisponibles;
// ...
if (datosDisponibles)
procesarDatos();
else
esperarDatos () ;
Recuerde que sólo una sentencia puede aparecer inmediatamente después del if o del
else. Si se quiere incluir más sentencias, es necesario crear un bloque tal y como se hace a
continuación:
int bytesDisponibles;
// ...
if (bytesDisponibles > 0) {
procesarDatos () ;
bytesDisponibles -= n;
} else
esperarDatos ( ) ;
Aquí, las dos sentencias contenidas en el bloque if serán ejecutadas si bytesDisponibles es
mayor que cero.
Algunos programadores estiman conveniente utilizar las llaves siempre que utilizan la
sentencia if, incluso aunque sólo haya una sentencia en la cláusula. Esto facilita añadir otras
sentencias en un momento posterior y no hay que preocuparse por haber olvidado las llaves. De
hecho, una causa bastante común de errores es olvidar definir un bloque cuando es necesario.
En el siguiente fragmento de código se muestra un ejemplo:
int bytesDisponibles;
// ...
if (bytesDisponibles > 0){
procesarDatos();
bytesDisponibles -= n;
}else
esperarDatos();
bytesDisponibles = n;
Parece evidente que la sentencia bytesDisponibles = n; debía haber sido ejecutada dentro
de la cláusula else teniendo en cuenta su nivel de identación. Sin embargo, como recordará, un
espacio en blanco es insignificante para Java y no es posible que el compilador reconozca qué se
quería hacer en realidad. Este código compilará correctamente pero se comportará de manera
errónea cuando se ejecute.
El ejemplo anterior se corrige en el código que sigue a continuación:
int bytesDisponibles;
// ...
www.detodoprogramacion.com
Capítulo 5:
Sentencias de control
PARTE I
if (bytesDisponibles > 0) {
procesarDatos();
bytesDisponibles -= n;
} else {
esperarDatos();
bytesDisponibles = n;
}
if anidados
Un if anidado es una sentencia if que está contenida dentro de otro if o else. Los if anidados
son muy habituales en programación. Cuando se anidan if lo más importante es recordar que
una sentencia else siempre corresponde a la sentencia if más próxima dentro del mismo bloque
y que no esté ya asociada con otro else. Veamos un ejemplo:
if (i == 10) {
if (j < 20) a = b;
if (k > 100) c = d;
else a =c;
}
else a = d;
79
// este if está
// asociado con este else
// este else se refiere a if (i == 10)
Tal como indican los comentarios, el else final no está asociado con if (j < 20), ya que no están
dentro del mismo bloque (aunque se trate del if más próximo sin un else). La sentencia else
final está asociada con if (i == 10). El else interior corresponde al if (k > l00), ya que éste es el if
más próximo dentro del mismo bloque.
if-else-if múltiples
Una construcción muy habitual en programación es la de if-else-if múltiples. Esta construcción se
basa en una secuencia de if anidados. Su formato es el siguiente:
if (condición)
sentencia;
else if (condición)
sentencia;
else if (condición)
sentencia;
.
.
.
else
sentencia;
La sentencia if se ejecuta de arriba abajo. Tan pronto como una de las condiciones que controlan
el if sea true, las sentencias asociadas con ese if serán ejecutadas, y el resto ignoradas. Si
ninguna de las condiciones es verdadera, entonces se ejecutará el else final. El else final actúa
como una condición por omisión, es decir, si todas las demás pruebas condicionales fallan,
entonces se ejecutará la sentencia del último else.
www.detodoprogramacion.com
80
Parte I:
El lenguaje Java
Si no hubiera un else final y todas las demás condiciones fueran false, entonces no se
ejecutará ninguna acción.
El siguiente programa utiliza un if-else-if múltiple para determinar en qué estación se
encuentra un mes particular.
// Ejemplo de sentencias if-e1se-if.
c1ass IfElse {
pub1ic static void main (String args[]) {
int mes = 4; // Abril
String estacion;
if (mes == 12 || mes == 1 || mes == 2)
estacion = "Invierno";
e1se if (mes == 3 || mes == 4 || mes == 5)
estacion = "Primavera";
e1se if (mes == 6 || mes == 7 || mes == 8)
estacion = "Verano";
e1se if (mes == 9 || mes == 10 || mes == 11)
estacion = "Otoño";
else
estacion = "Mes desconocido";
System.out.println ("Abril está en " + estación + ".");
}
}
Ésta es la salida que se obtiene al ejecutar este programa:
Abril está en Primavera.
Analicemos este programa antes de continuar. Se puede comprobar que
independientemente del valor de mes, sólo se ejecutará una sentencia de asignación.
switch
La sentencia switch es una sentencia de bifurcación múltiple de Java. Esta sentencia proporciona
una forma sencilla de dirigir la ejecución a diferentes partes del programa en función del valor
de una expresión. Así, en muchas ocasiones, es una mejor alternativa que una larga serie de
sentencias if-else-if. El formato general de una sentencia switch es:
switch (expresión) {
case valorl:
// secuencia de sentencias
break;
case valor2:
// secuencia de sentencias
break;
.
.
.
case valorN:
// secuencia de sentencias
break;
www.detodoprogramacion.com
Capítulo 5:
default:
// secuencia de sentencias por omisión
La expresión debe ser del tipo byte, short, int o char; cada uno de los valores especificados
en las sentencias case debe ser de un tipo compatible con el de la expresión. (También puede
utilizar una enumeración para controlar un sentencia switch. Las enumeraciones son descritas
en el Capítulo 12). Cada uno de estos valores debe ser un literal único, es decir, una constante no
una variable. No se permite que aparezcan valores duplicados en las sentencias case.
La sentencia switch funciona de la siguiente forma: se compara el valor de la expresión con
cada uno de los valores constantes que aparecen en las sentencias case. Si coincide con alguno,
se ejecuta el código que sigue a la sentencia case. Si ninguna de las constantes coincide con el
valor de la expresión, entonces se ejecuta la sentencia default. Sin embargo, la sentencia default
es opcional. Si ningún case coincide y no existe la sentencia default, no se ejecuta ninguna
acción.
La sentencia break se utiliza dentro del switch para terminar una secuencia de sentencias.
Cuando aparece una sentencia break, la ejecución del código se bifurca hasta la primera línea
que se encuentra después de la sentencia switch. El efecto que se consigue es el de “saltar fuera”
del switch.
A continuación se presenta un ejemplo sencillo de la sentencia switch:
// Un ejemplo sencillo de switch.
class EjemploSwitch {
public static void main (String args[]) {
for (int i=0; i<6; i++)
switch (i) {
case 0:
System.out.println ("i es cero.");
break;
case 1:
System.out.println ("i es uno.");
break;
case 2:
System.out.println ("i es dos.");
break;
case 3:
System.out.println ("i es tres");
break;
default:
System.out.println ("i es mayor que 3.");
}
}
}
La salida que tiene lugar cuando se ejecuta este fragmento de código es la siguiente:
i
i
i
i
i
i
es
es
es
es
es
es
cero.
uno.
dos.
tres.
mayor que 3.
mayor que 3.
www.detodoprogramacion.com
81
PARTE I
}
Sentencias de control
82
Parte I:
El lenguaje Java
Como se puede ver, cada vez que se ejecuta el ciclo se ejecutan las sentencias asociadas con la
constante case que coincide con i. Todas las demás son ignoradas. Cuando i es mayor que 3,
ninguna constante case coincide, y se ejecuta la sentencia default.
La sentencia break es opcional. Si se omite break, la ejecución continúa hasta el siguiente
case. A veces, es conveniente tener múltiples sentencias case sin ninguna sentencia break entre
ellas. Por ejemplo, considere el siguiente programa:
// En un switch, las sentencias break son opcionales.
class BreakAusente {
public static void main (String args[]) {
for (int i=0; i<12; i++)
switch (i) {
case 0:
case 1:
case 2:
case 3:
case 4:
System.out.println ("i es menor que 5");
break;
case 5:
case 6:
case 7:
case 8:
case 9:
System.out.println ("i es menor que 10");
break;
default:
System.out.println ("i es 10 o mayor");
}
}
}
Este programa genera la siguiente salida:
i
i
i
i
i
i
i
i
i
i
i
i
es
es
es
es
es
es
es
es
es
es
es
es
menor que 5
menor que 5
menor que 5
menor que 5
menor que 5
menor que 10
menor que 10
menor que 10
menor que 10
menor que 10
10 o mayor
10 o mayor
Como puede verse, la ejecución pasa a través de cada sentencia case hasta que se llega a un
break, o al final del switch.
Evidentemente el código anterior no es más que un ejemplo ideado para aclarar la forma
en que funciona la sentencia switch. Sin embargo, omitir la sentencia break tiene muchas
www.detodoprogramacion.com
Capítulo 5:
Sentencias de control
// Una versión mejorada del programa de las estaciones.
class Switch {
public static void main (String args[]) {
int mes = 4;
String estacion;
switch (mes) {
case 12:
case 1:
case 2:
estacion = "Invierno";
break;
case 3:
case 4:
case 5:
estacion = "Primavera";
break;
case 6:
case 7:
case 8:
estacion = "Verano";
break;
case 9:
case 10:
case 11:
estacion = "Otoño";
break;
default:
estacion = "Mes desconocido";
}
System.out.println ("Abri1 está en " + estacion + ".");
}
}
Sentencias switch anidadas
Se puede utilizar un switch como parte de la secuencia de sentencias de un switch exterior.
A esto se denomina switch anidado. Dado que una sentencia switch define su propio bloque,
no surgen conflictos entre el case contenido en el switch interior y los contenidos en el switch
exterior. Por ejemplo, el siguiente fragmento de código es perfectamente válido:
switch (contador) {
case 1:
switch (var) { // switch anidado
case 0:
System.out.println ("var es cero");
break;
case l: // no hay conflictos con el switch exterior
System.out.println ("var es uno");
break;
www.detodoprogramacion.com
PARTE I
aplicaciones prácticas en programas reales. Para mostrar un uso más realista considere esta otra
versión del ejemplo de las estaciones presentado anteriormente. Esta versión utiliza un switch
para conseguir una implementación más eficiente.
83
84
Parte I:
El lenguaje Java
}
break;
case 2: // …
Aquí la sentencia case 1 del switch interior no entra en conflicto con la sentencia case 1
del switch exterior. La variable contador sólo se compara con la lista de las constantes case del
nivel exterior. Si contador es igual a l, entonces la variable var se compara con la lista de
constantes case del switch interior.
En resumen, la sentencia switch tiene tres características destacables:
• La sentencia switch se diferencía de la sentencia if en que la primera sólo comprueba la
igualdad, mientras que la segunda puede evaluar cualquier tipo de expresión boolenana.
Es decir, la sentencia switch busca solamente la coincidencia entre el valor de la
expresión y una de las constantes de las sentencias case.
• Dos constantes de dos sentencias case en un mismo switch no pueden tener el mismo
valor. Sin embargo, sí puede ocurrir que una sentencia switch contenida dentro de otro
switch exterior tengan constantes iguales en sus correspondientes sentencias case.
• Una sentencia switch es más eficiente que un conjunto de sentencias if anidadas.
El último punto es especialmente interesante, ya que da una idea de cómo funciona el
compilador Java. Cuando se compila una sentencia switch, el compilador Java examina cada una
de las constantes en las sentencias case y crea una “tabla de salto” que se utiliza para seleccionar el
camino de ejecución en función del valor de la expresión. Por ello, en caso de que sea
necesario seleccionar entre un gran grupo de valores, la sentencia switch se ejecutará mucho
más rápidamente que el código equivalente formado con una sucesión de sentencias if-else. El
compilador puede hacer esto, ya que sabe que las constantes de las sentencias case son todas
del mismo tipo y simplemente deben ser comparadas con el valor de la expresión de la sentencia
switch. El compilador no tiene el mismo conocimiento acerca de una larga lista de expresiones if.
Sentencias de iteración
Las sentencias de iteración de Java son for, while y do-while. Estas sentencias crean lo que
comúnmente se denominan ciclos. Como probablemente sabe, un ciclo ejecuta repetidas veces el
mismo conjunto de instrucciones hasta que se cumple una determinada condición. Java tiene un
ciclo para cada necesidad de programación.
while
El ciclo while es la sentencia de iteración más importante de Java. Con este ciclo se repite una
sentencia o un bloque mientras la condición de control es verdadera. Su forma general es:
while (condición) {
// cuerpo del ciclo
}
La condición puede ser cualquier expresión booleana. El cuerpo del ciclo se ejecutará mientras la
expresión condicional sea verdadera. Cuando la condición sea falsa, la ejecución pasa a la
siguiente línea de código localizada inmediatamente después del ciclo. Las llaves no son
necesarias si solamente se repite una sentencia en el cuerpo del ciclo.
www.detodoprogramacion.com
Capítulo 5:
Sentencias de control
// Ejemplo de un ciclo while.
c1ass While {
public static void main (String args[]) {
int n = 10;
while (n > 0) {
System.out.println ("tick " + n);
n--;
}
}
}
Cuando se ejecuta este programa, la salida es:
tick
tick
tick
tick
tick
tick
tick
tick
tick
tick
10
9
8
7
6
5
4
3
2
1
Ya que el ciclo while evalúa la expresión condicional al inicio del ciclo, el cuerpo del
mismo no se ejecutará nunca si al comenzar la condición es falsa. Por ejemplo, en el siguiente
fragmento, la llamada a println ( ) no se ejecuta nunca:
int a = 10, b = 20;
while (a > b)
System.out.println ("Esto no se mostrará");
El cuerpo del ciclo while (o de cualquier otro ciclo de Java) puede estar vacío ya que una
sentencia nula, que consiste únicamente en un punto y coma, es sintácticamente válida en Java.
Considere, por ejemplo, las siguientes líneas de código:
// El cuerpo de un ciclo puede estar vacío.
class SinCuerpo {
public static void main (String args[]) {
int i, j;
i = 100;
j = 200;
// Para localizar el punto medio entre i y j
while (++i < --j); // no existe el cuerpo en este ciclo
System.out.println ("El punto medio es " + i);
}
}
Este programa encuentra el punto medio entre i y j. La salida que se genera es la siguiente:
El punto medio es 150
www.detodoprogramacion.com
PARTE I
El ciclo while que se presenta a continuación cuenta hacia atrás comenzando en 10 e
imprime exactamente diez líneas con la palabra “tick”:
85
86
Parte I:
El lenguaje Java
El ciclo while funciona de la siguiente manera: el valor de i se incrementa y el valor de j
se reduce. A continuación se comparan estos valores. Si el nuevo valor de i es aún menor
que el nuevo valor de j, entonces el ciclo se repite. Si i es igual o mayor que j, el ciclo se
detiene. Al salir del ciclo, i mantendrá un valor intermedio entre los valores iniciales de i y j.
(Naturalmente este procedimiento sólo funciona cuando al comenzar i es menor que j). Como
ha visto, no es necesario que exista un cuerpo del ciclo; en este caso todas las acciones se
producen dentro de la propia expresión condicional. En programas profesionales escritos en
Java es frecuente encontrar ciclos cortos sin ningún cuerpo cuando se pueden introducir en la
expresión lógica que controla el ciclo todas las acciones necesarias.
do-while
Como se acaba de ver, si la expresión condicional que controla un ciclo while es inicialmente falsa,
el cuerpo del ciclo no se ejecutará ni una sola vez. Sin embargo, puede haber casos en los que se
quiera ejecutar el cuerpo del ciclo al menos una vez, incluso cuando la expresión condicional sea
inicialmente falsa. En otras palabras, puede que se desee evaluar la expresión condicional al final
del ciclo, en lugar de hacerlo al principio. Afortunadamente, Java dispone de un ciclo que lo hace
exactamente así, el ciclo do-while. El ciclo do-while ejecuta siempre, al menos una vez, el cuerpo,
ya que la expresión condicional se encuentra al final. Su forma general es:
do {
// cuerpo del ciclo
} while (condición);
En cada iteración del ciclo do-while se ejecuta en primer lugar el cuerpo del ciclo, y a
continuación se evalúa la expresión condicional. Si la expresión es verdadera, el ciclo se repetirá.
En caso contrario, el ciclo finalizará. Como en todos los demás ciclos de Java, la condición debe
ser una expresión booleana.
Las siguientes líneas son un ejemplo de un ciclo do-while. El ejemplo es otra versión del
programa “tick” y genera la misma salida que se obtuvo anteriormente.
// Ejemplo del ciclo do-while.
c1ass DoWhile {
public static void main (String args[]) {
int n = 10;
do {
System.out.println ("tick " + n);
n--;
} while (n > 0);
}
}
Aunque el ciclo que se acaba de presentar en las líneas anteriores es técnicamente correcto,
se puede escribir de una manera más eficiente:
do {
System.out.println ("tick " + n);
} while (--n > 0);
www.detodoprogramacion.com
Capítulo 5:
Sentencias de control
// Uso de un ciclo do-while para procesar un menú de selección
c1ass Menu {
public static void main (String args[])
throws java.io.IOException {
char eleccion;
do {
System.out.println ("Ayuda para:");
System.out.println (" 1. if");
System.out.println (" 2. switch");
System.out.println (" 3. while");
System.out.println (" 4. do-while");
System.out.println (" 5. for\n");
System.out.println ("Elige una opción:");
eleccion = (char) System.in.read();
} while (eleccion < '1' || eleccion > '5');
System.out.println ("\n");
switch (eleccion) {
case '1':
System.out.println
System.out.println
System.out.println
break;
case '2':
System.out.println
System.out.println
System.out.println
System.out.println
System.out.println
System.out.println
System.out.println
break;
case '3':
System.out.println
System.out.println
break;
case '4':
System.out.println
System.out.println
System.out.println
System.out.println
break;
("La sentencia if:\n");
("if (condición) sentencia;");
("else sentencia;");
("La sentencia switch:\n");
("switch (expresion) {");
(" case constante:");
(" conjunto de sentencias");
(" break;");
(" // ...");
("}");
("La sentencia while:\n");
("while (condición) sentencia;");
("La sentencia do-while:\n");
("do {");
("sentencia;");
("} while (condición);");
www.detodoprogramacion.com
PARTE I
En este ejemplo, en la expresión (--n > 0) se combina el decremento de n y la comparación de
la misma variable n con cero en una única expresión. Esto se realiza de la siguiente forma: en
primer lugar, la sentencia --n reduce el valor de n y devuelve el nuevo valor de n; este valor se
compara con cero, si es mayor que cero el ciclo continúa, y en caso contrario, finaliza.
El ciclo do-while es muy útil cuando se procesa un menú de selección, ya que normalmente
se desea que el cuerpo del menú se ejecute al menos una vez. Considere el siguiente programa
en el que se implementa un sistema de ayuda muy sencillo para las sentencias de selección e
iteración de Java:
87
88
Parte I:
El lenguaje Java
case '5':
System.out.println ("La sentencia for:\n");
System.out.print ("for (inicialización; condición; iteración)");
System.out.println (" sentencia;");
break;
}
}
}
La salida que se genera con este programa es la siguiente:
Ayuda para:
l. if
2. switch
3. while
4. do-while
5. for
Elige una opción:
4
La sentencia do-while:
do {
sentencia;
} while (condición);
En este programa el ciclo do-while se utiliza para verificar que el usuario ha elegido una opción
válida. En caso contrario, se vuelven a presentar al usuario todas las opciones. Ya que el menú se
debe presentar al menos una vez, el ciclo do-while es el más indicado para llevar esto a cabo.
Otros elementos interesantes en este ejemplo son los siguientes: observe que los caracteres se
leen desde el teclado mediante la instrucción System.in.read ( ). Ésta es una de las funciones de
Java que permiten introducir datos desde el teclado. Aunque los métodos de E/S de datos por
consola de Java no serán discutidos en detalle sino hasta el Capítulo 13, System.in.read ( ) se
utiliza aquí para obtener la elección del usuario. Esta función permite leer caracteres desde una
entrada estándar (estos caracteres se devuelven como enteros lo que permite asignarlos a la
variable char). Por omisión, la entrada estándar tiene un buffer, y esto obliga a presionar la tecla
ENTER antes de que cualquier carácter escrito sea enviado al programa.
La entrada de datos por consola en Java es bastante limitada e incómoda. Además, la mayor
parte de los programas y applets profesionales en Java son gráficos y basados en el sistema
de ventanas. Por estas razones, en este libro no se ha hecho mucho uso de la entrada por
consola. Sin embargo, es útil en este contexto. Otro punto de interés es el siguiente: Como se
está utilizando la función System.in.read( ), el programa debe especificar la cláusula throws
java.io.IOException. Esta línea es necesaria para la gestión de los errores que se produzcan
en la entrada de datos. Esto es parte de las características que tiene Java para la gestión de
excepciones, las cuales serán analizadas en el Capítulo 10.
for
En el Capítulo 2 se presentó un ejemplo sencillo del ciclo for. Como se podrá comprobar el ciclo
for es una construcción versátil y potente.
Comenzando con JDK 5, existen dos formas del ciclo for. La primera forma es la tradicional
que se ha utilizado desde la versión original de Java. La segunda es una forma nueva conocida
www.detodoprogramacion.com
Capítulo 5:
Sentencias de control
for (inicialización; condición; iteración) {
// cuerpo
}
Si solamente se repite una sentencia, no es necesario el uso de las llaves.
El ciclo for actúa como se describe a continuación: cuando comienza, se ejecuta la parte
de inicialización. Generalmente, la inicialización es una expresión que establece el valor de
la variable de control del ciclo, que actúa como un contador que lo controla. Es importante
comprender que la expresión de inicialización se ejecuta una sola vez. A continuación, se evalúa
la condición, que debe ser una expresión booleana mediante la que, normalmente, se compara la
variable de control con un valor de referencia. Si la expresión es verdadera, entonces se ejecuta
el cuerpo del ciclo. Si es falsa, el ciclo finaliza. A continuación se ejecuta la parte correspondiente
a la iteración. Habitualmente ésta es una expresión en la que se incrementa o reduce el valor
de la variable de control. Cada vez que se recorre el ciclo, en primer lugar se vuelve a evaluar la
expresión condicional, a continuación se ejecuta el cuerpo y después la expresión de iteración.
Este proceso se repite hasta que la expresión condicional sea falsa.
A continuación otra versión del programa “tick”, ahora utilizando un ciclo for:
// Ejemplo del ciclo for
class ForTick {
public static void main (String args[]) {
int n;
for (n=l0; n>0; n--)
System.out.println ("tick " + n);
}
}
Declaración de variables de control dentro del ciclo
A menudo, la variable que controla el ciclo for sólo se necesita en el ciclo, y no se utiliza en
ninguna otra parte. En este caso, es posible declarar esta variable en la sección de inicialización
del for. Por ejemplo, el programa anterior se puede reescribir de forma que la variable de control
del ciclo n se declare como int dentro del for:
// Declaración de la variable de control del ciclo dentro del for
class ForTick {
public static void main (String args[]) {
// aquí se declara n dentro del ciclo for
for (int n=l0; n>0; n--)
System.out.println ("tick " + n);
}
}
Cuando se declara una variable dentro del ciclo for, hay un punto importante que se ha
de tener en cuenta: la vida de esa variable finaliza cuando lo hace la sentencia for, (es decir, el
alcance de la variable está limitado al ciclo for). Fuera del ciclo for la variable no existirá. Si la
www.detodoprogramacion.com
PARTE I
como “for-each”. Ambos tipos de ciclos for son explicados a detalle aquí, comenzando con la
forma tradicional.
La forma general de la sentencia for tradicional es la siguiente:
89
90
Parte I:
El lenguaje Java
variable de control del ciclo interviene en alguna otra parte del programa, no se puede declarar
dentro del ciclo for.
Cuando la variable de control no se va a utilizar en ninguna otra parte del código, la mayoría
de programadores la declaran dentro del ciclo for. El programa que aparece a continuación es
un programa sencillo para comprobar si un número es primo o no. Observe que la variable de
control, i, se declara dentro del ciclo for, ya que no se utiliza en ninguna otra parte.
// Prueba de números primos
class NumeroPrimo {
public static void main (String args[]) {
int num;
boolean esPrimo = true;
num = 14;
for (int i=2; i <= num/i; i++) {
if ({num % i) == 0) {
esPrimo = false;
break;
}
}
if (esPrimo) System.out.println ("El número es primo");
else System.out.println ("El número no es primo");
}
}
Uso del separador coma
En ocasiones puede ser necesario incluir más de una sentencia en las secciones de inicialización
e iteración del ciclo for. Considere, por ejemplo, el ciclo que aparece en el siguiente programa:
class Ejemplo {
public static void main (String args[]) {
int a, b;
b = 4;
for (a=l; a i) {
System.out.println ();
continue exterior;
}
System.out.print (" " + (i * j));
}
}
System.out.println ();
}
}
103
104
Parte I:
El lenguaje Java
System.out.println ("Antes de return.");
If (t) return; // vuelve al método llamante
System.out.println ("Esto no se ejecutará.");
}
}
La salida del programa es la siguiente:
Antes de return.
Como puede verse la última sentencia println( ) no se ejecuta. Cuando se ejecuta el return, el
control vuelve al método llamante.
Un último punto a tener en cuenta en el programa anterior es que la sentencia if(t) es
necesaria. Sin ella, el compilador Java generará un error al saber que la última sentencia println( )
nunca se ejecutaría. Para evitar que se produzca este error, mediante la sentencia if engañamos al
compilador.
www.detodoprogramacion.com
6
CAPÍTULO
Clases
L
as clases son el núcleo de Java. Es la construcción lógica sobre la que se basa el lenguaje
Java porque define la forma y naturaleza de un objeto. De tal forma que son la base de la
programación orientada a objetos en Java. Cualquier concepto que se quiera implementar en
Java debe estar encapsulado dentro de una clase.
Dada la importancia que tienen las clases en Java, este capítulo y los próximos se dedican a este
tema. Aquí introduciremos los elementos básicos de una clase y aprenderemos cómo se usan las
clases para crear objetos. También veremos los métodos, constructores y la palabra clave this.
Fundamentos de clases
Las clases se han utilizado desde el comienzo de este libro. Sin embargo, hasta ahora se habían
utilizado sólo de una forma muy rudimentaria. Las clases creadas en los capítulos anteriores existían
simplemente para encapsular el método main( ), que ha permitido mostrar los fundamentos de la
sintaxis de Java. Como veremos, las clases son sustancialmente más potentes que las presentadas
hasta el momento.
Probablemente la característica más importante de una clase es que define un nuevo tipo de
dato. Una vez definido, este nuevo tipo de dato se puede utilizar para crear objetos de ese tipo o
clase. De este modo, una clase es un template (un modelo) para un objeto, y un objeto es una instancia
de una clase. Debido a que un objeto es una instancia de una clase, a menudo las dos palabras objeto
e instancia se usan indistintamente.
La forma general de una clase
Cuando se define una clase, se declara su forma y naturaleza exactas, especificando los datos que
contiene y el código que opera sobre esos datos. Las clases más sencillas pueden contener solamente
código o solamente datos, pero, en la práctica, la mayoría de las clases contienen datos y código.
Como veremos, el código de una clase define la interfaz con sus datos.
Una clase se declara mediante la palabra clave class. Las clases que se han utilizado hasta el
momento son realmente ejemplos muy limitados de su forma completa. Las clases pueden ser (y
normalmente lo son), mucho más complejas. La forma general de definir una clase es la siguiente:
class nombre_de_clase {
tipo variable_de:instancia1;
tipo variable_de_instancia2;
105
www.detodoprogramacion.com
106
Parte I:
El lenguaje Java
// ...
tipo variable_de_instanciaN;
tipo nombre_de_método1 (parámetros) {
// cuerpo del método
}
tipo nombre_de_método2 (parámetros) {
// cuerpo del método
}
}
// ...
tipo nombre_de_metodoN (parámetros) {
// cuerpo del método
}
Los datos, o variables, definidos en una clase se denominan variables de instancia. El código está
contenido en los métodos. El conjunto de los métodos y las variables definidos dentro de una
clase se denominan miembros de la clase. En la mayor parte de las clases, los métodos definidos
acceden y actúan sobre las variables de instancia, es decir, los métodos determinan cómo se
deben utilizar los datos de una clase.
Las variables definidas en una clase se llaman variables de instancia porque cada instancia
de la clase (esto es, cada objeto de la clase), contiene su propia copia de estas variables. Así, los
datos de un objeto son distintos y únicos de los de otros. Éste es un concepto importante sobre
el que volveremos más adelante.
Todos los métodos tienen el mismo formato general, similar al del método main( ) que
hemos estado utilizando hasta el momento. Sin embargo, la mayor parte de los métodos no se
especifican como static o public. Observe que la forma general de una clase no especifica un
método main( ). Las clases de Java no tienen necesariamente un método main( ). Solamente
se requiere un método main( ) si esa clase es el punto de inicio del programa. Los applets no
requieren un método main( ).
NOTA Si usted está familiarizado con C++, observará que en Java, la declaración de una clase y la
implementación de los métodos se almacenan en el mismo sitio y no se definen separadamente.
Esto, en ocasiones, da lugar a archivos .java muy largos, ya que cualquier clase debe estar
definida completamente en un solo archivo. Esta característica de diseño se estableció en Java,
ya que se supuso que, a largo plazo, tener en un sólo sitio las especificaciones, declaraciones e
implementación daría como resultado un código más fácil de mantener.
Una clase simple
Comencemos nuestro estudio con un ejemplo sencillo, la clase denominada Caja. Esta clase
define tres variables de instancia: ancho, alto y largo. En este caso, Caja no contiene método
alguno, más adelante los añadiremos.
c1ass Caja {
double ancho;
double alto;
www.detodoprogramacion.com
Capítulo 6:
Clases
107
double largo;
}
Caja miCaja = new Caja(); // crea un objeto de la clase Caja llamado miCaja
Cuando se ejecute esta sentencia, miCaja será una referencia a una instancia de Caja. Además,
será una realidad “física”. De momento no nos preocuparemos por los detalles de esta sentencia.
Cada vez que creemos una instancia de una clase, estaremos creando un objeto que
contiene su propia copia de cada variable de instancia definida por la clase. Por lo tanto, cada
objeto Caja contendrá sus propias copias de las variables de instancia ancho, alto y largo. Para
acceder a estas variables, utilizaremos el operador punto (.). El operador punto liga el nombre del
objeto con el nombre de una de sus variables de instancia. Por ejemplo, la siguiente sentencia
sirve para asignar a la variable ancho del objeto miCaja el valor l00.
miCaja.ancho = 100;
Esta sentencia indica al compilador que debe asignar a la copia de ancho que está contenida
en el objeto miCaja el valor 100. En general, el operador punto se usa para acceder tanto a las
variables como a los métodos de un objeto. El siguiente es un programa completo que utiliza la
clase Caja:
/* Un programa que utiliza la clase Caja.
El nombre de este archivo es CajaDemo.java
*/
class Caja {
double ancho;
double alto;
double largo;
}
// Esta clase declara un objeto de la clase Caja.
class CajaDemo {
public static void main (String args[]) {
Caja miCaja = new Caja();
double vol;
// asignación de valores a las variables del objeto miCaja
miCaja.ancho = 10;
miCaja.alto = 20;
miCaja.largo = 15;
// Se calcula el volumen de la caja
vol = miCaja.ancho * miCaja.alto * miCaja.largo;
System.out.println ("El volumen es " + vol);
}
}
www.detodoprogramacion.com
PARTE I
Como se ha dicho anteriormente, una clase define un nuevo tipo de dato. En este caso, el nuevo
tipo se llama Caja. Utilizaremos este nombre para declarar objetos de tipo Caja. Es importante
recordar que la declaración de una clase solamente crea un modelo o patrón y no un objeto real.
Así que el código anterior no crea ningún objeto de la clase Caja.
Para crear un objeto de tipo Caja habrá que utilizar una sentencia como la siguiente:
108
Parte I:
El lenguaje Java
Al archivo que contiene este programa se le debe llamar CajaDemo.java, ya que el método
main( ) está dentro de la clase denominada CajaDemo, no en la clase denominada Caja.
Cuando se compila este programa, se generan dos archivos .class, uno para Caja y otro para
CajaDemo. El compilador Java crea automáticamente para cada clase su propio archivo .class.
No es necesario que las clases Caja y CajaDemo estén en el mismo archivo fuente. Se puede
escribir cada clase en su propio archivo, es decir, en los archivos Caja.java y CajaDemo.java,
respectivamente.
Para ejecutar este programa, debemos ejecutar CajaDemo.class, y obtendremos la siguiente
salida:
El volumen es 3000.0
Tal y como se ha visto anteriormente, cada objeto tiene sus propias copias de las variables
de instancia. Esto significa que si tenemos dos objetos Caja, cada uno tiene sus propias copias de
largo, ancho y alto. Es importante tener en cuenta que los cambios en las variables de instancia
de un objeto no afectan a las variables de otro. Por ejemplo, el siguiente programa declara dos
objetos Caja.
// Este programa declara dos objetos Caja.
class Caja {
double ancho;
double alto;
double largo;
}
class CajaDemo2{
public static void main (String args[]) {
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
double vol;
// asignación de valores a las variables de la instancia miCaja1
miCaja1.ancho = 10;
miCaja1.alto = 20;
miCaja1.largo = 15;
/* asignación de valores diferentes a las variables de la instancia miCaja2
*/
miCaja2.ancho = 3;
miCaja2.alto = 6;
miCaja2.1argo = 9;
// calcula el volumen de la primera caja
vol = miCaja1.ancho * miCaja1.alto * miCaja1.largo;
System.out.println("E1 volumen es " + vol);
// calcula el volumen de la segunda caja
vol = miCaja2.ancho * miCaja2.alto * miCaja2.largo;
System.out.println("E1 volumen es " + vol);
}
}
La salida que se obtiene es la siguiente:
www.detodoprogramacion.com
Capítulo 6:
Clases
109
El volumen es 3000.0
El volumen es 162.0
Declaración de objetos
Tal y como se acaba de explicar, cuando se crea una clase, se está creando un nuevo tipo de datos
que se utilizará para declarar objetos de ese tipo. Sin embargo, la obtención de objetos de una
clase es un proceso que consta de dos etapas. En primer lugar, se debe declarar una variable del
tipo de la clase. Esta variable no define un objeto, sino que simplemente es una referencia a un
objeto. En segundo lugar, se debe obtener una copia física del objeto y asignarla a esa variable.
Para ello se utiliza el operador new que asigna dinámicamente, durante el tiempo de ejecución,
memoria a un objeto y devuelve una referencia al mismo. Esta referencia es algo así como la
dirección en memoria del objeto creado por la operación new. Luego se almacena esta referencia
en la variable. Todos los objetos de una clase en Java se asignan dinámicamente. Veamos con más
detalle este procedimiento.
En los ejemplos anteriores se utilizó una línea similar a la siguiente para declarar un objeto
de la clase Caja:
Caja miCaja = new Caja();
Esta sentencia combina las dos etapas descritas anteriormente y, para mostrar más claramente
cada una de ellas, dicha sentencia se puede volver a escribir del siguiente modo:
Caja miCaja; // declara la referencia a un objeto
miCaja = new Caja(); // reserva espacio en memoria para el objeto
La primera línea declara miCaja como una referencia a un objeto de la clase Caja. Después
de que se ejecute esta línea, miCaja contiene el valor null, que indica que todavía no apunta a
un objeto real. Cualquier intento de utilizar miCaja en esta situación dará lugar a un error de
compilación. En la siguiente línea se reserva memoria para un objeto real y se asigna miCaja
como la referencia a dicho objeto. Una vez que se ejecute la segunda línea, ya se puede utilizar
miCaja como si fuera un objeto de la clase Caja. En realidad, miCaja simplemente contiene la
dirección de memoria del objeto real. El efecto de estas dos líneas se describe en la Figura 6.1.
NOTA
Los lectores familiarizados con C/C++ habrán observado, probablemente, que las referencias
a objetos son muy semejantes a los apuntadores. Básicamente, esto es correcto. Una referencia
a objeto es semejante a un apuntador a memoria. La principal diferencia –y la clave para la
seguridad de Java– es que no se pueden manipular las referencias tal y como se hace con los
apuntadores. Por lo tanto, una referencia no puede apuntar a una dirección arbitraria de memoria
ni se puede manipular como si fuese entero.
El operador new
Como se explicó, el operador new reserva memoria dinámicamente para un objeto. Su forma
general es:
variable = new nombre_de_clase ();
www.detodoprogramacion.com
PARTE I
Como se puede comprobar, los datos de miCajal son completamente independientes de los
datos contenidos en miCaja2.
110
Parte I:
El lenguaje Java
FIGURA 6-1
Declaración de un objeto
de tipo Caja.
Declaración
Caja miCaja;
Efecto
nulo
miCaja
Ancho
miCaja = nueva Caja();
miCaja
Alto
Largo
objeto Caja
Aquí, variable es una variable cuyo tipo es la clase creada, y el nombre_de_clase es el nombre de
la clase que está siendo instanciada. El nombre de la clase seguido de paréntesis está especificando
una llamada al método constructor de la clase. Un constructor define lo que ocurre cuando se crea
un objeto de una clase. Los constructores son una parte importante de todas las clases y tienen
muchos atributos significativos. En la práctica, la mayoría de las clases definen explícitamente
sus propios constructores en la definición de la clase. Cuando no se definen explícitamente, Java
suministra automáticamente el constructor por omisión. Esto es lo que ha ocurrido con la clase
Caja. Por ahora seguiremos utilizando el constructor por omisión, aunque pronto veremos cómo
definir nuestros propios constructores.
En este momento nos podríamos plantear la siguiente pregunta: ¿Por qué no es necesario
utilizar el operador new en el caso de los enteros o de los caracteres? La respuesta es que los
tipos primitivos no se implementan como objetos sino como variables “normales”. Esto se
hace así con el objeto de lograr una mayor eficiencia. Los objetos tienen muchas características
y atributos que obligan a Java a tratarlos de forma diferente a la que utiliza con los tipos
básicos. Al no aplicar la misma sobrecarga a los tipos primitivos que a los objetos, Java puede
implementar a los tipos básicos más eficientemente. Más adelante se verán versiones con
objetos de los tipos primitivos, las cuales están disponibles para su uso en situaciones en las que
se necesitan objetos completos para trabajar con valores primitivos.
Es importante tener en cuenta que el operador new reserva memoria para un objeto durante
el tiempo de ejecución. La ventaja de hacerlo así es que el programa crea exactamente los objetos
que necesita durante su ejecución. Sin embargo, dado que la memoria disponible es finita, puede
ocurrir que ese operador new no sea capaz de reservar memoria para un objeto porque no exista
ya memoria disponible. Si esto ocurre, se producirá una excepción en tiempo de ejecución. (En el
Capítulo 10 se verá la gestión de ésta y otras excepciones). En los ejemplos que se presentan en
este libro no es necesario que nos preocupemos por el hecho de quedamos sin memoria, pero sí
es preciso considerar esta posibilidad en los programas reales.
Volvamos de nuevo a la distinción entre clase y objeto. Una clase crea un nuevo tipo de
dato que se utilizará para crear objetos, es decir, una clase crea un marco lógico que define las
relaciones entre sus miembros. Cuando se declara un objeto de una clase, se está creando una
instancia de esa clase. Por lo tanto, una clase es una construcción lógica, mientras que un objeto
www.detodoprogramacion.com
Capítulo 6:
Clases
Asignación de variables de referencia a objetos
Las variables de referencia a objetos actúan de una forma diferente a la que se podría esperar
cuando tiene lugar una asignación. Por ejemplo, ¿qué hace el siguiente fragmento de código?
Caja bl = new Caja();
Caja b2 = bl;
Podríamos pensar que a b2 se le asigna una referencia a una copia del objeto que se referencia
mediante bl, es decir, que bl y b2 se refieren a objetos distintos. Sin embargo, esto no es así.
Cuando este fragmento de código se ejecute, bl y b2 se referirán al mismo objeto. La asignación
de bl a b2 no reserva memoria ni copia parte alguna del objeto original. Simplemente hace que
b2 se refiera al mismo objeto que bl. Por lo tanto, cualquier cambio que se haga en el objeto a
través de b2 afectará al objeto al que se refiere bl, ya que, en definitiva, se trata del mismo objeto.
Esta situación se representa gráficamente a continuación.
b1
Ancho
Alto
objeto Caja
Largo
b2
Aunque bl y b2 se refieren al mismo objeto, no están relacionados de ninguna otra forma. Por
ejemplo, una asignación posterior a bl simplemente desenganchará bl del objeto original sin
afectar al objeto o a b2. Por ejemplo:
Caja bl = new Caja();
Caja b2 = bl;
// ...
bl = null;
En este caso, bl ha sido asignado a null, pero b2 todavía apunta al objeto original.
RECUERDE Cuando se asigna una variable de referencia a objeto a otra variable de referencia a
objeto, no se crea una copia del objeto, sino que sólo se hace una copia de la referencia.
Métodos
Como se mencionó al comienzo de este capítulo, las clases están formadas por variables de
instancia y métodos. El concepto de método es muy amplio ya que Java les concede una gran
potencia y flexibilidad. La mayor parte del siguiente capítulo se dedica a los métodos. Sin
www.detodoprogramacion.com
PARTE I
tiene una realidad física, esto es, un objeto ocupa un espacio de memoria. Es importante tener en
cuenta esta distinción.
111
112
Parte I:
El lenguaje Java
embargo, es preciso introducir en este momento algunas nociones básicas para empezar a
incorporar métodos a las clases.
La forma general de un método es la siguiente:
tipo nombre_de_método (parámetros) {
// cuerpo del método
}
Donde tipo especifica el tipo de dato que devuelve el método, el cual puede ser cualquier tipo
válido, incluyendo los tipos definidos mediante clases creadas por el programador. Cuando el
método no devuelve ningún valor, el tipo devuelto debe ser void. El nombre del método se
especifica en nombre_de_método, que puede ser cualquier identificador válido que sea distinto
de los que ya están siendo utilizados por otros elementos del programa. Los parámetros son
una sucesión de pares de tipo e identificador separados por comas. Los parámetros son,
esencialmente, variables que reciben los valores de los argumentos que se pasa a los métodos
cuando se les llama. Si el método no tiene parámetros, la lista de parámetros estará vacía.
Los métodos que devuelven un tipo diferente del tipo void devuelven el valor a la rutina
llamante mediante la siguiente forma de la sentencia return:
return valor;
Donde valor es el valor que el método retorna.
En los siguientes apartados se verá cómo crear distintos tipos de métodos, incluyendo los
que tienen parámetros y los que devuelven valores.
Adición de un método a la clase Caja
Aunque crear una clase que contenga solamente datos es correcto, rara vez se hace. En la
mayor parte de las ocasiones se usarán métodos para acceder a las variables de instancia
definidas por la clase. De hecho los métodos definen la interfaz para la mayor parte de
las clases. Esto permite que la clase oculte la estructura interna de los datos detrás de las
abstracciones de un conjunto de métodos. Además de definir métodos que proporcionen el
acceso a los datos, también se pueden definir métodos cuyo propósito sea el de ser utilizados
internamente por la propia clase.
Comencemos por añadir un método a la clase Caja. En los programas anteriores se
calculaba el volumen de una caja en la clase CajaDemo; sin embargo, el volumen de la caja
depende del tamaño de la caja. Por este motivo tiene más sentido que sea la clase Caja la que se
encargue del cálculo del volumen. Para ello se debe añadir un método a la clase Caja, tal y como
se muestra a continuación:
// Este programa incluye un método en la clase Caja.
class Caja {
double ancho;
double alto;
double largo;
// presenta el volumen de una caja
void volumen () {
System.out.print ("El volumen es ");
System.out.println (ancho * alto * largo);
}
}
www.detodoprogramacion.com
Capítulo 6:
Clases
// Se asignan valores a las variables del objeto miCaja1
miCaja1.ancho = 10;
miCaja1.alto = 20;
miCaja1.largo = 15;
/* asigna diferentes valores a las variables
del objeto de miCaja2 */
miCaja2.ancho = 3;
miCaja2.alto = 6;
miCaja2.largo = 9;
// muestra el volumen de la primera caja
miCaja1.volumen ();
// muestra el volumen de la segunda caja
miCaja2.volumen ();
}
}
Este programa genera la siguiente salida, que es la misma que se obtuvo en la versión
anterior.
El volumen es 3000.0
El volumen es 162.0
Analicemos más detenidamente las siguientes dos líneas de código:
miCaja1.volumen();
miCaja2.volumen();
La primera invoca al método volumen( ) en miCajal, es decir, llama al método volumen( ),
relativo al objeto miCajal, utilizando el nombre del objeto seguido por el operador punto. Por
lo tanto, la llamada al método miCaja1.volumen( ) presenta el volumen de la caja definida
por miCajal, y la llamada a miCaja2.volumen( ) presenta el volumen de la caja definida por
miCaja2. Cada vez que se llama a volumen( ) se presenta el volumen de la caja especificada.
Si no está familiarizado con el concepto de llamada a un método, el siguiente análisis
le ayudará a aclarar las cosas. Cuando se ejecuta miCaja1.volumen( ), el intérprete de Java
transfiere el control al código definido dentro del método volumen( ). Una vez que estas
sentencias se han ejecutado, el control es devuelto a la rutina llamante, y la ejecución continúa
en la línea de código que sigue a la llamada. En un sentido más general, un método de Java es
una forma de implementar subrutinas.
Dentro del método volumen( ), es muy importante observar que la referencia a las variables
de instancia ancho, alto y largo es directa sin que vayan precedidas del nombre de un objeto o
del operador punto. Cuando un método utiliza una variable de instancia definida por su propia
clase, lo hace directamente, sin referencia explícita a un objeto y sin utilizar el operador punto.
Siempre que se llama a un método, esté está relacionado con algún objeto de su clase. Una vez
que la llamada tiene lugar, el objeto es conocido.
www.detodoprogramacion.com
PARTE I
class CajaDemo3 {
public static void main (String args[]) {
Caja miCaja1 = new Caja();
Caja miCaja2 = new Caja();
113
114
Parte I:
El lenguaje Java
Por lo tanto, en un método no es necesario especificar el objeto por segunda ocasión. Esto
significa que ancho, alto y largo dentro de volumen( ) se refieren implícitamente a las copias de
esas variables que están en el objeto que llama a volumen( ).
Revisando, cuando se accede a una variable de instancia por un código que no forma parte
de la clase en la que está definida la variable de instancia, se debe hacer mediante un objeto
utilizando el operador punto. Sin embargo, cuando el código forma parte de la misma clase en
la que se define la variable de instancia a la que accede dicho código, la referencia a esa variable
puede ser directa. Esto se aplica de la misma forma a los métodos.
Devolución de un valor
La implementación del método volumen( ) realiza el cálculo del volumen de una caja dentro de
la clase Caja a la que pertenece, sin embargo esta implementación no es la mejor. Por ejemplo,
puede ser un problema si en otra parte del programa se necesita el valor del volumen de la caja,
pero sin que sea necesario presentar dicho valor. Una mejor forma de implementar el método
volumen( ) es realizar el cálculo del volumen y devolver el resultado a la parte del programa que
llama al método. En el siguiente ejemplo, que es una versión mejorada del programa anterior, se
hace eso.
// Ahora volumen() devuelve el volumen de una caja.
class Caja {
double ancho;
double alto;
double largo;
// cálculo y devolución del valor
double volumen() {
return ancho * alto * largo;
}
}
class CajaDemo4 {
public static void main (String args[]) {
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
double vol;
// se asigna valores a las variables de instancia de miCaja1
miCaja1.ancho = 10;
miCaja1.alto = 20;
miCaja1.largo = 15;
/* se asigna diferentes valores a las variables
de instancia de miCaja2 */
miCaja2.ancho = 3;
miCaja2.alto = 6;
miCaja2.largo = 9;
// se obtiene el volumen de la primera caja
vol = miCajal. volumen ();
System.out.println ("El volumen es " + vol);
www.detodoprogramacion.com
Capítulo 6:
Clases
}
}
En este ejemplo, cuando se llama al método volumen( ), se coloca en la parte derecha de la
sentencia de asignación. En la parte izquierda está la variable, en este caso vol, que recibirá el
valor devuelto por volumen( ). Por lo tanto, después de que se ejecute la sentencia:
vol = miCajal.volumen();
el valor de miCajal.volumen( ) es 3,000 y este valor se almacena en vol.
Dos puntos importantes a considerar sobre la devolución de valores son:
• El tipo de datos devueltos por un método debe ser compatible con el tipo de retorno
especificado por el método. Por ejemplo, si el tipo de retorno de un método es booleano,
no se puede devolver un entero.
• La variable que recibe el valor devuelto por un método (vol, en este caso) debe ser
también compatible con el tipo de retorno especificado por el método.
Una cuestión más: el programa anterior se puede escribir de forma más eficiente teniendo
en cuenta que realmente no es necesario que exista la variable vol. Se puede utilizar la llamada
a volumen( ) directamente en la sentencia println( ), como se muestra a continuación.
System.out.println(“El volumen es “ + miCaja1.volumen());
En este caso, cuando se ejecuta println( ), se llama directamente a miCajal.volumen( ) y se
pasa su valor a println( ).
Métodos con parámetros
Mientras que algunos métodos no necesitan parámetros, la mayoría sí. Los parámetros
permiten generalizar un método, es decir, un método con parámetros puede operar sobre gran
variedad de datos y/o ser utilizado en un gran número de situaciones diferentes. Para ilustrar
este punto usaremos un ejemplo muy sencillo. El siguiente método devuelve el cuadrado del
número 10:
int cuadrado ()
{
return 10 * 10;
}
Efectivamente este método devuelve el cuadrado de 10, pero su utilización es muy limitada.
Sin embargo, si se modifica de forma que tome un parámetro, como se muestra a continuación,
entonces se consigue que cuadrado( ) tenga una mayor utilidad.
int cuadrado(int i)
{
return i * i;
}
www.detodoprogramacion.com
PARTE I
// se obtiene el volumen de la segunda caja
vol = miCaja2 .volumen ();
System.out.println ("El volumen es " + vol);
115
116
Parte I:
El lenguaje Java
Ahora, cuadrado( ) devolverá el cuadrado de cualquier valor usado en la llamada al método, es
decir, cuadrado( ) es ahora un método de propósito general que puede calcular el cuadrado de
cualquier número entero.
Aquí está un ejemplo de ello:
int
x =
x =
y =
x =
x, y;
cuadrado(5); // x es igual a 25
cuadrado(9); // x es igual a 81
2;
cuadrado (y) ; // x es igual a 4
En la primera llamada a cuadrado( ), se pasa el valor 5 al parámetro i. En la segunda, i recibirá el
valor 9. La tercera invocación pasa el valor de y, que en este ejemplo es 2. Como muestran estos
ejemplos, cuadrado( ) devuelve el cuadrado de cualquier valor que se pase al método.
Es importante tener una idea precisa de estos dos términos, parámetros y argumentos. Un
parámetro es una variable, definida por un método, que recibe un valor cuando se llama al
método. Por ejemplo, en cuadrado( ) el parámetro es i. Un argumento es un valor que se pasa
a un método cuando se le llama. Por ejemplo, cuadrado(l00) pasa 100 como un argumento.
Dentro de cuadrado( ), el parámetro i recibe ese valor.
Se puede utilizar un método parametrizado para mejorar la clase Caja. En los ejemplos
anteriores, las dimensiones de cada caja se establecen por separado mediante una sucesión de
sentencias:
micaja1.ancho = 10;
miCaja1.alto = 20;
micaja1.largo = 15;
Este código funciona, pero presenta problemas por dos razones. En primer lugar, resulta torpe y
propenso a errores; por ejemplo, fácilmente se puede olvidar dar valor a una de las dimensiones.
En segundo lugar, en los programas de Java correctamente diseñados, sólo se puede acceder a
las variables de instancia por medio de métodos definidos por sus clases. De ahora en adelante,
permitiremos alterar el comportamiento de un método, pero no el de una variable de instancia
accesible desde el exterior de la clase.
Una mejor solución es crear un método que tome las dimensiones de la caja dentro de sus
parámetros y establezca las variables de instancia apropiadamente. En el siguiente programa se
implementa este concepto:
// Este programa usa un método parametrizado.
class Caja {
double ancho;
double alto;
double largo;
// cálculo y devolución del volumen
double volumen () {
return ancho * alto * largo;
}
// establece las dimensiones de la caja
void setDim (double w, double h, double d) {
ancho = w;
www.detodoprogramacion.com
Capítulo 6:
Clases
117
alto = h;
largo = d;
class CajaDemo5 {
public static void main (String args[]) {
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
double vol;
// inicializa cada caja
miCaja1.setDim (10, 20, 15);
miCaja2.setDim (3, 6, 9);
// calcula el volumen de la primera caja
vol = miCaja1.volumen ();
System.out.println ("El volumen es " + vol);
// calcula el volumen de la segunda caja
vol = miCaja2.volumen ();
System.out.println ("El volumen es " + vol);
}
}
El método setDim( ) se utiliza para establecer las dimensiones de cada caja. Por ejemplo,
cuando se ejecuta:
miCaja1.setDim(10, 20, 15);
el valor 10 se copia en el parámetro w; el valor 20, en el parámetro h, y el valor 15, en el
parámetro d. Dentro del método setDim( ) los valores de w, h y d se asignan a las variables
ancho, alto y largo, respectivamente.
Para muchos lectores, los conceptos presentados en los apartados anteriores les resultarán
familiares. Sin embargo, si conceptos tales como la llamada a métodos, argumentos y parámetros
le resultan nuevos, puede resultar conveniente que dedique algún tiempo a familiarizarse con
ellos antes de seguir adelante, puesto que son fundamentales para la programación en Java.
Constructores
El proceso de inicializar todas las variables en una clase cada vez que se crea una instancia puede
resultar tedioso, incluso cuando se añaden métodos como setDim( ). Puede resultar más simple
y más conciso realizar todas las inicializaciones cuando el objeto se crea por primera vez. El
proceso de inicialización es tan común que Java permite que los objetos se inicialicen cuando
son creados. Esta inicialización automática se lleva a cabo mediante el uso de un constructor.
Un constructor inicializa un objeto inmediatamente después de su creación. Tiene el
mismo nombre que la clase en la que reside y, sintácticamente, es similar a un método. Una
vez definido, se llama automáticamente al constructor después de crear el objeto y antes de
que termine el operador new. Los constructores resultan un poco diferentes, a los métodos
convencionales, porque no devuelven ningún tipo, ni siquiera void. Esto se debe a que el
tipo implícito que devuelve un constructor de clase es el propio tipo de la clase. La tarea del
constructor es inicializar el estado interno de un objeto de forma que el código que crea a la
www.detodoprogramacion.com
PARTE I
}
}
118
Parte I:
El lenguaje Java
instancia pueda contar con un objeto completamente inicializado que pueda ser utilizado
inmediatamente.
Se puede modificar el ejemplo anterior de forma que las dimensiones de la caja se inicialicen
automáticamente cuando se construye el objeto. Para ello se sustituye el método setDim( ) por
un constructor. Comencemos definiendo un constructor sencillo que simplemente asigne los
mismos valores a las dimensiones de cada caja.
/* La clase Caja usa un constructor para inicializar
las dimensiones de las caja.
*/
class Caja {
double ancho;
double alto;
double largo;
// Este es el constructor para Caja.
Caja() {
System.out.println("Constructor de Caja");
ancho = 10;
alto = 10;
largo = 10;
}
// calcula y devuelve el volumen
doub1e volumen () {
return ancho * alto * largo;
}
}
c1ass CajaDemo6 {
pub1ic static void main (String args[]) {
// declara, reserva memoria, e inicial iza objetos de tipo Caja
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
doub1e vol;
// obtiene el volumen de la primera caja
vol = miCajal.volumen () ;
System.out.println ("E1 volumen es " + vol);
// obtiene el volumen de la segunda caja
vol = miCaja2.vo1umen ();
System.out.println ("El volumen es " + vol);
}
}
Cuando se ejecuta este programa, genera el siguiente resultado:
Constructor de Caja
Constructor de Caja
El volumen es 1000.0
El volumen es 1000.0
Como puede observarse, miCajal y miCaja2 han sido inicializados por el constructor
de Caja( ) en el momento de su creación. Como el constructor asigna el mismo valor, 10, a
www.detodoprogramacion.com
Capítulo 6:
Clases
variable = new nombre_de_clase ();
Ahora resulta más evidente la necesidad de los paréntesis después del nombre de clase. Lo que
ocurre realmente es que se está llamando al constructor de la clase. Por lo tanto, en la línea:
Caja miCajal = new Caja();
new Caja( ) es la llamada al constructor de Caja( ). Cuando no se define explícitamente un
constructor de clase, Java crea un constructor por defecto de clase. Este es el motivo de que la
línea anterior funcionara correctamente en las versiones previas de Caja en las que no se definía
constructor alguno. El constructor por omisión asigna, automáticamente, a todas las variables el
valor inicial igual a cero. Para clases sencillas, resulta suficiente utilizar el constructor por defecto,
pero no para clases más sofisticadas. Una vez definido el propio constructor, el constructor por
omisión ya no se utiliza.
Constructores con parámetros
Aunque el constructor de Caja( ) en los ejemplos previos inicializa un objeto Caja, no es muy
útil que todas las cajas tengan las mismas dimensiones. Necesitamos una forma de construir
objetos Caja de diferentes dimensiones. La solución más sencilla es añadir parámetros al
constructor, con lo que se consigue que éste sea mucho más útil. La siguiente versión de Caja
define un constructor con parámetros que asigna a las dimensiones de la caja los valores
especificados por esos parámetros.
Prestemos especial atención a la forma en que se crean los objetos de Caja.
/* Aquí, Caja usa un constructor parametrizado para
inicializar las dimensiones de una caja.
*/
class Caja {
double ancho;
double alto;
double largo;
// Este es el constructor de Caja.
Caja (double w, double h, double d) {
ancho = w;
alto = h;
largo = d;
}
// calcula y devuelve el volumen
double volumen () {
return ancho * alto * largo;
}
}
class CajaDemo7 {
public static void main(String args[]) {
www.detodoprogramacion.com
PARTE I
todas las dimensiones de la caja, miCajal y miCaja2 tienen el mismo volumen. La sentencia
println( ) dentro de Caja( ) sólo sirve para mostrar cómo funciona el constructor. La mayoría
de los constructores no presentan alguna salida, sino que simplemente inicializan un objeto.
Antes de seguir, examinemos de nuevo el operador new. Cuando se reserva espacio de
memoria para un objeto, se hace de la siguiente forma:
119
120
Parte I:
El lenguaje Java
// declara, reserva memoria, e inicializa los objetos de Caja
Caja miCajal = new Caja(10, 20, 15);
Caja miCaja2 = new Caja(3, 6, 9);
double vol;
// obtiene el volumen de la primera caja
vol = miCaja1.volumen();
System.out.println ("El volumen es " + vol);
// obtiene el volumen de la segunda caja
vol = miCaja2.volumen();
System.out.println ("El volumen es " + vol);
}
}
La salida de este programa es la siguiente:
El volumen es 3000.0
El volumen es 162.0
Como se puede ver, cada objeto es inicializado como se especifica en los parámetros de su
constructor. Por ejemplo, en la siguiente línea:
Caja miCajal = new Caja(l0, 20, 15);
Los valores 10, 20 y 15 se pasan al constructor de Caja( ) cuando new crea el objeto. Así las
copias de ancho, alto y largo de miCajal contendrán los valores 10, 20 y 15, respectivamente.
La palabra clave this
En algunas ocasiones, un método necesita referirse al objeto que lo invocó. Para permitir
esta situación, Java define la palabra clave this, la cual puede ser utilizada dentro de cualquier
método para referirse al objeto actual. this es siempre una referencia al objeto sobre el que
ha sido llamado el método. Se puede usar this en cualquier lugar donde esté permitida una
referencia a un objeto del mismo tipo de la clase actual.
Consideremos la siguiente versión de Caja( ) para comprender mejor cómo funciona this.
// Un uso redundante de this.
Caja (double w, double h, double d) {
this.ancho = w;
this.alto = h;
this.largo = d;
}
Esta versión de Caja( ) opera exactamente igual que la versión anterior. El uso de this es
redundante pero correcto. Dentro de Caja( ), this se refiere siempre al objeto llamante. Aunque
en este caso es redundante, en otros contextos this es útil; uno de esos contextos se explica en la
siguiente sección.
Ocultando variables de instancia
En Java es ilegal declarar a variables locales con el mismo nombre dentro del mismo contexto.
Curiosamente, puede haber variables locales, desde parámetros formales hasta métodos, que
coincidan en parte con los nombres de las variables de instancia de clase. Sin embargo, cuando
www.detodoprogramacion.com
Capítulo 6:
Clases
// Uso de this para resolver colisiones en el espacio de nombres
Caja (double ancho, double alto, double largo) {
this.ancho = ancho;
this.alto = alto;
this.largo = largo;
}
NOTA
El uso de this en este contexto puede ser confuso, y algunos programadores tienen la
precaución de no utilizar nombres de variables locales y parámetros formales que puedan ocultar
variables de instancia. Otros programadores creen precisamente lo contrario, es decir, que puede
resultar conveniente, para una mayor claridad, utilizar los mismos nombres, y usan this para
superar el ocultamiento de la variable de instancia. Adoptar una tendencia u otra es una cuestión
de preferencias.
Recolección automática de basura
Ya que en Java se reserva espacio de memoria para los objetos dinámicamente mediante la
utilización de operador new, surge la pregunta sobre cómo destruir los objetos y liberar el
correspondiente espacio de memoria para su posterior utilización. En algunos lenguajes como
C++, la memoria asignada dinámicamente debe ser liberada de forma manual mediante
el operador delete. Esta situación se resuelve en Java de forma diferente. Java gestiona
automáticamente la liberación de la memoria. Esta técnica se denomina recolección de basura y
consiste en lo siguiente: cuando no existen referencias a un objeto, se asume que el objeto no se
va a necesitar más, y la memoria ocupada por dicho objeto puede ser liberada. No es necesario
destruir objetos explícitamente como en C++. La recolección de basura sólo se produce
esporádicamente durante la ejecución del programa. No se producirá simplemente porque haya
uno o dos objetos que no se utilicen más. Los diferentes intérpretes de Java siguen distintos
procedimientos de recolección de basura, pero en realidad no hay que preocuparse mucho por
ello al escribir nuestros programas.
El método finalize( )
En algunas ocasiones es necesario realizar alguna acción cuando se destruye un objeto. Por
ejemplo, si un objeto sustenta algún recurso que no pertenece a Java, como un descriptor de
archivo o un tipo de letra del sistema de ventanas, entonces es necesario liberar estos recursos
antes de destruir el objeto.
www.detodoprogramacion.com
PARTE I
una variable tiene el mismo nombre que una variable de instancia, la variable local esconde a
la variable de instancia. Por esta razón, ancho, alto y largo no se utilizaron como los nombres
de los parámetros en el constructor Caja( ) dentro de la clase Caja. Si se hubieran utilizado,
entonces ancho se hubiera referido al parámetro formal, ocultando la variable de instancia
ancho. Si bien normalmente será más sencillo utilizar nombres diferentes, this permite hacer
referencia directamente al objeto y resolver de esta forma cualquier colisión entre nombres,
que pudiera darse entre las variables de instancia y las variables locales. La siguiente versión de
Caja( ) utiliza ancho, alto, y largo como nombres de parámetros y, después, this para acceder a
variables de instancia que tienen los mismos nombres.
121
122
Parte I:
El lenguaje Java
Para gestionar estas situaciones, Java proporciona un mecanismo denominado finalización
mediante el cual se pueden definir acciones específicas que se producirán cuando el sistema de
recolección de basura vaya a eliminar un objeto.
Para añadir un finalizador a una clase basta con definir el método finalize( ). El intérprete
de Java llamará a ese método siempre que esté a punto de eliminar un objeto de esa clase.
Dentro del método finalize( ) se especificarán aquellas acciones que se han de efectuar antes
de destruir un objeto. El sistema de recolección de basura se ejecuta periódicamente, buscando
objetos a los que ya no haga referencia ningún estado en ejecución, o indirectamente a través
de otros objetos referenciados. Justo antes de eliminar un objeto, el intérprete de Java llama al
método finalize( ) de ese objeto.
El método finalize( ) tiene la forma general:
protected void finalize ( )
{
// código de finalización
}
Aquí, la palabra clave protected es un especificador que impide el acceso a finalize( ) por parte
de un código definido fuera de su clase. Éste y otros especificadores de acceso se explican en el
Capítulo 7.
Es importante entender que sólo se llama al método finalize( ) justo antes de que actúe el
sistema de recolección de basura, y no, por ejemplo, cuando un objeto está fuera del contexto.
Esto significa que no se puede saber exactamente cuándo será, o incluso si será, ejecutado el
método finalize( ). Por lo tanto, el programa debe incluir otros medios que permitan liberar los
recursos del sistema y anexos utilizados por el objeto. No nos debemos apoyar en el método
finalize( ) para la operación normal del programa.
NOTA Si usted está familiarizado con C++ entonces sabe que C++ permite la definición de un
destructor para una clase al que se llama cuando un objeto queda fuera de contexto. Java no
proporciona destructores basándose en este concepto. El método finalize( ) consiste únicamente
en un aproximación a esta funcionalidad. A medida que usted vaya adquiriendo una mayor
experiencia en el manejo de Java, verá que la necesidad de las funciones de un destructor es
mínima, debido al sistema de recolección de basura de que dispone Java.
Una clase Stack
Aunque la clase Caja ha sido útil para ilustrar los elementos esenciales de una clase, su valor
práctico es escaso. Este capítulo termina con un ejemplo más sofisticado que permite mostrar
la verdadera potencia de las clases. Como recordará del análisis sobre programación orientada
a objetos (POO), presentado en el Capítulo 2, una de las ventajas más importantes de la misma
es el encapsulado de datos y código. La clase es el mecanismo por medio del cual se consigue
dicho encapsulado en Java. Al crear una clase, se crea un nuevo tipo de datos que definen tanto
la naturaleza de los datos como las rutinas utilizadas para manipularlos. Además, los métodos
definen un interfaz consistente y controlada para los datos de la clase. Por lo tanto, se puede
utilizar la clase a través de sus métodos sin preocuparse por los detalles de su implementación o
por la gestión real de los datos dentro de la clase. En cierto sentido, una clase es como “una caja
www.detodoprogramacion.com
Capítulo 6:
Clases
// Esta clase define un pila de enteros que puede almacenar hasta 10 valores.
c1ass Stack {
int stck[] = new int[10];
int tos;
// Inicializa el índice del elementos superior en la pila
Stack () {
tos = -1;
}
// Coloca un dato en la pila
void push (int item) {
if (tos == 9)
System.out.println("La pila está llena.");
else
stck[++tos] = item;
}
// Retira un dato de la pila
int pop () {
if (tos < 0) {
System.out.println("La pila está vacía.");
return 0;
}
else
return stck [tos--];
}
}
La clase Stack define dos variables y tres métodos. El arreglo stck almacena la pila de enteros.
Este arreglo es indexado por la variable tos, que contiene en todo momento el índice
del elemento en la parte superior de la pila. El constructor Stack( ) inicializa la variable tos
con el valor –1, que indica que la pila está vacía. El método push( ) coloca un dato en la pila, y
para recuperarlo se llama al método pop( ). Como el acceso a la pila se lleva a cabo mediante
los métodos push( ) y pop( ), el hecho de que la pila esté almacenada en un vector no tiene
importancia por lo que se refiere a la utilización de la pila. Por ejemplo, aunque una pila pueda
estar almacenada en una estructura de datos más compleja como una lista, la interfaz definida
por push( ) y pop( ) será la misma.
www.detodoprogramacion.com
PARTE I
negra”. No es necesario saber lo que ocurre dentro de la caja para poder utilizarla por medio de
su interfaz. De hecho al estar oculto el contenido de la “caja” éste puede cambiar sin afectar la
percepción exterior. A medida que nuestro código utiliza a la clase por medio de sus métodos,
los detalles internos pueden cambiar sin causar efectos fuera de la clase.
La aplicación práctica de lo dicho anteriormente se muestra mediante uno de los ejemplos
típicos del encapsulamiento: la pila. Una pila (stack, por su nombre en inglés) almacena datos de
manera que se retiran en orden inverso al de entrada, es decir, un stack es como una pila
de platos encima de una mesa el primer plato puesto encima de la mesa es el último en ser
utilizado. Las pilas se controlan mediante dos operaciones tradicionales denominadas push y pop.
Para colocar un dato en la parte superior de la pila se utiliza la operación de push, y para retirarlo
la operación pop. Veamos cuan sencillo resulta encapsular el mecanismo completo de una pila.
La clase denominada Stack, que se muestra a continuación, implementa una pila de
enteros.
123
124
Parte I:
El lenguaje Java
La clase TestStack prueba el funcionamiento de la clase Stack: crea dos pilas de enteros,
coloca algunos valores en cada una y después los retira:
class TestStack {
public static void main (String args[]) {
Stack miPilal = new Stack();
Stack miPila2 = new Stack();
// pone algunos números en la pila
for (int i=0; i ");
System.out.println(nombre + ": $" + bal);
}
}
class CuentaBalance {
public static void main(String args[]) {
Balance actual[] = new Balance[3];
www.detodoprogramacion.com
PARTE I
en un subdirectorio del directorio actual, éste será encontrado. Segundo, podemos especificar
la ruta o rutas de directorios, en donde buscar nuestros paquetes, utilizando la variable de
ambiente CLASSPATH.
Tercero, podemos utilizar la opción –classpath con java y javac para especificar la ruta del
directorio donde se localizan nuestras clases.
Por ejemplo, observe la siguiente especificación:
185
186
Parte I:
El lenguaje Java
actual [0] = new Balance("K. J. Fielding", 123.23);
actual [1] = new Balance("Will Tell", 157.02);
actual [2] = new Balance("Tom Jackson", -12.33);
for(int i=0; i<3; i++) actual[i].show();
}
}
Llamemos a este archivo CuentaBalance.java, y se coloquémoslo en un directorio denominado
MiPaquete.
Luego compilemos este archivo, asegurando que el archivo resultante .class también esté
en el directorio MiPaquete. Ahora es posible la ejecución de la clase CuentaBalance, usando la
siguiente línea de comando:
java MiPaquete.CuentaBalance
Recuerde que deberá estar un directorio arriba de MiPaquete cuando ejecute este comando (o
bien utilizar una de las dos opciones alternativas descritas en la sección anterior para asignar a la
variable de entorno CLASSPATH la ubicación del MiPaquete).
Tal y como se ha explicado, CuentaBalance es parte del paquete MiPaquete. Esto significa
que no se puede ejecutar por sí misma, es decir, no es posible utilizar la siguiente línea de
comandos:
java CuentaBalance
CuentaBalance debe estar precedida por el nombre de su paquete.
Protección de acceso
En los capítulos precedentes hemos presentado varios aspectos del mecanismo de control de
acceso en Java, así como sus especificadores de acceso. Por ejemplo, ya sabemos que el acceso
a los miembros privados de una clase sólo está permitido para otros miembros de esa clase.
Los paquetes añaden otra dimensión al control de acceso. Java proporciona muchos niveles de
protección que permiten un control adecuado de la visibilidad de las variables y métodos dentro
de las clases, subclases y paquetes.
Las clases y los paquetes son medios que permiten la encapsulación y definen el
espacio de nombres y campo de acción de las variables y los métodos. Los paquetes actúan
como contenedores para las clases y otros paquetes subordinados. Las clases actúan como
contenedores de datos y código. La clase es la unidad más pequeña de abstracción en Java. Dada
la interacción entre clases y paquetes, Java establece cuatro categorías de visibilidad para los
miembros de la clase:
• Subclases en el mismo paquete
• No subclases en el mismo paquete
• Subclases en diferentes paquetes
• Clases que no están en el mismo paquete ni son subclases
Los tres especificadores de acceso, private, public y protected, proporcionan diferentes
formas de producir los diferentes niveles de acceso requeridos por estas categorías. La Tabla 9.1
resume las interacciones.
www.detodoprogramacion.com
Capítulo 9:
TABLA 9.1
Privado
Sin Modificar Protegido
Público
Misma clase
Sí
Sí
Si
Sí
Subclase
del mismo
paquete
No
Sí
Sí
Sí
No subclase
del mismo
paquete
No
Sí
Sí
Sí
Subclase
de diferente
paquete
No
No
Sí
Sí
No subclase
de diferente
paquete
No
No
No
Sí
Aunque el mecanismo de control de acceso de Java puede parecer muy complicado, es
posible simplificarlo como se explica a continuación. Se puede acceder a cualquier elemento
declarado como public desde cualquier parte del programa. No se puede acceder a un
elemento declarado como private desde fuera de su clase. Cuando un elemento no tiene una
especificación de acceso explícita, es visible para las subclases así como para otras clases que
estén dentro del mismo paquete. En esto consiste el acceso por omisión. Si se desea que un
elemento sea visible desde fuera del paquete actual, pero solamente para subclases derivadas
directamente de la clase a que pertenece el elemento, hay que declarar al elemento como
protected.
La Tabla 9.1 se aplica sólo a los miembros de las clases. Una clase tiene solamente dos
niveles posibles de acceso: por defecto y público. Cuando se declara una clase como public, es
accesible por cualquier otra parte del código. Si una clase tiene acceso por defecto, entonces
sólo se puede acceder a ella por código que esté dentro del mismo paquete. Cuando una clase
es public, ésta debe ser la única clase pública en el archivo en el cual está declarada, y el archivo
debe tener el mismo nombre que esa clase pública.
Ejemplo de acceso
El siguiente ejemplo muestra todas las combinaciones de modificadores de control de acceso. En
el ejemplo hay dos paquetes y cinco clases. Recuerde que las clases de los dos paquetes deben
ser almacenadas en directorios que tengan el nombre de sus respectivos paquetes; en este caso,
pl y p2.
El código fuente del primer paquete define tres clases: Proteccion, Derivada, y
MismoPaquete. La primera clase define cuatro variables del tipo int en cada uno de los modos
de protección permitidos. La variable n se declara con la protección por omisión, mientras que
n_pri es private, n_pro es protected, y n_pub es public.
Cada una de las otras clases de este ejemplo intenta acceder a las variables en una instancia
de esta primera clase. Las líneas que no se compilan debido a las restricciones de acceso, se
www.detodoprogramacion.com
187
PARTE I
Acceso a los miembros
de una clase
Paquetes e interfaces
188
Parte I:
El lenguaje Java
marcan como comentario en la línea correspondiente. Antes de cada una de estas líneas hay un
comentario que indica los lugares desde los que el acceso estaría permitido.
La segunda clase, Derivada, es una subclase de Proteccion dentro del mismo paquete, pl.
Esto garantiza que Derivada accede a las variables de Proteccion excepto a n_pri, la variable
declarada como private. La tercera clase, MismoPaquete, no es una subclase de Proteccion,
pero está en el mismo paquete y tiene acceso a todo excepto a n_pri.
Este es el archivo Proteccion.java:
package p1;
public class Proteccion {
int n = 1;
private int n_pri = 2;
protected int n_pro = 3;
public int n_pub =4;
public Proteccion() {
System.out.println("Constructor
System.out.println("n = " + n);
System.out.println("n_pri = " +
System.out.println("n_pro = " +
System.out.println("n_pub = " +
}
base");
n_pri);
n_pro);
n_pub);
}
Este es el archivo Derivada.java:
package p1;
class Derivada extends Proteccion {
Derivada () {
System.out.println("Constructor de la clase Derivada");
System.out.println("n = " + n);
// Sólo para su clase
// System.out.println("n_pri =" + n_pri);
System.out.println("n_pro = " + n_pro);
System.out.println("n_pub = " + n_pub);
}
}
Este es el archivo MismoPaquete.java:
package p1;
class MismoPaquete {
MismoPaquete () {
Proteccion p = new Proteccion();
System.out.println("Constructor de la clase MismoPaquete");
System.out.println("n = " + p.n);
// Sólo para su clase
// System.out.println("n_pri = " + p.n_pri);
System.out.println("n_pro = " + p.n_pro);
www.detodoprogramacion.com
Capítulo 9:
Paquetes e interfaces
189
System.out.println("n_pub = " + p.n_pub);
}
A continuación se presenta el código fuente del otro paquete, p2. Las dos clases definidas
en p2 muestran las otras dos condiciones afectadas por el control de acceso. La primera clase,
Proteccion2, es una subclase de pl.Proteccion. Esto garantiza el acceso a todas las variables de
p1.Proteccion, excepto a n_pri, que se ha declarado como private, y a n, la variable declarada
con la protección por omisión, es decir, a la que sólo tendrán acceso desde elementos de su
clase o de su paquete, pero no desde subclases pertenecientes a otros paquetes. Finalmente, la
clase OtrosPaquetes tiene acceso solamente a una variable, n_pub, la cual fue declarada como
public.
Éste es el archivo Proteccion2.java:
package p2;
class Proteccion2 extends p1.Proteccion {
Proteccion2() {
System.out.println("Constructor de clase con herencia
en paquetes distintos");
// Sólo para su clase o paquete
// System.out.println("n = " + n);
// Sólo para su clase
// System.out.println("n _pri = " + n_pri);
System.out.println("n_pro = " + n_pro);
System.out.println("n_pub = " + n_pub);
}
}
Este es el archivo OtroPaquete.java:
package p2;
c1ass OtroPaquete {
OtroPaquete () {
p1.Proteccion p = new p1.Proteccion();
System.out.println("Constructor de la clase OtroPaquete");
// Sólo para su clase o paquete
// System.out.println("n = " + p.n);
// Sólo para su clase
// System.out.println("n_pri = " + p.n_pri);
// Sólo para su clase, subc1ase o paquete
// System.out.println("n_ro = "+ p.n_pro);
System.out.print1n("n_pub = " + p.n_pub);
}
}
Para probar estos dos paquetes se pueden utilizar los dos archivos de prueba que se
muestran a continuación. En primer lugar, el correspondiente al paquete pl:
www.detodoprogramacion.com
PARTE I
}
190
Parte I:
El lenguaje Java
// Demo paquete p1.
package p1;
// Crea instancias de las distintas clases del paquete p1.
public c1ass Demo {
pub1ic static void main(String args[]){
Proteccion ob1 = new Proteccion();
Derivada ob2 = new Derivada ();
MismoPaquete ob3 = new MismoPaquete ();
}
}
El archivo de prueba para p2 es el siguiente:
// Ejemplo del paquete p2.
package p2;
// Crea instancias de las distintas clases del paquete p2.
public c1ass Demo {
pub1ic static void main(String args[]} {
Proteccion2 ob1 = new Proteccion2();
OtroPaquete ob2 = new OtroPaquete ();
}
}
Importar paquetes
Después de haber visto la existencia de los paquetes, que constituyen un mecanismo adecuado
para separar en compartimentos unas clases de otras, resulta sencillo entender por qué todas las
clases incorporadas a Java están almacenadas en paquetes. No existen clases del núcleo de Java
en el paquete sin nombre utilizado por omisión; todas las clases estándares están almacenadas
en algún paquete con nombre propio. Ya que las clases contenidas en un paquete deben ser
accedidas utilizando el nombre o nombres del paquete que las contiene, puede resultar tedioso
escribir el nombre completo para cada clase que se quiera usar. Por este motivo, Java incluye la
sentencia import, que permite hacer visibles ciertas clases o paquetes completos. Una vez que
se ha importado una clase, se puede hacer referencia a ella usando sólo su nombre. La sentencia
import es una comodidad para el programador, y no es técnicamente necesaria para escribir un
programa. Sin embargo, al escribir un programa en el que haya una cantidad considerable de
clases, la sentencia import permitirá ahorrar escritura.
En un archivo fuente Java, las sentencias import tienen lugar inmediatamente después de
la sentencia package, en caso de que exista, y antes de la definición de cualquier clase. La forma
general de la sentencia import es la siguiente:
import pkg1 [.pkg2] .(nombre_de_clase | *);
Aquí, pkgl es el nombre del paquete de nivel superior, y pkg2 es el nombre del paquete
subordinado contenido en el paquete exterior y separado por un punto (.). En la práctica, no
existe límite para la profundidad de una jerarquía de paquetes, excepto el impuesto por el
sistema de archivos. Finalmente, se puede especificar explícitamente un nombre_de_clase o un
asterisco (*), para indicar al compilador de Java que debe importar el paquete completo. El
siguiente fragmento de código muestra el uso de ambas formas:
www.detodoprogramacion.com
Capítulo 9:
Paquetes e interfaces
191
import java.util.Date;
import java.io.*;
especialmente si se importan varios paquetes grandes. Por esta razón conviene nombrar
explícitamente las clases que se quiere usar, en lugar de importar el paquete completo. Sin
embargo, la utilización del asterisco no tiene efecto alguno sobre el tiempo de ejecución o tamaño
de las clases.
Todas las clases estándares de Java están almacenadas en un paquete denominado java.
Las funciones que constituyen el lenguaje básico de Java están almacenadas en un paquete
contenido en el paquete java y que se llama java.lang. Normalmente, es necesario importar
cada paquete o clase que se quiera utilizar, pero debido a que Java no tiene utilidad sin la
funcionalidad que se encuentra en java.lang, esta importación la realiza el compilador para
todos los programas implícitamente. Esto es equivalente a tener al comienzo de todos los
programas la siguiente línea:
import java.lang.*;
Si en dos paquetes distintos, que se importan utilizando la opción de asterisco, existen clases
con el mismo nombre, el compilador no dará ningún mensaje a menos que se trate de utilizar
una de esas clases. En ese caso, se obtendrá un error en tiempo de compilación y será necesario
poner explícitamente el nombre de la clase especificando su paquete.
La sentencia import es opcional. En cualquier ubicación en que se utilice el nombre de una
clase, se puede poner el nombre completo, que incluye los nombres de su jerarquía de paquetes
completa. Por ejemplo, el siguiente fragmento de código utiliza una sentencia de importación.
import java.util.*;
class MiFecha extends Date {
}
El mismo ejemplo sin la sentencia import se ve así:
class MiFecha extends java.util.Date {
}
En esta segunda versión se utiliza el nombre completo de la clase Date.
Tal y como se muestra en la Tabla 9.1, cuando se importa un paquete, sólo aquellos
elementos del paquete declarados como public estarán disponibles para las clases que no son
subclases del código importado. Por ejemplo, si se quiere que la clase Balance del paquete
MiPaquete, mostrada anteriormente, sea la única clase accesible para uso general fuera de
MiPaquete, entonces será necesario declararla como public y ponerla en su propio archivo, tal
y como se muestra a continuación:
package MiPaquete;
/* Ahora, la clase Balance, su constructor, y su método
show() son públicos. Esto significa que pueden ser utilizados
por código que no sea una subclase y esté fuera de su paquete.
*/
public class Balance {
www.detodoprogramacion.com
PARTE I
PRECAUCIÓN La opción que utiliza el asterisco puede incrementar el tiempo de compilación,
192
Parte I:
El lenguaje Java
String nombre;
double bal;
public Balance(String n, double b) {
nombre = n;
bal = b;
}
public void show() {
if (bal<0)
System.out.print(" - - > ");
System.out.println(nombre + ": $" + bal);
}
}
Ahora la clase Balance es public. También su constructor y su método show( ) son public.
Esto significa que se puede acceder a ellos desde cualquier tipo de código que esté fuera de
MiPaquete. Por ejemplo, en las líneas que siguen, TestBalance importa MiPaquete y entonces
puede hacer uso de la clase Balance:
import MiPaquete.*;
class TestBalance {
public static void main(String args[]) {
/* Como Balance es pública, se puede usar la clase Balance
y llamar a su constructor. */
Balance test = new Balance("J. J. Jaspers", 99.88);
test.show(); // también se puede llamar a show()
}
}
Si se elimina el especificador public de la clase Balance y se intenta compilar TestBalance,
se obtendrán los errores que se han comentado anteriormente.
Interfaces
Mediante la palabra clave interfaz, se puede abstraer completamente la interfaz de una clase
de su implementación, es decir, usando una interfaz es posible especificar lo que una clase
debe hacer, pero no cómo debe hacerlo. Las interfaces son sintácticamente semejantes a las
clases, pero carecen de las variables de instancia, y sus métodos se declaran sin un cuerpo. En la
práctica, esto implica que se pueden definir interfaces que no hagan suposiciones sobre cómo se
implementan. Una vez definida una interfaz, cualquier número de clases puede implementarla.
Además una clase puede implementar cualquier número de interfaces.
Para implementar una interfaz, una clase debe crear el conjunto completo de métodos
definidos por la interfaz. Sin embargo, cada clase tiene la libertad de determinar los detalles de
su implementación. Mediante la palabra clave interfaz, Java permite aplicar por completo la idea
“una interfaz, múltiples métodos” definida por el polimorfismo.
Las interfaces se diseñan para dar soporte a la resolución dinámica de métodos durante
la ejecución. Normalmente, para que un método de una clase pueda ser llamado desde
otra clase, es preciso que ambas clases estén presentes durante la compilación, con el fin de
que el compilador de Java pueda comprobar que el formato de los métodos es compatible.
www.detodoprogramacion.com
Capítulo 9:
Paquetes e interfaces
NOTA
Las interfaces aportan la mayor parte de la funcionalidad que se requiere en muchas
aplicaciones. En otros lenguajes como C++ esto se consigue recurriendo a la herencia
múltiple.
Definición de una interfaz
Una interfaz se define de manera muy similar a como lo sería una clase. La forma general de
definir una interfaz es la siguiente:
acceso nombre_interfaz {
tipo_devuelto método1 (lista_de_parámetros);
tipo_devuelto método2 (lista_de_parámetros);
tipo var_final1 = valor;
tipo var_final2 = valor;
// ...
tipo_devuelto métodoN (lista_de_parámetros);
tipo var_finalN = valor;
}
Cuando no se indica ningún especificador de accesso, se aplica el valor de acceso por omisión
y la interfaz está disponible sólo para otros miembros del paquete en que se declara. Cuando
se declara como public, la interfaz puede ser utilizada por cualquier otro código. En este caso,
la interfaz debe ser la única declarada pública en su archivo y el archivo debe tener el mismo
nombre que la interfaz. nombre es el nombre de la interfaz y puede ser cualquier identificador
válido. Observe que los métodos que se declaran no tienen cuerpo y terminan con un punto
y coma después de la lista de parámetros. Son esencialmente métodos abstractos, ya que no
puede haber implementación por defecto de un método declarado dentro de una interfaz. Cada
clase que incluya una interfaz debe implementar todos sus métodos.
Es posible declarar variables dentro de las declaraciones de interfaces. Las variables declaradas
dentro de una interfaz son implícitamente, final y static, esto significa que no pueden ser
alteradas por la implementación de la clase y que deben ser inicializadas con un valor constante.
Todos los métodos y variables son implícitamente public.
A continuación se muestra un ejemplo de definición de una interfaz sencilla, que contiene
un método llamado callback( ) que toma un sólo parámetro entero.
interface Callback {
void callback(int param);
}
www.detodoprogramacion.com
PARTE I
Este requisito da lugar por sí mismo a un entorno de clases estático y no extensible.
Inevitablemente, en un sistema como este, la funcionalidad aumenta a medida que se sube en
la jerarquía de las clases, de forma que los mecanismos estarán disponibles para más y más
subclases. Las interfaces se diseñan para evitar este problema, desconectando la definición
de un método o de un conjunto de métodos de la jerarquía de herencia. Como las interfaces
tienen una jerarquía distinta de la de las clases, es posible que clases que no están relacionadas
en términos de jerarquía implementen la misma interfaz, lo que muestra el verdadero poder de
las interfaces.
193
194
Parte I:
El lenguaje Java
Implementación de interfaces
Una vez definida una interfaz, una o más clases pueden implementarla. Implementar una
interfaz consiste en incluir la sentencia implements en la definición de la clase y crear los
métodos definidos por la interfaz. La forma general de una clase que incluye la sentencia
implements es la siguiente:
class nombre_de_clase [extends superclase] [implements interfaz [,interfaz...]] {
// cuerpo de la clase
}
Si una clase implementa más de una interfaz, las interfaces se separan con comas. Si una
clase implementa dos interfaces que declaran al mismo método, entonces los clientes de
ambas interfaces deberán usar al mismo método. Los métodos que implementan una interfaz
deben declararse como public. Además, la firma del método implementado debe coincidir
exactamente con el formato especificado en la definición de la interfaz.
El siguiente ejemplo muestra una clase que implementa la interfaz Callback, presentada
anteriormente.
class Cliente implements Callback {
// Implementa la interfaz Callback
public void callback(int p) {
System.out.println("callback llamado con" + p);
}
}
Observe que se declara callback( ) usando el especificador de acceso public.
NOTA Cuando se implementa un método de una interfaz, debe ser declarado como public.
Se permite y es común que las clases que implementan interfaces definan miembros
adicionales propios. Por ejemplo, la siguiente versión de Cliente implementa callback( ) y
añade el método nonIfaceMeth( ):
class Cliente implements Callback {
// Implementa la interfaz Callback
public void callback(int p) {
System.out.println("callback llamado con" + p);
}
void nonIfaceMeth() {
System.out.println("Las clases que implementan interfaces " +
"pueden definir también otros miembros.");
}
}
Acceso a la clase implementada mediante referencias del tipo de la interfaz
Se pueden declarar variables como referencia a objetos que usan una interfaz como tipo en
lugar de una clase. Se puede hacer referencia a cualquier instancia de cualquier clase que
implementa una interfaz declarada por medio de tales variables. Cuando se llama a un método
por medio de una de estas referencias, se llamará a la versión correcta que se basa en la
www.detodoprogramacion.com
Capítulo 9:
Paquetes e interfaces
PRECAUCIÓN Teniendo en cuenta que la búsqueda dinámica de un método durante la ejecución
supone un mayor tiempo de proceso, en comparación con la llamada normal a un método en Java,
conviene ser cuidadosos y no utilizar interfaces innecesariamente en códigos cuyo rendimiento es
crítico.
En el siguiente ejemplo se llama al método callback( ) por medio de una variable de
referencia a la interfaz:
class TestIface {
public static void main(String args[]) {
Callback c = new Cliente();
c. callback (42) ;
}
}
La salida de este programa es la siguiente:
callback llamado con 42
Observe que se ha declarado la variable c del tipo de la interfaz Callback, aunque se le ha
asignado una instancia de Cliente. Aunque se puede utilizar c para acceder al método
callback( ), no sirve para acceder a otros miembros de la clase Cliente. Una variable de
referencia a una interfaz sólo tiene conocimiento de los métodos que figuran en la declaración
de su interfaz. Por lo tanto, c no podría utilizarse para acceder al método nonIfaceMeth( ), ya
que éste está definido en Cliente pero no en Callback.
El ejemplo anterior muestra, de una manera mecánica, cómo una variable de referencia
a una interfaz puede acceder a la implementación de un objeto, pero no muestra el poder del
polimorfismo de tal referencia. Como ejemplo de esta utilidad, se crea, en primer lugar, una
segunda implementación de Callback:
// Otra implementación de Callback.
class OtroCliente implements Callback {
// Implementa la interfaz de Callback
public void callback(int p) (
System.out.println("Otra versión de callback");
System.out.println("El cuadrado de p es . + (p*p));
}
}
Ahora probemos la siguiente clase:
class TestIface2 {
public static void main(String args[]) {
www.detodoprogramacion.com
PARTE I
instancia actual de la interfaz que está siendo referenciada. Ésta es una de las características
clave de las interfaces. El método que se va a ejecutar se determina dinámicamente
durante la ejecución, permitiéndose que las clases en las que se encuentra dicho método
se creen después del código llamante, que puede seleccionar una interfaz sin tener ningún
conocimiento sobre el método “llamado”. Este proceso es similar al que se tenía al utilizar una
referencia de una superclase para acceder a un objeto de una subclase, tal y como se describió
en el Capítulo 8.
195
196
Parte I:
El lenguaje Java
Callback c = new Cliente();
OtroCliente ob = new OtroCliente();
c.callback(42);
c = ob; // c ahora es una referencia a un objeto de la clase OtroCliente
c.callback(42);
}
}
La salida de este programa es la siguiente:
callback llamado con 42
Otra versión de callback
El cuadrado de p es 1764
Como se puede ver, la versión del método callback( ) llamada se determina por el tipo del
objeto al que hace referencia c en tiempo de ejecución. Aunque éste es un ejemplo muy sencillo,
enseguida se verá otro más práctico.
Implementaciones parciales
Cuando una clase incluye una interfaz, pero no implementa completamente los métodos
definidos por esa interfaz, entonces la clase debe ser declarada como abstracta, utilizando la
palabra clave abstract. Por ejemplo:
abstract class Incomplete implements Callback {
int a, b;
void show() {
System.out.println(a + " " + b);
}
// ...
}
Donde Incomplete una clase que no implementa el método callback( ) y debe ser declarada
abstracta. Cualquier clase que herede Incomplete debe implementar el método callback( ), o
bien ser declarada también como abstracta.
Interfaces anidadas
Una interfaz puede ser declarada como miembro de una clase o de otra interfaz, cuando ello
ocurre la interfaz es llamada una interfaz miembro o una interfaz anidada. Una interfaz anidada
puede ser declarada como public, private o protected. Esto es diferente a lo que sucede con las
interfaces no anidadas las cuales deben ser declaradas como public o con el nivel de acceso por
omisión, como se describió antes. Cuando una interfaz anidada es utilizada fuera de su ámbito,
ésta debe ser llamada con su nombre y el de la clase o interfaz en la cual está contenida. De
manera que, fuera de la clase o interfaz en la cual la interfaz anidada se encuentra el nombre a
utilizar debe ser especificado completamente.
A continuación un ejemplo que muestra el uso de interfaces anidadas:
// Ejemplo de interfaces anidadas
// Esta clase contiene una interface anidada
class A {
// ésta es la interfaz anidada
public interface NestedIF {
www.detodoprogramacion.com
Capítulo 9:
Paquetes e interfaces
197
boolean isNotNegative(int x);
}
// B implementa la interfaz anidada
class B implements A.NestedIF {
public boolean isNotNegative(int x) {
return x < 0 ? false : true;
}
}
class NestedIFDemo {
public static void main(String args[]) {
// Utiliza una referencia a una interfaz anidada
A.NestedIF nif = new B();
if(nif.isNotNegative(l0))
System.out.println("10 es un número no negativo");
if(nif.isNotNegative(-12))
System.out.println("Esto no aparecerá en pantalla");
}
}
Observe que A define una interfaz miembro llamada NestedIF la cual es declarada public.
Enseguida B implementa la interfaz anidada a través de
implements A.NestedIF
Observe también que el nombre utilizado para implementar NestedIF es la especificación
completa que incluye nombre de la interfaz anidada y de su clase contenedora. Dentro del
método main se crea una referencia a A.NestedIF llamada nif y se le asigna una referencia a un
objeto de clase B. Esto es correcto debido a que B implementa a A.NestedIF.
Utilizando interfaces
Veamos un caso más práctico que nos ayude a entender la potencia de las interfaces. En
los capítulos anteriores se desarrolló una clase denominada Stack que implementaba una
pila sencilla de tamaño fijo. Sin embargo, existen otras formas de implementar una pila. Por
ejemplo, la pila puede ser de tamaño fijo o variable. Además, la pila se puede almacenar en un
arreglo, una lista, un árbol binario, etc. Independientemente de cómo se haya implementado
la pila, la interfaz es siempre la misma, es decir, los métodos push( ) y pop( ) definen la
interfaz de la pila al margen de los detalles de la implementación. Como la interfaz de la pila
es independiente de su implementación, es fácil definir dicha interfaz, dejando para cada
implementación los detalles más específicos. Veamos dos ejemplos.
En primer lugar se presenta la interfaz que define una pila de enteros. Coloquemos
este código en un archivo denominado IntStack.java. De está interfaz construiremos dos
implementaciones más adelante.
// Definición de la interfaz de una pila de enteros.
interfaz IntStack {
void push(int item); // almacena un elemento
int pop(); // recupera un elemento
}
www.detodoprogramacion.com
PARTE I
}
198
Parte I:
El lenguaje Java
El siguiente programa crea una clase llamada FixedStack que implementa una versión de
una pila de enteros de longitud fija.
// Esta implementación de IntStack utiliza almacenamiento fijo.
class FixedStack implements IntStack {
private int stck[];
private int tos;
// Reserva espacio e inicializa la pila
FixedStack(int size) {
stck = new int[size];
tos = -1;
}
// Coloca un elemento en la pila
public void push(int item) {
if(tos==stck.length-l) // se utiliza la variable miembro length
para conocer el tamaño del arreglo
System.out.println("La pila está llena.");
else
stck[++tos] = item;
}
// Retira un elemento de la pila
public int pop() {
if(tos < 0) {
System.out.println("La pila está vacía .");
return 0;
}
else
return stck[tos--];
}
}
c1ass IFTest (
public static void main(String args[]) {
FixedStack miPilal = new FixedStack(5);
FixedStack miPila2 = new FixedStack(8) ;
// Se almacenan algunos números en la pila
for(int i=0; i<5; i++) miPilal.push(i);
for(int i=0; i<8; i++) miPila2.push(i);
// Se retiran esos números de la pila
System.out.println ("Contenido de miPilal:");
for(int i=0; i<5; i++)
System.out.println(miPilal.pop());
System.out.println("Contenido de miPila2:") ;
for(int i=0; i<8; i++)
System.out.println(miPila2.pop());
}
}
A continuación se muestra otra implementación de IntStack que crea una pila dinámica
utilizando la misma definición de la interfaz. En esta implementación cada pila se construye con
www.detodoprogramacion.com
Capítulo 9:
Paquetes e interfaces
// Implementación de una pila de tamaño "creciente".
class DynStack implements IntStack {
private int stck[];
private int tos;
// Se reserva espacio y se inicializa la pila
DynStack(int size) {
stck = new int[size];
tos = -1;
}
// Se almacena un elemento en la pila
public void push(int item) {
// Si la pila está llena, se reserva espacio para una pila mayor
if(tos==stck.length-1) {
int temp[] = new int[stck.length * 2]: // Se duplica el tamaño
for(int i=0; ijava MultiCatch
a = 0
División entre 0: java.lang.ArithmeticException: / by zero
Después de los bloques try/catch.
C:\>java MultiCatch TestArg
a = 1
Índice del arreglo fuera de rango:
java.lang.ArraylndexOutOfBoundsException: 42
Después de los bloques try/catch.
Cuando se utilizan varias sentencias catch, es importante recordar que las subclases de la
clase Exception deben estar delante de cualquiera de sus superclases. Esto se debe a que una
sentencia catch que utiliza una superclase captura las excepciones de sus subclases y, por lo
tanto, éstas no se ejecutarán si están después de la superclase. Además, en Java se produce un
error si hay código no alcanzable. Como ejemplo, consideremos el siguiente programa:
/* Este programa contiene un error.
Una subclases debe ir delante de su superclase
en una serie sentencias catch. Si no,
se creará código inalcanzable y eso
resultará en un error en tiempo de compilación.
*/
class SuperSubCatch {
public static void main(String args[]) {
try {
int a = 0;
int b = 42 / a;
} catch(Exception e) {
www.detodoprogramacion.com
Capítulo 10:
Gestión de excepciones
}
}
Si se intenta compilar este programa, se recibirá un mensaje de error que establece
que no se accede a la segunda sentencia catch porque la excepción ya ha sido capturada.
Como ArithmeticException es una subclase de la clase Exception, la primera sentencia
catch gestionará todos los errores que se basan en la clase Exception, incluyendo
ArithmeticException. Esto significa que la segunda sentencia catch no se ejecuta. Para
solucionar el problema basta colocar en orden inverso a las sentencias catch.
Sentencias try anidadas
La sentencia try puede ser anidada. Esto es, una sentencia try puede estar dentro del bloque de
otro try. Cada vez que una sentencia try es ingresada, el contexto de esa excepción se vuelve a
colocar en la pila. Si una sentencia try colocada en el cuerpo de otra sentencia try no realiza la
gestión de una excepción particular, la pila es liberada y la siguiente sentencia try inspeccionada
en busca de una coincidencia. Esto continua hasta que una de las sentencias catch tiene éxito
o hasta que todas las sentencias try anidadas han sido pasadas. Si ninguna sentencia catch
coincide, la máquina virtual de Java atrapará la excepción. Veamos un ejemplo de sentencias try
anidadas:
// Ejemplo de sentencias try anidadas
class NestTry {
public static void main(String args[]) {
try {
int a = args.length;
/* Si no se utiliza ningún argumento en la línea
de comandos, la siguiente sentencia generará una
excepción de división entre cero. */
int b = 42 / a;
System.out.println("a = " + a);
try { // bloque try anidado
/* Si se utiliza un argumento en la línea de órdenes
entonces se genera una excepción de división entre cero
en el siguiente código */
if(a==l) a = a/(a-a); // división entre cero
/* Si se utilizan dos argumentos en la línea de órdenes
entonces se genera una excepción de índice de arreglo
fuera de rango */
if (a==2) {
int c [] = { 1 };
www.detodoprogramacion.com
PARTE I
System.out.println("Capturando una excepción genérica.");
}
/* Este catch nunca se alcanzará porque la excepción de tipo
ArithmeticException es una subclase de la clase Exception. */
catch(ArithmeticException e) { // ERROR – esto no se ejecuta
System.out.println( "Esto nunca se ejecuta.");
}
211
212
Parte I:
El lenguaje Java
c[42] = 99; // genera una excepción por el índice
de arreglo fuera de rango
}
} catch(ArraylndexOutOfBoundsException e) {
System.out.println("Índice del arreglo fuera de rango: " + e);
}
} catch(ArithmeticException e) {
System.out.println("División entre 0: " + e);
}
}
}
Como se puede ver, este programa anida un bloque try con otro. El programa trabaja como
sigue. Cuando se ejecuta el programa sin argumentos en la línea de órdenes, una excepción
de división entre cero se genera por el bloque try exterior. La ejecución de programa con un
argumento en la línea de órdenes genera una excepción de división entre cero en el bloque
try anidado. Dado que el bloque interno no atrapa la excepción, ésta es enviada al bloque try
externo, donde es gestionada. Si ejecutamos el programa con dos argumentos en la línea de
órdenes, una excepción de índice de arreglo fuera de rango se genera en el bloque interno. Éste
es un ejemplo de la salida desplegada por el programa anterior:
C:\>java NestTry
División entre 0: java.lang.ArithmeticException: / by zero
C:\>java NestTry Uno
a = 1
División entre 0: java.lang.ArithmeticException: / by zero
C:\>java NestTry Uno Dos
a = 2
Índice del arreglo fuera de rango:
java.lang.ArrayIndexOutOfBoundsException: 42
El anidamiento de sentencias try puede ocurrir de manera menos evidente cuando están de
por medio invocaciones a métodos. Por ejemplo, podemos encerrar una llamada a un método en
un bloque try y dentro de ese método colocar otra sentencia try. En este caso, la sentencia try
en el método se considera anidada dentro del bloque try externo que mandó llamar al método.
A continuación se presenta el programa previo reorganizado para que el bloque anidado try
ahora esté localizado dentro del método nesttry.
/* La sentencia try puede estar anidada implícitamente
vía llamadas a métodos */
class MethNestTry {
static void nesttry (int a) {
try ( // bloque try anidado
/* Si se utiliza un argumento en la línea de órdenes,
se generará una excepción de división entre cero
en el siguiente código. */
if (a==l) a = a / (a - a); // división entre cero
/* Si se utilizan dos argumentos en la línea de órdenes
entonces se genera una excepción de índice de arreglo
fuera de rango */
www.detodoprogramacion.com
Capítulo 10:
Gestión de excepciones
}
public static void main (String args []) {
try {
int a = args.length;
/* Si no se utiliza ningún argumento en la línea
de comandos, la siguiente sentencia generará una
excepción de división entre cero. */
int b = 42 / a;
System.out.println("a = " + a);
nesttry (a) ;
} catch(ArithmeticException e) (
System.out.println("División entre 0: " + e);
}
}
}
La salida de este programa es idéntica a la del ejemplo anterior.
throw
Hasta el momento, se han capturado excepciones lanzadas por el intérprete Java. Sin embargo,
también el propio programa puede lanzar explícitamente una excepción mediante la sentencia
throw. La forma general de esta sentencia es la siguiente:
throw objetoThrowable;
Donde objetoThrowable debe ser un objeto del tipo Throwable o una subclase de
Throwable. No se pueden utilizar como excepciones tipos sencillos como int o char, ni
tampoco clases String y Object que no son Throwable. Se puede obtener un objeto de la clase
Throwable de dos formas: utilizando un parámetro en la cláusula catch, o creando un nuevo
objeto con el operador new.
La ejecución del programa se para inmediatamente después de una sentencia throw;
y cualquiera de las sentencias que siguen no se ejecutarán. A continuación se inspecciona
el bloque try más próximo que la encierra, para ver si contiene una sentencia catch que
coincida con el tipo de excepción. Si es así, el control se transfiere a esa sentencia. Si no, se
inspecciona el siguiente bloque try que la engloba, y así sucesivamente. Si no se encuentra
una sentencia catch cuyo tipo coincida con el de la excepción, entonces el gestor de
excepciones por omisión interrumpe el programa e imprime el trazado de la pila.
A continuación se presenta un programa de ejemplo que crea y lanza una excepción. El
gestor que captura la excepción la relanza al gestor más externo.
www.detodoprogramacion.com
PARTE I
if (a==2) {
int c [] = { 1 };
c[42] = 99; // genera una excepción por el índice de arreglo
fuera de rango
}
} catch(ArraylndexOutOfBoundsException e) {
System.out.println("Índice del arreglo fuera de rango: " + e);
}
213
214
Parte I:
El lenguaje Java
// Ejemplo de la sentencia throw.
class ThrowDemo (
static void demoproc() {
try {
throw new NullPointerException("demo");
} catch(NullPointerException e) {
System.out.println("!Capturada dentro de demoproc.");
throw e; // se relanza la excepción
}
}
public static void main(String args[]) {
try {
demoproc () ;
} catch(NullPointerException e) {
System.out.println("Recapturada: " + e);
}
}
}
Este programa tiene dos oportunidades para tratar el mismo error. En la primera, el método
main( ) establece un contexto de excepción, y a continuación llama a demoproc( ). El método
demoproc( ) entonces establece otro contexto de gestión de excepciones e inmediatamente
lanza una nueva instancia de NullPointerException, que se captura en la siguiente línea.
Entonces la excepción se relanza. La salida resultante es la siguiente:
Capturada dentro de demoproc.
Recapturada: java.lang.NullPointerException: demo
El programa también ilustra cómo se crea uno de los objetos de la clase Exception estándar
de Java. Preste especial atención a la siguiente línea:
throw new NullPointerException("demo");
Aquí, new se utiliza para construir una instancia de NullPointerException. Todas las
excepciones incorporadas por Java en el tiempo de ejecución tienen al menos dos constructores:
uno sin parámetros y el otro con un parámetro del tipo cadena. Cuando se utiliza la segunda
forma, el argumento especifica una cadena que describe la excepción. Cuando se usa el objeto
como argumento de un print( ) o un println( ) se imprime esta cadena. También se puede
obtener el texto de la cadena mediante una llamada al método getMessage( ), que está definido
por la clase Throwable.
throws
Si un método puede dar lugar a una excepción que no es capaz de gestionar él mismo, se debe
especificar este comportamiento de forma que los métodos que llamen al primero puedan
protegerse contra esa excepción. Para ello se incluye una cláusula throws en la declaración
del método. Una cláusula throws da un listado de los tipos de excepciones que el método
podría lanzar. Esto es necesario para todas las excepciones, excepto las del tipo Error o
RuntimeException, o cualquiera de sus subclases.
www.detodoprogramacion.com
Capítulo 10:
Gestión de excepciones
tipo nombre_ método ( lista_de_parámetros ) throws lista_de_excepciones
{
// cuerpo del método
}
Donde, lista_de_excepciones es una lista de las excepciones que el método puede lanzar,
separadas por comas.
A continuación se muestra un programa incorrecto que trata de lanzar una excepción que
no captura. Como el programa no específica una sentencia throws que declare este hecho, no se
compilará.
// Este programa contiene un error y no compilará.
class ThrowsDemo {
static void throwOne() {
System.out.println("Dentro de throwOne.");
throw new IllegalAccessException("demo"):
}
public static void main(String args[] ) {
throwOne():
}
}
Es necesario hacer dos cambios para conseguir que este ejemplo compile. El primero
consiste en declarar que throwOne( ) lanza IllegalAccessException. El segundo es que main( )
debe definir una sentencia try/catch que capture esta excepción.
El ejemplo anterior corregido se muestra a continuación:
// Ahora es correcto.
class ThrowsDemo {
static void throwOne() throws IllegalAccessException {
System.out.println("Dentro de throwOne.");
throw new IllegalAccessException("demo"):
}
public static void main(String args[]) {
try {
throwOne ();
} catch (IllegalAccessException e) {
System.out.println("Capturada" + e);
}
}
}
La salida generada por la ejecución de este programa es la siguiente:
Dentro de throwOne
Capturada java.lang.IllegalAccessException: demo
www.detodoprogramacion.com
PARTE I
Todas las demás excepciones que un método puede lanzar se deben declarar en la cláusula
throws. Si esto no se hace así, el resultado es un error de compilación.
La forma general de la declaración de un método que incluye una sentencia throws es la
siguiente:
215
216
Parte I:
El lenguaje Java
finally
Cuando se lanzan excepciones, la ejecución dentro de un método sigue un camino no lineal
y bastante brusco que altera el flujo normal. Dependiendo de cómo se haya codificado el
método, puede incluso suceder que una excepción provoque que el método finalice de forma
prematura. Esto puede ser un problema en algunos casos. Por ejemplo, si un método abre un
archivo cuando comienza y lo cierra cuando finaliza, entonces no se puede permitir que el
mecanismo de gestión de excepciones omita la ejecución del código que cierra el archivo. La
palabra clave finally está diseñada para resolver este tipo de contingencias.
finally crea un bloque de código que se ejecutará después de que se haya completado un
bloque try/catch y antes de que se ejecute el código que sigue al bloque try/catch. El bloque
finally se ejecuta independientemente de que se haya lanzado o no alguna excepción. Si se ha
lanzado una excepción, el bloque finally se ejecuta, incluso aunque ninguna sentencia catch
coincida con la excepción. Cuando un método está a punto de devolver el control al método
llamante desde dentro de un bloque try/catch por medio de una excepción no capturada o
de una sentencia return explícita, se ejecuta también la cláusula finally justo antes de que
el método devuelva el control. Esta acción tiene utilidad para cerrar descriptores de archivos
o liberar cualquier otro recurso que se hubiera asignado al comienzo de un método con la
intención de liberarlo antes de devolver el control. La cláusula finally es opcional. Sin embargo,
cada sentencia try requiere, al menos, una sentencia catch o finally.
El siguiente programa muestra tres métodos distintos, que finalizan en tres diferentes
formas, ninguno sin ejecutar sus respectivas sentencias finally:
// Demostración de finally.
class FinallyDemo {
// A través de una excepción exterior al método.
static void procA() {
try {
System.out.println("Dentro de procA");
throw new RuntimeException("demo");
} finally {
System.out.println("finally de procA ");
}
}
// Se devuelve el control desde un bloque.
static void procB() {
try {
System.out.println("Dentro de procB");
return;
} finally {
System.out.println("finally de procB");
}
}
// Ejecución normal de un bloque try.
static void procC() {
try {
System.out.println("Dentro de procC");
} finally {
www.detodoprogramacion.com
Capítulo 10:
Gestión de excepciones
217
System.out.println("finally de procC");
}
public static void main(String args[]) {
try {
procA () ;
} catch (Exception e) {
System.out.println("Excepción capturada");
}
procB () ;
procC () ;
}
}
En este ejemplo, procA( ) sale prematuramente del bloque try lanzando una excepción. La
sentencia finally se ejecuta durante la salida. En el método procB( ) se sale del bloque try por
medio de la sentencia return. La sentencia finally se ejecuta antes de que el método procB( )
devuelva el control. En el método procC( ), la sentencia try se ejecuta normalmente, sin error.
Sin embargo, sí se ejecuta el bloque finally.
NOTA
Si se asocia un bloque finally con un bloque try, el bloque finally se ejecuta cuando concluye
el bloque try.
La salida generada por el programa anterior es la siguiente:
Dentro de procA
finally de procA
Excepción capturada
Dentro de procB
finally de procB
Dentro de procC
finally de procC
Excepciones integradas en Java
Dentro del paquete estándar java.lang, Java define varias clases de excepciones. Algunas ya
se han usado en los ejemplos anteriores. Las excepciones más comunes son subclases del
tipo estándar RuntimeException. Como se explicó antes estas excepciones no necesitan ser
incluidas en la lista throws de ningún método. Dentro del lenguaje Java, se denomina excepciones
no comprobadas a estas excepciones, ya que el compilador no controla si el método gestiona o
lanza estas excepciones. Las excepciones de este tipo definidas en java.lang se detallan en la
Tabla 10.1. La Tabla 10.2 muestra un listado de las excepciones definidas por java.lang que
deben ser incluidas en la lista throws de un método si ese método puede generar una de estas
excepciones y no puede gestionarla por sí mismo. A estas excepciones se denomina excepciones
comprobadas. Java define muchos otros tipos de excepciones en diversos paquetes de su
biblioteca de clases.
www.detodoprogramacion.com
PARTE I
}
218
Parte I:
El lenguaje Java
Excepciones
Significado
ArithmeticException
Error aritmético, como, por ejemplo, división entre cero.
ArrayIndexOutOfBoundsException Índice del arreglo fuera de su límite o rango.
ArrayStoreException
Se ha asignado a un elemento de un arreglo un tipo incompatible.
ClassCastException
Conversión de tipo inválido.
IllegalArgumentException
Uso inválido de un argumento al llamar a un método.
IllegalMonitorStateException
Operación de monitor inválida, tal como esperar un hilo no
bloqueado.
IllegalStateException
El entorno o aplicación están en un estado incorrecto o inválido.
IllegalThreadStateException
La operación solicitada es incompatible con el estado actual del
hilo.
IndexOutOfBoundsException
Algún tipo de índice está fuera de rango o de su límite.
NegativeArraySizeException
Arreglo creado con un tamaño negativo.
NullPointerException
Uso incorrecto de una referencia a null.
NumberFormatException
Conversión incorrecta de un valor tipo string a un formato
numérico.
SecurityException
Intento de violación de seguridad.
StringIndexOutOfBounds
Intento de sobrepasar el límite o rango de un valor string.
UnsupportedOperationException
Operación no soportada.
TABLA 10.1 Excepciones no comprobadas definidas en java.lang como subclases de RuntimeException
Excepciones
Significado
ClassNotFoundException
No se ha encontrado la clase.
CloneNotSupportedException Intento de duplicación de un objeto que no implementa la interfaz
Cloneable.
IllegalAccessException
Se ha denegado el acceso a una clase.
InstantiationException
Intento de crear un objeto de una clase abstracta o interfaz.
InterruptedException
Hilo interrumpido por otro hilo.
NoSuchFieldException
No existe el campo solicitado.
NoSuchMethodException
No existe el método solicitado.
TABLA 10.2 Excepciones comprobadas definidas en java.lang
Creando excepciones propias
Aunque las excepciones del núcleo de Java gestionan la mayor parte de los errores habituales,
nosotros podríamos desear crear nuestros propios tipos de excepciones para tratar situaciones
específicas que se presenten en nuestras aplicaciones. Esto se puede hacer de forma bastante
www.detodoprogramacion.com
Capítulo 10:
Gestión de excepciones
Método
Descripción
Throwable fillInStackTrace( )
Devuelve un objeto de la clase Throwable que contiene el trazado
completo de la pila. Este objeto puede volver a ser lanzado.
Throwable getCause( )
Devuelve la excepción subyacente a la excepción actual. Si no existe
una excepción subyacente devuelve null.
String getLocalizedMessage( ) Devuelve una cadena con la descripción localizada de la excepción.
String getMessage( )
Devuelve la descripción de la excepción.
StackTraceElement[ ]
getStackTrace( )
Devuelve un arreglo que contiene el trazado de la pila, un elemento
a la vez, el arreglo es de tipo StackTraceElement. El método en la
parte superior de la pila es el último método llamado antes de que la
excepción fuera lanzada. Este método se localiza en la primera posición
del arreglo. La clase StackTraceElement da al programa acceso a la
información de cada elemento, como por ejemplo el nombre del método.
Throwable initCause
(Throwable causeExc)
Asocia la referencia causeExc con la excepción invocada como la
causa de la misma. Regresa una referencia a la excepción.
void printStackTrace( )
Presenta en pantalla el trazado de la pila
void printStackTrace(PrintStr
eam stream)
Envía el trazado de la pila a un determinado flujo.
printStackTrace(PrintWriter
stream)
Envía el trazado de la pila a un determinado flujo.
void setStackTrace(StackTrace Coloca en la pila los elementos especificados en el arreglo elements.
Element elements[ ])
Este método es para aplicaciones especializadas, no para uso
convencional.
String toString( )
Devuelve una cadena con la descripción de la excepción. Este método
es llamado por println( ) cuando se desea imprimir un objeto de la
clase Throwable.
TABLA 10.3 Métodos definidos por la clase Throwable
Además, estos métodos se pueden sobrescribir en las clases de excepción propias.
La clase Exception define cuatro constructores. Dos de ellos incluidos por JDK 1.4 para
soportar excepciones encadenadas, se describen en la siguiente sección. Los otros dos se
describen aquí:
Exception ( )
Exception (String msg)
www.detodoprogramacion.com
PARTE I
sencilla, definiendo una subclase de la clase Exception, que es por supuesto, una subclase de
Throwable. No es necesario que estas subclases creadas por el usuario implementen nada;
simplemente, su existencia en el sistema nos permitirá usarlas como excepciones.
La clase Exception no define por sí misma método alguno, pero hereda, evidentemente, los
métodos que proporciona la clase Throwable. Además, todas las excepciones, incluyendo las
creadas por nosotros, pueden disponer de los métodos definidos por la clase Throwable. Dichos
métodos se muestran en la Tabla 10.3.
219
220
Parte I:
El lenguaje Java
La primera forma crea una excepción sin descripción. La segunda forma nos permite especificar
una descripción para la excepción.
Aunque especificar una descripción cuando se crea una excepción es útil, alguna veces es
mejor sobrescribir al método toString( ), esto debido a que la versión de toString( ) definida
por la clase Throwable y heredada por la clase Exception primero despliega el nombre de
la excepción seguido de dos puntos y en seguida la descripción de la excepción dada en el
constructor. Al sobrescribir el método toString( ) es posible evitar que se muestre el nombre
de la excepción y los dos puntos, con ello podemos generar una salida más limpia, deseable en
algunos casos.
En el siguiente ejemplo se declara una subclase de la clase Exception que se usa
posteriormente para señalar una condición de error en un método. Dicha subclase sobrescribe
el método toString( ) para poder imprimir una descripción cuidadosamente adaptada de la
excepción.
// Este programa crea un tipo de excepción propio.
class MiExcepcion extends Exception {
private int detalle;
MiExcepcion (int a) {
detalle = a;
}
public String toString() {
return " MiExcepcion [" + detalle + "]";
}
}
class ExcepcionDemo (
static void compute(int a) throws MiExcepcion {
System.out.println("Ejecuta compute(" + a + ")");
if(a > 10)
throw new MiExcepcion(a);
System.out.println("Finalización normal");
public static void main(String args[]) {
try {
compute (1);
compute (20) ;
} catch (MiExcepcion e) {
System.out.println("Captura de: " + e);
}
}
}
En este ejemplo se define una subclase de Exception llamada MiExcepcion. Esta subclase
es muy sencilla: tiene únicamente un constructor y un método sobrecargado, toString( ), que
permitirá presentar el valor de la excepción.
La clase ExcepcionDemo define un método llamado compute( ) y que lanza un objeto del
tipo MyException. La excepción se lanza cuando el parámetro entero del método compute( )
es mayor que 10. El método main( ) establece un gestor de excepciones para MiExcepcion, y a
continuación llama al método compute( ) con un valor válido del parámetro, es decir, menor que
10, y con un valor no válido, para mostrar los dos caminos que sigue el código. El resultado es el
siguiente:
www.detodoprogramacion.com
Capítulo 10:
Gestión de excepciones
PARTE I
Ejecuta compute(l)
Finalización normal
Ejecuta compute(20)
Captura MiExcepcion[20]
221
Excepciones encadenadas
A partir del JDK 1.4, una nueva característica se ha incorporado en el subsistema de excepciones:
las excepciones encadenadas. La característica de excepción encadenada nos permite asociar
una excepción con otra. Esta segunda excepción describe la causa de la primera excepción.
Por ejemplo, imaginemos una situación en la cual un método lanza una excepción del tipo
ArithmeticException debido a un intento de división entre cero. Sin embargo, la causa real del
problema fue la ocurrencia de un error de E/S la cual causa que el divisor reciba un valor
inapropiado. Aunque el método lanzará una excepción ArithmeticException, debido a que
ese es el error que ha ocurrido, podríamos además desear que el código que llamó al método
conozca que la causa subyacente fue un error de E/S. Las excepciones encadenadas nos permiten
gestionar ésta y otras situaciones en las cuales existen capas o niveles de excepciones.
Para crear excepciones encadenadas se añadieron dos métodos y dos constructores a la clase
Throwable. Los constructores son:
Throwable ( Throwable exc )
Throwable ( String msg, Throwable exc )
En la primera forma, exc es la excepción que causa a la excepción actual. Esto es, exc es la razón
subyacente a la ocurrencia de la nueva excepción. La segunda forma nos permite especificar
una descripción al mismo tiempo que se especifica la excepción causante de la actual. Estos dos
constructores también han sido añadidos a las clases Error, Exception y RuntimeException.
Los métodos de encadenado de excepciones añadidos a la clase Throwable son getCause( )
e initCause( ). Estos métodos se muestran en la Tabla 10-3 y se repiten aquí
Throwable getCause( )
Throwable initCause(Throwable exc)
El método getCause( ) regresa la excepción subyacente a la excepción actual. Si no existe
una excepción subyacente regresa null. El método initCause( ) asocia exc con la excepción que
realiza la invocación y regresa una referencia a la excepción. Así podemos asociar una causa
con una excepción después de que la excepción ha sido creada. Sin embargo, la excepción
causante puede ser asociada sólo una vez. Por ello, es posible llamar a initCause( ) sólo una vez
para cada objeto de excepción. Si la excepción causante fue establecida por un constructor, no
es posible establecerla de nuevo utilizando initCause( ). En general, initCause( ) es utilizada
para asignar una causa a excepciones cuyas clases tipo no cuentan con los dos constructores
adicionales descritos antes.
Veamos un ejemplo que ilustra el mecanismo de gestión de excepciones encadenadas:
// Ejemplo de excepciones encadenadas
class ExcepcionEncadenadaDemo {
static void demoproc() {
// crea una excepción
NullPointerException e =
new NullPointerException("capa superior");
www.detodoprogramacion.com
222
Parte I:
El lenguaje Java
// añadir una causa
e.initCause(new ArithmeticException("causa"));
throw e;
}
public static void main(String args[]) {
try {
demoproc() ;
} catch (NullPointerException e) {
// mostrar la excepción superior
System.out.println("Atrapada: " + e);
// mostrar la excepción causante
System.out.println ("Causa Original: " +
e.getCause() );
}
}
}
La salida resultante de la ejecución del programa anterior es:
Atrapada: java.lang.NullPointerException: capa superior
Causa Original: java.lang.ArithmeticException: causa
En este ejemplo, la excepción de nivel superior es NullPointerException. A ésta se añade
una excepción de tipo ArithmeticException como causante. Cuando la excepción es lanzada
fuera del método demoproc( ), es atrapada por el método main( ). Ahí, la excepción de nivel
superior es mostrada seguida por la excepción subyacente, la cual es obtenida mediante la
llamada al método getCause( ).
Las excepciones encadenadas pueden ser continuadas con cualquier profundidad necesaria.
Así, la excepción causa puede a su vez tener una excepción causante. Aunque una cadena
excesivamente larga de excepciones puede ser signo de un pobre diseño del sistema.
Las excepciones encadenadas no son algo que todo programa necesite. Sin embargo, en
casos en los cuales el conocimiento de una causa subyacente es útil las excepciones encadenadas
ofrecen una solución elegante.
Utilizando excepciones
La gestión de excepciones proporciona un mecanismo muy potente para controlar programas
complejos, con muchas características dinámicas, durante la ejecución. Es importante considerar
a try, throw y catch como formas limpias de gestionar errores y problemas inesperados en la
lógica de un programa. A diferencia de otros lenguajes en los cuales se acostumbra devolver
un código de error cuando se produce un fallo, Java utiliza excepciones. Así cuando un método
puede fallar debemos hacer que lance una excepción. Ésta es una manera más limpia de tratar
los modos de fallo.
Una última cuestión que se ha de tener en cuenta sobre la gestión de excepciones en Java, es
que no se debe considerar este mecanismo como otra vía para realizar ramificaciones, ya que si
se hace así, lo que se consigue es crear un código que puede resultar finalmente incomprensible
y de difícil mantener.
www.detodoprogramacion.com
11
CAPÍTULO
Programación multihilo
A
diferencia de la mayoría de los lenguajes de programación, Java proporciona soporte para
la programación multihilo. Un programa multihilo contiene dos o más partes que pueden ser
ejecutadas de manera concurrente o simultánea. Cada parte del programa se denomina hilo,
y cada hilo define un camino de ejecución independiente. Por lo tanto, la programación multihilo es
una forma especializada de multitarea.
Probablemente esté familiarizado con la multitarea, ya que casi todos los sistemas operativos
modernos la permiten. Sin embargo, existen dos tipos distintos de multitarea: la basada en procesos
y la basada en hilos. Es importante comprender la diferencia entre las dos. Para la mayoría de
lectores la forma más familiar es la multitarea basada en el proceso. Un proceso es, en esencia, un
programa que se está ejecutando. Por lo tanto, la multitarea basada en procesos es la característica
que permite a su computadora ejecutar dos o más programas concurrentemente. Por ejemplo, la
multitarea basada en procesos permite que se ejecute el compilador Java al mismo tiempo que se
está utilizando el editor de textos. En la multitarea basada en procesos, un programa es la unidad
más pequeña de código que el sistema de cómputo puede gestionar.
En un entorno de multitarea basada en hilos, el hilo es la unidad de código más pequeña
que se puede gestionar. Esto significa que un sólo programa puede realizar dos o más tareas
simultáneamente. Por ejemplo, un editor de textos puede dar formato a un texto al mismo tiempo
que está imprimiendo, ya que estas dos acciones las realizan dos hilos distintos. Por tanto, la
multitarea basada en procesos actúa sobre “tareas generales”, mientras que la multitarea basada en
hilos gestiona los detalles.
La multitarea basada en hilos requiere un menor costo de operación que la basada en procesos.
Los procesos son tareas más pesadas, es decir requieren más recursos, y necesitan espacio de
direccionamiento propio. La comunicación entre procesos es costosa y limitada. El intercambio de
contextos al pasar de un proceso a otro también es costoso. Los hilos, por otra parte, son tareas
ligeras. Comparten el mismo espacio de direccionamiento y el mismo proceso. Tanto la comunicación
entre hilos como el intercambio de contextos de un hilo al próximo tienen un costo bajo. Los
programas de Java utilizan entornos de multitarea basada en procesos, pero la multitarea basada en
procesos no está bajo el control de Java. Sin embargo, la multitarea basada en hilos sí.
La multitarea basada en hilos permite escribir programas muy eficientes que hacen uso
óptimo del CPU, ya que el tiempo que éste está libre se reduce al mínimo. Esto es especialmente
importante en los entornos interactivos de red en los que Java funciona, donde el tiempo libre del
CPU es común. Por ejemplo, la velocidad de transmisión de datos en la red es mucho más baja que
la velocidad de proceso de la computadora. Incluso la velocidad de lectura y escritura en el sistema
223
www.detodoprogramacion.com
224
Parte I:
El lenguaje Java
local de archivos es mucho más baja que la velocidad de proceso del CPU. Naturalmente, la
velocidad con que el usuario introduce la información, es mucho más baja que la velocidad de la
computadora. En un entorno tradicional de un solo hilo, el programa tiene que esperar a que se
realicen cada una de estas tareas antes de procesar la siguiente, aunque el CPU esté inactivo la
mayor parte del tiempo. La multitarea basada en hilos permite acceder y aprovechar este tiempo
de inactividad del CPU.
Si ya ha programado en sistemas operativos tales como Windows, entonces ya conoce
la programación multihilo. Sin embargo, la forma en que Java maneja los hilos hace que la
programación multihilo sea especialmente simple, ya que muchos de los detalles son
gestionados por Java y no por el programador.
El modelo de hilos en Java
El intérprete de Java depende de los hilos en muchos aspectos, y todas las bibliotecas de
clases se han diseñado teniendo en cuenta el modelo multihilo. De hecho, Java utiliza los
hilos para permitir que todo el entorno sea asíncrono. Esto aumenta la eficacia, impidiendo el
desaprovechamiento de ciclos del CPU.
El valor del entorno multihilo se comprende mejor cuando se compara con el otro modo
de funcionamiento. Los sistemas de un solo hilo utilizan un enfoque denominado ciclo de
evento con sondeo. En este modelo, un solo hilo de control se ejecuta en un ciclo infinito,
sondeando una única cola de eventos para decidir cuál se procesará a continuación. Una vez
que este mecanismo de sondeo obtiene una señal que indica que un archivo de la red está
listo para ser leído, entonces el ciclo de evento selecciona el gestor de control apropiado para
ese evento. Hasta que este gestor regrese el control, nada más puede ocurrir en el sistema,
y esto supone un desaprovechamiento del CPU. Puede ocurrir también que una parte de un
programa domine el sistema e impida que otros eventos sean procesados. En general, en un
entorno de un solo hilo, cuando un hilo bloquea (es decir, suspende la ejecución) porque está
esperando algún recurso, el programa entero se detiene.
La ventaja de la programación multihilo en Java es que se elimina el mecanismo principal
de ciclo/sondeo. Un hilo puede detenerse sin paralizar el resto de las partes del programa. Por
ejemplo, el tiempo de inactividad que se produce cuando un hilo lee datos de la red o espera a
que el usuario introduzca una información, puede ser aprovechado por otro. La programación
multihilo permite que los ciclos de animación paren durante un segundo entre una imagen y la
siguiente sin que todo el sistema se detenga. Cuando en un programa Java, un hilo se bloquea,
solo ese hilo se detiene y todos los demás continúan su ejecución.
Los hilos pueden encontrarse en distintos estados. Un hilo puede estar ejecutándose o
preparado para ejecutarse tan pronto como disponga de tiempo de CPU. Un hilo que está
ejecutándose puede estar suspendido, lo que significa que temporalmente se suspende su
actividad. Un hilo suspendido puede reanudarse, permitiendo que continúe su tarea donde
la dejó. Un hilo puede estar bloqueado cuando está esperando un determinado recurso. En
cualquier instante, un hilo puede detenerse, finalizando su ejecución de forma inmediata. Una
vez detenido, un hilo no puede reanudarse.
Prioridades en hilos
Java asigna a cada hilo una prioridad que determina cómo se debe tratar ese hilo en
comparación con los demás. Las prioridades de los hilos son valores enteros que especifican la
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
• Un hilo puede ceder voluntariamente el control. Esto se hace por abandono explícito, al
quedarse dormido, o bloqueado por una E/S pendiente. Cuando esto ocurre, se examinan
todos los demás hilos, y se le asigna el CPU al que esté preparado para ejecutarse y tenga
la más alta prioridad.
• Un hilo puede ser desalojado por otro con prioridad más alta. En este caso, un hilo de
prioridad más baja que no libera el procesador es simplemente desalojado, sin tener en
cuenta lo que esté haciendo, por otro de prioridad más alta. Básicamente, cuando un hilo
de prioridad más alta desee ejecutarse, lo hará. A esto se denomina multitarea por desalojo.
Una situación un poco más complicada es la que se produce cuando dos hilos de la misma
prioridad compiten por el CPU. En sistemas operativos como Windows, los hilos con la misma
prioridad se reparten automáticamente el tiempo mediante un algoritmo circular (round-robin).
Para otros sistemas operativos, los hilos deben ceder voluntariamente el control a otros de la
misma prioridad. Si no lo hacen así, estos últimos no se ejecutarán.
PRECAUCIÓN Las diferentes formas en que los distintos sistemas operativos realizan la conmutación
de contexto de hilos con la misma prioridad pueden ocasionar problemas de portabilidad.
Sincronización
La programación multihilo introduce un comportamiento asíncrono en los programas, aunque
en ocasiones puede ser necesario el sincronismo. Esto sucede, por ejemplo, si se quiere que dos
hilos se comuniquen y compartan una estructura complicada de datos, como una lista enlazada.
En este caso será necesario asegurar que los dos hilos no entren en conflicto. Esto es, impedir
que uno de los hilos escriba mientras el otro está realizando la lectura. Java implementa para
este propósito un elegante modelo clásico de sincronización entre procesos, el monitor. El
monitor es un mecanismo de control que fue definido por primera vez por C. A. R. Hoare. Se
puede considerar un monitor como una pequeña caja que contiene solamente un hilo. Una vez
que un hilo entra en el monitor, todos los demás hilos deben esperar hasta que el hilo salga del
monitor. De esta forma, un monitor se puede utilizar para proteger el hecho de que varios hilos
manipulen al mismo tiempo un recurso.
La mayor parte de los sistemas multihilo presentan los monitores como objetos que los
programas deben obtener y manipular explícitamente. Java proporciona una solución más clara.
No existe la clase “Monitor”; en su lugar, cada objeto tiene su propio monitor implícito que se
introduce automáticamente cuando se llama a uno de los métodos sincronizados del objeto.
Cuando un hilo está dentro de un método sincronizado, ningún otro hilo puede llamar a ningún
otro método sincronizado del mismo objeto. Esto permitirá escribir un código multihilo muy
claro y conciso, debido a que el soporte para la sincronización se encuentra establecido dentro
del lenguaje.
www.detodoprogramacion.com
PARTE I
prioridad relativa de un hilo sobre otro. Como valor absoluto, una prioridad no tiene sentido
alguno; un hilo de prioridad más alta no se ejecuta más rápidamente que otro de prioridad más
baja si es el único hilo que se está ejecutando. La prioridad de un hilo se utiliza para decidir
cuándo se debe pasar de la ejecución de un hilo a la del siguiente. A esto se denomina cambio
de contexto. Las reglas que determinan cuándo debe tener lugar un cambio de contexto son muy
sencillas:
225
226
Parte I:
El lenguaje Java
Intercambio de mensajes
Después de dividir el programa en distintos hilos, es necesario definir cómo se comunicarán
entre sí. Cuando se programa en otros lenguajes, existe una dependencia del sistema
operativo para establecer la comunicación entre hilos, y esto, evidentemente, añade
mayor costo de ejecución. En contraste, Java proporciona una forma limpia y de bajo costo
que permite la comunicación entre dos o más hilos, por medio de llamadas a métodos
predefinidos que tienen todos los objetos. El sistema de mensajes de Java permite que un hilo
entre en un método sincronizado de un objeto, y espere ahí hasta que otro hilo le notifique
explícitamente que debe salir.
La clase Thread y la interfaz Runnable
El sistema multihilo de Java está construido en torno a la clase Thread, sus métodos, y su
correspondiente interfaz, Runnable. La clase Thread encapsula a un hilo de ejecución. Debido
a que no se puede hacer referencia directamente al estado de un hilo en ejecución, es necesario
utilizar una instancia de la clase Thread que represente a dicho hilo. Para crear un nuevo hilo, el
programa deberá extender la clase Thread o bien implementar la interfaz Runnable.
La clase Thread define varios métodos que ayudan a la gestión de los hilos. A continuación,
se muestran los métodos que se utilizarán en este capítulo:
Método
Significado
getName
Obtiene el nombre de un hilo.
getPriority
Obtiene la prioridad de un hilo.
isAlive
Determina si un hilo todavía se está ejecutando.
join
Espera la terminación de un hilo.
run
Punto de entrada de un hilo.
sleep
Suspende un hilo durante un periodo de tiempo.
start
Comienza un hilo llamando a su método run.
Hasta el momento, todos los ejemplos de este libro han utilizado un solo hilo de ejecución.
El resto del capítulo explica cómo funcionan la clase Thread y la interfaz Runnable para crear y
gestionar hilos, comenzando con el hilo que tienen todos los programas de Java: el hilo principal.
El hilo principal
Cuando un programa Java comienza su ejecución, hay un hilo ejecutándose inmediatamente.
Este hilo se denomina normalmente hilo principal del programa, porque es el único que se ejecuta
al comenzar el programa. El hilo principal es importante por dos razones:
• Es el hilo a partir del cual se crean el resto de los hilos del programa.
• Normalmente, debe ser el último que finaliza su ejecución debido a que es el responsable
de realizar diversas acciones de cierre.
Aunque el hilo principal se crea automáticamente cuando el programa comienza, se puede
controlar a través de un objeto Thread. Para ello, se debe obtener una referencia al mismo
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
static Thread currentThread( )
Este método devuelve una referencia al hilo desde donde fue llamado. Una vez obtenida la
referencia del hilo principal, se le puede controlar del mismo modo que a cualquier otro hilo.
Comencemos revisando el siguiente ejemplo:
// Control del hilo principal.
class DemoHiloActual {
public static void main (String args[]) {
Thread t = Thread.currentThread();
System.out.println ("Hilo actual: " + t);
// Cambio del nombre del hilo
t.setName ("Mi Hilo");
System.out.println ("Después del cambio de nombre: " + t);
try {
for (int n = 5; n > 0; n--) {
System.out.println (n);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal");
}
}
}
En este programa, se obtiene una referencia al hilo actual (el hilo principal en este caso)
llamando al método currentThread( ), dicha referencia es almacenada en la variable local t.
A continuación, el programa visualiza la información referente al hilo. Entonces, el programa
llama al método setName( ) para cambiar el nombre interno del hilo, y se visualiza de nuevo la
información referente al hilo.
Luego, un ciclo cuenta de forma descendente desde cinco, con una pausa de un segundo
entre cada línea. Esta pausa se obtiene utilizando el método sleep( ). El argumento del
método sleep( ) especifica el retraso en milisegundos. Observe el bloque try /catch en
el que se encuentra el ciclo. El método sleep( ) de Thread podría lanzar una excepción
InterruptedException. Esto es lo que ocurriría si algún otro hilo intentase interrumpir a
éste mientras está dormido. En el ejemplo, si se le interrumpe se imprime un mensaje. En un
programa real, sería necesario resolver la situación de una forma distinta. La salida generada por
el programa es:
Hilo actual: Thread[main,5,main]
Después del cambio de nombre: Thread[Mi Hilo,5,main]
5
4
3
2
1
www.detodoprogramacion.com
PARTE I
llamando al método currentThread( ), que es un miembro public static de la clase Thread. Su
forma general es la siguiente:
227
228
Parte I:
El lenguaje Java
Observe la salida que se produce cuando se usa t como argumento de println( ). Aparecen en
orden: el nombre del hilo, su prioridad y el nombre de su grupo. Por omisión, el nombre del
hilo principal es main. Su prioridad es 5, que es el valor por omisión, y main es también el
nombre del grupo de hilos a los que pertenece este hilo. Un grupo de hilos es una estructura de
datos que controla el estado de una colección de hilos como un todo. Después de cambiar el
nombre del hilo, se vuelve a presentar t. Esta vez se imprime el nuevo nombre del hilo.
Analicemos con más detenimiento los métodos definidos por Thread que son utilizados
en el programa. El método sleep( ) hace que se suspenda la ejecución del hilo desde el que fue
llamado durante un periodo de tiempo especificado en milisegundos. Su forma general es la
siguiente:
static void sleep (long milisegundos) throws InterruptedException
El número de milisegundos que se suspende la ejecución se especifica en la variable
milisegundos. Este método puede lanzar una excepción InterruptedException.
El método sleep( ) tiene una segunda forma, que se muestra a continuación, y que permite
especificar el periodo de tiempo en términos de milisegundos y nanosegundos:
static void sleep (long milisegundos, int nanosegundos) throws InterruptedException
Esta segunda forma es útil en entornos que permitan periodos de tiempo tan cortos como el
nanosegundo.
Como se ha visto en el programa anterior, se puede asignar un nombre a un hilo con el
método setName( ). También se puede obtener el nombre de un hilo llamando al método
getName( ) (este procedimiento no se muestra en el programa). Estos métodos son miembros
de la clase Thread y se declaran de la siguiente forma:
final void setName (String nombreHilo)
final String getName( )
Donde nombreHilo especifica el nombre del hilo.
Creación de un hilo
En un sentido amplio, se puede crear un hilo creando un objeto del tipo Thread. Java define dos
formas en la que se puede hacer esto:
• Implementando la interfaz Runnable.
• Extendiendo la propia clase Thread.
Los dos siguientes apartados analizan cada una de estas opciones.
Implementación de la interfaz Runnable
La forma más fácil de crear un hilo es crear una clase que implemente la interfaz Runnable. Esta
interfaz permite abstraer el concepto de una unidad de código ejecutable. Se puede construir un
hilo sobre cualquier objeto que implemente la interfaz Runnable. Para ello, una clase necesita
implementar un único método llamado run( ), que se declara de la siguiente forma:
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
229
public void run( )
Thread (Runnable objetoHilo, String nombreHilo)
En este constructor, objetoHilo es una instancia de una clase que implementa la interfaz
Runnable y define el punto en el que comenzará la ejecución del hilo. La variable nombreHilo
indica el nombre del nuevo hilo.
El nuevo hilo que se acaba de crear no comenzará su ejecución hasta que se llame al método
start( ), declarado dentro de Thread. Esencialmente, start( ) ejecuta una llamada a run( ). A
continuación se muestra el método start( ):
void start( )
En el ejemplo siguiente se crea un nuevo hilo y se inicia su ejecución:
// Creación de un segundo hilo.
class NewThread implements Runnable {
Thread t;
NewThread () {
//Crea el segundo hilo
t = new Thread (this, "Hilo demo");
System.out.println ("Hilo hijo: " + t);
t.start(); // Comienzo del hilo
}
// Este es el punto de entrada para el segundo hilo.
public void run() {
try {
for (int i = 5; i > 0; i--) {
System.out.println ("Hilo hijo: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo hijo. ");
}
System.out.println ("Salida del hilo hijo.");
}
}
class DemoHilo {
public static void main (String args[]) {
new NewThread (); // creación de un nuevo hilo
www.detodoprogramacion.com
PARTE I
Dentro del método run( ), se definirá el código que constituye el nuevo hilo. Es importante
entender que run( ) puede llamar a otros métodos, usar otras clases y declarar variables de la
misma forma que el hilo principal. La única diferencia es que el método run( ) establece el punto
de entrada para otro hilo de ejecución concurrente dentro del programa. Este hilo finalizará
cuando el método run( ) devuelva el control.
Después de crear una clase que implemente la interfaz Runnable, se creará un objeto del
tipo Thread dentro de esa clase. Thread define varios constructores. A continuación se presenta
el que se usa en este ejemplo:
230
Parte I:
El lenguaje Java
try {
for (int i = 5; i > 0; i--) {
System.out.println ("Hilo principal: " + i);
Thread.sleep(l000);
}
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal.");
}
System.out.println ("Salida del hilo principal.");
}
}
La siguiente sentencia dentro del constructor de NewThread sirve para crear objeto
Thread.
t = new Thread (this, "Hilo demo");
Pasando this como primer argumento, se indica que el nuevo hilo llame al método run( )en este
objeto. A continuación, se llama al método start( ) para que comience la ejecución del hilo con el
método run( ). Esto hace que comience el ciclo for del hilo hijo. Después de llamar a start( ), el
constructor de NewThread finaliza volviendo a main( ). El hilo principal se reanuda, entrando
en el ciclo for. A partir de ahí, ambos hilos continúan su ejecución, compartiendo el CPU, hasta
que sus respectivos ciclos terminan. La salida que se obtiene al ejecutar este programa es la
siguiente (la salida puede variar dependiendo de la velocidad del procesador y la carga de tareas):
Hilo hijo: Thread [Hilo demo, 5, main]
Hilo principal: 5
Hilo hijo: 5
Hilo hijo: 4
Hilo principal: 4
Hilo hijo: 3
Hilo hijo: 2
Hilo principal: 3
Hilo hijo: 1
Salida del hilo hijo.
Hilo principal: 2
Hilo principal: 1
Salida del hilo principal.
Como ya se ha dicho antes, en un programa multihilo, muchas veces el hilo principal debe
ser el último que finalice su ejecución. De hecho, con algunos intérpretes de Java antiguos, si el
hilo principal finaliza antes de que algún hilo hijo haya completado su ejecución, entonces
el intérprete Java puede “bloquearse”. El programa anterior asegura que el hilo principal es el
último que finaliza su ejecución, ya que el hilo principal se suspende durante 1000 milisegundos
entre cada iteración, mientras que el hilo hijo lo hace solamente durante 500 milisegundos, esto
hace que el hilo hijo finalice antes que el hilo principal. En breve veremos una mejor forma de
esperar a que un hilo termine.
Extensión de la clase Thread
La segunda forma de crear un hilo es crear una nueva clase que extienda la clase Thread, y crear
entonces una instancia de esa clase. La nueva clase debe sobrescribir el método run( ), que es el
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
// Creación de un segundo hilo extendiendo la clase Thread
class NewThread extends Thread {
NewThread() {
// Creación de un nuevo hilo
super ("Hilo demo");
Systern.out.println ("Hilo hijo: " + this);
start();
// Comienza el hilo
}
// Este es el punto de entrada para el segundo hilo.
public void run () {
try {
for (int i = 5; i > 0; i--) {
Systern.out.println ("Hilo hijo: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
Systern.out.println ("Interrupción del hilo hijo.");
}
System.out.println ("Salida del hilo hijo.");
}
}
class ExtendThread {
public static void main (String args[]) {
new NewThread(); // Creación de un nuevo hilo
try {
for (int i = 5; i > 0; i--) {
Systern.out.println ("Hilo principal: " + i);
Thread.sleep(l000);
}
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal.");
}
System.out.println ("Salida del hilo principal");
}
}
Este programa genera la misma salida que la versión anterior. Como se puede ver, el hilo hijo se
crea a través de una instancia de la clase NewThread, que es una clase derivada de Thread.
Observe que mediante la llamada a super( ) dentro de NewThread, se invoca la siguiente
forma del constructor de Thread:
public Thread (String nombreHilo)
donde la variable nombreHilo especifica el nombre del hilo.
www.detodoprogramacion.com
PARTE I
punto de entrada para el nuevo hilo. También debe llamar al método start( ) para comenzar la
ejecución del nuevo hilo. A continuación se presenta el programa anterior, realizando esta vez
una extensión de la clase Thread:
231
232
Parte I:
El lenguaje Java
Elección de una de las dos opciones
En este momento nos podemos preguntar por qué Java permite crear un hilo hijo de dos formas
distintas y cuál de las dos es mejor. Las respuestas a estas preguntas nos llevan al mismo punto.
La clase Thread define varios métodos que pueden ser, sobrescritos por una clase derivada. De
estos métodos, el único que debe ser sobrescrito es el método run( ), que es, naturalmente el
mismo método que se requiere implementar la interfaz Runnable. Muchos programadores de
Java opinan que las clases sólo se deben extender cuando van a ser mejoradas o modificadas
de alguna forma. Así, si no se va a sobrescribir ningún otro método de la clase Thread,
probablemente sea mejor implementar la interfaz Runnable. Evidentemente esto depende del
programador. Sin embargo, en el resto de este capítulo, se crearán hilos utilizando clases que
implementen Runnable.
Creación de múltiples hilos
Hasta ahora sólo se han usado dos hilos: el hilo principal y un hilo hijo. Sin embargo nuestros
programas pueden generar tantos hilos como se requiera. El siguiente programa crea tres hilos
hijo:
// Creación de múltiples hilos.
class NewThread implements Runnable {
String name; // nombre del hilo
Thread t;
NewThread (String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println ("Nuevo hilo: " + t);
t.start(); // Comienza el hilo
}
// Este es el punto de entrada del hilo.
public void run() {
try {
for (int i = 5; i > 0; i-) {
System.out.println (name + ":" + i);
Thread.sleep(l000);
}
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo" + name);
}
System.out.println (" Salida del hilo" + name);
}
}
class MultiThreadDemo {
public static void main (String args[]) {
new NewThread ("Uno"); // comienzo de los hilos
new NewThread ("Dos");
new NewThread ("Tres") ;
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
}
}
La salida de este programa es:
Nuevo hilo: Thread [Uno, 5, main]
Nuevo hilo: Thread [Dos, 5, main]
Nuevo hilo: Thread [Tres, 5, main]
Uno: 5
Dos: 5
Tres: 5
Uno: 4
Dos: 4
Tres: 4
Uno: 3
Tres: 3
Dos: 3
Uno: 2
Tres: 2
Dos: 2
Uno: 1
Tres: 1
Dos: 1
Salida del hilo Uno.
Salida del hilo Dos.
Salida del hilo Tres.
Salida del hilo principal.
Como se puede ver, una vez que los tres hilos han comenzado, comparten el CPU. Observe la
llamada a sleep(10000) en main( ), que hace que el hilo principal quede suspendido durante 10
segundos, y así se asegura que finalizará al último.
Uso de isAlive( ) y join( )
Como ya se ha mencionado, a menudo se requiere que el hilo principal sea el último en
terminar. En los programas anteriores, esto se consiguió utilizando el método sleep( ) dentro
de main( ), con un retraso suficiente para asegurar que todos los hilos hijos terminarán antes
que el hilo principal. Sin embargo, esta solución no es muy satisfactoria, y, además, sugiere una
pregunta: ¿Cómo puede un hilo saber si otro ha terminado? Afortunadamente, la clase Thread
facilita la respuesta a esta pregunta.
Existen dos formas de determinar si un hilo ha terminado. La primera consiste en llamar al
método isAlive( ) en el hilo. Éste es un método definido por la clase Thread, y su forma general
es la siguiente:
www.detodoprogramacion.com
PARTE I
try {
// Espera a que los otros hilos terminen
Thread.sleep(l0000);
} catch (InterruptedException e) {
System.out.println("Interrupción del hilo principal");
}
System.out.println ("Salida del hilo principal");
233
234
Parte I:
El lenguaje Java
final boolean isAlive( )
El método isAlive( ) devuelve el valor true si el hilo al que se hace referencia todavía está
ejecutándose, y devuelve el valor false en caso contrario.
El método isAlive( ) es útil en ocasiones; sin embargo, el método, que se utiliza
habitualmente para esperar a que un hilo termine es el método join( ). Su forma general es:
final void join( ) throws InterruptedException
Este método espera hasta que termine el hilo sobre el que se realizó la llamada. Su nombre surge
de la idea de que el hilo llamante espere hasta que el hilo especificado se reúna con él. Otras
formas de join( ) permiten especificar un tiempo máximo de espera para que termine el hilo
especificado.
A continuación se presenta una versión mejorada del ejemplo anterior que utiliza al método
join( ) para asegurar que el hilo principal es el último en terminar. También sirve como ejemplo
del método isAlive( ).
// Uso de join() para esperar a que los hilos terminen.
class NewThread implements Runnable {
String name; // nombre del hilo
Thread t;
NewThread (String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println ("Nuevo hilo: " + t);
t.start(); // comienzo del hilo
}
// Este es el punto de entrada del hilo.
public void run() {
try {
for (int i = 5; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(l000);
}
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo" + name);
}
System.out.println("Salida del hilo" + name);
}
}
class DemoJoin {
public static void main (String args[]) {
NewThread obl = new NewThread ("Uno");
NewThread ob2 = new NewThread ("Dos");
NewThread ob3 = new NewThread ("Tres");
System.out.println ("El hilo Uno está vivo: "
+ obl.t.isAlive());
System.out.println ("El hilo Dos está vivo: "
+ ob2.t.isAlive());
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
System.out.println ("El hilo Uno está vivo: "
+ obl.t.isAlive());
System.out.println("El hilo Dos está vivo: "
+ ob2.t.isAlive());
System.out.println("El hilo Tres está vivo: "
+ ob3.t.isAlive());
System.out.println("Salida del hilo principal.");
}
}
La salida de este programa es la siguiente (la salida puede variar dependiendo de la velocidad
del procesador y la carga de tareas):
Nuevo hilo: Thread[Uno,5,main]
Nuevo hilo: Thread[Dos,5,main]
Nuevo hilo: Thread[Tres,5,main]
El hilo Uno está vivo: true
El hilo Dos está vivo: true
El hilo Tres está vivo: true
Espera la finalización de los otros hilos.
Uno: 5
Dos: 5
Tres: 5
Uno: 4
Dos: 4
Tres: 4
Uno: 3
Dos: 3
Tres: 3
Uno: 2
Dos: 2
Tres: 2
Uno: 1
Dos: 1
Tres: 1
Salida del hilo Dos.
Salida del hilo Tres.
Salida del hilo Uno.
Hilo Uno está vivo: false
Hilo Dos está vivo: false
Hilo Tres está vivo: false
Salida del hilo principal.
www.detodoprogramacion.com
PARTE I
System.out.println ("El hilo Tres está vivo: "
+ ob3.t.isAlive());
//Espera a que los otros hilos terminen
try {
System.out.println ("Espera la finalización de los otros hilos.");
obl.t.join ();
ob2.t.join();
ob3.t.join() ;
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal");
}
235
236
Parte I:
El lenguaje Java
Como se puede ver, cuando finaliza la llamada al método join( ), los hilos han finalizado su
ejecución.
Prioridades de los Hilos
El planificador de hilos utiliza las prioridades de los hilos para decidir cuándo se debe permitir
la ejecución de cada hilo. En teoría, los hilos de prioridad más alta disponen de más tiempo del
CPU que los de prioridad más baja. En la práctica, el tiempo del CPU del que dispone un hilo,
depende de varios factores además de su prioridad, (por ejemplo la forma en que el sistema
operativo implementa la multitarea puede afectar la disponibilidad relativa de tiempo de CPU).
Un hilo de prioridad alta puede desalojar a uno de prioridad más baja. Por ejemplo, cuando un
hilo de prioridad más baja se está ejecutando y otro de prioridad más alta reanuda su ejecución
(después de estar suspendido o esperando una E/S, por ejemplo), este segundo desalojará al de
prioridad más baja.
En teoría hilos de la misma prioridad deben tener el mismo acceso al CPU, pero puede no
ser exactamente así. Recuerde que Java está diseñado para funcionar en una amplia gama de
entornos. Algunos de estos entornos implementan la multitarea de forma fundamentalmente
diferente a otros. Por seguridad, los hilos que comparten la misma prioridad, deberían ceder el
control de vez en cuando. Esto asegura que todos los hilos tienen la oportunidad de ejecutarse
bajo un sistema operativo con multitarea no apropiativa. En la práctica, incluso en entornos
no apropiativos, la mayoría de los hilos tienen la oportunidad de ejecutarse, ya que la mayoría
de los hilos se encuentran en algún instante en una situación de bloqueo, como por ejemplo
una operación de E/S. Cuando esto ocurre, el hilo que está bloqueado se suspende, y otros
hilos pueden ejecutarse. Pero es mejor no confiar en esto si realmente se desea una ejecución
multihilo libre de irregularidades. También hay que tener en cuenta que algunos tipos de tareas
hacen un uso intensivo del CPU. Los hilos correspondientes a estas tareas dominan el CPU, y
conviene que cedan el control ocasionalmente para que los otros hilos puedan ser ejecutados.
Para establecer la prioridad de un hilo se utiliza el método setPriority( ), que es miembro de
la clase Thread. Su forma general es:
final void setPriority (int nivel)
Donde nivel especifica la nueva prioridad del hilo. El valor de nivel debe estar comprendido
en el rango MIN_PRIORITY y MAX_PRIORITY. Actualmente, estos valores son 1 y 10,
respectivamente. Para volver a establecer la prioridad por omisión de un hilo se utiliza el valor
NORM_PRIORITY, que actualmente es 5. Estas prioridades están definidas como variables
final en la clase Thread.
Para obtener la prioridad de un hilo se utiliza el método getPriority( ) de Thread, como se
indica a continuación:
final int getPriority( )
Las implementaciones de Java pueden presentar un comportamiento muy diferente en
lo que a la planificación respecta. Las versiones Windows XP/98/NT/2000 funcionan, más o
menos, como se podría esperar. Sin embargo, otras versiones pueden funcionar de manera
absolutamente diferente. Muchas de las contradicciones surgen cuando hay hilos que adoptan
un comportamiento apropiativo, en lugar de liberar el tiempo del CPU de forma cooperativa. La
mejor forma para obtener un comportamiento predecible en distintas plataformas con Java es
utilizar hilos que cedan el control de la CPU voluntariamente.
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
// Ejemplo de prioridades de los hilos.
class Clicker implements Runnable {
long click = 0;
Thread t;
private volatile boolean running = true;
public clicker(int p) {
t = new Thread(this);
t.setPriority(p);
}
public void run () {
while (running) {
click++;
}
}
public void stop () {
running = false;
}
public void start () {
t.start();
}
}
class HiLoPri {
public static void main (String args []) {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY) ;
clicker hi = new clicker(Thread.NORM_PRIORITY + 2);
clicker lo = new clicker(Thread.NORM_PRIORITY - 2);
lo.start ();
hi.start ();
try {
Thread.sleep(l0000) ;
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal.");
}
lo.stop();
hi.stop();
// Espera a que terminen los hilos hijos.
try {
hi.t.join();
lo.t.join();
} catch (InterruptedException e) {
System.out.println ("Captura de la excepción InterruptedException");
}
www.detodoprogramacion.com
PARTE I
El siguiente ejemplo presenta dos hilos con distintas prioridades, que no se ejecutan de
igual manera en plataformas apropiativas y no apropiativas. Un hilo tiene una prioridad dos
niveles por encima de la prioridad normal, definida por Thread.NORM_PRIORITY, y el otro,
dos niveles por debajo de la normal. Los dos hilos comienzan, y se permite su ejecución durante
diez segundos. Cada hilo ejecuta un ciclo, contando el número de iteraciones. Después de diez
segundos, el hilo principal detiene a ambos hilos y se visualiza el número de veces que cada hilo
recorrió su ciclo.
237
238
Parte I:
El lenguaje Java
System.out.println ("Hilo de prioridad baja: " + lo.click);
System.out.println ("Hilo de prioridad alta: " + hi.click);
}
}
Cuando este programa se ejecuta bajo Windows, la salida que se muestra a continuación,
indica que los hilos realizaron el cambio de contexto, aunque ninguno de los dos cedió
voluntariamente el CPU ni estuvo bloqueado por operaciones de E/S. El hilo de prioridad más
alta dispuso, aproximadamente, del 90 por ciento del tiempo de CPU.
Hilo de prioridad baja: 4408112
Hilo de prioridad alta: 589626904
Obviamente, la salida exacta producida por este programa depende de la velocidad del CPU, y
del número de otras tareas que se están ejecutando en el sistema. Cuando este mismo programa
se ejecuta en un sistema no apropiativo, se obtienen resultados diferentes.
Otra cuestión de interés en el programa anterior es la siguiente: la variable running va
precedido de la palabra clave volatile. Aunque la palabra clave volatile se analizará con más
detalle en el capítulo 13, se utiliza aquí para asegurar que el valor de running será examinado
cada vez que se recorra el siguiente ciclo:
while (running) {
click++;
}
Sin la utilización de la palabra clave volatile, Java podría optimizar el ciclo de tal forma que el
valor de running se guardaría en una copia local. El uso de volatile impide esta optimización,
indicando a Java que el valor de running puede cambiar de una manera no directamente
evidente en el código inmediato.
Sincronización
Cuando dos o más hilos tienen que acceder a un recurso compartido, es necesario asegurar de
alguna manera que sólo uno de ellos accede a ese recurso en cada instante. El proceso mediante
el que se consigue esto se denomina sincronización. Como se verá, Java proporciona un soporte
único, en cuanto a lenguaje, para la sincronización.
La clave para la sincronización es el concepto de monitor, también llamado semáforo. Un
monitor es un objeto que se utiliza como un candado mutuamente exclusivo, o mutex. Sólo uno
de los hilos puede poseer un monitor en un determinado instante. Cuando un hilo adquiere un
candado, se dice que ha entrado en el monitor. Todos los demás hilos que intenten acceder al
monitor bloqueado serán suspendidos hasta que el primero salga del monitor. Se dice que estos
otros hilos están esperando al monitor. Un hilo que posea un monitor puede volver a entrar en el
mismo monitor si así lo desea.
Si ha trabajado con sincronización al utilizar otros lenguajes, como C y C++, sabrá que
puede resultar un tanto compleja. Esto se debe a que la mayoría de lenguajes no implementan
la sincronización, sino que, para la sincronización de hilos, utilizan funciones primitivas del
sistema operativo. Afortunadamente, Java implementa la sincronización mediante elementos del
lenguaje, con lo que la mayor parte de la complejidad asociada a la misma ha sido eliminada.
Un código se puede sincronizar de dos formas. Ambas implican el uso de la palabra clave
synchronized, y se analizan a continuación.
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
239
Métodos sincronizados
// Este programa no está sincronizado.
class Callme {
void call (String msg) {
System.out.print ("[" + msg);
try {
Thread.sleep (l000);
} catch (InterruptedException e) {
System.out.println ("Interrumpido");
}
System.out.println ("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller (Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start() ;
}
public void run() {
target.call(msg);
}
}
www.detodoprogramacion.com
PARTE I
La sincronización resulta sencilla en Java, porque todos los objetos tienen su propio
monitor implícito asociado. Para entrar en el monitor de un objeto, basta con llamar a
un método modificado con la palabra clave synchronized. Mientras un hilo esté dentro
de un método sincronizado, todos los demás hilos que traten de llamar a ese método, o a otro
método sincronizado sobre la misma instancia, tendrán que esperar. Para salir del monitor y
abandonar el control del objeto, el propietario del monitor sólo tiene que salir
del método sincronizado.
Para entender mejor que la sincronización es necesaria, comencemos con un ejemplo
sencillo que no la usa, pero debería. El siguiente programa tiene tres clases. La primera, Callme,
tiene un sólo método llamado call( ). El método call( ) tiene un parámetro del tipo String
denominado msg. Este método intenta imprimir la cadena msg entre corchetes. La cuestión
de interés es que, después de que el método call( ) imprime el corchete de apertura y la cadena
msg, se llama a Thread.sleep(l000), lo que detiene el hilo en curso durante un segundo.
El constructor de la siguiente clase, Caller, toma una referencia a una instancia de la clase
Callme y un String, los cuales se almacenan en las variables target y msg respectivamente.
El constructor también crea un nuevo hilo que llamará al método run de este objeto. El hilo es
iniciado inmediatamente. El método run( ) de Caller llama al método call( ) de la instancia
target de Callme, pasando la cadena msg. Finalmente, la clase Synch comienza creando una
instancia de Callme, y tres instancias de Caller, cada una con una cadena diferente. La misma
instancia de Callme se pasa a cada instancia de Caller.
240
Parte I:
El lenguaje Java
class Synch {
public static void main (String args[]) {
Callme target = new Callme();
Caller obl = new Caller (target, "Hola");
Caller ob2 = new Caller (target, "Sincronizado");
Caller ob3 = new Caller (target, "Mundo".) ;
// espera a que terminen los hilos
try {
obl.t.join();
ob2.t.join();
ob3.t.join();
} catch (InterruptedException e) {
System.out.println ("Interrumpido");
}
}
}
La salida producida por este programa es la siguiente:
[Hola [Sincronizado [Mundo]
]
]
Al llamar a sleep( ), el método call( ) permite cambiar la ejecución a otro hilo. El resultado
es una salida en la que se mezclan los tres mensajes de forma confusa. En este programa no
hay nada que impida a los tres hilos llamar al mismo método, en el mismo objeto y al mismo
tiempo. Esto es lo que se conoce como una condición de carrera (race condition), ya que los tres
métodos compiten uno con otro para completar el método. Este ejemplo utiliza sleep( ) para que
los efectos sean repetibles y obvios. En la mayoría de las situaciones, una condición de carrera
es más sutil y menos predecible, porque no se puede tener seguridad de cuándo se produce el
cambio de contexto. Esto puede dar lugar a que un programa se ejecute de forma correcta unas
veces e incorrecta otras.
Para corregir el programa anterior, se debe producir un acceso en serie al método call( ), es
decir, se debe restringir el acceso a un único hilo en cada instante. Para ello, simplemente hay
que colocar por delante de la definición del método call( ) la palabra clave synchronized, tal y
como se muestra a continuación:
class Callme {
synchronized void call (String msg) {
...
Esto impide que otros hilos accedan al método call( ) mientras un determinado hilo lo está
utilizando. Después de añadir la palabra synchronized al método call( ), la salida del programa
es la siguiente:
[Hola]
[Sincronizado]
[Mundo]
Siempre que se tenga un método, o un grupo de métodos, que manipulan el estado interno
de un objeto en una situación de múltiples hilos, se debe usar la palabra clave synchronized
para salvaguardar dicho estado de las condiciones de carrera. Recuerde que una vez que un hilo
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
La sentencia synchronized
La creación de métodos sincronizados en clases creadas por el programador es una forma fácil
y efectiva de conseguir la sincronización; sin embargo, no funciona con todas las clases. Veamos
por qué. Suponga que quiere sincronizar el acceso a objetos de una clase que no fue diseñada
para el acceso de múltiples hilos, es decir, la clase no utiliza métodos sincronizados. Además,
la clase fue creada por otros programadores, y no tiene acceso al código fuente. Por lo tanto,
no puede añadir la palabra clave synchronized a los métodos necesarios. ¿Cómo se puede
conseguir que el acceso a un objeto de esa clase sea sincronizado? Afortunadamente, la solución
es fácil. Simplemente hay que poner llamadas a los métodos definidos por esa clase dentro de un
bloque sincronizado.
Ésta es la forma general de la sentencia synchronized:
synchronized (objeto) {
// sentencias que deben ser sincronizadas
}
donde objeto es una referencia al objeto que se quiere sincronizar. Si se quiere sincronizar
una única sentencia, no son necesarias las llaves. Un bloque sincronizado asegura que sólo
se producirá una llamada a un método miembro de objeto después de que el hilo actual haya
entrado en el monitor del objeto.
La siguiente es una versión alternativa del ejemplo anterior, que utiliza un bloque
sincronizado dentro del método run( ):
// Este programa utiliza un bloque sincronizado.
class Callme {
void call (String msg) {
System.out.print ("[" + msg);
try {
Thread.sleep (l000);
} catch (InterruptedException e) {
System.out.println ("Interrumpido");
}
System.out.println ("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller (Callme targ, String s) {
target = targ;
msg = s;
t = new Thread (this);
t.start();
}
www.detodoprogramacion.com
PARTE I
entra en un método sincronizado de una instancia, ningún otro hilo puede entrar en ningún
otro método sincronizado de la misma instancia. Sin embargo, sí se podrá llamar a métodos no
sincronizados de la misma instancia.
241
242
Parte I:
El lenguaje Java
// Sincronización de las llamadas a call()
public void run() {
synchronized (target) { // Bloque sincronizado
target.call (msg);
}
}
}
class Synchl {
public static void main (String args[]) {
Callme target = new Callme();
Caller obl = new Caller (target, "Hola") ;
Caller ob2 = new Caller (target, "Sincronizado");
Caller ob3 = new Caller (target, "Mundo");
// Espera a que los hilos terminen
try {
obl.t.join();
ob2.t.join();
ob3.t.joinO;
} catch(InterruptedException e) {
System.out.println ("Interrumpido");
}
}
}
Aquí no se ha modificado con la palabra synchronized al método call( ). En su lugar, se utiliza
la sentencia synchronized dentro del método run( ) de Caller. La salida que se obtiene es la
misma que en el ejemplo anterior, ya que cada hilo espera a que el anterior termine antes de
proceder.
Comunicación entre hilos
En los ejemplos anteriores se bloqueaba el acceso asíncrono a ciertos métodos para los demás
hilos. Este uso de los monitores implícitos de los objetos en Java es bastante eficaz, pero se
puede conseguir un nivel más refinado de control mediante la comunicación entre procesos, la
cual es especialmente simple en Java.
Como se ha explicado anteriormente, la programación multihilo sustituye la programación
basada en ciclos de eventos, al dividir las tareas en unidades discretas y lógicas. Los hilos
tienen, además, una segunda ventaja: permiten eliminar el sondeo, que es un mecanismo
mediante el cual se comprueba de forma repetitiva si se cumple una condición. Cuando dicha
condición se cumple, se ejecuta una determinada acción. Esto supone un desaprovechamiento
del CPU.
Consideremos, por ejemplo, el problema clásico de colas, en que un hilo está produciendo
unos datos y otro los está consumiendo. Para hacer el problema más interesante, consideremos,
además, que el hilo productor tiene que esperar hasta que el hilo consumidor termine, antes de
generar más datos.
En un sistema con sondeo, el hilo consumidor desperdiciaría muchos ciclos de CPU
esperando la producción de datos. Una vez que el productor hubiera finalizado, comenzaría el
sondeo, desaprovechándose más ciclos de CPU hasta que el hilo consumidor terminara, etc. Esta
situación evidentemente no es deseable.
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
• wait( ) indica al hilo que realiza la llamada que debe abandonar el monitor y quedar
suspendido hasta que algún otro hilo entre en el mismo monitor y llame al método
notify( ).
• notify( ) activa un hilo que previamente llamó a wait( ) en el mismo objeto.
• notifyAll( ) activa todos los hilos que llamaron previamente a wait( ) en el mismo objeto.
Uno de esos hilos comenzará a ejecutarse.
Estos métodos se declaran dentro de la clase Object, tal y como se muestra a continuación:
final void wait( ) throws InterruptedException
final void notify( )
final void notifyAll( )
Existen formas adicionales de wait( ) que permiten especificar un determinado periodo de espera.
Antes de pasar a un ejemplo que ilustre la comunicación entre los hilos, es importante
tratar otro aspecto. Aunque wait( ) normalmente espera hasta que notify( ) o notifyAll( ) sea
llamado, existe una posibilidad que en casos muy raros el hilo en espera pueda ser despertado
debido a una falsa alarma. En este caso, un hilo de espera puede reiniciar sin que notify( )
o notifyAll( ) hayan sido llamadas, en esencia el hilo reinicia sin razón aparente. Dada esta
remota posibilidad, la empresa SUN (creadora de Java) recomienda que las llamadas a wait( )
se realicen dentro de un ciclo que compruebe la condición de los hilos que están esperando. El
siguiente ejemplo muestra esta técnica.
Veremos a continuación un ejemplo que usa wait( ) y notify( ). Para comenzar considere
el siguiente ejemplo de programa que implementa de manera incorrecta una forma sencilla
del problema de productor/consumidor. El ejemplo consiste en cuatro clases: Q, la cola que se
intenta sincronizar; Producer, el objeto hilo que genera los datos para la cola; Consumer, el hilo
objeto que consume los datos de la cola, y PC, la mini clase que crea las clases Q, Producer y
Consumer.
// Una implementación incorrecta del problema de productor / consumidor.
class Q {
int n;
synchronized int get() {
System.out.println ("Consume: " + n);
return n;
}
synchronized void put (int n) {
this.n = n;
System.out.println ("Produce: " + n);
}
}
www.detodoprogramacion.com
PARTE I
Para evitar el sondeo, Java aporta un elegante mecanismo de comunicación entre
procesos por medio de los métodos wait( ), notify( ) y notifyAll( ). Estos métodos se han
implementado como métodos final en la clase Object, de forma que están incluidos en todas
las clases automáticamente. Sólo se puede llamar a estos tres métodos desde dentro de un
método sincronizado. Las reglas de uso de estos tres métodos son bastante sencillas, aunque
conceptualmente avanzadas desde la perspectiva de las ciencias de la computación:
243
244
Parte I:
El lenguaje Java
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;
new Thread (this, "Productor").start();
}
public void run(){
int i = 0;
while (true) {
q.put (i++) ;
}
}
}
class Consumer implements Runnable {
Q q;
Consumer (Q q) {
this.q = q;
new Thread(this, "Consumidor").start();
}
public void run() {
while(true) {
q.get() ;
}
}
}
class PC {
public static void main (String args[]) {
Q q = new Q();
new Producer(q);
new Consumer(q);
System.out.println ("Pulse Control-C para finalizar.");
}
}
Aunque los métodos put( ) y get( ) de Q son métodos sincronizados, nada impide que el
productor vaya más rápido que el consumidor, ni que el consumidor recolecté el mismo valor de
la cola dos veces. Por ello, se obtienen las salidas que se muestran continuación, visiblemente
incorrectas. La salida exacta depende de la velocidad del procesador y de la carga de tareas.
Produce:
Consume:
Consume:
Consume:
Consume:
Consume:
1
1
1
1
1
1
www.detodoprogramacion.com
Capítulo 11:
2
3
4
5
6
7
7
245
PARTE I
Produce:
Produce:
Produce:
Produce:
Produce:
Produce:
Consume:
Programación multihilo
Como se puede ver, después de que el productor genera un 1, el consumidor comienza y obtiene
el mismo 1 cinco veces seguidas. Entonces, el productor continúa y genera los valores del 2 al 7,
sin dejar al consumidor la oportunidad de obtenerlos.
La forma correcta de escribir este programa en Java consiste en utilizar los métodos wait( ) y
notify( ) para la comunicación en ambos sentidos:
// una implementación correcta del problema productor / consumidor.
class Q {
int n;
boolean valueSet = false;
synchronized int get() {
while (!valueSet)
try {
wait () ;
} catch (InterruptedException e) {
System.out.println ("Captura de la excepción InterruptedException");
}
System.out.println ("Consume: " + n);
valueSet = false;
notify () ;
return n;
}
synchronized void put (int n) {
while (valueSet)
try {
wait ();
} catch(InterruptedException e) {
System.out.println ("Captura de la excepción de InterruptedException");
}
this.n = n;
valueSet = true;
System.out.println ("Produce: " + n);
notify();
}
}
class Producer implements Runnable {
Q q;
Producer (Q q) {
this.q = q;
new Thread (this, "Productor").start();
}
www.detodoprogramacion.com
246
Parte I:
El lenguaje Java
public void run(){
int i = 0;
while (true) {
q.put (i++);
}
}
}
class Consumer implements Runnable {
Q q;
Consumer (Q q) {
this.q = q;
new Thread (this, "Consumidor").start();
}
public void run()
while (true) {
q.get();
}
}
}
class PCFixed {
public static void main (String args[]){
Q q = new Q();
new Producer(q);
new Consumer(q);
System.out.println ("Pulse Control+C para finalizar.");
}
}
Dentro de get( ), se llama a wait( ). Esto ocasiona que se suspenda la ejecución hasta que
Producer notifique que los datos están listos. Cuando esto sucede, se reanuda la ejecución
dentro de get( ). Una vez obtenidos los datos, desde el método get( ) se llama a notify( ). Esto
indica a Producer que puede colocar más datos en la cola. Dentro de put( ), el método wait( )
suspende la ejecución hasta que Consumer haya retirado el dato de la cola. Cuando la ejecución
continúa, se coloca el siguiente dato en la cola y se llama a notify( ), lo que indica a Consumer
que debe retirarlo.
La salida generada muestra el comportamiento correcto:
Produce:
Consume:
Produce:
Consume:
Produce:
Consume:
Produce:
Consume:
Produce:
Consume:
1
1
2
2
3
3
4
4
5
5
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
247
Bloqueos
• En general, ocurre pocas veces cuando los dos hilos coinciden en el tiempo de forma
correcta.
• Puede implicar a más de dos hilos y dos objetos sincronizados, es decir, el bloqueo
puede darse en una secuencia de eventos más compleja que la que se acaba de
describir.
Para una comprensión completa del bloqueo, es útil ver cómo se produce en la práctica. En
el siguiente ejemplo se crean dos clases, A y B, con los métodos foo( ) y bar( ), respectivamente,
que hacen una breve pausa cada uno antes de llamar al método de la otra clase. La clase
principal, denominada Deadlock, crea una instancia de A y otra de B, dando lugar a un
segundo hilo para establecer la condición de bloqueo. Los métodos foo( ) y bar( ) utilizan sleep( )
para obligar a que se produzca la condición de bloqueo.
// Un ejemplo de bloqueo.
class A {
synchronized void foo(B b) {
String name = Thread.currentThread().getName();
System.out.println (name + "entra en A.foo");
try{
Thread.sleep (l000);
} catch (Exception e) {
System.out.println ("Se interrumpe A");
}
System.out.println (name + " intenta llamar al método B.last()");
b.last();
}
synchronized void last() {
System.out.println ("Dentro de A.last");
}
}
class B {
synchronized void bar (A a) {
String name = Thread.currentThread().getName();
System.out.println (name + " entra en B.bar") ;
www.detodoprogramacion.com
PARTE I
Un tipo especial de error, que es necesario evitar y está relacionado específicamente con
la multitarea, es el bloqueo (mejor conocido como deadlock por su nombre en inglés). Este
error se produce cuando dos hilos tienen una dependencia circular en una pareja de objetos
sincronizados. Supongamos, por ejemplo, que un hilo entra en el monitor sobre el objeto X
y otro hilo en el monitor sobre el objeto Y. Si el hilo de X intenta llamar a cualquier método
sincronizado del objeto Y, tal y como se puede esperar, quedará bloqueado. Sin embargo, si el
hilo de Y, a su vez, intenta llamar a cualquier método sincronizado de X, quedará esperando
indefinidamente, ya que, para acceder a X, tendrá que liberar antes su propio candado en Y con
objeto de que el primer hilo pudiera finalizar. El bloqueo es un error difícil de depurar, por dos
razones:
248
Parte I:
El lenguaje Java
try {
Thread.sleep(l000);
} catch(Exception e) {
System.out.println ("Se interrumpe B");
}
System.out.println (name + " intenta llamar a A.last()");
a.last ();
}
synchronized void last() {
System.out.println ("Dentro de A.last");
}
}
class Deadlock implements Runnable {
A a = new A();
B b = new B();
Deadlock() {
Thread.currentThread().setName("Hilo Principal");
Thread t = new Thread(this, "Hilo hijo");
t.start();
a.foo(b); // Este hilo se bloquea en a.
System.out.println ("Regresa al hilo principal");
}
public void run() {
b.bar(a); // Este hilo se bloquea en b.
System.out.println ("Regresa al otro hilo");
}
public static void main (String args[]){
new Deadlock () ;
}
}
Al ejecutar este programa se obtiene la siguiente salida:
Hilo
Hilo
Hilo
Hilo
principal entra en A.foo
hijo entra en B.bar
principal intenta llamar a B.last()
hijo intenta llamar a A.last()
Al ejecutar el programa, el sistema se bloquea, por ello es necesario presionar CTRL+C
para finalizar. El volcado completo del hilo y de la memoria caché completos se puede ver
presionando CTRL+BREAK en una PC. De esta forma se comprueba que el Hilo hijo posee el
monitor de b mientras está esperando el monitor de a.
Al mismo tiempo, el Hilo principal posee a a y está esperando obtener b. Este programa
no se completará nunca. Como lo ilustra este ejemplo, si un programa multihilo no funciona
correctamente, una de las primeras condiciones que se deben revisar es el bloqueo.
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
249
Suspensión, reanudación y finalización de hilos
Suspensión, reanudación y finalización de hilos con Java 1.1 y versiones anteriores
Antes de Java 2, un programa utilizaba los métodos suspend( ) y resume( ), definidos por la
clase Thread, para parar y reanudar la ejecución de un hilo. La forma general de estos métodos
es:
final void suspend( )
final void resume( )
El siguiente programa es un ejemplo del uso de estos métodos:
// Uso de suspend() y resume().
class NewThread implements Runnable {
String name; // nombre del hilo
Thread t;
NewThread (String threadname) {
name = threadname;
t = new Thread (this, name);
System.out.println ("Nuevo hilo: " + t);
t.start(); // Comienzo del hilo
}
// Este es el punto de entrada del hilo.
public void run() {
try {
for (int i = 15; i > 0; i--) {
System.out.println (name + ": " + i);
Thread.sleep(200);
}
} catch (InterruptedException e) {
System.out.println (" Interrupción del hilo" + name);
}
System.out.println (" Salida del hilo" + name);
}
}
www.detodoprogramacion.com
PARTE I
Algunas veces es necesario suspender la ejecución de un hilo. Por ejemplo, un hilo se puede
utilizar para visualizar la hora del día, si el usuario no quiere este reloj, se puede suspender a este
hilo. Cualquiera que sea el caso, suspender un hilo es sencillo, y, una vez suspendido, volverlo a
activar también es fácil.
Los mecanismos que se utilizan en las nuevas versiones de Java, a partir de Java 2, para
suspender, finalizar y reanudar un hilo, son diferentes a los existentes en las versiones previas.
Aunque para cualquier nuevo código se debe utilizar el enfoque de Java 2, es conveniente
comprender cómo se realizaban estas operaciones en entornos con las versiones anteriores, si
se quiere actualizar o mantener un código antiguo. También es necesario comprender el motivo
de los cambios que se realizan en Java 2. Por estas razones, la siguiente sección describe la forma
original en que se controlaba la ejecución de un hilo, y en una sección posterior se describe el
enfoque empleado en Java 2.
250
Parte I:
El lenguaje Java
class SuspendResume {
public static void main (String args[]) {
NewThread obl = new NewThread ("Uno");
NewThread ob2 = new NewThread ("Dos");
try {
Thread.sleep(l000);
obl.t.suspend() ;
System.out.println ("Suspensión del hilo Uno");
Thread.sleep(l000);
obl.t.resume() ;
System.out.println ("Reanudación del hilo Uno");
ob2.t.suspend() ;
System.out.println ("Suspensión del hilo Dos");
Thread.sleep(l000);
ob2.t.resume();
System.out.println ("Reanudación del hilo Dos");
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal");
}
// Espera a que terminen los otros hilos
try {
System.out.println ("Espera la finalización de los otros hilos.");
ob1.t .join();
ob2.t.join() ;
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal");
}
System.out.println ("Salida del hilo principal.");
}
}
La salida generada por este programa es la siguiente (la salida puede variar por la velocidad del
procesador y la carga de tareas).
Nuevo hilo: Thread[Uno,5,main]
Uno: 15
Nuevo hilo: Thread[Dos,5,main]
Dos: 15
Uno: 14
Dos: 14
Uno: 13
Dos: 13
Uno: 12
Dos: 12
Uno: 11
Dos: 11
Suspensión del hilo Uno
Dos: 10
Dos: 9
Dos: 8
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
La clase Thread también define un método, llamado stop( ), que finaliza el hilo. Su forma
general es:
final void stop( )
Una vez finalizado, un hilo no puede reanudarse utilizando el método resume( ).
La forma moderna de suspensión, reanudación y finalización de hilos
Aunque los métodos suspend( ), resume( ) y stop( ), definidos por la clase Thread, parecen
razonables y un enfoque adecuado para la gestión de la ejecución de los hilos, no deben ser
utilizados por los nuevos programas de Java. La razón es la siguiente. El método suspend( ) de
la clase Thread ha sido descontinuado en Java 2 debido a que puede dar lugar a fallos graves
del sistema. Suponiendo que un hilo ha obtenido el acceso exclusivo sobre estructuras de
datos críticos, si ese hilo se suspende, no abandona ese acceso exclusivo. Por ende, otros hilos
que pueden estar esperando esos recursos podrían estar bloqueados.
También se descontinúa el método resume( ), ya que aunque no causa problemas no se
puede usar sin su equivalente método suspend( ).
El método stop( ) de la clase Thread también se descontinúa en Java 2, debido a que
también puede causar graves fallos del sistema. Supongamos que un hilo está escribiendo en
una estructura de datos importante y que sólo ha completado parte de los cambios. Si ese hilo se
finaliza en ese momento, esa estructura de datos podría quedar en un estado corrupto.
Al no poder usar los métodos suspend( ), resume( ) o stop( ) en Java 2 para controlar
un hilo, se podría pensar que no hay forma de parar, reiniciar o terminar un hilo, pero
afortunadamente esto no es así. Un hilo debe ser diseñado de forma que el método run( )
www.detodoprogramacion.com
PARTE I
Dos: 7
Dos: 6
Reanudación del hilo Uno
Suspensión del hilo Dos
Uno: 10
Uno: 9
Uno: 8
Uno: 7
Uno: 6
Reanudación del hilo Dos
Espera la finalización de los otros hilos.
Dos: 5
Uno: 5
Dos: 4
Uno: 4
Dos: 3
Uno: 3
Dos: 2
Uno: 2
Dos: 1
Uno: 1
Salida del hilo Dos.
Salida del hilo Uno.
Salida del hilo principal.
251
252
Parte I:
El lenguaje Java
compruebe periódicamente si ese hilo debe suspender, reanudar o finalizar su propia ejecución.
Normalmente esto se realiza estableciendo una variable bandera que indica el estado de la
ejecución del hilo. Mientras esta variable tenga asignado el valor “ejecutar”, el método run( )
debe continuar dejando que el hilo se ejecute. Si se asigna a esta variable el valor “suspender”, el
hilo debe parar, y si se le asigna el valor “finalizar”, el hilo debe terminar. Naturalmente, existen
muchas formas diferentes en las que se puede escribir el código correspondiente, pero la idea es
la misma para todos los programas.
El siguiente ejemplo pone de manifiesto cómo se pueden utilizar los métodos wait( ) y notify( ),
heredados de Object, para controlar la ejecución de un hilo. Este ejemplo es semejante al de la
sección anterior; sin embargo se han eliminado las llamadas a los métodos descontinuados.
Veamos cómo funciona este programa.
La clase NewThread contiene una variable de instancia boolean denominada
suspendFlag, que se utiliza para controlar la ejecución del hilo. El constructor inicializa
a suspendFlag con el valor false. El método run( ) contiene una sentencia de bloque
synchronized que revisa la variable suspendFlag. Si esa variable tiene el valor true, se invoca
al método wait( ) para suspender la ejecución del hilo. El método mysuspend( ) asigna a la
variable suspendFlag el valor true. El método myresume( ) asigna a la variable suspendFlag
el valor false e invoca a notify( ) para reactivar el hilo. Finalmente, se ha modificado el método
main( ) para llamar a los métodos mysuspend( ) y myresume( ).
// Versión moderna de suspensión y reanudación de un hilo
class NewThread implements Runnable {
String name; // nombre del hilo
Thread t;
boolean suspendFlag;
NewThread (String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println ("Nuevo hilo: " + t);
suspendFlag = false;
t.start(); // Comienzo del hilo
}
// Este es el punto de entrada del hilo.
public void run() {
try {
for (int i = 15; i > 0; i--) {
System.out.println (name + ": " + i);
Thread.sleep (200);
synchronized (this) {
while (suspendFlag) {
wait() ;
}
}
}
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo" + name);
}
www.detodoprogramacion.com
Capítulo 11:
Programación multihilo
253
System.out.println ("Salida del hilo" + name);
}
PARTE I
void mysuspend () {
suspendFlag = true;
}
synchronized void myresume()
suspendFlag = false;
notify ();
}
}
class SuspendResume {
public static void main (String args[]) {
NewThread obl = new NewThread("Uno");
NewThread ob2 = new NewThread("Dos") ;
try {
Thread.sleep (l000);
obl.mysuspend ();
System.out.println ("Suspensión del hilo Uno");
Thread.sleep (l000);
obl.myresume();
System.out.println ("Reanudación del hilo Uno");
ob2.mysuspend() ;
System.out.println ("Suspensión del hilo Dos");
Thread.sleep(l000);
ob2.myresume();
System.out.println ("Reanudación del hilo Dos");
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal");
}
// espera a que los otros hilos terminen
try {
System.out.println ("Espera la finalización de los otros hilos.");
obl.t.join () ;
ob2.t.join();
} catch (InterruptedException e) {
System.out.println ("Interrupción del hilo principal");
}
System.out.println ("Salida del hilo principal.");
}
}
La salida de este programa es la misma que la que aparece en el apartado anterior. Más
adelante se verán más ejemplos en los que se usa el mecanismo moderno de control de hilos.
Aunque este mecanismo no es tan claro como el de la versión anterior, es la forma de asegurar
que no se producirán errores en tiempo de ejecución, y es el enfoque que se debe utilizar en el
nuevo código.
www.detodoprogramacion.com
254
Parte I:
El lenguaje Java
Programación multihilo
La clave para utilizar de manera eficaz las características multihilo de Java es pensar de
manera concurrente, en lugar de hacerlo de forma lineal o en serie. Por ejemplo, si se tienen
dos subsistemas de un programa que se pueden ejecutar concurrentemente, conviene
hacer, con cada uno de esos subsistemas, hilos individuales. Con un uso adecuado de la
programación multihilo se pueden crear programas muy eficientes. Sin embargo, conviene
tener la precaución de no crear demasiados hilos, ya que en ese caso se puede degradar
el rendimiento del programa en lugar de mejorarlo. Conviene recordar que el cambio de
contexto lleva asociado una carga de trabajo adicional. Si se crean demasiados hilos, se
gastará más tiempo de CPU en los cambios de contexto entre hilos que en la ejecución del
programa.
www.detodoprogramacion.com
12
CAPÍTULO
Enumeraciones, autoboxing
y anotaciones (metadatos)
E
ste capítulo examina tres anexos recientes en el lenguaje Java: enumeraciones, autoboxing y
anotaciones (también llamadas metadatos). Cada uno de ellos extiende el poder del lenguaje
al ofrecer una forma estilizada de gestionar tareas comunes de programación. Este capítulo
también presenta los tipos envueltos de Java e introduce el concepto de reflexión.
Enumeraciones
Versiones anteriores a JDK 5 carecían de una característica que muchos programadores sentían era
necesaria: enumeraciones. En su forma simple, una enumeración es una lista de constantes. Aunque
Java ofrece otras características que proveen de alguna manera funcionalidades similares, tales como
las variables final, para muchos programadores aún faltaba el concepto puro de enumeraciones
–especialmente porque las enumeraciones están presentes en la mayoría de los lenguajes
comúnmente utilizados. A partir de JDK 5, las enumeraciones fueron agregadas al lenguaje Java, y
ahora están disponibles para los programadores en Java.
En la forma más simple, las numeraciones en Java parecen similares a las enumeraciones
de otros lenguajes. Sin embargo, esta similitud es sólo superficial. En lenguajes como C++, las
enumeraciones simplemente son listas de constantes de tipo entero. En Java, una enumeración
define un tipo (una clase), esto expande enormemente el concepto de enumeración. Por ejemplo, en
Java, una enumeración puede tener constructores, métodos y variables de instancia. Además, aunque
las enumeraciones en Java tardaron varios años en aparecer, la rica implementación hecha de ellas en
Java justifica la espera.
Fundamentos de las enumeraciones
Una enumeración se crea utilizando la palabra clave enum. Por ejemplo, ésta es una enumeración
simple que lista algunas categorías de manzanas.
//Una enumeración de categorías de manzanas
enum Manzana {
Jonathan, GoldenDel, RedDel, Winesap, Cortland
}
Nota de los traductores: Hemos preferido dejar la palabra autoboxing sin traducir. El término hace
referencia al proceso de convertir un dato primitivo en un objeto equivalente automáticamente.
Se dice que el dato original es colocado dentro del objeto, como un regalo dentro de una caja.
www.detodoprogramacion.com
255
256
Parte I:
El lenguaje Java
Los identificadores Jonathan, GoldenDel y el resto, son llamados constantes de enumeración.
Cada uno está implícitamente declarado como un miembro de tipo public, static y final de la
clase Manzana. Además, su tipo es el tipo de la enumeración en la cual fueron declarados, en
este caso es Manzana.
Una vez que se tiene definida una enumeración, se puede crear una variable de ese tipo. Sin
embargo, aunque las enumeraciones definen a una clase tipo, no se instancia un enum usando
new. En lugar de eso, se declara y usa una variable enumeración tal como se hace con los tipos
primitivos. Por ejemplo, el siguiente código declara ap como una variable del tipo enumerado
Manzana:
Manzana ap;
Dado que ap es de tipo Manzana, los únicos valores que le pueden ser asignados (o puede
contener) son aquellos definidos por la enumeración. Por ejemplo, la siguiente línea asigna a ap
el valor RedDel:
ap = Manzana.RedDel;
Note que el símbolo RedDel es precedido por Manzana.
Dos constantes de enumeración pueden ser comparadas en busca de una igualdad
utilizando el operador relacional ==. Por ejemplo, la siguiente sentencia compara el valor de ap
con el de la constante GoldenDel:
if (ap == Manzana.GoldenDel) //…
Un valor de enumeración puede también ser utilizado para controlar una sentencia switch.
Claro está que todas las sentencias case deben ser constantes de la misma variable enumerada
utilizada en la expresión de switch. Por ejemplo, la siguiente es una sentencia switch
perfectamente válida:
//Usa una enumeración para controlar una sentencia switch
switch (ap) {
case Jonathan;
// …
case Winesap;
// …
Note que las sentencias case, los nombres de las constantes enumeradas son listadas sin
estar precedidas por el nombre de sus tipo de enumeración. Esto es, Winesap se utiliza
en lugar de Manzana.Winesap. Esto se debe a que el tipo de la enumeración de la variable en
la expresión switch específica implícitamente el tipo enumerado para las constantes utilizadas
en las sentencias case. No es necesario utilizar el nombre de la enumeración junto al nombre de
las constantes en la sentencia case. De hecho, hacerlo causaría un error de compilación.
Cuando una constante enumerada es mostrada en pantalla con una sentencia println( ), su
nombre es mostrado en pantalla. Por ejemplo, la siguiente sentencia
System.out.println(Apple.Winesap);
Despliega en pantalla el nombre Winesap.
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
257
El siguiente programa coloca todas las piezas juntas utilizando la enumeración Manzana:
class EnumDemo {
public static void main(String args[])
{
Manzana ap;
ap = Apple.RedDel;
// mostrar en pantalla un valor de tipo enum
System.out.println("Valor de ap: "+ ap);
System.out.println();
ap = Manzana.GoldenDel;
// comparar dos valores de tipo enum
if(ap == Apple.GoldenDel)
System.out.println("ap contiene GoldenDel.\n");
// uso de una variable enum en una sentencia switch
switch (ap) {
case Jonathan:
System.out.println ("La manzana Jonathan es roja.");
break;
case GoldenDel:
System.out.println("La manzana Golden Delicious es amarilla.");
break;
case RedDel:
System.out.println("La manzana Red Delicious es roja.");
break;
case Winesap:
System.out.println("La manzana Winesap es roja.");
break;
case Cortland:
System.out.println("La manzana Cortland es roja.");
break;
}
}
}
La salida de este programa se muestra a continuación:
El valor de ap: RedDel
ap contiene: GoldenDel.
La manzana Golden Delicious es amarilla.
www.detodoprogramacion.com
PARTE I
// Una enumeración de tipos de manzanas
enum Manzana {
Jonathan, GoldenDel, RedDel, Winesap, Cortland
}
258
Parte I:
El lenguaje Java
Los métodos values( ) y valuesOf( )
Todas las enumeraciones automáticamente contienen dos métodos predefinidos: values( ) y
valueOf( ). La siguiente es su forma general:
public static enum-type[ ] values( )
public static enum-type valueOf(String str)
El método values( ) regresa un arreglo que contiene una lista de constantes enumeradas.
El método valueOf( ) regresa la constante enumerada cuyo valor corresponde a la cadena
pasada en el parámetro str. En ambos casos, enum-type es el tipo de enumeración. Por
ejemplo, en el caso de la enumeración Manzana que se mostró anteriormente, Manzana.
valueOf(“Winesap”) regresa Winesap.
El siguiente programa muestra el uso de los métodos values( ) y valueOf( ):
// Uso de los métodos predefinidos para las enumeraciones
// Una enumeración de tipos de manzana.
enum Manzana {
Jonathan, GoldenDel, RedDel, Winesap, Cortland
}
class EnumDemo2 {
public static void main(String args[])
{
Manzana ap;
System.out.println("Estas son todas las constantes de tipo Manzana:");
// usando el método values()
Manzana allapples[] = Manzana.values();
for(Manzana a : allapples)
System.out.println(a) ;
System.out.println();
// usando el método valueOf ()
ap = Manzana.valueOf ("Winesap") ;
System.out.println("ap contiene " + ap);
}
}
La salida del programa es la siguiente:
Estas son todas las constantes de tipo Manzana:
Jonathan
Golden Del
RedDel
Winesap
Cortland
ap contiene Winesap
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
for (Manzana a: Manzana.values())
System.out.println(a);
Ahora, nótese cómo el valor correspondiente al nombre Winesap fue obtenido por la
llamada al método valueOf( ).
ap = Manzana.valueOf("Winesap");
Como se explicó antes, valueOf( ) regresa el valor en la enumeración asociado con el nombre
de la constante representada como una cadena.
NOTA
Los programadores de C/C++ notarán que Java hace mucho más sencillo el traducir entre el
nombre legible de una constante enumerada y su valor binario. Ésta es una ventaja significante
de la implementación de enumeraciones en Java.
Las enumeraciones en Java son tipos de clase
Como se explicó una enumeración de Java es un tipo de clase. Aunque no se instancia
un enum utilizando new, éstos tienen casi las mismas capacidades de las clases. El
hecho de que enum defina una clase hace que la enumeración de Java tenga poderes que
en una enumeración en otros lenguajes simplemente no existen. Por ejemplo, se pueden tener
constructores, agregar variables de instancia y métodos, e incluso implementar interfaces.
Es importante entender que cada constante de la enumeración es un objeto de su propio
tipo enumerado. Así, cuando se define un constructor para un enum, el constructor es llamado
cuando cada constante de enumeración es creada. También, cada constante de enumeración
tiene su propia copia de cualquier variable de instancia definida para la enumeración. Por
ejemplo, consideremos la siguiente versión de la enumeración Manzana:
//Uso de constructores, variables y métodos en una enumeración.
enum Manzana {
Jonathan(l0), GoldenDel(9), RedDel(12), Winesap(15), Cortland(8);
private int price; // precio de cada Manzana
// constructor
Manzana (int p) { price = p; }
int getPrice () { return price; }
}
class EnumDemo3
public static void main (String args[])
{
Manzana ap;
www.detodoprogramacion.com
PARTE I
Nótese que el programa utiliza un ciclo estilo for-each el cual itera a través del arreglo de
constantes obtenidas cuando se llama al método values( ). Para ilustrar esto, se creó la variable
allapples y se le asignó una referencia a un arreglo con los valores de la enumeración. Sin
embargo, este paso no es necesario porque el for podría haber sido escrito como se muestra a
continuación, eliminando la necesidad de la variable allapples:
259
260
Parte I:
El lenguaje Java
// mostrar el precio de Winesap
System.out.println("Winesap cuesta " +
Manzana.Winesap.getPrice() +
" centavos.\n");
// mostrar todos los tipos de manzana y su precio.
System.out.println( "Todas las manzanas y sus precios: ");
for(Manzana a : Manzana.values())
System.out.println (a+ " cuesta " + a.getPrice() +
" centavos.");
}
}
La salida de este programa se muestra a continuación:
Winesap cuesta 15 centavos
Todas las manzanas y sus precios:
Jonathan cuesta 10 centavos
GoldenDel cuesta 9 centavos
RedDel cuesta 12 centavos
Winesap cuesta 15 centavos
Cortland cuesta 8 centavos
Esta versión de Manzana agrega tres cosas. La primera es la variable de instancia precio, la
cual es utilizada para almacenar el precio de cada tipo de manzana. La segunda es el constructor
Manzana, al cual se pasa el precio de cada manzana. La tercera es el método getPrice( ), el cual
regresa el valor del precio.
Cuando se declara la variable ap en main( ), el constructor Manzana es llamado una
vez para cada constante especificada. Nótese como los argumentos para el constructor son
especificados dentro de paréntesis al lado de cada constante, como se muestra a continuación:
Jonathan (10), GoldenDel (9), RedDel (12), Winesap (15), Cortland (8);
Estos valores son pasados al parámetro p de Manzana( ), el cual asigna el valor a la variable
precio. El constructor es llamado una vez para cada constante.
Dado que cada constante en la enumeración tiene su propia copia de la variable precio, es
posible obtener el precio de un tipo específico de manzana llamando al método getPrice( ). Por
ejemplo, en el método main( ) el precio de Winesap es obtenido por la siguiente llamada:
Manzana.Winesap.getPrice()
El precio para cada una de la variedades es obtenido utilizando un ciclo a través de la
enumeración con un ciclo for. Debido a que hay una copia de precio para cada constante
en la enumeración, el valor asociado con una constante es independiente del valor asociado
con otra. Éste es un concepto poderoso, y sólo está disponible cuando las enumeraciones son
implementadas como clases tal como lo hace Java.
Aunque el ejemplo anterior contiene sólo un constructor, una enumeración puede tener
dos o más constructores sobrecargados, tal como las otras clases lo pueden hacer. Por ejemplo,
la siguiente versión de Manzana provee un constructor por omisión que inicializa el precio a –1,
para indicar que no existe un precio disponible:
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
private int price; // precio de cada manzana
// constructor
Manzana (int p) { price = p; }
// constructor sobrecargado
Manzana () { price = -1; }
int getPrice () { return price; }
}
Nótese que en esta versión, para RedDel no se proporcionan argumentos. Esto significa que el
constructor por omisión es llamado, y la variable precio para RedDel tendrá el valor –1.
Existen dos restricciones que se aplican a las enumeraciones. Primero, una enumeración no
puede heredar de otra clase. En segundo lugar, una enumeración no puede ser una superclase.
Esto significa que una enumeración no puede ser extendida. Por lo demás, una enumeración
actúa de igual forma que cualquier otro tipo de clase. La clave es recordar que cada constante en
la enumeración es un objeto de la clase en la cual está definida.
Las enumeraciones heredan de la clase enum
Aunque no se puede heredar a una superclase cuando se declara un enum, todas las
enumeraciones automáticamente heredan una: java.lang.Enum. Esta clase define varios
métodos que están disponibles para el uso de todas las enumeraciones. La clase Enum se
describe a detalle en la Parte II, por ahora revisaremos sólo tres de sus métodos.
Es posible obtener la posición de una constante en la enumeración, también llamado su
valor ordinal, llamando al método ordinal( ), definido como:
final int ordinal( )
Este método regresa el valor ordinal de la constante que lo invoca. Los valores ordinales
comienzan en cero. Así, en la enumeración Manzana, Jonathan tiene un valor ordinal cero,
GoldenDel tiene un valor ordinal 1, RedDel tiene un valor ordinal 2, y así sucesivamente.
Es posible comparar los valores ordinales de dos constantes de la misma enumeración
utilizando el método compareTo( ). El cual está definido como:
final int compareTo(tipoEnum e)
Donde tipoEnum es el tipo de la enumeración, y e es la constante a comparar con la constante
que la invoca al método. Recuerde que la constante que invoca y la constante e deben ser del
mismo tipo de enumeración. Si la constante que invoca tiene un valor ordinal menor que
e, entonces compareTo( ) regresa un valor negativo. Si los dos valores ordinales son iguales,
entonces se regresa cero. Si la constante que invoca tiene un valor mayor que e, entonces se
regresa un valor positivo.
Es posible comparar la igualdad de una constante de enumeración con cualquier otro objeto
utilizando equals( ), este método sobrescribe al método equals( ) definido por la clase Object.
Aunque equals( ) puede comparar una constante de enumeración con cualquier otro objeto,
esos dos objetos serán iguales sólo si ambos hacen referencia a la misma constante, dentro de
www.detodoprogramacion.com
PARTE I
// Uso de constructores en enumeraciones
enum Manzana {
Jonathan(l0), GoldenDel (9), RedDel, Winesap (15), Cortland (8) ;
261
262
Parte I:
El lenguaje Java
la misma enumeración. El simple hecho de tener valores ordinales en común no causará que
equals( ) regrese el valor de verdad si las dos constantes son de diferentes enumeraciones.
Es posible comparar dos referencias enumeración en busca de igualdad utilizando ==. El
siguiente programa muestra el uso de los métodos ordinal( ), compareTo( ) y equals( ):
// Ejemplo de los métodos ordinal(), compareTo(), y equals().
// Una enumeración de variedades de manzana
enum Manzana {
Jonathan, GoldenDel, RedDel, Winesap, Cortland
}
class EnumDemo4 {
public static void main(String args[])
{
Manzana ap, ap2, ap3;
// Obtener todos los valores ordinales utilizando el método ordinal().
System.out.println(“Estas son todas las constantes manzana" +
"y sus valores ordinales: ");
for(Manzana a : Manzana.values())
System.out.println(a+ " " + a.ordinal());
ap = Apple.RedDel;
ap2 = Apple.GoldenDel;
ap3 = Apple.RedDel;
// uso de los métodos compareTo() y equals()
if(ap.compareTo(ap2) < 0)
System.out.println(ap + " va antes de " + ap2);
if(ap.compareTo(ap2) > 0)
System.out.println(ap2 + " va antes de " + ap);
if(ap.compareTo(ap3) == 0)
System.out.println(ap + " es igual a " + ap3);
System.out.println() ;
if(ap.equals(ap2))
System.out.println("¡Error!") ;
if(ap.equals(ap3))
System.out.println(ap + " es igual a " + ap3);
if (ap == ap3)
System.out.println(ap + " == " + ap3);
}
}
La salida del programa se muestra a continuación:
Estas son todas las constantes manzana y sus valores ordinales:
Jonathan 0
GoldenDel 1
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
GoldenDel va antes de RedDel
RedDel es igual a RedDel
RedDel es igual a RedDel
RedDel == RedDel
Otro ejemplo con enumeraciones
Antes de continuar, veamos un ejemplo diferente que utiliza enum. En el Capítulo 9 se
construyó un programa de toma de decisiones automáticas. En esa versión, las variables
llamadas NO, SI, QUIZAS, DESPUES, PRONTO Y NUNCA, fueron declaradas dentro de una
interfaz y utilizadas para representar las posibles respuestas. Técnicamente no hay ningún error
con esa solución; sin embargo, la enumeración es una mejor opción. A continuación se muestra
una versión mejorada de ese programa, la cual utiliza una enumeración llamada Respuestas
para definir las posibles respuestas. Se recomienda al lector comparar esta versión con la original
del Capítulo 9.
//
//
//
//
Una versión mejorada del programa de "Toma de Decisiones"
escrito en el capítulo 9. Esta versión utiliza una
enumeración, en vez de variables de interfaz para
representar los valores de las respuestas.
import java.util.Random;
// Una enumeración de posibles respuestas.
enum Respuestas {
NO, SI, QUIZAS, DESPUES, PRONTO, NUNCA
}
class Question {
Random rand = new Random();
Respuestas ask ( ) {
int prob = (int) (100 * rand.nextDouble());
if (prob < 15)
return Respuestas.QUIZAS; // 15%
else if (prob < 30)
return Respuestas.NO; // 15%
else if (prob < 60)
return Respuestas.SI; // 30%
else if (prob < 75)
return Respuestas.DESPUES; // 15%
else if (prob < 98)
return Respuestas.PRONTO; / / 13%
else
return Respuestas.NUNCA; // 2%
}
}
www.detodoprogramacion.com
PARTE I
RedDel 2
Winesap3
Cortland 4
263
264
Parte I:
El lenguaje Java
class AskMe {
static void answer(Respuestas result) {
switch (result) {
case NO:
System.out.println("No") ;
break;
case SI:
System.out.println("Si") ;
break;
case QUIZAS:
System.out.println("Quizás") ;
break;
case DESPUES:
System.out.println("Después") ;
break;
case PRONTO:
System.out.println("Pronto");
break;
case NUNCA:
System.out.println("Nunca") ;
break;
}
}
public static void main(String args[]) {
Question q = new Question() ;
answer(q.ask()) ;
answer(q.ask()) ;
answer(q.ask()) ;
answer(q.ask()) ;
}
}
Envoltura de tipos
Como sabemos, Java utiliza tipos primitivos (también llamados tipos simples), tales como int
y double, como los tipos de datos básicos del lenguaje. Los tipos primitivos son utilizados
para favorecer el rendimiento. Utilizar objetos para valores primitivos agregaría una
sobrecarga, incluso para cálculos simples, poco deseable. Por ello, los tipos primitivos no son
parte de la jerarquía de objetos y por ende no heredan de la clase Object.
A pesar de los beneficios de rendimiento ofrecidos por los tipos primitivos, existen
ocasiones cuando se requiere su representación como un objeto. Por ejemplo, no es posible
pasar como parámetro a un método un tipo primitivo por referencia. Además, muchas de
las estructuras de datos estándares implementadas por Java trabajan sobre objetos, lo que
significa que no es posible usar estas estructuras de datos para almacenar datos primitivos.
Para gestionar estas situaciones (y otras) Java provee la envoltura de tipos, que consiste en
proporcionar clases que encapsulan a un tipo primitivo dentro de un objeto. Las clases que
sirven como envolturas de tipos son descritas a detalle en la Parte II, pero son introducidas aquí
debido a que están relacionadas directamente con la característica de autoboxing de Java.
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
Character
Character es la envoltura del tipo char. El constructor para Character es
Character (char ch)
Donde ch especifica el carácter que será envuelto por el objeto Character que está siendo creado.
Para obtener el valor char contenido en el objeto Character, se llama al método charValue( ),
como se muestra a continuación:
char charValue( )
el cuál regresa al carácter encapsulado.
Boolean
Boolean es la envoltura alrededor de los valores del tipo primitivo boolean. El cual define estos
constructores:
Boolean (boolean boolValue)
Boolean (String boolString)
En la primera versión, boolValue debe ser true o false. En la segunda versión, si boolString
contiene la cadena “true” (en minúsculas o mayúsculas), entonces el nuevo objeto Boolean será
verdadero, de otra forma, será falso.
Para obtener el valor del objeto Boolean, se utiliza el método booleanValue( ), como se
muestra a continuación:
boolean booleanValue( )
el cual regresa el valor de tipo boolean equivalente al del objeto invocado.
Las envolturas de tipos numéricos
Por mucho, las envolturas más comúnmente usadas son aquellas que representan valores
numéricos. Estas envolturas son Byte, Short, Integer, Long, Float y Double. Todas las
envolturas de los tipos numéricos heredan de la clase abstracta Number. Number declara
métodos que regresan el valor de un objeto en cada uno de los diferentes formatos. Estos
métodos se muestran a continuación:
byte byteValue( )
double doubleValue( )
float floatValue( )
int intValue( )
long longValue( )
short shortValue( )
Por ejemplo, doubleValue( ) regresa el valor de un objeto como un valor de tipo double,
floatValue( ) regresa el valor como un valor de tipo float, y así sucesivamente. Estos métodos
son implementados por cada una de las envolturas de tipos numéricos.
www.detodoprogramacion.com
PARTE I
Las envolturas de tipos son Double, Float, Long, Integer, Short, Byte, Character
y Boolean. Estas clases ofrecen un conjunto amplio de métodos que permiten integrar
completamente a los tipos primitivos dentro de la jerarquía de objetos de Java. Cada uno es
examinado brevemente a continuación.
265
266
Parte I:
El lenguaje Java
Todas las envolturas de tipos numéricos definen constructores que permiten a un objeto
ser construido a partir de un valor dado o a partir de una cadena que represente el valor. Por
ejemplo, aquí se presentan los constructores definidos para la clase Integer:
Integer(int num)
Integer(String str)
Si str no contiene un valor numérico válido entonces una excepción de tipo
NumberFormatException es lanzada. Todas las envolturas de tipo sobrescriben al método
toString( ). El cual regresa en una forma compresible el valor contenido dentro de la envoltura.
Esto permite, por ejemplo, desplegar el valor del objeto envuelto cuando es usado en un
println( ) sin tener que convertirlo a su tipo primitivo.
El siguiente programa demuestra cómo se utiliza una envoltura de tipo numérico para
encapsular un valor y después extraerlo.
// demostración de envoltura de tipos
class Wrap {
public static void main(String args[]) {
Integer iOb = new Integer(l00);
int i = iOb.intValue();
System.out.println(i + " " + iOb); // muestra 100 100
}
}
Este programa envuelve el valor entero de 100 dentro de un objeto Integer llamado iOb. El
programa entonces obtiene ese valor llamando intValue( ) y almacena el resultado en i.
El proceso de encapsulación de un valor dentro de un objeto es llamado boxing. Así, en el
programa, esta línea realiza el boxing del valor 100 dentro de un objeto Integer.
Integer iOb = new Integer(100);
El proceso de extracción del valor desde una envoltura de tipos es llamado unboxing. Por ejemplo,
el programa realiza unboxing del valor de iOb con la siguiente línea:
int i = iOb.intValue();
El mismo procedimiento general utilizado por el programa anterior para boxing y unboxing ha
sido empleado desde la versión original de Java. Sin embargo, con la llegada del JDK 5, Java
mejoró considerablemente esta característica adicionando el concepto de autoboxing que se
describe a continuación.
Autoboxing
A partir de JDK 5, Java agregó dos importantes características: autoboxing y auto-unboxing.
Autoboxing es el proceso por medio del cual un tipo primitivo es automáticamente encapsulado
dentro de un objeto generado por envoltura de tipos, en cualquier lugar donde un objeto
de ese tipo se requiera. No es necesario construir explícitamente un objeto. Auto-unboxing
es el proceso mediante el cual el valor de un objeto (generado por envoltura de tipos) es
automáticamente despojado de su envoltura de tipo cuando el valor es requerido. No es
necesario llamar a un método tal como intValue( ) o doubleValue( ).
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
Integer iOb = 100; // autoboxing de un valor de tipo int
Note que no se crea explícitamente un objeto usando la palabra clave new. Java gestiona esto
automáticamente.
Para realizar el unboxing de un objeto, simplemente debe asignar el objeto referenciado a
una variable de tipo primitivo. Por ejemplo, para realizar unboxing de iOb, se utiliza la siguiente
línea:
int i = iOb; //auto-unboxing
Java gestiona los detalles automáticamente.
Ésta es una nueva versión del programa anterior re-escrito utilizando autoboxing /
unboxing:
// Ejemplo de autoboxing / unboxing
class AutoBox {
public static void main (String args []) {
Integer iOb = 100; / / autoboxing un valor de tipo int
int i = iOb; // auto-unboxing
System.out.println(i+ " " + iOb); // muestra 100 100
}
}
Autoboxing y métodos
Además de ocurrir en los casos simples de asignación de valores, el autoboxing ocurre en
cualquier momento que un tipo primitivo debe ser convertido en un objeto y auto-unboxing
toma lugar cuando un objeto debe ser convertido a un tipo primitivo. Así, autoboxing y autounboxing pueden ocurrir cuando un argumento se pasa a un método, o cuando un valor es
devuelto por un método. Por ejemplo, considere el siguiente código:
// autoboxing y auto-unboxing ocurren cuando
// un método recibe argumentos o devuelve valores
class AutoBox2 {
// este método recibe un argumento del tipo Integer y regresa
// un valor del tipo primitivo int
static int m (Integer v) {
return v ; // auto-unboxing el objeto v a un valor int
}
www.detodoprogramacion.com
PARTE I
Agregar autoboxing y auto-unboxing estiliza enormemente el código de muchos algoritmos,
removiendo el tedio del boxing y unboxing manual de valores. Esto también ayuda a prevenir
errores. Además, es muy importante para la implementación de tipos parametrizados, la cual
opera sólo en objetos. Finalmente, autoboxing hace el trabajo con el Framework de Colecciones
(descrita en la Parte II) mucho más sencilla.
Con el autoboxing ya no se necesita construir manualmente un objeto para envolver un
tipo primitivo. Sólo se necesita asignar el valor a una referencia de una envoltura del tipo. Java
automáticamente construye el objeto. Por ejemplo, ésta es la forma moderna de construir un
objeto Integer que envuelve al valor de 100:
267
268
Parte I:
El lenguaje Java
pub1ic static void main(String args[]) {
// Se envía un valor de tipo int al método m()
y asigna el valor a un objeto Integer.
// El argumento 100 sufre autoboxing,
// al igual que el valor regresado por el método
Integer iOb = m(100);
System.out.println(iOb) ;
}
}
El programa despliega el siguiente resultado:
100
En el programa, note que m( ) especifica un parámetro Integer y regresa un valor de tipo int
como resultado. Dentro del main( ), al método m( ) se le pasa el valor 100. Dado que m( ) está
esperando un Integer, al valor 100 se aplica autoboxing. Luego el método m( ) regresa el valor
int equivalente a su argumento. Esto causa que la variable v sufra auto-unboxing. Finalmente,
este valor int es asignado al objeto iOb en main( ), el cuál causa que el valor int regresado pase
nuevamente por autoboxing.
Autoboxing en expresiones
En general, autoboxing y auto-unboxing ocurren en cualquier momento en que una conversión
de un valor a un objeto o de un objeto a un valor es requerida. Esto aplica también a las
expresiones. Dentro de una expresión a los objetos se les aplica automáticamente unboxing y
al resultado de la expresión autoboxing si es necesario. Por ejemplo, consideremos el siguiente
programa:
// autoboxing y auto-unboxing ocurren en las expresiones.
class AutoBox3 {
pub1ic static void main(String args[]) {
Integer iOb, iOb2;
int i;
iOb = 100;
System.out.println("Valor original de iOb: " + iOb);
// El código siguiente aplica automáticamente unboxing a iOb,
// realiza un incremento y luego aplica autoboxing nuevamente
// para colocar el resultado en iOb
++iOb;
System.out.println("Después de ++iOb: " + iOb);
// La expresión se evalúa después de que a iOb se le aplica unboxing,
// al resultado se le aplica autoboxing y luego se almacena en iOb2.
iOb2 = iOb + (iOb / 3);
System.out.println("iOb2 después de evaluar la expresión es: " + iOb2);
// La misma expresión se evalúa ahora sin que sea necesario
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
}
}
La salida se muestra a continuación:
Valor original de iOb: 100
Después de ++iOb: " + 101
iOb2 después de evaluar la expresión es: 134
i después de evaluar la expresión es: 134
En el programa es importante poner especial atención en la siguiente línea:
++iOb;
Esta línea causa que el valor en iOb sea incrementado. Funciona de la siguiente forma: iOb pasa
por el proceso de unboxing, el valor es incrementado, y al resultado se le aplica autoboxing.
El proceso de auto-unboxing también permite que se mezclen diferentes tipos de objetos
numéricos en una expresión. Una vez que los valores pasan por unboxing, se aplican las
conversiones y promociones estándares. Por ejemplo, el siguiente programa es perfectamente
válido:
c1ass AutoBox4 {
pub1ic static void main(String args[])
// autoboxing y auto-unboxing dentro de expresiones
Integer iOb = 100;
Doub1e dOb = 98.6;
dOb = dOb + iOb;
System.out.println("dOb después de la expresión: " + dOb);
}
}
La salida de ese código es:
dOb después de la expresión: 198.6
Como se puede ver, tanto el objeto Double dOb como el objeto Integer iOb participan en la
adición y el resultado pasa por autoboxing antes de ser almacenado en dOb.
Debido al auto-unboxing, es posible utilizar objetos numéricos enteros para controlar una
sentencia switch. Por ejemplo, considere el siguiente segmento de código:
Integer iOb = 2;
switch (iOb) (
case 1: System.out.println ("uno") ;
break;
case 2: System.out.println ("dos") ;
break;
www.detodoprogramacion.com
PARTE I
// aplicar autoboxing al resultado
i = iOb + (iOb / 3);
System.out.println ("i después de evaluar la expresión es: " + i);
269
270
Parte I:
El lenguaje Java
default: System.out.println("error");
}
Cuando la expresión en el switch es evaluada, a iOb se le aplica unboxing y su valor entero es
obtenido.
Los ejemplos muestran cómo la aplicación de autoboxing y auto-unboxing a objetos
numéricos dentro de expresiones es intuitiva y fácil. En el pasado, un código similar habría
involucrado conversión de tipos y llamadas a métodos, como por ejemplo intValue( ).
Autoboxing en valores booleanos y caracteres
Como se describió anteriormente, Java también proporciona envolturas para los tipos
primitivos boolean y char. Esas envolturas son Boolean y Character. Los procesos de
autoboxing y auto-unboxing se aplican a esas envolturas también. Por ejemplo, considere el
siguiente programa:
// Autoboxing y unboxing de objetos Boolean y Character.
class AutoBox5 {
public static void main(String args[]) {
// autoboxing y unboxing aplicado a un valor boolean.
Boolean b = true;
// b pasa por auto-unboxing cuando es utilizada
// en una expresión condicional
if(b) System.out.println("b es true");
// autoboxing y unboxing aplicado a un valor char.
Character ch = 'x'; // autoboxing un char
char ch2 = ch; // unboxing un char
System.out.println("ch2 es " + ch2);
}
}
La salida se muestra a continuación:
b es true
ch2 es x
El punto más importante a considerar en este programa es el auto-unboxing de b dentro de
la expresión condicional if. Como recordará, las expresiones condicionales que controlan un if
deben ser evaluadas a un resultado de tipo boolean. Debido al auto-unboxing, el valor boolean
que está contenido en b es obtenido automáticamente cuando la expresión condicional lo
requiere. La llegada del autoboxing y el unboxing ha permitido que un objeto Boolean pueda ser
utilizado para controlar una sentencia if.
Debido al auto-unboxing, un objeto de tipo Boolean ahora también puede ser utilizado para
controlar cualquiera de las sentencias de ciclo de Java. Cuando un objeto Boolean es utilizado
en la expresión condicional de un while, for o do/while, se le aplica automáticamente unboxing
para convertirlo en su equivalente boolean. Por ejemplo el siguiente código es perfectamente
válido:
Boolean b;
// ...
while (b) { // . . .
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
271
Autoboxing y la prevención de errores
// Aquí se produce un error debido al unboxing manual
class UnboxingError {
public static void main(String args []) {
Integer iOb = 1000; // autoboxing del valor 1000
int i = iOb.byteValue(); // ¡unboxing manual como tipo byte !
System.out.println(i); // ¡esto NO desplegará 1000!
}
}
El programa no despliega el valor esperado 1000, en su lugar despliega –24. La razón es que el
valor dentro de la variable iOb pasa por un unboxing manualmente por la llamada al método
byteValue( ), el cual causa el truncamiento del valor 1000 almacenado en iOb. Esto da como
resultado que el valor –24 sea asignado a i. El auto-unboxing previene este tipo de errores
porque el valor en iOb, mediante auto-unboxing, dará lugar a un valor compatible con int.
Comúnmente, autoboxing siempre crea el objeto correcto, y auto-unboxing siempre
produce el valor correcto, no hay forma de que el proceso produzca un tipo de objeto o un valor
incorrecto. En los casos excepcionales donde se requiere un tipo diferente del que es arrojado por
el proceso automático, es posible realizar manualmente boxing y unboxing de los valores. Claro
que, los beneficios del autoboxing y unboxing se perderían. En general, los nuevos programas
deberían utilizar autoboxing y unboxing. Es la forma en que los programas modernos de Java
serán escritos.
Una advertencia sobre el uso autoboxing
Ahora que Java incluye autoboxing y auto-unboxing, podría resultar tentador utilizar objetos
tales como Integer o Double y abandonar a los tipos primitivos del todo. Por ejemplo, con
autoboxing y unboxing es posible escribir código como éste:
// uso incorrecto de autoboxing y unboxing
Double a, b, c;
a = 10.0;
b = 4.0;
c = Math.sqrt(a*a + b*b);
System.out.println("La hipotenusa es: " + c) ;
En este ejemplo, objetos de tipo Double contienen los valores que son usados para calcular la
hipotenusa del triángulo rectángulo. Aunque este código es técnicamente correcto y de hecho
funciona correctamente, hace un muy mal uso del autoboxing y unboxing. Es mucho menos
eficiente que el código equivalente escrito utilizando el tipo primitivo double. La razón es que
cada aplicación de autoboxing y auto-unboxing agrega trabajo adicional que no se presenta
cuando se usan tipos primitivos.
www.detodoprogramacion.com
PARTE I
Además de las facilidades que ofrecen, también ayudan a prevenir errores. Por ejemplo,
consideremos el siguiente programa:
272
Parte I:
El lenguaje Java
En general, el uso de la envoltura de tipos debe restringirse solamente a los casos en
los cuales la representación de un objeto de un tipo primitivo sea requerida. Autoboxing y
unboxing no fueron agregados a Java para eliminar los tipos primitivos.
Anotaciones (metadatos)
A partir de JDK 5, una nueva característica fue agregada a Java la cual permite incrustar
información suplementaria dentro de un archivo fuente. Esta información, llamada anotación,
no cambia las acciones del programa. Una anotación no cambia la semántica de un programa.
Sin embargo, la información de la anotación puede ser usada por varias herramientas durante
las etapas de desarrollo e implementación. Por ejemplo, una anotación puede ser procesada por
un generador de código fuente. El término metadato también es utilizado para referirse a esta
característica, pero el término anotación es más descriptivo y se utiliza más comúnmente.
Fundamentos de las anotaciones
Una anotación se crea a través de un mecanismo basado en una interfaz. Comencemos con un
ejemplo. Aquí está la declaración para una anotación llamada MiAnotacion:
// un tipo simple de anotación
@interface MiAnotacion {
String str();
int val();
}
Primero, observe que la palabra clave interface está precedida por una @. Esto le dice al
compilador que estamos declarando un tipo de anotación. Ahora, observe los dos miembros
str( ) y val( ). Todas las anotaciones consisten únicamente en declaraciones de métodos para los
cuales no se provee cuerpo alguno. Java implementa esos métodos. Además, los métodos actúan
más como campos, como se verá a continuación.
Una anotación no puede incluir una cláusula extends. Sin embargo, todos los tipos de
anotación automáticamente extienden a la interfaz Annotation. La interfaz Annotation es
una super interfaz de todas las anotaciones y está declarada dentro del paquete java.lang.
annotation. La interfaz sobrescribe los métodos hashCode( ), equals( ) y toString( ) definidos
por la clase Object, además define al método annotationType( ) el cual regresa un objeto
de tipo Class que representa a la anotación que hizo la invocación.
Una vez que se ha declarado es posible utilizar la anotación. Cualquier tipo de declaración
puede tener una anotación asociada. Por ejemplo, clases, métodos, campos, parámetros y
constantes enumeradas pueden tener anotaciones asociadas. Incluso una anotación puede tener
anotaciones asociadas. En todos los casos, la anotación precede al resto de la declaración.
Cuando se aplica una anotación, se dan valores a sus miembros. Por ejemplo, a continuación
un ejemplo de la anotación MiAnotacion aplicada a un método:
// Aplicando una anotación a un método
@MiAnotacion (str = "Ejemplo de Anotación", val = 100)
public static void miMetodo() { // …
Esta anotación se liga al método miMetodo( ). Observe cuidadosamente la sintaxis de la
anotación. El nombre de la anotación está precedido por una @ y seguido por una lista de
inicialización de miembros entre paréntesis. Para darle un valor a un miembro, al nombre del
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
Especificación de la política de retención
Antes de explorar más a fondo a las anotaciones, es necesario discutir la política de retención de las
anotaciones. Una política de retención determina en qué punto una anotación es desechada. Java
define tres políticas al respecto, las cuales se encuentran encapsuladas en la enumeración java.
lang.annotation.RetentionPolicy. Dichas políticas son SOURCE, CLASS y RUNTIME.
Una anotación con una política de retención SOURCE es conservada sólo en el archivo
fuente y es descartada durante la compilación.
Una anotación con una política de retención CLASS es almacenada en el archivo .class
durante la compilación. Sin embargo, no está disponible a través de la máquina virtual de Java
durante el tiempo de ejecución.
Una anotación con una política de retención de RUNTIME es almacenada en el archivo
.class durante la compilación y está disponible a través de la máquina virtual de Java durante el
tiempo de ejecución. Así, la retención RUNTIME ofrece la persistencia más grande para una
anotación.
Una política de retención para una anotación se especifica utilizando una de las anotaciones
predefinidas de Java: @Retention. Su forma general se muestra a continuación:
@Retention(política)
Aquí, política debe ser una de las constantes enumeradas discutidas previamente. Si no se
especifica una política de retención para una anotación, la política utilizada por omisión es
CLASS.
La siguiente versión de MiAnotacion utiliza @Retention para especificar la política de
retención RUNTIME. Así, MiAnotacion estará disponible para la máquina virtual de Java
durante la ejecución del programa.
@Retention (RetentionPolicy.RUNTIME)
@interface MiAnotacion{
String str();
int val();
}
Obtención de anotaciones en tiempo de ejecución
Aunque las anotaciones están diseñadas en su mayor parte para ser utilizadas por otras
herramientas de desarrollo e implementación, si se especifica una política de retención de
RUNTIME, entonces pueden ser requeridas en tiempo de ejecución por cualquier programa de
Java utilizando reflexión. La reflexión es la característica que permite que información acerca de la
clase sea obtenida en tiempo de ejecución. El API de reflexión está contenido en el paquete java.
lang.reflect. Existe un gran número de formas de utilizar reflexión y no todas se examinarán
aquí. Sin embargo, veremos algunos ejemplos que aplican a las anotaciones.
El primer paso para usar reflexión es obtener un objeto del tipo Class que represente la
clase a la cual pertenecen las anotaciones que se desean obtener. Class es una de las clases
www.detodoprogramacion.com
PARTE I
miembro se les asigna un valor. Además, en el ejemplo, la cadena “Ejemplo de Anotación” se
asigna al miembro str de MiAnotacion. Nótese que no hay paréntesis después de str en esta
asignación. Cuando a un miembro de la anotación se le da un valor, sólo se escribe su nombre.
Así que los miembros de la anotación parecen campos en este contexto.
273
274
Parte I:
El lenguaje Java
predefinidas en Java, está definida en java.lang, y se describe a detalle en a Parte II de este
libro. Existen varias formas de obtener un objeto de tipo Class, una de las más fáciles es
llamando al método getClass( ), el cuál está definido en la clase Object. Su forma general es:
final Class getClass( )
Esta línea regresa el objeto de tipo Class que representa al objeto invocado. getClass( )
y muchos otros métodos relativos a la reflexión hacen uso de las características de tipos
parametrizados. Sin embargo, dado que la característica de tipos parametrizados será discutida
hasta el Capítulo 14, estos métodos son mostrados y usados aquí en su forma más cruda.
Como resultado, se nos estará presentando un mensaje de advertencia cuando compilemos los
programas siguientes. En el Capítulo 14, aprenderemos sobre los tipos parametrizados.
Después de obtener un objeto de tipo Class, podemos utilizar sus métodos para obtener
información sobre los elementos declarados por la clase, incluyendo sus anotaciones. Si se desea
obtener las anotaciones asociadas con un elemento específico declarado dentro de una clase, se
debe en primer lugar obtener un objeto que representa dicho objeto. Por ejemplo, Class provee
(entre otros) los métodos getMethod( ), getField( ) y getConstructor( ), los cuales obtienen
información acerca de un método, campo y constructor respectivamente. Estos métodos regresan
objetos de tipo Method, Field y Constructor.
Para entender el proceso, trabajemos con un ejemplo que obtiene las anotaciones asociadas
con un método. Para hacer eso, primero se debe obtener un objeto Class que representa la clase
y entonces llamar a getMethod( ) en ese objeto Class, especificando el nombre del método.
getMethod( ) tiene esta forma general:
Method getMethod(String nombreMetodo, Class … parametroTipos)
El nombre del método se pasa a través de nombreMetodo. Si el método tiene argumentos,
entonces será necesario especificar objetos de tipo Class que representen esos tipos utilizando
parametroTipos. Observe que parametroTipos es un parámetro varargs. Esto significa que
se pueden especificar tantos tipos de parámetros como sea necesario, incluyendo cero.
getMethod( ) regresa un objeto Method que representa el método. Si el método no está
presente se lanza una excepción del tipo NoSuchMethodException.
Para los objetos Class, Method, Field o Constructor, se pueden obtener sus anotaciones
asociadas llamando al método getAnnotation( ). Su forma general se muestra a continuación:
Annotation getAnnotation(Class tipoAnotacion)
Donde tipoAnotacion es un objeto de tipo Class que representa a la anotación en la cual estamos
interesados. El método regresa una referencia a la anotación. Utilizando esta referencia, se
pueden obtener los valores asociados con los miembros de la anotación. El método regresa
null si la anotación no es encontrada, lo cual ocurriría si la anotación no tiene una retención
RUNTIME.
A continuación se presenta un programa que ensambla todas las piezas mostradas
anteriormente y utiliza reflexión para mostrar la anotación asociada con un método.
import java.lang.annotation.*;
import java.lang.reflect.*;
// Declaración de un tipo de anotación
@Retention(RetentionPolicy.RUNTIME)
@interface MiAnotacion {
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
275
String str();
int val () ;
class Meta {
// colocar una anotación a un método
@MiAnotacion(str = "Anotación de Ejemplo", val = 100)
public static void miMetodo() {
Meta ob = new Meta();
// Obtener la anotación del método
// y desplegar los valores de sus miembros.
try {
// Primero, se obtiene un objeto de tipo Class que representa
// a la clase
Class c = ob.getClass ();
// Ahora, se obtiene un objeto de tipo Method que representa
// a este método
Method m = c.getMethod ("miMetodo") ;
// Luego, se obtiene la anotación
MiAnotacion a = m.getAnnotation(MiAnotacion.class);
// Finalmente, se muestran los valores
System.out.println(a.str() + " " + a.val ());
} catch (NoSuchMethodException exc) {
System.out.println ("método no encontrado.");
}
}
public static void main (String args []) {
miMetodo () ;
}
}
La salida del programa se muestra a continuación:
Anotación de Ejemplo 100
Este programa utiliza reflexión, como se describió, para obtener y desplegar los valores de
str y val de la anotación MiAnotacion asociada con miMetodo( ) in la clase Meta. Debemos
poner atención en dos aspectos particulares. Primero, en la línea:
MiAnotacion a = m.getAnnotation(MiAnotacion.class);
observe la expresión MiAnotacion.class. Esta expresión proporciona un objeto de tipo Class
para la anotación de tipo MiAnotacion. Esta construcción se denomina literal de clase. Es
posible utilizar este tipo de expresión en cualquier momento que un objeto Class para una clase
conocida sea necesario. Por ejemplo, esta sentencia pudo ser utilizada para obtener el objeto
Class para Meta:
Class c = Meta.class;
Claro está que esto sólo funciona cuando se conoce el nombre de la clase de un objeto de
manera anticipada, lo cual podría no siempre ser el caso. En general, es posible obtener una
literal de clase para clases, interfaces, tipos primitivos y arreglos.
www.detodoprogramacion.com
PARTE I
}
276
Parte I:
El lenguaje Java
El segundo punto de interés es la forma en que los valores asociados con str y val son
obtenidos para ser mostrados por la siguiente línea:
System.out.println(a.str () + " " + a.val() );
Note que son invocados utilizando la sintaxis de llamada a métodos. Esta misma forma se utiliza
para obtener el valor de cualquier miembro de una anotación.
Un segundo ejemplo de reflexión
En el ejemplo anterior, miMetodo( ) no tiene parámetros. Así que cuando se llamó a
getMethod( ) sólo se pasó como parámetro el nombre del método. Sin embargo, para obtener
un método que tiene parámetros se deben especificar objetos de tipo Class representando los
tipos de esos parámetros como argumentos para getMethod( ). Como ejemplo veamos una
versión ligeramente diferente del programa anterior:
import java.lang.annotation.*;
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME)
@interface MiAnotacion {
String str();
int val () ;
}
class Meta {
// miMetodo ahora tiene dos argumentos
@MiAnotacion(str = "Dos parámetros", val = 19)
public static void miMetodo(String str, int i)
{
Meta ob = new Meta() ;
try {
Class c = ob.getClass();
// Aquí se especifican los tipos de los parámetros
Method m = c.getMethod("miMetodo",String.class, int.class);
MiAnotacion a = m.getAnnotation(MiAnotacion.class);
System.out.println(a.str()+" "+ a.val());
} catch (NoSuchMethodException exc) {
System.out.println("método no encontrado.");
}
}
public static void main(String args[]) {
miMetodo("prueba", 10);
}
}
La salida de esta versión se muestra a continuación:
Dos parámetros 19
En esta versión miMetodo( ) toma un parámetro String y un parámetro int. Para obtener
información de este método, getMethod( ) debe ser llamado como se muestra a continuación:
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
277
Method m = c.getMethod("miMetodo", String.class, int.class);
Obteniendo todas las anotaciones
Se pueden obtener todas las anotaciones que tienen retención RUNTIME y que está asociadas
a algún elemento, llamando al método getAnnotations( ) sobre ese elemento. getAnnotations( )
tiene la siguiente forma general:
Annotation[ ] getAnnotations( )
Esto regresa un arreglo con las anotaciones. getAnnotations( ) puede ser llamado por objetos
de tipo Class, Method, Constructor y Field.
Aquí está otro ejemplo de reflexión que muestra como obtener todas las anotaciones
asociadas con una clase y con un método. Se declaran dos anotaciones y luego se utilizan esas
anotaciones en una clase y en un método.
// Mostrar todas las anotaciones de una clase y un método
import java.lang.annotation.*;
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME)
@interface MiAnotacion {
String str();
int val();
}
@Retention(RetentionPolicy.RUNTIME)
@interface What {
String description();
}
@What(description = "Una prueba de anotación para clase")
@MiAnotacion(str = "Meta2", val = 99)
class Meta2 {
@What(description = "Una prueba de anotación en método")
@MiAnotacion(str = "Probando", val = 100)
public static void miMetodo() {
Meta2 ob = new Meta2();
try {
Annotation annos[] = ob.getClass() .getAnnotations();
// Mostrar todas las anotaciones para Meta2.
System.out.println("Todas las anotaciones para Meta2:");
for(Annotation a : annos)
System.out.println(a);
System.out.println();
// Mostrar todas las anotaciones para miMetodo.
Method m = ob.getClass( ).getMethod("miMetodo");
annos = m.getAnnotations();
System.out.println("Todas las anotaciones para miMetodo:");
for(Annotation a : annos)
www.detodoprogramacion.com
PARTE I
donde los objetos Class para String e int son enviados como argumentos adicionales.
278
Parte I:
El lenguaje Java
System.out.println(a) ;
} catch (NoSuchMethodException exc) {
System.out.println("método no encontrado");
}
}
public static void main(String args[]) {
miMetodo () ;
}
}
La salida del programa anterior es:
Todas las anotaciones para Meta2:
@What(description = "Una prueba de anotación para clase")
@MiAnotacion(str = "Meta2", val = 99)
Todas las anotaciones para miMetodo:
@What(description = "Una prueba de anotación en método")
@MiAnotacion(str = "Probando", val = 100)
El programa utiliza getAnnotations( ) para obtener un arreglo con todas las anotaciones
asociadas con la clase Meta2 y con el método miMetodo( ). Como se explicó, getAnnotations( )
regresa un arreglo de objetos Annotation. Recuerde que Annotation es una super-interfaz
de todas las interfaces de anotaciones y que sobrescribe al método toString( ) de la clase Object.
Así, cuando se imprime en pantalla una referencia a una Annotation, se llama al método
toString( ) para generar una cadena que describe a la anotación, como se muestra en la salida
del ejemplo anterior.
La interfaz AnnotatedElement
Los métodos getAnnotation( ) y getAnnotations( ) utilizados en los ejemplos anteriores
se definen en la interfaz AnnotatedElement, la cual está definida en java.lang.reflect. Esta
interfaz proporciona reflexión para anotaciones y es implementada por las clases Method, Field,
Constructor, Class y Package.
Además de getAnnotation( ) y getAnnotations( ), AnnotatedElement define otros dos
métodos. El primero es getDeclaredAnnotations( ), que tiene la siguiente forma general:
Annotation[ ] getDeclaredAnnotations( )
Este método regresa todas las anotaciones no heredadas presentes en el objeto que realiza la
invocación. El segundo método es isAnnotationPresent( ), el cual tiene la siguiente forma
general:
boolean isAnnotationPresent (Class tipoAnotacion)
Éste devuelve verdadero si la anotación especificada por tipoAnotacion está asociada con el objeto
que realiza la invocación, en caso contrario devuelve falso.
NOTA Los métodos getAnnotation( ) y isAnnotationPresent( ) hacen uso de la característica de
tipos parametrizados para garantizar la seguridad de tipos. Dado que los tipos parametrizados serán
revisados hasta el capítulo 14, sus firmas se muestran en este capítulo en su forma más cruda.
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
279
Utilizando valores por omisión
tipo miembro( ) default valor;
Donde, valor debe ser de un tipo compatible con el tipo del miembro.
Esta versión de la anotación @MiAnotacion incluye valores por omisión:
// Declaración de un tipo de anotación que incluye valores por omisión
@Retention(RetentionPolicy.RUNTIME)
@interface MiAnotacion {
String str () default "Probando";
int val() default 9000;
}
Esta declaración da un valor por omisión de “Probando” a str y 9000 a val. Esto significa que
ningún valor necesita ser especificado cuando se utiliza @MiAnotacion. Sin embargo a ambos
se les puede dar valores si se desea. Éstas son las cuatro formas en que @MiAnotacion puede
ser usada:
@MyAnno() // str y val toman valores por omisión
@MyAnno(str = "algún texto") // val toma el valor por omisión
@MyAnno(val = 100) // str toma el valor por omisión
@MyAnno(str = "Probando" , val = 100) // ningún miembro toma valores por
omisión.
El siguiente programa ejemplifica el uso de los valores por omisión en una anotación.
import java.lang.annotation.*;
import java.lang.reflect.*;
// Una declaración de tipo de anotación con valores por omisión en sus miembros
@Retention(RetentionPolicy.RUNTIME)
@interface MiAnotacion {
String str () default "Probando";
int val() default 9000;
}
class Meta3 {
// Aplicando una anotación con valores por omisión a un método
@MiAnotacion ()
public static void miMetodo() {
Meta3 ob = new Meta3();
// Obtener las anotaciones asociadas al método
// y desplegar los valores de sus miembros
try {
Class c = ob.getClass();
Method m = c.getMethod("miMetodo");
www.detodoprogramacion.com
PARTE I
Se pueden dar valores por omisión a los miembros de las anotaciones para que sean utilizados
si no se especifica un valor cuando la anotación es aplicada. Un valor por omisión se especifica
agregando una cláusula default a la declaración de un miembro. La forma general de la
declaración es:
280
Parte I:
El lenguaje Java
MiAnotacion a = m.getAnnotation(MiAnotacion.class);
System.out.println (a.str () + " " + a.val ());
} catch (NoSuchMethodException exc) {
System.out.println("método no encontrado");
}
}
public static void main(String args[]) {
miMetodo() ;
}
}
La salida del código anterior es;
Probando 9000
Anotaciones de marcado
Las anotaciones de marcado son un tipo especial de anotaciones que no contienen miembros. Su
único propósito es marcar una declaración. Es decir, su presencia como anotación es suficiente.
La mejor forma de determinar si una anotación de marcado está presente es utilizando el
método isAnnotationPresent( ), el cual está definido por la interfaz AnnotatedElement.
A continuación un ejemplo que usa una anotación de marcado. Debido a que una interfaz
de marcado no contiene miembros, el sólo determinar si está presente o no es suficiente.
import java.lang.annotation.*;
import java.lang.reflect.*;
// Una anotación de marcado
@Retention(RetentionPolicy.RUNTIME)
@interface MyMarker { }
class Marker {
// Aplicamos la anotación anterior sobre un método
// Observe que los paréntesis no son necesarios
@MyMarker
public static void miMetodo() {
Marker ob = new Marker() ;
try {
Method m = ob. getClass ().getMethod ("miMetodo") ;
// Se determina si la anotación está presente
if(m.isAnnotationPresent(MyMarker.class))
System.out.println("La anotación de marcado está presente");
} catch (NoSuchMethodException exc) {
System.out.println("método no encontrado");
}
}
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
}
La salida de este programa, mostrada a continuación, confirma que @MyMaker está presente:
La anotación de marcado está presente
En el programa, observe que no se necesita colocar paréntesis al lado de @MyMaker al
aplicarlo. Así, @MyMaker es aplicado simplemente anotando su nombre, como sigue:
@MyMaker
No está mal suministrar un par de paréntesis vacíos, pero no son necesarios.
Anotaciones de un solo miembro
Las anotaciones de un solo miembro contienen solamente un miembro y funcionan como una
anotación normal excepto por el hecho de que permiten una forma corta de especificar el valor
del miembro. Cuando solamente un miembro está presente, es posible especificar simplemente
el valor para dicho miembro cuando la anotación es aplicada y no es necesario especificar el
nombre del miembro. Sin embargo, para el uso de esta forma corta, el nombre del miembro debe
ser la palabra value tal cual.
El siguiente ejemplo crea y usa una anotación de un solo miembro:
import java.lang.annotation.*;
import java.lang.reflect.*;
// Una anotación de un solo miembro
@Retention(RetentionPolicy.RUNTIME)
@interface MySingle {
int value(); // el nombre de esta variable debe ser value
}
class Single {
// Se aplica la anotación anterior sobre un método.
@MySingle(l00)
public static void miMetodo() {
Single ob = new Single();
try {
Method m = ob.getClass().getMethod("miMetodo");
MySingle anno = m.getAnnotation(MySingle.class);
System.out.println(anno.value()); // mostrará 100
} catch (NoSuchMethodException exc) {
System.out.println("método no encontrado");
}
}
www.detodoprogramacion.com
PARTE I
public static void main(String args[]) {
miMetodo() ;
}
281
282
Parte I:
El lenguaje Java
public static void main(String args[]) {
miMetodo();
}
}
Como era de esperarse, este programa despliega el valor de 100. En el programa, @MySingle se
utiliza en miMetodo( ), como se muestra a continuación:
@MySingle(100)
Observe que el signo = no necesita ser especificado.
Es posible utilizar la misma sintaxis para anotaciones que tiene más miembros, pero todos
los miembros adicionales deben tener valores por omisión. Por ejemplo, aquí agregamos al
miembro xyz con un valor por omisión de cero.
@interface UnaAnotacion{
int value();
int xyz() default 0;
}
En los casos donde se desea usar el valor por omisión de xyz, se puede aplicar @unaAnotacion,
como se muestra a continuación, simplemente especificando el valor del miembro value
utilizando la sintaxis de anotación con un solo miembro.
@UnaAnotacion(88)
En este caso, xyz tiene el valor por omisión de cero, y value tiene el valor 88. Claro está, que
especificar un valor diferente para xyz requiere que ambos miembros sean explícitamente
nombrados, como se muestra a continuación
@UnaAnotacion(value = 88, xyz = 99)
Recuerde, en cualquier momento que se esté utilizando la anotación de un solo miembro, el
nombre de dicho miembro debe ser value.
Anotaciones predefinidas en Java
Java define varias anotaciones predefinidas. La mayoría son especializadas, pero siete son de
propósito general. De esas siete, cuatro son importadas de java.lang.annotation: @Retention,
@Documented, @Target, y @Inherited. Tres, @Override, @Deprecated y @SupressWarnings
están incluidas en java.lang. Cada una se describe a continuación.
@Retention
@Retention está diseñada para ser usada sólo como una anotación a otra anotación. Ésta
especifica la política de retención como se describió anteriormente en este capítulo.
@Documented
La anotación @Documented es una interfaz de marcado que le dice a una herramienta que
una anotación sea documentada. Está diseñada para ser usada solo como una anotación
para una declaración de anotación.
@Target
La anotación @Target especifica el tipo de declaraciones en las cuales una anotación puede ser
aplicada. Está diseñada para ser utilizada únicamente como una anotación a otra anotación.
www.detodoprogramacion.com
Capítulo 12:
Enumeraciones, autoboxing y anotaciones (metadatos)
Constante
La anotación puede ser aplicada a
ANNOTATION_TYPE
Otra anotación
CONSTRUCTOR
Constructor
FIELD
Campo
LOCAL_VARIABLE
Variable local
METHOD
Método
PACKAGE
Paquete
PARAMETER
Parámetro
TYPE
Clase, interfaz o enumeración
Se puede especificar uno o más de estos valores en una anotación @Target. Para especificar
múltiples valores, se deben especificar en una lista delimitada con llaves. Por ejemplo, para
especificar que una anotación aplica sólo a campos y variables locales, se puede usar la siguiente
anotación:
@Target ( {ElementType.FIELD, ElementType.LOCAL_VARIABLE})
@Inherited
@Inherited es una anotación de marcado que puede ser usada solo sobre declaraciones
de anotaciones y afecta únicamente a anotaciones que serán utilizadas en la declaración de
clases. @Inherited causa que la anotación de una super clase sea heredada por sus subclases.
Por consiguiente, cuando una solicitud por una anotación específica es hecha a la subclase,
si la anotación no está presente en la subclase entonces se revisará en la superclase. Si en la
superclase está presente la anotación y además tiene especificada la anotación @Inherited,
entonces la anotación se devolverá como resultado de la solicitud.
@Override
@Override es una anotación de marcado y puede ser utilizada sólo en métodos. Un método
anotado con @Override debe sobrescribir un método de una superclase. Si no existe, se tendrá
como resultado un error en tiempo de compilación. Esto se utiliza para garantizar que un
método de una superclase es sobrescrito, y no solo sobrecargado.
@Deprecated
@Deprecated es una anotación de marcado que indica que una declaración es obsoleta y ha sido
reemplazada por una nueva forma.
@SupressWarnings
@SupressWarnings especifica que una o más advertencias que podrían ser generadas por el
compilador serán suprimidas. Las advertencias a suprimir son especificadas por su nombre en
una cadena. Esta anotación puede ser aplicada a cualquier tipo de declaración.
www.detodoprogramacion.com
PARTE I
@Target toma un argumento, el cual debe ser una constante de la enumeración ElementType.
Este argumento especifica el tipo de declaración en el cual la anotación puede ser aplicada. Las
constantes se muestran junto con el tipo de declaración al cual corresponden.
283
284
Parte I:
El lenguaje Java
Restricciones para las anotaciones
Existen algunas restricciones que se aplican a la declaración de anotaciones. Primero, ninguna
anotación puede heredar de otra. Segundo, todos los métodos declarados por una anotación
deben ser métodos sin parámetros. Además, deben devolver lo siguiente:
• Un tipo primitivo, como un int o un double.
• Un objeto de tipo String o Class.
• Un tipo enum.
• Otro tipo de anotación.
• Un arreglo de elementos de uno de los tipo mencionados anteriormente.
Las anotaciones no pueden utilizar tipos parametrizados. Los tipos parametrizados se describen
en el Capítulo 14. Finalmente, los métodos de una anotación no pueden especificar la cláusula
throws.
www.detodoprogramacion.com
13
CAPÍTULO
E/S, applets y otros temas
E
ste capítulo introduce dos de los paquetes más importantes de Java: io y applet. El paquete io
contiene el sistema básico de E/S (entradas/salidas) de Java, incluyendo la E/S con archivos. El
paquete applet gestiona los applets. La gestión de las E/S y de los applets se realiza mediante
bibliotecas del API de Java, y no mediante palabras reservadas del lenguaje. Por este motivo, en la
Parte II de este texto, que examina la interfaz de las clases de Java, se analizan en profundidad estos
dos tópicos. Este capítulo examina las bases de estos dos subsistemas, de forma que se ve cómo se
integran en el lenguaje Java, tanto en la programación como en su entorno de ejecución. Este capítulo
también examina las últimas palabras claves de Java: transient, volatile, instanceof, native, strictfp
y assert. Y concluye examinando la combinación de palabras clave static import y un uso adicional
de la palabra clave this.
Fundamentos de E/S
En los programas ejemplo que aparecen en los anteriores doce capítulos no se ha hecho mucho uso
del subsistema de E/S. De hecho, en dichos ejemplos, aparte de los métodos print( ) y println( ), no
se ha usado de manera significativa ningún otro de los métodos de E/S. La razón es simple y es que
en la mayor parte de las aplicaciones reales de Java no se utilizan programas cuya salida sea basada
en texto por consola, sino que son aplicaciones gráficas que basan su interacción con el usuario en
un conjunto de herramientas gráficas denominado AWT (por sus siglas en inglés, Abstract Window
Toolkit) o Swing. Aunque los programas con salida basada en texto son ideales en la enseñanza
del lenguaje, no se utilizan en programas reales. Además, la E/S por consola es bastante limitada y
engorrosa incluso en programas sencillos. Por todo ello, la E/S basada en texto por consola no es muy
importante en la programación en Java.
A pesar de lo expuesto en el párrafo anterior, Java proporciona un sistema de E/S completo y
flexible en lo referente a archivos y redes. El sistema de E/S de Java es coherente y consistente. De
hecho, una vez que se entienden sus fundamentos, el resto del sistema de E/S se domina fácilmente.
Flujos
Los programas en Java realizan las E/S a través de flujos. Un flujo es una abstracción de una entidad
que produce o consume información. Un flujo está ligado a un dispositivo físico por el sistema de
E/S de Java. Todos los flujos se comportan de igual manera, incluso en el caso de que los dispositivos
285
www.detodoprogramacion.com
286
Parte I:
El lenguaje Java
físicos reales a los que están ligados sean diferentes. Por lo tanto, las mismas clases y métodos
de E/S se pueden aplicar a cualquier tipo de dispositivo. Esto significa que un flujo de entrada se
puede utilizar para distintos tipos de entrada: un archivo de disco, el teclado o una conexión de
red. Del mismo modo, un flujo de salida se puede referir a la consola, a un archivo de disco o a
una conexión de red. Los flujos son una forma clara y sencilla de tratar las entradas/salidas sin
que el código tenga que tener en cuenta, por ejemplo, si el dispositivo es un teclado o la red. Java
implementa los flujos en una jerarquía de clases definida en el paquete java.io.
Flujos de bytes y flujos de caracteres
Java define dos tipos de flujos: de bytes y de caracteres. Los flujos de bytes proporcionan un
medio conveniente para gestionar la entrada y salida de bytes. Los flujos de bytes se utilizan, por
ejemplo, cuando se escriben o leen datos binarios. Los flujos de caracteres, por el contrario, son
adecuados para gestionar la entrada y salida de caracteres. Utilizan el código Unicode y, por lo
tanto, se pueden utilizar internacionalmente. En algunos casos, los flujos de caracteres pueden
ser más eficientes que los flujos de bytes.
La versión inicial de Java (Java 1.0) no incluía el flujo de caracteres; esto implica que todas
la E/S estaban orientadas a byte. El flujo de caracteres fue añadido por Java 1.1. Y esto hizo que
se desecharan algunas clases y métodos orientados a byte. Por este motivo, en algunos casos,
resulta apropiado actualizar códigos antiguos que no utilizaban el flujo de caracteres, para
aprovechar la ventaja que éste tiene.
Conviene también tener en cuenta que, en el más bajo nivel, todas las E/S están orientadas
bytes. El flujo basado en caracteres simplemente proporciona un medio conveniente y eficaz para
el manejo de estos.
En los siguientes apartados se presenta una visión general de los flujos orientados a byte y
de los flujos orientados a carácter.
Las clases de flujos de bytes
Los flujos de bytes se definen mediante dos jerarquías de clases. En el nivel superior hay dos
clases abstractas: InputStream y OutputStream. Cada una de estas clases abstractas tiene
varias subclases no abstractas que gestionan las diferencias entre los diversos dispositivos tales
como, archivos de disco, conexiones de red, e incluso espacios de memoria. Las clases referentes
a flujos de bytes se muestran en la Tabla 13-1. Sólo unas pocas de estas clases se discuten más
adelante, en este apartado. Las demás se describen en la Parte II. Recuerde que para utilizar las
clases de flujos se debe importar el paquete java.io.
Las clases abstractas InputStream y OutputStream definen varios métodos que las
otras clases implementan. Dos de los más importantes son los métodos read( ) y write( ), que
permiten, respectivamente, leer y escribir bytes de datos. Ambos métodos se declaran como
abstractos dentro de InputStream y OutputStream y son sobrescritos en las clases derivadas.
Las clases de flujos de caracteres
Los flujos de caracteres se definen mediante dos jerarquías de clases. En el nivel más alto se
encuentran las clases abstractas, Reader y Writer. Estas clases gestionan el flujo de caracteres
Unicode. Java define varias subclases de estas dos clases. Las clases referentes a flujos de
caracteres se muestran en la Tabla 13-2.
Las clases abstractas Reader y Writer definen varios métodos que las otras clases
implementan. Dos de los métodos más importantes son los métodos read( ) y write( ), que
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
Clase
Significado
BufferedInputStream
Flujo de entrada con buffer
BufferedOutputStream
Flujo de salida con buffer
ByteArrayInputStream
Flujo de entrada que lee desde un arreglo de bytes
ByteArrayOutputStream
Flujo de salida que escribe en un arreglo de bytes
DataInputStream
Flujo de entrada que tiene métodos para leer los tipos primitivos o
básicos de Java
DataOutputStream
Flujo de salida que tiene métodos para escribir los tipos primitivos o
básicos de Java
FileInputStream
Flujo de entrada que lee desde un archivo
FileOutputStream
Flujo de salida que escribe en un archivo
FilterInputStream
Implementa InputStream
FilterOutputStream
Implementa OutputStream
InputStream
Clase abstracta que define un flujo de entrada
ObjectInputStream
InputStream para objetos
ObjectOutputStream
OutputStream para objetos
OutputStream
Clase abstracta que define un flujo de salida
PipeInputStream
Canal de entrada
PipeOutputStream
Canal de salida
PrintStream
Flujo de salida que contiene los métodos print() y println()
PushbackInputStream
Flujo de entrada que permite -cuando se ha leído un byte- que se
devuelva de nuevo al flujo de entrada
RandomAccessFile
Permite el acceso aleatorio a un archivo de E/S
SequenceInputStream
Flujo de entrada que es una combinación de dos o más flujos de entrada
que serán leídos secuencialmente, uno después de otro
TABLA 13-1 Clases de flujos de bytes
Clase
Significado
BufferedReader
Flujo de entrada de caracteres con buffer
BufferedWriter
Flujo de salida de caracteres con buffer
CharArrayReader
Flujo de entrada que lee desde un arreglo de caracteres
CharArrayWriter
Flujo de salida que escribe en un arreglo de caracteres
FileReader
Flujo de entrada que lee desde un archivo
FileWriter
Flujo de salida que escribe en un archivo
FilterReader
Filtro de lectura
FilterWriter
Filtro de escritura
TABLA 13-2 Clases de flujos de caracteres
www.detodoprogramacion.com
PARTE I
sirven para leer y escribir caracteres, respectivamente. Estos métodos son sobrescritos en las
clases derivadas.
287
288
Parte I:
El lenguaje Java
Clase
Significado
InputStreamReader
Flujo de entrada que convierte bytes a caracteres
LineNumberReader
Flujo de entrada que cuenta las líneas
OutputStreamWriter
Flujo de salida que convierte caracteres a bytes
PipedReader
Canal de entrada
PipedWriter
Canal de salida
PrintWriter
Flujo de salida que contiene los métodos print() y println()
PushbackReader
Flujo de entrada que permite regresar caracteres a un flujo de entrada
Reader
Clase abstracta que define un flujo de entrada de caracteres
StringReader
Flujo de entrada que lee desde un String
StringWriter
Flujo de salida que escribe en un String
Writer
Clase abstracta que define un flujo de salida de caracteres
TABLA 13-2 Clases de flujos de caracteres (continuación)
Flujos predefinidos
Como sabemos, todos los programas de Java importan automáticamente el paquete java.lang.
Este paquete define una clase denominada System, que encapsula algunos aspectos del entorno
de ejecución. Por ejemplo, utilizando algunos de sus métodos, se puede obtener la hora actual o
los valores de diversas propiedades asociadas al sistema. System también contiene tres variables
con flujos predefinidos: in, out y err. Estos campos se declaran como public, static y final en
la clase System. Esto significa que pueden ser utilizadas por cualquier parte del programa sin
necesidad de una referencia a un objeto específico de tipo System.
System.out hace referencia al flujo de salida estándar, que, por omisión, es la consola.
System.in se refiere al flujo de entrada estándar, que, por omisión, es el teclado. System.err se
refiere al flujo de error estándar, que, también por defecto, es la consola. Sin embargo, cualquiera
de estos flujos puede ser redirigido a cualquier dispositivo compatible de E/S.
System.in es un objeto del tipo InputStream; System.out y System.err son objetos del
tipo PrintStream. Todos estos son flujos de bytes, aunque se utilizan normalmente para leer y
escribir caracteres desde y en la consola. Como se verá, estos flujos se pueden envolver en flujos
basados en caracteres, si se desea.
En los capítulos anteriores ya se ha utilizado System.out en los ejemplos. Se puede utilizar
System.err de la misma forma, pero, como se explica en el próximo apartado, el uso de System.
in es un poco más complicado.
Entrada por consola
En Java 1.0, la única forma de realizar la entrada por consola era mediante un flujo de bytes,
y un código que utilice este enfoque sigue siendo válido. Hoy en día, utilizar un flujo de bytes
para leer una entrada por consola es todavía técnicamente posible, pero este procedimiento
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
BufferedReader(Reader entrada)
Donde entrada es el flujo que será ligado a la instancia de BufferedReader que se está siendo
creada. Reader es una clase abstracta. Una de sus subclases concretas es InputStreamReader,
que convierte bytes en caracteres. Para obtener un objeto InputStreamReader ligado a System.
in, se utiliza el siguiente constructor:
InputStreamReader (InputStream entrada)
Como System.in se refiere a un objeto del tipo InputStream, se puede utilizar en el lugar de
entrada. La siguiente línea de código realiza las dos acciones anteriores para crear un objeto
BufferedReader conectado al teclado:
BufferedReader br = new BufferedReader ( new
InputStreamReader(System.in));
Después de la ejecución de esta sentencia, br es un flujo basado en caracteres ligado a la consola
a través de System.in.
Lectura de caracteres
Para leer un carácter desde un BufferedReader, se utiliza el método read( ). La versión de
read( ) que utilizaremos es:
int read( ) throws IOException
Cada vez que se llame a read( ), este método lee un carácter del flujo de entrada y lo devuelve
como un valor entero. Cuando encuentra el final del flujo, devuelve el valor -1. También puede
lanzar una excepción del tipo IOException.
El siguiente programa muestra un ejemplo en el que se utiliza el método read( ) para
leer caracteres de la consola hasta que el usuario pulsa la letra “q”. Observe que cualquier
excepción de E/S que se genere es lanzada fuera de main( ). Este manejo es común cuando
se lee información de la consola, aunque la excepción puede ser gestionada, si se desea.
// Uso de un BufferedReader para leer caracteres de la consola.
import java.io.*;
class BRRead {
public static void main(String args[])
throws IOException
{
char c;
BufferedReader br = new
www.detodoprogramacion.com
PARTE I
no es recomendable. En Java 2, se aconseja utilizar un flujo basado en caracteres para leer una
entrada por consola, ya que de esta forma resulta más sencillo internacionalizar y mantener el
código.
En Java, la entrada por consola se lleva a cabo leyendo de System.in. Para obtener
un flujo basado en caracteres conectado a la consola, se envuelve System.in en un objeto
BufferedReader. BufferedReader proporciona un buffer para el flujo de entrada. El constructor
que se utiliza comúnmente es:
289
290
Parte I:
El lenguaje Java
BufferedReader(new InputStrearnReader(System.in));
System.out.println("Introduzca caracteres, pulse 'q' para salir.");
// lectura de caracteres
do{
c = (char) br.read();
System.out.println(c);
} while (c!= 'q');
}
}
Como ejemplo de ejecución de este programa, se presenta la siguiente salida:
Introduzca caracteres, pulse 'q' para salir.
123abcq
1
2
3
a
b
c
q
Esta salida puede ser ligeramente distinta de la esperada. Esto es así porque, por omisión,
System.in es un flujo con buffer. Esto significa que realmente no se pasa ninguna entrada al
programa hasta que no se pulsa la tecla ENTER. Como se puede imaginar, esto hace que read( )
no sea de mucha utilidad para la entrada interactiva por consola.
Lectura de cadenas
Para leer una cadena desde el teclado, se usa la versión del método readLine( ) que es miembro
de la clase BufferedReader. Su forma general es la siguiente:
String readLine( ) throws IOException
Como se puede ver, este método devuelve un objeto String.
El siguiente programa es un ejemplo del uso de la clase BufferedReader y del método
readLine( ); el programa lee e imprime líneas de texto hasta que se escribe la palabra “stop”:
// Lectura de una cadena desde la consola utilizando la clase BufferedReader.
import java.io.*;
class BRReadLines {
public static void main(String args[])
throws IOException
{
// Se crea un objeto BufferedReader usando System.in
BufferedReader br = new BufferedReader(new
InputStreamReader(System.in));
String str;
System.out.println("Introduzca las líneas de texto.");
System.out.println("Introduzca 'stop' para salir.");
do{
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
291
str = br.readLine();
}
}
El siguiente ejemplo crea un pequeño editor de texto utilizando un arreglo de objetos
String, y después lee líneas de texto, almacenándolas en el arreglo. Leerá hasta un máximo de
100 líneas o hasta que se introduzca la palabra “stop”. En el ejemplo se utiliza un objeto de la
clase BufferedReader para leer de la consola.
// Un pequeño editor.
import java.io.*;
class PequeñoEditor {
public static void main(String args[])
throws IOException
{
// se crea un BufferedReader usando System.in
BufferedReader br = new BufferedReader(new
InputStreamReader(System.in));
String str[] = new String[l00];
System.out.println("Introduzca las líneas de texto.");
System.out.println("Introduzca 'stop' para salir.");
for(int i=0; i
Las sentencias width y height especifican las dimensiones del área de la pantalla utilizada
por el applet. (La etiqueta APPLET contiene otras opciones que se analizarán con más detalle
en la Parte II). Después de crear el archivo, se ejecuta el navegador y se carga este archivo. Como
resultado se ejecuta SimpleApplet.
Para ejecutar SimpleApplet con un visualizador de applets, se ejecuta también el archivo
HTML anterior. Por ejemplo, si el archivo HTML se llama RunApp.html, entonces la siguiente
línea de comandos servirá para ejecutar SimpleApplet:
C:\>appletviewer RunApp.html
Sin embargo, se puede utilizar un método mejor para realizar pruebas rápidamente. Este
método consiste en incluir un comentario en la cabecera del archivo fuente de Java con la
etiqueta APPLET. De esta forma, el código está documentado con un prototipo de las sentencias
HTML necesarias, y se puede probar el applet compilado, iniciando simplemente el visualizador
de applets con el archivo de código fuente Java. Si se utiliza este método, el archivo fuente
SimpleApplet sería el siguiente:
import java.awt.*;
import java.applet.*;
/*
*/
public class SimpleApplet extends Applet {
public void paint(Graphics g) {
g.drawString("Un applet sencillo", 20, 20);
}
}
De esta forma, se pueden desarrollar rápidamente applets siguiendo estos tres pasos:
1. Editar el archivo fuente Java.
2. Compilar el programa.
3. Ejecutar el visualizador de applets, especificando el nombre del archivo fuente del applet.
El visualizador encontrará la etiqueta APPLET en el comentario y ejecutará el applet.
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
299
En la siguiente ilustración de muestra la ventana generada por SimpleApplet:
PARTE I
Aunque los applets se discuten con más profundidad más adelante en este libro, los puntos
clave que conviene recordar hasta este momento son:
• Los applets no necesitan un método main( ).
• Los applets se ejecutan con un visualizador de applets o un navegador compatible con
Java.
• Para las E/S del usuario no se utilizan las clases de flujo de E/S de Java. En su lugar, se
utiliza la interfaz que proporciona AWT o Swing.
Los modificadores transient y volatile
Java define dos modificadores de tipo interesantes: transient y volatile. Estos modificadores se
utilizan para tratar situaciones específicas.
Cuando una variable de instancia se declara como transient, entonces no es necesario
mantener su valor cuando el objeto se almacena. Por ejemplo:
class T {
transient int a; // no persistente
int b; // persistente
}
Si un objeto del tipo T se guarda en un área de almacenamiento persistente, el contenido de a
no se guardaría, mientras que el de b sí.
El modificador volatile indica al compilador que la variable modificada por volatile se
puede cambiar de forma inesperada por otras partes del programa. Una de estas situaciones
está relacionada con los programas multihilos, tal y como se ha visto en un ejemplo del
Capítulo 11. En ocasiones, en un programa multihilo, dos o más hilos comparten la misma
variable de instancia. Por razones de eficacia, cada hilo puede guardar su propia copia de
la variable compartida. La copia real o maestra de la variable se actualiza en diferentes
instantes, como por ejemplo cuando entra en un método synchronized. Si bien este
enfoque puede funcionar correctamente, también puede ser ineficiente en ocasiones. Lo que
importa realmente es que la copia maestra de la variable refleje siempre el estado actual.
Para garantizar esto, simplemente hay que especificar la variable como volatile, que indica
al compilador que siempre debe utilizar la copia maestra de una variable volatile (o, por lo
menos, mantener siempre actualizadas las copias privadas respecto a la maestra, y viceversa).
Además, los accesos a la variable maestra se deben ejecutar en el mismo orden en que se
ejecutan sobre cualquier copia privada.
www.detodoprogramacion.com
300
Parte I:
El lenguaje Java
instanceof
En ocasiones es útil conocer el tipo de un objeto en tiempo de ejecución. Por ejemplo, si
tuviéramos un hilo de ejecución que genera varios tipos de objetos, y otro hilo que procesa esos
objetos. En esta situación, es interesante para el hilo que procesa conocer el tipo de cada objeto
cuando lo recibe. Otra situación en la que es importante conocer el tipo de un objeto en tiempo
de ejecución es la de la conversión de tipos. En Java, una conversión inválida da lugar a un error
en tiempo de ejecución. En el tiempo de compilación es factible detectar muchas conversiones
inválidas, sin embargo, las conversiones en las que están implicadas jerarquías de clases pueden
dar lugar a conversiones inválidas que sólo se pueden detectar en la ejecución. Por ejemplo,
imaginemos una superclase, denominada A que produce dos subclases denominadas B y C. Se
puede convertir un objeto del tipo B al tipo A, o convertir un objeto del tipo C al tipo A, pero
no está permitido convertir un objeto del tipo B al tipo C o viceversa. Como un objeto del tipo
A puede referirse a objetos del tipo B o C, ¿cómo se puede saber, durante la ejecución, qué tipo
de objeto es el que realmente está siendo referenciado antes de intentar la conversión al tipo C?
Podría ser un objeto de los tipos A, B o C. Si es del tipo B, se lanzará una excepción en tiempo de
ejecución. Java proporciona el operador instanceof para responder a esta cuestión.
El operador instanceof tiene la siguiente forma general:
objeto instanceof tipo
donde objeto es una instancia de una clase, y tipo es una clase. Si objeto es del tipo especificado
o se puede convertir en ese tipo, entonces el operador instanceof dará como resultado el valor
true. En caso contrario, su resultado es false. Por lo tanto, instanceof es el medio por el cual el
programa puede obtener información sobre un objeto en tiempo de ejecución.
El siguiente programa muestra la utilización del operador instanceof:
// Ejemplo del operador instanceof.
c1ass A {
int i, j;
}
c1ass B {
int i, j;
}
class C extends A {
int k;
}
class D extends A {
int k;
}
class InstanceOf {
public static void main(String args[]) {
A a = new A();
B b = new B();
C c = new C();
D d = new D();
www.detodoprogramacion.com
Capítulo 13:
es una instancia de A");
es una instancia de B");
es una instancia de C");
se puede convertir a A");
if(a instanceof C)
System.out.println{"a se puede convertir a C");
System.out.print1n();
// Comparar los tipos de tipos derivados
A ob;
ob = d; // una referencia a d de tipo A
System.out.print1n("ob hace referencia a d");
if(ob instanceof D)
System.out.print1n("ob es una instancia de D");
System.out.print1n();
ob = c; // una referencia a c de tipo A
System.out.print1n("ob hace referencia a c");
if(ob instanceof D)
System.out.println("ob se puede convertir a D");
else
System.out.println("ob no se puede convertir a D");
if(ob instanceof A)
System.out.print1n("ob se puede convertir a A");
System.out.print1n();
// todos los objetos se
if(a instanceof Object)
System.out.print1n("a
if(b instanceof Object)
System.out.println("b
if(c instanceof Object)
System.out.println("c
if(d instanceof Object)
System.out.println("d
301
pueden convertir en Object
se puede convertir a Object");
se puede convertir a Object");
se puede convertir a Object");
se puede convertir a Object");
}
}
La salida de este programa es la siguiente:
a es una instancia de A
b es una instancia de B
c es una instancia de C
c se puede convertir a A
www.detodoprogramacion.com
PARTE I
if(a instanceof A)
System.out.print1n("a
if(b instanceof B)
System.out.println("b
if{c instanceof C)
System.out.print1n{"c
if(e instanceof A)
System.out.print1n{"c
E/S, applets y otros temas
302
Parte I:
El lenguaje Java
ob hace referencia a d
ob es una instancia de D
ob hace referencia a c
ob no se puede convertir a D
ob se puede convertir a A
a
b
c
d
se
se
se
se
puede
puede
puede
puede
convertir
convertir
convertir
convertir
a
a
a
a
Object
Object
Object
Object
El operador instanceof no se necesita en la mayoría de programas, ya que, generalmente, se
conoce el tipo de objetos con los que se está trabajando. Sin embargo, puede ser muy útil cuando
se escriben rutinas generalizadas que operan sobre objetos de una jerarquía de clases compleja.
strictfp
Java 2 ha añadido una palabra clave nueva al lenguaje Java, denominada strictfp. Con
la creación de Java 2, el modelo de cálculo en punto flotante se ha relajado ligeramente.
Específicamente, el nuevo modelo no requiere el truncamiento de ciertos valores intermedios
que se producen durante los cálculos. Modificando una clase o método con la palabra clave
strictfp, se asegura que los cálculos en punto flotante, y por tanto todos los truncamientos,
se efectúen del mismo modo que en las versiones anteriores de Java. Cuando se modifica una
clase con strictfp, automáticamente se modifican todos los métodos de la clase con strictfp.
El siguiente fragmento, por ejemplo, indica a Java que utilice el modelo original de punto
flotante para los cálculos en todos los métodos definidos en MiClase:
strictfp class MiClase { //...
Muchos programadores no utilizan nunca strictfp, porque afecta únicamente a un pequeño
grupo de problemas.
Métodos nativos
Aunque poco frecuente, ocasionalmente puede ser necesario llamar a una subrutina escrita en
otro lenguaje distinto de Java. Normalmente, esa subrutina existe como un código ejecutable
para la CPU y el entorno de trabajo, es decir, código nativo. Por ejemplo, puede ser conveniente
llamar a una subrutina de código nativo para lograr un tiempo de ejecución más rápido, o
bien puede ser necesario utilizar una biblioteca especializada, como un paquete estadístico.
Sin embargo, como los programas Java se compilan en un código binario que es interpretado
después por el intérprete Java, podría parecer imposible llamar a una subrutina de código nativo
desde un programa Java. Afortunadamente, esta conclusión es falsa.
Java facilita la palabra clave native que se utiliza para declarar métodos de código nativo.
Una vez declarados, se puede llamar a estos métodos desde el programa Java del mismo modo
que se llama cualquier otro método de Java.
Para declarar un método nativo, se coloca el nombre del método precedido por el
modificador native, pero no se define ningún cuerpo para el método. Por ejemplo:
public native int meth ();
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
NOTA
Los pasos precisos que se han de seguir varían según las diferentes versiones y entornos de
Java. También dependen del lenguaje en el que esté implementado el código nativo. La siguiente
discusión considera un entorno Windows. El lenguaje en el que se implementa el método nativo
es C.
La manera más fácil de entender el proceso es por medio de un ejemplo. Para comenzar
veamos un programa corto que utiliza un método nativo denominado test( ):
// Un ejemplo sencillo que utiliza métodos nativos.
public class NativeDemo {
int i;
public static void main(String args[]) {
NativeDemo ob = new NativeDemo();
ob.i = 10;
System.out.println("Esto es ob.i antes del método nativo:" +
ob.i) ;
ob.test(); // llamada a un método nativo
System.out.println("Esto es ob.i después del método nativo:" +
ob.i);
}
// Declaración del método nativo
public native void test();
// carga la DLL que contiene el método estático
static{
System.loadLibrary("NativeDemo");
}
}
Observe que el método test( ) se declara como native y no tiene cuerpo. Este método será
implementado en C. Observe también el bloque static. Como se ha explicado anteriormente en
este libro, un bloque static se ejecuta una sola vez, cuando el programa comienza la ejecución
o, más precisamente, cuando se carga por primera vez su clase. En este caso, se utiliza para
cargar la biblioteca de enlace dinámico (DLL) que contiene la implementación nativa de test( ).
Posteriormente se verá cómo se crea esta biblioteca.
La biblioteca se carga utilizando el método loadLibrary( ), que es parte de la clase System.
Su forma general es:
static void loadLibrary(String nombreArchivo)
Donde nombreArchivo es una cadena que especifica el nombre del archivo que contiene la
biblioteca. En el entorno Windows se supone que este archivo tiene la extensión. DLL.
www.detodoprogramacion.com
PARTE I
Después de declarar un método nativo, se debe escribir el método y seguir una serie compleja de
pasos para enlazado con el código Java.
La mayor parte de los métodos nativos están escritos en C. El mecanismo que se utiliza para
integrar código C con programas Java se denomina Interfaz Nativa de Java (JNI, por sus siglas en
inglés, Java Native Interface). Una descripción detallada de la INI está más allá de los propósitos
de este libro, pero la siguiente descripción proporciona información suficiente en la mayoría de
las aplicaciones.
303
304
Parte I:
El lenguaje Java
Cuando se compila el programa Java, se genera el archivo NativeDemo.cIass. A
continuación se debe utilizar javah.exe para generar el archivo: NativeDemo.h. (javah.exe está
incluido en JDK.) En la implementación de test( ) habrá que incluir el archivo NativeDemo.h.
Para generar este archivo se utiliza el siguiente comando:
javah -jni NativeDemo
Este comando genera un archivo cabecera denominado NativeDemo.h. Este archivo debe
incluirse en el archivo C que implementa test( ). La salida generada por este comando es la
siguiente:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class NativeDemo */
#ifndef _Inc1uded_NativeDemo
#define _Included_NativeDemo
#ifdef _ _cplusplus
extern "C" {
#endif
/*
* Class: NativeDemo
* Method: test
* Signature: ()V
*/
JNIEXPORT void JNlCALL Java_NativeDemo_test
(JNIEnv *, jobject);
#ifdef _ _cplusplus
}
#endif
#endif
Prestemos especial atención a la siguiente línea, que define el prototipo para la función
test( ):
JNIEXPORT void JNICALL Java_NativeDemo_test(JNIEnv *, jobject);
Observe que el nombre de la función es Java_NativeDemo_test( ). Éste es el nombre que se
debe utilizar para la función, es decir, en lugar de crear una función C denominada test( ), se
creará una función denominada Java_NativeDemo_test( ). La componente NativeDemo
del prefijo se añade porque identifica el método test( ) como parte de la clase NativeDemo.
Recuerde que otra clase puede definir su propio método nativo test( ) completamente
diferente del declarado por NativeDemo. Al incluir el nombre de la clase en el prefijo es
posible distinguir distintas versiones del método. Como regla general, las funciones nativas
tendrán un nombre cuyo prefijo incluya el nombre de la clase en la que se declaran.
Después de generar el archivo de cabecera necesario, se escribe la implementación de test( )
y se almacena en un archivo denominado NativeDemo.c:
/* Este archivo contiene la versión C
del método test().
*/
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
JINIEXPORT void JNlCALL Java_NativeDemo_test(JNIEnv *env, jobject obj)
{
jclass cls;
jfieldID fid;
jint i;
printf("Comienzo del método nativo.\n");
cls = (*env)->GetObjectClass(env, obj);
fid = (*env)->GetFieldID(env, cls, "i", "I");
if(fid == 0) {
printf ("No se puede obtener el id del campo. \n");
return;
}
i = (*env)->GetIntField(env, obj, fid);
printf("i = %d\n" , i);
(*env)->SetIntField(env, obj, fid, 2*i);
printf("Final del método nativo.\n");
}
Observe que este archivo incluye jni.h, que contiene la información de la interfaz. Este archivo
lo proporciona el compilador Java. El archivo cabecera NativeDemo.h fue creado anteriormente
por javah.
En esta función, el método GetObjectClass( ) se usa para obtener una estructura C con
información sobre la clase NativeDemo. El método GetFieldID( ) devuelve una estructura
C con información relativa al campo llamado “i” de la clase. GetIntField( ) recupera el valor
original de ese campo. SetIntField( ) almacena un valor actualizado en ese campo. (Para otros
métodos que manejen otros tipos de datos, véase el archivo jni.h.)
Después de crear NativeDemo.c, se debe compilar para obtener el correspondiente
archivo DLL. Para ello, con el compilador de C/C++ de Microsoft, se utiliza la siguiente
línea de comandos (será necesario especificar la ubicación de los archivos jni.h y su archivo
complementario jni_md.h):
C1 /LD NativeDemo.c
Esto genera un archivo denominado NativeDemo.dll. Una vez hecho todo esto, se puede
ejecutar el programa Java, obteniendo la siguiente salida:
Esto es ob.i antes del método nativo: 10
Comienzo del método nativo.
i = 10
Final del método nativo.
Esto es ob.i después del método nativo: 20
Problemas con los métodos nativos
Los métodos nativos parecen ofrecer muchas posibilidades, porque permiten tanto acceder a
bibliotecas de rutinas existentes como conseguir tiempos de ejecución más rápidos. Sin embargo,
introducen dos problemas significativos:
www.detodoprogramacion.com
PARTE I
#include
#include "NativeDemo.h"
#include
305
306
Parte I:
El lenguaje Java
• Riesgos potenciales para la seguridad: Dado que un método nativo ejecuta realmente
código máquina, puede acceder a cualquier parte del sistema local; es decir, un código
nativo no está confinado al entorno de ejecución de Java. Esto podría permitir la entrada
de virus, por ejemplo. Por este motivo, los applets no pueden usar métodos nativos.
Además, la carga de archivos DLL puede estar restringida en el sistema y sujeta a la
aprobación del administrador.
• Pérdida de portabilidad: Dado que el código nativo está contenido en una DLL,
debe estar presente en la máquina que ejecuta el programa Java. Más aún, como cada
código nativo depende del CPU y del sistema operativo, cada DLL es intrínsecamente
no portable. Por tanto, una aplicación Java que utilice métodos nativos sólo se podrá
ejecutar en una máquina compatible con la DLL que ha sido instalada.
El uso de métodos nativos se debe restringir, debido a que aportan a los programas Java un
riesgo de seguridad, y también afectan a la portabilidad.
assert
Otra adición relativamente nueva en Java es la palabra clave assert. Ésta es utilizada durante el
desarrollo de un programa para crear aserciones. Una aserción es una condición que debe ser
cierta durante la ejecución del programa. Por ejemplo, podríamos tener un método que debería
regresar siempre un número positivo. Esto puede ser verificado realizando la aserción respecto a
que el valor regresado por el método debe ser mayor a cero utilizando una sentencia assert. En
tiempo de ejecución si la condición se cumple no ocurre ninguna acción particular; sin embargo,
si la condición es falsa se lanza un error del tipo AssertionError. Las aserciones son utilizadas
comúnmente durante la fase de pruebas para verificar que alguna condición necesaria se cumpla.
Prácticamente no se utilizan en el código terminado.
La palabra clave assert tiene dos formas, la primera es la siguiente:
assert condición;
Donde condición es una expresión que produce un resultado del tipo boolean. Si el resultado
es true, la aserción se satisface y por tanto no se realiza ninguna acción. Si condición es falsa,
entonces la aserción ha fallado y se lanza un objeto AssertionError.
La segunda forma de assert es:
assert condición : expresión;
En esta versión, expresión es un valor que es enviado al constructor del objeto tipo
AssertionError. Este valor es convertido a String y mostrado cuando el objeto es lanzado si la
aserción no se satisface. Comúnmente, esta expresión es directamente una cadena de caracteres,
sin embargo cualquier expresión de tipo diferente a void está permitida siempre que ésta pueda
ser convertida a cadena.
El siguiente ejemplo utiliza assert para verificar que el valor regresado por el método
getnum( ) sea positivo.
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
// el método regresa un valor entero
static int getnum() {
return val--;
}
public static void main(String args[])
{
int n;
for(int i=0; i < 10; i++) {
n = getnum();
assert n > 0; // fallará cuando n tenga el valor 0
System.out.println("n es" + n);
}
}
}
Para activar la revisión de aserciones en tiempo de ejecución es necesario especificar la opción
–ea en la ejecución de Java. Por ejemplo, para activar la revisión de aserciones para AssertDemo
se escribe:
java –ea AssertDemo
Después de compilar y ejecutar el programa se obtiene la siguiente salida:
n es 3
n es 2
n es 1
Exception in thread "main" java.lang.AssertionError
At AssertionDemo.main(AssertDemo.java:17)
En main( ) se realizan repetidamente llamadas al método getnum( ), las cuales retornan un
valor entero. El valor retornado por getnum( ) es asignado a la variable n y luego verificado
utilizando la sentencia assert:
assert n > 0; // fallará cuando n tenga el valor 0
Esta sentencia fallará cuando n sea igual a 0, lo cual ocurre en la cuarta llamada al método.
Cuando esto ocurre se lanza una excepción.
Como se explicó antes, es posible especificar el mensaje a mostrar cuanto la aserción no
se cumple. Por ejemplo, si en el programa anterior sustituimos la sentencia de aserción por la
siguiente:
assert n > 0 : "¡n es un valor negativo!";
El programa generaría la siguiente salida:
n es 3
www.detodoprogramacion.com
PARTE I
// Ejemplo de assert
class AssertDemo {
static int val = 3;
307
308
Parte I:
El lenguaje Java
n es 2
n es 1
Exception in thread "main" java.lang.AssertionError: ¡n es
un valor negativo!
At AssertDemo.main(AssertDemo.java:17)
Un punto importante a tomar en cuenta sobre las aserciones es el hecho de que no deben
ser utilizadas para que realicen acciones requeridas por el programa. Esto debido a que los
programas son comúnmente ejecutados por Java con revisión de aserciones desactivadas. Por
ejemplo, consideremos el siguiente programa:
// Una manera equivocada de utilizar assert
class AssertDemo {
// generador de números aleatorios
static int val = 3;
// regresa un entero
static int getnum() {
return val--;
}
public static void main(String args[])
{
int n = 0;
for(int i = 0; i < 10; i++) {
assert (n = getnum()) > 0; // ¡ésta no es una buena idea!
System.out.println("n es " + n);
}
}
}
En esta versión del programa la llamada a getnum( ) se movió dentro de la sentencia assert.
Aunque este código funciona correctamente cuando la revisión de aserciones es activada,
este código no funcionará cuando la revisión de aserciones no esté activa porque la llamada a
getnum( ) nunca será ejecutada.
Las aserciones son una buena adición a Java debido, principalmente, a que hacen más
eficiente la revisión de errores durante el desarrollo de software. Por ejemplo, antes de la
existencia de las aserciones, si se deseaba verificar que n contuviera un valor positivo se debía
utilizar un código como éste:
if(n < 0) {
System.out.println("n es un valor negativo");
return; // regresar o bien lanzar una excepción
}
Con aserciones sólo necesitamos una línea de código. Además no es necesario eliminar las
sentencias assert en el código terminado.
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
309
Opciones para activar y desactivar la aserción
-ea:MiPaquete
Para inactivar la verificación de aserciones en el paquete MiPaquete, escribiríamos:
-da:MiPaquete
Para activar o inactivar todos los subpaquetes de un paquete dado, se escribe el nombre del
paquete seguido de tres puntos. Por ejemplo:
- ea:MiPaquete...
También es posible especificar una clase con las opciones –ea y –da. Por ejemplo, esto activa
la revisión de aserciones en la clase AssertDemo:
-ea:AssertDemo
Importación estática de clases e interfaces
JDK 5 añade una nueva característica a Java denominada importación estática que
extiende las capacidades de la palabra clave import. Al combinar las palabras clave import y
static obtenemos un import que puede emplearse para importar los miembros estáticos de
una clase o interfaz. Cuando se utiliza importación estática, es posible acceder a los miembros
estáticos de manera directa mediante sus nombres, sin tener que incluir el nombre de la clase.
Esto simplifica y reduce la sintaxis requerida para utilizar miembros estáticos.
Para entender la utilidad de una importación estática veamos primero un ejemplo que no
la utiliza. El siguiente programa calcula la hipotenusa de un triángulo rectángulo. Este código
utiliza dos métodos estáticos de las bibliotecas de Java, específicamente de la clase Math, la
cual pertenece al paquete java.lang. El primer método es Math.pow( ), regresa el valor de
un número elevado a una determinada potencia. El segundo método es Math.sqrt( ), que
regresa la raíz cuadrada del valor recibido como argumento.
// Calcula la hipotenusa de un triángulo rectángulo
class Hypot {
public static void main(String args[]) {
double sidel, side2;
double hypot;
sidel = 3.0;
side2 = 4.0;
// observe como sqrt() y pow() deben estar
// precedidos por el nombre de la clase Math
www.detodoprogramacion.com
PARTE I
Al ejecutar programas, es posible inactivar las aserciones utilizando la opción –da. Para
activar o inactivar un paquete específico se utiliza la opción –ea o –da seguido del nombre del
paquete. Por ejemplo para activar la revisión de aserciones en un paquete llamado MiPaquete,
escribiríamos:
310
Parte I:
El lenguaje Java
hypot = Math.sqrt(Math.pow(sidel, 2) +
Math.pow(side2, 2));
System.out.println("Con catetos de longitud " +
sidel + " y " + side2 +
" la hipotenusa es " +
hypot) ;
}
}
Dado que pow( ) y sqrt( ) son métodos estáticos, deben ser llamados utilizando el nombre
de la clase que los contiene, ésa es la clase Math. Eso origina que la ecuación de cálculo de
hipotenusa se escriba como
hypot = Math.sqrt(Math.pow(sidel, 2) +
Math.pow(side2, 2));
Como lo ilustra este ejemplo, tener que especificar el nombre de la clase cada vez que pow( ),
sqrt( ) o cualquier otro método de la clase Math de Java, como sin( ), cos( ) y tan( ), son
llamados puede resultar tedioso.
Para eliminar la necesidad de incluir el nombre de la clase en este tipo de invocaciones, Java
provee el concepto de importación estática. El siguiente código es el mismo programa anterior
pero aplicando importación estática.
// Ejemplo de importación estática con los métodos pow y sqrt
import static java.lang.Math.sqrt;
import static java.lang.Math.pow;
// Calcular la hipotenusa de un triángulo rectángulo
class Hypot {
public static void main(String args[]) {
double sidel, side2;
double hypot;
sidel = 3.0;
side2 = 4.0;
// Aquí son llamados los métodos sqrt() y pow()
// sin necesitad de anteponer el nombre de la clase Math
hypot = sqrt(pow(sidel, 2) + pow(side2, 2));
System.out.println("Con catetos de longitud " +
sidel + " y " + side2 +
" la hipotenusa es " +
hypot) ;
}
}
En esta versión, los nombres de los métodos sqrt y pow están disponibles gracias a las
sentencias:
import static java.lang.Math.sqrt;
import static java.lang.Math.pow;
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
hypot = sqrt(pow(sidel, 2) + pow(side2, 2));
Esta forma tiene visiblemente una mejor legibilidad.
Existen dos formas para las sentencias import static. La primera, utilizada en el ejemplo
anterior, se utiliza para importar un miembro específico y su forma general es:
import static paquete.nombreTipo.nombreMiembroEstatico;
Aquí, nombreTipo es el nombre de la clase o interfaz que contiene al miembro estático. El nombre
completo del paquete está especificado por paquete. El nombre del miembro está especificado
por nombreMiembroEstatico.
La segunda forma permite importar todos los miembros estáticos de la clase o interfaz y su
forma general es:
import static paquete.nombreTipo.*;
Si se planea utilizar varios de los métodos o variables estáticos definidos en la clase entonces
esta forma nos permite tener acceso a todos ellos sin tener que especificar un import para
cada uno de manera individual. Así, el programa anterior pudo haber utilizado únicamente la
siguiente sentencia import para permitir el acceso a los métodos pow( ) y sqrt( ) (y también a
todos los demás miembros estáticos de la clase Math):
import static java.lang.Math.*;
La importación estática no se limita a la clase Math y tampoco se limita sólo a métodos. Por
ejemplo, la siguiente sentencia importa el campo estático out de la clase System:
import static java.lang.System.out;
Después de la sentencia anterior es posible enviar datos a la pantalla utilizando out sin
precederla del nombre de su clase:
out.println("Después de importar System.out, se puede utilizar out
directamente");
Si bien importar System.out, como se muestra en el ejemplo anterior, permite escribir
sentencias más cortas, su uso no es del todo una buena idea, ya que al mismo tiempo resta
claridad al código. Al leer la sentencia anterior, no es evidente de forma instantánea que out se
refiere a System.out.
De la misma forma en que se importan los miembros estáticos de clases e interfaces
definidos en el API de Java, es posible también importar los miembros estáticos de clase e
interfaces propias.
Es importante no abusar de las facilidades que brinda la importación estática. Debemos
recordar que la razón por la cual Java organiza sus bibliotecas en paquetes es eliminar la
colisión de nombres. Cuando se importan miembros estáticos sus nombres se están incluyendo
en el espacio de nombres global. De esta manera, se incrementa el riesgo de conflictos en el
espacio de nombres y de ocultar nombres de forma inadvertida. Si se utiliza sólo una o dos
veces un miembro estático en el programa es mejor no importarlo. Además ciertos nombres
www.detodoprogramacion.com
PARTE I
Con esto ya no es necesario colocar el nombre de la clase Math antes de los métodos sqrt( ) y
pow( ). Así, el cálculo de la hipotenusa puede ser realizado con la siguiente línea:
311
312
Parte I:
El lenguaje Java
estáticos, como System.out, son tan conocidos que no es recomendable importarlos. La
importación estática está diseñada para ser empleada en aquellas situaciones en las cuales se
utiliza repetidamente un miembro estático, como cuando se realizan cálculos matemáticos con
métodos de la clase Math. En esencia esta característica debe ser utilizada cuidando no abusar
de ella.
Invocación de constructores sobrecargados con la palabra clave this( )
Cuando se trabaja con constructores sobrecargados en ocasiones es útil para un constructor
invocar a otro. En Java, esto se logra utilizando la palabra clave this. Como sigue:
this(parámetros)
Cuando se ejecuta this( ), el constructor sobrecargado cuya lista de parámetros coincide con la
lista de parámetros especificada por parámetros es ejecutado. Luego se ejecutan las sentencias
del método constructor que realizó la llamada a this( ). La llamada a this( ) debe estar colocada
como primer línea dentro de un constructor.
Para entender cómo se utiliza this( ), veamos un pequeño ejemplo. Primero, considere la
siguiente clase que no utiliza this( ):
class MyClass
int a;
int b;
// inicializa a y b individualmente
MyClass(int i, int j) {
a = i;
b = j;
}
// inicializa a y b con el mismo valor
MyClass(int i) {
a = i;
b = i;
}
// inicializa a y b con el valor cero
MyClass( ) {
a = 0;
b = 0;
}
}
Esta clase contiene tres constructores, cada uno de los cuales inicializa los valores de
a y b. El primero permite pasar valores individuales para a y b. El segundo permite
pasar solo un valor, el cual es asignado por igual a a y b. El tercero da a a y b el valor
por omisión de cero. Utilizando this( ) es posible reescribir MyClass como se muestra a
continuación:
www.detodoprogramacion.com
Capítulo 13:
E/S, applets y otros temas
// inicializar a y b individualmente
MyClass(int i, int j) {
a = i;
b = j;
}
// inicializar a y b con el mismo valor
MyClass(int i) {
this(i, i); // llama a MyClass(i, i)
}
// inicializa a y b con el valor cero
MyClass( ) {
this(0); // llama a MyClass(0);
}
}
En esta versión de MyClass, el único constructor que asigna valores a los campos a y b es
MyClass(int, int), los otros dos constructores simplemente invocan, directa o indirectamente a
este constructor vía this( ). Por ejemplo, considere lo que ocurre cuando se ejecuta la siguiente
sentencia:
MyClass mc = new MyClass(8);
La llamada a MyClass(8) ocasiona una llamada a this(8, 8), la cual a su vez se convierte en una
llamada a MyClass(8, 8) que es el constructor de la clase MyClass que coincide con la lista
de parámetros listados en la invocación a this( ). Ahora veamos que ocurre con la siguiente
sentencia, la cual utiliza el constructor sin parámetros:
MyClass me2 = new MyClass();
En este caso, se llama a this(0), esa llamada se convierte en una llamada a MyClass(0). La
llamada a MyClass(0) a su vez desencadena una llamada a MyClass(0,0).
La invocación de constructores sobrecargados utilizando this( ) previene la duplicación
innecesaria de código. Reducir el código duplicado, en muchos casos, permite generar un código
objeto más pequeño lo que a su vez causa que el tiempo que le toma a la máquina virtual cargar
el código a memoria sea menor. Esto es especialmente importante para programas que han
de ser distribuidos por Internet. El uso de this( ) nos ayuda además a estructurar el programa
cuando los constructores contienen grandes cantidades de código repetido.
Sin embargo es recomendable ser cuidadosos. Los constructores que llaman a this( ) serán
ejecutados más lentamente que aquellos que contienen su propio código. Esto debido a que
el mecanismo de llamada y retorno entre métodos constructores añade tiempo adicional al
proceso. Si sólo se van a crear algunos objetos de la clase o bien si el constructor en la clase
que llama a this( ) será utilizado en pocas ocasiones entonces el incremento en el tiempo de
ejecución será insignificante. Sin embargo, si se planea crear un gran número de objetos de la
www.detodoprogramacion.com
PARTE I
class MyClass {
int a;
int b;
313
314
Parte I:
El lenguaje Java
clase (en el orden de miles de objetos) durante la ejecución del programa, entonces el efecto en
el rendimiento del programa será considerable. Dado que la creación de objetos afecta a todos
los usuarios de la clase debemos, en muchos casos, ser cuidadosos y colocar en la balanza si
necesitamos rapidez en el tiempo de carga del programa (código pequeño) aún a pesar de
incrementar el tiempo requerido para la creación de un objeto.
Una consideración adicional a tomar en cuenta es que para constructores muy pequeños,
como los utilizados en la clase MyClass, existe muy poca diferencia en el tamaño del código
utilizando o no this( ). Actualmente, existen casos donde no se genera ninguna reducción en el
tamaño del código. Esto debido al bytecode que se añade al código objeto para representar la
llamada y retorno de una función.
Por ello, en ese tipo de situaciones, aun cuando se elimina el código duplicado, this( )
no proporciona ahorro en el tiempo de carga. Sin embargo, si se añade costo que se añada
en términos de más tiempo requerido para construir objetos de la clase. De ahí que this( ) se
aplique sólo a constructores que contienen una gran cantidad de código y no a aquellos que
simplemente inicializan el valor de un pequeño número de variables.
Existen dos restricciones que se deben tener en cuenta cuando se utiliza this( ). Primero, no
podemos utilizar ninguna variable de instancia de la clase del constructor en la llamada a this( ).
Y segundo, no podemos utilizar super( ) y this( ) en el mismo constructor debido a que ambas
sentencias están definidas para ser las primeras en ejecutarse en el constructor.
www.detodoprogramacion.com
14
CAPÍTULO
Tipos parametrizados
D
esde la versión original 1.0 de 1995, muchas nuevas características han sido agregadas a
Java. La que ha tenido el efecto más profundo es la genérica, esto es los tipos parametrizados.
Introducidos en el JDK 5, los tipos parametrizados han cambiado Java en dos formas muy
importantes. Primero, agregaron un nuevo elemento sintáctico al lenguaje. Segundo, causó cambios
a muchas de las clases y métodos en el núcleo de la API de Java. Puesto que los tipos parametrizados
representan un gran cambio en el lenguaje, algunos programadores estuvieron renuentes a adoptar
su uso. Sin embargo, con la versión de JDK 6, los tipos parametrizados no pueden ser ignorados. En
pocas palabras, si se va a programar con Java SE 6, se van a estar utilizando tipos parametrizados
constantemente. Afortunadamente, los tipos parametrizados no son difíciles de utilizar y proveen
beneficios significativos para los programadores en Java.
A través del uso de tipos parametrizados, es posible crear clases, interfaces y métodos que
trabajarán de forma segura con varios tipos de datos. Muchos algoritmos son lógicamente los mismos
sin importar a qué tipo de datos estén siendo aplicados. Por ejemplo, el mecanismo que soporta una
pila es el mismo si la pila almacena datos de tipo Integer, String, Object o Thread. Con los tipos
parametrizados, se puede definir un algoritmo independientemente del tipo de datos, y luego aplicar
dicho algoritmo a una amplia variedad de tipos de datos sin esfuerzo adicional. El poder expresivo
que los tipos parametrizados agregan al lenguaje cambia fundamentalmente la forma en que se
escribe el código de Java.
Quizá la característica de Java que ha sido más significativamente afectada por los tipos
parametrizados es la Estructura de Colecciones. La Estructura de Colecciones es parte de la API de Java
y se describe a detalle en el Capítulo 17, sin embargo vale la pena hablar un poco de ella ahora. Una
colección es un grupo de objetos. La Estructura de Colecciones define muchas clases, tales como listas
y mapas, que administran las colecciones. Las clases que representan colecciones siempre han sido
capaces de trabajar con cualquier tipo de objeto. El beneficio que los tipos parametrizados agregan,
es que las clases de colección ahora pueden ser utilizadas con completa seguridad. Así, además de
proveer un nuevo elemento poderoso al lenguaje, los tipos parametrizados también habilitan una
característica existente que ha sido sustancialmente mejorada. Ésta es la razón por la cual los tipos
parametrizados representan una importante extensión a Java.
Este capítulo describe la sintaxis, teoría y uso de los tipos parametrizados. También muestra
como los tipos parametrizados proveen seguridad al manejar tipos en casos que, anteriormente,
315
www.detodoprogramacion.com
316
Parte I:
El lenguaje Java
eran complicados. Una vez que se haya completado este capítulo, seguramente el lector deseará
examinar el Capítulo 17 que habla de la Estructura de Colecciones. En el Capítulo 17
se encuentran muchos ejemplos funcionando de tipos parametrizados.
NOTA Los tipos parametrizados fueron agregados en JDK 5. El código fuente que utiliza tipos
parametrizados no puede ser compilado por versiones de javac anteriores a la versión 5.
¿Qué son los tipos parametrizados?
El término tipos parametrizados es el núcleo de la característica genérica. Los tipos parametrizados
son importantes porque proporcionan la habilidad de crear clases, interfaces y métodos en los
cuales los tipos de datos sobre los cuales operan son especificados como parámetros. Utilizando
tipos parametrizados, es posible crear una clase, por ejemplo, que automáticamente funcione
con diferentes tipos de datos. Una clase, una interfaz, o un método que opere sobre un tipo
parametrizado es llamada genérica, de ahí que hablemos de clases genéricas o método genéricos.
Es importante entender que Java siempre ha contado con la habilidad de crear clases,
interfaces y métodos generalizados gracias al uso de referencias a objetos de tipo Object. Dado
que Object es la superclase de todas las otras clases, una referencia de tipo Object se puede
referir a cualquier otro tipo de objeto. Así, en el código previo a la existencia de tipos
parametrizados, clases, interfaces y métodos generalizados, utilizaban referencias a Object para
operar sobre varios tipos de objetos. El problema era la falta de seguridad en el manejo de los
tipos de datos.
Los tipos parametrizados agregan la seguridad que hacia falta. Además estilizan el proceso
ya que evitan que sea necesario realizar la conversión explicita de tipos para traducir entre
Object y el tipo de datos sobre el que se requiere trabajar. Con los tipos parametrizados,
todas las conversiones de tipos son automáticas e implícitas. Por ello, los tipos parametrizados
expanden la capacidad de reutilizar código y permite hacerlo de una forma más fácil y segura.
NOTA Una advertencia para los programadores de C++: Aunque los tipos parametrizados son
similares a las plantillas de C++, no son lo mismo. Existen algunas diferencias fundamentales
entre los dos enfoques. Si se tiene experiencia en C++, es importante no sacar conclusiones
apresuradas sobre cómo funcionan los tipos parametrizados.
Un ejemplo sencillo con tipos parametrizados
Comencemos con un ejemplo simple de una clase genérica. El siguiente programa define dos
clases. La primera es la clase genérica llamada Gen y la segunda es la clase GenDemo que
utiliza a Gen.
// Un ejemplo de clase genérica
// Donde, T es un parámetro de tipo que
// será reemplazado por un tipo real
// cuando un objeto de tipo Gen sea creando
class Gen {
T ob; // declara un objeto de tipo T
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
// Devuelve ob.
T getob( ) {
return ob;
}
// Muestra el tipo de T.
void showType( ) {
System.out.println("Tipo de T es" +
ob.getClass( ).getName( ));
}
}
// Esta clase utiliza a la clase con tipos parametrizados.
class GenDemo {
public static void main(String args[]) {
// Crea una referencia a Gen para objetos Integer.
Gen iOb;
// Crea un objeto Gen y asigna su
// referencia a iOb. Nótese el uso de autoboxing
// para encapsular el valor 88 dentro de un objeto Integer
iOb = new Gen (88);
// Muestra el tipo de datos utilizado por iOb.
iOb.showType ( );
// Obtiene el valor en iOb. Nótese que
// no se necesita hacer la conversión explícita de tipos
int v = iOb.getob( );
System.out.println("valor: " + v);
System.out.println( );
// Crea un objeto Gen para valores de tipo String
Gen strOb = new Gen ("Prueba de Tipos Parametrizados");
// Muestra el tipo de dato utilizado por strOb.
strOb.showType( );
// Obtiene el valor de strOb. De nuevo, nótese
// que no se necesita hacer la conversión explícita de tipos
String str = strOb.getob( );
System.out.println("valor: " + str);
}
}
La salida producida por el programa es la siguiente:
Tipo de T es java.lang.Integer
valor: 88
www.detodoprogramacion.com
PARTE I
// Pasa al constructor una referencia a
// un objeto de tipo T
Gen(T o) {
ob = o;
}
317
318
Parte I:
El lenguaje Java
Tipo de T es java.lang.String
valor: Prueba de Tipos Parametrizados
Examinemos el programa con más detenimiento.
En primer lugar, observemos como la clase Gen es declarada en la siguiente línea:
class Gen {
Donde T es el nombre de un parámetro de tipo. Este nombre se utiliza como un marcador de
posición para el verdadero tipo que será pasado a Gen cuando un objeto sea creado. Así, T
es utilizado dentro de Gen donde sea que el tipo parametrizado sea requerido. Note que
T está colocado dentro de < >. Esta sintaxis puede ser generalizada. En cualquier momento que
se requiera declarar un tipo parametrizado, éste se especifica dentro de paréntesis angulares.
Dado que la clase Gen utiliza un tipo parametrizado, Gen es una clase genérica.
A continuación, T es utilizada para declarar un objeto llamado ob, como se muestra a
continuación:
T ob; //declara un objeto de tipo T
Como se explicó, T es un marcador de posición para el tipo actual que será especificado cuando
un objeto Gen sea creado. Así, ob será un objeto del tipo pasado en T. Por ejemplo, si el tipo
String es pasado en T, entonces en dicha instancia, ob será de tipo String.
Ahora considérese el constructor de Gen:
Gen (T o) {
ob = o;
}
Obsérvese que ob también es de tipo T. Esto significa que el tipo de o está determinado por
el tipo pasado en T cuando se crea un objeto de la clase Gen. Además debido a que tanto el
parámetro o como la variable ob son de tipo T, ambos serán del mismo tipo cuando un objeto
de la clase Gen sea creado.
El parámetro T también puede ser utilizado para especificar el tipo de dato a ser devuelto
por un método, como en el caso del método getob( ) mostrado a continuación:
T getob( ) {
return ob;
}
Debido a que ob también es de tipo T, su tipo es compatible con el tipo de retorno especificado
por getob( ).
El método showType( ) muestra el tipo de T mediante la llamada al método getName( )
sobre el objeto de tipo Class devuelto por la llamada a getClass( ) sobre ob. El método
getClass( ) está definido por la clase Object y es por ello un miembro de todos los tipos
de clases. Este método devuelve un objeto Class que corresponde al tipo de la clase del objeto
sobre el cuál es llamado. Class define el método getName( ), el cual regresa una cadena
representativa del nombre de la clase.
La clase GenDemo demuestra como utilizar la clase genérica Gen. Primeramente crea una
versión de Gen para enteros como se muestra aquí:
Gen iOb;
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
iOb = new Gen (88);
Nótese que cuando se llama al constructor de Gen, el argumento de especificación del tipo,
Integer, también es especificado. Esto es necesario porque el tipo de objeto (en este caso iOb) al
cual la referencia está siendo asignada es de tipo Gen. Por eso, la referencia regresada
por el operador new debe ser también de tipo Gen. Si no se escribe de esta forma, se
produce como resultado un error de compilación. Por ejemplo, la siguiente asignación causará
un error de compilación:
iOb = new Gen (88.0); // Error
Debido a que iOb es de tipo Gen no puede ser utilizado como referencia de un objeto
de tipo Gen. Esta revisión de tipos es uno de los beneficios más importantes de los
tipos parametrizados, porque asegura conversiones de tipos seguras.
Como los comentarios en el programa lo indican, la asignación
iOb = new Gen (88);
hace uso del autoboxing para encapsular el valor 88, el cual es de tipo int, dentro de un Integer.
Esto funciona debido a que Gen crea un constructor que toma un argumento Integer.
Dado que se espera un Integer, Java automáticamente aplica autoboxing para crear un Integer
con el valor 88. Claro está que la asignación podría también haber sido escrita explícitamente,
como se muestra a continuación:
iOb = new Gen (new Integer(88));
Sin embargo, no habría ningún beneficio al utilizar esta versión.
El programa muestra el tipo de ob dentro de iOb, el cuál es Integer. A continuación, el
programa obtiene el valor de ob con la siguiente línea:
int v = iOb.getob( );
Debido a que el tipo de regreso de getob( ) es T, el cual fue reemplazado por Integer cuando
iOb fue declarado, el tipo de regreso de getob( ) es también Integer. Gracias al auto-unboxing,
Integer se convierte en int cuando es asignado a v (la cual es de tipo int). Así, no hay necesidad
www.detodoprogramacion.com
PARTE I
Observe detalladamente esta declaración. Primero, note que el tipo Integer está especificado
dentro de paréntesis angulares después del nombre Gen. En este caso, Integer es el argumento
de tipo que es pasado al parámetro de tipo T en la clase Gen. Esta línea crea una versión de Gen
en la cual todas las referencias a T son trasladadas en referencias a Integer, y el tipo de retorno
para el método getob( ) también es Integer.
Antes de continuar, es necesario aclarar que el compilador de Java no crea diferentes
versiones de Gen, o de ninguna otra clase genérica. Aunque es útil pensar en esos términos,
no es en realidad lo que pasa. En su lugar, el compilador elimina toda la información de los
tipos parametrizados, sustituyéndolas por las conversiones de tipos necesarias, para hacer que
el código se comporte como si la versión especificada de la clase fuera creada. Esto es, en el caso
de nuestro ejemplo, existe solamente una versión de la clase Gen. El proceso de eliminar la
información de los tipos parametrizados es llamada “cancelación”, y hablaremos de ese tema más
adelante en este capítulo.
La siguiente línea asigna a iOb una referencia a una instancia de una versión de la clase
Gen que trabaja con elementos de tipo Integer:
319
320
Parte I:
El lenguaje Java
de convertir el tipo de regreso de getob( ) a Integer. Claro está que no es necesario el uso de la
característica auto-unboxing, la línea anterior podría haber sido escrita también como se muestra
a continuación:
int v = iOb.getob( ).intValue( );
Sin embargo, la característica de auto-unboxing vuelve al código más compacto.
A continuación GenDemo declara un objeto de tipo Gen:
Gen strOb = new Gen ("Prueba de tipos parametrizados");
Debido a que el argumento de tipo es String, String sustituye a T dentro de Gen. Esto crea
(conceptualmente) una versión de Gen con String, como el resto de las líneas en el programa lo
demuestran.
Los tipos parametrizados sólo trabajan con objetos
Cuando se declara una instancia con tipos parametrizados, el argumento de tipo que se pasa al
parámetro de tipo debe ser una clase. No se pueden utilizar tipos primitivos, como int o char.
Por ejemplo, con Gen, es posible pasar cualquier clase como valor para T, pero no se puede
utilizar un tipo primitivo como valor de T. Por consiguiente, la siguiente línea es incorrecta:
Gen strOb = new Gen(53); //Error, no se pueden utilizar
tipos primitivos
Claro que la restricción de uso de los tipos primitivos no es una restricción seria porque se puede
utilizar un envoltorio (como en el ejemplo anterior) para encapsular un tipo primitivo. Más
aún, los mecanismos de autoboxing y auto-unboxing de Java hacen transparente el uso de la
envoltura de tipos.
Los tipos parametrizados se diferencian por el tipo de sus argumentos
Un punto clave de entender acerca de los tipos parametrizados es que una referencia de una
versión específica de un tipo parametrizado no es un tipo compatible con otra versión del mismo
tipo parametrizado. Por ejemplo en el programa anterior, la siguiente línea de código es un error
y no compilará:
iOb = strOb;
// error
Aunque tanto iOb como strOb son de tipo Gen, son referencias a tipos diferentes
debido a que sus parámetros de tipos son diferentes. Esto es parte del proceso en que los tipos
parametrizados agregan seguridad y previenen errores.
Los tipos parametrizados son una mejora a la seguridad
En este punto, nos deberíamos estar preguntando lo siguiente: dado que las mismas
funcionalidades proporcionadas por la clase genérica Gen puede ser lograda sin tipos
parametrizados, simplemente especificando Object como el tipo de datos y empleando las
conversiones de tipo correctas, ¿cuál es el beneficio de utilizar tipos parametrizados en la clase
Gen? La respuesta es que los tipos parametrizados automáticamente garantizan seguridad en
el manejo de tipos en todas las operaciones en las que se involucre Gen. En el proceso, los tipos
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
// La funcionalidad de la clase NoGen es equivalente a la de la clase Gen
// pero no utiliza tipos parametrizados
class NoGen {
Object ob; // ob es ahora de tipo Object
// Pasa al constructor una referencia a
// un object de tipo Object
NoGen(Object o) {
ob = o;
}
// Devuelve un valor de tipo Object.
Object getob( ) {
return ob;
}
// Muestra el tipo de ob.
void showType( ) {
System.out.println("El tipo de ob es " +
ob.getClass( ).getName( ));
}
}
// Utilizando la clase no genérica
class NoGenDemo {
public static void main (String args[]) {
NoGen iOb;
// Crea un objeto NoGen y almacena
// un valor de tipo Integer en él. El autoboxing ocurre igual que antes.
iOb = new NoGen(88);
// Muestra el tipo de dato utilizado por iOb.
iOb.showType ( );
// Obtiene el valor de iOb.
// En este momento, es necesario hacer conversión de tipos
int v = (Integer) iOb.getob( );
System.out.println("valor: " + v);
System.out.println( ) ;
// Crea otro objeto NoGen y
// almacena un String en él
NoGen strOb = new NoGen("Prueba con tipos no parametrizados");
// Muestra el tipo de datos usados por strOb.
strOb.showType( ) ;
// Obtiene el valor de strOb.
// De nuevo, note que un conversión de tipos es necesaria.
www.detodoprogramacion.com
PARTE I
parametrizados eliminan la necesidad de codificar manualmente las operaciones de conversión
de tipos y de comprobación de tipos.
Para entender los beneficios de los tipos parametrizados, consideremos el siguiente
programa que crea un equivalente sin tipos parametrizados de Gen:
321
322
Parte I:
El lenguaje Java
String str = (String) strOb.getob( );
System.out.println("valor: " + str);
// Este programa compila, pero es conceptualmente incorrecto
iOb = strOb;.
v = (Integer) iOb.getob( ); // error en tiempo de ejecución
}
}
Existen muchas cosas interesantes en esta versión. Primero, note que NoGen reemplaza
todas las ocurrencias de T con Object. Esto hace a NoGen capaz de almacenar cualquier tipo
de objetos, tal como en la versión de los tipos parametrizados. Sin embargo, también evita que
el compilador de Java tenga conocimiento real sobre el tipo de dato almacenado en NoGen,
lo cual es malo por dos razones. Primero, deben emplearse conversiones explícitas de tipos
para recuperar los datos almacenados. Segundo, no podrán ser detectados muchos errores de
incompatibilidad de tipos sino hasta que el programa sea ejecutado. Veamos más de cerca cada
problema.
Observemos la siguiente línea:
int v = (Integer) iOb.getob( );
Debido a que el tipo de retorno de getob( ) es Object, la conversión a Integer es necesaria
para que al valor Integer se le aplique auto-unboxing y sea almacenado en v. Si se elimina la
conversión de tipos, el programa no compilará. Con la versión que utiliza tipos parametrizados,
la conversión fue implícita. En la versión que no utiliza tipos parametrizados, la conversión debe
ser explícita. Esto no sólo es incómodo, también es una fuente potencial de errores.
Ahora, considere la siguiente secuencia de instrucciones cerca del final del programa:
// Esto compila, pero es conceptualmente erróneo
iOb = strOb;
v = (Integer) iOb.getob( ); // error en tiempo de ejecución
Aquí, strOb es asignado a iOb. Sin embargo, strOb se refiere a un objeto que contiene una
cadena, no a un entero. Esta asignación es sintácticamente válida porque todas las referencias
a NoGen son iguales, y cualquier referencia a NoGen puede referirse a cualquier otro objeto
NoGen. Sin embargo, la sentencia es semánticamente incorrecta. En estas líneas, el tipo de
retorno de getob( ) es convertido a Integer, y luego se hace un intento de asignar ese valor a v.
El problema es que iOb ahora se refiere a un objeto que almacena un String y no un Integer.
Desafortunadamente, sin el uso de tipos parametrizados el compilador de Java no tiene forma
de saberlo. Por tanto, ocurre una excepción en tiempo de ejecución cuando se intenta realizar la
conversión a Integer. Como ya sabrá, es extremadamente malo que el código tenga excepciones
en tiempo de ejecución.
Las líneas anteriores no se presentan cuando se utilizan tipos parametrizados. Si esa
secuencia fuera escrita en la versión del programa que utiliza tipos parametrizados, el
compilador detectaría el problema y reportaría un error, de esta forma se previenen serios
defectos, que a la postre podrían causar excepciones en tiempo de ejecución. La habilidad
de crear código con tipos seguros en el cual los errores de incompatibilidad de tipos son
eliminados en tiempo de compilación, es la ventaja clave de los tipos parametrizados. Aunque
utilizar referencias de tipo Object para crear código con “tipos genéricos” siempre ha sido
posible, el código no es seguro y su mal uso podría resultar en excepciones en tiempo de
ejecución. Los tipos parametrizados previenen dichas excepciones. En esencia, a través de los
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
Una clase con tipos parametrizados con dos tipos como parámetro
Se puede declarar más de un parámetro de tipo en una clase genérica. Para especificar dos o más
parámetros de tipo, simplemente se utiliza una lista separada con comas. Por ejemplo, la clase
DosGen es una variación de la clase Gen con dos tipos parametrizados:
// Una clase simple con tipos parametrizados
// con dos parámetros de tipo: T y V.
class DosGen {
T ob1;
V ob2;
// Pasa al constructor como referencia
// un objeto de tipo T y un objeto de tipo V.
DosGen(T o1, V o2) {
ob1 = o1;
ob2 = o2;
}
// Muestra los tipos de T y V.
void showTypes( ) {
System.out.println("Tipo de T es" +
obl.getClass( ).getName( ));
System.out.println("Tipo de V es" +
ob2.getClass( ).getName( ));
T getobl( ) {
return obl;
}
V getob2( ) {
return ob2;
}
}
// Ejemplo de uso de DosGen.
class SimpGen {
public static void main(String args[]) {
DosGen tgObj =
new DosGen (88, "Tipos Parametrizados");
// Muestra los tipos
tgObj.showTypes( );
// Obtiene y muestra los valores.
int v = tgObj.getobl( );
System.out.println ( "valor: " + v);
String str = tgObj.getob2( );
System.out.println("valor: " + str);
}
}
www.detodoprogramacion.com
PARTE I
tipos parametrizados, los que eran errores en tiempo de ejecución se han convertido en errores
en tiempo de compilación. Esta es la principal ventaja.
323
324
Parte I:
El lenguaje Java
La salida de este programa se muestra a continuación:
Tipo de T es java.lang.Integer
Tipo de V es java.lang.String
valor: 88
valor: Tipos Parametrizados
Nótese como se declara la clase DosGen:
class DosGen {
Se especifican dos parámetros de tipo: T y V, separados por una coma. Debido a que tienen dos
parámetros de tipo, dos argumentos de tipo deben ser pasados a la clase DosGen cuando se
crea un objeto, como se muestra a continuación
DosGen tgObj =
new DosGen (88, "Tipos Parametrizados");
En este caso, Integer es sustituido por T, y String sustituido por V.
Aunque los dos argumentos de tipo son diferentes en este ejemplo, es posible que ambos
tipos sean iguales. Por ejemplo, la siguiente línea de código es válida:
DosGen x = DosGen ("A", "B");
En este caso, ambos T y V serían de tipo String. Claro que, si los argumentos de tipo fueran
siempre los mismos, entonces tener dos parámetros de tipo sería innecesario.
La forma general de una clase con tipos parametrizados
La sintaxis de los tipos parametrizados mostrada en los ejemplos anteriores puede ser
generalizada. Aquí está la sintaxis para declarar una clase con tipos parametrizados:
class nombre-de-la-clase{ //...
Aquí está la sintaxis para la declaración a una referencia de una clase genérica:
nombre-de-la-clase nombre-de-la-variable =
new nombre-de-la-clase (Lista de constantes);
Tipos delimitados
En los ejemplos anteriores, los parámetros tipo podrían ser reemplazados por cualquier clase. Esto
es bueno para muchos propósitos, pero algunas veces es útil limitar los tipos que pueden ser
pasados a un parámetro de tipo. Por ejemplo, asumiendo que se quiera crear una clase genérica
que contenga un método que regrese el promedio de un arreglo de números. Más aún, se quiere
utilizar la clase para obtener el promedio de un arreglo de cualquier tipo de números, incluyendo
enteros, flotantes y reales. Esto es, se desea especificar el tipo de los números de forma general,
utilizando un tipo parametrizado. Para crear tal clase, se podría intentar algo como lo que sigue:
//
//
//
//
La clase Stats intenta (sin éxito)
crear una clase con tipos parametrizados que puede calcular
el promedio de un arreglo de números de
cualquier tipo dado
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
// Pasa al constructor una referencia a
// un arreglo de tipo T.
Stats (T [] o) {
nums = o;
}
// Devuelve tipo double en todos los casos
double average( ) {
double sum = 0.0;
for(int i=0; i < nums.length; i++)
sum += nums[i].doubleValue( ); //Error
return sum / nums.length;
}
}
En la clase Stats, el método average( ) intenta obtener la versión de tipo double de cada
número en el arreglo nums llamando al método doubleValue( ). Debido a que todas las
clases numéricas, tales como Integer y Double, son subclases de Number, y Number define
al método doubleValue( ), este método está disponible para todas las clases numéricas. El
problema es que el compilador no tiene forma de saber que estamos intentando crear sólo
objetos numéricos con la clase Stats. Por ello, cuando se intenta compilar Stats, se produce un
error que indica que el método doubleValue( ) no es conocido. Para resolver este problema, se
necesita de alguna forma indicarle al compilador que la intención es pasar sólo tipos numéricos
a T. Además se requiere alguna forma para asegurar que sólo sean pasados a T tipos numéricos.
Para manejar tales situaciones, Java provee tipos delimitados. Cuando se especifica un
parámetro de tipo, se puede crear un delimitador superior que declara la superclase de la cual
todos los argumentos de tipo deben estar derivados. Esto se logra utilizando una cláusula
extends al especificar el parámetro de tipo, como se muestra a continuación:
Esto especifica que T sólo puede ser reemplazado por superclase o subclases de superclase. De
este modo, la superclase define un límite superior inclusivo.
Se puede utilizar un límite superior para solucionar el problema de la clase Stats mostrada
anteriormente especificando Number como delimitador superior, como se muestra a
continuación:
// En esta versión de Stats, el argumento de tipo para
// T debe ser Number, o una clase derivada
// de Number.
class Stats {
T[] nums; // arreglo de elementos de tipo Number o subclases de Number
// Pasa al constructor una referencia a
// un arreglo de valores de tipo Number o subclases de Number
Stats (T [] o) {
nums = o;
}
www.detodoprogramacion.com
PARTE I
//
// La clase contiene un error.
class Stats {
T[] nums; // nums es un arreglo de tipo T
325
326
Parte I:
El lenguaje Java
// Devuelve un valor de tipo double en todos los casos.
double average( ) {
double sum = 0.0;
for(int i=0; i < nums.length; i++)
sum += nums[i].doubleValue( );
return sum / nums.length;
}
}
// Muestra el uso de la clase Stats.
class BoundsDemo {
public static void main(String args[]) {
Integer inums[] = { 1, 2, 3, 4, 5 };
Stats iob = new Stats (inums);
double v = iob.average( );
System.out.println("El promedio es" + v);
Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };
Stats dob = new Stats(dnums);
double w = dob.average( );
System.out.println("Promedio es:" + w);
// Esto no compilará porque String no es una
// subclase de Number.
//
String strs[] = { "1", "2", "3", "4", "5" };
//
Stats strob = new Stats(strs);
//
//
double x = strob.average( );
System.out.println("El promedio es" + v);
}
}
La salida se muestra aquí:
Promedio es 3.0
Promedio es 3.3
Note como Stats ahora se declara como:
class Stats {
Dado que el tipo T está ahora es delimitado por Number, el compilador de Java sabe que todos
los objetos de tipo T puede llamar a doubleValue( ) porque es un método declarado en la clase
Number. Esto es, por sí mismo, una gran ventaja. Sin embargo, como un bono adicional, el
delimitador de T también evita que se pase como parámetro a Stats un tipo no numérico. Por
ejemplo, si se intenta eliminar el comentario de las líneas al final del programa, y recompilar el
programa, se recibirá un error de compilación porque String no es una subclase de Number.
Además de utilizar una clase como tipo delimitado, se puede también utilizar una interfaz.
De hecho, se pueden especificar múltiples interfaces como delimitadores. Más aún, un tipo
delimitado puede incluir tanto una clase como una o más interfaces. En este caso, la clase debe
ser especificada primero. Cuando un tipo delimitado incluye una interfaz, sólo son considerados
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
class Gen {//...
Donde, T está delimitado por una clase llamada MiClase y una interfaz llamada MiInterfaz.
De este modo, cualquier argumento de tipo pasado a T deberá ser una subclase de MiClase e
implementar MiInterfaz.
Utilizando argumentos comodines
Son tan útiles como la seguridad de tipos, y algunas veces pueden generar construcciones
perfectamente aceptables. Por ejemplo, considerando la clase Stats mostrada al final de
la sección anterior, suponga que se desea agregar un método llamado sameAvg( ) que
determine si dos objetos Stats contienen arreglos que producen el mismo promedio, sin
importar que tipos de datos numéricos contiene cada objeto. Por ejemplo, si un objeto
contiene los valores de tipo double 1.0, 2.0 y 3.0, y el otro objeto contiene valores enteros 2, 1
y 3, entonces el promedio sería el mismo. Una forma de implementar sameAvg( ) es pasarle
un argumento de tipo Stats, y luego comparar el promedio del argumento contra el promedio
del objeto que se invoca, devolviendo verdadero solo si el promedio es el mismo. Por ejemplo,
si se quiere llamar al método sameAvg( ), como se muestra a continuación:
Integer inums[] = {1, 2, 3, 4, 5};
Double dnums[] = {1.1, 2.2, 3.3, 4.4, 5.5}
Stats iob = new Stats (inums);
Stats dob = new Stats (dnums);
if (ib.sameAvg(dob))
System.out.println("El promedio es el mismo");
else
System.out.println("El promedio es diferente");
Inicialmente, crear al método sameAvg( ) parece ser un problema fácil. Debido a
que Stats es una clase con tipos parametrizados y su método average( ) puede funcionar
con cualquier tipo de objeto Stats, parecería que crear al método sameAvg( ) es simple.
Desafortunadamente, el problema comienza tan pronto como se intenta declarar un parámetro
de tipo Stats. Debido a que Stats es un tipo parametrizado, y la pregunta es ¿qué se especifica
en el parámetro de tipo de Stats cuando se declara un parámetro de ese tipo?
Al principio se podría pensar en una solución como la siguiente, en la cual T se usa como
tipo de parámetro:
// Esto no va a funcionar
// Determina si dos promedios son iguales.
boolean sameAvg(Stats ob) {
if (average( ) = = ob.average( ))
return true;
return false;
}
www.detodoprogramacion.com
PARTE I
correctos los argumentos de tipo que hayan implementado la interfaz. Cuando se especifica un
tipo delimitado que tiene una clase y una interfaz, o múltiples interfaces, se utiliza el operador &
para conectarlas. Por ejemplo:
327
328
Parte I:
El lenguaje Java
El problema con este intento es que funcionará solamente con otro objeto Stats cuyo
tipo sea el mismo que el objeto que invoca. Por ejemplo, si el objeto que invoca es de tipo
Stats, entonces el parámetro ob debe también ser tipo Stats. El método
no puede ser utilizado para comparar el promedio de un objeto de tipo Stats con el
promedio de un objeto de tipo Stats. Por consiguiente, esta estrategia no funcionará,
excepto en un contexto muy limitado y no producirá una solución general.
Para crear un método sameAvg( ) genérico, se debe utilizar otra característica de los tipos
parametrizados de Java: los argumentos comodines. Los argumentos comodines son especificados
por el símbolo ?, que representa a un tipo desconocido. A continuación se muestra una forma de
escribir el método sameAvg( ) utilizando un comodín:
// Determina si dos promedios son iguales
// Note el uso de comodines.
boolean sameAvg(Stats> ob) {
if (average( ) == ob.average( ))
return true;
return false;
}
Aquí Stats> se iguala con cualquier objeto Stats, lo cual permite comparar cualquier par de
objetos Stats. El siguiente programa lo demuestra:
// Uso de comodines
class Stats {
T[] nums; // arreglo de valores Number o de alguna subclase de Number
// Pasa al constructor una referencia a
// un arreglo de tipo Number o subclase de Number
Stats (T [] o) {
nums = o;
}
// Devuelve un valor de tipo double en todos los casos.
double average( ) {
double sum = 0.0;
for(int i=0; i < nums.length; i++)
sum += nums[i].doubleValue( );
return sum / nums.length;
}
// Determina si dos promedios son los mismos
// Note el uso de comodines
boolean SameAvg(Stats> ob) {
if(average( ) = = ob.average( ))
return true;
return false;
}
}
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
Double dnums [] = {1.1, 2.2, 3.3, 4.4, 5.5};
Stats dob = new Stats(dnums);
double w = dob.average( );
System.out.println("El promedio de dob es" + w);
Float fnums [] = { 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
Stats fob = new Stats (fnums);
double x = fob.average( );
System.out.println("El promedio de fob es " + x) ;
// Revisa cuál de los arreglos tiene el mismo promedio
System.out.print ("El promedio de iob y dob");
if(iob.sameAvg(dob))
System.out.println("son iguales.");
else
System.out.println ("son diferentes ");
System.out.print("El promedio de iob y fob ");
if(iob.sameAvg(fob))
System.out.println("son iguales.");
else
System.out.println("son diferentes.") ;
}
}
La salida se muestra a continuación:
El promedio
El promedio
El promedio
Promedio de
Promedio de
de iob es
de dob es
de fob es
iob y dob
iob y fob
3.0
3.3
3.0
son diferentes
son iguales.
Un último punto: Es importante entender que los comodines no afectan el tipo de objetos
Stats que pueden ser creados. Esto está controlado por la cláusula extends en la declaración
Stats. El comodín simplemente se iguala con cualquier objeto válido Stats.
Comodines delimitados
Los argumentos comodines pueden ser delimitados de la misma forma que un tipo
parametrizado. Un comodín delimitado es especialmente importante cuando se está creando un
tipo parametrizado que operará sobre una jerarquía de clases. Para entender porqué, veamos un
ejemplo. Considere la siguiente jerarquía de clases que encapsulan coordenadas:
www.detodoprogramacion.com
PARTE I
// Demuestra el uso de comodines
class ComodinDemo {
public static void main(String args[]) {
Integer inums[] = { 1, 2, 3, 4, 5 };
Stats iob = new Stats(inums);
double v = iob.average( );
System.out.println("El promedio de iob es" + v);
329
330
Parte I:
El lenguaje Java
// Coordenadas bidimensionales.
class DosD {
int x, y;
DosD(int a, int b) {
x = a;
y = b;
}
}
// Coordenadas tridimensionales.
class TresD extends DosD {
int z;
TresD(int a, int b, int c) {
super (a, b);
z = c;
}
}
// Coordenadas en cuarta dimensión.
class CuatroD extends TresD {
int t;
CuatroD(int a, int b, int c, int d) {
super (a, b, c);
t = d;
}
}
En la parte superior de la jerarquía está DosD, esta clase encapsula coordenadas
bidimensionales XY. DosD es heredado por TresD, la cual agrega una tercera dimensión,
creando coordenadas XYZ. TresD es heredado por CuatroD, la cual agrega una cuarta
dimensión (tiempo), produciendo una coordenada de cuatro dimensiones.
A continuación se muestra una clase genérica llamada Coords, la cual almacena un arreglo
de coordenadas:
//Esta clase contiene un arreglo de objetos coordenados
class Coords {
T[] cords;
Coords(T[] o) {cords = o;}
}
Note que Coords especifica un parámetro de tipo limitado por DosD. Esto significa que
cualquier arreglo almacenado en un objeto Coords contendrá objetos de tipo DosD o una de
sus subclases.
Ahora, asumiendo que se desea escribir un método que muestre las coordenadas X y Y
para cada elemento en el arreglo coords del objeto de tipo Coords. Dado que todos los tipos
de objetos Coords tienen al menos dos coordenadas (X y Y), es fácil de hacer esto utilizando un
comodín, como se muestra a continuación:
static void muestraXY(Coords > c) {
System.out.println("Coordenadas X Y: ");
for (int i=0; i < c.coords.length; i++)
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
}
Debido a que Coords es un tipo parametrizado limitado que especifica a DosD como su límite
superior, todos los objetos que pueden ser utilizados para crear un objeto Coords serán arreglos
de tipo DosD, o de clases derivadas de DosD. Así, el método muestraXY( ) puede desplegar el
contenido de cualquier objeto Coords.
Sin embargo, ¿qué pasa si se desea crear un método que muestre las coordenadas X, Y y Z
de un objeto TresD o CuatroD? El problema es que no todos los objetos Coords tendrán tres
coordenadas, un objeto Coords sólo tendrá coordenadas X y Y. Por lo tanto, ¿cómo se
escribiría un método que muestre las coordenadas X, Y y Z para un objeto Coords y
Coords, impidiendo que el método sea utilizado por un objeto Coords?
La respuesta es el argumento comodín delimitado.
Un comodín delimitado especifica un límite superior o un límite inferior para el tipo de
argumento. Esto permite restringir el tipo de objetos sobre el cual un método operará. El
comodín delimitado más común es el límite superior, el cual se crea utilizando la cláusula
extends de una forma muy similar a la que se usa para crear un tipo delimitado.
Utilizando un comodín delimitado, es fácil crear un método que muestre las coordenadas
X,Y y Z de un objeto Coords, si el objeto en cuestión tiene tres coordenadas. Por ejemplo,
el siguiente método muestraXYZ( ) despliega las coordenadas X, Y y Z de los elementos
almacenados en el objeto Coords, si dichos elementos son de tipo TresD (o son derivados de
TresD):
static void muestraXYZ(Coords extends TresD> c) {
System.out.println("Coordenadas X Y Z: ");
for (int i=0; i < c.coords.lenght; i++)
System.out.println(c.coords[i].x + " " +
c.coords[i].y + " " +
c.coords[i].z);
System.out.println( );
}
Note que ha sido agregada una cláusula extends en la declaración del comodín
delimitado con el parámetro c. Esto declara que el símbolo ? puede corresponder con
cualquier tipo siempre y cuando sea TresD, o una clase derivada de TresD. Así la cláusula
extends establece un límite superior que el símbolo ? debe satisfacer. Debido a esta
delimitación, el método muestraXYZ( ) puede ser llamado con referencias a objetos de tipo
Coords o Coords, pero no con referencia a tipos Coords.
Intentar llamar a muestraXYZ( ) con una referencia a Coords causará un error en
tiempo de compilación, de esa manera se garantiza seguridad en el manejo de tipos.
A continuación se presenta un programa que muestra en acción a los argumentos con
comodines delimitados:
// Argumentos con comodines delimitados
// Coordenadas bidimensionales
class DosD {
int x, y;
www.detodoprogramacion.com
PARTE I
System.out.println(c.coords[i].x + " " +
c.coords[i].y);
System.out.println( );
331
332
Parte I:
El lenguaje Java
DosD (int a, int b) {
x = a;
y = b;
}
}
// Coordenadas tridimensionales.
class TresD extends DosD {
int z;
TresD(int a, int b, int c) {
super(a, b);
z = c;
}
}
// Coordenadas en cuarta dimensión.
class CuatroD extends TresD {
int t;
CuatroD(int a, int b, int c, int d) {
super (a, b, c);
t = d;
}
}
// Esta clase contiene un arreglo de objetos para coordenadas.
class Coords {
T[] coords;
Coords(T[] o) { coords = o; }
}
// Demuestra el uso de comodines delimitados.
class ComodinDelimitado {
static void showXY(Coords> c) {
System.out.println("Coordenadas XY:");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i).x + " " +
c.coords[i).y);
System.out.println( );
}
static void showXYZ(Coords extends TresD> c) {
System.out.println("Coordenadas XYZ: ");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i].x + " " +
c.coords[i).y + " " +
c.coords [i].z);
System.out.println( );
}
static void showAll(Coords extends CuatroD> c) {
System.out.println("Coordenadas XYZT:");
for(int i=0; i < c.coords.length; i++)
System.out.println(c.coords[i).x + " " +
c.coords[i].y + " " +
www.detodoprogramacion.com
Capítulo 14:
}
public static void main(String args[]) {
DosD td[] = {
new DosD (0, 0),
new DosD(7, 9),
new DosD (18, 4),
new DosD(-l, -23)
};
Coords tdlocs = new Coords(td);
System.out.println("Contenido de tdlocs.");
showXY(tdlocs);
// bien, es un DosD
//
showXYZ(tdlocs); // Error, no es un TresD
//
showAll(tdlocs); // Error, no es un CuatroD
// Ahora, creamos algunos objetos CuatroD
CuatroD fd[] = {
new CuatroD(l, 2, 3, 4),
new CuatroD(6, 8, 14, 8),
new CuatroD(22, 9, 4, 9),
new CuatroD(3, -2, -23, 17)
};
Coords fdlocs = new Coords (fd) ;
System.out.println("Contenido de fdlocs.");
// Todos estos están correctos.
showXY(fdlocs);
showXYZ (fdlocs);
showAll(fdlocs);
}
}
La salida del programa se muestra a continuación:
Contenido de tdlocs.
Coordenadas XY:
0 0
7 9
18 4
-1 -23
Contenido de fdlocs.
Coordenadas XY:
1 2
6 8
22 9
3 -2
Coordenadas XYZ:
1 2 3
6 8 14
22 9 4
3 -2 -23
www.detodoprogramacion.com
333
PARTE I
c.coords[i].z + " " +
c.coords [i].t);
System.out.println( ) ;
Tipos parametrizados
334
Parte I:
El lenguaje Java
Coordenadas X Y Z T:
1 2 3 4
6 8 14 8
22 9 4 9
3 -2 -23 17
Note las siguientes líneas comentadas:
// showXYZ(tdlocs);
// showAll(tdlocs);
// Error, no es un TresD
// Error, no es un CuatroD
Debido a que tdlocs es un objeto Coords(DosD), no puede ser utilizado para llamar a
showXYZ( ) o showAll( ) debido a que el argumento de comodín delimitado lo impide. Para
probarlo, intentemos remover los símbolos de comentario y luego compilar el programa; recibirá
errores de compilación debido a la incompatibilidad de tipos.
En general, para establecer un límite superior para un comodín delimitado, se utiliza la
siguiente expresión de comodín:
extends superclase>
donde superclase es el nombre de la clase que sirve como límite superior. Recuerde que ésta es
una cláusula inclusiva porque la clase que define el límite superior también está considerada
dentro del límite.
También se puede especificar un límite inferior para un comodín delimitado, agregando una
cláusula super a la declaración del comodín delimitado. A continuación su forma general:
super subclase>
En este caso, sólo las clases que son superclases de subclase son argumentos aceptables. Ésta es
una cláusula exclusiva, porque no se considera a la subclase como parte del límite aceptable.
Métodos con tipos parametrizados
Como se mostró en los ejemplos anteriores, los métodos dentro de una clase genérica pueden
hacer uso de los parámetros de tipo de la clase y por consiguiente están automáticamente
ligados al tipo del parámetro. Sin embargo, es posible declarar un método genérico que utilice
uno o más parámetros de tipo propios. También es posible crear métodos genéricos contenidos
en clases no genéricas.
Comencemos con un ejemplo. El siguiente programa declara una clase no genérica llamada
GenMethDemo y un método estático genérico dentro de esa clase llamado estaEn( ). El
método estaEn( ) determina si un objeto es miembro de un arreglo. Este método puede ser
usado con cualquier tipo de objetos y arreglos siempre y cuando el arreglo contenga objetos que
sean compatibles con el tipo de los objetos que están siendo buscados.
// Ejemplo de método con tipos parametrizados
class GenMethDemo{
//Determina si un objeto está en un arreglo
static boolean estaEn(T x, V[] y){
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
335
for (int i=0; i < y.length; i++)
if (x.equals(y[i])) return true;
public static void main(String args[]) {
// Utiliza estaEn( ) sobre Integers.
Integer nums[] = { 1, 2, 3, 4, 5 };
if (estaEn(2, nums))
System.out.println("2 está en nums");
if (!estaEn(7, nums))
System.out.println("7 no está en nums");
System.out.println( ) ;
// Usa estaEn( ) sobre Strings.
String strs[] = { "uno", "dos", "tres" ,
"cuatro", "cinco" };
if (estaEn("dos", strs))
System.out.println{"dos está en strs");
if (!estaEn("siete", strs))
System.out.println{"siete no está en strs");
// ¡ups, no compilará! Los tipos deben ser compatibles.
// if (estaEn("dos", nums))
// System.out.println("dos está en strs");
}
}
La salida del programa se muestra a continuación:
2 está en nums
7 no está en nums
dos está en strs
siete no está en strs
Examinemos estaEn( ) más de cerca. Primero, note como se declara el método en la
siguiente línea:
static boolean estaEn(T x, V[] y) {
Los parámetros de tipo están declarados antes del tipo de retorno del método. Segundo, note
que el tipo V está limitado por T. Así, V debe ser el mismo tipo T, o una subclase de T. Esta
relación exige que estaEn( ) puede ser llamado sólo con argumentos que son compatibles.
También note que estaEn( ) es estático, habilitándolo para ser llamado independientemente de
cualquier objeto. Sin embargo, debe tenerse en cuenta que los métodos genéricos pueden ser
tanto estáticos como no estáticos. No hay restricción en este sentido.
Ahora, note como estaEn( ) es llamado dentro de main( ) utilizando una sintaxis
tradicional, sin la necesidad de especificar algún argumento de tipo. Esto es debido a que los
www.detodoprogramacion.com
PARTE I
return false;
}
336
Parte I:
El lenguaje Java
argumentos de tipos son automáticamente percibidos, y los tipos de T y V son ajustados como
corresponde. Por ejemplo, en la primera llamada:
if (estaEn(2, nums))
el tipo del primer argumento es Integer (debido al autoboxing), lo cuál causa que Integer sea
sustituido por T. El tipo base del segundo argumento es también Integer, lo cual ocasiona que
Integer también sea sustituido por V.
En la segunda llamada, el tipo String se utiliza como tipo para T y V. Observe el código
comentado, mostrado a continuación:
//
//
if (estaEn("dos", nums))
System.out.println("dos está en strs");
Si se remueven los comentarios y después se intenta compilar el programa, se recibirá un
mensaje de error. La razón es que el parámetro de tipo V está limitado por T con la cláusula
extends en la declaración de V. Esto significa que V debe ser de tipo T o una subclase de T. En
este caso, el primer argumento es de tipo String, haciendo a T de tipo String, pero el segundo
argumento es de tipo Integer, el cual no es una subclase de String. Esto genera un error de tipos
incompatibles en tiempo de compilación. Esta habilidad de forzar la seguridad en el manejo de
tipos es una de las ventajas más importantes de los métodos con tipos parametrizados.
La sintaxis utilizada para crear estaEn( ) puede ser generalizada. A continuación se muestra
la sintaxis para métodos con tipos parametrizados.
tipo-retorno nombre-metodo(lista-parametros){//...
En todos los casos, lista-param-tipo es una lista de tipos de parámetros separados por comas.
Note que para un método con tipos parametrizados, la lista de tipos de parámetros precede al
tipo de valor devuelto por el método.
Constructores con tipos parametrizados
También es posible hacer constructores con tipos parametrizados, incluso si sus clases no lo son.
Por ejemplo, considere el siguiente programa:
// Uso de constructores con tipos parametrizados.
class GenCons {
private double val;
GenCons(T arg) {
val = arg.doubleValue( );
}
void showval( ) {
System.out.println ("valor: " + val);
}
}
class GenConsDemo {
public static void main(String args[]) {
GenCons test = new GenCons(l00);
GenCons test2 = new GenCons(123.5F);
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
337
test.showval( );
test2.showval( );
PARTE I
}
}
La salida se muestra aquí:
valor: 100.0
valor: 123.5
Dado que GenCos( ) especifica un parámetro genérico, el cual debe ser una subclase de
Number, GenCos( ) puede ser llamado con cualquier tipo numérico, incluyendo Integer, Float,
o Double. Por lo tanto, aunque GenCos no es una clase genérica, su constructor es genérico.
Interfaces con tipos parametrizados
Además de las clases y métodos con tipos parametrizados, se pueden también tener interfaces
con tipos parametrizados. Las interfaces parametrizadas se especifican igual que las clases
parametrizadas. Veamos un ejemplo, que crea una interfaz llamada MinMax que declara los
métodos min( ) y max( ), los cuales se espera regresen el valor mínimo y el valor máximo de un
conjunto de objetos.
// Un ejemplo de interfaz con tipos parametrizados
// La interfaz MinMax
interface MinMax> {
T min( );
T max( );
}
// Ahora, una implementación de MinMax
class MiClase> implements MinMax {
T[] vals;
MiClase(T[] o) { vals = o; }
// Devuelve el valor mínimo en vals.
public T min ( ) {
T v = vals[0];
for(int i=l; i < vals.length; i++)
if(vals[i].compareTo(v) < 0) v = vals[i];
return v;
}
// Devuelve el valor máximo en vals.
public T max ( ) {
T v = vals [0];
for(int i=l; i < vals.length; i++)
if (vals [i].compareTo(v) > 0) v = vals[i];
return v;
}
}
www.detodoprogramacion.com
338
Parte I:
El lenguaje Java
class GenIFDemo {
public static void main(String args[]) {
Integer inums[] = {3, 6, 2, 8, 6 };
Character chs [] = {'b', 'r', 'p', 'w'};
MiClase iob = new MiClase (inums);
MiClase cob = new MiClase(chs);
System.out.println("Valor máximo en inums: " + iob.max( ));
System.out.println("Valor mínimo en inums: " + iob.min( ));
System.out.println("Valor máximo en chs: " + cob.max( ));
System.out.println("Valor mínimo en chs: " + cob.min{));
}
}
La salida se muestra a continuación:
Valor
Valor
Valor
Valor
máximo
mínimo
máximo
mínimo
en
en
en
en
inums: 8
inums: 2
chs: w
chs: b
Aun cuando la mayoría de los aspectos de este programa deberían ser fáciles de entender, es
necesario realizar un par de observaciones. Primero, note que MinMax está declarada como
sigue:
interface MinMax> {
En general, una interfaz con tipos parametrizados se declara de la misma forma que una clase
con tipos parametrizados. En este caso, el tipo de parámetro es T, y su límite superior es
Comparable, la cual es una interfaz definida por java.lang. Una clase que implementa
a Comparable define objetos que pueden ser ordenados. De esta forma, usar a Comparable
como límite superior asegura que MinMax sólo puede ser utilizada con objetos que son capaces
de ser comparados (véase el Capítulo 16 para mayor información de Comparable). Nótese que
Comparable es también una interfaz genérica (fue mejorada en JDK 5). Comparable tiene un
parámetro de tipo que especifica el tipo de los objetos que se están comparando.
A continuación, MiClase implementa a MinMax. Nótese la declaración de MiClase, que se
muestra aquí:
class MiClase implements MinMax {
Pongamos especial atención en la forma en que el parámetro de tipo, llamado T, está declarado
por MiClase y luego es pasado a MinMax. Debido a que MinMax requiere un tipo que
implemente de Comparable, la clase implementada (MiClase en este caso) debe especificar el
mismo límite. Más aún, una vez que dicho límite ha sido establecido, no hay necesidad de
especificarlo de nuevo en la cláusula de implementación. De hecho, estaría mal hacerlo. Por
ejemplo, esta línea es incorrecta y no compilará:
//Esto está mal.
class MiClase >
implements MinMax> {
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
class MiClase implements MinMax { // error
Dado que MiClase no declara un parámetro de tipo, no hay forma de pasar uno a MinMax.
En este caso, el identificador T es simplemente desconocido, y el compilador reportará un error.
Claro está, que si una clase implementa a la interfaz genérica proporcionando un tipo especifico,
como la que se muestra a continuación:
class MiClase implements MinMax { // correcto
entonces la implementación de la clase no necesita utilizar tipos parametrizados.
La interfaz con tipos parametrizados ofrece dos beneficios. Primero, puede ser
implementada por diferentes tipos de datos. Segundo, permite colocar restricciones (esto es,
límites) sobre el tipo de dato con los cuales la interfaz puede ser implementada. En el ejemplo de
MinMax, sólo tipos que implementan de la interfaz Comparable pueden ser pasados a T.
Aquí está la sintaxis generalizada para una interfaz con tipos parametrizados:
interface nombre-interfaz {//…
Donde, tipo-param-lista debe ser una lista de parámetros de tipo separados por coma. Cuando
una interfaz con tipos parametrizados es implementada, es necesario especificar los argumentos
de tipo, como se muestra a continuación:
class nombre-clase
implements nombre-interfaz {
Compatibilidad entre el código de versiones anteriores
y los tipos parametrizados
Dado que el soporte para tipos parametrizados es una adición reciente a Java, fue necesario
proveer algún camino de transición del código viejo previo a los tipos parametrizados. Al
momento de estar escribiendo este libro, existen aún millones y millones de líneas de código sin
tipos parametrizados que se deben mantener funcionales y compatibles con código nuevo que
utiliza tipos parametrizados. Los códigos previos a los tipos parametrizados deben ser capaces
de funcionar con tipos parametrizados y el código con tipos parametrizados debe ser capaz de
funcionar con código previo a los tipos parametrizados.
Para gestionar la transición hacia tipos parametrizados, Java permite a una clase con tipos
parametrizados ser utilizada sin ningún argumento de tipo. Esto crea un tipo en bruto para la
clase. Este tipo en bruto es compatible con los códigos anteriores, que no tiene conocimiento
de los tipos parametrizados. El principal inconveniente de utilizar el tipo en bruto es que la
seguridad en el manejo de tipos proporcionada por el uso de tipos parametrizados se pierde.
www.detodoprogramacion.com
PARTE I
Una vez que el tipo de parámetro ha sido establecido, simplemente se pasa a la interfaz sin
mayor modificación.
En general, si una clase implementa de una interfaz con tipos parametrizados, entonces las
clases también deben ser de tipos parametrizados, al menos extender de una, ya que requiere
un parámetro de tipo para pasarlo a la interfaz. Por ejemplo, el siguiente intento de declarar
MiClase es un error:
339
340
Parte I:
El lenguaje Java
A continuación se muestra un ejemplo:
// Uso de un tipo en bruto
class Gen {
T ob;
// declara un objeto de tipo T
// Pasa al constructor una referencia a
// un objeto de tipo T.
Gen(T o} {
ob = o;
}
// Devuelve ob.
T getob ( ) {
return ob;
}
}
// Uso del tipo en bruto.
class RawDemo {
public static void main(String args[]) {
// Crea un objeto de tipo Gen para Integer.
Gen iOb = new Gen (88);
// Crea un objeto Gen para String.
Gen strOb : new Gen ("Prueba de tipos parametrizados"};
// Crea un objeto Gen con tipo en bruto y le asigna
// un valor Double
Gen raw = new Gen(new Double(98.6));
// Es necesario hacer una conversión de tipos aquí,
dado que el tipo es desconocido
double d = (Double) raw.getob( );
System.out.println ("valor: " + d);
// El uso de un tipo en bruto puede generar una excepción en tiempo
// de ejecución. Aquí tenemos algunos ejemplos.
//
// La siguiente conversión causa un error en tiempo de ejecución
int i = (Integer) raw.getob( ); // error en tiempo de ejecución
// Esta asignación pasa por alto la seguridad de tipos
strOb = raw; // es correcto, pero potencialmente erróneo
//
String str = strOb.getob( ); // error en tiempo de ejecución
//
}
}
// Esta asignación también pasa por alto la seguridad de tipos
raw = iOb; // es correcto, pero potencialmente erróneo
d = (Double) raw.getob( ); // error en tiempo de ejecución
Este programa contiene muchas cosas interesantes. Primero, un objeto de tipo Gen con tipo
parametrizado en bruto se crea mediante la siguiente declaración:
Gen raw = new Gen(new Double(98.6));
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
// int i = (Integer) raw.getob( ); // error en tiempo de ejecución
En esta sentencia, se obtiene el valor del atributo ob del objeto llamado raw, y su valor es
convertido en un Integer. El problema es que el objeto raw contiene un valor Double, no un
valor Integer. Sin embargo, esto no puede ser detectado en tiempo de compilación puesto que
el tipo del objeto raw se desconoce. De esta forma, esta sentencia falla en tiempo de ejecución.
La siguiente secuencia asigna a strOb (referencia de tipo Gen) una referencia a un
objeto Gen de tipo bruto:
strOb = raw; // es correcto, pero potencialmente erróneo
//
String str = strOb.getob( ); // error en tiempo de ejecución
Esta sentencia, por sí misma, es sintácticamente correcta, pero cuestionable. Puesto que strOb
es de tipo Gen, se asume que contiene un String. Sin embargo, después de la
asignación, el objeto referido por strOb contiene un Double. De esta manera, en tiempo de
ejecución, cuando se intente asignar el contenido de strOb a str, el resultado será un error en
tiempo de ejecución, porque strOb contiene un Double. Así, la asignación de un tipo en bruto
por referencia a un tipo parametrizado pasa de lado el mecanismo de revisión de seguridad de
tipos.
La siguiente secuencia invierte el caso anterior
//
raw = iOb; // es correcto, pero potencialmente erróneo
d = (Double) raw.getob( ); // error en tiempo de ejecución
Aquí, un tipo parametrizado se asigna a una referencia de una variable de tipo en bruto. Aunque
esta sentencia es sintácticamente correcta, puede dar problemas, como se ilustra en la segunda
línea. En este caso, el objeto raw hace referencia a un objeto que contiene un objeto Integer,
pero la conversión asume que contiene un Double. Este error no se puede prevenir en tiempo
de compilación. Por el contrario, causa un error en tiempo de ejecución.
A causa del peligro potencial inherente a los tipos en brutos, javac muestra una advertencia
de tipos no comprobados cuando un tipo en bruto es utilizado en una forma que podría poner
en peligro la seguridad de tipos. En el programa anterior, las siguientes líneas provocan
advertencias de tipos no comprobados:
Gen raw = new Gen(new Double(98.6));
strOb = raw; // es correcto, pero potencialmente erróneo
En la primera línea, la llamada al constructor Gen sin el argumento de tipo causa la advertencia.
En la segunda línea, la asignación de una referencia de tipo en bruto a una variable de tipo
parametrizado es lo que genera la advertencia.
www.detodoprogramacion.com
PARTE I
Note que no hay argumentos de tipos especificados. En esencia, esto crea un objeto Gen cuyo
tipo T se reemplaza por Object.
Un tipo en bruto no es seguro. De esta manera, una variable de un tipo en bruto puede ser
asignada como referencia a cualquier tipo de objeto Gen. Al inverso también está permitido;
una variable de un tipo específico Gen puede ser asignada como referencia a un objeto Gen
de tipo en bruto. Sin embargo, ambas operaciones son potencialmente inseguras porque el
mecanismo de revisión de tipos parametrizados es evadido.
Esta falta de seguridad se ilustra con las líneas comentadas al final del programa.
Examinemos cada caso. Primero, considere la siguiente situación:
341
342
Parte I:
El lenguaje Java
Al principio, se podría pensar que la siguiente línea debería generar también una advertencia
de tipo no comprobado, pero no lo hace:
raw = iOb; // es correcto, pero potencialmente erróneo
No se genera ninguna advertencia en tiempo de compilación porque la asignación no causa una
pérdida de seguridad en el manejo de tipos más allá de la que ya ha ocurrido cuando el objeto
llamado raw fue creado.
Un punto final: se debe limitar el uso de tipos en brutos a aquellos casos en los cuales se
requiere mezclar código antiguo con código nuevo con tipos parametrizados. Los tipos en bruto
son simplemente una característica de transición y no algo que deba ser utilizado en códigos
nuevos.
Jerarquía de clases con tipos parametrizados
Las clases con tipos parametrizados pueden ser parte de una jerarquía de clases de la misma
forma en la que lo son las clases sin tipos parametrizados. De esta forma, una clase con tipos
parametrizados puede actuar como una superclase o ser una subclase. La diferencia clave
entre las jerarquías de clases con tipos parametrizados y sin tipos parametrizados es que en
una jerarquía con tipos parametrizados, cualquier argumento de tipo que sea requerido por
una superclase con tipos parametrizados debe ser proporcionado a la jerarquía por todas las
subclases. Esto es similar a la forma en que los argumentos del constructor se pasan en la
jerarquía.
Superclases con tipos parametrizados
El siguiente es un ejemplo simple de una jerarquía que utiliza una superclase con tipos
parametrizados:
// Una jerarquía de clases simples con tipos parametrizados
class Gen {
T ob;
Gen(T o) {
ob = o;
}
// Devuelve ob.
T getob ( ) {
return ob;
}
}
// Una subclase de Gen.
class Gen2 extends Gen {
Gen2(T o) {
super (o) ;
}
}
En esta jerarquía, Gen2 extiende la clase genérica Gen. Note que Gen2 se declara con la
siguiente línea:
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
343
class Gen2 extends Gen {
Gen2 num = new Gen2 (100);
pasa Integer como el parámetro de tipo para Gen. De esta forma, el atributo ob dentro de Gen
que es parte también de Gen2 será de tipo Integer.
Note también que Gen2 no utiliza el parámetro de tipo T excepto para pasarlo a la
superclase Gen. De esta manera, incluso si una subclase de una superclase genérica no requiere
ser genérica, aún así debe especificar el parámetro de tipo requerido por su superclase genérica.
Claro que, una subclase es libre de agregar sus propios parámetros, si los requiere. Por
ejemplo, aquí está una variación de la jerarquía anterior en la cual Gen2 agrega sus propios tipos
parametrizados:
// Una subclase puede agregar sus propios parámetros de tipo
class Gen {
T ob; // declara un objeto de tipo T
// Pasa al constructor una referencia a
// un objeto de tipo T.
Gen(T o) {
ob = o;
}
// Devuelve ob.
T getob ( ) {
return ob;
}
}
// Una subclase de Gen que define un segundo
// tipo de parámetro, llamado V.
class Gen2 extends Gen {
V ob2;
Gen2(T o, V o2) {
super (o) ;
ob2 = o2;
}
V getob2( ) {
return ob2;
}
}
// Crea un objeto de tipo Gen2.
class JerarquiaDemo {
public static void main(String args[]) {
www.detodoprogramacion.com
PARTE I
El parámetro de tipo T es especificado por Gen2 y también es pasado a Gen en la cláusula
extends. Esto significa que cualquier tipo que se pasa a Gen2 también se pasará a Gen. Por
ejemplo, la siguiente declaración:
344
Parte I:
El lenguaje Java
// Crea un objeto Gen2 para String e Integer.
Gen2 x =
new Gen2("El valor es: ", 99);
System.out.print(x.getob( ));
System.out.println(x.getob2( ));
}
}
Observe la declaración de esta versión de Gen2, mostrada a continuación:
class Gen2 extends Gen {
Donde T es el tipo que se pasa a Gen, y V es el tipo que se específica en Gen2. V se utiliza para
declarar un objeto llamado ob2, y como tipo de retorno para el método getob2( ). En el método
main( ) se crea un objeto Gen2 en el cual el parámetro de tipo T es String, y el parámetro de
tipo V es Integer. El programa muestra el siguiente resultado:
El valor es: 99
Subclases con tipos parametrizados
Es perfectamente válido para una clase sin tipos parametrizados ser la superclase de una
subclase con tipos parametrizados. Por ejemplo, considere el siguiente programa:
// Una clase sin tipos parametrizados puede ser una superclase
// de una subclase con tipos parametrizados
// Una clase sin tipos parametrizados
class NoGen {
int num;
NoGen(int i) {
num = i;
}
int getnum( ) {
return num;
}
}
// Una subclase con tipos parametrizados
class Gen extends NoGen {
T ob; // declara un objeto de tipo T
// Pasa al constructor una referencia a
// un objeto de tipo T.
Gen(T o, int i) {
super(i) ;
ob = o;
}
// Devuelve ob.
T getob( ) {
return ob;
}
}
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
// Crea un objeto Gen para String.
Gen w = new Gen ("Hola", 47);
System.out.print(w.getob( ) + " ");
System.out.println(w.getnum( )) ;
}
}
La salida del programa se muestra a continuación:
Hola 47
En el programa, note como Gen hereda de NoGen en la siguiente declaración:
class Gen extends NoGen {
Dado que NoGen no utiliza tipos parametrizados, no se le especifica ningún argumento de tipo.
Así, aunque Gen declara el parámetro de tipo T, éste no es requerido (ni puede ser utilizado) por
NoGen. De esta forma, NoGen es heredado por Gen en la forma normal. No se aplica ninguna
condición especial.
Comparación de tipos en tiempo de ejecución
Recordemos al operador instanceof descrito en el Capítulo 13. Como se explicó, instanceof
determina si un objeto es una instancia de una clase. El operador devuelve verdadero si un
objeto pertenece al tipo especificado o bien puede ser convertido en dicho tipo. El operador
instanceof puede ser aplicado a objetos de clases con tipos parametrizados. La siguiente clase
es un ejemplo de las implicaciones de una jerarquía de clases con tipos parametrizados en la
compatibilidad de tipos:
// Uso del operador instanceof con una jerarquía de clases
con tipos parametrizados
class Gen {
T ob;
Gen(T o) {
ob = o;
}
// Devuelve ob.
T getob ( ) {
return ob;
}
}
// Una subclase de Gen.
class Gen2 extends Gen {
Gen2 (T o) {
super (o) ;
}
}
www.detodoprogramacion.com
PARTE I
// Crea un objeto de tipo Gen
class JerarquiaDemo2 {
public static void main(String args[]) {
345
346
Parte I:
El lenguaje Java
// Implicaciones de los tipos parametrizados en jerarquía de clases
// con el operador instanceof
class JerarquiaDemo3 {
public static void main(String args[]) {
// Crea un objeto Gen para objetos Integer.
Gen iOb = new Gen (88);
// Crea un objeto Gen2 para objetos Integer.
Gen2 iOb2 = new Gen2 (99);
// Crea un objeto Gen2 para objetos String.
Gen2 strOb2 = new Gen2 ("Prueba de tipos parametrizados");
// Ve si iOb2 tiene alguna forma de Gen2.
if (iOb2 instanceof Gen2>)
System.out.println("iOb2 es instancia de Gen2");
// Ve si iOb2 tiene alguna forma de Gen
if (iOb2 instanceof Gen>)
System.out.println("iOb2 es instancia de Gen");
System.out.println( );
// Ve si strOb2 es un Gen2.
if (strOb2 instanceof Gen2>)
System.out.println("strOb2 es instancia de Gen2");
// Ve si strOb2 es un Gen.
if(strOb2 instanceof Gen>)
System.out.println("strOb2 es instancia de Gen");
System.out.println( );
// Ve si iOb es una instancia de Gen2. No lo es.
if(iOb instanceof Gen2>)
System.out.println("iOb es instancia de Gen2");
// Ve si iOb es una instancia de Gen. Si lo es.
if (iOb instanceof Gen>)
System.out.println("iOb es instancia de Gen");
// Lo siguiente no puede ser compilado porque
// la información de tipos parametrizados no existe en tiempo de ejecución
//
if(iOb2 instanceof Gen2)
//
System.out.println("iOb2 es instancia de Gen2");
}
}
La salida del programa se muestra a continuación:
iOb2 es una instancia de Gen2
iOb2 es una instancia de Gen
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
347
strOb2 es una instancia de Gen2
strOb2 es una instancia de Gen
En este programa, Gen2 es una subclase de Gen, la cual tiene un tipo parametrizado
llamado T. En el método main( ), se crean tres objetos. El primero es iOb, el cual es un objeto
de tipo Gen. El segundo es iOb2, el cual es una instancia de Gen2.
Finalmente, strOb2 que es un objeto de tipo Gen2.
Entonces, el programa ejecuta las siguientes pruebas con instanceof sobre el tipo de iOb2:
// Ve si iOb2 tiene alguna forma de Gen2.
if (iOb2 instanceof Gen2>)
System.out.println("iOb2 es instancia de Gen2");
// Ve si iOb2 tiene alguna forma de Gen
if (iOb2 instanceof Gen>)
System.out.println("iOb2 es instancia de Gen");
Como lo muestra la salida, ambos casos son exitosos. En la primer prueba, iOb2 se revisa
contra Gen2>. Esta prueba es exitosa simplemente porque confirma que iOb2 es un objeto
de algún tipo de Gen2. El uso del comodín permite al operador instanceof determinar si iOb2
es un objeto de cualquier tipo de Gen2. El siguiente, iOb2 se prueba contra Gen>, el tipo
superclase. Esto también es exitoso porque iOb2 es alguna forma de Gen, la superclase. Las
siguientes líneas, en el método main( ) muestran la misma secuencia (y mismos resultados) para
strOb2.
A continuación, iOb, la cual es una instancia de Gen (la superclase), se prueba
con estas líneas:
// Ve si iOb es una instancia de Gen2. No lo es.
if(iOb instanceof Gen2>)
System.out.println("iOb es instancia de Gen2");
// Ve si iOb es una instancia de Gen. Si lo es.
if (iOb instanceof Gen>)
System.out.println("iOb es instancia de Gen");
La primera condición falla porque iOb no es de ningún tipo de Gen2. La segunda condición es
exitosa porque iOb es de algún tipo de Gen.
Ahora, observemos más de cerca de las líneas comentadas:
// Lo siguiente no puede ser compilado porque
// la información de tipos parametrizados no existe en tiempo de ejecución
//
if(iOb2 instanceof Gen2)
//
System.out.println("iOb2 es instancia de Gen2");
Como los comentarios lo indican, estas líneas no pueden ser compiladas porque intentan
comparar iOb2 con un tipo específico de Gen2, en este caso, Gen2. Recuerde, que
no hay información de tipos parametrizados disponible en tiempo de ejecución. Además, no
hay forma de que el operador instanceof sepa si iOb2 es una instancia de Gen2 o
no.
www.detodoprogramacion.com
PARTE I
iOb es una instancia de Gen
348
Parte I:
El lenguaje Java
Conversión de tipos
Se puede convertir una instancia de una clase genérica en otra sólo si las dos son compatibles de
alguna forma y sus argumentos de tipos son los mismos. Por ejemplo, en el programa anterior,
esta conversión es correcta:
(Gen) iOb2
// es correcto
debido a que iOb2 es una instancia de Gen. Pero, la conversión:
(Gen) iOb2 // es incorrecta
no es correcta porque iOb2 no es una instancia de Gen
Sobreescritura de métodos en clases con tipos parametrizados
Un método en una clase con tipos parametrizados puede ser sobrescrito como cualquier otro
método. Por ejemplo, en el siguiente programa el método getob( ) es sobrescrito:
// Sobrescribe un método con tipos parametrizados en una clase
con tipos parametrizados
class Gen {
T ob; // declara un objeto de tipo T
// Pasa al constructor una referencia a
// un objeto de tipo T.
Gen(T o) {
ob = o;
}
// Devuelve ob.
T getob ( ) {
System.out.print("Llamada al método getob( ) de Gen: ");
return ob;
}
}
// Una subclase de Gen que sobrescribe getob( ).
class Gen2 extends Gen {
Gen2(T o) {
super(o);
}
// Sobrescribe getob( ).
T getob ( ) {
System.out.print("El método getob( ) de Gen2: ");
return ob;
}
}
// Sobreescritura de un método con tipos parametrizados
class SobrescrituraDemo {
public static void main(String args[]) {
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
349
// Crea un objeto Gen para Integer.
Gen iOb = new Gen (88);
// Crea un objeto Gen2 para String.
Gen2 strOb2 = new Gen2 ("Prueba de tipos parametrizados");
System.out.println(iOb.getob( ));
System.out.println(iOb2.getob( ));
System.out.println(strOb2.getob( ));
}
}
La salida se muestra a continuación:
El método getob( ) de Gen: 88
El método getob( ) de Gen2: 99
El método getob( ) de Gen2: Prueba de tipos parametrizados
Cómo están implementados los tipos parametrizados
Usualmente, no es necesario saber los detalles acerca de cómo el compilador de Java
transforma el código fuente en código objeto. Sin embargo, en el caso de los tipos
parametrizados, es importante entender de manera general el proceso debido a que
explica por qué las características de tipos parametrizados funcionan de la manera en que lo
hacen – y por qué su comportamiento es, en algunas ocasiones un tanto sorprendente. Por
esta razón, es necesario comentar brevemente cómo los tipos parametrizados están
implementados en Java.
Una restricción importante que controla la forma en que los tipos parametrizados fueron
agregados a Java fue la necesidad de mantener la compatibilidad con las versiones previas
de Java. Dicho de forma simple, el código con tipos parametrizados tiene que ser compatible
con el código pre-existente de tipos no parametrizados. Cualquier cambio en la sintaxis del
lenguaje de Java o de la JVM, tuvo que evitar la ruptura del código anterior. La forma en que
Java implementa los tipos parametrizados satisfaciendo esta restricción es a través del uso de la
técnica de la cancelación.
En general, así es como la cancelación funciona. Cuando el código de Java se compila, toda
la información de tipos parametrizados se elimina (cancela). Esto significa que se reemplaza el
parámetro de tipo con el tipo correspondiente, el cual es Object si no hay tipos especificados
explícitamente, y luego se aplican cambios de tipos (como se determinó en los argumentos de
tipo) para mantener la compatibilidad de tipos con los tipos especificados por los argumentos.
El compilador también implementa este tipo de compatibilidad. Esta estrategia de tipos
parametrizados significa que no existen parámetros de tipo en tiempo de ejecución. Son
simplemente un mecanismo de codificación.
La mejor forma de entender cómo trabaja la técnica de la cancelación es revisar las
siguientes dos clases:
www.detodoprogramacion.com
PARTE I
// Crea un objeto Gen2 para Integer.
Gen2 iOb2 = new Gen2(99);
350
Parte I:
El lenguaje Java
// Aquí, por omisión T es reemplazada por Object
class Gen {
T ob; // aquí, T será reemplazada por Object
Gen(T o) {
ob = o;
}
// Devuelve ob.
T getob ( ) {
return ob;
}
}
// Aquí, T es delimitado por String.
class GenStr {
T str; // aquí, T será reemplazada por String
GenStr(T o) {
str = o;
}
T getstr( ) { return str; }
}
Después de que estas dos clases son compiladas, la T en Gen será reemplazado por Object.
La T en GenStr será reemplazada por String. Se puede confirmar esto ejecutando javap sobre
las clases compiladas. El resultado se muestra a continuación:
class Gen extends java.lang.Object{
java.lang.Object ob;
Gen(java.lang.Object);
java.lang.Object getob();
}
class GenStr extends java.lang.Object{
java.lang.String str;
GenStr (java.lang.String);
java.lang.String getstr();
}
Dentro del código de Gen y GenStr, la conversión de tipos se utiliza para asegurar la
tipificación correcta. Por ejemplo, esta secuencia:
Gen iOb = new Gen(99);
int x = iOb.getob();
será compilada como si hubiera sido escrita así:
Gen iOb = new Gen(99) ;
int x = (Integer) iOb.getob();
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
class GenTypeDemo {
public static void main(String args[]) {
Gen iOb = new Gen (99);
Gen fOb = new Gen(102.2F);
System.out.println(iOb.getClass().getName());
System.out.println(fOb.getClass().getName());
}
}
La salida de este programa se muestra a continuación:
Gen
Gen
Como se puede ver, el tipo tanto de iOb como de fOb es Gen, no Gen y
Gen como se podría esperar. Recuerde, todos los tipos parametrizados son eliminados
durante la compilación. En tiempo de ejecución, sólo existen tipos en bruto.
Métodos puente
Ocasionalmente, el compilador necesitará agregar un método puente a una clase para gestionar
situaciones en las cuales la técnica de cancelación aplicada a un método sobrescrito en una
subclase no produce la misma cancelación que el método en la superclase. En este caso, se
genera un método que utiliza al tipo cancelado de la superclase, y este método llama al método
que tiene el tipo cancelado especificado por la subclase. Por supuesto, los métodos puente sólo
ocurren a nivel de bytecode, no son vistos por el programador y no están disponibles para su uso.
Aunque los métodos puente no son algo que normalmente deba preocuparnos, es educativo
ver la situación en la cual se generan. Considere el siguiente programa:
// Una situación en que se crea un método puente
class Gen {
T ob; // declara un objeto de tipo T
// Pasa al constructor una referencia a
// un objeto de tipo T.
Gen(T o) {
ob = o;
}
// Devuelve ob.
T getob () {
return ob;
}
}
// Una subclase de Gen.
class Gen2 extends Gen {
www.detodoprogramacion.com
PARTE I
Debido a la técnica de la cancelación, algunas cosas funcionan un poco diferente de lo que
se podría pensar. Por ejemplo, considere este pequeño programa que crea dos objetos de tipos
parametrizados de la clase Gen:
351
352
Parte I:
El lenguaje Java
Gen2(String o) {
super (o);
}
// Sobreescritura de getob().
String getob() {
System.out.print("Se llama al método String getob(): ");
return ob;
}
}
// Demuestra la situación que requiere un método puente.
class BridgeDemo {
public static void main(String args[]) {
// Crea un objeto Gen2 para String
Gen2 strOb2 = new Gen2("Prueba de tipos parametrizados");
System.out.println(strOb2ogetob()) ;
}
}
En el programa, la subclase Gen2 extiende de Gen, pero utiliza String como parámetro de
tipo para Gen:
class Gen2 extends Gen {
Además, dentro de Gen2, el método getob( ) se sobrescribe definiendo String como su tipo de
retorno:
// Sobreescritura de getob().
String getob() {
System.out.print("Se llama al método String getob(): ");
return ob;
}
Todo esto es perfectamente aceptable. El único problema es que a causa del tipo cancelado, la
forma esperada de getob( ) será:
Object getob(){//...
Para gestionar este problema, el compilador genera un método puente con la firma anterior que
llama a la versión del método con String. De esta forma, si se examina el archivo de la clase
Gen2 utilizando javap, se verán los siguientes métodos:
class Gen2 extends Gen{
Gen2(java.lang.String);
java.lang.String getob();
java.lang.Object getob(); // método puente
}
Como se puede ver, el método puente ha sido incluido (el comentario fue agregado por el autor,
y no por javap).
Existe un último punto a resaltar sobre los métodos puente. Note que la única diferencia
entre los dos métodos getob( ) está en el tipo de retorno. Normalmente, esto causaría un error,
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
Errores de ambigüedad
La inclusión de tipos parametrizados permite el surgimiento de un nuevo tipo de error del cual
debemos tener cuidado: ambigüedad. Los errores de ambigüedad ocurren cuando la técnica de
cancelación causa dos declaraciones aparentemente distintas de tipos parametrizados para
resolver el mismo tipo cancelado, ocasionando un conflicto. A continuación hay un ejemplo que
involucra sobrecarga de métodos:
// Ambigüedad causada por la técnica de la cancelación aplicada a
// métodos sobrecargados
class MyGenClass {
T obl;
V ob2;
// . . .
// Estos dos métodos sobrecargados son ambiguos
// y no compilarán.
void set(T o) {
obl = o;
}
void set(V o) {
ob2 = o;
}
}
Note que MyGenClass declara dos tipos parametrizados: T y V. Dentro de MyGenClass,
se hace un intento para sobrecargar set( ) con base a los parámetros de tipo T y V. Esto parece
razonable porque T y V parecen ser tipos diferentes. Sin embargo, existen dos problemas de
ambigüedad aquí.
Primero, debido a la forma en que MyGenClass está escrita, no hay requerimientos de que
T y V sean diferentes tipos. Por ejemplo, es perfectamente correcto (en principio) construir un
objeto MyGenClass como se muestra a continuación:
MyGenClass obj = new MyGenClass()
En este caso, ambos T y V serán reemplazados por String. Esto hace que ambas versiones de
set( ) sean idénticas, lo cuál, por supuesto, es un error.
El segundo y más importante problema es que el tipo cancelado de set( ) reduce ambas
versiones a lo siguiente:
void set(Object o) { // ...
De esta forma, la sobrecarga del método set( ) intentada en MyGenClass es intrínsicamente
ambigua.
Los errores de ambigüedad pueden ser difíciles de arreglar. Por ejemplo, si se conoce que V
será siempre de tipo String, se puede intentar arreglar MyGenClass escribiendo nuevamente
su declaración como se muestra a continuación:
www.detodoprogramacion.com
PARTE I
pero debido a que esto no está presente en el código fuente, no se genera ningún problema y la
ejecución se realiza de forma correcta por la JVM.
353
354
Parte I:
El lenguaje Java
class MyGenClass { // casi correcto
Este cambio causa que MyGenClass compile, e incluso se pueden instanciar objetos como el
que se muestra aquí:
MyGenClass x = new MyGenClass();
Esto funciona debido a que Java puede determinar exactamente a cuál método llamar. Sin
embargo, la ambigüedad vuelve cuando se intenta esta línea:
MyGenClass x = new MyGenClass();
En este caso, dado que tanto T como V son String, ¿cuál versión de set( ) será llamada?
Francamente, en el ejemplo anterior, sería mucho mejor utilizar dos métodos con
nombres separados, en lugar de intentar sobrecargar set( ). Frecuentemente, la solución para
la ambigüedad envuelve la reestructuración del código, porque frecuentemente la ambigüedad
significa que se tiene un error conceptual en el diseño.
Restricciones de los tipos parametrizados
Hay algunas restricciones que es necesario tener en mente cuando se utilizan tipos
parametrizados. Éstas involucran la creación de objetos de un parámetro de tipo, miembros
estáticos, excepciones, y arreglos. Cada una se examina a continuación.
Los tipos parametrizados no pueden ser instanciados
No es posible crear una instancia de un parámetro de tipo. Por ejemplo, considere el siguiente
caso:
//No se puede crear una instancia de T
class Gen {
T ob;
Gen( ) {
ob = new T( ); // error
}
}
Es incorrecto intentar crear una instancia de T. La razón debería ser fácil de entender:
porque T no existe en tiempo de ejecución, ¿cómo sabría el compilador qué tipo de objeto
crear? Recuerde que la cancelación elimina todos parámetros de tipo durante el proceso de
compilación.
Restricciones en miembros estáticos
Los miembros static de una clase no pueden utilizar a los parámetros de tipo de la clase. Por
ejemplo, todos los miembros estáticos de esta clase son incorrectos:
class Wrong{
//Error, no puede haber variables estáticas de tipo T.
static T ob;
//Error, no puede haber métodos estáticos de tipo T.
static T getob( ) {
www.detodoprogramacion.com
Capítulo 14:
Tipos parametrizados
355
return ob;
}
}
Aunque no se pueden declarar miembros estáticos que utilicen parámetros de tipo
declarados por la clase, se pueden declarar métodos estáticos de tipos parametrizados,
que definan sus propios tipos de parámetros, tal como se hizo anteriormente en este capítulo.
Restricciones en arreglos con tipos parametrizados
Existen dos restricciones importantes de los tipos parametrizados que aplican a los arreglos.
En primer lugar, no se puede instanciar un arreglo cuyo tipo base es un parámetro de tipo.
En segundo lugar, no se puede crear un arreglo como una referencia a un tipo parametrizado
específico. El siguiente programa muestra ambas situaciones:
// Tipos parametrizados y arreglos
class Gen {
T ob;
T vals[]; // correcto
Gen(T o, T[] nums) {
ob = o;
// Esta sentencia es incorrecta
// vals = new T[10]; //no se puede crear un arreglo de T
// Pero, esta sentencia es correcta.
vals = nums; // es correcto asignar una referencia a un arreglo existente
}
}
class GenArrays {
public static void main(String args[]) {
Integer n[] = { 1, 2, 3, 4, 5 };
Gen iOb = new Gen (50, n);
// No se puede crear un arreglo con una referencia a un tipo
parametrizado específico
// Gen gens[] = new Gen [10]; // error
// Esto es correcto
Gen> gens[] = new Gen> [10]; // correcto
}
}
Como lo muestra el programa, es válido declarar una referencia a un arreglo de tipo T, como lo
hace la siguiente línea:
T vals[]; // correcto
www.detodoprogramacion.com
PARTE I
// Error, no puede haber métodos estáticos que accedan a objetos
// de tipo T.
static void showob( ) {
System.out.println(ob) ;
}
356
Parte I:
El lenguaje Java
Pero no se puede hacer instancia un arreglo de tipo T, como lo muestra la siguiente línea
comentada.
// vals = new T[10]; //no se puede crear un arreglo de tipo T
La razón por la cual no se puede crear un arreglo de tipo T es porque T no existe en tiempo de
ejecución, entonces, no existe una forma para el compilador de saber qué tipo de arreglo tiene
que crear.
Sin embargo, se puede pasar una referencia a un arreglo de tipo compatible a Gen( ) cuando
un objeto es creado y asigna esa referencia a vals, como el programa lo hace en esta línea:
vals = nums; // es correcto asignar una referencia a un arreglo existente.
Esto funciona porque el arreglo que se pasa a Gen tiene un tipo conocido, el cual será el mismo
tipo que T en el momento en que el objeto sea creado.
Dentro del método main( ), note que no se puede declarar un arreglo de referencias a un
tipo parametrizado específico. Esto se muestra a continuación:
// Gen gens[] = new Gen[10]; // error
No compilará. Los arreglos de tipos parametrizados simplemente no están permitidos, debido a
que podrían causar la pérdida de seguridad en el manejo de tipos.
Se puede crear un arreglo como referencia a un tipo parametrizados si se utilizan comodines,
como se muestra a continuación:
Gen> gens[] = new Gen>[10];
// correcto
Esta estrategia es mejor que utilizar arreglos de tipos en bruto, porque al menos algunas
validaciones de tipos serán realizadas.
Restricciones en excepciones con tipos parametrizados
Una clase con tipos parametrizados no puede extender de Throwable. Esto significa que no se
pueden crear clases para excepciones genéricas.
Comentarios adicionales sobre tipos parametrizados
Los tipos parametrizados son una poderosa extensión de Java debido a que modernizan la
creación de tipos seguros y código reutilizable. Aunque la sintaxis de los tipos parametrizados
parece ser abrumadora al inicio, se vuelve natural después de utilizarla por un tiempo. El código
con tipos parametrizados será parte del futuro para todos los programadores en Java.
www.detodoprogramacion.com
II
PARTE
La biblioteca de Java
CAPÍTULO 15
Gestión de cadenas
CAPÍTULO 16
Explorando java.lang
CAPÍTULO 17
java.util parte 1: colecciones
CAPÍTULO 18
java.util parte 2: más clases de
utilería
CAPÍTULO 19
Entrada/salida: explorando
java.io
CAPÍTULO 20
Trabajo en red
CAPÍTULO 21
La clase applet
CAPÍTULO 22
Gestión de eventos
CAPÍTULO 23
AWT: trabajando con ventanas,
gráficos y texto
CAPÍTULO 24
AWT: controles, gestores de
organización y menús
CAPÍTULO 25
Imágenes
CAPÍTULO 26
Utilerías para concurrencia
CAPÍTULO 27
NES, expresiones regulares y
otros paquetes
www.detodoprogramacion.com
www.detodoprogramacion.com
15
CAPÍTULO
Gestión de cadenas
E
n el Capítulo 7 se realizó una breve introducción a la gestión de cadenas en Java. En este
capítulo trataremos este tema con mayor detalle. Como ocurre en la mayoría de los lenguajes
de programación, en Java una cadena es una secuencia de caracteres. Pero, al contrario
que muchos otros lenguajes que implementan las cadenas como arreglos de caracteres, Java las
implementa como objetos del tipo String.
La incorporación de las cadenas como objetos en Java permite proporcionar un conjunto completo
de características que facilitan su manipulación. Por ejemplo, Java tiene métodos para comparar dos
cadenas, buscar subcadenas, concatenar cadenas o intercambiar mayúsculas y minúsculas dentro de
una cadena. Además, los objetos de tipo String se pueden construir de diferentes maneras, facilitando
la obtención de una cadena cuando se necesita.
Sin embargo, ocurre algo hasta cierto punto inesperado: cuando se crea un objeto de tipo
String, se está creando una cadena que no se puede modificar; es decir, una vez creado un objeto
String, no se pueden cambiar los caracteres que lo conforman. A primera vista, esta puede parecer
una restricción muy seria. Sin embargo, no es este el caso. Aún se pueden llevar a cabo todo tipo de
operaciones con cadenas. La diferencia es que cada vez que se necesite una versión alterada de una
cadena existente, se debe crear un nuevo objeto String que contenga las modificaciones. La cadena
original se queda como estaba. Esto se hace así porque las cadenas fijas e inmutables se pueden
implementar mucho más eficientemente que las que cambian. Para los casos en que se desee una
cadena modificable, Java proporciona dos opciones: StringBuffer y StringBuilder. Los objetos de
estas dos clases contienen cadenas que se pueden modificar aún después de ser creadas.
Las clases String, StringBuffer y StringBuilder están definidas en el paquete java.lang, por lo
que se encuentran disponibles para todos los programas automáticamente. Todas están declaradas
como final, lo que significa que no se pueden crear subclases a partir de ellas. Esto permite ciertas
optimizaciones que mejoran el rendimiento en las operaciones con cadenas más comunes. Las tres
clases implementan la interfaz CharSequence.
Una cosa más: las cadenas que son objetos de tipo String son inmodificables, lo que significa
que el contenido de la instancia String no se puede cambiar después de crearse. Sin embargo, una
variable declarada como referencia String se puede cambiar en cualquier momento para que apunte
a otro objeto String.
359
www.detodoprogramacion.com
360
Parte II:
La biblioteca de Java
Los constructores String
La clase String soporta varios constructores. Para crear una cadena vacía se puede utilizar el
constructor por omisión. Por ejemplo,
String s = new String();
creará una instancia de String sin ningún carácter en ella.
A menudo se desea crear cadenas con valores iniciales. La clase String proporciona
diferentes constructores para ello. Para crear un objeto String inicializado con un arreglo de
caracteres, se puede usar el siguiente constructor:
String( char chars[ ])
Por ejemplo:
char chars[] = {'a', 'b', 'c'};
String s = new String(chars);
Este constructor inicializa s con la cadena “abc”.
Se puede especificar un subrango de un arreglo de caracteres como inicializador utilizando
el siguiente constructor:
String (char chars[ ], int indiceInicio, int numeroCaracteres)
Aquí, indiceInicio especifica el índice en que comienza el subrango, y numeroCaracteres es el
número de caracteres a emplear. Por ejemplo:
char chars[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
String s = new String(chars, 2, 3);
Esto inicializa s con los caracteres cde.
Se puede construir un objeto String que contenga la misma secuencia de caracteres que otro
utilizando este constructor:
String (String objetoString)
Aquí, objetoString es un objeto de tipo String. Por ejemplo:
// Construir un String a partir de otro.
class MakeString {
public static void main(String args[] ) {
char c[] = {'J', 'a', 'v', 'a'};
String s1 = new String(c);
String s2 = new String(s1);
System.out.println(sl);
System.out.println(s2);
}
}
La salida de este programa es como sigue:
Java
Java
Como se puede observar, s1 y s2 contienen la misma cadena.
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
361
Aunque el tipo char de Java usa 16 bits para representar el conjunto de caracteres Unicode,
el formato típico para las cadenas en Internet usa arreglos de bytes (8 bits) con el conjunto de
caracteres ASCII.
Dado que las cadenas ASCII de 8 bits son comunes, la clase String proporciona
constructores que inicializan una cadena a partir de un arreglo de bytes. Sus formas se muestran
a continuación:
String(byte caracteresAscii[ ])
String(byte caracteresAscii[ ], int indiceInicial, int numeroCaracteres)
// Construcción de una cadena a partir de un arreglo de caracteres.
class SubStringCons {
public static void main(String args[]) {
byte ascii[] = {65, 66, 67, 68, 69, 70 };
String s1 = new String(ascii);
System.out.println(s1);
String s2 = new String(ascii, 2, 3);
System.out.println(s2);
}
}
La salida de este programa es:
ABCDEF
CDE
También se definen versiones extendidas de los constructores byte-a-cadena, en los que se
puede especificar la codificación de caracteres que determina cómo se convierten los bytes en
caracteres. Sin embargo, la mayoría de las veces se utiliza la codificación proporcionada por la
plataforma.
NOTA
Los contenidos del arreglo se copian cada vez que se crea un objeto String a partir de un
arreglo. Si se modifican los contenidos del arreglo después de creada la cadena, el objeto String no
se modifica.
Es posible construir un objeto de tipo String a partir de un objeto StringBuffer utilizando
el constructor:
String (StringBuffer strBufObj)
También es posible construir un objeto String a partir de un objeto StringBuilder con el
constructor:
String (StringBuilder strBuildObj)
El siguiente constructor permite utilizar el conjunto de caracteres extendido Unicode:
www.detodoprogramacion.com
PARTE II
Aquí, caracteresAscii especifica el arreglo de bytes. La segunda forma permite especificar un
subrango. En cada uno de estos constructores, la conversión de byte a carácter se realiza usando
la codificación de caracteres por omisión de la plataforma. El siguiente programa ilustra estos
constructores:
362
Parte II:
La biblioteca de Java
String (int codigos[ ], int indiceInicial, int numeroCaracteres)
Aquí codigos es un arreglo que contiene los códigos Unicode. La cadena resultante es construida
dentro del rango que comienza en indiceInicial y hasta numeroCaracteres.
Java SE 6 añade además la posibilidad de construir una cadena a partir de un objeto del tipo
Charset.
NOTA En el Capítulo 16 se presenta con mayor detalle Unicode y cómo es gestionado por Java.
Longitud de una cadena
La longitud de una cadena es el número de caracteres que contiene. Para obtener este valor se
usa el método length( ), mostrado a continuación:
int length( )
El siguiente fragmento de código imprime “3”, pues la cadena s tiene tres caracteres:
char chars[] = {'a', 'b', 'c'};
String s = new String(chars);
System.out.println(s.length());
Operaciones especiales con cadenas
Dado que las cadenas son una parte común e importante de la programación, Java proporciona
dentro de sus sintaxis un soporte especial para diversas operaciones con cadenas. Estas
operaciones incluyen la creación automática de nuevas instancias String a partir de literales
de cadena, la concatenación de múltiples objetos String mediante el uso del operador +, así
como la conversión de otros tipos de datos en una representación de tipo cadena. Hay métodos
explícitos disponibles para realizar todas estas funciones, pero Java lo hace automáticamente
para facilitar el trabajo del programador, y también para añadir claridad.
Literales de cadena
Los ejemplos anteriores muestran cómo crear explícitamente una instancia String a partir
de un arreglo de caracteres mediante el operador new. Sin embargo, hay un modo más fácil de
hacer esto usando un literal de cadena. Por cada literal de cadena que haya en un programa,
Java automáticamente construye un objeto String. Por ello, se puede usar un literal de cadena
para inicializar un objeto String. Por ejemplo, el siguiente fragmento de código crea dos cadenas
equivalentes:
char chars[] = {'a', 'b','c'};
String s1 = new String(chars);
String s2 = "abc"; // utiliza un literal de cadena
Puesto que se crea un objeto String para cada literal de cadena, se puede utilizar una literal
de cadena en cualquier sitio en el que se pueda usar un objeto String. Por ejemplo, se puede
llamar directamente a los métodos con una cadena entre comillas como si fuera una referencia
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
363
a un objeto, tal como muestra la siguiente sentencia, que llama al método length( ) sobre la
cadena “abc”. Como es de esperar, imprime “3”:
System.out.println("abc".length());
Concatenación de cadenas
String edad = "9";
String s = "Ella tiene" + edad + " años.";
System.out.println(s);
Esto muestra la cadena “Ella tiene 9 años.”
Un uso práctico de la concatenación de cadenas se da cuando se crean cadenas muy largas.
En lugar de permitir que cadenas muy largas embarullen el código fuente, se pueden romper en
trozos más pequeños y usar el operador + para concatenarlas. Por ejemplo:
// Uso de la concatenación para evitar líneas largas.
class ConCat {
public static void main(String args[]) {
String longStr = "Ésta podría haber sido " +
"una línea muy larga que habría saltado " +
"a las siguientes líneas. Pero la " +
"concatenación de cadenas lo evita.";
System.out.println(longStr);
}
}
Concatenación de cadenas con otros tipos de datos
Se puede concatenar cadenas con otros tipos de datos. Por ejemplo, considere esta versión
ligeramente distinta del ejemplo anterior:
int edad = 9;
String s = "Ella tiene" + edad + " años.";
System.out.println(s);
En este caso, edad es de tipo int en lugar de otro objeto String, pero la salida del código
es la misma que antes. Esto debido a que el valor int de edad se convierte automáticamente
a su representación de cadena dentro de un objeto String; entonces esta cadena se concatena
como antes. El compilador convertirá un operando en su cadena equivalente siempre que el otro
operando del operador + sea una instancia de String.
Sin embargo, hay que tener cuidado al mezclar otros tipos de operaciones con expresiones
de concatenación de cadenas, ya que se puede obtener resultados sorprendentes. Consideremos
el siguiente fragmento de código:
String s = "cuatro: " + 2 + 2;
System.out.println(s);
www.detodoprogramacion.com
PARTE II
En general, Java no permite aplicar operadores a los objetos String. La única excepción a
esta regla es el operador +, que concatena dos cadenas produciendo un objeto String como
resultado. Esto permite yuxtaponer una serie de operaciones +. Por ejemplo, el siguiente
fragmento concatena tres cadenas:
364
Parte II:
La biblioteca de Java
La salida es:
cuatro: 22
en vez de:
cuatro: 4
que probablemente era el resultado esperado. La precedencia de operadores hace que en primer
lugar se concatene la cadena “cuatro:” con la cadena equivalente del primer número 2. Después,
se concatena este resultado con la cadena equivalente del segundo número 2. Para realizar
primero la suma de enteros hay que utilizar paréntesis:
String s = "cuatro: " + (2 + 2);
Ahora s contiene la cadena "cuatro: 4".
Conversión de cadenas y toString( )
Cuando Java convierte datos en su representación de cadena durante la concatenación, lo
hace llamando a una versión sobrecargada del método de conversión de cadenas valueOf( )
definido por String. El método valueOf( ) está sobrecargado para todos los tipos simples y
para el tipo Object. Para los tipos primitivos, valueOf( ) devuelve una cadena que contiene el
texto legible equivalente del valor con que se le llama. Para objetos, valueOf( ) llama al método
toString( ) sobre ese objeto. Analizaremos valueOf( ) con más detalle más adelante en este
capítulo. Aquí vamos a examinar el método toString( ), porque es la manera de obtener la
representación en cadena de objetos de clases creadas por el programador.
Todas las clases implementan el método toString( ) porque este método está definido en la
clase Object. Sin embargo, la implementación por omisión de toString( ) raramente es suficiente.
Para la mayoría de las clases importantes creadas por el programador, será deseable sobrescribir
el método toString( ) y proporcionar nuestras propias representaciones en forma de cadena.
Afortunadamente, esto es fácil de hacer. El método toString( ) tiene esta forma general:
String toString( )
Para implementar toString( ), basta simplemente con devolver un objeto String que contenga la
cadena legible que describa apropiadamente al objeto de la clase.
Al sobrescribir toString( ) en las clases creadas por el programador, se permite a las cadenas
resultantes integrarse totalmente en el entorno de programación de Java. Por ejemplo, se pueden
usar en las sentencias print( ) y println( ), así como en expresiones de concatenación. El siguiente
programa muestra esto sobrescribiendo toString( ) para la clase Box:
// Sobrescribir toString() para la claseBox.
class Box {
double anchura;
double altura;
double profundidad;
Box(double w, double h, double d) {
anchura = w;
altura = h;
profundidad =d;
}
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
365
public String toString() {
return "Las dimensiones son " + anchura + " por " +
profundidad + " por " + altura + ".";
}
}
La salida de este programa es:
Las dimensiones son 10.0 por 14.0 por 12.0
Box b: Las dimensiones son 10.0 por 14.0 por 12.0
Como se ve, el método toString( ) de la clase Box es llamado automáticamente cuando se
usa un objeto Box en una expresión de concatenación o en una llamada a println( ).
Extracción de caracteres
La clase String proporciona diferentes modos de extraer caracteres de un objeto String. A
continuación examinaremos cada uno de ellos. Aunque los caracteres que componen una cadena
dentro de un objeto String no se pueden indexar como si fueran un arreglo de caracteres,
muchos de los métodos de String emplean un índice (o desplazamiento) dentro de la cadena
para su funcionamiento. Al igual que los arreglos, los índices de cadenas comienzan en cero.
charAt( )
Para extraer un único carácter de un objeto String, se puede hacer referencia directamente a un
carácter individual mediante el método charAt( ). Tiene la siguiente forma general:
char charAt(int donde)
Aquí, donde es el índice del carácter que se quiere obtener. El valor de donde debe ser no negativo
y especificar una posición dentro de la cadena. charAt( ) devuelve el carácter en la posición
especificada. Por ejemplo,
char ch;
ch = "abc".charAt(l);
asigna el valor “b” a ch.
getChars( )
Si se necesita extraer más de un carácter a la vez, se puede usar el método getChars( ), el cual
tiene la forma general:
void getChars(int posInicial, int posFinal, char destino[ ], int posDestino)
www.detodoprogramacion.com
PARTE II
class toStringDemo {
public static void main(String args[]) {
Box b = new Box(10, 12, 14);
String s = "Box b: " + b; // concatena al objeto Box
System.out.println(b); // convierte Box a cadena
System.out.println(s);
}
}
366
Parte II:
La biblioteca de Java
Donde posInicial especifica el índice donde comienza la subcadena y posFinal la posición
siguiente a aquella en que se desea termine la subcadena. Así, la subcadena contiene
los caracteres desde posInicial hasta posFinal-l. El arreglo que reciben los caracteres es el
especificado por destino, y el índice dentro de destino a partir del cual se copia la subcadena es
indicado con posDestino. Hay que tener cuidado de que el arreglo de destino sea lo
suficientemente grande como para contener todos los caracteres de la subcadena especificada.
El siguiente programa muestra el uso de getChars( ):
c1ass getCharsDemo {
pub1ic static void main(String args[]) {
String s = "Esta es una demo del método getChars.";
int start = 12;
int end = 16;
char buf[] = new char[end - start];
s.getChars(start, end, buf, 0);
System.out.print1n(buf);
}
}
He aquí la salida de este programa:
demo
getBytes( )
Existe una alternativa a getChars( ) que almacena los caracteres en un arreglo de bytes. Este
método se llama getBytes( ), y utiliza las conversiones carácter a byte proporcionadas por
omisión por la plataforma. Su forma más simple es:
byte[ ] getBytes( )
También están disponibles otras formas de getBytes( ). La mayor utilidad de getBytes( )
se da cuando al exportar un valor String a un entorno que no soporta los caracteres Unicode
de 16 bits. Por ejemplo, la mayoría de los protocolos de Internet y formatos de archivos de texto
utilizan el código ASCII de 8 bits.
toCharArray( )
Si se desea convertir todos los caracteres de un objeto String a un arreglo de caracteres, el
modo más fácil de hacerlo es llamando al método toCharArray( ). Este método devuelve un
arreglo de caracteres con la cadena completa. Su forma general es:
char[ ] toCharArray( )
Esta función se proporciona para facilitar la tarea del programador, pues siempre es posible
conseguir el mismo resultado utilizando getChars( ).
Comparación de cadenas
La clase String incluye diferentes métodos para comparar cadenas o subcadenas dentro de
cadenas. A continuación examinaremos cada una de ellas.
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
367
equals( ) y equalsIgnoreCase( )
Para comparar la igualdad de dos cadenas se utiliza el método equals( ), el cual tiene la siguiente
forma general:
boolean equals(Object str)
boolean equalsIgnoreCase(String str)
Aquí, str es el objeto String que se compara con el objeto String que llama al método. Devuelve
true si las cadenas contienen los mismos caracteres en el mismo orden, y false si no.
He aquí un ejemplo que muestra el uso de equals( ) y equalsIgnoreCase( ):
// Ejemplo con equals() y equalsIgnoreCase().
class equalsDemo {
public static void main(String args[]) {
String sl = "Hola";
String s2 = "Hola";
String s3 = "Adiós";
String s4 = "HOLA";
System.out.println (sl + " equals " + s2 + " ->
sl.equals(s2));
System.out.println (sl + " equals " + s3 + " ->
sl.equals(s3));
System.out.println (sl + " equals " + s4 + " ->
sl.equals(s4));
System.out.println (sl + " equalsIgnoreCase " +
sl.equalsIgnoreCase(s4));
}
}
" +
" +
" +
s4 + " -> " +
La salida del programa se muestra a continuación:
Hola
Hola
Hola
Hola
equals Hola -> true
equals Adiós -> false
equals HOLA -> false
equalsIgnoreCase HOLA -> true
regionMatches( )
El método regionMatches( ) compara una región específica dentro de una cadena con otra
región específica dentro de otra cadena. Hay una forma sobrecargada del método que permite
ignorar la diferencia entre mayúsculas y minúsculas en tales comparaciones. Las formas
generales de estos dos métodos son:
boolean regionMatches(int posInicial, String str2,
int posInicialStr2, int numCaracts)
www.detodoprogramacion.com
PARTE II
Aquí, str es el objeto String que se compara con el objeto String que llama al método. Devuelve
true si las cadenas contienen los mismos caracteres en el mismo orden, y false en caso contrario.
La comparación distingue mayúsculas de minúsculas.
Para hacer una comparación que ignore las diferencias entre mayúsculas y minúsculas,
podemos utilizar equalsIgnoreCase( ), el cual, al comparar dos cadenas, considera a los
caracteres de A-Z iguales a los caracteres a-z. El método tiene la forma general:
368
Parte II:
La biblioteca de Java
boolean regionMatches(boolean ignorarCaso,
int posInicial, String str2,
int posInicialStr2, int numCaracts)
En ambas versiones, posInicial especifica el índice en que comienza la región dentro del
objeto String que llama al método. El objeto String comparado se especifica en str2. El índice
en que comienza la comparación dentro de str2 se especifica en posInicialStr2. La longitud de la
subcadena comparada se pasa en numCaracts. En la segunda versión, si ignorarCaso es true, se
ignora la diferencia entre mayúsculas y minúsculas en los caracteres.
startsWith( ) y endsWith( )
La clase String define dos rutinas que son formas más o menos especializadas de
regionMatches( ). El método startsWith( ) determina si un objeto String dado comienza con
una cadena especificada. Análogamente, endsWith( ) determina si el String en cuestión termina
con una cadena especificada. Esos métodos tienen las siguientes formas generales:
boolean startsWith(String str)
boolean endsWith(String str)
Aquí, str es la cadena que se busca. Si la cadena coincide, se devuelve true; de lo contrario, se
devuelve false. Por ejemplo,
"Klostix".endsWith("tix")
y
"Oscludo".startswith ("Os")
devuelven en ambos casos true.
Una segunda forma de startsWith( ), mostrada a continuación, permite especificar un punto
de inicio:
boolean startsWith(String str, int posInicio)
Aquí, posInicial especifica el índice dentro de la cadena que llama al método en el que comenzará
la búsqueda. Por ejemplo.
"Klostix".startsWith("tix", 4)
devuelve true.
Comparando equals( ) con el Operador = =
Es importante entender que el método equals( ) y el operador == realizan dos funciones
diferentes. Como se acaba de explicar, el método equals( ) compara los caracteres dentro de
un objeto String. El operador == compara dos referencias de objeto para ver si se refieren a la
misma instancia. El siguiente programa muestra cómo dos objetos String diferentes pueden
contener los mismos caracteres, pero las referencias a estos objetos son distintas.
// comparando equals() con el operador ==
class EqualsNotEqualTo {
public static void main(String args[]) {
String s1 = "Hola";
String s2 = new String (s1);
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
369
System.out.println (s1 + " equals " + s2 + "->" +
s1.equals(s2));
System.out.println(s1 + "==" + s2 + "->" + (s1 == s2));
}
}
Hola equa1s Hola -> true
Hola == Hola -> false
compareTo( )
A menudo no basta simplemente con saber si una cadena es idéntica a otra. En las aplicaciones
que requieren ordenar datos se necesita saber si una cadena es menor, igual o mayor que la otra.
Una cadena es menor que otra si está delante de ella en orden alfabético. Una cadena es mayor
que otra si está después de ella en orden alfabético. El método compareTo( ) de la clase String
sirve para esto. Tiene la forma general:
int compareTo(String str)
Aquí, str es la cadena que se compara con el objeto String que llama al método. El resultado
devuelto por la comparación se interpreta como sigue:
Valor
Significado
Menor que cero
La cadena que llama al método es menor que str.
Mayor que cero
La cadena que llama al método es mayor que str.
Cero
Ambas cadenas son iguales.
El siguiente programa de ejemplo ordena un arreglo de cadenas, utilizando el método
compareTo( ) para determinar la posición de cada cadena:
// Ordenación de cadenas por el método burbuja.
class SortString {
static String arr[] = {
"Ahora", "es", "el", "momento", "de", "que", "todos", "los",
"hombres", "buenos", "vengan", "a", "ayudar", "a", "su", "país"
};
public static void main(String args[]) {
for(int j = 0; j < arr.length; j++) {
for(int i = j + 1; i < arr.length; i++) {
if(arr[i].compareTo(arr[j]) < 0) {
String t = arr[j];
arr[j] = arr[i] ;
arr[i] = t;
}
}
www.detodoprogramacion.com
PARTE II
La variable s1 se refiere a la instancia String creada por “Hola”. El objeto al que se refiere
s2 se crea con s1 como inicializador. Por tanto, los contenidos de ambos objetos String son
idénticos, pero son objetos distintos. Esto significa que s1 y s2 no se refieren a los mismos
objetos y por tanto, no son ==, como se muestra a continuación con la salida del ejemplo
anterior:
370
Parte II:
La biblioteca de Java
System.out.println(arr[j]);
}
}
}
La salida de este programa es la siguiente lista de palabras:
Ahora
a
a
ayudar
buenos
de
el
es
hombres
los
momento
país
que
su
todos
vengan
Como se ve por la salida de este ejemplo, compareTo( ) toma en cuenta las mayúsculas y las
minúsculas. La palabra “Ahora” ha sido listada en primer lugar porque comienza con mayúscula,
lo que significa que tiene un valor más bajo en el conjunto de caracteres ASCII.
Para ignorar las diferencias entre mayúsculas y minúsculas al comparar dos cadenas, debemos
utilizar compareToIgnoreCase( ), cuya forma es:
int compareTolgnoreCase(String str)
Este método devuelve los mismos resultados que compareTo( ), salvo que las diferencias entre
mayúsculas y minúsculas se ignoran. Si se utiliza este método en el programa anterior, la palabra
“Ahora” ya no saldría como primera de la lista.
Búsqueda en las Cadenas
La clase String proporciona dos métodos que permiten buscar un carácter o una subcadena
dentro de una cadena:
• indexOf( ) Busca la primera aparición de un carácter o subcadena.
• lastIndexOf( ) Busca la última aparición de un carácter o subcadena.
Estos dos métodos están sobrecargados de distintas formas. En todos los casos, los métodos
devuelven el índice en que se encontró el carácter o subcadena, o –1 si no se encontró.
Para buscar la primera aparición de un carácter, se utiliza:
int indexOf(int ch)
Para buscar la última aparición de un carácter, se utiliza:
int lastIndexOf(int ch)
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
371
donde ch es el carácter buscado.
Para buscar la primera o última aparición de una subcadena, se utiliza:
int indexOf(String str)
int lastIndexOf(String str)
donde str especifica la subcadena.
Se puede especificar una posición de inicio para la búsqueda utilizando las siguientes
formas:
int indexOf(String str, int posInicial)
int lastlndexOf(String str, int posInicial)
Donde posInicial especifica el índice de la posición donde comienza la búsqueda. Para indexOf( ),
la búsqueda se realiza desde posInicial hasta el final de la cadena. Para lastIndexOf( ), la
búsqueda se realiza desde posInicial hasta cero.
El siguiente ejemplo muestra el uso de varios métodos para buscar dentro de cadenas:
// Ejemplo del uso de indexOf() y lastIndexOf().
class indexOfDemo {
public static void main(String args[]) {
String s = "Ahora es el momento de que todos los " +
"hombres buenos vengan a ayudar a su país.";
System.out.println(s);
System.out.println("indexOf(e) = " +
s. indexOf ( 'e' ) ) ;
System.out.println("lastIndexOf(e) = " +
s.lastIndexOf('e'));
System.out.println("indexOf(es) = " +
s.indexOf("es")) ;
System.out.println("lastIndexOf(es) = " +
s.lastIndexOf("es"));
System.out.println("indexOf(e, 10) = " +
s.indexOf('e' , 10));
System.out.println("lastIndexOf(e, 50) = " +
s.lastIndexOf('e', 50));
System.out.println("indexOf(es, 10) = " +
s. indexOf ("es", 10));
System.out.println("lastIndexOf(es, 50) = " +
s.lastIndexOf ("es", 50));
}
}
Ésta es la salida del programa:
Ahora es el momento de que todos los hombres buenos vengan
a ayudar a su país.
indexOf(e) = 6
lastlndexOf(e) = 53
indexOf(es) = 6
lastlndexOf(es) = 42
www.detodoprogramacion.com
PARTE II
int indexOf(int ch, int posInicial)
int lastIndexOf(int ch, int posInicial)
372
Parte II:
La biblioteca de Java
indexOf(e, 10) = 15
lastlndexOf(e, 50) = 47
indexOf(es, l0) = 42
lastIndexOf(es, 50) = 42
Modificación de una cadena
Dado que los objetos String son inmutables, cada vez que se quiera modificar un objeto String
se debe o bien copiarlo en un objeto del tipo StringBuffer o StringBuilder, o bien utilizar uno
de los siguientes métodos de la clase String los cuales construyen una nueva copia de la cadena
con las modificaciones respectivas.
substring( )
Se puede extraer una subcadena utilizando el método substring( ). Este método tiene dos
formas. La primera es:
String substring (int posInicial)
Donde posInicial especifica el índice donde comienza la subcadena. Esta forma devuelve una
copia de la subcadena que comienza en posInicial y sigue hasta el final de la cadena que llama al
método.
La segunda forma del método substring( ) permite especificar tanto el índice de inicio como
el índice final de la subcadena:
String substring (int posInicial, int posFinal)
Aquí, posInicial especifica el índice de inicio, y posFinal el punto de parada. La cadena devuelta
contiene todos los caracteres desde el índice inicial hasta el índice final, pero sin incluirlo.
El siguiente programa utiliza substring( ) para reemplazar todas las apariciones de una
subcadena dentro de una cadena por otra:
// Reemplazo de subcadenas.
class StringReplace {
public static void main(String args[]) {
String org = "This is a test. This is, too.";
String search = "is";
String sub = "was";
String result = "";
int i;
do { // reemplazar subcadenas
System.out.println(org);
i = org.indexOf(search);
if(i != -1) {
result = org.substring(0, i);
result = result + sub;
result = result + org.substring(i + search.length( ));
org = result;
}
while(i != -1);
}
}
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
373
La salida del programa se muestra a continuación:
This is a test. This is, too.
Thwas is a test. This is, too.
Thwas was a test. This is, too.
Thwas was a test. Thwas is, too.
Thwas was a test. Thwas was, too.
concat( )
String concat(String str)
Este método crea un nuevo objeto que contiene la cadena invocante con los
contenidos de str añadidos al final. concat( ) hace la misma función que el operador +.
Por ejemplo:
String s1 = "uno";
String s2 = s1.concat("dos");
pone la cadena “unodos” en s2. Esto genera el mismo resultado que la siguiente secuencia:
String s1 = "uno";
String s2 = s1 + "dos";
replace( )
El método replace( ) tiene dos formas. La primera reemplaza todas las apariciones de un
carácter en la cadena que invoca por otro carácter. Tiene la siguiente forma general:
String replace(char original, char reemplazo)
Donde original especifica el carácter a ser reemplazado por el carácter especificado. El método
devuelve la cadena resultante. Por ejemplo.
String s = "Hola".replace('l', 'w');
pone la cadena “Howa” en s.
La segunda forma del método replace( ) reemplaza una secuencia de caracteres por otra. El
método está definido como:
String replace(CharSequence original, CharSequence reemplazo)
trim( )
El método trim( ) devuelve una copia de la cadena invocante de la que se han quitado todos los
espacios en blanco que pudiera tener al principio y al final. Tiene esta forma general:
String trim( )
He aquí un ejemplo:
String s = " Hola Mundo
".trim();
pone la cadena “Hola Mundo” en s.
www.detodoprogramacion.com
PARTE II
Se pueden concatenar dos cadenas utilizando el método concat( ), como se muestra a
continuación:
374
Parte II:
La biblioteca de Java
El método trim( ) es bastante útil para procesar comandos de usuario. Por ejemplo, el
siguiente programa pide al usuario su país y luego muestra la capital de ese país. El ejemplo
utiliza trim( ) para quitar los espacios en blanco iniciales o finales que el usuario haya
introducido sin darse cuenta.
// Ejemplo del método de trim( ).
import java.io.*;
c1ass UseTrim {
public static void main(String args[])
throws IOException
{
// crear un objeto BufferedReader con System.in
BufferedReader br = new
BufferedReader(new InputStreamReader(System.in));
String str;
System.out.println("Escriba 'fin' para terminar.");
System.out.println("Escriba País: ");
do {
str = br.readLine( );
str = str.trim( ); // quitar espacios en blanco
if(str.equals("México"))
System.out.println("La capital es
else if (str .equals ("Argentina"))
System.out.println("La capital es
else if(str.equals("España"))
System.out.println("La capital es
else if(str.equals("El Salvador"))
System.out.println("La capital es
// ...
} while(!str.equals("fin"));
Ciudad de México");
Buenos Aires.");
Madrid.");
San Salvador.");
}
}
Conversión de datos mediante valueOf( )
El método valueOf( ) convierte datos desde su formato interno hasta una forma legible por los
humanos. Es un método estático que se sobrecarga dentro de la clase String para todos los tipos
de Java incorporados, de modo que cada tipo se puede convertir adecuadamente en una cadena.
valueOf( ) también está sobrecargado para el tipo Object, por lo que un objeto de cualquier
tipo de clase creado por el programador también se puede usar como argumento. Recuerde que
Object es una superclase para todas las clases. He aquí algunas formas del método:
static String valueOf(double num)
static String valueOf(long num)
static String valueOf(Object ob)
static String valueOf(char chars[ ])
Tal como ya hemos visto, valueOf( ) es llamado cuando se necesita una representación en
forma de cadena de algún otro tipo de datos, por ejemplo en operaciones de concatenación. Se
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
static String valueOf(char chars[ ], int posInicial, int numChars)
Donde chars es el arreglo que contiene los caracteres, posInicial es el índice del arreglo de
caracteres en que comienza la subcadena deseada, y numChars especifica la longitud de la
subcadena.
Cambio entre mayúsculas y minúsculas dentro de una cadena
El método toLowerCase( ) convierte todos los caracteres de una cadena de mayúsculas a
minúsculas. El método toUpperCase( ) convierte todos los caracteres de una cadena de minúsculas
a mayúsculas. Los caracteres no alfabéticos, como los números, no se ven afectados. La forma
general de estos métodos es:
String toLowerCase( )
String toUpperCase( )
Ambos métodos devuelven un objeto String que contiene el equivalente en mayúsculas o
minúsculas de la cadena que invoca.
He aquí un ejemplo que usa toLowerCase( ) y toUpperCase( ):
// Uso de toUpperCase () y toLowerCase () .
class ChangeCase {
public static void main(String args[])
{
String s = "Esto es una prueba.";
System.out.println("Original: " + s);
String mayúsculas = s.toUpperCase();
String minúsculas = s.toLowerCase();
System.out.println("En mayúsculas: " + mayúsculas);
System.out.println("En minúsculas: " + minúsculas);
}
}
La salida producida por el programa es la siguiente:
Original: Esto es una prueba.
En mayúsculas: ESTO ES UNA PRUEBA.
En minúsculas: esto es una prueba.
www.detodoprogramacion.com
PARTE II
puede llamar a este método directamente con cualquier tipo de dato y obtener una representación
razonable en forma de cadena. Todos los tipos simples se convierten a su representación String
común. Cualquier objeto que se le pase a valueOf( ) devolverá el resultado de la llamada al
método toString( ) de dicho objeto. De hecho, se puede simplemente llamar a toString( )
directamente y obtener el mismo resultado.
Para la mayoría de los arreglos, valueOf( ) devuelve una cadena algo críptica, lo que indica
que es un arreglo de algún tipo. Para arreglos de tipo char, sin embargo, se crea un objeto String
que contiene los caracteres del arreglo char. He aquí una versión especial de valueOf( ) que
permite especificar un subconjunto de un arreglo char. Tiene la forma general:
375
376
Parte II:
La biblioteca de Java
Existen versiones sobrecargadas de toLowerCase( ) y toUpperCase( ) que permiten
especificar un objeto de tipo Locale para controlar la conversión.
Otros métodos para trabajar con cadenas
Además de los métodos mencionados anteriormente, la clase String incluye otros tantos
métodos. La siguiente tabla es un resumen de métodos disponibles en la clase String.
Método
Descripción
int codePointAt(int i)
Devuelve el punto de código Unicode en la posición
especificada por i
int codePointBefore(int i)
Devuelve el punto de código Unicode en la posición que
precede a i
int codePointCount(int inicio, int fin)
Devuelve el número de puntos de código Unicode en la
porción de la cadena entre las posiciones inicio y fin–1.
boolean contains(CharSequence str)
Devuelve verdadero si el objeto que invoca contiene
la cadena especificada por str. Devuelve falso en caso
contrario.
boolean contentEquals(CharSequence str) Devuelve verdadero si la cadena que realiza la invocación
contiene el mismo texto que str. En caso contrario
devuelve falso.
boolean contentEquals(StringBuffer str)
Devuelve verdadero si la cadena que realiza la invocación
contiene el mismo texto que str. En caso contrario
devuelve falso.
static String format (String fmtstr,
Object … args)
Devuelve una cadena en el formato especificado por
fmtstr. En el Capítulo 18 se habla a detalle del formato de
cadenas.
static String format(Locale loc,
String fmtstr,
Object … args)
Devuelve una cadena en el formato especificado por
fmtstr. El formato está dirigido por el objeto Locale. En el
Capítulo 18 se habla a detalle del formato de cadenas.
boolean isEmpty( )
Devuelve verdadero si la cadena que realiza la invocación
no contiene caracteres y tiene una longitud de cero. Este
método fue añadido por Java SE 6.
boolean matches (String regExp)
Devuelve verdadero si la cadena que invoca corresponde con
la expresión regular establecida en regExp. En caso contrario
devuelve falso.
int offsetByCodePoints(int start, int num)
Devuelve el índice si la cadena que invoca es num puntos
de código después del incio de índice especificado por
start.
String replaceFirst (String regExp,
String newStr)
Devuelve una cadena en la cual la primera subcadena que
coincide con la expresión regular establecida en regExp
es reemplazada por la cadena newStr.
String replaceAll (String regExp,
String newStr)
Devuelve una cadena en la cual todas las subcadenas
que coinciden con la expresión regular establecida en
regExp son remplazadas por la cadena newStr.
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
Descripción
String[ ] split (String regExp)
Descompone a la cadena que realiza la invocación
en partes y devuelve un arreglo que contiene dicho
resultado. Cada parte es delimitada por la expresión
regular definida por regExp.
String [ ] split (String regExp, int max)
Descompone la cadena que invocó en partes y regresa
un arreglo que contiene dicho resultado. Las partes son
separadas acorde con lo que indique la expresión regular
definida por regExp. El número de partes es especificado
por max. Si max contiene un valor positivo, la expresión
regular se aplica como máximo max–1 veces, y la última
cadena en el arreglo resultante contiene el sobrante de la
cadena que invoca. En caso contrario, si max es un valor
negativo entonces la expresión regular se aplica tantas
veces como sea posible y la cadena es completamente
separada en partes, incluso se conservan las cadenas
vacías que pudieran quedar al final del arreglo resultante.
Si max es cero, entonces la expresión regular se
aplica tantas veces como sea posible, la cadena es
completamente separada en partes y si quedaran
cadenas vacías insertadas al final del arreglo resultante,
éstas se eliminan.
CharSequence
subSequence (int posInicial,
int posFinal)
Devuelve una subcadena tomada de la cadena que realizó
la invocación, comenzando en posInicial y hasta posFinal.
Este método es utilizado por la interfaz CharSequence, la
cual es implementada por la clase String.
Observe que varios de estos métodos utilizan expresiones regulares. Las expresiones regulares
se describen en el Capítulo 27.
StringBuffer
StringBuffer es una clase semejante a String que proporciona buena parte de la funcionalidad
de las cadenas. Como sabemos, String representa secuencias de caracteres de inmutables de
longitud fija. En contraste, StringBuffer representa secuencias de caracteres que pueden crecer
y sobrescribirse. A un objeto StringBuffer se le puede insertar o añadir al final caracteres y
subcadenas. StringBuffer crecerá automáticamente para hacer espacio para estas adiciones
y, a menudo, tiene más caracteres asignados en memoria que los que realmente necesita, para
dejar espacio para crecer en tamaño. Java utiliza ambas clases intensivamente, pero muchos
programadores sólo manejan String y dejan a Java manipular StringBuffer de manera
automática utilizando el operador sobrecargado +.
Constructores StringBuffer
StringBuffer define los siguientes cuatro constructores:
StringBuffer( )
StringBuffer(int tamaño)
www.detodoprogramacion.com
PARTE II
Método
377
378
Parte II:
La biblioteca de Java
StringBuffer(String str)
StringBuffer(CharSequence chars)
El constructor por omisión (el que no lleva parámetros) reserva espacio para 16 caracteres sin
reasignación de memoria. La segunda versión acepta un argumento entero que explícitamente
fija el tamaño del espacio reservado. La tercera versión acepta como argumento un objeto String
que fija los contenidos iniciales del objeto StringBuffer y reserva espacio para 16 caracteres
más sin reasignación. StringBuffer asigna espacio para 16 caracteres adicionales cuando no se
solicita una longitud explícita, debido a que la reasignación es un proceso costoso en tiempo.
Además que reasignaciones frecuentes pueden fragmentar la memoria. Asignando espacio para
unos pocos caracteres adicionales, StringBuffer reduce el número de reasignaciones que puedan
surgir. El cuarto constructor crea un objeto que contiene la secuencia de caracteres definida por
chars.
length( ) y capacity( )
La longitud actual de un StringBuffer se puede obtener por medio del método length( ),
mientras que la capacidad total asignada se obtiene con el método capacity( ). Sus formas
generales son:
int length( )
int capacity( )
He aquí un ejemplo:
// Longitud y capacidad de un StringBuffer.
class StringBufferDemo {
public static void main(String args[]) {
StringBuffer sb = new StringBuffer("Hola");
System.out.println("valor = " + sb);
System.out.println("longitud = " + sb.length());
System.out.println("capacidad = " + sb.capacity());
}
}
La salida del programa muestra cómo StringBuffer reserva espacio extra para
manipulaciones adicionales:
valor = Hola
longitud = 4
capacidad = 20
La variable sb se inicializa con la cadena “Hola”, su longitud es 4 y su capacidad es 20 debido a
que se añade automáticamente espacio para otros 16 caracteres.
ensureCapacity( )
Si se desea preasignar espacio para un cierto número de caracteres después de que se ha
construido un StringBuffer, se puede utilizar ensureCapacity( ). Este método es muy
útil cuando se conoce de antemano que se van a añadir muchas cadenas pequeñas a un
StringBuffer. ensureCapacity( ) cuya forma general de es:
void ensureCapacity(int capacidad)
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
379
Donde capacidad especifica el tamaño del espacio en donde se almacenará la información.
setLength( )
Para fijar la longitud del espacio de almacenamiento dentro de un objeto StringBuffer, se utiliza
el método setLength( ). Su forma general es:
void setLength(int len)
charAt( ) y setCharAt( )
El valor de un carácter específico en un StringBuffer se puede obtener por medio del método
charAt( ). También se puede asignar un valor a un carácter dentro del StringBuffer utilizando el
método setCharAt( ). Sus formas generales son:
char charAt(int donde)
void setCharAt(int donde, char ch)
Para charAt( ), donde especifica el índice del carácter que se desea obtener. Para setCharAt( ),
donde especifica el índice del carácter cuyo valor será alterado, y ch es el nuevo valor de dicho
carácter. Para ambos métodos, donde no debe ser negativo y no debe especificar una posición
más allá del tamaño del espacio de almacenamiento.
El siguiente ejemplo muestra el uso de charAt( ) y setCharAt( ):
// Uso de charAt () y setCharAt ().
class setCharAtDemo {
public static void main{String args[]) {
StringBuffer sb = new StringBuffer{"Hola.");
System.out.println{"StringBuffer antes = " + sb);
System.out.println("charAt(l) antes =" + sb.charAt{l));
sb.setCharAt{l, 'i');
sb.setLength(2) ;
System.out.println{"StringBuffer después = " + sb);
System.out.println("charAt(l) después = " + sb.charAt(l));
}
}
La salida generada por este programa es la siguiente:
StringBuffer antes = Hola.
charAt(l) antes = o
StringBuffer después = Hi
charAt(l) después = i
getChars( )
Para copiar una subcadena de un objeto StringBuffer dentro de un arreglo, se utiliza el método
getChars( ). Este método tiene la siguiente forma general:
www.detodoprogramacion.com
PARTE II
Donde len especifica la longitud del búfer. Este valor no debe ser negativo.
Al aumentar el tamaño del espacio de almacenamiento, se rellena la cadena con caracteres
nulos al final de la misma. Si se llama a setLength( ) con un valor menor que el actualmente
devuelto por length( ), entonces los caracteres almacenados más allá de la nueva longitud se
perderán. El programa ejemplo setCharAtDemo en la siguiente sección utiliza setLength( )
para acortar un StringBuffer.
380
Parte II:
La biblioteca de Java
void getChars(int inicioOrigen, int finalOrigen, char destino[ ], int inicioDestino)
Aquí, inicioOrigen es el índice donde comienza la subcadena, y finalOrigen es un índice posterior
en una unidad al del final de la subcadena deseada.
Esto significa que la subcadena contiene los caracteres desde inicioOrigen hasta finalOrigen-l.
El arreglo que recibe los caracteres se especifica en destino. El índice dentro de destino a partir del
cual se copia la subcadena se proporciona en inicioDestino. Hay que tener cuidado en asegurar
que el arreglo destino sea lo suficientemente grande como para albergar todos los caracteres de
la subcadena especificada.
append( )
El método append( ) concatena la representación textual de cualquier tipo de datos al final del
objeto StringBuffer que llama al método. Este método tiene varias versiones sobrecargadas. He
aquí algunas de sus formas:
StringBuffer append(String str)
StringBuffer append(int num)
StringBuffer append(Object obj)
Se llama a String.valueOf( ) por cada parámetro para obtener su representación como
cadena. El resultado se añade al objeto StringBuffer. La cadena resultante es devuelta por
cada versión de append( ). Esto permite encadenar llamadas sucesivas unas tras otras, como se
muestra en el siguiente ejemplo:
// Ejemplo con append().
class appendDemo {
public static void main(String args[]) {
String s;
int a = 42;
StringBuffer sb = new StringBuffer(40);
s = sb.append("a = ") .append(a) .append("!") .toString( );
System.out.println(s);
}
}
La salida de este ejemplo se muestra a continuación:
a = 42!
Cuando más a menudo se llama al método append( ) es al utilizar el operador + sobre
objetos String. Java cambia automáticamente las modificaciones de una instancia String en
operaciones similares sobre una instancia StringBuffer. Así que una concatenación llama a
append( ) sobre un objeto StringBuffer. Después de que la concatenación se ha llevado a cabo,
el compilador introduce una llamada a toString( ) para transformar el StringBuffer modificable
en un String constante. Toda esta complicación puede parecer poco razonable. ¿Por qué no
tener simplemente una clase de cadena y que se comporte más o menos como StringBuffer?
La respuesta está en el rendimiento. Hay muchas optimizaciones que el intérprete de Java
puede hacer si sabe que los objetos String son inmutables. Afortunadamente, Java esconde la
mayor parte de la complejidad de la conversión entre String y StringBuffer. De hecho, muchos
programadores nunca sentirán la necesidad de usar StringBuffer directamente, y serán capaces
de expresar la mayoría de las operaciones en términos del operador + sobre variables String.
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
381
insert( )
El método insert( ) inserta una cadena dentro de otra. Está sobrecargado para aceptar valores de
todos los tipos simples, además de instancias de tipo String y Object. Al igual que append( ),
llama a String.valueOf( ) para obtener la representación como cadena del valor con el que es
llamado. Esta cadena se inserta entonces en el objeto StringBuffer que invoca. Éstas son
algunas de sus formas:
Donde, indice especifica el índice dentro del objeto StringBuffer en cuyo punto se inserta la
cadena.
El siguiente programa ejemplo inserta “trabajo con ” entre “Yo” y “Java”:
// Ejemplo con insert().
class insertDemo {
public static void main(String args[]) {
StringBuffer sb =new StringBuffer(" ¡Yo Java!");
sb.insert(4, "trabajo con ");
System.out.println(sb) ;
}
}
La salida del programa es ésta:
¡Yo trabajo con Java!
reverse( )
Se puede invertir el orden de los caracteres en un objeto StringBuffer utilizando el método
reverse( ), mostrado a continuación:
StringBuffer reverse( )
Este método devuelve el objeto sobre el que fue llamado con el orden invertido. El siguiente
programa muestra el uso de reverse( ):
// Utilizando de reverse( ) para invertir un StringBuffer.
class ReverseDemo {
public static void main(String args[]) {
StringBuffer s = new StringBuffer("abcdef");
System.out.println(s);
s.reverse () ;
System.out.println(s);
}
}
Ésta es la salida producida por el programa:
abcdef
fedcba
www.detodoprogramacion.com
PARTE II
StringBuffer insert(int indice, String str)
StringBuffer insert(int indice, char ch)
StringBuffer insert(int indice, Object obj)
382
Parte II:
La biblioteca de Java
delete( ) y deleteCharAt( )
Se pueden eliminar caracteres de un StringBuffer por medio de los métodos delete( ) y
deleteCharAt( ). Estos métodos se muestran aquí:
StringBuffer delete(int posInicial, int posFinal)
StringBuffer deleteCharAt(int pos)
El método delete( ) borra una sucesión de caracteres del objeto que lo invoca. Aquí,
posInicial especifica el índice del primer carácter que se ha de borrar, y posFinal es superior en
una unidad al del último carácter que se ha de borrar. Es decir, la subcadena borrada va desde
posInicial hasta posFinal-l. El método delete( ) devuelve el objeto StringBuffer resultante.
El método deleteCharAt( ) borra el carácter en la posición especificada por pos, devolviendo
el objeto StringBuffer resultante.
Veamos un programa que ejemplifica el uso de los métodos delete( ) y deleteCharAt( ):
// Ejemplo con delete()y deleteCharAt()
class deleteDemo {
public static void main(String args[]) {
StringBuffer sb = new StringBuffer("Esto es una prueba.");
sb.delete(4, 7);
System.out.println("Después de delete: " + sb);
sb.deleteCharAt(0);
System.out.println("Después de deleteCharAt: " + sb);
}
}
Se produce la siguiente salida:
Después de delete: Esto una prueba.
Después de deleteCharAt: sto una prueba.
replace( )
En un StringBuffer es posible reemplazar un conjunto de caracteres por otro utilizando el
método replace( ). Su firma se muestra a continuación:
StringBuffer replace(int posInicial, int posFinal, String str)
La subcadena que se reemplaza viene especificada por los índices posInicial y posFinal. Así, la
subcadena desde posInicial hasta posFinal-1 es reemplazada. La cadena reemplazante se pasa en
str. El método devuelve el objeto StringBuffer resultante.
El siguiente programa muestra el uso de replace( ):
// Ejemplo con replace()
class replaceDemo {
public static void main(String args()) {
StringBuffer sb = new StringBuffer("Esto es una prueba.");
sb.replace(5, 7, "era");
System.out.println("Después de replace: " + sb);
}
}
www.detodoprogramacion.com
Capítulo 15:
Gestión de cadenas
383
y la salida es:
Después de replace: Esto era una prueba.
substring( )
Es posible obtener una porción de un StringBuffer mediante el método substring( ), el cual
tiene las siguientes dos formas:
La primera forma devuelve la subcadena que empieza en posInicial y sigue hasta el final del
objeto StringBuffer que invoca. La segunda forma devuelve la subcadena que empieza en
posInicial y termina en posFinal-l. Estos métodos funcionan igual que los definidos para String,
ya descritos anteriormente.
Otros métodos para trabajar con StringBuffer
Adicionalmente a los métodos descritos, la clase StringBuffer incluye muchos otros métodos.
La siguiente tabla muestra algunos más.
Método
Descripción
StringBuffer appendCodePoint(int ch) Agrega un punto de código Unicode al final del objeto
invocante. El método devuelve una referencia al objeto.
int codePointAt(int i)
Devuelve el punto de código Unicode en la posición
especificada por i.
int codePointBefore(int i)
Devuelve el punto de código Unicode en la posición anterior
a la especificada por i.
int codePointCount(int inicio, int fin)
Devuelve el número de puntos de código Unicode en la
porción de la cadena que invoca que se encuentran entre
inicio y fin – 1.
int indexOf(String str)
Busca en la cadena que invoca la primera ocurrencia de str.
Devuelve el índice de la coincidencia o –1 si no se encuentra
ninguna coincidencia.
int indexOf(String str, int inicio)
Busca en la cadena que invoca la primera ocurrencia de
str, a partir de la posición inicio. Devuelve el índice de la
coincidencia o –1 si no se encuentra ninguna coincidencia.
int lastIndexOf(String str)
Busca en la cadena que invoca la última ocurrencia de str.
Devuelve el índice de la coincidencia o –1 si no se encuentra
ninguna coincidencia.
int lastIntexOf(String str, int inicio)
Busca en la cadena que invoca la última ocurrencia de
str, a partir de la posición inicio. Devuelve el índice de la
coincidencia o –1 si no se encuentra ninguna coincidencia.
int offsetByCodePoints(int inicio, int
n)
Devuelve el índice en la cadena invocante que esta n puntos
de código Unicode más allá del índice inicial especificado por
inicio.
www.detodoprogramacion.com
PARTE II
String substring(int posInicial)
String substring(int posInicial, int posFinal)
384
Parte II:
La biblioteca de Java
Método
Descripción
CharSequence subsequence (int inicio, int fin)
Devuelve una subcadena de la cadena que invoca,
comenzando en inicio y que concluye en la posición
fin. Este método es utilizado por la interfaz
CharSequence, la cual es implementada en la
clase StringBuffer.
void trimToSize( )
Reduce el tamaño del espacio de almacenamiento
de caracteres del objeto que invoca para ajustarlo
exactamente al contenido actual.
El siguiente programa muestra el uso de los métodos indexOf( ) y lastIndexOf( ):
class IndexOfDemo {
public static void main(String args[]) {
StringBuffer sb = new StringBuffer("uno dos uno");
int i;
i.sb.indexOf("uno");
System.out.println("Primer índice: " + i);
i = sb.lastIndexOf("uno");
System.out.println("Último índice: " + i);
}
}
Ésta es la salida mostrada por el programa:
Primer índice: 0
Último índice: 8
StringBuilder
A partir del JDK 5 se anexa la clase StringBuilder a las capacidades de gestión de cadenas de
Java. La clase StringBuilder es idéntica a la clase StringBuffer excepto por una cosa: no es
una clase sincronizada, lo cual significa que no es seguro utilizarla en programas que trabajan
con múltiples hilos. La ventaja de StringBuffer es el aumento de rendimiento en términos de
velocidad. Sin embargo, en los casos en los que se trabaja con programación multihilo debemos
utilizar StringBuffer en lugar de StringBuilder.
www.detodoprogramacion.com
16
CAPÍTULO
Explorando java.lang
E
ste capítulo trata sobre las clases e interfaces definidas en java.lang. Como sabemos, java.lang
se importa automáticamente en todos los programas. El paquete java.lang contiene las clases
e interfaces fundamentales para prácticamente cualquier programa en Java. Es el paquete de
Java más ampliamente utilizado.
java.lang incluye las siguientes clases:
Boolean
InheritableThreadLocal
Runtime
System
Byte
Integer
RuntimePermission
Thread
Character
Long
SecurityManager
ThreadGroup
Class
Math
Short
ThreadLocal
ClassLoader
Number
StackTraceElement
Throwable
Compiler
Object
StrictMath
Void
Double
Package
String
Enum
Process
StringBuffer
Float
ProcessBuilder
StringBuilder
También hay dos clases definidas por Character: Chacter.Subset y Charter.UnicodeBlock.
java.lang también define las siguientes interfaces:
Appendable
Comparable
CharSequence
Iterable
Cloneable
Readable
Runnable
Muchas de las clases contenidas en java.lang contienen métodos catalogados como en desuso,
la mayoría de los cuales aparecieron en Java 1.0. Estos métodos en desuso son aún provistos por Java
para soportar cualquier legado de código y no se recomiendan para código nuevo. La mayoría de los
desusos tuvieron lugar antes de Java SE 6, y estos métodos en desuso no se discuten aquí.
385
www.detodoprogramacion.com
386
Parte II:
La biblioteca de Java
Envoltura de tipos primitivos
Como se mencionó en la primera parte de este libro, Java utiliza tipos primitivos como int y char,
por razones de rendimiento. Estos tipos de datos no son parte de la jerarquía de objetos. Éstos se
pasan por valor al método y no pueden ser pasados directamente por referencia. Tampoco existe
alguna forma en que dos métodos hagan referencia a la misma instancia de un int. En ocasiones,
se requiere la creación de un objeto para representar uno de estos tipos primitivos. Por ejemplo,
existe una colección de clases que se discutirá en el Capítulo 17 que trabaja sólo con objetos; para
almacenar un tipo primitivo en una de esas clases, se necesita envolver al tipo primitivo en una
clase. Para solucionar esta necesidad Java provee clases que corresponden a cada tipo primitivo.
En esencia, estas clases encapsulan, o envuelven, los tipos primitivos dentro de una clase. De este
modo, estas clases son comúnmente referidas como tipos envueltos. Los tipos envueltos fueron
introducidos en el Capítulo 12. Y se examinan a detalle aquí.
Number
La clase abstracta Number define una superclase que está implementada por las clases que
envuelven los tipos numéricos byte, short, int, long, float, y double. La clase Number tiene
métodos abstractos que devuelven el valor del objeto en cada uno de los diferentes formatos.
Por ejemplo, doubleValue( ) devuelve el valor como double, floatValue( ) devuelve el valor
como float, y así sucesivamente. Estos métodos son los siguientes:
byte byteValue( )
double doubleValue( )
float floatValue( )
int intValue( )
long longValue( )
short shortValue( )
Los valores devueltos por estos métodos pueden estar redondeados.
Number tiene seis subclases concretas que contienen valores explícitos de cada tipo
numérico: Double, Float, Byte, Short, Integer y Long.
Double y Float
Double y Float son envoltorios para valores de punto flotante del tipo double y float
respectivamente. Los constructores para Float son éstos:
Float(double num)
Float(float num)
Float(String str) throws NumberFormatException
Como se ve, los objetos Float se pueden construir con valores de los tipos float o double.
También se pueden construir a partir de la representación como cadena de un número de punto
flotante.
Los constructores de Double son los siguientes:
Double(double num)
Double(String str) throws NumberFormatException
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
Máximo exponente (agregado por Java SE 6)
MAX_VALUE
Máximo valor positivo
MIN_EXPONENT
Mínimo exponente (agregado por Java SE 6)
MIN_NORMAL
Mínimo valor normal positivo (agregado por Java SE 6)
MIN_VALUE
Mínimo valor positivo
NaN
No es un número
POSITIVE_INFINITY
Más infinito
NEGATIVE_INFINITY
Menos infinito
SIZE
El tamaño en bits del valor envuelto
TYPE
El objeto Class para float o double
Método
Descripción
byte byteValue( )
Devuelve el valor del objeto que invoca como un byte.
static int compare(float num1,
float num2)
Compara el valor de num1 y num2. Devuelve 0 si el valor es igual.
Devuelve un valor negativo si num1 es menor que num2. Devuelve
un valor positivo si num1 es mayor que num2.
int compareTo(Float f)
Compara numéricamente el valor del objeto que invoca con el valor
f. Devuelve 0 si los valores son iguales. Devuelve un valor negativo
si el objeto que invoca tiene un valor menor. Devuelve un valor
positivo si el objeto que invoca tiene un valor mayor.
double doubleValue( )
Devuelve el valor del objeto que invoca como double.
boolean equals(Object ObjFloat) Devuelve verdadero si el objeto Float que invoca es equivalente a
ObjFloat. De lo contrario, devuelve falso.
static int floatToIntBits(float
num)
Devuelve el patrón de bits de precisión simple compatible-IEEE
correspondiente a num.
static int floatToRawIntBits(float Devuelve el patrón de bits de precisión simple compatible-IEEE
num)
correspondiente a num. El valor Nan se conserva.
float floatValue( )
Devuelve el valor del objeto que invoca como float.
int hashCode( )
Devuelve el código de dispersión del objeto que invoca.
static float intBitsToFloat(int
num)
Devuelve el equivalente float del patrón de bits de precisión simple
compatible-IEEE especificado por num.
int intValue( )
Devuelve el valor del objeto que invoca como int.
boolean isInfinite( )
Devuelve verdadero si el objeto que invoca contiene un valor
infinito. En caso contrario, devuelve falso.
static boolean isInfinite(float
num)
Devuelve verdadero si num especifica un valor infinito. De lo
contrario devuelve falso.
TABLA 16-1 Los métodos definidos por la clase Float
www.detodoprogramacion.com
PARTE II
MAX_EXPONENT
PARTE II
Los objetos Double se pueden construir con un valor double o una cadena que contenga el
valor de punto flotante.
Los métodos definidos por Float se muestran en la Tabla 16-1. Los métodos definidos
por Double se muestran en la Tabla 16-2. Tanto Float como Double definen las siguientes
constantes:
387
388
Parte II:
La biblioteca de Java
Método
Descripción
boolean isNaN( )
Devuelve verdadero si el objeto que invoca contiene un valor que
no es un número. De lo contrario, devuelve falso.
static boolean isNaN(float num) Devuelve verdadero si num especifica un valor que no es un
número. De lo contrario devuelve falso.
long longValue( )
Devuelve el valor del objeto que invoca como long.
static float
parseFloat(String str) throws
NumberFormatException
Devuelve el equivalente float del número contenido en la cadena
especificada por str utilizando la base 10.
short shortValue( )
Devuelve el valor del objeto que invoca como short.
static String toHexString(float
num)
Devuelve una cadena que contiene el valor de num en formato
hexadecimal
String toString( )
Devuelve la cadena equivalente del objeto que invoca.
static String toString(float num)
Devuelve la cadena equivalente del valor especificado por num.
static Float valueOf(float num)
Devuelve un objeto Float que contiene el valor especificado por
num.
static Float valueOf(String str)
Devuelve el objeto Float que contiene el valor especificado por la
throws NumberFormatException cadena str.
TABLA 16-1 Los métodos definidos por la clase Float (continuación)
Método
Descripción
byte byteValue( )
Devuelve el valor del objeto que invoca como un byte.
static int compare(double num1, double num2) Compara los valores de num1 y num2. Devuelve 0 si
el valor es igual. Devuelve un valor negativo si num2
es menor a num2. Devuelve un valor positivo si num1
es mayor que num2.
int compareTo(Double d)
Compara el valor numérico del objeto que invoca
con el de d. Devuelve 0 si los valores son iguales.
Devuelve un valor negativo si el objeto que invoca es
menor a d. Devuelve un valor positivo si el objeto que
invoca es mayor que d.
static long doubleToLongBits(double num)
Devuelve el patrón de bits de doble precisión
compatible IEEE que corresponde al num.
static long doubleToRawLongBits(double num) Devuelve el patrón de bits de doble precisión
compatible IEEE que corresponde al num. El valor NaN
se conserva.
double doubleValue( )
Devuelve el valor del objeto que invoca como un double.
boolean equals(Object ObjDouble)
Devuelve verdadero si el objeto Double que invoca es
equivalente a ObjDouble. De lo contrario, devuelve falso.
float floatValue( )
Devuelve el valor del objeto que invoca como un float.
int hashcode( )
Devuelve el código de dispersión del objeto que invoca.
int intValue( )
Devuelve el valor del objeto que invoca como un int.
TABLA 16-2 Los métodos definidos por la clase Double
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
Devuelve verdadero si el objeto que invoca contiene
un valor infinito. De lo contrario, devuelve falso.
static boolean isInfinite(double num)
Devuelve verdadero si num especifica un valor
infinito. De lo contrario devuelve falso.
boolean isNaN( )
Devuelve verdadero si el objeto que invoca contiene
un valor que no es un número. De lo contrario,
devuelve falso.
static boolean isNaN(double num)
Devuelve verdadero si num especifica un valor que
no es un número. De lo contrario, devuelve falso.
static double longBitsToDouble(long num)
Devuelve el equivalente double del patrón de bits
de doble precisión IEEE compatible especificado por
num.
long longValue( )
Devuelve el valor del objeto que invoca como un long.
static double parseDouble(String str) throws
NumberFormatException
Devuelve el equivalente double del número contenido
en la cadena especificada por str utilizando base 10.
short shortValue( )
Devuelve el valor del objeto que invoca como un
short.
static String toHexString(double num)
Devuelve una cadena que contiene el valor de num
en formato hexadecimal.
String toString( )
Devuelve la cadena equivalente del objeto que
invoca.
static String toString(double num)
Devuelve la cadena equivalente del valor especificado
por num.
static Double valueOf(double num)
Devuelve un objeto Double que contiene el valor
especificado por num.
static Double valueOf(String str) throws
NumberFormatException
Devuelve un objeto Double que contiene el valor
especificado por la cadena str.
TABLA 16-2 Los métodos definidos por la clase Double (continuación)
El siguiente ejemplo crea dos objetos Double, uno utilizando un valor double y el otro
pasando una cadena que contiene la representación de un double:
class DoubleDemo {
public static void main(String args[]) {
Double d1 = new Double(3.14159);
Double d2 = new Double("314159E-5");
System.out.println(dl + " = " + d2 + " -> " + dl.equals(d2));
}
}
Como se puede ver en la salida del programa, ambos constructores han creado instancias
Double idénticas, por ello el método equals( ) devuelve verdadero:
3.14159 = 3.14159 -> true
www.detodoprogramacion.com
PARTE II
Descripción
boolean isInfinite( )
PARTE II
Método
389
390
Parte II:
La biblioteca de Java
Los métodos isInfinite( ) e isNaN( )
Float y Double proporcionan los métodos isInfinite( ) e isNaN( ), que ayudan a manipular dos
valores double y float especiales. Estos métodos funcionan con dos valores únicos definidos
por la especificación de punto flotante de IEEE: infinito y NaN (Not a Number). El método
isInfinite( ) devuelve verdadero si el valor probado es infinitamente grande o pequeño en
magnitud. isNaN( ) devuelve verdadero si el valor que se prueba no es un número.
El siguiente ejemplo crea dos objetos Double; uno es infinito, y el otro no es un número:
// Ejemplo con isInfinite() e isNaN()
class InfNaN {
public static void main(String args[]) {
Double d1 = new Double(1/0.);
Double d2 = new Double(0/0.);
System.out.println(dl + ": " + dl.isInfinite() + ", " + dl.isNaN());
System.out.println(d2 + ": " + d2.islnfinite() + ", " + d2.isNaN());
}
}
El programa genera esta salida:
Infinity: true, false
NaN: false, true
Byte, Short, Integer y Long
Las clases Byte, Short, Integer y Long son envoltorios para los tipos enteros byte, short, int y
long respectivamente. Sus constructores son éstos:
Byte(byte num)
Byte(String str) throws NumberFormatException
Short(short num)
Short(String str) throws NumberFormatException
Integer(int num)
Integer(String str) throws NumberFormatException
Long(long num)
Long(String str) throws NumberFormatException
Como se ve, estos objetos se pueden construir a partir de valores numéricos o de cadenas que
contengan valores válidos de números enteros.
Los métodos definidos por estas clases se muestran en las Tablas 16-3 a 16-6. Como puede
observarse, estas clases definen métodos para obtener enteros a partir de cadenas o convertir
cadenas de nuevo en enteros. Existen variantes de estos métodos que permiten especificar
la base numérica para la conversión. Bases comunes son: 2 para binario, 8 para octal, 10 para
decimal y 16 para hexadecimal.
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
391
Se definen las siguientes constantes:
Valor mínimo
MAX_ VALUE
Valor máximo
SIZE
El tamaño en bits de un valor envuelto
TYPE
El objeto Class para byte, short, int, o long
Descripción
byte byteValue( )
Devuelve el valor del objeto que invoca como un byte.
int compareTo(Byte b)
Compara el valor numérico del objeto que invoca con el
de b. Devuelve 0 si los valores son iguales. Devuelve un
valor negativo si el objeto que invoca tiene menor valor.
Devuelve un valor positivo si el objeto que invoca tiene
mayor valor.
static Byte decode(String str)
throws NumberFormatException
Devuelve un objeto Byte que contiene el valor
especificado por la cadena str.
double doubleValue( )
Devuelve el valor del objeto que invoca como un double.
boolean equals(Object ObjByte)
Devuelve verdadero si el objeto Byte que invoca es
equivalente a ObjByte. De lo contrario, devuelve falso.
float floatValue( )
Devuelve el valor del objeto que invoca como un float.
int hashCode( )
Devuelve el código de dispersión del objeto que invoca.
int intValue( )
Devuelve el valor del objeto que invoca como un int.
long longValue( )
Devuelve el valor del objeto que invoca como un long.
static byte parseByte(String str)
throws NumberFormatException
Devuelve el byte equivalente del número contenido en la
cadena especificada en str utilizando la base 10.
static byte parseByte(String str, int base)
throws NumberFormatException
Devuelve el equivalente byte del número contenido en la
cadena especificada en str usando la base especificada
por base.
short shortValue( )
Devuelve el valor del objeto que invoca como un short.
String toString( )
Devuelve una cadena que contiene el equivalente decimal
del objeto que invoca.
static String toString(byte num)
Devuelve una cadena que contiene el equivalente decimal
de num.
static Byte valueOf(byte num)
Devuelve un objeto Byte que contiene el valor
especificado en num.
static Byte valueOf(String str)
throws NumberFormatException
Devuelve un objeto Byte que contiene el valor
especificado por la cadena str.
static Byte valueOf(String str, int base)
throws NumberFormatException
Devuelve un objeto Byte que contiene el valor
especificado por la cadena str usando la base
especificada.
TABLA 16-3 Los métodos definidos por Byte
www.detodoprogramacion.com
PARTE II
Método
PARTE II
MIN_ VALUE
392
Parte II:
La biblioteca de Java
Método
Descripción
byte byteValue( )
Devuelve el valor del objeto que invoca como un byte.
int compareTo(Short s)
Compara el valor numérico del objeto que invoca con el
de s. Devuelve 0 si los valores son iguales. Devuelve
un valor negativo si el objeto que invoca tiene menor
valor. Devuelve un valor positivo si el objeto que invoca
tiene mayor valor.
static Short decode(String str)
throws NumberFormatException
Devuelve un objeto Short que contiene el valor
especificado por la cadena str.
double doubleValue( )
Devuelve el valor del objeto que invoca como un
double.
boolean equals(Object ObjShort)
Devuelve verdadero si el objeto Short que invoca es
equivalente a ObjShort. De lo contrario, devuelve falso.
float floatValue( )
Devuelve el valor del objeto que invoca como un float.
int hashCode( )
Devuelve el código de dispersión del objeto que invoca.
int intValue( )
Devuelve el valor del objeto que invoca como un int.
long longValue( )
Devuelve el valor del objeto que invoca como un long.
static short parseShort(String str)
throws NumberFormatException
Devuelve el equivalente short del número contenido en
la cadena especificada en str usando base 10.
static short parseShort(String str, int base) Devuelve el equivalente short del número contenido
throws NumberFormatException
en la cadena especificada en str usando la base
especificada.
static short reverseBytes(short num)
Intercambia el orden los bytes más altos y los más
bajos de num y devuelve el resultado.
short shortValue( )
Devuelve el valor del objeto que invoca como un short.
String toString( )
Devuelve una cadena que contiene el equivalente
decimal del objeto que invoca.
static String toString(short num)
Devuelve una cadena que contiene el equivalente
decimal de num.
static Short valueOf(short num)
Devuelve un objeto Short que contiene el valor
especificado por num.
static Short valueOf(String str)
throws NumberFormatException
Devuelve un objeto Short que contiene el valor
especificado por la cadena str usando base 10.
static Short valueOf(String str, int base)
throws NumberFormatException
Devuelve un objeto Short que contiene el valor
especificado por la cadena str usando la base
especificada.
TABLA 16-4 Los métodos definidos por la clase Short
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
static int bitCount(int num)
Devuelve el número de bits determinados en num
byte byteValue( )
Devuelve el valor del objeto que invoca como un byte.
int compareTo(Integer i)
Compara el valor numérico del objeto que invoca con el
de i. Devuelve 0 si los valores son iguales. Devuelve un
valor negativo si el objeto que invoca tiene menor valor.
Devuelve un valor positivo si el objeto que invoca tiene
mayor valor.
static Integer decode(String str) throws
NumberFormatException
Devuelve un objeto Integer que contiene el valor
especificado por la cadena str.
double doubleValue( )
Devuelve el valor del objeto que invoca como un
double.
boolean equals(Object ObjInteger)
Devuelve verdadero si el objeto Integer que invoca
es equivalente a ObjInteger. De lo contrario, devuelve
falso.
float floatValue( )
Devuelve el valor del objeto que invoca como un float.
static Integer getInteger(String
nomPropiedad)
Devuelve el valor asociado a la propiedad de entorno
especificada por nomPropiedad. En caso de fallo, se
devuelve null.
static Integer getInteger(String
nomPropiedad, int omisión)
Devuelve el valor asociado a la propiedad de entorno
especificada por nomPropiedad. En caso de fallo, se
devuelve el valor omisión.
static Integer getInteger(String
nomPropiedad, Integer omisión)
Devuelve el valor asociado a la propiedad de entorno
especificada por nomPropiedad. En caso de fallo, se
devuelve el valor por omisión.
int hashCode( )
Devuelve el código de dispersión del objeto que invoca.
static int highestOneBit(int num)
Determina la posición del bit de mayor orden con valor
en num. Devuelve un valor en el cual sólo este bit
está definido. Si no hay algún bit definido, entonces se
devuelve cero.
int intValue( )
Devuelve el valor del objeto que invoca como un int.
long longValue( )
Devuelve el valor del objeto que invoca como un long.
static int lowestOneBit(int num)
Determina la posición del bit de orden menor definido
en num. Devuelve el valor en el cual sólo este bit está
definido. Si no hay algún bit definido, entonces se
devuelve cero
static int numberOfLeadingZeros(int num)
Devuelve el número de bits de mayor orden en cero que
preceden al primer bit de mayor orden definido en num.
Si num es cero, se devuelve 32.
TABLA 16-5 Los métodos definidos por la clase Integer
www.detodoprogramacion.com
PARTE II
Descripción
PARTE II
Método
393
394
Parte II:
La biblioteca de Java
Método
Descripción
static int numberOfTrailingZeros(int num)
Devuelve el número de bits de menor orden en cero
que preceden al primer bit de menor orden definido en
num. Si num es cero, se devuelve 32.
static int parseInt(String str) throws
NumberFormatException
Devuelve el entero equivalente del número contenido
en la cadena especificada en str utilizando la base 10.
static int parseInt(String str, int base)
throws NumberFormatException
Devuelve el entero equivalente del número contenido
en la cadena especificada en str utilizando la base
especificada.
static int reverse(int num)
Invierte el orden de los bits en num y devuelve el
resultado.
static int reverseBytes(int num)
Invierte el orden de los bytes en num y devuelve el
resultado.
static int rotateLeft(int num, int n)
Devuelve el resultado de rotar num, n posiciones a la
izquierda.
static int rotateRight(int num, int n)
Devuelve el resultado de rotar num, n posiciones a la
derecha.
static int signum(int num)
Devuelve –1 si num es negativo, 0 si es cero y 1 si es
positivo.
short shortValue( )
Devuelve el valor del objeto que invoca como un short.
static String toBinaryString(int num)
Devuelve una cadena que contiene el binario
equivalente de num.
static String toHexString(int num)
Devuelve una cadena que contiene el hexadecimal
equivalente de num.
static String toOctalString(int num)
Devuelve una cadena que contiene el octal equivalente
de num.
String toString( )
Devuelve una cadena que contiene el decimal
equivalente del objeto que invoca.
static String toString(int num)
Devuelve una cadena que contiene el decimal
equivalente de num.
static String toString(int num, int base)
Devuelve una cadena que contiene el decimal
equivalente de num utilizando la base especificada.
static Integer valueOf(int num)
Devuelve un objeto Integer que contiene el valor
especificado por num.
static Integer valueOf(String str) throws
NumberFormatException
Devuelve un objeto Integer que contiene el valor
especificado por la cadena str.
static Integer valueOf(String str, int base)
throws NumberFormatException
Devuelve un objeto Integer que contiene el valor
especificado por la cadena str utilizando la base
especificada.
TABLA 16-5 Los métodos definidos por la clase Integer (continuación)
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
Devuelve el número de bits con valor en num.
byte byteValue( )
Devuelve el valor del objeto que invoca como un byte.
int compareTo(Long l)
Compara el valor numérico del objeto que invoca con el
de l. Devuelve 0 si los valores son iguales. Devuelve un
valor negativo si el objeto que invoca tiene menor valor.
Devuelve un valor positivo si el objeto que invoca tiene
mayor valor.
static Long decode(String str) throws
NumberFormatException
Devuelve un objeto Long que contiene el valor
especificado por la cadena str.
double doubleValue( )
Devuelve el valor del objeto que invoca como un
double.
boolean equals(Object ObjLong)
Devuelve verdadero si el objeto Long que invoca es
equivalente a ObjLong. De lo contrario, devuelve falso.
float floatValue( )
Devuelve el valor del objeto que invoca como un float.
static Long getLong(String nomPropiedad)
Devuelve el valor asociado a la propiedad de entorno
especificada por nomPropiedad. En caso de fallo, se
devuelve null.
static Long getLong(String nomPropiedad,
long om)
Devuelve el valor asociado a la propiedad de entorno
especificada por nomPropiedad. En caso de fallo, se
devuelve el valor del parámetro om.
static Long getLong(String nomPropiedad,
Long om)
Devuelve el valor asociado a la propiedad de entorno
especificada por nomPropiedad. En caso de fallo, se
devuelve el valor del parámetro om.
int hashCode( )
Devuelve el código de dispersión del objeto que invoca.
static long highestOneBit(long num)
Determina la posición del bits de mayor orden de num
con valor 1. Devuelve un valor con el cual sólo este bit
está definido. Si ningún bit tiene valor 1, entonces se
devuelve cero.
int intValue( )
Devuelve el valor del objeto que invoca como un int.
long longValue( )
Devuelve el valor del objeto que invoca como un long.
static long lowestOneBit(long num)
Determina la posición del bit de menor orden definido
en num. Devuelve un valor con el cuál solo este bit
está definido. Si ningún bit tiene valor 1, entonces se
devuelve cero.
static int numberOfLeadingZeros(long
num)
Devuelve el número de bits de orden mayor en cero que
preceden al primer bit de mayor orden en 1, dentro de
num. Si num es cero, 64 es devuelto.
static int numberOfTrailingZeros(long num)
Devuelve el número de bits de menor orden en cero
que preceden el primer bit de menor orden en 1 dentro
de num. Si num es cero, 64 es devuelto.
TABLA 16-6 Los métodos definidos por la clase Long
www.detodoprogramacion.com
PARTE II
Descripción
static int bitCount(long num)
PARTE II
Método
395
396
Parte II:
La biblioteca de Java
Método
Descripción
static long parseLong(String str) throws
NumberFormatException
Devuelve el equivalente long del número contenido en
la cadena especificada en str en base 10.
static long parseLong(String str, int base)
throws NumberFormatException
Devuelve el equivalente long del número contenido
en la cadena especificada en str utilizando la base
especificada.
static long reverse(long num)
Invierte el orden de los bits en num y devuelve el
resultado.
static long reverseBytes(long num)
Invierte el orden de los bytes en num y devuelve el
resultado.
static long rotateLeft(long num, int n)
Devuelve el resultado de rotar num n posiciones a la
izquierda.
static long rotateRight(long num, int n)
Devuelve el resultado de rotar num n posiciones a la
derecha.
static int signum(long num)
Devuelve –1 si num es negativo, 0 si es cero y 1 si es
positivo.
short shortValue( )
Devuelve el valor del objeto que invoca como un short.
static String toBinaryString(long num)
Devuelve una cadena que contiene el equivalente
binario de num.
static String toHexString(long num)
Devuelve una cadena que contiene el equivalente
hexadecimal de num.
static String toOctalString(long num)
Devuelve una cadena que contiene el equivalente octal
de num.
String toString( )
Devuelve una cadena que contiene el equivalente
decimal del objeto que invoca.
static String toString(long num)
Devuelve una cadena que contiene el equivalente
decimal de num.
static String toString(long num, int base)
Devuelve una cadena que contiene el decimal
equivalente de num utilizando la base especificada.
static Long valueOf(long num)
Devuelve un objeto Long que contiene el valor
especificado por num.
static Long valueOf(String str) throws
NumberFormatException
Devuelve un objeto Long que contiene el valor
especificado por la cadena str.
static Long valueOf(String str, int base)
throws NumberFormatException
Devuelve un objeto Long que contiene el valor
especificado por la cadena str utilizando la base
especificada.
TABLA 16-6 Los métodos definidos por la clase Long (continuación)
Conversión entre números y cadenas
Una de las tareas más habituales en programación es convertir la representación como cadena
de un número en su formato interno, binario. Afortunadamente, Java proporciona una manera
fácil de hacerlo. Las clases Byte, Short, Integer y Long proporcionan los métodos parseByte( ),
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
import java.io.*;
class ParseDemo {
public static void main(String args[]) throws IOException {
// crear un BufferedReader utilizando System.in
BufferedReader br = new
BufferedReader(new InputStrearnReader(System.in));
String str;
int i;
int sum=0;
System.out.println("Introduzca números y 0 para salir.");
do {
str = br.readLine();
try {
i = Integer.parseInt(str);
} catch (NumberFormatException e) {
System.out.println("Formato no válido");
i = 0;
}
sum += i;
System.out.println("La suma actual es: " + sum);
} while(i != 0);
}
}
Para convertir un número entero en una cadena decimal, han de utilizarse las versiones
de toString( ) definidas en las clases Byte, Short, Integer o Long. Las clases Integer y Long
también proporcionan los métodos toBinaryString( ), toHexString( ) y toOctalString( ), que
convierten un valor en una cadena a formato binario, hexadecimal u octal, respectivamente.
El siguiente programa ejemplifica la conversión binaria, hexadecimal y octal:
/* Convertir un entero en binario, hexadecimal
y octal.
*/
class StringConversions {
public static void main(String args[]) {
int num = 19648;
System.out.println(num + " en binario: " +
Integer.toBinaryString(num));
www.detodoprogramacion.com
PARTE II
/* Este programa suma una lista de números introducidos
por el usuario. Convierte la representación como cadena
de cada número en un entero utilizando el método parseInt().
*/
PARTE II
parseShort( ), parseInt( ) y parseLong( ) respectivamente. Estos métodos devuelven el
equivalente byte, short, int o long de la cadena numérica que los llama. Existen métodos
similares para las clases Float y Double.
El siguiente programa ejemplifica el uso del método parseInt( ). El programa suma una lista
de enteros introducidos por el usuario. Lee los enteros utilizando readLine( ) y usa parseInt( )
para convertir las cadenas leídas en sus valores entero equivalentes.
397
398
Parte II:
La biblioteca de Java
System.out.println(num + " en octal: " +
Integer.toOctalString(num));
System.out.println(num + " en hexadecimal: " +
Integer.toHexString(num));
}
}
La salida del programa es ésta:
19648 en binario: 100110011000000
19648 en octal: 46300
19648 en hexadecimal: 4cc0
Character
Character es un envoltorio simple para un char. El constructor para Character es:
Character(char ch)
Donde ch especifica el carácter que será envuelto por el objeto Character creado.
Para obtener el valor char contenido en un objeto Character, ha de llamarse al método
charValue( ) como se muestra a continuación:
char charValue( )
El método devuelve el carácter.
La clase Character define diferentes constantes, incluyendo las siguientes:
MAX_RADIX
La base mayor
MIN_RADIX
La base menor
MAX_ VALUE
El valor mayor de carácter
MIN_VALUE
El valor menor de carácter
TYPE
El objeto Class correspondiente a char
La clase Character incluye diferentes métodos estáticos que clasifican caracteres y los convierten
de mayúsculas a minúsculas o viceversa. Los métodos se muestran en la Tabla 16-7. El siguiente
ejemplo muestra el uso de algunos de estos métodos.
// Ejemplo con varios métodos Is...
class IsDemo {
public static void main(String args[]) {
char a[] = {'a' , 'b' , '5' , '?', 'A', ' '};
for(int i=0; i args)
ProcessBuilder(String … args)
Donde, args es una lista de argumentos que especifican el nombre del programa a ser ejecutado
junto con cualquier argumento de línea de comandos requerida. En el primer constructor, los
argumentos son pasados en un objeto de tipo List. En el segundo, se especifican a través de
parámetros varargs. La Tabla 16-12 describe los métodos definidos por la clase ProcessBuilder.
www.detodoprogramacion.com
PARTE II
Existen diferentes formas alternativas del método exec( ), pero la mostrada en el ejemplo
es la más común. El objeto Process devuelto por el método exec( ) puede manipularse mediante
los métodos de la clase Process después de que el nuevo programa inicia su ejecución. Se puede
eliminar el subproceso con el método destroy( ). El método waitFor( ) hace que el programa
Java espere hasta la terminación del subproceso. El método exitValue( ) devuelve el valor
devuelto por el subproceso cuando éste termina, que es típicamente 0 si no hay problemas. A
continuación se muestra el ejemplo anterior del método exec( ) modificado para esperar a que el
proceso en ejecución termine:
PARTE II
} catch (Exception e) {
System.out.println("Error al ejecutar notepad.");
}
407
408
Parte II:
La biblioteca de Java
Para crear un proceso utilizando la clase ProcessBuilder, simplemente se crea una instancia
de ProcessBuilder, especificando el nombre del programa y cualquier argumento que se
necesite. Para comenzar la ejecución del programa, se llama al método start( ) sobre la instancia.
A continuación un ejemplo que ejecuta el editor de textos notepad de Windows. Note que se
especifica el nombre del archivo a editar como un argumento.
class PBDemo {
public static void main (String args[]) {
try {
ProcessBuilder proc =
new ProcessBuilder("notepad.exe", "archivoPrueba");
proc.start();
} catch (Exception e){
System.out.println("Error ejecutando notepad");
}
}
}
Método
Descripción
List command( )
Devuelve una referencia a un objeto List que contiene el
nombre del programa y sus argumentos. Los cambios en
esta lista afectan al proceso que invoca.
ProcessBuilder command(List args) Define el nombre del programa y sus argumentos con lo
especificado por args. Los cambios a esta lista afecta
al proceso que invoca. Devuelve una referencia al objeto
que invoca.
ProcessBuilder command(String … args)
Define el nombre del programa y sus argumentos con lo
especificado por args. Devuelve una referencia al objeto
que invoca.
File directory( )
Devuelve el directorio de trabajo actual del objeto que
invoca. Este valor será null si el directorio es el mismo
que el del programa de Java que comenzó al proceso.
ProcessBuilder directoy(File dir)
Define el directorio actual de trabajo del objeto que
invoca. Devuelve una referencia al objeto que invoca.
Map environment( )
Devuelve las variables de ambiente asociadas con el
objeto que invoca como pares clave/valor.
boolean redirectErrorStream( )
Devuelve verdadero si el flujo de error estándar ha sido
redireccionado al flujo de salida estándar. Devuelve falso
si los flujos están separados.
ProcessBuilder
redirectErrorStream(boolean fusion)
Si fusion es verdadero, entonces el flujo de error
estándar es redireccionado a la salida estándar. Si fusion
es falso, los flujos son separados, éste es el estado por
omisión. Devuelve una referencia al objeto que invoca.
Process start( )
throws IOException
Comienza al proceso especificado por el objeto que invoca.
En otras palabras, ejecuta el programa especificado.
TABLA 16-12 Los métodos definidos por la clase ProcessBuilder
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
409
La clase System
Descripción
static void arraycopy(Object fuente,
int inicioFuente,
Object destino,
int inicioDestino,
int tamaño)
Copia un arreglo. El arreglo a ser copiado se pasa en
fuente, y el índice del punto en que comenzará la copia
dentro de fuente se pasa en inicioFuente. El arreglo que
recibirá la copia se pasa en destino, y el índice del punto
donde comenzará la copia dentro de destino se pasa en
inicioDestino. El número de elementos que se copiarán se
especifica en tamaño.
static String clearProperty(String v)
Elimina la variable de ambiente especificada por v. El valor
previo asociado con v es devuelto.
static Console console( )
Devuelve la consola asociada con la JVM. Se devuelve null
si la JVM actual no tiene consola (agregado por Java SE 6).
static long currentTimeMillis( )
Devuelve la hora actual en milisegundos desde la
medianoche del 1 de enero de 1970.
static void exit(int codigoSalida)
Detiene la ejecución y devuelve el valor de codigoSalida al
proceso padre (habitualmente el sistema operativo). Por
convención, 0 indica terminación normal. Todos los otros
valores indican algún tipo de error.
static void gc( )
Inicia la recolección de basura.
static Map getenv( )
Devuelve un objeto Map que contiene las variables de
ambiente actuales y sus valores.
static String getenv(String v)
Devuelve el valor asociado con la variable de ambiente
especificada por v.
static Properties getProperties( )
Devuelve las propiedades asociadas con el intérprete de
Java. (La clase Properties se describe en el Capítulo 17).
static String getProperty(String p)
Devuelve la propiedad asociada a p. Un objeto null se
devuelve si la propiedad deseada no es encontrada.
static String getProperty(String p,
String om)
Devuelve la propiedad asociada con p. Si la propiedad
deseada no se encuentra, se devuelve el valor especificado
en om.
static SecurityManager
getSecurityManager( )
Devuelve el gestor de seguridad en uso o un objeto null si
no hay un gestor de seguridad instalado.
static int identityHashCode (Object obj) Devuelve la identidad del código de dispersión para obj.
TABLA 16-13 Los métodos definidos por la clase System
www.detodoprogramacion.com
PARTE II
Método
PARTE II
La clase System contiene una colección de métodos y variables estáticos. La entrada, salida
y salida de errores estándar del intérprete de Java se almacenan en las variables in, out y err
respectivamente. Los métodos definidos por System se muestran en la Tabla 16-13. Nótese que
muchos de los métodos arrojan una excepción del tipo SecurityException si la operación no
está permitida por el gestor de seguridad.
Veamos algunos usos comunes de System.
410
Parte II:
La biblioteca de Java
Método
Descripción
static Channel inheritedChannel( )
thows IOException
Devuelve el canal heredado por la Máquina Virtual de Java.
Devuelve null si no se hereda ningún canal.
static void load(String nombreArchivo)
Carga la biblioteca dinámica contenida en el archivo
especificado por nombreArchivo; nombreArchivo debe
especificar la ruta completa del archivo.
static void loadLibrary(String
nomBiblioteca)
Carga la biblioteca dinámica cuyo nombre está asociado con
nomBiblioteca.
static String mapLibraryName (String b) Devuelve el nombre en una plataforma específica para la
biblioteca llamada b.
static long nanoTime( )
Obtiene un tiempo cronometrado de la manera más precisa
posible en el sistema y devuelve su valor en términos de
nanosegundos comenzando desde algún punto arbitrario. La
exactitud del tiempo medido es impredecible.
static void runFinalization( )
Inicia llamadas a los métodos finalize() de objetos no
utilizados y que no han sido reciclados.
static void setErr(PrintStream flujoErr)
Establece como el flujo estándar de error a flujoErr.
static void setIn(PrintStream flujoEnt)
Establece como el flujo estándar de entrada a flujoEnt.
static void setOut(PrintStream flujoSal)
Establece como el flujo estándar de salida a flujoSal.
static void
setProperties(Properties p)
Establece las propiedades actuales del sistema a los
valores especificados por p.
static String setProperty(String p,
String v)
Asigna el valor v a la propiedad llamada p.
static void setSecurity Manager
(SecurityManager s)
Establece el gestor de seguridad al indicado por el objeto s.
TABLA 16-13 Los métodos definidos por la clase System (continuación)
Uso de currentTimeMillis( )
Un uso de la clase System que puede ser de particular interés es el uso del método
currentTimeMillis( ) para medir cuánto tardan en ejecutarse diversas partes del programa. El
método currentTimeMillis( ) devuelve la hora actual en milisegundos desde la medianoche
del 1 de enero de 1970. Para cronometrar una parte del programa se almacena este valor justo
antes del comienzo de la parte en cuestión; inmediatamente después de terminar su ejecución, se
llama a currentTimeMillis( ) de nuevo. El tiempo transcurrido será entonces el valor obtenido al
terminar, menos el almacenado al comenzar. El siguiente programa lo ejemplifica:
// Cronometrando la ejecución de un programa.
class Elapsed {
public static void main(String args[]) {
long inicio, fin;
System.out.println("Cronometrando un ciclo de 0 a 1,000,000");
// tiempo transcurrido en un ciclo de 0 a 1,000,000
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
System.out.println("Tiempo en milisegundos: " + (fin-inicio));
}
}
PARTE II
inicio = System.currentTimeMillis(); // tiempo inicial
for(int i=0; i < 1000000; i++) ;
fin = System.currentTimeMillis(); // tiempo final
411
La siguiente es una posible salida de la ejecución del programa (recuérdese que los resultados
podrán variar cada vez):
Si el sistema tiene un cronómetro que ofrece precisión en nanosegundos, entonces se podría
sobrescribir el código anterior para usar nanoTime( ) en lugar de currentTimeMillis( ).
Por ejemplo, a continuación está la porción clave del programa anterior, reescrita para utilizar
nanoTime( ):
start = System.nanoTime(); // tiempo inicial
for (int i=0; i < 1000000; i++);
fin = System.natoTime(); // tiempo final
Uso de arraycopy( )
El método arraycopy( ) se puede utilizar para copiar rápidamente un arreglo de cualquier tipo
de un sitio a otro. Esto es mucho más rápido que utilizar un ciclo equivalente, escrito a mano en
Java. Aquí tenemos un ejemplo de dos arreglos que se copian mediante el método arraycopy( ).
Primero, se copia el arreglo a en el arreglo b. Después, todos los elementos de a se desplazan
una posición hacia abajo, y por último, los elementos de b una posición hacia arriba.
// Ejemplo con arraycopy().
class ACDemo {
static byte a[] = { 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 };
static byte b[] = { 77, 77, 77, 77, 77, 77, 77, 77, 77, 77 };
public static void main(String args[]) {
System.out.println("a = " + new String(a));
System.out.println("b = " + new String(b));
System.arraycopy(a, 0, b, 0, a.length);
System.out.println("a = " + new String(a));
System.out.println("b = " + new String (b));
System.arraycopy(a, 0, a, 1, a.length - 1);
System.arraycopy(b, 1, b, 0, b.length - 1);
System.out.println("a = " + new String(a));
System.out.println("b = " + new String(b));
}
}
Como se ve en la siguiente salida, se puede copiar en cualquier dirección utilizando la misma
fuente y el mismo destino:
a = ABCDEFGHIJ
b = MMMMMMMMMM
a = ABCDEFGHIJ
www.detodoprogramacion.com
PARTE II
Cronometrando un ciclo de 0 a 1,000,000
Tiempo en milisegundos: 10
412
Parte II:
La biblioteca de Java
b = ABCDEFGHIJ
a = AABCDEFGHI
b = BCDEFGHIJJ
Propiedades del entorno
Las siguientes propiedades están disponibles:
file.separator
java.specification.version
line.separator
java.class.path
java.vendor
os.arch
java.class.version
java.vendor.url
os.name
java.compiler
java.version
os.version
java.ext.dirs
java.vm.name
path.separator
java.home
java.vm.specification.name
user.dir
java.io.tmpdir
java.vm.specification.vendor
user.home
java.library.path
java.vm.specification.version
user.name
java.specification.name
java.vm.vendor
java.specification.vendor
java.vm.version
Es posible obtener los valores de las diversas variables de entorno llamando al método
System.getProperty( ). Por ejemplo, el siguiente programa muestra el directorio actual del
usuario:
class ShowUserDir {
public static void main(String args[]) {
System.out.println(System.getProperty("user.dir"));
}
}
La clase Object
Como mencionamos en la Parte I, Object es una superclase de todas las demás clases. Object
define los métodos mostrados en la Tabla 16-14, los cuales están disponibles para todos los
objetos.
Método
Descripción
Object clone( )
throws
CloneNotSupportedException
Crea un nuevo objeto que es igual que el objeto que invoca.
boolean equals(Object objeto)
Devuelve verdadero si el objeto que invoca es equivalente a
objeto.
void finalize( )
throws Throwable
Método finalize( ) por omisión. Normalmente es sobrescrito por
las subclases.
TABLA 16-14 Los métodos definidos por la clase Object
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
Obtiene un objeto Class que describe al objeto que invoca.
int hashCode( )
Devuelve el código de dispersión asociado al objeto que invoca.
final void notify( )
Reanuda la ejecución de un hilo en espera del objeto que
invoca.
final void notifyAll( )
Reanuda la ejecución de todos los hilos en espera del objeto
que invoca.
String toString( )
Devuelve una cadena que describe el objeto.
final void wait( ) throws
InterruptedException
Espera a otro hilo de ejecución
final void wait(long milisegundos)
throws InterruptedException
Espera a otro hilo de ejecución durante el número de
milisegundos indicado.
final void wait(long milisegundos,
int nanosegundos)throws
InterruptedException
Espera a otro hilo de ejecución durante el número de
milisegundos más nanosegundos indicados.
TABLA 16-14 Los métodos definidos por la clase Object (continuación)
El método clone( ) y la interfaz Cloneable
La mayoría de los métodos definidos por Object se tratan a lo largo de este libro. Sin embargo,
uno de ellos merece especial atención: el método clone( ). El método clone( ) genera un
duplicado del objeto sobre el que se llama. Sólo se pueden clonar clases que implementen la
interfaz Cloneable.
La interfaz Cloneable no define ningún miembro. Se usa para indicar que una clase permite
la realización de una copia bit a bit de un objeto (esto es, un clon). Si se intenta llamar al método
clone( ) sobre una clase que no implementa Cloneable, se produce una excepción de tipo
CloneNotSupportedException. Cuando se hace un clon, no se invoca al método constructor del
objeto en clonación. Un clon es simplemente una copia exacta del original.
La clonación es una acción potencialmente peligrosa, porque puede ocasionar efectos
colaterales no deseados. Por ejemplo, si el objeto clonado contiene una referencia en una
variable llamada refOb, entonces cuando se hace el clon, refOb en el clon hace referencia al
mismo objeto que refOb en el original. Si el clon hace un cambio en los contenidos del objeto
referenciado por refOb, entonces quedará cambiado también para el objeto original. Otro
ejemplo: si un objeto abre un flujo de E/S y luego se clona, habrá dos objetos capaces de operar
sobre el mismo flujo. Además, si uno de esos objetos cierra el flujo, el otro podría intentar
escribir en él, causando un error. En algunos casos se necesitará sobrescribir el método clone( )
definido por la clase Object para gestionar este tipo de problemas.
Puesto que la clonación puede causar problemas, clone( ) se declara como protected dentro
de Object. Esto significa que debe ser llamado desde un método definido por una clase que
implemente la interfaz Cloneable, o bien debe ser sobrescrito explícitamente por esa clase para
que sea público. Veamos un ejemplo de cada uno de estos dos casos.
El siguiente programa implementa Cloneable y define el método cloneTest( ), que llama al
método clone( ) de Object.
www.detodoprogramacion.com
PARTE II
Descripción
final Class>getClass( )
PARTE II
Método
413
414
Parte II:
La biblioteca de Java
// Ejemplo con el método clone()
class TestClone implements Cloneable {
int a;
double b;
// Este método llama a clone() de Object.
TestClone cloneTest() {
try {
// llama a clone en Object.
return (TestClone) super.clone();
} catch(CloneNotSupportedException e) {
System.out.println("Clonación no permitida.");
return this;
}
}
}
class CloneDemo {
public static void main(String args[]) {
TestClone xl = new TestClone();
TestClone x2;
x1.a =10;
x1.b = 20.98;
x2 =xl.cloneTest(); // clonación de xl
System.out.println("xl: " + xl.a + " " + x1.b);
System.out.println("x2: " + x2.a + " " + x2.b);
}
}
Aquí, el método cloneTest( ) llama al método clone( ) de la clase Object y devuelve el resultado.
Nótese que el objeto devuelto por clone( ) debe convertirse en su tipo apropiado (TestClone).
El siguiente ejemplo sobrescribe clone( ) para que pueda ser llamado desde código fuera de
su clase. Para hacer esto, su especificador de acceso debe ser public, como en este ejemplo:
// Sobrescribe el método clone().
class TestClone implements Cloneable {
int a;
double b;
// clone() está ahora sobrescrito y es público.
public Object clone() {
try(
// llama a clone en Object.
return super.clone();
} catch(CloneNotSupportedException e) {
System.out.println("Clonación no permitida.");
return this;
}
}
}
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
x1.a = 10;
x1.b = 20.98;
PARTE II
class CloneDemo2 {
public static void main(String args[]) {
TestClone xl = new TestClone();
TestClone x2;
415
// aquí se llama a clone() directamente.
x2 = (TestClone) xl.clone();
}
}
Los efectos colaterales causados por la clonación son difíciles de detectar a primera vista. Es
fácil pensar que una clase es segura para la clonación cuando de hecho no lo es. En general, es
mejor no implementar Cloneable para ninguna clase si no hay una buena razón para ello.
Class
Class encapsula el estado en tiempo de ejecución de un objeto o interfaz. Los objetos del tipo
Class se crean automáticamente, cuando se cargan las clases. No se puede declarar
explícitamente un objeto Class. Generalmente, se obtiene un objeto Class llamando al método
getClass( ) definido por Object.
Class es un tipo genérico que se declara como se muestra a continuación:
class Class
Donde, T es el tipo de la clase o interfaz representada. Un ejemplo de los métodos definidos por
Class se muestran en la Tabla 16-15
Método
Descripción
static Class > forName(String nombre)
throws ClassNotFoundException
Devuelve un objeto Class dando su nombre completo.
static Class> forName(String nombre,
boolean c,
ClassLoader cgr)
throws ClassNotFoundException
Devuelve un objeto Class, dando su nombre completo.
El objeto se carga utilizando el cargador especificado
en cgr. Si el parámetro c es verdadero, el objeto es
inicializado.
A
getAnnotation(Class aTipo)
Devuelve un objeto Annotation que contiene la
anotación asociada con aTipo para el objeto
invocado.
TABLA 16-15 Algunos métodos definidos por la clase Class
www.detodoprogramacion.com
PARTE II
System.out.println("xl: " + xl.a + " " + xl.b);
System.out.println("x2: " + x2.a + " " + x2.b);
416
Parte II:
La biblioteca de Java
Método
Descripción
Annotation[ ] getAnnotations( )
Obtiene todas las anotaciones asociadas con el objeto
que invoca y las almacena en un arreglo de objetos de
tipo Annotation. Devuelve una referencia a este arreglo.
Class>[ ] getClasses( )
Devuelve un objeto Class para cada una de las clases
e interfaces públicas que son miembros del objeto que
invoca.
ClassLoader getClassLoader( )
Devuelve el objeto ClassLoader que cargó la clase o
interfaz utilizada para instanciar al objeto que invoca.
Constructor
getConstructors (Class> … pTipos)
throws NoSuchMethodException,
SecurityException
Devuelve un objeto de tipo Constructor que representa
al constructor del objeto que invoca que tiene los tipos
de parámetros especificados por pTipos.
Constructor>[ ] getConstructors( )
throws SecurityException
Obtiene un objeto de tipo Constructor para cada
constructor público del objeto que invoca y los almacena
en un arreglo. Devuelve la referencia a dicho arreglo.
Annotation[ ] getDeclaredAnnotations( )
Obtiene un objeto de tipo Annotation para todas las
anotaciones que están declaradas por el objeto que
invoca y los almacena en un arreglo. Devuelve una
referencia a dicho arreglo (las anotaciones heredadas
son ignoradas).
Constructor>[ ] getDeclared
Constructors( )throws SecurityException
Obtiene un objeto de tipo Constructor para cada constructor declarado por el objeto que invoca y los almacena
en un arreglo. Devuelve la referencia a este arreglo (los
constructores de las superclases son ignorados).
Field[ ] getDeclaredFields( )
throws SecurityException
Devuelve un objeto Field para todos los campos
declarados por esta clase y los almacena en un arreglo.
Devuelve una referencia a dicho arreglo (los campos
heredados se ignoran).
Method[ ] getDeclaredMethods( )
throws SecurityException
Devuelve un objeto Method para cada uno de los
métodos declarados por esta clase o interfaz y los
almacena en un arreglo. Devuelve la referencia de dicho
arreglo (los métodos heredados son ignorados).
Field[ ] getFields(String campoNom)
throws NoSuchMethodException,
SecurityException
Devuelve un objeto Field que representa el campo
especificado por campoNom para el objeto que invoca.
Field[ ] getFields( )throws
SecurityException
Devuelve un objeto Field para todos los campos públicos
del objeto que invoca y los almacena en un arreglo.
Devuelve la referencia a dicho arreglo.
Class>[ ] getInterfaces( )
Cuando se invoca a un objeto, este método devuelve un
arreglo de las interfaces implementadas por clase del
objeto. Cuando se invoca a una interfaz, este método
devuelve un arreglo de interfaces extendidas por la
interfaz.
TABLA 16-15 Algunos métodos definidos por la clase Class (continuación)
www.detodoprogramacion.com
Capítulo 16:
Método
Explorando java.lang
417
Descripción
Obtiene un objeto Method para cada método público
del objeto que invoca y los almacena en un arreglo.
Devuelve la referencia a dicho arreglo.
String getName( )
Devuelve el nombre completo de la clase o la interfaz
del objeto que invoca.
ProtectionDomain getProtectionDomain( )
Devuelve el dominio de protección asociado con el
objeto que invoca.
Class super T>getSuperclass( )
Devuelve la superclase del objeto que invoca. El valor
devuelto es null si el objeto que invoca es de tipo
Object.
boolean isInterface( )
Devuelve verdadero si el objeto que invoca es una
interfaz. De lo contrario devuelve falso.
T newInstance( )
throws IllegalAccessException,
InstantiationException
Crea una nueva instancia (por ejemplo, un objeto
nuevo) que es del mismo tipo que el objeto que
invoca. Esto es equivalente a utilizar el operador
new con el constructor por omisión de la clase. Se
devuelve el nuevo objeto creado.
String toString( )
Devuelve una cadena que representa al objeto o
interfaz que invoca.
TABLA 16-15 Algunos métodos definidos por la clase Class (continuación)
Los métodos definidos por Class son a menudo útiles en situaciones en que se necesita
información de un objeto en tiempo de ejecución. Como lo muestra la Tabla 16-15, la clase
proporciona métodos que permiten determinar información adicional sobre una clase concreta,
como por ejemplo sus campos, sus métodos y constructores públicos. Además de otras cosas.
Esto es importante para el funcionamiento de los Java Beans, el cual se tratará más adelante en
este libro.
El siguiente programa ejemplifica el uso del método getClass( ) (heredado de Object) y
getSuperclass( ) (de la clase Class):
// Ejemplo con información en tiempo de ejecución.
c1ass X {
int a;
float b;
}
class Y extends X
double c;
}
c1ass RTTI {
www.detodoprogramacion.com
PARTE II
Method[ ] getMethods( )
throws SecurityException
PARTE II
Devuelve un objeto de tipo Method que representa el
Method[ ] getMethod(String m,
Class> …paramTipos) método especificado por m y que tiene los tipos de
parámetros especificados por paramTipos.
throws NoSuchMethodException,
SecurityException
418
Parte II:
La biblioteca de Java
public static void main(String args[]) {
X x = new X();
Y y = new y();
Class >clObj;
clObj = x.getClass(); //obtiene la clase
System.out.println("x es un objeto del tipo: " +
clObj.getName());
clObj = y.getClass(); //obtiene la clase
System.out.println("y es un objeto del tipo: " +
clObj.getName());
clObj = clObj.getSuperclass();
System.out.println("la superclase de y es: " +
clObj.getName());
}
}
La salida de este programa es la siguiente:
x es un objeto del tipo: X
y es un objeto del tipo: Y
la superclase de y es: X
ClassLoader
La clase abstracta ClassLoader define cómo se cargan las clases. Una aplicación puede crear
subclases que extiendan ClassLoader, implementando sus métodos. Esto permite cargar clases
de un modo diferente al que normalmente utiliza el intérprete de Java. Sin embargo, esto es algo
que no se hace normalmente.
Math
La clase Math contiene todas las funciones de punto flotante que se utilizan en geometría
y trigonometría, así como varios métodos de propósito general. Math define dos constantes
double: E(aproximadamente 2.72) y PI (aproximadamente 3.14).
Funciones trascendentes
Los siguientes métodos aceptan parámetros double para ángulos en radianes y devuelven el
resultado de su función transcendental:
Método
Descripción
static double sin(double arg)
Devuelve el seno del ángulo especificado por arg en radianes.
static double cos( double arg)
Devuelve el coseno del ángulo especificado por arg en radianes.
static double tan(double arg)
Devuelve la tangente del ángulo especificado por arg en radianes.
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
Descripción
static double asin(double arg)
Devuelve el ángulo cuyo seno viene dado por arg.
static double acos(double arg)
Devuelve el ángulo cuyo coseno viene dado por arg.
static double atan(double arg)
Devuelve el ángulo cuya tangente viene dada por arg.
static double atan2(double x, double y)
Devuelve el ángulo cuya tangente es x/y.
Los siguientes métodos calculan el seno, coseno y tangente hiperbólicos de un ángulo.
Método
Descripción
static double sinh(double arg)
Devuelve el seno hiperbólico de un ángulo especificado por arg.
static double cosh(double arg)
Devuelve el coseno hiperbólico de un ángulo especificado por arg.
static double tanh(double arg)
Devuelve la tangente hiperbólica de un ángulo especificado por arg.
Funciones exponenciales
Math define los siguientes métodos exponenciales:
Método
Descripción
static double cbrt(double arg)
Devuelve la raíz cúbica de arg.
static double exp(double arg)
Devuelve e elevado a arg.
static double expm1(double arg)
Devuelve e elevado a arg-1.
static double log(double arg)
Devuelve el logaritmo natural de arg.
static double log10(double arg)
Devuelve el logaritmo base 10 de arg.
static log1p(double arg)
Devuelve el logaritmo natural de arg+1.
static double pow(double y, double x)
Devuelve y elevado a x; por ejemplo, pow(2.0, 3.0)
devuelve 8.0.
static double scalb(double arg, int factor) Devuelve val * 2 factor (agregado por Java SE 6).
static float scalb(float arg, int factor)
Devuelve val * 2 factor (agregado por Java SE 6).
static double sqrt(double arg)
Devuelve la raíz cuadrada de arg.
Funciones de redondeo
La clase Math define varios métodos que proporcionan distintos tipos de operaciones de
redondeo. Las cuales se muestran en la Tabla 16-16. Observe los dos métodos ulp( ) al final de la
tabla. En este contexto, ulp representa las siglas en inglés de la frase “unidades en el último lugar”.
Los métodos ulp obtienen el número de unidades entre un valor y el valor superior siguiente.
Pueden ser utilizados para evaluar la exactitud de un resultado.
www.detodoprogramacion.com
PARTE II
Método
PARTE II
Los siguientes métodos toman como parámetro el resultado de una función trascendente y
devuelven, en radianes, el ángulo que produciría ese resultado. Son las funciones inversas de las
anteriores.
419
420
Parte II:
La biblioteca de Java
Método
Descripción
static int abs(int arg)
Devuelve el valor absoluto de arg.
static long abs(long arg)
Devuelve el valor absoluto de arg.
static float abs(float arg)
Devuelve el valor absoluto de arg.
static double abs(double arg)
Devuelve el valor absoluto de arg.
static double ceil(double arg)
Devuelve el menor número entero mayor o igual a arg.
static double floor(double arg)
Devuelve el mayor número entero menor o iguales a arg.
static int max(int x, int y)
Devuelve el máximo de x e y.
static long max(long x, long y)
Devuelve el máximo de x e y.
static float max(float x, float y)
Devuelve el máximo de x e y.
static double max(double x, double y)
Devuelve el máximo de x e y.
static int min(int x, int y)
Devuelve el mínimo de x e y.
static long min(long x, long y)
Devuelve el mínimo de x e y.
static float min(float x, float y)
Devuelve el mínimo de x e y.
static double min(double x, double y)
Devuelve el mínimo de x e y.
static double nextAfter(double arg,
Comenzado con el valor de arg, devuelve el siguiente valor
double adelante) en dirección hacia adelante. Si arg = = adelante, entonces
adelante se devuelve (agregado por Java SE 6).
static float nextAfter(float arg,
double adelante)
Comenzado con el valor de arg, devuelve el siguiente valor
en dirección hacia adelante. Si arg = = adelante, entonces
adelante se devuelve (agregado por Java SE 6).
static double nextUp(double arg)
Devuelve el siguiente valor en dirección positiva desde arg
(agregado por Java SE 6).
static float nextUp(float arg)
Devuelve el siguiente valor en dirección positiva desde arg
(agregado por Java SE 6).
static double rint(double arg)
Devuelve el entero más cercano en valor a arg.
static int round(float arg)
Devuelve arg redondeando al int más cercano.
static long round(double arg)
Devuelve arg redondeado al long más cercano.
static float ulp(float arg)
Devuelve el ulp para arg.
static double ulp(double arg)
Devuelve el ulp para arg.
TABLA 16-16 Los métodos de redondeo definidos por la clase Math
Otros métodos en la clase Math
Además de los métodos que acabamos de comentar, Math define los siguientes métodos:
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
static double copySign(double arg,
double signarg)
Devuelve arg con el signo especificado en signarg.
(agregado por Java SE 6).
static float copySign(float arg,
double signarg)
Devuelve arg con el signo especificado en signarg
(agregado por Java SE 6).
static int getExponent(double arg)
Devuelve el exponente base 2 utilizado para la
representación binaria de arg (agregado por Java SE 6).
static int getExponent(float arg)
Devuelve el exponente base 2 utilizado para la
representación binaria de arg (agregado por Java SE 6).
static double
IEEEremainder (double dividendo,
double divisor)
Devuelve el residuo de la división dividendo/divisor.
static hypot (double lado1, double lado2) Devuelve la longitud de la hipotenusa de un triángulo
dada la longitud de dos lados opuestos.
static double random( )
Devuelve un número pseudoaleatorio comprendido entre
0 y 1.
static float signum(double arg)
Determina el signo de un valor. Devuelve 0 si arg es 0, 1
si arg es mayor que 0, y -1 si arg es menor que 0.
static float signum(float arg)
Determina el signo de un valor. Devuelve 0 si arg es 0, 1
si arg es mayor que 0, y -1 si arg es menor que 0.
static double toRadians(double a)
Convierte grados en radianes. El ángulo especificado en
a debe estar especificado en radianes. Se devuelve el
resultado en grados.
static double toDegrees( double a)
Convierte radianes en grados. El ángulo especificado
en a debe estar especificado en grados. Se devuelve el
resultado en radianes.
Aquí tenemos un ejemplo del uso de los métodos toRadians( ) y toDegrees( ):
// Ejemplo de los métodos toDegrees() y toRadians().
class Angles {
public static void main(String args[]) {
double theta = 120.0;
System.out.println(theta + " grados son" +
Math.toRadians(theta) + " radianes.");
theta =1.312;
System.out.println(theta + " radianes son " +
Math.toDegrees(theta) + " grados.");
}
}
La salida de este programa es:
120.0 grados son 2.0943951023931953 radianes.
1.312 radianes son 75.17206272116401 grados.
www.detodoprogramacion.com
PARTE II
Descripción
PARTE II
Método
421
422
Parte II:
La biblioteca de Java
StrictMath
La clase StrictMath define un conjunto completo de métodos matemáticos paralelos a los de
Math. La diferencia es que la versión StrictMath garantiza la generación de resultados idénticos
en todas las implementaciones de Java, mientras que a los métodos de Math se les permite un
poco de libertad en su rango de valores para mejorar el rendimiento.
Compiler
La clase Compiler permite la creación de entornos de Java en que el código binario de
Java se compila en código ejecutable, en lugar de interpretarse. Esta clase no se utiliza en la
programación convencional.
Thread, ThreadGroup y Runnable
La interfaz Runnable y las clases Thread y ThreadGroup dan soporte a la programación
multihilo. Cada una es examinada a continuación.
NOTA En el Capítulo 11 se hace una introducción a las técnicas utilizadas para gestionar hilos,
implementar la interfaz Runnable y crear programas multihilo.
La interfaz Runnable
La interfaz Runnable debe ser implementada por cualquier clase que inicie un hilo separado
de ejecución. Runnable sólo define un método abstracto, llamado run( ), que es el punto de
entrada al hilo. Se define como sigue:
abstract void run( )
Los hilos creados por el programador deben implementar este método.
Thread
Thread crea un nuevo hilo de ejecución. Define los siguientes constructores comunes:
Thread( )
Thread(Runnable objHilo)
Thread(Runnable objHilo, String nomHilo)
Thread(String nomHilo)
Thread(ThreadGroup objGrupo, Runnable objHilo)
Thread(ThreadGroup objGrupo, Runnable objHilo, String nomHilo)
Thread(ThreadGroup objGrupo, String nomHilo)
objHilo es una instancia de una clase que implementa la interfaz Runnable y define dónde
comienza la ejecución de un hilo. El nombre del hilo se especifica en nomHilo. Cuando no se
proporciona un nombre, la Máquina Virtual de Java crea uno. objGrupo especifica el grupo de
hilos al que el nuevo hilo pertenecerá. Cuando no se proporciona un grupo de hilos, el nuevo
hilo pertenecerá al mismo grupo que su hilo padre.
Thread define las siguientes constantes:
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
Método
static int activeCount( )
Descripción
Devuelve el número de hilos en el grupo al que pertenece
el hilo.
void checkAccess( )
Causa que el gestor de seguridad verifique que el hilo
actual pueda acceder y/o cambiar el hilo sobre el que se
llama a checkAccess( ).
static Thread currentThread( )
Devuelve un objeto Thread que encapsula el hilo que llama
a este método.
static void dumpStack( )
Muestra la pila de llamadas del hilo.
static int enumerate(Thread hilos[ ])
Pone en el arreglo hilos copias de todos los objetos
Thread en el grupo del hilo actual. El método devuelve el
número de hilos.
static Map getAllStackTraces( )
todos los hilos activos. En el objeto Map, cada entrada
consiste de una clave, la cual es un objeto Thread, y su
valor, el cuál es un arreglo de StackTraceElement.
ClassLoader getContextClassLoader( )
Devuelve el cargador de clases que se utiliza para cargar
clases y recursos para este hilo.
static Thread.UncaughtExceptionHandler Devuelve el manejador por omisión utilizado para gestionar
getDefaultUncaughExceptionHandler( ) las excepciones libres.
long getID( )
Devuelve el ID del hilo que invoca.
final String getName( )
Devuelve el nombre del hilo.
final int getPriority( )
Devuelve la prioridad del hilo.
StackTraceElement[ ] getStackTrace( )
Devuelve un arreglo que contiene la pila de rastreo para el
hilo que invoca.
Thread.State getState( )
Devuelve el estado del hilo que invoca.
final ThreadGroup getThreadGroup( )
Devuelve el objeto ThreadGroup del que el hilo que invoca
es miembro.
Thread.UncaughtExceptionHandler
Devuelve el manejador utilizado por el hilo que invoca para
getUncaughtExceptionHandler( )
gestionar las excepciones libres.
static boolean holdsLock(Object obj)
Devuelve verdadero si el hilo que invoca posee el candado
sobre obj. Devuelve falso en cualquier otro caso.
TABLA 16-17 Los métodos definidos por la clase Thread
www.detodoprogramacion.com
PARTE II
Como sus nombres lo indican, estas constantes especifican las prioridades máxima, mínima y
normal de los hilos.
Los métodos definidos por Thread se muestran en la Tabla 16-17. En versiones anteriores
de Java, la clase Thread incluía además a los métodos stop( ), suspend( ) y resume( ). Sin
embargo, como se explica en el Capítulo 11, éstos se han desechado por ser intrínsecamente
inestables. Java también ha descartado al método count StackFrames( ) debido a que llama a
los métodos suspend( ) y destroy( ), lo cual puede causar un bloqueo del tipo conocido como
deadlock.
PARTE II
MAX_PRIORITY
MIN_PRIORITY
NORM_PRIORITY
423
424
Parte II:
La biblioteca de Java
Método
void interrupt( )
static boolean interrupted( )
final boolean isAlive( )
final boolean isDaemon( )
boolean isInterrupted( )
final void join( )
throws InterruptedException
final void join(long milisegundos)
throws InterruptedException
final void join(long milisegundos,
int nanosegundos) throws
InterruptedException
void run( )
void setContextClassLoader(ClassLoader cl)
Descripción
Interrumpe el hilo.
Devuelve verdadero si el hilo actualmente en ejecución
ha sido programado para su interrupción. De lo contrario,
devuelve falso.
Devuelve verdadero si el hilo sigue activo. De lo
contrario, devuelve falso.
Devuelve verdadero si el hilo es un hilo demonio. De lo
contrario, devuelve falso.
Devuelve verdadero si el hilo está interrumpido. De lo
contrario, devuelve falso.
Espera hasta que el hilo termine.
Espera el número de milisegundos especificado a que
termine el hilo sobre el que es llamado.
Espera el número de milisegundos más nanosegundos
especificados a que termine el hilo sobre el que es
llamado.
Comienza la ejecución de un hilo.
Establece al cargador de clases cl como el cargador a
utilizar por el hilo que invoca.
Marca el hilo como un hilo de tipo demonio.
Define a em como el manejador de excepciones libres
por omisión.
final void setDaemon(boolean estado)
static void
setDefaultUncaughtExceptionHandler
(Thread.UncaughtExceptionHandler em)
final void setName(String nomHilo)
Establece el nombre del hilo al indicado en nomHilo.
final void setPriority(int prioridad)
Establece la prioridad del hilo a la especificada por
prioridad.
void
Define a em como el manejador de excepciones libres
setUncaughtExceptionHandler
por omisión para el hilo que invoca.
(Thread.UncaughtExceptionHandler em)
static void sleep(long milisegundos)
Suspende la ejecución del hilo durante el número de
throws InterruptedException
milisegundos especificado.
static void sleep(long milisegundos,
Suspende la ejecución del hilo durante el número de
int nanosegundos)
milisegundos más nanosegundos especificados.
throws InterruptedException
void start( )
Inicia la ejecución del hilo.
String toString( )
Devuelve la cadena equivalente de un hilo.
static void yield( )
El hilo que invoca cede el CPU a otro hilo.
TABLA 16-17 Los métodos definidos por la clase Thread (continuación)
ThreadGroup
La clase ThreadGroup crea un grupo de hilos. Esta clase define los siguientes dos constructores:
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
425
ThreadGroup(String nomGrupo)
ThreadGroup(ThreadGroup objPadre, String nomGrupo)
Descripción
int activeCount( )
Devuelve el número de hilos en el grupo y en todos los
grupos de los que este hilo es padre.
int activeGroupCount( )
Devuelve el número de grupos de los que el hilo que
invoca es padre.
final void checkAccess( )
Hace que el gestor de seguridad verifique que el hilo
que invoca puede acceder y/o cambiar el grupo sobre
el que checkAccess( ) es llamado.
final void destroy( )
Destruye el grupo de hilos (y todos los grupos hijos)
sobre el que se llama.
int enumerate(Thread grupo[ ])
Los hilos que pertenecen al grupo de hilos que invoca
se colocan en el arreglo grupo.
int enumerate(Thread grupo[ ], boolean
todos)
Los hilos que pertenecen al grupo de hilos que invoca
se colocan en el arreglo grupo. Si el parámetro todos
es verdadero, los hilos en todos los subgrupos del hilo
también se incluyen en el arreglo grupo.
int enumerate(ThreadGroup grupo[ ])
Los subgrupos del grupo de hilos que invoca se colocan
en el arreglo grupo.
int enumerate(ThreadGroup grupo[ ],
boolean todos)
Los subgrupos del grupo de hilos que invoca se colocan
en el arreglo grupo. Si el parámetro todos es verdadero,
entonces todos los subgrupos de los subgrupos (y así
sucesivamente) también se incluyen en el arreglo grupo.
final int getMaxPriority( )
Devuelve la prioridad máxima establecida en el grupo.
final String getName( )
Devuelve el nombre del grupo.
final ThreadGroup getParent( )
Devuelve null si el objeto ThreadGroup que invoca
no tiene padre. De lo contrario, devuelve el padre del
objeto que invoca.
final void interrupt( )
Llama al método interrupt( ) de todos los hilos del
grupo.
TABLA 16-18 Los métodos definidos por la clase ThreadGroup
www.detodoprogramacion.com
PARTE II
Método
PARTE II
En ambas formas, nomGrupo indica el nombre del grupo de hilos. La primera versión crea un
nuevo grupo que tiene al hilo actual como padre. En la segunda forma, el padre se especifica en
objPadre. Los métodos definidos por ThreadGroup se muestran en la Tabla 16-18.
Los grupos de hilos ofrecen una forma cómoda de gestionar conjuntos de hilos como
una unidad. Esto es especialmente interesante en situaciones en las que se desea suspender y
reanudar simultáneamente una serie de hilos relacionados. Por ejemplo, imagínese un programa
en que un conjunto de hilos se utiliza para imprimir un documento, otro para mostrar el
documento en pantalla y otro para guardar el documento a un archivo en disco. Si se aborta
la impresión, será muy deseable tener un modo de parar todos los hilos relacionados con la
impresión.
426
Parte II:
La biblioteca de Java
Método
Descripción
final boolean isDaemon( )
Devuelve verdadero si el grupo es un grupo demonio. De lo
contrario, devuelve falso.
boolean isDestroyed( )
Devuelve verdadero si el grupo ha sido destruido. De lo
contrario, devuelve falso.
void list( )
Muestra información del grupo.
final boolean parentOf(ThreadGroup
grupo)
Devuelve verdadero si el hilo que invoca es el padre de
grupo (o el grupo mismo). De lo contrario, devuelve falso.
final void setDaemon(boolean
esDemonio)
Si esDemonio es verdadero, entonces el grupo que invoca
se marca como grupo demonio.
final void setMaxPriority(int prioridad)
Establece la máxima prioridad del grupo que invoca al valor
dado por el parámetro prioridad.
String toString( )
Devuelve la cadena equivalente del grupo.
void uncaughtException(Thread hilo,
Throwable e)
Este método es llamado cuando se produce una excepción y
ésta no ha sido gestionada.
TABLA 16-18 Los métodos definidos por la clase ThreadGroup (continuación)
Los grupos de hilos ofrecen esta posibilidad. El siguiente programa, el cual crea dos grupos de
hilos de dos hilos cada uno, ilustra este uso:
// Uso de grupos de hilos.
class NewThread extends Thread {
boolean suspendBandera;
NewThread(String nomHilo, ThreadGroup tgOb){
super (tgOb, nomHilo);
System.out.println("Nuevo hilo: " + this);
suspendBandera = false;
start(); // Iniciar el hilo
}
// Este es el punto de entrada al hilo.
public void run() {
try {
for (int i = 5; i > 0; i--) {
System.out.println(getName( ) + ": " + i);
Thread.sleep(l000);
synchronized(this) {
while(suspendBandera) {
wait();
}
}
}
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
}
void mysuspend() {
suspendBandera = true;
}
PARTE II
synchronized void myresume() {
suspendBandera = false;
notify();
}
}
class ThreadGroupDemo {
public static void main(String args[]) {
ThreadGroup groupA = new ThreadGroup ("Grupo A");
ThreadGroup groupB = new ThreadGroup ("Grupo B");
NewThread
NewThread
NewThread
NewThread
ob1
ob2
ob3
ob4
=
=
=
=
new
new
new
new
NewThread
NewThread
NewThread
NewThread
PARTE II
} catch (Exception e) {
System.out.println("Excepción en " + getName());
}
System.out.println(getName() + " saliendo.");
427
("Uno", groupA);
("Dos", groupA);
("Tres", groupB);
("Cuatro", groupB);
System.out.println (" \nEsta es la salida de list () : ");
groupA.list();
groupB.list();
System.out.println();
System.out.print1n("Suspendiendo Grupo A");
Thread tga[] = new Thread[groupA.activeCount()];
groupA.enumerate(tga);
// obtener los hilos en el grupo
for(int i = 0; i < tga.length; i++) {
((NewThread)tga[i]).mysuspend(); // suspender cada hilo
}
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
System.out.println("Hilo principal interrumpido.");
}
System.out.println("Reanudando Grupo A");
for(int i = 0; i < tga.length; i++) {
((NewThread)tga[i]).myresume(); // reanudar hilos en el grupo
}
// Esperar a que los hilos terminen
try {
System.out.print1n("Esperando a que los hilos terminen.");
obl.join();
ob2.join();
ob3.join();
ob4.join();
www.detodoprogramacion.com
428
Parte II:
La biblioteca de Java
} catch (Exception e) {
System.out.println("Excepción en el hilo principal");
}
System.out.println("Hilo principal saliendo.");
}
}
Un ejemplo de salida de este programa se muestra a continuación (la salida obtenida por el
usuario puede variar):
Nuevo hilo: Thread[Uno,5,Grupo A]
Nuevo hilo: Thread[Dos,5,Grupo A]
Nuevo hilo: Thread[Tres,5,Grupo B]
Nuevo hilo: Thread[Cuatro,5,Grupo B]
Ésta es la salida de list():
java.lang.ThreadGroup[name = Grupo A, maxpri = l0]
Thread[Uno,5,Grupo A]
Thread[Dos,5,Grupo A]
java.lang.ThreadGroup[name = Grupo B, maxpri=l0]
Thread[Tres,5,Grupo B]
Thread[Cuatro,5,Grupo B]
Suspendiendo Grupo A
Tres: 5
Cuatro: 5
Tres: 4
Cuatro: 4
Tres: 3
Cuatro: 3
Tres: 2
Cuatro: 2
Reanudando Grupo A
Esperando a que los hilos terminen.
Uno: 5
Dos: 5
Tres: 1
Cuatro: 1
Uno: 4
Dos: 4
Tres saliendo.
Cuatro saliendo.
Uno: 3
Dos: 3
Uno: 2
Dos: 2
Uno: 1
Dos: 1
Uno saliendo.
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
429
Dos saliendo.
Hilo principal saliendo.
Java define dos clases adicionales relacionadas con hilos en java.lang:
• ThreadLocal. Se utiliza para crear variables locales de hilo. Cada hilo tendrá su propia
copia de una variable local de hilo.
• InheritableThreadLocal. Crea variables locales de hilo que pueden ser heredadas.
Package
La clase Package encapsula datos de versión asociados a un paquete. La información de versión
de un paquete se está volviendo cada vez más importante por la proliferación de paquetes
y, porque un programa en Java puede necesitar conocer qué versión de cierto paquete está
disponible. Los métodos definidos por la clase Package se muestran en la Tabla 16-19. El
siguiente programa ilustra la clase Package mostrando los paquetes de los que el programa tiene
actualmente conocimiento.
// Ejemplo de la clase Package
class PkgTest {
public static void main(String args[]) {
Package pkgs[];
pkgs = Package.getPackages();
for(int i=0; i < pkgs.length; i++)
System.out.println(
pkgs[i].getName() +" "+
pkgs[i].getImplementationTitle() + " " +
pkgs[i].getImplementationVendor() + " " +
pkgs[i].getlmplementationVersion()
);
}
}
www.detodoprogramacion.com
PARTE II
ThreadLocal e InheritableThreadLocal
PARTE II
Dentro del programa, note que el grupo de hilos A se ha suspendido durante cuatro
segundos. Como la salida confirma, esto provoca la pausa de los hilos Uno y Dos, pero los
hilos Tres y Cuatro continúan corriendo. Tras los cuatro segundos, los hilos Uno y Dos se
reanudan. Obsérvese cómo se suspende y reanuda el grupo de hilos A. Primero se obtienen
los hilos del grupo A llamando al método enumerate( ) sobre el grupo A. Luego, cada hilo
se suspende por iteración a lo largo del arreglo resultante. Para reanudar los hilos del grupo
A, se recorre de nuevo la lista y se reanuda cada hilo. Una última cosa: este ejemplo utiliza el
modo recomendado para suspender y reanudar hilos. No confía en los métodos descartados
suspend( ) y resume( ).
430
Parte II:
La biblioteca de Java
Método
Descripción
A
getAnnotation(Class aTipo)
Devuelve un objeto Annotation que contiene la
anotación asociada con el aTipo para el objeto que
invoca.
Annotation[ ]getAnnotations( )
Devuelve todas las anotaciones asociadas con
el objeto que invoca en un arreglo de objetos
Annotation. Devuelve la referencia a dicho arreglo.
Annotation[ ]getDeclaredAnnotations( )
Devuelve un objeto Annotation para todas las
anotaciones que están declaradas por el objeto
que invoca (las anotaciones heredadas se
ignoran).
String getImplementationTitle( )
Devuelve el título del paquete que invoca.
String getImplementationVendor( )
Devuelve el nombre del implementador del paquete
que invoca.
String getImplementationVersion( )
Devuelve el número de versión del paquete que
invoca.
String getName( )
Devuelve el nombre del paquete que invoca.
static Package getPackage(String nomPaquete)
Devuelve un objeto Package con el nombre
especificado por nomPaquete.
static Package[ ] getPackages( )
Devuelve todos los paquetes de los que
el programa que invoca tiene actualmente
conocimiento.
String getSpecificationTitle( )
Devuelve el título de la especificación del paquete
que invoca.
String getSpecificationVendor( )
Devuelve el nombre del propietario de la
especificación del paquete que invoca.
String getSpecificationVersion( )
Devuelve el número de versión de la especificación
del paquete que invoca.
int hashCode( )
Devuelve el código de dispersión del paquete que
invoca.
boolean isAnnotationPresent(
Class extends Annotation> an)
Devuelve verdadero si la anotación descrita por an
está asociada con el objeto que invoca. Devuelve
falso en caso contario.
boolean isCompatible With(String numVer)
throws NumberFormatException
Devuelve verdadero si numVer es menor o igual al
número de versión del paquete que invoca.
boolean isSealed( )
Devuelve verdadero si el paquete que invoca está
sellado. Devuelve falso en caso contrario.
boolean isSealed(URL url)
Devuelve verdadero si el paquete que invoca está
sellado en relación a url. Devuelve falso en caso
contrario.
String toString( )
Devuelve la cadena equivalente al paquete que
invoca.
TABLA 16-19 Los métodos definidos por la clase Package
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
431
RuntimePermission
PARTE II
La clase RuntimePermission se refiere al mecanismo de seguridad de Java y no lo vamos a
tratar más aquí.
Throwable
SecurityManager
La clase SecurityManager es una clase abstracta que puede ser implementada por subclases
propias para crear un gestor de seguridad. Generalmente no es necesario implementar un gestor
de seguridad propio. Sin embargo, si se desea hacer, es necesario consultar la documentación
que viene con el sistema de desarrollo de Java.
StackTraceElement
La clase StackTraceElement describe un elemento individual de una pila de rastreo cuando
ocurre una excepción. Cada elemento de la pila representa un punto de ejecución, el cual
incluye algunas cosas, tales como, el nombre de la clase, el nombre del método, el nombre de
un archivo y el número de la línea del código fuente. Un arreglo de StackTraceElements se
devuelve mediante el método getStackTrace( ) de la clase Throwable.
La clase StackTraceElement tiene un constructor:
StackTraceElement(String classNom, String metNom, String nomArch, int linea)
Donde, el nombre de la clase se especifica por classNom, el nombre del método se especifica en
metNom, el nombre del archivo en nomArch, y el número de la línea de código se pasa en línea.
Si no hay un número de línea válido, se utiliza un número negativo para línea. Además, un valor
de –2 para el argumento línea, indica que este elemento de la pila de rastreo hace referencia a un
método nativo.
Los métodos soportados por StackTraceElement se muestran en la Tabla 16-20. Estos métodos
proporcionan acceso programático a la pila de rastreo.
Método
Descripción
boolean equals(Object obj)
Devuelve verdadero si el StackTraceElement que invoca es el
mismo que el definido en obj. De otra forma, devuelve falso.
String getClassName( )
Devuelve el nombre de la clase del punto de ejecución descrito
por el objeto StrackTraceElement que invoca.
String getFileName( )
Devuelve el nombre del archivo del punto de ejecución descrito
por el objeto StrackTraceElement que invoca.
TABLA 16-20 Los métodos definidos por la clase StackTraceElement
www.detodoprogramacion.com
PARTE II
La clase Throwable da soporte al sistema de gestión de excepciones de Java, y es la clase de la
que se derivan todas las clases de excepción. Esta clase se examina en el Capítulo 10.
432
Parte II:
La biblioteca de Java
Método
Descripción
int getLineNumber( )
Devuelve el número de la línea del código fuente del punto de
ejecución descrito por el objeto StrackTraceElement que invoca. En
algunos casos, el número de la línea no estará disponible, en tal
caso se devuelve un valor negativo.
String getMethodName( )
Devuelve el nombre del método del punto de ejecución descrito por
el objeto StrackTraceElement que invoca.
int hashCode( )
Devuelve el código de dispersión del objeto StrackTraceElement
que invoca.
boolean isNativeMethod( )
Devuelve verdadero si el objeto StrackTraceElement que invoca
describe a un método nativo. En cualquier otro caso, devuelve falso.
String toString( )
Delvuelve la cadena equivalente a la secuencia que invoca.
TABLA 16-20 Los métodos definidos por la clase StackTraceElement (continuación)
Enum
Tal como se describió en el Capítulo 12, las enumeraciones son una adición reciente al lenguaje
Java (recuerde que las enumeraciones son creadas utilizando la palabra clave enum). Todas las
enumeraciones automáticamente heredan de Enum. Enum es una clase genérica que se declara
como se muestra a continuación:
class Enum >
Donde, E representa el tipo de la enumeración. Enum no tiene constructores públicos.
Enum define varios métodos que están disponibles para ser utilizados en todas las
enumeraciones. Estos métodos se describen en la Tabla 16-21.
Método
Descripción
protected final Object clone( )
Invocar a este método causa que se lance una excepción
throws CloneNotSupportedException CloneNotSupportedException. Esto previene que las
enumeraciones sean clonadas.
final int compareTo(E e)
Compara el valor ordinal de dos constantes de la misma
enumeración. Devuelve un valor negativo si la constante
invocada tiene un valor ordinal menor que el valor ordinal de
e, cero si los dos valores ordinales son iguales, y un valor
positivo si la constante que invoca tiene un valor ordinal
mayor que el valor ordinal de e.
final boolean equals (Object obj)
Devuelve verdadero si obj y el objeto que invoca se refieren a
la misma constante.
final Class getDeclaringClass( )
Devuelve el tipo de enumeración de la cual la constante que
invoca es miembro.
final int hashCode( )
Devuelve el código de dispersión para el objeto que invoca.
TABLA 16-21 Los métodos definidos por la clase Enum
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
final String name( )
Devuelve el nombre sin cambio de la constante que invoca.
final int ordinal( )
Devuelve un valor que indica la posición de la constante
enumerada en la lista de constantes.
String toString( )
Devuelve el nombre de la constante que invoca. Este
nombre podría diferir de la utilizada en la declaración de la
enumeración.
static > T
valueOf(Class tipo, String nom)
Devuelve la constante asociada con nom en la enumeración
de tipo especificado por tipo.
TABLA 16-21 Los métodos definidos por la clase Enum (continuación)
La interfaz CharSequence
La interfaz CharSequence define métodos que garantizan acceso de sólo lectura a una
secuencia de caracteres. Estos métodos se muestran en la Tabla 16-22. Esta interfaz está
implementada por String, StringBuffer y StringBuilder. También es implementada por
CharBuffer, que forma parte del paquete java.nio (descrito posteriormente en este libro).
La interfaz Comparable
Los objetos de clases que implementan a la interfaz Comparable se pueden ordenar. En otras
palabras, las clases que implementan a la interfaz Comparable definen objetos que se pueden
comparar de alguna manera que tenga sentido. La interfaz Comparable es genérica y se
declara como sigue:
interface Comparable
Donde T representa el tipo de objetos que se está comparando.
La interfaz Comparable declara un método que se utiliza para determinar lo que Java llama
el orden natural de instancias de una clase. La firma del método se muestra aquí:
int compareTo(T obj)
Método
Descripción
char charAt(int idx)
Devuelve el carácter en el índice especificado por idx.
int length( )
Devuelve el número de caracteres en la secuencia que
invoca.
CharSequence
subSequence(int inicioidx, int finidx)
Devuelve un subconjunto de la secuencia que invoca
comenzando en inicioidx y terminando en finidx–1.
String toString( )
Devuelve la cadena equivalente a la secuencia que invoca.
TABLA 16-22 Los métodos definidos por la clase CharSequence
www.detodoprogramacion.com
PARTE II
Descripción
PARTE II
Método
433
434
Parte II:
La biblioteca de Java
Este método compara el objeto que invoca con obj. Devuelve 0 si los valores son iguales, un valor
negativo si el objeto que invoca tiene un valor menor. De lo contrario, devuelve un valor positivo.
Esta interfaz está implementada por varias de las clases ya vistas en este libro.
Específicamente, las clases Byte, Character, Double, Float, Long, Short, String e Integer
definen un método compareTo( ). Además, como explica el próximo capítulo, los objetos que
implementan esta interfaz se pueden utilizar en diferentes colecciones.
La interfaz Appendable
Los objetos de una clase que implementan la interfaz Appeandable pueden tener un carácter
o secuencias de caracteres adjuntos. La interfaz Appeandable define estos tres métodos:
Appeandable append(char ch) throws IOException
Appeandable append(CharSequence chars) throws IOException
Appeandable append(CharSequence chars, int comienzo, int final) throws IOException
En la primer forma, el carácter ch es añadido al objeto que invoca. En la segunda forma, la
secuencia de caracteres chars se añade al objeto que invoca. La tercera forma permite indicar una
porción (especificada por inicio y final) de la secuencia especificada por chars. En todos los casos
se devuelve una referencia al objeto que invoca.
La interfaz Iterable
La interfaz Iterable debe ser implementada por cualquier clase cuyos objetos vayan a ser
utilizados en un ciclo estilo for-each. En otras palabras, para que un objeto sea utilizado dentro
de un ciclo estilo for-each, su clase debe implementar a la interfaz Iterable. La interfaz Iterable
es una interfaz genérica que tiene esta declaración:
interface Iterable
Donde, T es el tipo del objeto que será iterado. La interfaz define un método, iterator( ), el cuál
se muestra a continuación:
Iteratoriterator( )
Este método devuelve un iterador para los elementos contenidos en el objeto que invoca.
NOTA Los iteradores se describen a detalle en el Capítulo 17.
La interfaz Readable
La interfaz Readable indica que un objeto puede ser utilizado como una fuente de caracteres.
Define un método llamado read( ), el cual se muestra aquí:
int read(CharBuffer buf) throws IOException
www.detodoprogramacion.com
Capítulo 16:
Explorando java.lang
435
Este método lee caracteres dentro de buf. Devuelve el número de caracteres leídos, o –1 si
encuentra EOF.
PARTE II
Los subpaquetes de java.lang
Java define varios subpaquetes:
java.lang.annotation
java.lang.instrument
java.lang.management
java.lang.ref
java.lang.reflect
PARTE II
•
•
•
•
•
A continuación una breve descripción de cada uno:
java.lang.annotation
La característica de las anotaciones de Java está soportada por java.lang.annotation, que define la
interfaz Annotation, y las enumeraciones ElementType y RetentionPolicy. Las anotaciones se
describen en el Capítulo 12.
java.lang.instrument
java.lang.instrument define características que pueden ser utilizadas para agregar
instrumentación a varios aspectos de la ejecución de un programa. Define las interfaces
Instrumentation y ClassFileTransformer y la clase ClassDefinition.
java.lang.management
El paquete java.lang.management provee soporte para la administración de la JVM y el
ambiente de ejecución. Utilizando las características en java.lang.management, se pueden
observar y administrar varios aspectos de un programa en ejecución.
java.lang.ref
Ya hemos visto que las facilidades para la recolección de basura proporcionadas por Java
determinan cuándo no existen referencias a un objeto. Se supone entonces que el objeto no se
volverá a necesitar y su memoria se recicla. Las clases en el paquete java.lang.ref, facilitan un
control más flexible sobre el proceso de recolección de basura. Por ejemplo, supóngase que
un programa ha creado numerosos objetos que se querrán volver a utilizar más adelante. Se
puede seguir manteniendo referencias a esos objetos, pero eso puede requerir mucha memoria.
En lugar de eso, se pueden definir referencias “suaves” a esos objetos. Un objeto que es
“accesible suavemente” puede ser reciclado por el recolector de basura si baja la memoria
disponible. En ese caso, el recolector de basura asigna null a las referencias “suaves” a ese
objeto. En caso contrario, el recolector de basura guarda al objeto para su posible uso futuro.
Un programador tiene la posibilidad de determinar si un objeto “suavemente accesible” ha
sido reciclado o no. Si ha sido reciclado, se puede volver a crear. De lo contrario, el objeto sigue
www.detodoprogramacion.com
436
Parte II:
La biblioteca de Java
disponible para su reutilización. También se pueden crear referencias “débiles” y “fantasmas”
a objetos. El estudio de estas y otras características del paquete java.lang.ref queda fuera del
ámbito de este libro.
java.lang.reflect
La reflexión es la capacidad de un programa para analizarse a sí mismo. El paquete java.lang.
reflect proporciona la capacidad de obtener información sobre los campos, constructores,
métodos y modificadores de una clase. Entre otras cosas, se necesita esta información para
construir herramientas de software que permitan trabajar con componentes de Java Beans.
Las herramientas utilizan la reflexión para determinar dinámicamente las características de
un componente. La reflexión fue introducida en el Capítulo 12 y es también examinado en el
Capítulo 27.
El paquete java.lang.reflect define varias clases, incluyendo Method, Field y Constructor.
También define varias interfaces, incluyendo AnnotatedElement, Member y Type.
Adicionalmente, el paquete java.lang.reflect incluye una clase Array que permite la creación y
acceso a arreglos dinámicamente.
www.detodoprogramacion.com
17
CAPÍTULO
java.util parte 1:
colecciones
E
n este capítulo inicia nuestro análisis del paquete java.util. Este importante paquete contiene
una gran variedad de clases e interfaces que proporcionan diversas funcionalidades. Por
ejemplo, java.util tiene clases para generar números pseudo aleatorios, gestionar fechas
y horas, observar eventos, manipular conjuntos de bits, separar cadenas en partes y manipular el
formato de datos. El paquete java.util contiene además uno de los subsistemas más poderosos de
Java: la estructura de colecciones. La estructura de colecciones es una sofisticada jerarquía de interfaces
y clases que proporcionan la tecnología necesaria para la administración de grupos de objetos. Esto
amerita un estudio cuidadoso por parte de cualquier programador.
Debido a la gran cantidad de funcionalidades que proporciona java.util este paquete es
considerablemente grande. Ésta es una lista de las clases disponibles en el paquete java.util:
AbstractCollection
EventObject
Random
AbstractList
FormattableFlags
ResourceBundle
AbstractMap
Formatter
Scanner
ServiceLoader (añadida en Java SE 6)
AbstractQueue
GregorianCalendar
AbstractSequentialList
HashMap
SimpleTimeZone
AbstractSet
HashSet
Stack
AbstractDequeue (añadida en Java SE 6)
Hashtable
StringTokenizer
ArrayList
IdentityHashMap
Timer
Arrays
LinkedHashMap
TimerTask
BitSet
LinkedHashSet
TimeZone
Calendar
LinkedList
TreeMap
Collections
ListResourceBundle
TreeSet
Currency
Locale
UUID
Date
Observable
Vector
WeakHashMap
Dictionary
PriorityQueue
EnumMap
Properties
EnumSet
PropertyPermission
EventListenerProxy
PropertyResourceBundle
437
www.detodoprogramacion.com
438
Parte II:
La biblioteca de Java
java.util define las siguientes interfaces:
Collection
Comparator
Dequeue (añadida en Java SE 6)
Enumeration
EventListener
Formattable
Iterator
List
ListIterator
Map
Map.Entry
NavigableMap (añadida en Java SE 6)
NavigableSet (añadida en Java SE 6)
Observer
Queue
RandomAccess
Set
SortedMap
SortedSet
Este capítulo examina los miembros de java.util que son parte de la estructura de
colecciones y el Capítulo 18 analiza sus otras clases e interfaces.
Introducción a las colecciones
La estructura de colecciones de Java estandariza el modo en que los programas trabajan con
grupos de objetos. Las colecciones no formaban parte de la versión original de Java, fueron
añadidas en J2SE 1.2. Anterior a la existencia de la estructura de colecciones, Java proporcionaba
clases ad hoc, como Dictionary, Vector, Stack y Properties para almacenar y manipular grupos
de objetos. Aunque estas clases eran bastante útiles, les faltaba un contexto central que las
unificara. Así por ejemplo, el modo de usar Vector era distinto del modo de usar Properties.
Esta aproximación anterior no estaba diseñada para ser fácilmente extensible o adaptable. Las
colecciones son una respuesta a este (y otros) problemas.
La estructura de colecciones se diseñó para conseguir diferentes objetivos. Primero, debía
proporcionar un alto rendimiento. Las implementaciones de las colecciones fundamentales
(arreglos dinámicos, listas enlazadas, árboles y tablas de dispersión) son altamente eficientes.
Rara vez o nunca es necesario escribir manualmente código para alguna de estas “máquinas de
datos”. Segundo, la estructura de colecciones debía permitir que diferentes tipos de colecciones
trabajaran en forma similar y con un alto grado de interoperabilidad. Tercero, debía ser fácil
extender y/o adaptar una colección. Para este fin, toda la estructura de colecciones se ha
diseñado alrededor de un conjunto de interfaces estándar y varias implementaciones estándar
de estas interfaces (como LinkedList, HashSet y TreeSet), Se proporcionan listas para usarse.
También se puede implementar una colección propia, si se desea. Varias implementaciones de
propósito especial han sido creadas para mayor comodidad del programador, y algunas
implementaciones parciales se proporcionan para facilitar la creación de colecciones propias.
Finalmente, se han añadido mecanismos que permiten la integración de arreglos estándar
dentro de la estructura de colecciones.
Los algoritmos son otra parte importante del mecanismo de colecciones. Los algoritmos
operan sobre colecciones, se definen como métodos estáticos dentro de la clase Collections y
están disponibles para todas las colecciones. No es necesario que cada clase en la estructura de
colecciones implemente versiones propias. Los algoritmos aportan medios estándares para la
manipulación de colecciones.
Otro elemento asociado con la estructura de colecciones es la interfaz Iterator. Un iterador
proporciona un medio de propósito general y estandarizado para acceder a los elementos
dentro de una colección, uno por uno. Así, un iterador proporciona un medio para enumerar los
contenidos de una colección. Debido a que toda colección implementa la interfaz Iterator,
los elementos de cualquier clase de la estructura de colecciones son accesibles mediante los
métodos definidos por Iterator. Así, www.detodoprogramacion.com
por ejemplo, con sólo unos pequeños cambios, el código
Capítulo 17:
java.util parte 1: colecciones
439
NOTA
Para los lectores familiarizados con C++ les ayudará saber que la tecnología de colecciones
de Java es parecida en su espíritu a la tecnología de la Biblioteca de Plantillas Estándar (STL por
sus siglas en inglés) definida por C++. Lo que C++ llama contenedor, Java lo llama colección. Sin
embargo existen diferencias importantes entre la estructura de colecciones de Java y STL.
Cambios recientes en las colecciones
Recientemente, la estructura de colecciones sufrió cambios fundamentales que incrementaron significativamente su poder y modernizaron su uso. Los cambios fueron ocasionados por la adición,
en el JDK 5, de tipos parametrizados, autoboxing/unboxing y ciclos estilo for-each. Aunque revisaremos estos elementos a lo largo de este capítulo, veremos una breve introducción a continuación.
Los tipos parametrizados se aplican a las colecciones
La adición de tipos parametrizados causó un cambio significativo a la estructura de colecciones.
Todas las colecciones soportan en la actualidad tipos parametrizados y muchos de los métodos
que trabajan con colecciones utilizan parámetros con tipos parametrizados. Agregar tipos
parametrizados afectó todas las partes de la estructura de colecciones.
Los tipos parametrizados agregaron una cualidad hasta ese momento ausente de
las colecciones: seguridad en el manejo de tipos. Antes de los tipos parametrizados todas las
colecciones almacenaban referencias a instancias de la clase Object, lo cual significa que
cualquier colección podía almacenar cualquier tipo de objeto. Por ello, era posible de manera
accidental almacenar elementos de tipos no compatibles en una colección. Lo cual a su vez
podría causar un error, en tiempo de ejecución, de incompatibilidad de tipos. Utilizando tipos
parametrizados es posible declarar explícitamente el tipo de dato a ser almacenado y por ende
eliminar los errores de incompatibilidad de tipos.
Aunque anexar tipos parametrizados cambió la declaración de la mayoría de las clases
e interfaces, así como de una gran cantidad de métodos, en su conjunto la estructura de
colecciones continua funcionando de la misma forma que antes. No obstante, si el lector está
familiarizado con la versión previa de la estructura de colecciones (sin tipos parametrizados)
quizá encuentre la nueva sintaxis un poco intimidatoria. No debemos preocuparnos, con el
tiempo nos acostumbraremos a la sintaxis de tipos parametrizados.
Finalmente debemos mencionar que para aprovechar las ventajas que los tipos parametrizados
aportan a las colecciones, será necesario reescribir código que programas viejos. Esto es muy
importante debido a que el códigowww.detodoprogramacion.com
sin tipos parametrizados generará mensajes de advertencia
PARTE II
para hacer un ciclo por los elementos de un conjunto también se puede utilizar para hacer un
ciclo por los elementos de una lista.
Además de las colecciones, la estructura define varias interfaces de mapeo y clases. Los mapas
almacenan pares clave/valor. Aunque los mapas no son “colecciones” propiamente dichas, están
totalmente integrados a la estructura de colecciones. Es posible obtener una vista como colección
de un mapa. Esta vista contiene los elementos del mapa almacenados en una colección. Así se
pueden procesar los contenidos de un mapa como si fuera una colección si se desea.
El mecanismo de colecciones se ha adaptado a algunas de las clases originalmente definidas
por java.util para que también se pudieran integrar en el nuevo sistema. Es importante entender
que aunque la adición de colecciones alteró la arquitectura de muchas de las clases de utilería, no
causó el descarte de ninguna. Las colecciones simplemente proporcionan una mejor manera de
hacer varias cosas.
440
Parte II:
La biblioteca de Java
cuando sea compilado con una versión actual del compilador de Java. Para eliminar estos
mensajes será necesario añadir parametrización de tipos a todo código relacionado con el
manejo de colecciones.
El autoboxing facilita el uso de tipos primitivos
El uso de autoboxing y unboxing facilita el almacenamiento de tipos primitivos en colecciones.
Como se verá más adelante una colección sólo puede almacenar referencias, no tipos primitivos.
En el pasado, si se deseaba almacenar un tipo primitivo, como un entero, en una colección, era
necesario manualmente encerrar el valor primitivo en un objeto que lo envolviera.
Cuando se requería utilizar el valor era necesario manualmente sacarlo del objeto que
lo envolvía. Con el uso de autoboxing y unboxing, Java puede realizar automáticamente la
envoltura y desenvoltura necesarias cuando se almacenan y recuperan tipos primitivos, ya no es
necesario realizar manualmente estas operaciones.
El ciclo estilo for-each
Otra de las mejoras aplicables a todas las clases que representan colecciones en la estructura de
colecciones es que ahora implementan la interfaz Iterable, lo cual significa que todas pueden
ser recorridas linealmente utilizando un ciclo estilo for-each. En el pasado, recorrer linealmente
una colección requería del uso de un iterador (los iteradotes se describen más adelante en este
capítulo), con el cual el programador construía manualmente un ciclo. Aunque los iteradores aún
son necesarios para algunos propósitos, en muchos casos, los ciclos basados en iteradores pueden
ser reemplazados por ciclos for.
Las interfaces de la estructura de colecciones
La estructura de colecciones define diversas interfaces. Esta sección da una visión general de
cada una de ellas. Es necesario comenzar con las interfaces de colección porque determinan
la naturaleza fundamental de las clases de colección. Dicho de otro modo, las clases concretas
simplemente proporcionan diferentes implementaciones de las interfaces estándar. Las interfaces
que son base fundamental de las colecciones se resumen en la siguiente tabla:
Interfaz
Collection
Dequeue
List
NavigableSet
Queue
Set
SortedSet
Descripción
Permite trabajar con grupos de objetos; está en la cima de la jerarquía de colecciones.
Extiende a la interfaz Queue para manejar una fila doble. (Añadida en Java SE 6).
Extiende la interfaz Collection para manejar secuencias (listas de objetos).
Extiende la interfaz SortedSet para manejar recuperación de información con base a
búsquedas de coincidencias próximas (Añadido por Java SE 6).
Extiende la interfaz Collection para manejar un tipo especial de lista donde los elementos
son eliminados sólo desde el inicio de la lista.
Extiende la interfaz Collection para manejar conjuntos, los cuales deben contener
elementos únicos.
Extiende la interfaz Set para manejar conjuntos ordenados.
Además de las interfaces de colección, las colecciones también utilizan las interfaces
Comparator, RandomAccess, Iterator y ListIterator, que se describen en profundidad más
adelante en este capítulo. Brevemente, Comparator define cómo se comparan dos objetos;
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
La interfaz collection
La interfaz Collection es la base sobre la que se construye la estructura de colecciones. Toda
clase que defina una colección debe implementar esta interfaz. La interfaz Collection es una
interfaz genérica declarada como:
interface Collection
Aquí E especifica el tipo de objeto que la colección almacenará. La interfaz Collection extiende
a la interfaz Iterable. Esto significa que todas las colecciones pueden ser recorridas utilizando un
ciclo estilo for-each. Recuerde que sólo las clases que implementan la interfaz Iterable pueden
ser recorridas con un ciclo for.
La interfaz Collection declara los métodos medulares que tendrán todas las colecciones.
Estos métodos se resumen en la Tabla 17-1. Dado que todas las colecciones implementan la
interfaz Collection, es necesaria cierta familiaridad con sus métodos para comprender claramente
la Estructura de Colecciones de Java. Varios de estos métodos pueden producir una excepción de
tipo UnsupportedOperationException. Como se ha explicado, esto ocurre si una colección no
puede ser modificada. Una excepción de tipo ClassCastException se genera cuando un objeto
es incompatible con otro, como por ejemplo cuando se intenta añadir a una colección un objeto
incompatible. Una excepción de tipo NullPointerException se genera si se intenta almacenar un
objeto null en una colección que no soporte almacenar elementos con este valor. Una excepción
de tipo IllegalStateException se genera cuando se intenta agregar un elemento a una colección
de tamaño predefinido y ésta se encuentra llena.
Los objetos se añaden a una colección llamando al método add( ). Nótese que el método
add( ) toma un argumento de tipo E, lo que significa que los objetos que se añadan a la
colección deben ser compatibles con el tipo de dato esperado por la colección. Es posible añadir
todos los elementos contenidos en una colección a otra llamando al método addAll( ).
Se puede quitar un objeto de la colección utilizando el método remove( ). Para quitar
un grupo de objetos se llama al método removeAll( ). Es posible quitar todos los elementos
excepto los de un grupo específico llamando al método retainAll( ). Para vaciar una colección se
utiliza el método clear( ).
Se puede determinar si una colección contiene un objeto específico llamando al método
contains( ). Para determinar si una colección contiene todos los elementos de otra, se utiliza el
método containsAll( ). Se puede saber si una colección está vacía llamando al método is
Empty( ). El número de elementos contenidos actualmente en una colección se obtiene
llamando al método size( ).
Los métodos toArray( ) devuelve un arreglo que contiene los elementos almacenados en la
colección que invoca. El primero regresa un arreglo de objetos tipo Object. El segundo regresa
www.detodoprogramacion.com
PARTE II
Iterator y ListIterator enumeran los objetos dentro de una colección. Implementando
RandomAccess, una lista indica que soporta el acceso aleatorio a sus elementos.
Para proporcionar la máxima flexibilidad en su uso, las interfaces de colección permiten que
algunos métodos sean opcionales. Los métodos opcionales permiten modificar los contenidos
de una colección. Las colecciones que soportan estos métodos se llaman modificables. Las
que no permiten que sus contenidos cambien se llaman no modificables. Si se intenta utilizar
uno de esos métodos en una colección no modificable, se produce una excepción del tipo
UnsupportedOperationException. Todas las colecciones incorporadas en la estructura de
colecciones son modificables.
Las siguientes secciones examinan las interfaces de colección.
441
442
Parte II:
La biblioteca de Java
un arreglo de elementos que tienen el mismo tipo que el arreglo especificado como parámetro.
Normalmente esta segunda forma es más apropiada debido a que regresa el arreglo con el
tipo deseado. Estos métodos son más importantes de lo que podría parecer a primera vista. A
menudo, procesar el contenido de una colección utilizando la sintaxis del manejo de arreglos es
benéfico. Proporcionando un camino de conexión entre las colecciones y los arreglos, se puede
tener lo mejor de ambos mundos.
Se puede comparar si dos colecciones son iguales llamando al método equals( ). El
significado preciso de “igualdad” puede diferir entre colecciones. Por ejemplo, se puede
implementar equals( ) de modo que compare los valores de los elementos almacenados en una
colección. Alternativamente, equals( ) puede comparar las referencias a esos elementos.
Otro método muy importante es iterator( ), el cual devuelve un iterador a una colección.
Los iteradores son utilizados frecuentemente cuando se trabaja con colecciones.
La interfaz List
La interfaz List extiende la interfaz Collection y declara el comportamiento de una colección
que almacena una sucesión de elementos. Se puede insertar o acceder a elementos por su
posición en la lista, utilizando una indexación con inicio en cero.
Método
boolean add(E obj)
Descripción
Añade obj a la colección que invoca. Devuelve true si obj fue añadido a la
colección. Devuelve false si obj ya es un miembro de la colección y la colección no admite duplicados.
boolean addAll(Collection
extends E> c)
Añade todos los elementos de c a la colección que invoca. Devuelve true
si la operación tuvo éxito (esto es, los elementos fueron añadidos). De lo
contrario, devuelve false.
void clear( )
Quita todos los elementos de la colección que invoca.
boolean contains(Object obj)
Devuelve true si obj es un elemento de la colección que invoca. De lo
contrario, devuelve false.
boolean containsAll(Collection Devuelve true si la colección que invoca contiene todos los elementos de
> c)
c. De lo contrario, devuelve false.
boolean equals(Object obj)
Devuelve true si la colección que invoca y obj son iguales. De lo contrario,
devuelve false.
int hashCode( )
Devuelve el código de dispersión de la colección que invoca.
boolean isEmpty( )
Devuelve true si la colección que invoca está vacía. De lo contrario,
devuelve false.
Iterator iterator( )
Devuelve un iterador para la colección que invoca.
boolean remove(Object obj)
Quita una instancia de obj de la colección que invoca. Devuelve true si el
elemento fue quitado. De lo contrario, devuelve false.
boolean removeAll(Collection Quita de la colección que invoca todos los elementos de c. Devuelve true
> c)
si la colección ha cambiado (esto es, si se han quitado elementos). De lo
contrario, devuelve false.
int size( )
Devuelve el número de elementos contenidos en la colección que invoca.
Object[ ] toArray( )
Devuelve un arreglo que contiene todos los elementos almacenados en
la colección que invoca. Los elementos del arreglo son copias de los
elementos de la colección.
TABLA 17-1
Los métodos definidos por Collection
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
Descripción
T[ ] toArray (T a [ ])
Devuelve un arreglo que contiene los elementos de la colección que
invoca. Los elementos del arreglo son copias de los elementos de la
colección. Si el tamaño de a es igual al número de elementos, estos se
devuelven en a. Si el tamaño de a es menor que el número de elementos,
se asigna memoria para un nuevo arreglo del tamaño necesario y ese
nuevo arreglo es devuelto. Si el tamaño de a es mayor que el número de
elementos, al elemento de a siguiente al último elemento de la colección
se le asigna null. Si algún elemento de la colección es de un tipo que no
es un subtipo de a se produce una excepción de tipo ArrayStoreException.
TABLA 17-1 Los métodos definidos por Collection (continuación)
Una lista puede contener elementos duplicados. List es una interfaz genérica declarada como:
interface List
Donde E especifica el tipo de los objetos que la lista debe contener.
Además de los métodos definidos por Collection, List define algunos propios, los cuales se
resumen en la Tabla 17-2. Nótese de nuevo que varios de estos métodos producirán una excepción
de tipo UnsupportedOperationException si la lista no se puede modificar, y una excepción
ClassCastException cuando un objeto es incompatible con otro, como cuando se intente añadir
un objeto incompatible a la lista. Varios métodos generan una excepción de tipo IndexOutOf
Método
Descripción
void add(int indice, E obj)
Inserta obj en la lista que invoca en la posición correspondiente al índice pasado en indice. Los elementos preexistentes en esa posición y más allá del punto
de inserción se corren hacia arriba. Así no se sobrescribe ningún elemento.
Inserta todos los elementos de c en la lista que invoca en el índice
boolean addAll(int indice,
Collection especificado. Los elementos preexistentes en la posición dada por indice y
Extends E> c) más allá se desplazan hacia arriba para hacer sitio a los nuevos elementos
sin sobrescribir ningún elemento. Devuelve true si la lista que invoca cambia,
y false, si no.
E get(int indice)
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.
ListIterator listIterator( )
Devuelve un iterador al inicio de la lista que invoca.
ListIterator
listIterator(int indice)
Devuelve un iterador a la lista que invoca que comienza en el índice
especificado.
E remove(int indice)
Quita de la lista que invoca el elemento en la posición dada por indice y
devuelve el elemento borrado. La lista resultante es compactada. Esto es,
los índices de los elementos subsiguientes disminuyen en uno.
E set(int indice, E obj)
Asigna obj a la posición especificada por indice dentro de la lista que invoca.
List subList(int inicio,
int fin)
Devuelve una lista que incluye los elementos desde inicio hasta fin-1 en la lista
que invoca. Los elementos en la lista devuelta también son referenciados por el
objeto que invoca.
TABLA 17-2 Los métodos definidos por List
www.detodoprogramacion.com
PARTE II
Método
443
444
Parte II:
La biblioteca de Java
BoundException si se utiliza con ellos un índice inválido. Una excepción de tipo NullPointer
Exception se genera si se intenta almacenar el un objeto null y elementos de valor null no
están permitidos en la lista. Una excepción de tipo IllegalArgumentException se genera si se
proporciona un argumento no válido.
A las versiones de add( ) y addAll( ) definidas por Collection, List añade los métodos
add(int,E) y addAll(int,Collection). Estos métodos insertan elementos en la posición
especificada. Además, las semánticas de los métodos add(E) y addAll(Collection) definidos en
Collection son cambiadas por List para que añadan los elementos al final de la lista.
Para obtener el objeto almacenado en una posición específica, se llama a get( ) con el índice
del objeto. Para asignar un valor a un elemento de la lista, se llama a set( ), especificando el
índice del objeto que se ha de cambiar. Para encontrar el índice de un objeto, se utiliza indexOf( )
o lastIndexOf( ).
Se puede obtener una sublista de una lista llamando al método subList( ), especificando los
índices inicial y final de la sublista.
La interfaz Set
La interfaz Set define un conjunto. Extiende la interfaz Collection y declara el comportamiento
de una colección que no permite elementos duplicados. Por tanto, el método add( ) devuelve
false si se intenta añadir elementos duplicados en el conjunto.
No define ningún método adicional propio. Set es una interfaz genérica declarada como:
interface Set
Donde, E especifica el tipo de objetos que el conjunto almacenará
La interfaz SortedSet
La interfaz SortedSet extiende a la interfaz Set y declara el comportamiento de un conjunto
ordenado en orden ascendiente. SortedSet es una interfaz genérica declarada como:
Interface SortedSet
Donde, E especifica el tipo de objetos que el conjunto almacenará.
Además de los métodos definidos por Set, la interfaz SortedSet declara los métodos
mostrados en la Tabla 17-3. Varios métodos producen una excepción NoSuchElementException
cuando no hay ningún elemento en el conjunto que invoca. Se produce una excepción
ClassCastException cuando un objeto es incompatible con los elementos de un conjunto. Se
produce una excepción NullPointerException si se intenta utilizar un objeto null y null no está
permitido en el conjunto. Se genera una excepción IllegalArgumentException si un argumento
no permitido es dado a un método.
SortedSet define varios métodos que hacen el procesado de conjuntos más cómodo. Para
obtener el primer objeto en el conjunto, se llama al método first( ). Para obtener el último
elemento se llama al método last( ). Para obtener un subconjunto de un SortedSet se llama
al método subSet( ). Para obtener el subconjunto que comienza con el primer elemento del
conjunto, se utiliza el método headSet( ). Si se quiere obtener el subconjunto que termina
el conjunto, se utiliza el método tailSet( ).
La interfaz NavigableSet
La interfaz NavigableSet fue añadida en Java SE 6. Esta interfaz extiende SortedSet y declara
el comportamiento de una colecciónwww.detodoprogramacion.com
que soporta la recuperación de elementos basada en la
Capítulo 17:
java.util parte 1: colecciones
interface NavigableSet
Donde, E especifica el tipo de los objetos que el conjunto almacenará. Además a los métodos
heredados de SortedSet, la interfaz NavigableSet añade los métodos listados en la Tabla 17-4.
Método
Descripción
Comparator super E>
comparator( )
Devuelve el comparador del conjunto ordenado que invoca. Si para este
conjunto se utiliza la ordenación natural, se devuelve null.
E first( )
Devuelve el primer elemento en el conjunto ordenado que invoca.
SortedSet headSet(E fin)
Devuelve un SortedSet que contiene los elementos menores que el
valor del parametro fin contenidos en el conjunto ordenado que invoca.
Los elementos en el conjunto ordenado devuelto también son referenciados por el conjunto ordenado que invoca.
E last( )
Devuelve el último elemento en el conjunto ordenado que invoca.
SortedSet subSet(E inicio,
E fin)
Devuelve un SortedSet que incluye los elementos entre inicio y fin-l. Los
elementos en la colección devuelta también son referenciados por el
objeto que invoca.
SortedSet tailSet(E inicio)
Devuelve un SortedSet que contiene los elementos mayores o iguales
que inicio contenidos en el conjunto ordenado. Los elementos en el
conjunto devuelto también son referenciados por el objeto que invoca.
E ceiling (E obj)
Busca en el conjunto al elemento e más pequeño tal que e >=obj. Si
encuentra un elemento con estas características lo devuelve en caso
contrario devuelve null.
TABLA 17-3 Los métodos definidos por SortedSet
Método
Descripción
Iterator descendingIterator( ) Devuelve un iterador que realiza un recorrido del elemento más grande al
más pequeño. En otras palabras, este método devuelve un iterador inverso.
NavigableSet
Devuelve un NavigableSet que es el inverso del objeto que invoca. El
descendingSet( )
conjunto resultante es una referencia al conjunto que invoca.
E floor (E obj)
Busca en el conjunto al elemento e más grande tal que e<=obj. Si
encuentra un elemento con estas características lo devuelve en caso
contrario devuelve null.
Devuelve un NavigableSet que incluye todos los elementos del conjunto
NavigableSet
que invoca que son menores que upperBound. Si i es true entonces un
headSet(E upperBound,
elemento igual a upperBound también se incluiría en el resultado. El
boolean i)
conjunto resultante es una referencia al conjunto que invoca.
E higher(E obj)
Busca en el conjunto el elemento e más grande tal que e>obj. Si
encuentra un elemento con estas características lo devuelve en caso
contrario devuelve null.
E lower(E obj)
Busca en el conjunto el elemento e más grande tal que e
subSet(E
lowerBound,
boolean low,
E upperBound,
boolean high)
NavigableSet
tailSet (E lowerBound,
boolean i)
Descripción
Devuelve el primer elemento y lo elimina del conjunto que invoca. Debido
a que el conjunto está ordenado, el elemento eliminado es el elemento
con el valor menor. Devuelve null si el conjunto está vacío.
Devuelve el último elemento y lo elimina del conjunto que invoca. Debido
a que el conjunto está ordenado, el elemento eliminado es el elemento
con el valor mayor. Devuelve null si el conjunto está vacío.
Devuelve un NavigableSet que incluye todos los elementos del conjunto
que invoca que son mayores que lowerBound y menores que upperBound.
Si low es true, entonces un elemento igual a lowerBound sería incluido en
el resultado. Si high es true entonces un elemento igual a upperBound
sería incluido en el resultado. El conjunto resultante es una referencia al
conjunto que invoca.
Devuelve un NavigableSet que incluye todos los elementos del conjunto
que invoca que son mayores a lowerBound. Si i es true, entonces un
elemento igual a lowerBound sería incluido en el resultado. El conjunto
resultante es una referencia al conjunto que invoca.
TABLA 17-4 Los métodos definidos por NavigableSet (continuación)
Una excepción de tipo ClassCastException se genera cuando un objeto no es compatible con
los elementos en el conjunto. Una excepción de tipo NullPointerException es generada si se
intenta utilizar un objeto null y null no está permitido en el conjunto. Una excepción de tipo
IllegalArgumentException se genera si un argumento inválido es utilizado.
La interfaz Queue
La interfaz Queue extiende de la interfaz Collection y declara el comportamiento de una fila, la
cual es a menudo una lista que implementa un comportamiento de primero en entrar – primero
en salir. Sin embargo, existen tipos de filas donde el orden es un criterio adicional a considerar al
momento de insertar y borrar elementos. Queue es una interfaz genérica declarada como;
interface Queue
Método
Descripción
E element( )
Devuelve el elemento al inicio de la fila. El elemento no es removido de la fila. Se
genera una excepción de tipo NoSuchElementException si la fila está vacía.
Boolean offer(E obj) Intenta añadir obj a la fila. Devuelve true si obj fue añadido con éxito a la fila y false
en caso contrario.
E peek( )
Devuelve el elemento al inicio de la fila. Devuelve null si la fila está vacía. El
elemento no es removido.
E remove( )
Remueve el elemento al inicio de la fila y lo devuelve. Genera una excepción de tipo
NoSuchElementException si la fila está vacía.
TABLA 17-5 Métodos definidos por Queue
Donde, E especifica el tipo de objetos que la fila contendrá. Los métodos definidos por Queue se
muestran en la Tabla 17-5.
Varios métodos generan una excepción de tipo ClassCastException cuando un objeto no
es compatible con los elementos de la fila. Una excepción NullPointerException se genera
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
La interfaz Dequeue
La interfaz Dequeue fue añadida por Java SE 6. Esta interfaz extiende de Queue y declara el
comportamiento de una fila con doble final. Una fila con doble final puede trabajar como una
fila estándar, primero en entrar – primero en salir, o como una pila último en entrar – primero en
salir. Dequeue es una interfaz genérica que está declarada como:
interface Dequeue
Donde, E especifica el tipo de objetos que la fila doble contendrá. Adicionalmente a los métodos
que se heredan de Queue, la interfaz Dequeue añade los métodos listados en la Tabla 17-6.
Varios métodos generan una excepción ClassCastException cuando un objeto es incompatible
con los elementos en la fila doble. Una excepción de tipo NullPointerException se genera si se
intenta almacenar un objeto null y los elementos null no están permitidos en la fila doble. Una
excepción IllegalArgumentException se genera si un argumento no válido es proporcionado
a un método. Una excepción IllegalStateException se genera si se intenta añadir un
elemento a una fila doble de tamaño fijo y ésta se encuentra llena. Una excepción de tipo
NoSuchElementException se genera si se intenta quitar un elemento de una fila doble vacía.
Nótese que Dequeue incluye los métodos push( ) y pop( ). Estos métodos permiten a
Dequeue funcionar como una pila. Además observe el método descendingIterator( ), el cual
devuelve un iterador que devuelve elementos en orden inverso. En otras palabras, devuelve un
iterador que se mueve del final al inicio de la colección. La implementación de un Dequeue
puede realizarse limitando su tamaño a un número predefinido de elementos.
Cuando esto se hace y la inserción de un elemento nuevo falla, es posible manejar la falla
de dos formas. Primero, métodos como addFirst( ) y addLast( ) generan una excepción de tipo
IllegalStateException si una fila doble de tamaño predefinido está llena.
Método
Descripción
void addFirst(E obj)
Añade obj al inicio de la fila doble. Genera una excepción IllegalState
Exception si la fila tiene tamaño predefinido y no existe espacio disponible.
void addLast(E obj)
Añade obj al final de la fila doble. Genera una excepción IllegalState
Exception si la fila tiene tamaño predefinido y no existe espacio disponible.
TABLA 17-6 Métodos definidos por Dequeue
www.detodoprogramacion.com
PARTE II
si se intenta almacenar un objeto null y los elementos null no están permitidos en la fila.
Una excepción de tipo IllegalArgumentException se genera si un argumento no válido es
proporcionado a un método. Una excepción IllegalStateException se genera si se intenta
añadir un elemento a una fila de tamaño fijo y ésta se encuentra llena. Una excepción de tipo
NoSuchElementException se genera si se intenta quitar un elemento de una fila vacía.
A pesar de su simplicidad, Queue ofrece varios aspectos interesantes. Primero, los
elementos sólo pueden ser removidos desde el inicio de la fila. Segundo, existen dos métodos
que obtienen y remueven elementos: poll( ) y remove( ). La diferencia entre ellos es que poll( )
devuelve null si la fila está vacía mientras que remove( ) lanza una excepción. Tercero, existen
dos métodos, element( ) y peek( ), que obtienen pero no remueven el elemento al inicio de
la fila. Difieren únicamente en que element( ) genera una excepción si la fila está vacía, pero
peek( ) devuelve null. Finalmente, observe que offer( ) sólo intenta añadir un elemento a la fila.
Debido a que algunas filas tienen tamaño fijo y podrían estar llenas, offer( ) puede fallar.
447
448
Parte II:
La biblioteca de Java
Método
Descripción
Iterator descendingIterator( )
Devuelve un iterador que se mueve desde el final al inicio de la fila
doble. En otras palabras, devuelve un iterador inverso.
E getFirst( )
Devuelve el primer elemento de la fila doble. El objeto no
es removido de la fila. Este método genera una excepción
NoSuchElementException si la fila doble está vacía.
E getLast( )
Devuelve el último elemento en la fila doble. El objeto no
es removido de la fila doble. Genera una excepción de tipo
NoSuchElementException si la fila doble está vacía.
boolean offerFirst(E obj)
Intenta añadir obj al inicio de la fila doble. Devuelve true si obj
fue añadido y false en caso contrario. Este método devuelve false
cuando se intenta añadir obj a una fila doble de tamaño predefinido
que está llena.
boolean offerLast(E obj)
Intenta añadir obj al final de la fila doble. Devuelve true si obj fue
añadido y false en caso contrario.
E peekFirst( )
Devuelve el elemento al inicio de la fila doble. Devuelve null si la fila
doble está vacía. El objeto devuelto no es removido.
E peekLast( )
Devuelve el elemento al final de la fila doble. Devuelve null si la fila
doble está vacía. El objeto devuelto no es removido.
E pollFirst( )
Devuelve y remueve el elemento al inicio de la fila doble. Devuelve
null si la fila doble está vacía.
E pollLast( )
Devuelve y remueve el elemento al final de la fila doble. Devuelve null
si la fila doble está vacía.
E pop( )
Devuelve y remueve el elemento al inicio de la fila doble. Genera una
excepción NoSuchElementException si la fila doble está vacía.
void push(E obj)
Añade obj al inicio de la fila doble. Genera una excepción
IllegalStateException si la fila tiene tamaño predefinido y no existe
espacio disponible.
E removeFirst( )
Devuelve y remueve el elemento al inicio de la fila doble. Genera una
excepción de tipo NoSuchElementException si la fila doble está vacía.
boolean
removeFirstOcurrence (Object obj)
Remueve la primera ocurrencia de obj de la fila doble. Devuelve
true si el elemento es removido con éxito y false si la fila doble no
contiene a obj.
E removeLast( )
Devuelve y elimina el elemento al final de la fila doble. Genera una
excepción de tipo NoSuchElementException si la fila doble está vacía.
boolean
Devuelve la última ocurrencia de obj de la fila doble. Devuelve true si el
removeLastOcurrence (Object obj) elemento es removido con éxito y false si la fila doble no contiene a obj.
TABLA 17-6
Métodos definidos por Dequeue (continuación)
Segundo, los métodos como offerFirst( ) y offerLast( ) devuelven false si el elemento no puede
ser añadido.
Las clases de la estructura de colecciones
Ahora que nos hemos familiarizado con las interfaces de colección, estamos listos para examinar
las clases estándar que las implementan. Algunas de las clases proporcionan implementaciones
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
Clase
Descripción
AbstractCollection
Implementa la mayor parte de la interfaz Collection.
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 su uso por una colección que utilice acceso
secuencial a sus elementos, en vez de aleatorio.
LinkedList
Implementa una lista enlazada extendiendo AbstractSequentialList.
ArrayList
Implementa un arreglo dinámico extendiendo AbstractList.
ArrayDequeue
Implementa una fila dinámica con doble final extendiendo AbstractCollection e
implementando la interfaz Dequeue. Añadido en Java SE 6.
AbstractSet
Extiende AbstractCollection e implementa la mayor parte de la interfaz Set.
EnumSet
Extiende AbstractSet para utilizarlo con elementos de tipo enum.
HashSet
Extiende AbstracSet para su uso con una tabla de dispersión.
LinkedHashSet
Extiende HashSet para permitir recorridos en orden de inserción.
PriorityQueue
Extiende AbstractQueue para soportar una fila basada en prioridades.
TreeSet
Implementa un conjunto almacenado en un árbol. Extiende AbstractSet.
Las siguientes secciones examinan las clases de colección concretas e ilustran su uso.
NOTA Además de las clases de colección, se han rediseñado varias clases preexistentes como Vector,
Stack y Hashtable para que soporten colecciones. Estas se examinarán más adelante en este
capítulo.
La clase ArrayList
La clase ArrayList extiende AbstractList e implementa la interfaz List. ArrayList es una clase
genérica declarada como:
class ArrayList
Donde, E especifica el tipo de objetos que la lista podría almacenar.
ArrayList soporta arreglos dinámicos que pueden crecer según se necesite. En Java, los arreglos
estándar son de longitud fija. Después de creado un arreglo, su tamaño no puede aumentar ni
disminuir, lo que significa que hay que saber de antemano cuántos elementos habrá en él. Pero
a veces no se sabe exactamente que tan grande se necesita que sea un arreglo hasta el tiempo de
ejecución. Para resolver esta situación, la estructura de colecciones define ArrayList. En esencia,
una ArrayList es un arreglo de longitud variable que almacena referencias a objeto. Esto es, un
ArrayList puede aumentar o disminuir de tamaño dinámicamente.
Los ArrayList se crean con un tamaño inicial. Cuando este tamaño se excede, la colección
se agranda automáticamente. Cuando se quitan objetos, el arreglo puede disminuir de tamaño.
www.detodoprogramacion.com
PARTE II
completas que se pueden usar tal cual. Otras son abstractas y proporcionan el esqueleto de
implementaciones que se usan como puntos de partida para crear colecciones concretas.
Ninguna de las clases de colección está sincronizada, pero, como veremos más adelante en este
capítulo, es posible obtener versiones sincronizadas.
Las clases de colección estándar se resumen en la siguiente tabla:
449
450
Parte II:
La biblioteca de Java
NOTA Los arreglos dinámicos son soportados también por la clase Vector, la cual se describe más
adelante en este capítulo.
ArrayList tiene los constructores siguientes:
ArrayList( )
ArrayList(Collection extends E> c)
ArrayList(int capacidad)
El primer constructor construye un ArrayList vacío. El segundo construye un ArrayList que se
inicializa con los elementos de la colección c. El tercero construye un ArrayList que tiene la
capacidad inicial especificada. La capacidad es el tamaño del arreglo subyacente que se utiliza
para almacenar los elementos. La capacidad crece automáticamente según se añaden elementos
a la lista.
El siguiente programa muestra un uso sencillo de ArrayList. Se crea un ArrayList, y luego
se le añaden objetos de tipo String. Recuérdese que una cadena entrecomillada se traduce a un
objeto String. Finalmente la lista se muestra, algunos de los elementos se remueven y la lista se
muestra de nuevo.
// Ejemplo con ArrayList.
import java.util.*;
class ArrayListDemo {
public static void main(String args[]) {
// crea un ArrayList
ArrayList al = new ArrayList();
System.out.println("Tamaño inicial de al: " +
al. size());
// añadir elementos al ArrayList
al.add("C");
al.add("A");
al.add("E");
al.add("B");
al.add("D");
al.add("F");
al.add(l, "A2");
System.out.println("Tamaño de a1 después de las adiciones: " +
al.size());
// mostrar el ArrayList
System.out.println{"Contenido de al: " + al);
// quitar elementos del ArrayList
al.remove("F");
al.remove(2);
System.out.println("Tamaño de a1 después de quitar elementos: " +
al.size());
System.out.println("Contenido de al: " + al);
}
}
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
451
La salida de este programa se muestra aquí:
Nótese que el objeto a1 comienza vacía y va creciendo según se le añaden elementos. Cuando se
quitan elementos, su tamaño se reduce.
En el ejemplo precedente, los contenidos de una colección se muestran utilizando
la conversión por omisión proporcionada por el método toString( ), heredado de
AbstractCollection. Aunque esto basta para programas y ejemplos cortos, rara vez se utiliza
este método para mostrar los contenidos de una colección en el mundo real. Habitualmente el
programador proporciona sus propias rutinas de salida. Sin embargo, aún usaremos la salida
por omisión producida por toString( ) para algunos de los siguientes ejemplos.
Aunque la capacidad de un objeto ArrayList aumenta automáticamente según se
almacenan objetos en él, se puede aumentar su capacidad manualmente llamando al método
ensureCapacity( ). Esto puede ser deseable si se conoce de antemano que más adelante se
almacenarán en la colección muchos más elementos de los que actualmente puede contener.
Aumentando su capacidad una vez, al inicio, se pueden evitar distintas reasignaciones de
memoria más adelante. Dado que las reasignaciones son costosas en términos de tiempo, evitar
reasignaciones innecesarias mejora el rendimiento. La firma de ensureCapacity( ) se muestra a
continuación:
void ensureCapacity(int cap)
Donde cap es la nueva capacidad.
A la inversa, para reducir el tamaño del arreglo subyacente a un objeto ArrayList para que
su tamaño sea exactamente igual al número de elementos que actualmente contiene, se llama al
método trimToSize( ) de la siguiente forma:
void trimToSize( )
Obtención de un arreglo a partir de un ArrayList
Cuando se trabaja con ArrayList, en ocasiones se desea obtener un arreglo con los contenidos
de la lista. Como ya se ha explicado, esto se puede lograr llamando al método toArray( ) definido
en Collection. Existen diferentes razones por las cuales se puede desear convertir una colección en
un arreglo, como:
• Conseguir tiempos más rápidos de procesamiento para ciertas operaciones.
• Pasar un arreglo a un método que no está sobrecargado para aceptar una colección.
• Integrar el código de usuario más nuevo, basado en las colecciones, con código
preexistente que no entiende de colecciones.
Cualquiera que sea la razón, convertir una ArrayList en un arreglo es algo trivial.
Como se explicó antes, se tienen dos versiones del método toArray( ), los cuales se
muestran aquí nuevamente por comodidad.
Object[ ] toArray( )
T[ ] toArray(T arreglo[ ])
www.detodoprogramacion.com
PARTE II
Tamaño inicial de a1: 0
Tamaño de a1 después de las adiciones: 7
Contenidos de a1: [C, A2, A, E, B, D, F]
Tamaño de a1 después de quitar elementos: 5
Contenidos de a1: [C, A2, E, B, D]
452
Parte II:
La biblioteca de Java
El primer método devuelve un arreglo de elementos tipo Object. El segundo devuelve un arreglo
de elementos que tienen el tipo definido en T. Normalmente, la segunda forma es más cómoda
porque devuelve un arreglo de un tipo adecuado. El siguiente programa demuestra el uso del
método toArray( ).
// Convertir una ArrayList en un arreglo.
import java.util.*;
class ArrayListToArreglo{
public static void main{String args[]) {
// Crear un ArrayList
ArrayList al = new ArrayList();
// Añadir elementos
al.add(l);
al.add(2);
al.add(3);
al.add(4);
System.out.println("Contenido de al: " + al);
// Obtener el arreglo
Integer ia[] = new Integer [al.size()];
ia = a1.toArray(ia);
int sum = 0;
// Sumar el arreglo
for(int i : ia) sum += i;
System.out.println("La suma es: " + sum);
}
}
La salida del programa se muestra aquí:
Contenidos de al: [1, 2, 3, 4]
La suma es: 10
El programa comienza creando una colección de enteros. A continuación, se llama a
toArray( ), que obtiene un arreglo de objetos tipo Integer. Luego, los contenidos de este arreglo
se suman utilizando un ciclo estilo for-each.
Otro elemento interesante del programa anterior es el siguiente. Como sabemos, las
colecciones sólo pueden almacenar referencias, no valores, a tipos primitivos. Sin embargo,
autoboxing hace posible pasar valores de tipo int al método add( ) sin tener que envolverlo
manualmente en un objeto de tipo Integer. Autoboxing realiza una envoltura automática. En
este sentido, autoboxing mejora significativamente la facilidad con la cual las colecciones pueden
ser utilizadas para almacenar valores primitivos.
La clase LinkedList
La clase LinkedList extiende de AbstractSequentialList e implementa la interfaz List,
Dequeue y Queue. LinkedList proporciona una estructura de datos de tipo lista enlazada.
LinkedList es una clase genérica declarada como:
class LinkedList
Donde, E especifica el tipo de los objetos que la lista almacenará. LinkedList tiene los siguientes
dos constructores:
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
453
El primer constructor crea una lista enlazada vacía. El segundo construye una lista enlazada que
se inicializa con los elementos de la colección c.
Debido a que LinkedList implementa la interfaz Dequeue tenemos acceso a los métodos
definidos por Dequeue. Por ejemplo, para añadir elementos al inicio de una lista se puede usar
addFirst( ) u offerFirst( ). Para añadir elementos al final de la lista se puede usar addLast( ) u
offerLast( ). Para obtener el primer elemento, se puede usar getFirst( ) o peekFirst( ). Para obtener el último elemento se utiliza getLast( ) o peekLast( ). Para remover el primer elemento, se
utiliza removeFirst( ) o pollFirst( ). Para eliminar el último elemento se utiliza removeLast( ) o
pollLast( ).
El siguiente programa ilustra varios de los métodos soportados por LinkedList:
// Ejemplo con LinkedList.
import java.util.*;
class LinkedListDemo {
public static void main(String args[]) {
// crear una lista enlazada
LinkedList l1 = new LinkedList();
// añadir elementos a la lista vinculada
l1.add("F");
l1.add("B");
l1.add("D");
l1.add("E");
l1.add("C");
l1.addLast("Z");
l1.addFirst("A");
l1.add(l, "A2");
System.out.println("Contenido original de l1: " + l1);
// quitar elementos de la lista enlazada
l1.remove("F");
l1.remove(2);
System.out.println("Contenido de l1 tras quitar elementos:
+ l1);
// quitar los elementos primero y último
l1.removeFirst();
l1.removeLast();
System.out.println("l1 tras quitar el primero y el último:
+ l1);
// obtener y asignar un valor
String val = l1.get(2);
l1.set(2, val + " cambiado");
System.out.println("l1 tras el cambio: " + l1);
}
}
La salida de este programa es ésta:
www.detodoprogramacion.com
PARTE II
LinkedList( )
LinkedList(Collection extends E> c)
454
Parte II:
La biblioteca de Java
Contenido original de l1: [A, A2, F, B, D, E, C, Z]
Contenido de l1 tras quitar elementos: [A, A2, D, E, C, Z]
l1 tras quitar el primero y el último: [A2, D, E, C]
l1 tras el cambio: [A2, D, E cambiado, C]
Como LinkedList implementa la interfaz List, las llamadas a add(E) añaden elementos al
final de la lista, al igual que addLast( ). Para insertar elementos en una posición específica, ha de
usarse la forma add(int,E) de add( ), como se ilustra con la llamada a add(l,”A2”) en el ejemplo.
Nótese cómo el tercer elemento de ll se cambia empleando llamadas a get( ) y set( ). Para
obtener el valor actualizado de un elemento, ha de pasarse a get( ) el índice en que se almacena
dicho elemento. Para asignar un valor nuevo a ese índice, ha de pasarse a set( ) el índice y su
nuevo valor.
La clase HashSet
HashSet extiende AbstractSet e implementa la interfaz Set. Crea una colección que usa una
tabla de dispersión para el almacenamiento. HashSet es una clase genérica que está declarada
como:
class HashTable
Donde, E especifica el tipo de objetos que la colección almacenará.
Como muchos lectores sabrán, una tabla de dispersión almacena información usando
un mecanismo llamado dispersión. Con el uso de la dispersión, la información de una clave se
utiliza para determinar un valor único, llamado su código de dispersión. El código de dispersión
se utiliza entonces como el índice donde se almacenan los datos asociados con la clave. La
transformación de la clave en su código de dispersión se lleva a cabo automáticamente —nunca
se ve el código de dispersión—. Además, el código de dispersión no puede indexar directamente
la tabla de dispersión. La ventaja de la dispersión es que permite mantener constante el tiempo
de ejecución de operaciones básicas, como add( ), contains( ), remove( ) y size( ), incluso para
conjuntos grandes.
HashSet define los siguientes constructores:
HashSet( )
HashSet(Collection extends c)
HashSet(int capacidad)
HashSet(int capacidad, float llenar)
La primera forma construye un conjunto de dispersión por omisión. La segunda forma lo
inicializa usando los elementos de c. La tercera inicializa la capacidad del conjunto de dispersión
a capacidad. La cuarta forma inicializa a partir de sus argumentos tanto la capacidad como la
razón de llenado (también llamada capacidad de carga) del conjunto de dispersión. La razón de
llenado debe estar comprendida entre 0.0 y 1.0, y determina qué tan de lleno puede estar el
conjunto de dispersión antes de que se aumente de tamaño. Específicamente, cuando el número
de elementos es mayor que la capacidad del conjunto de dispersión multiplicada por su razón
de llenado, el conjunto de dispersión se expande. Para constructores que no toman razón de
llenado, se utiliza 0.75.
HashSet no define ningún método además de los ya proporcionados por sus superclases e
interfaces.
Es importante mencionar que un conjunto de dispersión no garantiza el orden de sus
elementos, porque el proceso de dispersión no se suele prestar a la creación de conjuntos
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
// Ejemplo con HashSet.
import java.util.*;
class HashSetDemo {
public static void main(String args[]) {
// crear un conjunto de dispersión
HashSet hs = new HashSet();
// añadir elementos al conjunto de dispersión
hs.add("B");
hs.add("A");
hs.add("D");
hs.add("E");
hs.add("C");
hs.add("F");
System.out.println(hs);
}
}
Ésta es la salida del programa:
[D, A, F, C, B, E]
Como se ha explicado, los elementos no se almacenan en orden y la salida exacta puede variar.
La clase LinkedHashSet
La clase LinkedHashSet extiende HashSet y no añade miembros propios. Es una clase genérica
declarada como:
class LinkedHashSet
Aquí, E especifica el tipo de objetos que el conjunto almacenará. Sus constructores son iguales a
los de HashSet.
LinkedHashSet mantiene una lista enlazada de los elementos que se agregan al conjunto,
en el orden en que son agregados. Esto permite recorrer el conjunto en el orden en que los
elementos fueron agregados. Esto es, cuando se recorre el LinkedHashSet utilizando un
iterador, los elementos serán devueltos en el orden en el cual fueron insertados. Éste es también
el orden en que serán contenidos en la cadena devuelta por toString( ) cuando es llamado
con un objeto LinkedHashSet. Para ver el efecto de un LinkedHashSet, intente sustituir
LinkedHashSet por HashSet en el programa anterior. La salida será:
[B, A, D, E, C, F]
Éste es el orden en el cual los elementos fueron insertados.
La Clase TreeSet
TreeSet extiende de AbstractSet e implementa la interfaz NavigableSet. TreeSet crea una
colección que utiliza un árbol para el almacenamiento de datos. Los objetos se almacenan
ordenados, en orden ascendente. Los tiempos de acceso y recuperación son bastante rápidos,
www.detodoprogramacion.com
PARTE II
ordenados. Si hace falta almacenamiento ordenado, entonces es mejor elegir otra colección,
como por ejemplo TreeSet.
Aquí hay un ejemplo que ilustra HashSet:
455
456
Parte II:
La biblioteca de Java
lo que hace de TreeSet una excelente elección cuando se almacenan grandes cantidades de
información ordenada que debe encontrarse rápidamente.
TreeSet es una clase genérica que se declara como:
Class TreeSet
Donde, E especifica el tipo de objetos que el conjunto almacenará.
TreeSet define los siguientes constructores:
TreeSet( )
TreeSet(Collection extends E> c)
TreeSet( Comparator super E> comp)
TreeSet(SortedSet ss)
La primera forma construye un TreeSet vacío que se ordenará en orden ascendente de
acuerdo con el orden natural de sus elementos. La segunda forma construye un TreeSet que contiene los elementos de c. La tercera forma construye un TreeSet vacío que se ordenará de acuerdo
con el comparador especificado por comp. (Los comparadores se describen más adelante en este
capítulo.) La cuarta forma construye un TreeSet que contiene los elementos de ss.
Aquí hay un ejemplo que muestra el uso de un TreeSet:
// Ejemplo con TreeSet.
import java.util.*;
class TreeSetDemo {
public static void main(String args[]) {
// Crear un conjunto en árbol
TreeSet ts = new TreeSet();
// Añadir elementos al árbol
ts.add("C");
ts.add("A");
ts.add("B");
ts.add("E");
ts.add("F");
ts.add("D");
System.out.println(ts);
}
}
La salida del programa se muestra a continuación:
[A, B, C, D, E, F]
Como hemos explicado, dado que TreeSet almacena sus elementos en un árbol, se ordenan
automáticamente.
Debido a que TreeSet implementa la interfaz NavigableSet (la cual fue añadida por Java SE 6),
se pueden utilizar los métodos definidos por NavigableSet para recuperar elementos de un
TreeSet. Por ejemplo, considerando el programa anterior, la siguiente sentencia utiliza al método
subset( ) para obtener un subconjunto de ts que contiene el elemento entre C (incluyéndola) y F
(sin incluirla). Luego se despliega el conjunto resultante.
System.out.println(ts.subset( "C", "F"));
La salida producida por la línea anterior es:
[C, D, E]
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
457
De forma similar se puede experimentar con otros métodos definidos por NavigableSet.
PriorityQueue extiende a AbstractQueue e implementa la interfaz Queue. PriorityQueue
crea una fila basada en prioridades acorde con un comparador definido. PriorityQueue es una
clase genérica declarada como:
class PriorityQueue
Donde, E especifica el tipo de objetos almacenados en la fila. PriorityQueue crece de manera
dinámica según sea necesario.
PriorityQueue define seis constructores:
PriorityQueue( )
PriorityQueue(int capacidad)
PriorityQueue(int capacidad, Comparator super E> comp)
PriorityQueue(Collection extends E>c)
PriorityQueue(PriorityQueue extends E>c)
PriorityQueue(SortedSet extends E>c)
El primer constructor construye una fila vacía. Su capacidad inicial es 11. El segundo constructor construye una fila que tiene la capacidad inicial especificada en el parámetro capacidad. El
tercer constructor construye una fila con la capacidad y el comparador especificados. Los últimos tres constructores crean filas que se inicializan con los elementos de la colección c, pasada
como argumento. En todos los casos, la capacidad crece automáticamente conforme se agregan
elementos.
Si ningún comparador se especifica cuando se construye una PriorityQueue, se utiliza el
comparador por omisión para el tipo de dato almacenado en la fila. El comparador por omisión
ordenara la fila en orden ascendente. Así, el inicio de la fila contendrá al menor de los valores.
Sin embargo, proporcionando un comparador personalizado, se puede especificar un esquema
diferente de ordenamiento. Por ejemplo, cuando se almacenan elementos que incluyen horas o
fechas, se puede priorizar la fila para que el elemento más antiguo sea el primero de la fila.
Se puede obtener una referencia al comparador utilizado por una PriorityQueue llamando
a su método comparator( ), definido como:
Comparator super E> comparator( )
Este método devuelve al comparador. Si la fila que invoca utiliza ordenamiento natural, el
método devuelve null.
Es importante considerar que aunque se puede recorrer una PriorityQueue utilizando
un iterador, el orden de dicha iteración no está definido. Para utilizar adecuadamente una
PriorityQueue se deben llamar a los métodos offer( ) y poll( ) definidos en la interfaz Queue.
La clase ArrayDequeue
Java SE 6 añade la clase ArrayDequeue, la cual extiende de AbstractCollection e implementa
la interfaz Dequeue. Esta clase no añade métodos propios. ArrayDequeue crea un arreglo
dinámico sin restricciones de capacidad. La interfaz Dequeue soporta implementaciones que
restringen la capacidad de almacenamiento, sin embargo establecer dicha restricción es opcional.
ArrayDequeue es una clase genérica declarada como:
class ArrayDequeue
www.detodoprogramacion.com
PARTE II
La clase PriorityQueue
458
Parte II:
La biblioteca de Java
Aquí, E especifica el tipo de objetos almacenados en la colección.
ArrayDequeue define los siguientes constructores:
ArrayDequeue( )
ArrayDequeue(int tamaño)
ArrayDequeue(Collection extends E> c)
El primer constructor construye una fila doble vacía. Su capacidad inicial es 16. El segundo
constructor construye una fila doble que cuenta con la capacidad inicial definida por el
argumento tamaño. El tercer constructor crea una fila doble que está inicializada con los
elementos de la colección dada en el argumento c. En todos los casos, la capacidad crece
conforme se necesita para manejar los elementos añadidos a la fila doble.
El siguiente programa demuestra el uso de ArrayDequeue utilizando esta colección para
imitar el comportamiento de una pila:
// Ejemplo con ArrayDequeue.
import java.util.*;
class ArrayDequeueDemo {
public static void main(String args[]) {
// Crear un ArrayDequeue
ArrayDequeue adq = new ArrayDequeue();
// Usar el ArrayDequeue como pila
adq.push("A");
adq.push("B");
adq.push("D");
adq.push("E");
adq.push("F");
System.out.print("Sacando elementos de la pila: ");
while(adq.peek() != null)
System.out.println(adq.pop() + " ");
System.out.println();
}
}
La salida del programa se muestra a continuación:
Sacando elementos de la pila: F E D B A
La clase EnumSet
EnumSet extiende AbstractSet e implementa Set. Está hecho específicamente para ser usado
con llaves de un tipo enumerado. Es una clase genérica declarada como:
class EnumSet>
Donde, E especifica a los elementos. Note que E debe extender Enum, lo cual refuerza los
requerimientos de que los elementos deben ser del tipo enum especificado.
EnumSet no define constructores. En lugar de ello, utiliza los métodos de fábrica mostrados
en la Tabla 17-7 para crear objetos. Todos los métodos generan NullPointerException en caso
de que se presente un problema. Los métodos copyOf( ) y range( ) pueden además generar una
excepción de tipo IllegalArgumentException. Observe que el método of( ) está sobrecargado
www.detodoprogramacion.com
Capítulo 17:
java.util parte 1: colecciones
Acceso a una colección por medio de un iterator
A menudo se desea ejecutar un recorrido a lo largo de los elementos de una colección. Por
ejemplo, se puede querer mostrar cada elemento. Una forma de hacer esto es empleando un
iterador, un objeto que implementa la interfaz Iterator o la interfaz ListIterator. Un Iterator
proporciona un ciclo a lo largo de una colección, obteniendo o quitando elementos. ListIterator
Método
static >
EnumSet allOf(Class