Transcript
PIENSA EN JAVA
PIENSA EN JAVA Cuarta Edición
BRUCEECKEL President, MindView, ¡ne.
Traducción Vuelapluma
· PEARSON
, Prentice . , Hall Madrid e México e Santa Fe de Bogotá e Buenos Aires e Caracas e Lima Montevideo e San Juan e San José e Santiago e Sao Paulo e White Plains e
/
Datos de catalogación bibliográfica
PIENSA EN JAVA Bruce EckeJ
PEARSON EDUCACIÓN S.A. , Madrid, 2007 ISBN: 978-84-8966-034-2 Materia: Informática, 004 Fonnato: 215 x 270 mm.
Páginas: 1004
Todos los derechos reservados. Queda prohibida, salvo excepción prevista en la Ley, cualquier forma de reproducción, distribución, comunicación pública y transfonnación de esta obra sin contar con autorización de los ti tulares de propiedad intelectual. La infracción de los derechos mencionados puede ser constitutiva de delito contra la propiedad intelectual (arls. 270 y sgls. Código Penal).
DERECHOS RESERVADOS © 2007 por PEARSON EDUCACIÓN S.A. Ribera del Loira, 28 28042 Madrid PIENSA EN JAVA Bruee Eekel ISBN: 978-84-8966-034-2 Deposito Legal: M-4.753-2007 PRENTICE HALL es un sello editorial autori zado de PEARSON EDUCACIÓN S.A. Authorized translation from the English language edition, entitled THINlUNG IN JAVA, 4" Edition by ECKEL BRUCE, published by Pearsoo Education loe, publishing as Prentiee Hall, Copyright © 2006 EQillPO EDITORIAL Editor: Miguel Martín-Romo Técnico editorial: Marta Caicoya EQUIPO DE PRODUCCIÓN: Director: José A. CIares Técnico: María Alvear Diseño de Cubierta: Equipo de diseño de Pearson Ed ucación S.A. Impreso por: Gráficas Rógar, S. A. IMPRESO EN ESPAÑA - PRINTED IN SPAIN Este libro ha sido impreso con papel y tintas ecológicos
Dedicatoria A Dawn
Resumen del contenido Prefacio ...................... . ..•... . ....•. . .... . ........ . . . .... xix Introducción .... . .............. . ... . ...... . ..... . . ....... . . ... . . . xxv 1 Introducción a los objetos . .. . . .. . ....... . ......... . . .... . . .. .. . ..... 1 2 Todo es un objeto ..... . ... . ..... . ...... . ................... . ...... 23 3 Operadores ................ . ...... . .......... . ............ . ...... 43 4 Control de ejecución ......... . . ... . ........ . ... . .. . . . .... . ......... 71 5 Inicialización y limpieza ..... . .......... .. ...................... . ... 85 6 Control de acceso .... . ....... . ............... . ................ . .. 121 7 Reutilización de clases ......... . ............ . ......... . . . ...... . . . 139 8 Polimorfismo .. .... . ... . . .. . .. .. .... . . .. ....... ... ...•....... . .. 165 9 Interfaces ... . ......................................... . . ....... 189 10 Clases internas .............. . ......... . . . ...... . ......... . . . .... 211 11 Almacenamiento de objetos .... . ..... .. ........ .. . . .. ............. 241 12 Tratamiento de errores mediante excepciones .. . ............ . .. . ..... 277 13 Cadenas de caracteres ........ . ...... . .. . . . . . ..... . ............... 317 14 Información de tipos . ........ . . . .. . ..... .. ......... . ............. . 351 15 Genéricos . . .... . .................. . . . ..................... . ..... 393 16 Matrices . ................... .. . . . . .... .. ...... . . . . . . .. .. . ....... 483 17 Análisis detallado de los contenedores .... .. ........................ 513 18 E/S ..... .. . . ............. . ........ . .. . ............. . . .... .. .. . .. 587 19 Tipos enumerados .... . .... .. . . . . . .. . . . .. ......... . ...... .. ...... 659 20 Anotaciones . ....... . ....... . .... . .. .. .... ... .. .. ......... .. . . ... 693 21 Concurrencia ......... . ..... . . . . .. . .... . ............... .. . .. ..... 727 22 Interfaces gráficas de usuario ... . ...... . .. . . . ........ . . ...... . .. . .. 857 A Suplementos . ........... . . . .................... . ......... . ...... 955 B Recursos ......... . . . .. . .. . ......... . . . ...... . ........ . .. .. ..... 959 índice .. .. . . ........ . ............ . .... . .......... . ....... .. ..... 963
Contenido
Prefacio ... .. ..... . .. . ........ .. ... .. . . xix Java SES Y SE6 ........ .
. .. .. . .... ,. xx
16
Java e Internet. ¿Qué es la Web?
. 16
. . . .... .. ..... . xx
Programación del lado del clicntc ............. 18
La cuarta edición .. .. .. . . .. . .. ..... ... .... xx
Programación del lado del servidor . . . . . . . . .. .. 21
Java SE6.
... . xxi
Cambios
Sobre el diseño de la cubierta .......... . .... xxii
Agradecimientos. . . . . . . . . .
. ..........
XXll
Introducción . ..... .. .. .... . ........... . xxv Prerrequisitos ... Aprendiendo Java ..... .. .
.... xxv . , •• . •. . . . XXVI
Objetivos ................ . .
. . . . . XXVI
Enseñar con este libro ........... . ........ Documentación del JDK en HTML .
XXVll
. . XXVI1
Ejercicios .............................
XXVll
Fundamentos para Java . .. ........... .... . XXVIl Código fuente ........ . ..... . . . .. . ..... Estándares de codificación.
XXVIII
Resumen........................
. ... 22
2 Todo es un objeto ....... . ...... . ........ 23 Los objetos se manipulan mediante referencias .. 23
Es necesario crear todos los objetos.
. . 24
Los lugares de almacenamiento ... ..... ... .
. . 24
Caso especial: tipos primitivos. . . . . . . . . . . . . ..25 Matrices en Java. . . . . .
. . ... . .. .. . .. 26
Nunca es necesario destruir un objeto ......... 27 Ámbito. .. .. . . . . . . . . . .. Ámbito de los objetos ..
. .. . .... . .. 27 ............. . .. 27
Creación de nuevos tipos de datos: class ..... '.. 28 Campos y métodos .
. . .... . ... . ... ... 28
. .. xxix
Métodos, argumentos y valores de retomo ...... 29
Errores .. ............................... xxx
La lista de argumentos ....................... 30
1 Introducción a los objetos . ... ...... . ... . . . 1 El progreso de la abstracción ............ •. ... 1
Construcción de un programa Java
.... 3 1
Visibilidad de los nombres.
. .... 31
Todo objeto tiene una interfaz ..... . .. ........ 3
Utilización de otros componentes.
Un objeto proporciona servicios ...... . ........ 4
La palabra clave static
. ... . .. . 31
.........
. . .. 32
La implementación oculta . .......... ......... 5
Nuestro primer programa Java
.. 33
Reutili zación de la implementación ..... ... .... 6
Compilación y ejecución.
. . . 35
. .. . ...... 6
Comentarios y documentación embebida ....... 35
Relaciones es-un y es-como-un ....... ... . . .... . 8
Documentación mcdiantc comcntarios .......... 36
Herencia. . . . . . . . . . . . . . . . . . . . .
Objetos intercambiables con polimorfismo ...... 9
Sintaxis.
La jerarquía de raíz única. .
HTML embebido . ... . ... . .. .
. .... ...... 11
Contenedores ... .......................... 12 Tipos paramctrizados (genéricos)
. . ... 13
. . 36
.... 37
Algunos marcadores de ejemplo ... . ... . . . Ejemplo de documentación.
.37 . . 39
.. . . . . ... 39
Creación y vida de los objetos ... . ... ........ 13
Estilo de codificación.
Tratamiento de excepciones: manejo de errores .. 15
Resumen .............. . ..... ... .. ... .. .. 40
Programación concurrente .................. 15
Ejercicios ...... .. .. . . . ......• .. .......... 40
x Piensa en Java
3 Operadores ... ... .... ... . .. .... . . . .... . 43 Instrucciones simples de impresión .....•.. ... 43 Utilización de los operadores Java
. 44
..... .. .. ... . . 89
Sobrecarga con primitivas.
Sobrecarga de los valores de retomo.
.. .. 92
Constructores predetenninados .
. .. 92
Precedencia ..... .......... . ...... . .. . .... 44
La palabra clave tbis ....... ........ .... ... 93
Asignación ..................... ... .. ... . 44
Invocación de constructores desde otros constructores .... . . ... ... . .. .. . ... .. ... . 95
Creación dc alias en las llamadas a métodos ...... 46
Operadores matemáticos. . . .
. .......... 46
Operadores unarios más y menos .............. 48
Autoincremento y autodecremento . .
. 48
Operadores relacionales . . . . . . . . . . . . . .
. 49
Comprobac ión de la equivalencia de obj etos ..... 49
Operadores lógicos ........................ 51 Cortocircuitos . . . . . . . . . . . . . . . . . . . . . .
Literales. . . . . . . .
. ... . 52
. ........ . . . ....•...... 53
Notación exponencial .. . . . ........ . . ........ 54
Operadores bit a bit. ............. . •...•. ... 55 Operadores de desplazamiento.
. 55
Operador temario if-clsc
. 58
Operadores + y += para String ............... 59 Errores comunes a la hora de utilizar operadores. 60 Operadores de proyección
... 60
Truncamiento y redondeo
..... 61
Promoción ..... . . .... . . . . . . . . . . . ... . . .. . . . 62
Java no tiene operador "sizeof' . . . .
. ... 62
Compedio de operadores . ....... .. .... . .... 62 Resumen .... ......... ....• . . . . . ... . ... .. 70
4 Control de ejecución .. . .................. 71 troe y false ........... . ..... . . ..... •.. ... 7 1 if-else.
. .. ..... . . • . • . . ... . . . . .•. •..... 71
El significado de sta tic . . ... ............... . . 96
Limpieza: finalización y depuración de memoria .............. .
...... 97
¿Para qué se utiliza finalizeO?
.. . 97
Es necesario efectuar las tareas de limpieza
... 98
La condición de terminación . . .... . ... . ... .. .. 98 ... 100
Cómo funciona un depurador de memoria
Inicialización de miembros ..
.. 102
Especificación de la ini cial ización
... 103
Tnicialización mediante constmctores ....
104
Orden de inicialización .....
105
Inicialización de datos estáticos .
.. . 106
Ini cial ización static explícita ................ 108 Iniciali zación de instancias no estáticas
Inicialización de matrices ..... .
..... 109
.... 110 . 113
Listas variables de argumentos
Tipos enwnerados ..... . ..... ..... . . . . .. .. 11 7 Resnmen ............ .... . . . • . ... .. ..... 119
6 Control de acceso . .. . . . .. . . .... . . .. . .. . 121 package: la unidad de biblioteca ....... . ... . 122 Organización del código .
............. . . 123
Creación de nombres de paquete unívocos ...... 124 Una biblioteca personalizada de herramientas
.. 126
.. .... .. . .. .. ........... 72
Uti lización de importaciones para modificar el comportamiento . . ... . ... .. .. . .... . . 128
do-while ... . ... ...... .. . . . . . . . . . ... . . ... . 73
Un consejo sobre los nombres de paquete .. ... . . 128
Iteración for ...
.. .. .. ...... ...... .. 73
El operador coma
Sintaxis[oreach ...... .
.........
. ... 74
. ..... ...... 74
returo ...... ...... . • .. ... .... .. . . . . . .... 76 break y continue . .. .............. . .... ... 77
Especificadores de acceso Java
.... 128 .. . . 129
Acceso de paquete
public: acceso de interfaz .. .. .. .. . .......... 129 private: ino lo toque! protected: acceso de herencia
. . . . . . •.
. 130 .. 131
La despreciada instrucción "goto" .... •.. ..... 78
Interfaz e implementación ................. 133
switch
Acceso de clase
... ..... ....... .............. 81
Resumen ... ....... ........ . . • ... . ....... 83
Res umen .
.......... .. ........... 134 ..... 136
5 Inicialización y limpieza . . .. . ... .. ... . . . . . 85
7 Reutilización de clases .............. .. .. 139
Inicialización garantizada con el constructor .... 85
Sintaxis de la composición ...... ... .... .... 139
Sobrecarga de métodos ..................... 87
Sintaxis de la herencia ..........•... •.. ... 142
Cómo se distingue entre métodos sobrecargados .. 88
Inicialización de la clase base .. ......... . . .. . 143
Contenido xi
Delegación ....... .
....... . .... ... . . 145
Ampliación de la interfaz mediante herencia .. 201
Combinación de la composición y de la herencia .... . ............ 147
Co lisiones de nombres al combinar interfaces ... 202
Cómo garantizar una limpieza apropiada Ocultac ión de nombres
.. 148
...........
. .. . 151
Adaptaci ón a una interfaz ..
....... 203
Campos en las interfaces
.205
Inicializac ión de campos en las interfaces.
.205
Cómo elegir entre la composición y la herencia 152
Anidamiento de interfaces. . . . . .
protected .......
. .............. . .... 153
Interfaces y factorías ............. . . . ...... 208
Upcasting (generalización) ..... . . ..... ..... 154
Resumen ........ ........ .. . . ....... .... 210
¿Por qué "upcasling'? .
. .. . . 155
Nueva comparación entre la composición y la herencia. .. . . .. . . .
La palabra clave final
. . . 155
.............
156
10 Clases internas .. .. . . .. ......... . . .. ... 211 Creación de clases internas
. . .. ... 156
Utilización de .this y .new .
Métodos final
. . .. ... 159
Clases interna y generalización
.. 161
Una advertencia sobre final
.. .... ..... 213 .... . ..... 214
Clases internas en los métodos y ámbi tos
. 16 1
Inicialización y carga de clases .... Inicialización con herenc ia
Resumen.
.... ... . ..... 2 11
El enlace con la clase externa ..
Datos final .. ....... .... . .
Clases final
. 206
... 162 ........
.......
. . 162
. . 163
8 Polimorfismo . ... . . . . . . .. .. . . .... . .. .. 165 Nuevas consideraciones sobre la generalización 165 Por qué olvidar el tipo de un objeto
. ..... 166
El secreto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Un nuevo aná lisi s del método factoría Clase anidadas .............. . Clases dentro de interfaces .. . Acceso al exterior desde una clase múltiplemente anidada .. .
¿Para qué se usan las clases internas? Cierres y retro llamada .. Clases internas y marcos de control . .
..... 222
.224 .225
.... 226 .227 .229 . ... 230
Acoplamiento de las llamadas a métodos ....... 168
Cómo heredar de cIases internas ............ 236
Especi ficación del comportamiento correcto
168
¿Pueden sustituirse las clases internas? ....... 236
Amp1iabi1idad ....................... .
. 171
Clases internas locales .................... 238
Error: "sustitución" de métodos privare ElTor: campos y métodos static
Constructores y polimorfismo Orden de las llamadas a los constructores.
. . 173
Identificadores de una clase interna
.. 239
174
Resumen ......................... . ..... 239
... 175
11 Almacenamiento de objetos .. ... ... . ..... 241
176 ...... 177
Genéricos y contenedores seguros respecto al tipo ................. .... .. 242
Comportamiento de los métodos polimórficos dentro de los constructores . . .. ... . 181
Conceptos básicos ....... .... . .. .... ...... 244
Herencia y limpieza
Tipos de retorno covariantes Diseño de sistemas con herencia
.. . . . ..... 183 .... ... ... . 183
Sustitución y extensión .... . .. .. ....... . .... 184 Especialización e infonnación de tipos en tiempo de ejecución ..
Resumen ......... .
Adición de grupos de e lementos
Impresión de contenedores ................ 247 List ...................... . ............ 248 Iterator. . . . . . . . . . . . . . . . . .
.186
......... 187
9 Interfaces ... . ........ ... ..... .. .. . ... 189
.245
Listl te.-ator .
. 252 . .. . 254
LinkedList ...... .. .. ... . . .. . . ... .. .. .. . 255 Stack. . . . . . . . . . . . . . . . . . Set. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. ... 256 . .258
Clases abstractas y métodos abstractos ....... 189
Map ...... . .... . . . . . . . . ... . . .... .... ... 260
Interfaces. . . . . . . . . . .. . . . . . . .. . . . .
Queue.... ....... ........
. .. 192
Desacoplamiento completo .... . . . .. . . . .. . . 195 "Herencia múltiple" en Java ......... . ...... 199
PriorityQucue. . . . . . . . . . . . .
. ... 263 . 264
Comparación entre CoHection e Iterator ..... 266
xii Piensa en Java
La estructura foreach y los iteradores
... 268
El método basado en adaptadores. . . . .
Resumen. . . .
........
. .. 270
. ...... . 273
12 Tratamiento de errores mediante excepciones . ...• ........... .. 277 Conceptos ....................... . • . .... 277 . ........ 278
Excepciones básicas ...... . Argumentos de las excepciones. .
. 279
Detección de una excepción ................ 279 El bloque try . . .
. .. 280
Rutinas de tratamiento de excepciones ......... 280
Creación de nuestras propias excepciones Excepciones y registro. . . . . . . . . . . . . . . .
.. 281 . .. 283
La especificación de la excepción .... . .. .... 286 Cómo capturar una excepción
.. . . . ..... 286 .. 288
La traza de la pila
Regeneración de una excepción ....... . .. .... 289 Encadenamiento de excepciones ... .... . ..... 29 1
Excepciones estándar de Java .............. 294 Caso especial: RuntimeException .. . . . . . ..... 294
Realización de tareas de limpieza con finally .. 295 ¿Para qué sirve finally? .
...........
Utilización de finaU)' durante la ej ecución de la instrucción return. Un error: la excepción perdida
. .. 296
Restricciones de las excepciones ............ 301 ..303
Localización de excepciones ........•.•.... 307 Enfoques alternativos.
. .........•.... 308 .309
Historia .. Perspectivas ... . ......... .
.3 10
Paso de las excepciones a la consola
... 312
Conversión de las excepciones comprobadas ...... 3 13 en no comprobadas
Directrices relativas a las excepciones
. 3 15
Resumen ............................... 315
13 Cadenas de caracteres ... ............ ... 317 Cadenas de caracteres inmutables . . . . . . .
. . 317
Comparación entre la sobrecarga de '+' y
StringBuilder . . . . . . . . . . . . . . . .
Operaciones con cadenas de caracteres ....... 322 printfO·
. ... 324
. . . . . ... .. . ..... 324
..325
. ........ 326
Especificadores de formato ..
Conversiones posibles con Formattcr .
. 327
String.format().
. 329
Expresiones regulares
.........
........ • ..... 331
Fundamentos básicos. .
..........
. 33 1
Creación de expresiones regulares ... .... ...... 333
· . 335
Cuantificadores
. ... . 336
Pattcrn y Matcher
split().
· . 342 · . 343
Operaciones de sustitución
. .. 344
reset( ) . Expresiones regulares y E/S en Java
Análisis de la entrada .
.... 345
. ............. 346 .348
Delimitadores de Scanner Análisis con expresiones regulares.
. ..... 348
StringTokenizer. .............
. .... 349
Resumen.
. ..... .. . . . ....... . . .. .... 350
14 Información de tipos . ........... • ... . ... 351 La necesidad de RTTI. ........ . .
. ... 35 1 .353
El objeto Class ....
.357
Referencias de clase genéricas
.. 359
Nueva sintaxis de proyección ................ 36 1
Comprobación antes de una proyección.
.361
Utilización de literales de clase
· . 367 ..... 368
Instanceof dinámico .. Recuento recurs ivo. . . . . . . . . . . . . . . . . .
Factorías registradas . . . .
. 369
. ..... ........ 37 1
instanceof y equivalencia de clases .......... 373 Reflexión: infonnación de clases en tiempo de ejecución ............................ 374 Un extractor de métodos de clases
. 375
Proxies dinámicos. . . . . . .
. 378
Objetos nulos. . . . . . . . . . . . . . . . . . . . . . . . .. 38 1 .387
Objetos maqueta y stubs ..
.. 387
Interfaces e información de tipos
. .. .... 392
Resumen.
. 318
Recursión no intencionada .... ........ ..... 32 1 Formateo de la salida ....... .
. . . . . ....
La clase Formatter .....
Literales de clase ........ .
.. 299 .... 300
Constructores ................... .•. .
System.out.format( ) .... .. . . .... ... . . ... . . 324
15 Genéricos ..... .. ..................... 393 Co mparación con
c++ .................... 394
Genéricos simples ...... .... . . . .• .•.•.. Una biblioteca de tuplas
.394 .396
Contenido xiii
..... 398
Una clase que implementa una piJa
.. 399
RandornList ...
lnterfaces genéricas Métodos genéricos ..
... ........... 403
Compensación de la carencia de tipos latentes .. 467 Reflexión
............
. 467
...... 404
Aplicación de un método a una secuencia .. . .. 468
. .... 405
¿Qué pasa cuando no disponemos de la intcrfaz correcta? . . . . . . . . . . . . 471
Varargs y métodos genéricos.
Un método genérico para utilizar con generadores . ............. 406 Un gene rador de propósito general .
. .... 407
Simplificación del uso de las tuplas
..... 408
.... ... ............. 409
Clases internas anónimas . . . . ......... 412 Construcción de modelos complejos ......... 4 13 ............. 4 15 El misterio del borrado . .. ............ 417
Compatibi lidad de la migración.
.419
El problema del borrado de tipos . .... . . . . .
.4 19
.... 42 1
El efecto de los límites
Simulación de tipos latentes mediante adaptadores472
Utilizando los objetos de función como estrategias .................. 474 Resumen: ¿realmente es tan malo el ..479 mecanismo de proyección? ..... . Lecturas adicionales ..
..........
. . 481
16 Matrices ...... .. .. .................. . . 483 Por qué las matrices son especiales
. . 483
Las matrices son objetos de primera clase .... 484 Devolución de una matriz ...
. .... 487
~l
.. 488 Matrices multidimensionales .......... .... •.... 491 Matrices y genéricos Creación de datos de prueba .......... 493 Array,.fillO . . . . . . . . . . . . . . . ... 493 Generadores de datos ............. 494
........... .. ..... ..... 434
Creación de matrices a partir de generadores . ... 498
Compensación del borrado de tipos
.... 424 ..425
Creación dc instanc ias de tipos. Matrices de genéricos L~i~s ... Comodines
. .462
Tipos latentes . .......................... 464
de tipo
La técnica usada en C++ .
. .... 461
.......... 399
Aprovechamiento de la inferencia del argumento
Una utilidad Set
Utilizac ión del patrón Decorador .. Mixins con proxies dinámicos.
...............
. .. 42 7
. ......................
¿Hasta qué punto es inteligente el compilador? .. 436 Contravarianza ....
............... 438
.. 440
Comodines no limitados Conversión de captura.
Problemas .....
.. 507
Búsquedas en una matriz ordenada
.. 508
..... 447
Sobrecarga.
. .. 449
Secuestro de una interfaz por parte de la clase base
Autolimitación
. . .. ... 503
Ordenación de una matriz
Implementación de interfaces parametrizadas . ... 447
Covarianza de argumentos.
Comparación de matrices . ....... . Comparaciones de elementos de matriz
. .. 445
Resumen .................... .
..... 450
... 504
....... 509
17 Análisis detallado de los contenedores . ... 513 Taxonomía completa de los contenedores ..... 513 Rclleno de contenedores.
. 449
.. ......... 450
Gcnéricos curiosamente recurrentes ..
. ... 502
. ... 445
de tipo. . .
Tipos autolimitados ........ .
. . .. 502
Copia en una matriz
. .... 444
No pueden usarse primitivas corno parámetros
Proyecciones de tipos y advertencias
Utilidades para matrices
. . 514
Una solución basada en generador .... ... ..... 515
. . 5 16
Generadores de mapas .
... 519
Utilización de clases abstractas
.... 451
Funcionalidad de las colecciones .. .. . . ..... 525
. .. 453
Operaciones opcionales ............. .. .... 528
Seguridad dinámica de los tipos ... . . ....... 456
Operaciones no soportadas
. ...... .. ..... 529
Excepciones
.. 457
Funcionalidad de List ....
. ......... 530
Mixins ........... .
.. 458
Conjuntos y orden de
Mixins en C++.
............. 459
Mezclado de clases utilizando interfaces.. . .... 460
SortedSet.
a~nacenam iento.
.........
. . . .. . 533 . 536
Colas ... ............• . •.•.... . . . . •.... 537
xiv Piensa en Java
. . . .. . . . .
· .... 538
Colas dobles. ....... . .. .. ........
Colas con prioridad. .
. 539
Mapas ...... .. ..... . ..... . . . ........... 540 Rendimiento
. 541
SortedMap . .
.544
LinkedHashMap.
. .... 545
Almacenamiento y códigos hash ..... . • . .. . . 545 Funcionamiento de hashCode( )
.. 548
Orígenes y destinos de los datos
. . . 601
Modificación del comportamiento de los flujos
dc datos.
...........
. ... 601
Clases no modificadas
.... 602
RandomAccessFile . . . .. . ............... 602 Utilización típica de los flujos de E/S Archivo de entrada con buffer . Entrada desde memoria.
. ... 603 . .. .. .... 603
. .......... .. .... 604
Entrada de memoria formateada .... .. .. . ..... 604
Mejora de la velocidad con el almacenamiento hash . . . . . . . . ........ . .. 550
Salida básica a archivo ..... . .... . .... . .... 605
Sustituc ión de hashCodeQ ......... .
.... . 553
Almacenamiento y recuperación de datos ...... 607
.... 558
Lectura y escritura de archivos de acceso aleatorio ..
Selección de una implementación
Un marco de trabajo para pruebas de rendimiento 558 Selección entre listas ....................... 561 Peligros asociados a las micropruebas de rendimiento .... ...... . . .
.566
Lectura de la entrada estándar ............... 613
....................... 572
Cambio de System.out a un objeto PrintWriter . .. 613
.. 575
Almacenamiento de referencias. . . . . . . . .. . . . . ..
. . 578 . . . . 580
Contenedores Java 1.0/ 1.1 ...... .. . ........ 581 Vector y Enumeration . . . . . . . . . . . . . .
BitSet.
Resumen. . . .
. .... 58 1
. . .. ..... . ............. . ... 582 . . .. ... . .. . . . ... 582
. . . . . . . .. . . . . . . . . . . . . ..584
. ........ . . . .. . ..... 585
18 Entrada/salida . ....... . ................ 587 La clase File ...... .. ...... . Una utilidad para listados de directorio. Utilidades de directorio .......... . Búsqueda y creación de directorios
Redireccionamiento de la E/S estándar
Control de procesos · .... 576
Sincronización de una colección o un mapa .... 577
Stack
. . . . . . ..... 612
E/S estándar ..................... . ...... 6 12
Creación de colecc iones o mapas no modificables
Hashtable . .
Lectura de archivos binarios . . .
... 567
Ordenaciones y búsquedas en las listas
WeakHashMap.
. ....... .. .... 609
Utilidades de lectura y escritura de archivos . . . 609
.. ... 569
Selección de un tipo de conjunto Selección de un tipo de mapa
Utilidades
Flujos de datos canalizados.
.. 608
· ... 587 . ... 587
. ... 590 ..... 594
... 613
............
. . 614
Los paquetes new ......• ...... .... ...... 616 Conversión de datos
. 618
Extracción de primitivas ..
..... . . ........ 62 1
Buffers utilizados como vistas
... 622
Manipulación de datos con buffers .... . • . .
.. 625
Deta lles acerca de los buffers
... 625
Archivos mapeados en memoria.
. .. 629
Bloqueo de archivos.
. . 632
Compresión .................... . . . •. . ... 634 Compresión simp le con GZIP ..
. . 635
Almacenamiento de múltiples archivos con Zip . . 635
Archivos Java (lAR) . . Serialización de objetos Localización de la clase Control en la serialización ..
. ................ 637
.. . . .... ... 639 ..... 642 .642
...................... 596
Utilización de la persistencia ...... ..... . . . . .. 649
Tipos de InputStream ..... .. ... . . . . . . , .... . 597
XML .............................. .. .. 654
Tipos de OutputStream ..................... 598
Preferencias .... . . . .......... . . .. . ....... 656
Entrada y salida
Adición de atributos e interfaces útil es Lectura de un flujo InputStream con FiltcrInputStream .
. 598
Resumen . . ......... • .•.• .............. . 658
19 Tipos enumerados .......... . .... . ..... 659 . . 599
Escritura de un flujo OutputStream con FilterOutputStream ............. . ...... 599
Lectores y escritores ....... . .. , . . ........ 600
Características básicas de las enumeraciones .. 659 Utilización de importaciones estáticas con las enumeraciones ..... .. . ... ... 660
Contenido xv Adición de métodos a lma enumeración ...... 661 Sustitución de los métodos de una enumeración . 662
Mejora del diseño del código .. ..... ,..
Conceptos básicos sobre hebras .. , , .
Enumeraciones en las instrucciones switch ... 662
Definición de las tarcas .
El misterio de values( )
La clase Thread
.......
Implementa, no hereda .....
o
•
o
o
o
. .. 663
o
o
•••••
o
Selección aleatoria ... . ......... . , . Utilización de interfaces con propósitos de organización .. ... . . o
•
••
o
o
o
o
665
•••
667
•
. . 73 0
o •••
o
o
•
•
•
671
........ 672
ylétodos específicos de constante ............ 673 Cadena de responsabilidad en las enumeraciones 676 . 680
Máquinas de estado con enumeraciones. .
Despacho múl tiple .. . ................... 684
731 .732
Utilización de Executor
.... . 734
Producción de valores de retomo de las tareas . . 736 , ..... .... . . . ... . 737
Prioridad.
· . 738
Cesión del control .
Uti lización de EnurnMap
••
. ....... . .. .... 731
Cómo donnir una tarea .
•••••••••••
Utilización de EnurnSet en lugar de ........ indicadores
••
.. 666
o
. .. 740
Hebras demonio.
. , .. 740
Variaciones de código
.. 744
Terminología .
. .. , 748
Absorción de una hebra
,748
Creación de interfaces de usuario de rcspuesta rápida . . . .. ......... 750
Cómo despachar con enumeraciones ........... 686
Grupos dc hebras.
· , 751
Utilización de méwdos específicos de constante. 688
Captura dc excepciones ... . ... .. . .
· . 751
Cómo despachar con mapas En urnMap.
..690
Utilización de una matriz 2-D
.690
Reswnen.
.... 691
20 Anotaciones
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
Sintaxis básici:t
o
o
693
... 694
Definición de anotaciones. .
. .. 694
........ ,
Meta-anotaciones
... 695
Escritura de procesadores de anotaciones , .... 696 Elementos de anotación. Restricciones de valor predeterminado.
Soluciones alternativas
. , . 709
Utilización de @Unit con genéricos
.. 716
No hace falta ningún "agrupamiento"
.. 717
Implementación de@Unit ,., ...... . . . . . .... 717 El iminación del código de prueba .
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o •••• ••
o
o
o
o
o
Clases atómicas . ..
. . ... 764
Secciones críticas
. .. 770
Almacenamiento local de las hebras
... 77 1
o
Interrupción.
••
••••
.784
waitO y notifyAIIO ... , ... .
o
o
,
.. , 784
•
.. ,788
notifyO y notifyAIIO, Productores y consumidores.
. .... 791
Productores-consumidores y colas
.796
Utilización de canalizaciones para la E/S entre tareas . . . . . . . . . . . . . . . . . . . . . . . .
Interbloqueo . . . . . . CountDownLatch .. CyclicBarrier DelayQueue. PriorityBlockingQueue .
772
... 782
Cooperación en tre tareas
Nuevos componentes de biblioteca
. . . ....... . 728
0
. ......... , ... .. . 776
724
727
•
Comprobación de la existencia de una interrupción .
. 723
Las múltiples caras de la concurrencia .. , , . , , . 728 Ejecuc ión más rápida
, , 765
Sincron ización sobre otros objetos.
.. 775
Uti lización del patrón de diseño Visitante con apt ..... , , , , , , , , .. ........ . ... . . 706
o
. . .. . 760
Tcnuinación durantc el bloqueo
Uti lización de apt para procesar anotaciones .. 703
21 Concurrencia
Atomicidad y volatilidad.
.. 772
. 700
. .. ,
. . 756
El jardín ornamental
Implementación del procesador
Resumen. . . . . . . . .
.... 754
Resolución de la contienda por los recursos compartidos.. ...................
Terminación de tareas
.... . . 700
Pruebas unitarias basadas en anotaciones
Acceso inapropiado a los recursos
. . . 697
.. 700
Las anotaciones no soportan la herencia
.753
. . 697
... ...... 697
Generación de archivos externos
Compartición de recursos
, .....
. , 800
801
o ••••
,805 .. 805 .... .. . 807 .... 809
. • ,.,., . 0 ,., • ... .
.
811
xvi Piensa en Java
El controlador de invernadero implementado con ScbeduledExecutor . ........... ..... 8 14
Tableros con fichas . . . . . . . . . . . . . . . .
Semaphore
Menús. .
.......... . .. .. .. • ..... 8 17
Exchanger ...... .. . . . . . . .. ..... . . .. ...... 820
Simulación . . . . . . . . .
. .. .. . . •.• . • ... .. 82 1
Simulación de un cajero
.... 82 1
Simulación de un restaurante.
...826
. .. . 887
Recuadros de mensaje . ... . ............ ..... 888 . .... .. . . . ........ . . . 890
Menús emergentes.
894
Dibujo ..... .
..895
Cuadros de diálogo ...
. .. .... .... 898
Cuadros de diálogo de archivos
.... 901
. .... ... . . ..... 829
HTML en los componentes Swing
.... 902
Optimización del rendimiento .............. 834
Deslizadores y barras de progreso
..... 903
Selección del aspecto y del estilo
. 904
Distribución de trabajo . ...
Comparación de las tecnologías mutex
.. 834
Contenedores libres de bloqueos ..
. .... 841
Árboles, tablas y portapapeles . . . .
. ... . . .. 906
. . . . . . . .. .847
JNLP Y Java Web Start .................... 906
. ................ 848
Concurrencia y Swing . ....... . . . . . . .. . .... 9 10
Objetos activos ................. . .•.•.... 850
Tareas de larga duración .... . ....... .. ...... 910
Resumen ....................•. . . . ...... 853
Hebras visua les. . . . ............... 916 Programación visual y componentes JavaBean . 9 18
Bloqueo optimista. ReadWriteLocks . . . .
Lecturas adicionales
. ...... . . . .. . ..... 855
22 Interfaces gráficasde usuario ... .. . . . . ... 857
¿Qué es un componenle JavaBean? . .... . .. .. . 919
Applets . . ........ .................. ..... 858
Extracción de la información Beanlnfo con lntrospector ...
Fundamentos de Swing ..... ... . .......... 859
Una Bean más sofisticada . ...
...920
.. ... .. 924
Sincronización en JavaBeans
. . . . 927
Definición de un botón ...........•. •..... 862
Empaquetado de una Bean . ......... .
. ... 930
Captura de un suceso ............... . ..... 862 Áreas de texto ....... .... .....•. . .•..... 864
Más información sobre componentes Bean. .
Un entorno de visuali zación
. .. .. ....... 86 1
Soporte avanzado de componentes Bean . .... . . 931 .932
Control de la disposición ........•......... 865
Alternativas a Swing . ................. .... 932
BorderLayout . ............. .. ............ 866
Construcción de clientes web Flash con Flex .. 932
"lowLayout. .. .. . .. .. . .. . . . .. .. .. .. .. .
. 867
G ridLayout . .
.867
Grid.BagLayout. ..
.. . .... .. .... ..... 868
Helio, Flex ............................... 932 Compilación de MXML. . . .
Posicionamiento absoluto
........ .. .... ... 868
Contenedores y controles. . .
BoxLayout. ........ .
. ... .. .• . . . . ..... 868
Efectos y estil os
¿Cuál es la mejor solución? . ....... ... ..... 868 El modelo de sucesos de Swing ........... 868 Tipos de sucesos y de escuchas
... 869
Control de múltiples sucesos .....
.874
Una selección de componentes Swing ....... 876 Botones.
. .... 876
. ..... 933
MXML y ActionScript . .................... . 934 . . ..... . 935
.936
Sucesos . .
. . ...... ... . 93 7
Conexión con Java.
........... 93 7 Modelos de datos y acoplamiento de datos .. . . . . 939 Construcción e implantac ión de aplicaciones . ... 940
Creación de aplicaciones SWT ........ Instalación de SWT . . . . . . . . . . . . . . . . . . . .
.. .. .... .
.941 . 94 1 .. .. 94 1
Iconos . ........ . . . . . . . . . .. . . .. . . . . .
.878
Helio, SWT
Sugerencias
.880
Eliminación del código redundante. . . .
Campos de tex to
.880
Menús . ................. ....... ....... ... 945
Bordes
.88 1
Paneles con fichas, botones y sucesos events . ... 946
.... 882
Gráficos . ............ .
Casill as de verificación
... 883
Concurrencia en SWT ...
Botones de opción. . . .
. ...... 884
Un mini-editor.
Cuadros combi nados (listas desplegables) ...... 885 Cuadros de lista.
. ..... 886
. 944
. ........ .. 949
.. 950 ¿S WT o Swing? . . . . . . .. . . .. . . . . ......... 952 Resumen ......... . . ... . . . . . . . . . ... . .... 952 Recursos .... . .. ... . . . . .. ... .. . . ... . . ... . 953
Contenido xvii
A Suplementos ..................... . .... 955
B Recursos ...... .. .......... . .... .... .. 959
Suplementos descargables ......... .
.... 955
Software ............. .... ... ... .
... 959
Thinking in C: fundamentos para Java ..
. . . . 955
Editores y entornos !DE ..... . ... ... .
... 959
Libros ........... .
... 959
Análisis y diseño . ..
.. ... 960
Python . ........ .
.. .. 962
Seminario ThinJ..ing in Java . . . . . . . . . . . . . . 955 Seminario CD Hallds-On Java .... 956 Seminario Thinking in Objects .....•... .... 956 Thinking in Entelprise Java . ....... . . .. ... . 956 Thinking in Patlerns (con Java) ............. 957 Seminario Thinking in Patterns ... 957 Consultoría y revisión de diseño .... 957
Mi propia lista de libros
........
. ... 962
Indice ............ .. .. ... . . . .... . .... . 963
Prefacio Originalmente, me enfrenté a Java como si fuera "simplemente otro lenguaje más de programación", lo cual es cierto en muchos sentidos. Pero, a medida que fue pasando el tiempo y lo fui estudiando con mayor detalle, comencé a ver que el objetivo fundamental de este lenguaje era distinto de los demás lenguajes que había visto hasta el momento . La tarea de programación se basa en gestionar la complejidad: la complejidad del problema que se quiere resolver, sumada a la complej idad de la máquina en la cual hay que resolverlo. Debido a esta complejidad, la mayoría de los proyectos de programación terminan fallando. A pesar de lo cual, de todos los lenguajes de programación que conozco, casi ninguno de ellos había adoptado como principal objetivo de diseño resolver la complej idad inherente al desarrollo y el mantenimiento de los programas. 1 Por supuesto, muchas decisiones del diseño de lenguajes se realizan teniendo presente esa complejidad, pero siempre termina por considerarse esencial introducir otros problemas dentro del conj unto de los objetivos. Inevitablemente, son estos otros problemas los que hacen que los programadores terminen no pudiendo cumplir el objetivo principalmente con esos lenguajes. Por ejemplo, C++ tenía que ser compatible en sentido descendente con C (para permitir una fácil migración a los programadores de C), además de ser un lenguaje eficiente. Ambos objetivos resultan muy útiles y explican parte del éxito de C++, pero también añaden un grado adicional de complejidad que impide a algunos proyectos finalizar (por supuesto, podemos echar la culpa a los programadores y a los gestores, pero si un lenguaje puede servir de ayuda detectando los errores que cometemos, ¿por qué no utilizar esa posibilidad?). Otro ejemplo, Visual BASIC (VB) estaba ligado a BASIC, que no había sido diseñado para ser un lenguaje extensible, por lo que todas las extensiones añadidas a VB han dado como resultado una sintaxis verdaderamente inmanejable. Ped es compatible en sentido descendente con awk, sed, grep y otras herramientas Unix a las que pretendía sustituir, y como resultado, se le acusa a menudo de generar "código de sólo escritura" (es decir, después de pasado un tiempo se vuelve completamente ilegible). Por otro lado, C++, VB, Ped y otros lenguajes como Smalltalk han centrado algo de esfuerzo de diseño en la cuestión de la complejidad, y como resultado, ha tenido un gran éxito a la hora de resolver ciertos tipos de problemas.
Lo que más me ha impresionado cuando he llegado a entender el lenguaje Java es que dentro del conjunto de objetivos de diseño establecido por Sun, parece que se hubiera decidido tratar de reducir la complejidad para el programador. Como si quienes marcaron esos obj etivos hubieran dicho: "Tratemos de reducir el tiempo y la dificultad necesarios para generar código robusto" . Al principio, este objetivo daba como resultado un código que no se ejecutaba especialmente rápido (aunque esto ha mejorado a lo largo del tiempo), pero desde luego ha permitido reducir considerablemente el tiempo de desarrollo, que es inferior en un 50 por ciento o incluso más al tiempo necesario para crear un programa en c ++ equivalente. Simplemente por esto, ya podemos ahorrar cantidades enormes de tiempo y de dinero, pero Java no se detiene ahí, sino que trata de hacer transparentes muchas de las complejas tareas que han llegado a ser importantes en el mundo de la programación, como la utilización de múltiples hebras o la programación de red, empleando para conseguir esa transparencia una serie de características del lenguaje y de bibliotecas preprogramadas que pueden hacer que muchas tareas lleguen a resultar sencillas. Finalmente, Java aborda algunos problemas realmente complejos: programas interplataforma, cambios de código dinámicos e incluso cuestiones de seguridad, todos los cuales representan problemas de una complejidad tal que pueden hacer fracasar proyectos completos de programación. Por tanto, a pesar de los problemas de prestaciones, las oportunidades que Java nos proporciona son inmensas, ya que puede incrementar significativamente nuestra productividad como programadores. Java incrementa el ancho de banda de comunicación entre las personas en todos los sentidos: a la hora de crear los programas, a la hora de trabajar en grupo, a la hora de construir interfaces para comunicarse con los usuarios, a la hora de I
Sin embargo, creo que el lenguaje Python es el que más se acerca a ese objetivo. Consulte www.Python. OIg.
xx Piensa en Java
ejecutar los programas en diferentes tipos de máquinas y a la hora de escribir con sencillez aplicaciones que se comuniquen a través de Internet. En mi opinión, los resultados de la revolución de las comunicaciones no se percibirán a partir de los efectos de desplazar grandes cantidades de bits de un sitio a otro, sino que seremos conscientes de la verdadera revolución a medida que veamos cómo podemos comunicamos con los demás de manera más sencilla, tanto en comunicaciones de persona a persona, como en grupos repartidos por todo el mundo. Algunos sugieren que la siguiente revolución será la formación de una especie de mente global derivada de la interconexión de un número suficiente de personas. No sé si Java llegará a ser la herramienta que fomente dicha revolución, pero esa posibilidad me ha hecho sentir, al menos, que estoy haciendo algo importante al tratar de enseñar este lenguaje.
Java SES Y SE6 Esta edición del libro aprovecha en buena medida las mejoras realizadas al lenguaje Java en 10 que Sun originalmente denominó JDK 1.5 Y cambió posteriormente a JDK5 o J2SE5 . Posteriormente, la empresa eliminó el obsoleto "2" y cambió el nombre a Java SE5. Muchos de los cambios en el lenguaj e Java SE5 fueron decididos para mejorar la experiencia de uso del programador. Como veremos, los diseñadores del lenguaje Java no obtuvieron un completo éxito en esta tarea, pero en general dieron pasos muy significativos en la dirección correcta. Uno de los objetivos más importantes de esta edición es absorber de manera completa las mejoras introducidas por Java SE5/6, presentarlas y emplearlas a lo largo de todo el texto. Esto quiere decir que en esta edición se ha tomado la dura decisión de hacer el texto únicamente compatible con Java SE5/6, por lo que buena parte del código del libro no puede compilarse con las versiones anteriores de Java; el sistema de generación de código dará errores y se detendrá si se intenta efectuar esa compilación. A pesar de todo, creo que los beneficios de este enfoque compensan el riesgo asociado a dicha decisión. Si el lector prefiere por algún motivo las versiones anteriores de Java, se puede descargar el texto de las versiones anteriores de este libro (en inglés) en la dirección www.MindView.net. Por diversas razones, la edición actual del libro no está en formato electrónico gratuito, sino que sólo pueden descargarse las ediciones anteriores.
Java SE6 La redacción de este libro ha sido, en sí misma, un proyecto de proporciones colosales y al que ha habido que dedicar muchísimo tiempo. Y antes de que el libro fuera publicado, la versión Java SE6 (cuyo nombre en clave es mustang) apareció en versión beta. Aunque hay unos cuantos cambios de menor importancia en Java SE6 que mejoran algunos de los ejemplos incluidos en el libro, el tratamiento de Java SE6 no ha afectado en gran medida al contenido del texto; las principales mejoras de la nueva versión se centran en el anmento de la velocidad y en determinadas funcionalidades de biblioteca que caían fuera del alcance del texto. El código incluido en el libro ha sido comprobado con una de las primeras versiones comerciales de Java SE6, por lo que no creo que vayan a producirse cambios que afecten al contenido del texto. Si hubiera algún cambio importante a la hora de lanzar oficialmente JavaSE6, ese cambio se verá reflejado en el código fuente del libro, que puede descargarse desde www.MindView.net. En la portada del libro se indica que este texto es para "Java SE5/6", lo que significa "escrito para Java SE5 teniendo en cuenta los significativos cambios que dicha versión ha introducido en el lenguaje, pero siendo el texto igualmente aplicable a Java SE6".
La cuarta edición La principal satisfacción a la hora de realizar una nueva edición de un libro es la de poder "corregir" el texto, aplicando todo aquello que he podido aprender desde que la última edición viera la luz. A menudo, estas lecciones son derivadas de esa frase que dice: "Aprender es aquello que conseguimos cuando no conseguimos lo que queremos", y escribir una nueva edición del libro constituye siempre una oportunidad de corregir errores o hacer más amena la lectura. Asimismo, a la hora de abordar una nueva edición vienen a la mente nuevas ideas fascinantes y la posibilidad de cometer nuevos errores se ve más que compensada por el placer de descubrir nuevas cosas y la capacidad de expresar las ideas de una forma más adecuada.
Prefacio xxi
Asimismo, siempre se tiene presente, en el fondo de la mente, ese desafio de escribir un libro que los poseedores de las ediciones anteriores estén dispuestos a comprar. Ese desafio me anima siempre a mejorar, reescribir y reorganizar todo lo que puedo, con el fin de que el libro constituya una experiencia nueva y valiosa para los lectores más fieles.
Cambios El CD-ROM que se había incluido tradicionalmente como parte del libro no ha sido incluido en esta edición. La parte esencial de dicho CD, el seminario multimedia Thinking in e (creado para MindView por Chuck Allison), está ahora disponible como presentación Flash descargable. El objetivo de dicho seminario consiste en preparar a aquellos que no estén lo suficientemente familiarizados con la sintaxis de C, de manera que puedan comprender mejor el material presentado en este libro. Aunque en dos de los capítulos del libro se cubre en buena medida la sintaxis a un nivel introductorio, puede que no sean suficientes para aquellas personas que carezcan de los conocimientos previos adecuados, y la presentación Thinking in e trata de ayudar a dichas personas a alcanzar el nivel necesario. El capítulo dedicado a la concurrencia, que antes llevaba por título "Programación multihebra", ha sido completamente reescrito con el fin de adaptarlo a los cambios principales en las bibliotecas de concurrencia de Java SE5, pero sigue proporcionando información básica sobre las ideas fundamentales en las que la concurrencia se apoya. Sin esas ideas fundamentales, resulta dificil comprender otras cuestiones más complejas relacionadas con la programación multihebra. He invertido muchos meses en esta tarea, inmerso en ese mundo denominado "concurrencia" y el resultado final es que el capítulo no sólo proporciona los fundamentos del tema sino que también se aventura en otros territorios más novedosos. Existe un nuevo capítulo dedicado a cada una de las principales características nuevas del lenguaje Java SE5, y el resto de las nuevas características han sido reflejadas en las modificaciones realizadas sobre el material existente. Debido al estudio continuado que realizo de los patrones de diseño, también se han introducido en todo el libro nuevos patrones. El libro ha sufrido una considerable reorganización. Buena parte de los cambios se deben a razones pedagógicas, junto con la perfección de que quizá mi concepto de "capítulo" necesitaba ser revisado. Adicionalmente, siempre he tendido a creer que un tema tenía que tener "la suficiente envergadura" para justificar el dedicarle un capítulo. Pero luego he visto, especialmente a la hora de enseñar los patrones de diseño, que las personas que asistían a los seminarios obtenían mejores resultados si se presentaba un único patrón y a continuación se hacía, inmediatamente, un ejercicio, incluso si eso significaba que yo sólo hablara durante un breve período de tiempo (asimismo, descubrí que esta nueva estructura era más agradable para el profesor). Por tanto, en esta versión del libro he tratado de descomponer los capítulos según los temas, sin preocuparme de la longitud final de cada capítulo. Creo que el resultado representa una auténtica mejora. También he llegado a comprender la enorme importancia que tiene el tema de las pruebas de código. Sin un marco de pruebas predefinido, con una serie de pruebas que se ejecuten cada vez que se construya el sistema, no hay forma de saber si el código es fiable o no. Para conseguir este objetivo en el libro, he creado un marco de pruebas que permite mostrar y validar la salida de cada programa (dicho marco está escrito en Python, y puede descargarse en www.MindVíew.net. El tema de las pruebas, en general, se trata en el suplemento disponible en http://www.MindVíew.net/Books/BetterJava. que presenta lo que creo que son capacidades fundamentales que todos los programadores deberían tener como parte de sus conocimientos básicos. Además, he repasado cada uno de los ejemplos del libro preguntándome a mí mismo: "¿Por qué lo hice de esta manera?" . En la mayoría de los casos, he realizado algunas modificaciones y mejoras, tanto para hacer los ejemplos más coherentes entre sí, como para demostrar lo que considero que son las reglas prácticas de programación en Java, (al menos dentro de los límites de un texto introductorio). Para muchos de los ejemplos existentes, se ha realizado un rediseño y una nueva implementación con cambios significativos con respecto a las versiones anteriores. Aquellos ejemplos que me parecía que ya no tenían sentido han sido eliminados y se han añadido, asimismo, nuevos ejemplos. Los lectores de las ediciones anteriores han hecho numerosísimos comentarios muy pertinentes, lo que me llena de satisfacción. Sin embargo, de vez en cuando también me llegan algunas quejas y, por alguna razón, tilla de las más frecuentes es que "este libro es demasiado voluminoso". En mi opinión, si la única queja es que este libro tiene "demasiadas páginas", creo que el resultado global es satisfactorio (se me viene a la mente el comentario de aquel emperador de Austria que se quejaba de la obra de Mozart diciendo que tenía "demasiadas notas"; por supuesto, no trato en absoluto de compararme con Mozart). Además, debo suponer que ese tipo de quejas proceden de personas que todavía no han llegado a familiarizarse con la enorme variedad de características del propio lenguaje Java y que no han tenido ocasión de consultar el resto de libros dedicados a este tema. De todos modos, una de las cosas que he tratado de hacer en esta edición es recortar aquellas partes
xxii Piensa en Java
que han llegado a ser obsoletas, o al menos, no esenciales. En general, se ha repasado todo el texto eliminando lo que ya había dejado de ser necesario, incluyendo los cambios pertinentes y mejorando el contenido de la mejor manera posible. No me importa demasiado eliminar algunas partes, porque el material original cOlTespondiente continúa estando en el sitio web (www.MindVíew.net).graciasa laversióndescargabledelastresprimerasedicionesdel libro.Asimismo. el lector tiene a su disposición material adicional en suplementos descargables de esta edición. En cualquier caso, para aquellos lectores que sigan considerando excesivo el tamaño del libro les pido disculpas. Lo crean o no, he hecho cuanto estaba en mi mano para que ese tamaño fuera el menor posible.
Sobre el diseño de la cubierta La cubierta del libro está inspirada por el movimiento American Arts & Crafls Movement que comenzó poco antes del cambio de siglo y alcanzó su cenit entre 1900 y 1920. Comenzó en Inglaterra como reacción a la producción en masa de la revolución industrial y al estilo altamente ornamental de la época victoriana. Arts & Crafls enfatizaba el diseño con formas naturales, como en el movimiento art nOllveau, como el trabajo manual y la importancia del artesano, sin por ello renunciar al uso de herramientas modernas. Existen muchos ecos con la situación que vivimos hoy en día: el cambio de siglo, la evolución desde los rudimentarios comienzos de la revolución informática hacia algo más refinado y significativo y el énfasis en la artesanía del software en lugar de en su simple manufactura. La concepción de Java tiene mucho que ver con este punto de vista. Es un intento de elevar al programador por encima del sistema operativo, para transformarlo en un "artesano del software". Tanto el autor de este libro como el diseñador de la cubierta (que son amigos desde la infancia) han encontrado inspiración en este movimiento, ambos poseemos muebles, lámparas y otros objetos originales de este período o inspirados en el mismo. El otro tema de la cubierta sugiere una vitrina coleccionista que un naturalista podría emplear para mostrar los especírnenes de insectos que ha preservado. Estos insectos son objetos situados dentro de los objetos compartimento. Los objetos compartimento están a su vez, colocados dentro del "objeto cubierta", lo que ilustra el concepto de agregación dentro de la programación orientada a objetos. Por supuesto, cualquier programador de habla inglesa efectuará enseguida entre los insectos "bugs" y los errores de programación (también bugs). Aquí, esos insectos/errores han sido capturados y presumiblemente muertos en un tarro y confinados fmalmente dentro de una vitrina, con lo que tratamos de sugerir la habilidad que Java tiene para encontrar, mostrar y corregir los errores (habilidad que constituye uno de sus más potentes atributos). En esta edición, yo me he encargado de la acuarela que puede verse como fondo de la cubierta.
Agradecimientos En primer lugar, gracias a todos los colegas que han trabajo conmigo a la hora de impartir seminarios, realizar labores de consultoría y desarrollar proyectos pedagógicos: Dave Bartlett, Bill Venners, Chuck AIlison, Jeremy Meyer y Jamie King. Agradezco la paciencia que mostráis mientras continúo tratando de desarrollar el mejor modelo para que una serie de personas independientes como nosotros puedan continuar trabajando juntos. Recientemente, y gracias sin duda a Internet, he tenido la oportunidad de relacionarme con un número sorprendentemente grande de personas que me ayudan en mi trabajo, usualmente trabajando desde sus propias oficinas. En el pasado, yo tendría que haber adquirido o alquilado una gran oficina para que todas estas personas pudieran trabajar, pero gracias a Internet, a los servicios de mensajeros y al teléfono, ahora puedo contar con su ayuda sin esos costes adicionales. Dentro de mis intentos por aprender a "trabajar eficazmente con los demás", todos vosotros me habéis ayudado enormemente y espero poder continuar aprendiendo a mejorar mi trabajo gracias a los esfuerzos de otros. La ayuda de Paula Steuer ha sido valiosísima a la hora de tomar mis poco inteligentes prácticas empresariales y transformarlas en algo razonable (gracias por ayudarme cuando no quiero encargarme de algo concreto, Paula). Jonathan Wilcox, Esq. , se encargó de revisar la estructura de mi empresa y de eliminar cualquier piedra que pudiera tener un posible escorpión, haciéndonos marchar disciplinadamente a través del proceso de poner todo en orden desde el punto de vista legal, gracias por tu minuciosidad y tu persistencia. Sharlynn Cobaugh ha llegado a convertirse en una auténtica experta en edición de sonido y ha sido una de las personas esenciales a la hora de crear los cursos de formación multimedia, además de ayudar en la resolución de muchos otros problemas. Gracias por la perseverancia que has demostrado a la hora de enfrentarte con problemas informáticos complejos. La gente de Amaio en Praga también ha sido de gran ayuda en numerosos proyectos. Daniel Will-Harris fue mi primera fuen-
Prefacio xxiii te de inspiración en lo que respecta al proyecto de trabajo a través de Internet y también ha sido imprescindible, por supuesto, en todas las soluciones de diseño gráfico que he desarrollado. A lo largo de los años, a través de sus conferencias y seminarios, Gerald Weinberg se ha convertido en mi entrenador y mentor extraoficial, por lo que le estoy enormemente agradecido. Ervin Varga ha proporcionado numerosas correcciones técnicas para la cuarta edición, aunque también hay otras personas que han ayudado en esta tarea, con diversos capítulos y ej emplos. Ervin ha sido el revisor técnico principal del libro y también se encargó de escribir la guía de soluciones para la cuarta edición. Los errores detectados por Ervin y las mejoras que él ha introducido en el libro han permitido redondear el texto. Su minuciosidad y su atención al detalle resultan sorprendentes y es, con mucho, el mejor revisor técnico que he tenido. Muchas gracias, Ervin. Mi weblog en la página www.Artima.com de Bill Venners también ha resultado de gran ayuda a la hora de verificar determinadas ideas. Gracias a los lectores que me han ayudado a aclarar los conceptos enviando sus comentarios; entre esos lectores debo citar a James Watson, Howard Lovatt, Michael Barker, y a muchos otros que no menciono por falta de espacio, en particular a aquellos que me han ayudado en el tema de los genéricos. Gracias a Mark Welsh por su ayuda continuada. Evan Cofsky continúa siendo de una gran ayuda, al conocer de memoria todos los arcanos detalles relativos a la configuración y mantenimiento del servidor web basados en Linux, así como a la hora de mantener optimizado y protegido el servidor MindView. Gracias especiales a mi nuevo amigo el café, que ha permitido aumentar enormemente el entusiasmo por el proyecto. Camp4 Coffee en Crested Butte, Colorado, ha llegado a ser el lugar de reunión normal cada vez que alguien venía a los seminarios de MindView y proporciona el mejor catering que he visto para los descansos en el seminario. Gracias a mi colega Al Smith por crear ese café y por convertirlo en un lugar tan extraordinario, que ayuda a hacer de Crested Butte un lugar mucho más interesante y placentero. Gracias también a todos los camareros de Camp4, que tan servicialmente atienden a sus clientes. Gracias a la gente de Prentice Hall por seguir atendiendo a todas mis peticiones, y por facilitarme las cosas en todo momento . Hay varias herramientas que han resultado de extraordinaria utilidad durante el proceso de desarrollo y me siento en deuda con sus creadores cada vez que las uso. Cygwin (www.cygwin.com) me ha permitido resolver innumerables problemas que Windows no puede resolver y cada día que pasa más me engancho a esta herrami enta (me hubiera encantado disponer de ella hace 15 años, cuando tenía mi mente orientada a Gnu Emacs). Eclipse de IBM (www.eclipse.org) representa una maravillosa contribución a la comunidad de desarrolladores y cabe esperar que se puedan obtener grandes cosas con esta herramienta a medida que vaya evolucionando. JetBrains IntelliJ Idea continúa abriendo nuevos y creativos caminos dentro del campo de las herramientas de desarrollo. Comencé a utilizar Enterprise Architect de Sparxsystems con este libro y se ha convertido rápidamente en mi herramienta UML favorita. El formateador de código Jalopy de Marco Hunsicker (www.triemax.com) también ha resultado muy útil en numerosas ocasiones y Marco me ha ayudado extraordinariamente a la hora de configurarlo para mis necesidades concretas. En mi opinión, la herramienta JEdit de Slava Pestov y sus correspondientes plug-ins también resultan útiles en diversos momentos (wwwjedit.org); esta herramienta es un editor muy adecuado para todos aquellos que se estén iniciando en el desarrollo de seminarios. y por supuesto, por si acaso no lo he dejado claro aún, utilizo constantemente Python (www.Python.org) para resolver pro-
blemas, esta herramienta es la criatura de mi colega Guido Van Rossum y de la panda de enloquecidos genios con los que disfruté enormemente haciendo deporte durante unos cuantos días (a Tim Peters me gustaría decirle que he enmarcado ese ratón que tomó prestado, al que le he dado el nombre oficial de "TimBotMouse"). Permitidme tan sólo recomendaros que busquéis otros lugares más sanos para comer. Asimismo, mi agradecimiento a toda la comunidad Python, formada por un conjunto de gente extraordinaria. Son muchas las personas que me han hecho llegar sus correcciones y estoy en deuda con todas ellas, pero quiero dar las gracias en particular a (por la primera edición): Kevin Raulerson (encontró numerosísimos errores imperdonables), Bob Resendes (simplemente increíble), John Pinto, Joe Dante, loe Sharp (fabulosos vuestros comentarios), David Combs (munerosas correcciones de clarificación y de gramática), Dr. Robert Stephenson, lohn Cook, Franklin Chen, Zev Griner, David Karr, Leander A. Strosehein, Steve Clark, Charles A. Lee, Austin Maher, Dennis P. Roth, Roque Oliveira, Douglas Dunn, Dejan Ristic, N eil Galarneau, David B. Malkovsky, Steve Wilkinson, y muchos otros. El Profesor Mare Meurrens dedicó
xxiv Piensa en Java
una gran cantidad de esfuerzo a pub licitar y difundir la versión electrónica de la primera edición de este libro en Europa. Gracias a todos aquellos que me han ayudado a reescribir los ejemplos para utilizar la biblioteca Swing (para la segunda edición), así como a los que han proporcionado otros tipos de comentarios: Jan Shvarts, Thomas Kirsch, Rahim Adatia, Rajesh Jain, Ravi Manthena, Banu Rajamani, Jens Brandt, Nitin Shivaram, Malcolm Davis y a todos los demás que me han manifestado su apoyo. En la cuarta edición, Chris Grindstaff resultó de gran ayuda durante el desarrollo de la sección SWT y Sean Neville escribió para mí el primer borrador de la sección dedicada a Flex. Kraig Brockschmidt y Gen Kiyooka son algunas de esas personas inteligentes que he podido conocer en alglm momento de vida y que han llegado a ser auténticos amigos, habiendo tenido una enorme influencia sobre mí. Son personas poco usuales en el sentido de que practican yoga y otras formas de engrandecimiento espiritual, que me resultan particularmente inspiradoras e instructivas. Me resulta sorprendente que el saber de Delphi me ayudara a comprender Java, ya que ambos lenguajes tienen en común muchos conceptos y decisiones relativas al diseño del lenguaje. Mis amigos de Delphi me ayudaron enormemente a la hora de entender mejor ese maravilloso entorno de programación. Se trata de Marco Cantu (otro italiano, quizá el ser educado en latín mejora las aptitudes de uno para los lenguajes de programación), Neil Rubenking (que solía dedicarse al yoga, la comida vegetariana y el Zen hasta que descubrió las computadoras) y por supuesto Zack Urlocker (el jefe de producto original de Delphi), un antiguo amigo con el que he recorrido el mundo. Todos nosotros estamos en deuda con el magnífico Anders Hejlsberg, que continúa asombrándonos con C# (lenguaje que, como veremos en el libro, fue una de las principales inspiraciones para Java SES). Los consejos y el apoyo de mi amigo Richard Hale Shaw (al igual que los de Kim) me han sido de gran ayuda. Richard y yo hemos pasado muchos meses juntos impartiendo seminarios y tratando de mejorar los aspectos pedagógicos con el fin de que los asistentes disfrutaran de una experiencia perfecta. El diseño del libro, el diseño de la cubierta y la fotografia de la cubierta han sido realizados por mi amigo Daniel Will-Harris, renombrado autor y diseñador (www.Will-Harris.com). que ya solía crear sus propios diseños en el colegio, mientras esperaba a que se inventaran las computadoras y las herramientas de autoedición, y que ya entonces se quejaba de mi forma de resolver los problemas de álgebra. Sin embargo, yo me he encargado de preparar para imprenta las páginas, por lo que los errores de fotocomposición son míos. He utilizado Microsoft® Word XP para Windows a la hora de escribir el libro y de preparar las páginas para imprenta mediante Adobe Acrobat; este libro fue impreso directamente a partir de los archivos Acrobat PDF. Como tributo a la era electrónica yo me encontraba fuera del país en el momento de producir la primera y la segunda ediciones finales del libro; la primera edición fue enviada desde Ciudad del Cabo (Sudáfrica), mientras que la segunda edición fue enviada desde Praga. La tercera y cuarta ediciones fueron realizadas desde Crested Butte, Colorado. En la versión en inglés del libro se utilizó el tipo de letra Ceorgia para el texto y los títulos están en Verdana. La letra de la cubierta original es ¡Te Rennie Mackintosh. Gracias en especial a todos mis profesores y estudiantes (que también son mis profesores). Mi gato Molly solía sentarse en mi regazo mientras trabajaba en esta edición, ofreciéndome así mi propio tipo de apoyo peludo y cálido. Entre los amigos que también me han dado su apoyo, y a los que debo citar (aunque hay muchos otros a los que no cito por falta de espacio), me gustaría destacar a: Patty Gast (extraordinaria masaj ista), Andrew Binstock, Steve Sinofsky, JD Hildebrandt, Tom Keffer, Brian McElhinney, BrinkJey Barr, Bill Gates en Midnight Engineering Magazine, Larry Constantine y Lucy Lockwood, Gene Wang, Dave Mayer, David Intersimone, Chris y Laura Sttand, Jos Almquists, Brad Jerbic, Marilyn Cvitanic, Mark Mabry, la familia Robbins, la familia Moelter (y los McMillans), Michael Wilk, Dave Stoner, los Cranstons, Larry Fogg, Mike Sequeira, Gary Entsminger, Kevin y Sonda Donovan, Joe Lordi, Dave y Brenda Bartlett, Patti Gast, Blake, Annette & Jade, los Rentschlers, los Sudeks, Dick, Patty, y Lee Eckel, Lynn y Todd, y sus familias. Y por supuesto, a mamá y papá.
Introd ucción
"El dio al hombre la capacidad de hablar, y de esa capacidad surgió el pensamiento. Que es la medida del Universo" Prometeo desencadenado, Shelley Los seres humanos ... eslamos, en buena medida, a merced del lenguaje concreto que nuestra sociedad haya elegido como medio de expresión. Resulta completamente ilusorio creer que nos ajustamos a la realidad esencialmente sin utilizar el lenguaje y que el lenguaje es meramente un medio incidental de resolver problemas específicos de comunicación y reflexión. Lo cierto es que e/ "mundo real" está en gran parle construido, de manera inconsciente, sobre los hábitos lingüísticos del grupo. El estado de la Lingüística como ciencia, 1929, Edward Sapir Como cualquier lenguaje humano, Java proporciona una forma de expresar conceptos. Si tiene éxito, esta forma de expresión será significativamente más fácil y flexible que las alternativas a medida que los problemas crecen en tamaño y en complejidad. No podemos ver Java sólo como una colección de características, ya que algunas de ellas no tienen sentido aisladas. Sólo se puede emplear la suma de las partes si se está pensando en el diseño y no simplemente en la codificación. Y para entender Java así, hay que comprender los problemas del lenguaje y de la programación en general. Este libro se ocupa de los problemas de la programación, porque son problemas, y del método que emplea Java para resolverlos. En consecuencia, el conjunto de características que el autor explica en cada capítulo se basa en la forma en que él ve cómo puede resolverse un tipo de problema en particular con este lenguaje. De este modo, el autor pretende conducir, poco a poco, al lector hasta el punto en que Java se convierta en su lengua materna. La actitud del autor a lo largo del libro es la de conseguir que el lector construya un modelo mental que le permita desarrollar un conocimiento profundo del lenguaje; si se enfrenta a un puzzle, podrá fijarse en el modelo para tratar de deducir la respuesta.
Prerrequisitos Este libro supone que el lector está familiarizado con la programación: sabe que un programa es una colección de instrucciones, qué es una subrutina, una función o una macro, conoce las instrucciones de control como "if' y las estructuras de bucle como "while", etc. Sin embargo, es posible que el lector haya aprendido estos conceptos en muchos sitios, tales como la programación con un lenguaje de macros o trabajando con una herramienta como Perl. Cuando programe sintiéndose cómodo con las ideas básicas de la programación, podrá abordar este libro. Por supuesto, el libro será más fác il para los programadores de C y más todavía para los de C++, pero tampoco debe autoexcluirse si no tiene experiencia con estos lenguajes (aunque tendrá que trabajar duro). Puede descargarse en www.MindView.net el seminario muJtimedia Thinking in e, el cual le ayudará a aprender más rápidamente los fundamentos necesarios para estudiar Java. No obstante, en el libro se abordan los conceptos de programación orientada a objetos (POO) y los mecanismos de control básicos de Java. Aunque a menudo se hacen referencias a las características de los lenguajes C y C++ no es necesario profundizar en ellos, aunque sí ayudarán a todos los programadores a poner a Java en perspectiva con respecto a dichos lenguajes, de los que al fin y al cabo desciende. Se ha intentado que estas referencias sean simples y sirvan para explicar cualquier cosa con la que una persona que nunca haya programado en C/C++ no esté familiarizado.
xxvi Piensa en Java
Aprendiendo Java Casi al mismo tiempo que se publicó mi primer libro, Using C+ + (Osbome/McGraw-I-lill, 1989), comencé a enseñar dicho lenguaje. Enseñar lenguajes de programación se convirtió en mi profesión; desde 1987 he visto en auditorios de todo el mundo ver dudar a los asistentes, he visto asimismo caras sorprendidas y expresiones de incredulidad. Cuando empecé a impartir cursos de formación a grupos pequeños, descubrí algo mientras se hacían ejercicios. Incluso aquellos que sonreían se quedaban con dudas sobre muchos aspectos. Comprendí al dirigir durante una serie de años la sesión de C++ en la Software Development Conference (y más tarde la sesión sobre Java), que tanto yo como otros oradores tocábamos demasiados temas muy rápidamente. Por ello, tanto debido a la variedad en el nivel de la audiencia como a la forma de presentar el material, se termina perdiendo audiencia. Quizá es pedir demasiado pero dado que soy uno de esos que se resisten a las conferencias tradicionales (yen la mayoría de los casos, creo que esa resistencia proviene del aburrimiento), quería intentar algo que permitiera tener a todo el mundo enganchado. Durante algún tiempo, creé varias presentaciones diferentes en poco tiempo, por lo que terminé aprendiendo según el método de la experimentación e iteración (una técnica que también funciona en el diseño de programas). Desarrollé un curso utilizando todo lo que había aprendido de mi experiencia en la enseñanza. Mi empresa, MindView, Inc. , ahora imparte el seminario Thinking in Java (piensa en Java); que es nuestro principal seminario de introducción que proporciona los fundamentos para nuestros restantes seminarios más avanzados. Puede encontrar información detallada en www.MindView.net. El seminario de introducción también está disponible en el CD-ROM Hands-On Java. La información se encuentra disponible en el mismo sitio web.
La respuesta que voy obteniendo en cada seminario me ayuda a cambiar y reenfocar el material hasta que creo que funciona bien como método de enseñanza. Pero este libro no son sólo las notas del seminario; he intentado recopilar el máximo de información posible en estas páginas y estructurarla de manera que cada tema lleve al siguiente. Más que cualquier otra cosa, el libro está diseñado para servir al lector solitario que se está enfrentando a un nuevo lenguaje de programación.
Objetivos Como mi anterior libro, Thinking in C+ +, este libro se ha diseñado con una idea en mente: la forma en que las personas aprenden un lenguaje. Cuando pienso en un capítulo del libro, pienso en términos de qué hizo que fuera una lección durante un seminario. La infonnación que me proporcionan las personas que asisten a un seminario me ayuda a comprender cuá-
les son las partes complicadas que precisan una mayor explicación. En las áreas en las que fui ambicioso e incluí demasiadas características a un mismo tiempo, pude comprobar que si incluía muchas características nuevas, tenía que explicarlas yeso
contribuía fácilmente a la confusión del estudiante. En cada capítulo he intentado enseñar una sola característica o un pequeño grupo de características asociadas, sin que sean necesarios conceptos que todavía no se hayan presentado. De esta manera, el lector puede asimilar cada pieza en el contex-
to de sus actuales conocimientos. Mis objetivos en este libro son los siguientes: 1. Presentar el material paso a paso de modo que cada idea pueda entenderse fácilmente antes de pasar a la siguiente. Secuenciar cuidadosamente la presentación de las características, de modo que se haya explicado antes de que se vea en un ejemplo. Por supuesto, esto no siempre es posible, por lo que en dichas situaciones, se proporciona una breve descripción introductoria.
2. Utilizar ejemplos que sean tan simples y cortos como sea posible. Esto evita en ocasiones acometer problemas del "mundo real", pero he descubierto que los principiantes suelen estar más contentos cuando pueden comprender todos los detalles de un ejemplo que cuando se ven impresionados por el ámbito del problema que resuelve. También, existe una seria limitación en cuanto a la cantidad de código que se puede absorber en el aula. Por esta razón, no dudaré en recibir críticas acerca del uso de "ejemplos de juguete", sino que estoy deseando recibirlas en aras de lograr algo pedagógicamente útil. 3. Dar lo que yo creo que es importante para que se comprenda el lenguaje, en lugar de contar todo lo que yo sé_ Pienso que hay una jerarquía de importancia de la información y que existen hechos que el 95% de los programadores nunca conocerán, detalles que sólo sirven para confundir a las personas y que incrementan su percepción de la complejidad del lenguaje. Tomemos un ejemplo de C, si se memoriza la tabla de precedencia de los
Introducción xxvii operadores (yo nunca lo he hecho), se puede escribir código inteligente. Pero si se piensa en ello, también confundirá la lectura y el mantenimiento de dicho código, por tanto, hay que olvidarse de la precedencia y emplear paréntesis cuando las cosas no estén claras. 4. Mantener cada sección enfocada de manera que el tiempo de lectura y el tiempo entre ejercicios, sea pequeño. Esto no sólo mantiene las mentes de los alumnos más activas cuando se está en un seminario, sino que también
proporciona al lector una mayor sensación de estar avanzando. 5. Proporcionar al alumno una base sólida de modo que pueda comprender los temas los suficientemente bien como para que desee acudir a cursos o libros más avanzados.
Enseñar con este libro La edición original de este libro ha evolucionado a partir de un seminario de una semana que era, cuando Java se encontraba en su infancia, suficiente tiempo para cubrir el lenguaje. A medida que Java fue creciendo y añadiendo más y más funcionalidades y bibliotecas, yo tenazmente trataba de enseñarlo todo en una semana. En una ocasión, un cliente me sugirió que enseñara "sólo los fundamentos" y al hacerlo descubrí que tratar de memorizar todo en una única semana era angustioso tanto para mí como para las personas que asistían al seminario. Java ya no era un lenguaje "simple" que se podía aprender en una semana. Dicha experiencia me llevó a reorganizar este libro, el cual ahora está diseñado como material de apoyo para un seminario de dos semanas o un curso escolar de dos trimestres. La parte de introducción termina con el Capítulo 12, Tratamiento de errores mediante excepciones, aunque también puede complementarla con una introducción a IDBC, Servlets y JSP. Esto proporciona las bases y es el núcleo del CD-ROM Hands-On Java. El resto del libro se corresponde con un curso de nivel intennedio y es el material cubierto en el CD-ROM Intermediale Thinking in Java. Ambos discos CD ROM pueden adquirirse a través de wl:vw.MindView.net. Contacte con Prentice-Hall en www.prenhallprofessional. com para obtener más información acerca del material para el profesor relacionado con este libro.
Documentación del JDK en HTML El lenguaje Java y las bibliotecas de Sun Microsystems (descarga gratuita en hllp://java.sun. com) se suministran con documentación en formato electrónico, que se puede leer con un explorador web. Muchos de los libros publicados sobre Java proporcionan esta documentación. Por tanto, o ya se tiene o puede descargase y, a menos que sea necesario, en este libro no se incluye dicha documentación, porque normalmente es mucho más rápido encontrar las descripciones de las clases en el explorador web que buscarlas en un libro (y probablemente la documentación en línea estará más actualizada). Basta con que utilice la referencia "JDK documentation". En este libro se proporcionan descripciones adicionales de las clases sólo cuando es necesario complementar dicha documentación, con el fin de que se pueda comprender un determinado ejemplo.
Ejercicios He descubierto que durante las clases los ejercicios sencillos son excepcionalmente útiles para que el alumno termine de comprender el tema, por lo que he incluido al final de cada capítulo una serie de ejercicios. La mayor parte de los ej ercicios son bastante sencillos y están diseñados para que se puedan realizar durante un tiempo razonable de la clase, mientras el profesor observa los progresos, asegurándose de que los estudiantes aprenden el tema. Algunos son algo más complejos, pero ninguno presenta un reto inalcanzable. Las soluciones a los ejercicios seleccionados se pueden encontrar en el documento electrónico The Thinking in Java Annotated So/ution Guide, que se puede adquirir en www.MindVíew.net.
Fundamentos para Java Otra ventaja que presenta esta edición es el seminario multimedia gratuito que puede descargarse en la dirección www.MindVíew.net. Se trata del seminario Thinking in e, el cual proporciona una introducción a los operadores, funciones
xxviii Piensa en Java
y la sintaxis de e en la que se basa la sintaxis de Java. En las ediciones anteriores del libro se encontraba en el eD Foundations for Java que se proporcionaba junto con el libro, pero ahora este seminario puede descargarse gratuitamente. Originalmente, encargué a ehuck Allison que creara Thinking in C como un producto autónomo, pero decidí incluirlo en la segunda edición de Thinking in C++ y en la segunda y tercera ediciones de Thinking in Java , por la experiencia de haber estado con personas que llegan a los seminarios sin tener una adecuada formación en la sintaxis básica de e. El razonamiento suele ser: "Soy un programador inteligente y no quiero aprender e, sino e ++ o Java, por tanto, me salto el e y paso directamente a ver el e++/Java". Después de asistir al seminario, lentamente todo el mundo se da cuenta de que el prerrequisito de conocer la sintaxis de e tiene sus buenas razones de ser. Las tecnologías han cambiado y han pennitido rehacer Thinking in C como una presentación Flash descargable en lugar de tener que proporcionarlo en CD. Al proporcionar este seminario en linea, puedo garantizar que todo el mundo pueda comenzar con una adecuada preparación. El seminario Thinking in C también permite atraer hacia el libro a una audiencia importante. Incluso aunque los capítulos dedicados a operadores y al control de la ejecución cubren las partes fundamentales de Java que proceden de C, el seminario en línea es una buena introducción y precisa del estudiante menos conocimientos previos sobre programación que este libro.
Código fuente Todo el código fuente de este libro está disponible gratuitamente y sometido a copyright, distribuido como un paquete único, visitando el sitio web www.MindView.net. Para asegurarse de que obtiene la versión más actual, éste es el sitio oficial de distribución del código. Puede distribuir el código en las clases y en cualquier otra situación educativa. El objetivo principal del copy right es asegurar que el código fuente se cite apropiadamente y evitar así que otros lo publiquen sin permiso. No obstante, mientras se cite la fuente, no constituye ningún problema en la mayoría de los medios que se empleen los ejemplos del libro. En cada archivo de código fuente se encontrará una referencia a la siguiente nota de copyright:
// ,! Copyright.txt Thi s eomputer source eode is Copyright ~ 2006 MindView,
lnc.
All Rights Reserved . Permission to use, eopy, mOdify, and distribute this computer souree code (Source Code) and its documentation wit hout fee and without a wri tten agreement for the pur poses set forth be low is hereby granted, provided that the aboye copyright notice, this paragraph and the fo llowing five numbered paragraphs appear in a l l copies. l. Permiss i on is granted to compile the Souree Code and to include the compiled code, in execu tab le format only , in personal and eommereial software programs . 2. Permission is granted to use the Souree Code wi thout modification in classroom situations, including in presenta tion materials, provided that the book "Thinking in Java 11 is cited as the origino 3. Permi ssion to incorpora te the Souree Code into printed media may be obtained by contact i ng : MindView, lne . 5343 Vall e Vist a La Mesa, California 91941 Wayne@MindView . net 4. The Bouree Code and documentation are copyrighted by MindView, lnc. The Souree eode is provided wi thout express
Introducción xxix or implied warranty of any kind, including any implied warranty of merchantability, fitness ter a particular purpose or non - infringement . MindView, lnc. does not warrant that the operation of any program that includes the Sauree Cede will be uninterrupted or error - free . MindView, lnc . makes no representation about the suitability of the Bouree Cede or of any software that includes the Sau ree Cede tor any purpose. The entire risk as to the quality and performance of any program that includes the Sauree Cade is with the user of the Sauree Codeo The user understands that the Sauree Cede was developed for research and instructional purposes and is advised not to rely exclusively for any reason on the Source Cede er any p r ogram that includes the Source Codeo Should the Source Cede er any resulting software prove defective, the user as sumes the cost of all necessary servicing, repair, or correction.
5. IN NO EVENT SHALL MINDVIEW, INC . , OR ITS PUBLI SHER BE LI ABLE TO ANY PARTY UNDER ANY LEGAL THEORY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL , OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION , OR ANY OTHER PECUNIARY LOSS, OR FOR PERSONAL I NJURIES, ARISING OUT OF THE USE OF TH IS SOURCE CODE AND ITS DOCUMENTATION, OR ARISING OUT OF THE INAB I LI TY TO US E ANY RESULTING PROGRAM , EVEN IF MINDVIEW , INC ., OR I TS PUBLISHER HAS BE EN ADV I SED OF THE POSSIBILITY OF SUCH DAMAGE . MI NDVIEW, INC . SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUD I NG, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY ANO FITNESS FOR A PARTICULAR PURPOSE. THE SOURCE CODE ANO DOCUMENTATION PROVIDED HEREUNDER IS ON AN "AS I S" BAS I S, WI THOUT ANY ACCOMPANYING SERVICES FROM MINDVI EW, I NC . , ANO MINDVIEW, INC. HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MOD I FICATIONS. Please note that MindVi e w , lnc. maintains a Web site wh i ch is the sole distribution point f or electronic copies of the Sou r ce Code, http://www . Mi n dView .net (and official mirror sites), where it i s freel y available under the terms stated above . lf you think you 1ve found an error in the Source Code , please submit a correction u sing the feedback system that you will find at
http : //www.MindView . net .
/// : Puede utilizar el código en sus proyectos y en la clase (incluyendo su material de presentaciones) siempre y cuando se mantenga la nota de copyright en cada uno de los archi vos fuente.
Estándares de codificación En el texto del libro, los identificadores (nombres de métodos, variables y clases) se escriben en negrita. La mayoría de las palabras clave se escriben en negrita, excepto aquellas palabras clave que se usan con mucha frecuencia y ponerlas en negrita podría volverse tedioso, como en el caso de la palabra "e1ass". En este libro, he utilizado un estilo de codificación particular para los ejemplos. Este estilo sigue el que emplea Sun en prácticamente todo el código que encontrará en su sitio (véase http://java.sun. com/docs/codeconv/index. htmf), y que parece que soporta la mayoría de los entornos de desarrollo Java. Si ha leído mis otros libros, observará también que el estilo de codificación de Sun coincide con el mío, lo que me complace, ya que yo no tengo nada que ver con la creación del estilo de
xxx Piensa en Java
Sun. El tema del estilo de fonnato es bueno para conseguir horas de intenso debate, por lo que no vaya intentar dictar un estilo correcto a través de mis ejemp los; tengo mis propias motivaciones para usar el estilo que uso. Dado que Java es un
lenguaje de programación de fonoato libre, se puede emplear el estilo con el que uno se encuentre a gusto. Una solución para el tema del esti lo de codificación consiste en utilizar una herramienta como Jalopy (www.triemax.com). la cual me ha ayudado en el desarrollo de este libro a cambiar el fonoato al que se adaptaba a mí. Los archi vos de código impresos en el libro se han probado con un sistema automatizado, por lo que deberían ejecutarse sin crrores de compi lación.
Este libro está basado y se ha comprobado con Java SE5/6. Si necesita obtener infonnación sobre versiones anteriores del lenguaje que no se cubren en esta ed ición, la ediciones primera y tercera del mismo pueden descargarse gratuitamente en www.MindView.net.
Errores No importa cuantas herramientas uti li ce un escritor para detectar los errores, algunos quedan ahí y a menudo son lo que primero ve el lector. Si descubre cua lquier cosa que piensa que es un error, por favor utili ce el víncu lo que encontrará para este libro en www.MindView.net y envÍeme el error junto con la corrección que usted crea. Cualqui er ayuda siempre es bienve-
nida.
Introducción a los objetos "Analizamos la Naturaleza, la organ izamos en conceptos y vamos asignando significados a medida que lo hacemos, fundamentalmente porque participamos en un acuerdo tácito suscrito por toda nuestra comunidad de hablantes y que está codificado en los propios patrones de nuestro idioma .. . nos resulta imposible hablar si no utilizamos la organización y clasificación de los datos decretadas por ese acuerdo". Benjamin Lee Whorf (1 897-1941) La génesis de la revolución de las computadoras se halla ba en una máquina. La génesis de nuestros lenguajes de programación tiende entonces a parecerse a dicha máquina. Pero las computadoras, más que máquinas, pueden considerarse como herramientas que permiten ampliar la mente ("bicicletas para la mente", como se enorgullece en decir Steve Jobs), además de un medio de expresión diferente. Como resultado, las herramientas empiezan a parecerse menos a máquinas y más a partes de nuestras mentes, al igual que ocurre con otras formas de expresión como la escritura, la pintura, la escultura, la an imación y la realización de películas. La programac ión orientada a objetos (POO) es parte de este movimiento dirigido al uso de las computadoras como un medio de expresión.
Este capítulo presenta los conceptos básicos de la programación orientada a objetos, incluyendo una introducción a los métodos de desarrollo. Este capítulo, y este libro, supone que el lector tiene cierta experiencia en programación, aunque no necesariamente en C. Si cree que necesita una mayor preparación antes de abordar este libro, debería trabajar con el seminario multimedia sobre C, Thinking in C. que puede descargarse en www.MindView.net. Este capítulo contiene material de carácter general y suplementario. Muchas personas pueden no sentirse cómodas si se enfrentan a la programación orientada a objetos sin obtener primero una visión general. Por tanto, aquí se presentan muchos conceptos que proporcionarán una sólida introducción a la PDO. Sin embargo, otras personas pueden no necesitar tener una visión general hasta haber visto algunos de los mecanismos primero, estas personas suelen perderse si no se les ofrece algo de código que puedan manipular. Si usted forma parte de este último glUpO, estará ansioso por ver las especifidades del lenguaje, por lo que puede saltarse este capítulo, esto no le impedirá aprender a escribir programas ni conocer el lenguaje. Sin embargo, podrá vo lver aquí cuando lo necesite para completar sus conocimientos, con el fin de comprender por qué son importantes los obj etos y cómo puede diseñarse con ellos.
El progreso de la abstracción Todos los lenguaj es de programación proporcionan abstracciones. Puede argumentarse que la complejidad de los problemas que sea capaz de resolver está directamente relacionada con el tipo (clase) y la calidad de las abstracciones, entendiendo por "clase", "¿qué es lo que se va a abstraer?", El lenguaj e ensamblador es una pequeña abstracción de la máquina subyacente. Muchos de los lenguajes denominados "imperativos" que le siguieron (como FORTRAN, BASIC y C) fueron abstracciones del lenguaje ensamblador. Estos lenguajes constituyen grandes mejoras sobre el lenguaje ensamblador, pero su principal abstracción requiere que se piense en ténninos de la estructura de la computadora en lugar de en la es tructura del problema que se está intentado resolver. El programado r debe establecer la asociación entre el modelo de la máquina (en el "espac io de la solución", que es donde se va a implementar dicha solución, como puede ser una computadora) y el modelo
2 Piensa en Java
del problema que es lo que realmente se quiere resolver (en el "espacio del problema", que es el lugar donde existe el problema, como por ejemplo en un negocio). El esfuerzo que se requiere para establecer esta correspondencia y el hecho de que sea extrínseco al lenguaje de programación, da lugar a programas que son dificil es de escribir y caros de mantener, además del efecto colateral de toda una industria de "métodos de programación", La alternativa a modelar la maquina es modelar el problema que se está intentado solucionar. Los primeros lenguajes como LISP y APL eligen vistas parciales del mundo ("todos los problemas pueden reducirse a listas" o "todos los problemas son algorítmicos", respectivamente). Prolog convierte todos los problemas en cadenas de decisión. Los lenguajes se han creado para programar basándose en restricciones y para programar de forma exclusiva manipulando símbolos gráficos (aunq ue se demostró que este caso era demasiado restrictivo). Cada uno de estos métodos puede ser una buena solución para resolver la clase de problema concreto para el que están diseñados, pero cuando se aplican en otro dominio resultan inadecuados. El enfoque orientado a objetos trata de ir un paso más allá proporcionando herramientas al programador para representar los elementos en el espacio del problema. Esta representación es tan general que el programador no está restringido a ningún tipo de problema en particular. Se hace referencia a los elementos en el espacio del problema denominando "objetos" a sus representaciones en el espacio de la solución (también se necesitarán otros objetos que no tendrán análogos en el espacio del problema). La idea es que el programa pueda adaptarse por sí sólo a la jerga del problema añadiendo nuevos tipos de objetos, de modo que cuando se lea el código que describe la solución, se estén leyendo palabras que también expresen el problema. Ésta es una abstracción del lenguaje más flexible y potente que cualquiera de las que se hayan hecho anteriormente l . Por tanto, la programación orientada a objetos permite describir el problema en ténninos del problema en lugar de en términos de la computadora en la que se ejecutará la solución. Pero aún existe una conexión con la computadora, ya que cada objeto es similar a una pequeña computadora (tiene un estado y dispone de operaciones que el programador puede pedirle que realice). Sin embargo, esto no quiere decir que nos encontremos ante una mala analogía de los objetos del mundo real, que tienen características y comportamientos. Alan Kay resumió las cinco características básicas del Smalltalk, el primer lenguaje orientado a objetos que tuvo éxito y uno de los lenguajes en los que se basa Java. Estas características representan un enfoque puro de la programación orientada a objetos. 1. Todo es un objeto. Piense en un objeto como en una variable: almacena datos, permite que se le "planteen solicitudes", picliéndole que realice operaciones sobre sí mismo. En teoría, puede tomarse cualquier componente conceptual del problema que se está intentado resolver (perros, edificios, servicios, etc.) y representarse como un objeto del programa.
2. Un programa es un montón de objetos que se dicen entre sí lo que tienen que hacer enviándose mensajes. Para hacer una solicitud a un objeto, hay que enviar un mensaje a dicho objeto. Más concretamente, puede pensar en que un mensaje es una solicitud para llamar a un método que pertenece a un determinado objeto. 3. Cada objeto tiene su propia memoria formada por otros objetos. Dicho de otra manera, puede crear una nueva clase de objeto definiendo un paquete que contenga objetos existentes. Por tanto, se puede incrementar la complejidad de un programa acuitándola tras la simplicidad de los objetos. 4. Todo objeto tiene un tipo asociado. Como se dice popularmente, cada objeto es una instancia de una clase, siendo "clase" sinónimo de "tipo". La característica distintiva más importante de una clase es "el conjunto de mensajes que se le pueden enviar". 5. Todos los objetos de un tipo particular pueden recibir los mismos mensajes. Como veremos más adelante, esta afirmación es realmente importante. Puesto que un objeto de tipo "círculo" también es un objeto de tipo "forma", puede garantizarse que un círculo aceptará los mensajes de forma. Esto quiere decir que se puede escribir código para comunicarse con objetos de tipo forma y controlar automáticamente cualquier cosa que se ajuste a la descripción de una forma. Esta capacidad de suplantación es uno de los conceptos más importantes de la programación orientada a objetos. Booch ofrece una descripción aún más sucinta de objeto: ¡Algunos diseñadores de lenguajes han decidido que la programación orientada a objetos por sí misma no es adecuada para resolver fácilmente todos los problemas de la programación, y recomiendan combinar varios métodos en lenguajes de programación mll/liparadigma. Consulte MII/I/paradim Programming in Leda de Timothy Budd (Addison-Wesley, 1995).
1 Introducción a los objetos 3
Un objeto tiene estado, comportamiento e identidad. Esto significa que un objeto puede tener datos internos (lo que le proporciona el estado), métodos (para proporcionar un comportamiento) y que cada objeto puede ser diferenciado de fanna unívoca de cualquier otro objeto; es decir, cada objeto tiene una dirección de memoria exclusiva. 2
Todo objeto tiene una interfaz Aristóteles fue probablemente el primero en estudiar cuidadosamente el concepto de tipo; hablaba de "la clase de peces y de la clase de pájaros". La idea de que todos los objetos, aún siendo únicos, son también parte de una clase de objetos que tienen características y comportamientos comunes ya se empleó en el primer lenguaje orientado a objetos, el Simula-67, que ya usaba su palabra clave fundamental class, que permite introducir un nuevo tipo en un programa. Simula, como su nombre implica, se creó para desarrollar simulaciones como la clásica del "problema del cajero de un banco". En esta simulación, se tienen muchos cajeros, clientes, cuentas, transacciones y unidades monetarias, muchísimos "'objetos". Los objetos, que son idénticos excepto por su estado durante la ejecución de un programa, se agrupan en "clases de objetos", que es de donde procede la palabra clave class. La creación de tipos de datos abstractos (clases) es un concepto fundamental en la programación orientada a objetos. Los tipos de datos abstractos funcionan casi exactamente como tipos predefmidos: pueden crearse variables de un tipo (llamadas objetos u instancias en la jerga de la POOl y manipular dichas variables (mediante el envío de mensajes o solicitudes, se envía un mensaje y el objeto sabe lo que tiene que hacer con él). Los miembros (elementos) de cada clase comparten algunos rasgos comunes. Cada cuenta tiene asociado un saldo, cada cajero puede aceptar un depósito, etc. Además, cada miembro tiene su propio estado. Cada cuenta tiene un saldo diferente y cada cajero tiene un nombre. Por tanto, los cajeros, clientes, cuentas, transacciones, etc., pueden representarse mediante una entidad unívoca en el programa informático. Esta entidad es el objeto y cada objeto pertenece a una detenninada clase que define sus características y comportamientos. Por tanto, aunque en la programación orientada a objetos lo que realmente se hace es crear nuevos tipos de datos, en la práctica, todos los lenguajes de programación orientada a objetos utilizan la palabra clave "class". Cuando vea la palabra "type" (tipo) piense en "elass" (elase), y viceversa 3 Dado que una clase describe un conjunto de objetos que tienen características (elementos de datos) y comportamientos (funcionalidad) idénticos, una clase realmente es un tipo de datos porque, por ej emplo, un número en coma flotante tamb ién tiene un conjunto de características y comportamientos. La diferencia está en que el programador define un clase para adaptar un problema en lugar de forzar el uso de un tipo de datos existente que fue diseñado para representar una unidad de almacenamiento en una máquina. Se puede ampliar el lenguaje de programación añadiendo nuevos tipos de datos específicos que se adapten a sus necesidades. El sistema de programación admite las nuevas clases y proporciona a todas ellas las comprobaciones de tipo que proporciona a los tipos predefinidos. El enfoque orientado a objetos no está limitado a la creación de simulaciones. Se esté o no de acuerdo en que cualquier programa es una simulación del sistema que se está diseñando, el uso de las técnicas de la POO puede reducir fácil mente un gran conjunto de problemas a una sencilla solución. Una vez que se ha definido una clase, se pueden crear tantos objetos de dicha clase como se desee y dichos objetos pueden manipularse como si fueran los elementos del problema que se está intentado resolver. Realmente, uno de los retos de la programación orientada a objetos es crear una correspondencia uno-a-uno entre los elementos del espacio del problema y los objetos del espacio de la solución. Pero, ¿cómo se consigue que un objeto haga un trabajo útil para el programador? Debe haber una [onua de hacer una solicitud al objeto para que haga algo, como por ejemp lo, completar una transacción, dibujar algo en pantalla o encender un interruptor. Además, cada objeto sólo puede satisfacer ciertas solicitudes. Las solicitudes que se pueden hacer a un objeto se definen mediante su infeljaz y es el tipo lo que determina la interfaz. Veamos un ejemplo con la representación de una bombilla: 2 Realmente, esto es poco restrictivo, ya que pueden existir objetos en diferentes máquinas y espacios de direcciones, y también sc pueden almacenar en disco. En estos casos, debe determinarse la identidad del objeto mediante alguna otra cosa que la dirección de memoria.
3 Algunas personas hacen una distinción, estableciendo que el tipo determina la interfaz micntras que la clase es una implemcntación concreta de dicha interfaz.
4 Piensa en Java
Tipo
Interfaz
Luz encenderO apagarO brillarO atenuarO
Luz 1z = new Luz {) ; 1z.encender () ;
La interfaz determina las solicitudes que se pueden hacer a un determinado objeto, por lo que debe existir un código en alguna parte que satisfaga dicha solicitud. Esto, junto con los datos ocultos, definen lo que denomina la implementación. Desde el punto de vista de la programación procedimental, esto no es complicado. Un tipo tiene un método asociado con cada posible solicitud; cuando se hace una determinada solicitud a un objeto, se llama a dicho método. Este proceso se resume diciendo que el programador "envía un mensaje" (hace una solicitud) a un objeto y el objeto sabe lo que tiene que hacer con ese mensaje (ejecuta el código). En este ejemplo, el nombre del tipo/clase es Luz, el nombre de este objeto concreto Luz es lz y las solicitudes que se pueden hacer a un objeto Luz son encender, apagar, brillar o atenuar. Se ha creado un objeto Luz definiendo una "referencia" (lz) para dicho objeto e invocando new para hacer una solicitud a un nuevo objeto de dicho tipo. Para enviar un mensaje al objeto, se define el nombre del objeto y se relaciona con la solicitud del mensaje mediante un punto. Desde el punto de vista del usuario de una clase predefinida, esto es el no va más de la programación con objetos. El diagrama anterior sigue el formato del lenguaje UML (Unified Modeling Langr/age, lenguaje de modelado unificado). Cada clase se representa mediante un recuadro escribiendo el nombre del tipo en la parte superior, los miembros de datos en la zona intermedia y los métodos (las funciones de dicho objeto que reciben cualquier mensaje que el programador enVÍe a dicho objeto) en la parte inferior. A menudo, en estos diagramas sólo se muestran el nombre de la clase y los métodos públicos, 00 incluyéndose la zona iotennedia, como en este caso. Si sólo se está interesado en el nombre de la clase, tampoco es necesario incluir la parte inferior.
Un objeto proporciona servicios Cuando se está intentando desarrollar o comprender el diseño de un programa, una de las mejores formas de pensar en los objetos es como si fueran "proveedores de servicios", El programa proporciona servicios al usuario y esto se conseguirá utilizando los servicios que ofrecen otros objetos. El objetivo es producir (o incluso mejor, localizar en las bibliotecas de código existentes) un conjunto de objetos que facilite los servicios idóneos para resolver el problema.
Una manera de empezar a hacer esto es preguntándose: "Si pudiera sacarlos de un sombrero mágico, ¿qué objetos resolverían el problema de la forma más simple?". Por ejemplo, suponga que quiere escribir un programa de contabilidad. Puede pensar en algunos objetos que contengan pantallas predefinidas para la introducción de los datos contables, otro conjunto de objetos que realicen los cálculos necesarios y un objeto que controle la impresión de los cheques y las facturas en toda clase de impresoras. Es posible que algunos de estos objetos ya existan, pero ¿cómo deben ser los que no existen? ¿Qué servicios deberían proporcionar esos objetos y qué objetos necesitarían para cumplir con sus obligaciones? Si se hace este planteamiento, llegará a un punto donde puede decir: "Este objeto es lo suficientemente sencillo como para escribirlo yo mismo" o "Estoy seguro de que este objeto ya tiene que existir". Ésta es una fonna razonable de descomponer un problema en un conjunto de objetos. Pensar en un objeto como en un proveedor de servicios tiene una ventaja adicional: ayuda a mejorar la cohesión del objeto. Una alta cohesión es una cualidad fundamental del diseño software, lo que significa que los diferentes aspectos de un componente de software (tal como un objeto, aunque también podría aplicarse a un método o a una biblioteca de objetos) deben "ajustar bien entre sí". Un problema que suelen tener los programadores cuando diseñan objetos es el de asignar demasiada funcionalidad al objeto. Por ejemplo, en el módulo para imprimir cheques, puede decidir que es necesario un objeto que sepa todo sobre cómo dar formato e imprimir. Probablemente, descubrirá que esto es demasiado para un solo objeto y que hay que emplear tres o más objetos. Un objeto puede ser un catálogo de todos los posibles diseños de cheque, al cual se le
Introducción a los objetos 5
puede consultar para obtener infonnación sobre cómo imprimir un cheque. Otro objeto o conjunto de objetos puede ser una interfaz de impresión genérica que sepa todo sobre las diferentes clases de impresoras (pero nada sobre contabilidad; por ello, probablemente es un candidato para ser comprado en lugar de escribirlo uno mismo). Y un tercer objeto podría utilizar los servicios de los otros dos para llevar a cabo su tarea. Por tanto, cada objeto tiene un conjunto cohesivo de servicios que ofrecer. En un buen diseño orientado a objetos, cada objeto hace una cosa bien sin intentar hacer demasiadas cosas. Esto además de pernlitir descubrir objetos que pueden adquirirse (el objeto interfaz de impresora), también genera nuevos objetos que se reutilizarán en otros diseños. Tratar los objetos como proveedores de servicios es una herramienta que simplifica mucho. No sólo es útil durante el proceso de diseño, sino también cuando alguien intenta comprender su propio código o reutilizar un objeto. Si se es capaz de ver el valor del objeto basándose en el servicio que proporciona, será mucho más fácil adaptarlo al diseño.
La implementación oculta Resulta útil descomponer el campo de juego en creadores de clases (aquellos que crean nuevos tipos de datos) y en programadores de clientes4 (los consumidores de clases que emplean los tipos de datos en sus aplicaciones). El objetivo del programador cliente es recopilar una caja de herramientas completa de clases que usar para el desarrollo rápido de aplicaciones. El objetivo del creador de clases es construir una clase que exponga al programador cliente sólo lo que es necesario y mantenga todo lo demás oculto. ¿Por qué? Porque si está oculto, el programador cliente no puede acceder a ello, lo que significa que el creador de clases puede cambiar la parte oculta a voluntad sin preocuparse del impacto que la modificación pueda implicar. Nonnalmente, la parte oculta representa las vulnerabilidades internas de un objeto que un programador cliente poco cuidadoso o poco formado podría corromper fácilmente, por lo que ocultar la implementación reduce los errores en los programas. En cualquier relación es importante tener límites que todas las partes implicadas tengan que respetar. Cuando se crea una biblioteca, se establece una relación con el programador de clientes, que también es un programador, pero que debe construir su aplicación utilizando su biblioteca, posiblemente con el fin de obtener una biblioteca más grande. Si todos los miembros de una clase están disponibles para cualquiera, entonces el programador de clientes puede hacer cualquier cosa con dicha clase y no hay forma de imponer reglas. Incluso cuando prefiera que el programador de clientes no manipule directamente algunos de los miembros de su clase, sin control de acceso no hay manera de impedirlo. Todo está a la vista del mundo. Por tanto, la primera razón que justifica el control de acceso es mantener las manos de los programadores cliente apartadas de las partes que son necesarias para la operación interna de los tipos de datos, pero no de la parte correspondiente a la interfaz que los usuarios necesitan para resolver sus problemas concretos. Realmente, es un servicio para los programadores de clientes porque pueden ver fácilmente lo que es importante para ellos y lo que pueden ignorar. La segunda razón del control de acceso es permitir al diseñador de bibliotecas cambiar el funcionamiento interno de la clase sin preocuparse de cómo afectará al programador de clientes. Por ejemplo, desea implementar una clase particular de una forma sencilla para facilitar el desarrollo y más tarde descubre que tiene que volver a escribirlo para que se ejecute más rápidamente. Si la interfaz y la implementación están claramente separadas y protegidas, podrá hacer esto fácilmente. Java emplea tres palabras clave explícitamente para definir los límites en una clase: public, private y protected. Estos modificadores de acceso detenninan quién puede usar las definiciones del modo siguiente: public indica que el elemento que le sigue está disponible para todo el mundo. Por otro lado, la palabra clave private, quiere decir que nadie puede acceder a dicho elemento excepto usted, el creador del tipo, dentro de los métodos de dicho tipo. private es un muro de ladrillos entre usted y el programador de clientes. Si alguien intenta acceder a un miembro private obtendrá un error en tiempo de compilación. La palabra clave protected actúa como private, con la excepción de que una clase heredada tiene acceso a los miembros protegidos (protected), pero no a los privados (private). Veremos los temas sobre herencia enseguida. Java también tiene un acceso "predeterminado", que se emplea cuando no se aplica uno de los modificadores anteriores. Normalmente, esto se denomina acceso de paquete, ya que las clases pueden acceder a los miembros de otras clases que pertenecen al mismo paquete (componente de biblioteca), aunque fuera del paquete dichos miembros aparecen como privados (private). 4
Ténnino acuñado por mi amigo Scott Meyers.
6 Piensa en Java
Reutilización de la implementación Una vez que se ha creado y probado una clase, idealmente debería representar una unidad de código útil. Pero esta reutilización no siempre es tan fácil de conseguir como era de esperar; se necesita experiencia y perspicacia para generar un diseño de un objeto reutili zable. Pero, una vez que se dispone de tal diseño, parece implorar ser reuti lizado. La reutilización de códi go es una de las grandes ventajas que proporcionan los lenguajes de programación orientada a objetos. La fonna más sencilla de reutilizar una clase consiste simplemente en emplear directamente un objeto de dicha clase, aunque también se puede colocar un objeto de dicha clase dentro de una clase nueva. Esto es 10 que se denomina "crear un objeto miembro". La nueva clase puede estar fonnada por cualquier número y tipo de otros objetos en cualquier combinación necesaria para conseguir la funcionalidad deseada en dicha nueva clase. Definir una nueva clase a partir de clases existentes se denomina composición (si la composición se realiza de forma dinámica, se llama agregación). A menudo se hace referencia a la composición como una relación "tiene un", como en "un coche tiene un motor".
~__co_c_h_e__~I·~------~
___
m_o_to_r__
~
Este diagrama UML indica la composición mediante un rombo relleno, que establece que hay un coche. Nonnalmente, yo utilizo una forma más sencilla: sólo una línea, sin el rombo, para indicar una asoc iación. 5 La composición conlleva una gran fl exibilidad. Los objetos miembro de la nueva clase normalmente son privados, lo que les hace inaccesibles a los programadores de clientes que están usando la clase. Esto le permite cambiar dichos miembros sin disturbar al código cliente existente. Los objetos miembro también se pueden modificar en tiempo de ejecución, con el fin de cambiar dinámicamente el comportamiento del programa. La herencia, que se describe a continuación, no proporciona esta flexibilidad, ya que el compilador tiene que aplicar las restricciones en tiempo de compilación a las clases creadas por herencia. Dado que la herencia es tan importante en la programación orientada a objetos, casi siempre se enfatiza mucho su uso, de manera que los programadores novatos pueden llegar a pensar que hay que emplearla en todas partes. Esto puede dar lugar
a que se hagan diseños demasiado complejos y complicados. En lugar de esto, en primer lugar, cuando se van a crear nuevas clases debe considerarse la composición, ya que es más simple y flexible. Si aplica este método, sus diseños serán más inteligentes. Una vez que haya adquirido algo de experiencia, será razonablemente obvio cuándo se necesita emplear la herencia.
Herencia Por sí misma, la idea de objeto es una buena herramienta. Permite unir datos y funcionalidad por concepto, lo que permite representar la idea de l problema-espacio apropiada en lugar de forzar el uso de los idiomas de la máqu ina subyacente. Estos conceptos se expresan como un idades fundamentales en el lenguaje de programación uti lizando la palabra clave c1ass. Sin embargo, es una pena abordar todo el problema para crear una clase y luego verse forzado a crear una clase nueva que podría tener una funcionalidad similar. Es mejor, si se puede, tomar la clase existente, clonarla y luego añadir o modificar lo que sea necesari o al clon. Esto es lo que se logra con la herencia, con la excepción de que la clase original (llamada clase base. supere/ase o e/ase padre) se modifica, el clon "modificado" (denominado e/ase derivada, clase heredada, subclase o clase hija) también refleja los cambios.
cy
I derivada I 5 Normalmente, es suficien te grado de detalle para la mayoría de los diagramas y no es necesario especificar si se está usan do una agregación o una composición.
1 Introducción a los objetos 7
La flecha de este diagrama UML apunta de la clase derivada a la clase base. Como veremos, puede haber más de una clase derivada. Un tipo hace más que describir las restricciones definidas sobre un conjunto de objetos; también tiene una relación con otros tipos. Dos tipos pueden tener características y comportamientos en común, pero un tipo puede contener más características que el otro y también es posible que pueda manejar más mensajes (o manejarlos de forma diferente). La herencia expresa esta similitud entre tipos utilizando el concepto de tipos base y tipos derivados. Un tipo base contiene todas las caracterÍsticas y comportamientos que los tipos derivados de él comparten. Es recomendable crear un tipo base para representar el núcleo de las ideas acerca de algunos de los objetos del sistema. A partir de ese tipo base, pueden deducirse otros tipos para expresar las diferentes fonnas de implementar ese núcleo. Por ejemplo, una máquina para el reciclado de basura clasifica los desperdicios. El tipo base es "basura" y cada desperdicio tiene un peso, un valor, etc., y puede fragmentarse, mezclarse o descomponerse. A partir de esto, se derivan más tipos específicos de basura que pueden tener características adicionales (una botella tendrá un color) o comportamientos (el aluminio puede modelarse, el acero puede tener propiedades magnéticas). Además, algunos comportamientos pueden ser diferentes (el valor del papel depende de su tipo y condición). Utilizando la herencia, puede construir una jerarquía de tipos que exprese el problema que está intentando resolver en términos de sus tipos. Un segundo ejemplo es el clásico ejemplo de la forma, quizá usado en los sistemas de diseño asistido por computadora o en la simulación de juegos. El tipo base es ""forma" y cada forma tiene un tamaño, un color, una posición, etc. Cada forma puede dibujarse, borrarse, desplazarse, colorearse, etc. A partir de esto, se derivan (heredan) los tipos específicos de formas (círculo, cuadrado, triángulo, etc.), cada una con sus propias características adicionales y comportamientos. Por ejemplo, ciertas fonnas podrán voltearse. AIgtmos comportamientos pueden ser diferentes, como por ejemplo cuando se quiere calcular su área. La jerarquía de tipos engloba tanto las similitudes con las diferencias entre las formas.
Forma
dibujar() borrar() mover() obtenerColor() definirColor()
I Círculo
I Cuadrado
Triángulo
Representar la solución en los mismos ténninos que el problema es muy útil, porque no se necesitan muchos modelos intermedios para pasar de una descripción del problema a una descripción de la solución. Con objetos, la jerarquía de tipos es el modelo principal, porque se puede pasar directamente de la descripción del sistema en el mundo real a la descripción del sistema mediante código. A pesar de esto, una de las dificultades que suelen tener los programadores con el diseño orientado a objetos es que es demasiado sencillo ir del principio hasta el final. Una mente formada para ver soluciones complejas puede, inicialmente, verse desconcertada por esta simplicidad. Cuando se hereda de un tipo existente, se crea un tipo nuevo. Este tipo nuevo no sólo contiene todos los miembros del tipo existente (aunque los privados están ocultos y son inaccesibles), sino lo que es más importante, duplica la interfaz de la clase base; es decir, todos los mensajes que se pueden enviar a los objetos de la clase base también se pueden enviar a los objetos de la clase derivada. Dado que conocemos el tipo de una clase por los mensajes que se le pueden enviar, esto quiere decir que la clase derivada es del mismo tipo que la clase base. En el ejemplo anterior, "un círculo es una forma". Esta equivalencia de tipos a través de la herencia es uno de los caminos fundamentales para comprender el significado de la programación orientada a objetos. Puesto que la clase base y la clase derivada tienen la misma interfaz, debe existir alguna implementación que vaya junto con dicha interfaz. Es decir, debe disponerse de algún código que se ejecute cuando un objeto recibe un mensaje concreto. Si
8 Piensa en Java
simplemente hereda una clase y no hace nada más, los métodos de la interfaz de la clase base pasan tal cual a la clase derivada, lo que significa que los objetos de la clase derivada no sólo tienen el mismo tipo sino que también tienen el mismo comportamiento, lo que no es especialmente interesante. Hay dos formas de diferenciar la nueva clase deri vada de la clase base original. La primera es bastante directa: simplemente, se añaden métodos nuevos a la clase derivada. Estos métodos nuevos no forman parte de la interfaz de la clase base, lo que significa que ésta simplemente no hacía todo lo que se necesitaba y se le han añadido más métodos. Este sencillo y primitivo uso de la herencia es, en ocasiones, la solución perfecta del problema que se tiene entre manos. Sin embargo, debe considerarse siempre la posibilidad de que la clase base pueda también necesitar esos métodos adicionales. Este proceso de descubrimiento e iteración en un diseño tiene lugar habitualmente en la programación orientada a objetos. Forma dibujarO borrarO moverO obtenerColorO definirColorO
I
I
I
Círculo
Triángulo
Cuadrado
VoltearVerticalO VoltearHorizontalO
Aunque en ocasiones la herencia puede implicar (especialmente en Java, donde la palabra clave para herencia es extends) que se van a añadir métodos nuevos a la interfaz, no tiene que ser así necesariamente. La segunda y más importante fonna de diferenciar la nueva clase es cambiando el comportamiento de un método existente de la clase base. Esto es lo que se denomina sustitución del método. Forma dibujarO borrarO moverO obtenerColorO definirColorO
I Círculo dibujarO borrarO
I Cuadrado dibujarO borrarO
Triángulo dibujarO borrarO
Para sustituir un método, basta con crear una nueva defmición para el mismo en la clase derivada. Es decir, se usa el mismo método de interfaz, pero se quiere que haga algo diferente en el tipo nuevo.
Relaciones es-un y es-corno-un Es habitual que la herencia suscite un pequeño debate: ¿debe la herencia sustituir sólo los métodos de la clase base (y no añadir métodos nuevos que no existen en la clase base)? Esto significaría que la clase derivada es exactamente del mismo
1 Introducción a los objetos 9 tipo que la clase base, ya que tiene exactamente la misma interfaz. Como resultado, es posible sustituir de forma exacta un objeto de la clase derivada por uno de la clase base. Se podría pensar que esto es una sustitución pura y a menudo se denomina principio de sustitución. En cierto sentido, ésta es la fonna ideal de tratar la herencia. A menudo, en este caso, la relación entre la clase base y las clases derivadas se dice que es una relación es-un, porque podemos decir, "un círculo es una forma". Una manera de probar la herencia es determinando si se puede aplicar la relación es-un entre las clases y tiene sentido.
A veces es necesario añ.adir nuevos elementos de interfaz a un tipo derivado, ampliando la interfaz. El tipo nuevo puede todavía ser sustituido por el tipo base, pero la sustitución no es perfecta porque el tipo base no puede acceder a los métodos nuevos. Esto se describe como una relación es-corno-un. El tipo nuevo tiene la interfaz del tipo antiguo pero también contiene otros métodos, por lo que realmente no se puede decir que sean exactos. Por ejemplo, considere un sistema de aire acondicionado. Suponga que su domicilio está equipado con todo el cableado para controlar el equipo, es decir, dispone de una interfaz que le permite controlar el aire frío. Imagine que el aparato de aire acondicionado se estropea y lo reemplaza por una bomba de calor, que puede generar tanto aire caliente como frío. La bomba de calor es-como-un aparato de aire acondicionado, pero tiene más funciones. Debido a que el sistema de control de su casa está diseñado sólo para controlar el aire frío, está restringido a la comunicación sólo con el sistema de frío del nuevo objeto. La interfaz del nuevo objeto se ha ampliado y el sistema existente sólo conoce la interfaz original. Termostato
Controles
bajarTemperaturaO
Sistema de frío enfriarO
I
t
Acondicionador de aire
enfriarO
I Bomba de calor enfriarO calentarO
Por supuesto, una vez que uno ve este diseño, está claro que la clase base "sistema de aire acondicionado" no es general y debería renombrarse como "sistema de control de temperatura" con el fin de poder incluir también el control del aire caliente, en esta situación, está claro que el principio de sustitución funcionará. Sin embargo, este diagrama es un ejemplo de lo que puede ocurrir en el diseño en el mundo real. Cuando se ve claro que el principio de sustitución (la sustitución pura) es la única fonma de poder hacer las cosas, debe aplicarse sin dudar. Sin embargo, habrá veces que no estará tan claro y será mejor añadir métodos nuevos a la interfaz de una clase derivada. La experiencia le proporcionará los conocimientos necesarios para saber qué método emplear en cada caso.
Objetos intercambiables con polimorfismo Cuando se trabaja con jerarquías de tipos, a menudo se desea tratar un objeto no como el tipo específico que es, sino como su tipo base. Esto penmite escribir código que no dependa de tipos específicos. En el ejemplo de las fonmas, los métodos manipulan las fonnas genéricas, independientemente de que se trate de círculos, cuadrados, triángulos o cualquier otra fonna que todavía no baya sido definida. Todas las fonmas pueden dibujarse, borrarse y moverse, por lo que estos métodos simplemente envían un mensaje a un objeto forma, sin preocuparse de cómo se enfrenta el objeto al mensaje. Tal código no se ve afectado por la adición de tipos nuevos y esta adición de tipos nuevos es la fonna más común de ampliar un programa orientado a objetos para manejar situaciones nuevas. Por ejemplo, puede derivar un subtipo nuevo de forma llamado pentágono sin modificar los métodos asociados sólo con las fonmas genéricas. Esta capacidad de ampliar fácilmente un diseño derivando nuevos subtipos es una de las principales fonnas de encapsular cambios. Esto mejora enormemente los diseños además de reducir el coste del mantenimiento del software. Sin embargo, existe un problema cuando se intenta tratar los objetos de tipos derivado como sus tipos base genéricos (círculos como formas, bicicletas como automóviles, cormoranes como aves, etc.). Si un método dice a una forma que se dibuje, O a un automóvil genérico que se ponga en marcha o a un ave que se mueva, el compilador no puede saber en tiempo de
10 Piensa en Java compilación de forma precisa qué parte del código tiene que ejecutar. Éste es el punto clave, cuando se envía el mensaje, el programador no desea saber qué parte del código se va a ejecutar; el método para dibujar se puede aplicar igualmente a un círculo, a un cuadrado o a un triángulo y los objetos ejecutarán el código apropiado dependiendo de su tipo específico. Si no se sabe qué fragmento de código se ej ecutará, entonces se añadirá un subtipo nuevo y el código que se ej ecute puede ser diferente sin que sea necesario realizar cambios en el método que lo llama. Por tanto, el compilador no puede saber de forma precisa qué fragmento de código hay qu e ejecutar y ¿qué hace entonces? Por ejemplo, en el siguiente diagrama, el objeto controladorAves sólo funciona con los objetos genéricos Ave y no sabe exactamente de qué tipo son. Desde la perspectiva del objeto controladorAves esto es adecuado ya que no ti ene que escribir código especial para determinar el tipo exacto de Ave con el que está trabajando ni el comportamiento de dicha Ave. Entonces, ¿cómo es que cuando se invoca al método moverO ignorando el tipo específico de Ave, se ejecuta el comportamiento correcto (un Ganso camina, vuela o nada y un Pingüino camina o nada)? Ave
controladorAves reubicarO
¿Qué ocurre cuando se
llama a moverO?
I
moverO
t
I
Ganso
Pingüino
moverO
moverO
La respuesta es una de las principales novedades de la programación orientada a objetos: el compilador no puede hacer una llamada a función en el sentido tradicional. La llamada a función generada por un compilador no-POO hace lo que se denomina un acoplamiento temprano, término que es posible que no haya escuchado antes. Significa que el compilador genera una llamada a un nombre de funció n específico y el sistema de tiempo de ejecución resuelve esta llamada a la dirección absoluta del código que se va a ejecutar. En la POO, el programa no puede determ inar la dirección del código hasta estar en tiempo de ejecución, por lo que se hace necesario algún otro esquema cuando se envía un mensaje a un objeto genérico. Para resolver el problema, los lenguajes orientados a objetos utilizan el concepto de acoplamiento tardío. Cuando se envía un mensaje a un objeto, el códi go al que se está llamando no se detelmina hasta el tiempo de ejecución. El compilador no asegura que el método exista, realiza una comprobación de tipos con los argumentos y devuelve un valor, pero no sabe exactamente qué código tiene que ejecutar. Para realizar el acoplamiento tardío, Java emplea un bit de código especial en lugar de una llamada absoluta. Este código calcula la dirección del cuerpo del método, utilizando la información almacenada en el objeto (este proceso se estudi a en detalle en el Capítulo 8, PolimOlfismo). Por tanto, cada objeto puede comportarse de forma di ferente de acuerdo con los contenidos de dicho bit de código especial. Cuando se envía un mensaj e a un objeto, realmente el objeto resuelve lo que tiene que hacer con dicho mensaje. En algunos lenguaj es debe establecerse explicitamente que un método tenga la flexibilidad de las propiedades del acoplamiento tardío (C++ utiliza la palabra clave virtual para ello). En estos lenguaj es, de manera predetenninada, los métodos no se acoplan de forma dinámica. En Java, el acoplam iento dinámico es el comportamiento predetenn inado y el programador no tiene que añadi r ninguna palabra clave adicional para definir el polimorfismo. Considere el ej emplo de las formas. La familia de clases (todas basadas en la misma interfaz uni forme) se ha mostrado en un diagrama anteriormente en el capítulo. Para demostrar el pol imorfismo, queremos escribir un fragmento de código que ignore los detalles específicos del tipo y que sólo sea indicado para la clase base. Dicho código se desacopla de la información específica del tipo y por tanto es más sencillo de escribir y de comprender. Y, por ejemplo, si se añade un tipo nuevo como Hexágono a través de la herencia, el código que haya escrito funcionará tanto para el nuevo tipo de Forma como para los tipos existentes. Por tanto, el programa es ampliable. Si escribe un método en Java (lo que pronto aprenderá a hacer) como el siguiente: void hacerAlgo(Forma forma) borrar. forma () ; / / ... dibujar . forma () i
{
1 Introducción a los objetos 11
Este método sirve para cualquier Forma, por lo que es independiente del tipo específico de objeto que se esté dibujando y borrando. Si alguna otra parte del programa utiliza el método hacerAlgoO: Circulo circulo = new Circulo() ; Triangulo triangulo = new Triangulo()j Linea linea = new Linea () ; hacerAlgo (c i r cul o ) i hacerAlgo (tr i angulo ) ; hacerAlgo (linea ) ;
Las llamadas a JiacerAlgo O funcionarán correctamente, independientemente del tipo exacto del objeto. De hecho, éste es un buen truco. Considere la línea: hacerAlgo (circulo )
j
Lo que ocurre aquí es que se está pasando un Circulo en un método que está esperando una Forma. Dado que un Circulo
es una Forma, hacerAlgoO puede tratarlo como tal. Es decir, cualquier mensaje que hacerAlgoO pueda enviar a Forma, un circulo puede aceptarlo. Por tanto, actuar así es completamente seguro y lógico. Llamamos a este proceso de tratar un tipo derivado como si fuera un tipo base upcasting (generalización). La palabra significa en inglés "proyección hacia arriba" y refleja la fonna en que se dibujan habitualmente los diagramas de herencia, con el tipo base en la parte superior y las clases derivadas abriéndose en abanico hacia abajo, upcasting es, por tanto, efectuar una proyección sobre un tipo base, ascendiendo por el diagrama de herencia. "Upcasting"
t
.- ______ Jo
,-_____J I
B
o
I
o
: o o
Círculo
Cuadrado
Triángulo
Un programa orientado a objetos siempre contiene alguna generalización, porque es la fonna de desvincularse de tener que conocer el tipo exacto con que se trabaja. Veamos el código de hacerAlgoO: forma .borrar()
i
II forma.dibu j ar() ;
Observe que no se dice, "si eres un Circulo, hacer esto, si eres un Cuadrado, hacer esto, etc.". Con este tipo de código lo que se hace es comprobar todos los tipos posibles de Forma, lo que resulta lioso y se necesitaría modificar cada vez que se añadiera una nueva clase de Forma. En este ejemplo, sólo se dice: "Eres una fonna, te puedo borrarO y dibujarO teniendo en cuenta correctamente los detalles". Lo que más impresiona del código del método hacerAlgoO es que, de alguna manera se hace lo correcto. U amar a dibujarO para Circulo da lugar a que se ejecute un código diferente que cuando se le llama para un Cuadrado o una Linea, pero cuando el mensaje dibujarO se envía a una Forma anónima, tiene lugar el comportamiento correcto basándose en el tipo real de la Forma. Esto es impresionante porque, como se ha dicho anteriormente, cuando el compilador Java está compilando el código de hacerAlgoO, no puede saber de forma exacta con qué tipos está tratando. Por ello, habitualmente se espera que llame a la versión de borrarO y dibujarO para la clase base Forma y no a la versión específica de Círculo, Cuadrado o Linea. Y sigue ocurriendo lo correcto gracias al polimorfismo. El compilador y el sistema de tiempo de ejecución controlan los detalles; todo lo que hay que saber es qué ocurre y, lo más importante, cómo diseñar haciendo uso de ello. Cuando se envía un mensaje a un objeto, el objeto hará lo correcto incluso cuando esté implicado el proceso de generalización.
La jerarquía de raíz única Uno de los aspectos de la POO que tiene una importancia especial desde la introducción de e++ es si todas las clases en últi ma instancia deberían ser heredadas de una única clase base. En Java (como en casi todos los demás lenguajes de POO
12 Piensa en Java
excepto e++) la respuesta es afinnativa. Y el nombre de esta clase base es simplemente Object. Resulta que las ventajas de una jerarquía de raíz única son enormes. Todos los objetos de una jerarquía de raíz única tienen una interfaz en común, por 10 que en última instancia son del mismo tipo fundamental. La alternativa (proporcionada por e++) es no saber que todo es del mismo tipo básico. Desde el punto de vista de la compatibilidad descendente, esto se ajusta al modelo de e mejor y puede pensarse que es menos restrictivo, pero cuando se quiere hacer programación orientada a objetos pura debe construirse una jerarquía propia con el fin de proporcionar la misma utilidad que se construye en otros lenguajes de programación orientada a objetos. Y en cualquier nueva biblioteca de clases que se adquiera, se empleará alguna otra interfaz incompatible. Requiere esfuerzo (y posiblemente herencia múltiple) hacer funcionar la nueva interfaz en un diseño propio. ¿Merece la pena entonces la "flexibilidad" adicional de e++? Si la necesita (si dispone ya de una gran cantidad de código en e), entonces es bastante valiosa. Si parte de cero, otras alternativas como Java a menudo resultan más productivas. Puede garantizarse que todos los objetos de una jerarquía de raíz única tengan una determinada funcionalidad. Es posible realizar detenninadas operaciones básicas sobre todos los objetos del sistema. Pueden crearse todos los objetos y el paso de argumentos se simplifica enonnemente. Una jerarquía de raíz única facilita mucho la implementación de un depurador de memoria, que es una de las mejoras fundamentales de Java sobre C++. y dado que la información sobre el tipo de un objeto está garantizada en todos los objetos, nunca se encontrará con un objeto cuyo tipo no pueda determinarse. Esto es especialmente importante en las operaciones en el nivel del sistema, corno por ejemplo el tratamiento de excepciones y para proporcionar un mayor grado de flexibilidad en la programación.
Contenedores En general, no se sabe cuántos objetos se van a necesitar para resolver un determinado problema o cuánto tiempo va a llevar. Tampoco se sabe cómo se van a almacenar dichos objetos. ¿Cómo se puede saber cuánto espacio hay que crear si no se conoce dicha infonnación hasta el momento de la ejecución? La solución a la mayoría de los problemas en el diseño orientado a objetos parece algo poco serio, esta solución consiste en crear otro tipo de objeto. El nuevo tipo de objeto que resuelve este problema concreto almacena referencias a otros objetos. Por supuesto, se puede hacer 10 mismo con una matriz, elemento que está disponible en la mayoría de los lenguajes. Pero este nuevo objeto, denominado contenedor (también se llama colección, pero la biblioteca de Java utiliza dicho término con un sentido diferente, por lo que en este libro emplearemos el término "contenedor"), se amplía por sí mismo cuando es necesario acomodar cualquier cosa que se quiera introducir en él. Por tanto, no necesitamos saber cuántos objetos pueden almacenarse en un contenedor. Basta con crear un objeto contenedor y dejarle a él que se ocupe de los detalles. Afortunadamente, los buenos lenguajes de programación orientada a objetos incluyen un conjunto de contenedores como parte del paquete. En e++, ese conjunto forma parte de la biblioteca estándar e++ y a menudo se le denomina STL (Standard Template Library, biblioteca estándar de plantillas). Smalltalk tiene un conjunto muy completo de contenedores, mientras que Java tiene también numerosos contenedores en su biblioteca estándar. En algunas bibliotecas, se considera que uno o dos contenedores genéricos bastan y sobran para satisfacer todas las necesidades, mientras que en otras (por ejemplo, en Java) la biblioteca tiene diferentes tipos de contenedores para satisfacer necesidades distintas: varios tipos diferentes de clases List (para almacenar secuencias), M aps (también denominados matrices asociativas y que se emplean para asociar objetos con otros objetos), Sets (para almacenar un objeto de cada tipo) y otros componentes como colas, árboles, pilas, etc. Desde el punto de vista del diseño, lo único que queremos es disponer de un contenedor que pueda manipularse para resolver nuestro problema. Si un mismo tipo de contenedor satisface todas las necesidades, no existe ninguna razón para disponer de varias clases de contenedor. Sin embargo, existen dos razones por las que sí es necesario poder disponer de diferentes contenedores. En primer lugar, cada tipo de contenedor proporciona su propio tipo de interfaz y su propio comportamiento externo. Umi pila tiene una interfaz y un comportamiento distintos que una cola, que a su vez es distinto de un conjunto o una lista. Es posible que alguno de estos tipos de contenedor proporcione una solución más flexible a nuestro problema que los restantes tipos. En segundo lugar, contenedores diferentes tienen una eficiencia distinta a la hora de realizar determinadas operaciones. Por ejemplo, existen dos tipos básicos de contenedores de tipo Lis!: ArrayList (lista matricial) y LinkedList (lista enlazada). Ambos son secuencias simples que pueden tener interfaces y comportamientos externos idénticos. Pero ciertas operaciones pueden llevar asociado un coste radicalmente distinto. La operación de acceder aleatoriamente a los elementos contenidos en un contenedor de tipo ArrayList es una operación de tiempo constante. Se tarda el mismo tiempo
1 Introducción a los objetos 13 independientemente de cuál sea el elemento que se haya seleccionado. Sin embargo, en un contenedor de tipo LinkedList resulta muy caro desplazarse a lo largo de la lista para seleccionar aleatoriamente un elemento, y se tarda más tiempo en localizar un elemento cuanto más atrás esté situado en la lista. Por otro lado, si se quiere insertar un elemento en mitad de la secuencia, es más barato hacerlo en un contenedor de tipo LinkedList que en otro de tipo ArrayList. Estas y otras operaciones pueden tener una eficiencia diferente dependiendo de la estructura subyacente de la secuencia. Podemos comenzar construyendo nuestro programa con un contenedor de tipo LinkedList y, a la hora de juzgar las prestaciones, cambiar a otro de tipo ArrayList. Debido a la abstracción obtenida mediante la interfaz List, podemos cambiar de un tipo de contenedor a otro con un impacto mínimo en el código.
Tipos parametrizados (genéricos) Antes de Java SES, los contenedores albergaban objetos del tipo universal de Java: Objee!. La j erarquía de raíz única indica que todo es de tipo Objeet, por lo que un contenedor que almacene objetos de tipo Object podrá almacenar cualquier
cosa. 6 Esto hacía que los contenedores fueran fáciles de reutilizar. Para utilizar uno de estos contenedores, simplemente se añaden a él referencias a objetos y luego se las extrae. Sin embargo, puesto que el contenedor sólo permite almacenar objetos de tipo Object, al añadir una referencia a objeto al contene-
dor, esa referencia se transforma en una referencia a Object perdiendo así su carácter. Al extraerla, se obtiene una referencia a Object y no una referencia al tipo que se hubiera almacenado. En estas condiciones, ¿cómo podemos transformar esa referencia en algo que tenga el tipo específico de objeto que hubiéramos almacenado en el contenedor?
Lo que se hace es volver a utilizar el mecanismo de transfonnación de tipos (cast), pero esta vez no efectuamos una generalización, subiendo por la jerarquía de herencia, sino que efectuamos una especialización, descendiendo desde la jerarquía hasta alcanzar un tipo más específico. Este mecanismo de transformación de tipos se denomina especialización (downcasting). Con el mecanismo de generalización (upcasting) , sabemos por ejemplo que un objeto Circulo es también de tipo Forma, por lo que resulta seguro realizar la transformación de tipos. Sin embargo, no todo objeto de tipo Object es nece-
sariamente de tipo Circulo o Forma por lo que no resulta tan seguro realizar una especialización a menos que sepamos concretamente lo que estamos haciendo. Sin embargo, esta operación no es del todo peligrosa, porque si efectuamos una conversión de tipos y transformamos el objeto a un tipo incorrecto, obtendremos un error de tipo de ejecución denominado excepción (lo que se describe más adelante). Sin embargo, cuando extraemos referencias a objetos de un contenedor, tenemos que disponer de alguna forma de recordar exactamente lo que son, con el fin de poder realizar la conversión de tipos apropiada. El mecanismo de especialización y las comprobaciones en tiempo de ejecución requieren tiempo adicional para la ejecución del programa y un mayor esfuerzo por parte del programador. ¿No sería más lógico crear el contenedor de manera que éste supiera el tipo de los elementos que almacena, eliminando la necesidad de efectuar conversiones de tipos y evitando los errores asociados? La solución a este problema es el mecanismo de tipos parametrizados. Un tipo parametrizado es una clase que el compilador puede personalizar automáticamente para que funcione con cada tipo concreto. Por ejemplo, con un contenedor parametrizado, el compilador puede personalizar dicho contenedor para que sólo acepte y devuelva objetos Forma.
Uno de los cambios principales en Java SES es la adición de tipos parametrizados, que se denominan genéricos en Java. El uso de genéricos es fáci lmente reconocible, ya que emplean corchetes angulares para encerrar alguna especificación de tipo, por ejemplo, puede crearse un contenedor de tipo ArrayList que almacene objetos de tipo Forma del siguiente modo: ArrayList
formas
~
new ArrayList () ;
También se han efectuado modificaciones en muchos de los componentes de las bibliotecas estándar para poder aprovechar el uso de genéricos. Como tendremos oportunidad de ver, los genéricos tienen una gran importancia en buena parte del código utilizado en este libro.
Creación y vida de los objetos Una de las cuestiones criticas a la hora de trabajar con los objetos es la forma en que éstos se crean y se destruyen. Cada objeto consigue una serie de recursos, especialmente memoria, para poder simplemente existir. Cuando un objeto deja de Los contenedores no permiten almacenar primitivas, pero la característica de alltobxing de Java SES hace que esta restricción tenga poca importancia. Hablaremos de esto en detalle más adelante en el libro.
6
14 Piensa en Java
ser necesario, es preciso eliminarlo, para que se liberen estos recursos y puedan emplearse en alguna otra cosa. En los casos más simples de programación, el problema de borrar los objetos no resulta demasiado complicado. Creamos el objeto, lo usamos mientras que es necesario y después lo destruimos. Sin embargo, no es dificil encontrarse situaciones bastante más complejas que ésta. Suponga por ejemplo que estamos diseñando un sistema para gestionar el tráfico aéreo de un aeropuerto (el mismo modelo serviría para gestionar piezas en un almacén o para un sistema de alquiler de vídeos o para una tienda de venta de mascotas). A primera vista, el problema parece muy simple: creamos un contenedor para almacenar las aeronaves y luego creamos una nueva aeronave y la insertamos en el contenedor por cada una de las aeronaves que entren en la zona de control del tráfico aéreo. De cara al borrado, simplemente basta con eliminar el objeto aeronave apropiado en el momento en que el avión abandone la zona. Pero es posible que tengamos algún otro sistema en el que queden registrados los datos acerca de los aviones; quizá se trate de datos que no requieran una atención tan inmediata como la de la función principal de control del tráfico aéreo. Puede que se trate de un registro de los planes de vuelo de todos los pequeños aeroplanos que salgan del aeropuerto. Entonces, podríamos definir un segundo contenedor para esos aeroplanos y, cada vez que se creara un objeto aeronave, se introduciría también en este segundo contenedor si se trata de un pequeño aeroplano. Entonces, algún proceso de segundo plano podría realizar operaciones sobre los objetos almacenados en este segundo contenedor en los momentos de inactividad. Ahora el problema ya es más complicado: ¿cómo podemos saber cuándo hay que destruir los objetos? Aún cuando nosotros hayamos terminado de procesar un objeto, puede que alguna otra parte del sistema no lo haya hecho. Este mismo problema puede surgir en muchas otras situaciones, y puede llegar a resultar enonnemente complejo de resolver en aquellos sistemas de programación (como C++) en los que es preciso borrar explícitamente un objeto cuando se ha terminado de utilizar. ¿Dónde se almacenan los datos correspondientes a un objeto y cómo se puede controlar el tiempo de vida del mismo? En C++, se adopta el enfoque de que el control de la eficiencia es el tema más importante, por lo que todas las decisiones quedan en manos del programador. Para conseguir la máxima velocidad de ejecución, las características de almacenamiento y del tiempo de vida del objeto pueden determinarse mientras se está escribiendo el programa, colocando los objetos en la pila (a estos objetos se los denomina en ocasiones variables automáticas o de ámbito) o en el área de almacenamiento estático. Esto hace que lo más prioritario sea la velocidad de asignación y liberación del almacenamiento, y este control puede resultar muy útil en muchas situaciones. Sin embargo, perdemos flexibilidad porque es preciso conocer la cantidad, el tiempo de vida y el tipo exacto de los objetos a la hora de escribir el programa. Si estamos tratando de resolver un problema más general, como por ejemplo, un programa de diseño asistido por computadora, un sistema de gestión de almacén o un sistema de control de tráfico aéreo, esta solución es demasiado restrictiva. La segunda posibilidad consiste en crear los objetos dinámicamente en un área de memoria denominada cúmulo. Con este enfoque, no sabemos hasta el momento de la ejecución cuántos objetos van a ser necesarios, cuál va a ser su tiempo de vida ni cuál es su tipo exacto. Todas estas características se determinan en el momento en que se ejecuta el programa. Si hace falta un nuevo objeto, simplemente se crea en el cúmulo de memoria, en el preciso instante en que sea necesario. Puesto que el almacenamiento se gestiona dinámicamente en tiempo de ejecución, la cantidad de tiempo requerida para asignar el almacenamiento en el cúmulo de memoria puede ser bastante mayor que el tiempo necesario para crear un cierto espacio en la pila. La creación de espacio de almacenamiento en la pila requiere normalmente tilla única instrucción de ensamblador, para desplazar hacia abajo el puntero de la pila y otra instrucción para volver a desplazarlo hacia arriba. El tiempo necesario para crear un espacio de almacenamiento en el cúmulo de memoria depende del diseño del mecanismo de almacenamiento. La solución dinámica se basa en la suposición, generalmente bastante lógica, de que los objetos suelen ser complicados, por lo que el tiempo adicional requerido para localizar el espacio de almacenamiento y luego liberarlo no tendrá demasiado impacto sobre el proceso de creación del objeto. Además, el mayor grado de flexibilidad que se obtiene resulta esencial para resolver los problemas de programación de carácter general. Java utiliza exclusivamente un mecanismo dinámico de as ignación de memoria7 . Cada vez que se quiere crear un objeto, se utiliza el operador new para constmir una instancia dinámica del objeto. Sin embargo, existe otro problema, referido al tiempo de vida de un objeto. En aquellos lenguajes que permiten crear objetos en la pila, el compilador determina cuál es la duración del objeto y puede destruirlo automáticamente. Sin embargo, si creamos el objeto en el cúmulo de memoria, el compilador no sabe cuál es su tiempo de vida. En un lenguaje como C++, 7 Los tipos primitivos, de los que hablaremos en breve, representun un caso especial.
1 Introducció n a los objetos 15
es preciso determinar mediante programa cuándo debe destruirse el objeto, lo que puede provocar pérdidas de memori a si no se real iza esta tarea correctamente (y este problema resulta bastante común en los programas C++). lava proporciona una característica denominada depurador de memoria, que descubre automáti camente cuándo un determinado obj eto ya no está en uso, en cuyo caso lo destruye. Un depurador de memoria resu lta mucho más cómodo que cualqui er otra solución alternativa, porque reduce el número de problemas que e l programador debe controlar, y reduce también la cantidad de código que hay que escribir. Además, lo que resulta más importante, el depurador de memoria proporciona un ni vel mucho mayor de protección contra el insidioso problema de las fugas de memoria, que ha hecho que muchos proyectos en C++ fracasaran. En Java, el depurador de memoria está diseñado para encargarse del problema de liberación de la memoria (aunque esto no incluye otros aspectos relativos al borrado de un objeto). El depurador de memoria "sabe" cuándo ya no se está usando un objeto, en cuyo caso libera automáticamente la memori a correspondiente a ese objeto. Esta característica, combinada con el hecho de que todos los objetos heredan de la clase raíz Object, y con el hecho de que sólo pueden crearse obj etos de una manera (en el cúmulo de memoria). hace que el proceso de programación en Java sea mucho más simple que en C++, hay muchas menos decisiones que tomar y muchos menos problemas que resolver.
Tratamiento de excepciones: manejo de errores Desde la aparición de los lenguajes de programación, el tratami ento de los errores ha constituido un problema peculiann ente dificil. Debido a que resulta muy complicado di señar un buen esquema de tratamiento de errores! muchos lenguajes simplemente ignoran este problema, dejando que lo resue lvan los diseftadores de bibliotecas, que al final terminan desarrollando soluciones parciales q ue funcionan en muchas situaciones pero cuyas medidas pueden ser obviadas fác ilmente; generalmente, basta con ignorarlas. Uno de los problemas princi pales de la mayoría de los esquemas de tratamiento de errores es que dependen de que el programador tenga cuidado a la hora de seguir un convenio preestablecido que no resulta obligatorio dentro del leng uaj e. Si el programador no tiene cuidado (lo cual suele suceder cuando hay prisa por terminar un proyecto), puede olvi darse fáci lmente de estos esquemas. Los mecanismos de tratamiento de excepciones integran la gestión de errores directamente dentro del lenguaje de programación. en ocasiones. dentro incluso del sistema operativo. Una excepción no es más que un objeto "generado" en el lugar donde se ha producido el error y que puede ser "capturado" mediante una rutina apropiada de tratamiento de excepciones diseñada para gestionar dicho tipo particular de error. Es como si el tratamiento de excepciones fuera una ruta de ejecución paralela y dife rente, que se toma cuando algo va mal. Y, como se utili za una ruta de ejecución independiente. ésta no tiene porqué interfe rir con el código que se ejecuta nonnalmente. Esto hace que el código sea más simple de escribir. porque no es necesario comprobar constantemente la existencia de eITores. Además. las excepciones generadas se diferencian de los típ icos valores de error devueltos por los métodos o por los indicadores activados por los métodos para avisar que se ha producido una condición de error; tanto los valores como los indicadores de error podrían ser ignorados por el programador. Las excepciones no pueden ignorarse. por 10 que se garantiza que en algún momento serán tratadas. Finalmente. las excepciones proporcionan un mecanismo para recuperarse de manera fiable de cualquier situación errónea. En lugar de limitarse a salir del programa. a menudo podemos corregir las cosas y restaurar la ejecución , lo que da como resultado programas mucho más robustos. El tratamiento de excepciones de Java resulta muy sobresal iente entre los lenguaj es de programación, porque en Java el tratamiento de excepciones estaba previsto desde el principio y estamos obligados a utili zarlo. Este esquema de tratamiento de excepciones es el único mecanismo aceptable en Java para informar de la existencia de errores. Si no se escribe el código de manera que trate adecuadamente las excepciones se obtiene un error en tiempo de compilación. Esta garantía de coherencia puede hacer que, en ocasiones. el tratamiento de erro res sea mucho más sencillo. Merece la pena resaltar que el tratamiento de excepciones no es una característica orientada a objetos, au nque en los lenguajes de programación orientada a objetos las excepciones se representan normalmente medi ante un objeto. Los mecanismos de tratamiento de excepciones ya existían antes de que hicieran su aparición los lenguajes orientados a objetos.
Programación concurrente Un concepto fundamental en el campo de la programación es la idea de poder gestionar más de una tarea al mismo tiempo. Muchos prob lemas de programación requieren que el programa detenga la tarea que estuviera rea liza ndo, resuelva algún
16 Piensa en Java
otro problema y luego vuelva al proceso principal. A lo largo del tiempo, se ha tratado de aplicar diversas soluciones a este problema. Inicialmente, los programadores que tenían un adecuado conocimiento de bajo nivel de la máquina sobre la que estaban programando escribían rutinas de servicio de interrupción, y la suspensión del proceso principal se llevaba a cabo mediante una interrupción hardware. Aunque este esquema funcionaba bien, resultaba complicado y no era portable, por lo que traducir un programa a un nuevo tipo de máquina resultaba bastante lento y muy caro.
En ocasiones, las interrupciones son necesarias para gestionar las tareas con requisitos críticos de tiempo, pero hay una amplia clase de problemas en la que tan sólo nos interesa dividir el problema en una serie de fragmentos que se ejecuten por separado (tareas), de modo que el programa completo pueda tener un mej or tiempo de respuesta. En un programa, estos fragmentos que se ejecutan por separado, se denominan hebras y el conjunto general se llama concurrencia. Un ejemplo bastante común de concurrencia es la interfaz de usuario. Utilizando distintas tareas, el usuario apretando un botón puede obtener una respuesta rápida, en lugar de tener que esperar a que el programa finalice con la tarea que esté actualmente realizando. Normalmente, las tareas son sólo una forma de asignar el tiempo disponible en un único procesador. Pero si el sistema operativo soporta múltiples procesadores, puede asignarse cada tarea a un procesador distinto, en cuyo caso las tareas pueden ejecutarse realmente en paralelo. Una de las ventajas de incluir los mecanismos de concurrencia en el nivel de lenguaje es que el programador no tiene que preocuparse de si hay varios procesadores o sólo uno; el programa se divide desde el punto de vista lógico en una serie de tareas, y si la máquina dispone de más de un procesador, el programa se ejecutará más rápido, sin necesidad de efectuar ningún ajuste especial. Todo esto hace que la concurrencia parezca algo bastante sencillo, pero existe un problema: los recursos compartidos. Si se están ejecutando varias tareas que esperan poder acceder al mismo recurso, tendremos un problema de contienda entre las tareas. Por ejemplo, no puede haber dos procesadores enviando información a una misma impresora. Para resolver el problema, los recursos que puedan compartirse, como por ejemplo una impresora, deben bloquearse mientras estén siendo utilizados por una tarea. De manera que la forma de funcionar es la siguiente: una tarea bloquea un recurso, completa el trabajo que tuviera asignado y luego elimina el bloqueo para que alguna otra tarea pueda emplear el recurso.
Los mecanismos de concurrencia en Java están integrados dentro de lenguaje y Java SES ha mejorado significativamente el soporte de biblioteca para los mecanismos de concurrencia.
Java e Internet Si Java no es, en defmitiva, más que otro lenguaje informático de programación, podríamos preguntamos por qué es tan importante y por qué se dice de él que representa una auténtica revolución dentro del campo de la programación. La respuesta no resulta obvia para aquéllos que provengan del campo de la programación tradicional. Aunque Java resulta muy útil para resolver problemas de programación en entornos autónomos, su importancia se debe a que permite resolver los problemas de programación que surgen en la World Wide Web.
¿Qué es la Web? Al principio, la Web puede parecer algo misterioso, con todas esas palabras extrañas como "surfear," "presencia web" y "páginas de inicio". Resulta útil, para entender los conceptos, dar un paso atrás y tratar de comprender lo que la Web es realmente, pero para ello es necesario comprender primero lo que son los sistemas cliente/servidor, que constituyen otro campo de la informática lleno de conceptos bastante confusos.
Informática cliente/servidor La idea principal en la que se basan los sistemas cliente/servidor es que podemos disponer de un repositorio centralizado de información· (por ejemplo, algún tipo de datos dentro de una base de datos) que queramos distribuir bajo demanda a una serie de personas o de computadoras. Uno de los conceptos clave de las arquitecturas cliente/servidor es que el repositorio de información está centralizado, por lo que puede ser modificado sin que esas modificaciones se propaguen hasta los consumidores de la información. El repositorio de información, el software que distribuye la información y la máquina o máquinas donde esa información y ese software residen se denominan, en conjunto, "servidor". El software que reside en las máquinas consumidoras, que se comunica con el servidor, que extrae la infonnación, que la procesa y que luego la muestra en la propia máquina consumidora se denomina cliente.
1 Introducción a los objetos 17 El concepto básico de informática cliente/servidor no es, por tanto, demasiado complicado. Los problemas surgen porque disponemos de un único servidor tratando de dar servicio a múltiples clientes al mismo ti empo. Generalmente, se utiliza algún tipo de sistema de gestión de bases de datos de modo que el diseñador "equilibra" la disposición de los datos entre distintas tablas, con el fin de optimizar el uso de los datos. Además, estos sistemas permiten a menudo que los clientes inserten nueva información dentro de un servidor. Esto quiere decir que es preciso garantizar que los nuevos datos de un cliente no sobreescriban los nuevos datos de otro cliente, al igual que hay que garantizar que no se pierdan datos en el proceso de añadirlos a la base de datos (este tipo de mecanismos se denomina procesamiento de transacciones) . A medida que se realizan modificaciones en el software de cliente, es necesario diseñar el software, depurarlo e instalarlo en las máquinas cliente, lo que resulta ser más complicado y más caro de lo que en un principio cabría esperar. Resulta especialmente problemático soportar múltiples tipos de computadoras y de sistemas operativos. Finalmente, es necesario tener en cuenta también la cuestión crucial del rendimiento: puede que tengamos cientos de clientes enviando cientos de solicitudes al servidor en un momento dado, por 10 que cualquier pequeño retardo puede llegar a ser verdaderamente crítico. Para min imizar la latencia, los programadores hacen un gran esfuerzo para tratar de descargar las tareas de procesamiento que en ocasiones se descargan en la máquina cliente, pero en otras ocasiones se descargan en otras máquinas situadas junto al servidor, utilizando un tipo especial de software denominado middleware, (el middleware se utiliza también para mejorar la facilidad de mantenimiento del sistema). Esa idea tan simple de distribuir la infonnación tiene tantos niveles de complejidad que el problema global puede parecer enigmáticamente insoluble. A pesar de lo cual, se trata de un problema crucial: la informática cliente/servidor representa aproximadamente la mitad de las actividades de programación en la actualidad. Este tipo de arquitectura es responsable de todo tipo de tareas, desde la introducción de pedidos y la realización de transacciones con tarjetas de crédito basta la distribución de cualquier tipo de datos, como por ejemplo cotizaciones bursátiles, datos científicos, infonnación de organismos gubernamentales. En el pasado, lo que hemos hecho es desarrollar soluciones individuales para problemas individuales, inventando una nueva solución en cada ocasión. Esas soluciones eran dificiles de diseñar y de utilizar, y el usuario se veía obligado a aprender una nueva interfaz en cada caso. De este modo, se llegó a un punto en que era necesario resolver el problema global de la infonnática cliente/servidor de una vez y para siempre.
La Web como un gigantesco servidor La Web es, en la práctica, un sistema gigantesco de tipo cliente/servidor. En realidad, es todavía más complejo, ya que lo que tenemos es un conjunto de servidores y clientes que coexisten en una misma red de manera simultánea. El usuario no necesita ser consciente de ello, por 10 que lo único que hace es conectarse con un servidor en cada momento e interactuar con él (aún cuando para llegar a ese servidor haya sido necesario ir saltando de servidor en servidor por todo el mundo hasta dar con el correcto). Inicialmente, se trataba de un proceso muy simple de carácter unidireccional: el usuario enviaba una solicitud al servidor y éste le devolvía un archivo, que el software explorador de la máquina (es decir, el cliente) se encargaba de interpretar, efectuando todas las tareas de formateo en la propia máquina local. Pero al cabo de muy poco tiempo, los propietarios de servidores comenzaron a querer hacer cosas más complejas que simplemente suministrar páginas desde el servidor. Querían disponer de una capacidad completa cliente/servidor, de forma que el cliente pudiera, por ejemplo enviar información al servidor, realizar búsquedas en una base de datos instalada en el servidor, añadir nueva infonnación al servidor o realizar un pedido (lo que requiere medidas especiales de seguridad). Éstos son los cambios que hemos vivido en los últimos años en el desarrollo de la Web. Los exploradores web representaron un gran avance: permitieron implementar el concepto de que un mismo fragmento de infonnación pudiera visualizarse en cualquier tipo de computadora sin necesidad de efectuar ninguna modificación. Sin embargo, los primeros exploradores eran bastante primitivos y se colapsaban rápidamente debido a las demandas que se les bacía. No resultaban peculiarmente interactivos y tendían a sobrecargar al servidor tanto como a la propia red Internet, porque cada vez que hacía falta hacer algo que requería programación, era necesario devolver la infonnación al servidor para que éste la procesara. De esta fonna, podía tardarse varios segundos o incluso minutos en averiguar simplemente que habíamos tecleado incorrectamente algo dentro de la solicitud. Como el explorador era simplemente un mecanismo de visualización no podía realizar ni siquiera la más simple de las tareas (por otro lado, resultaba bastante seguro ya que no podía ejecutar en la máquina local ningún programa que pudiera contener errores o virus). (
Para resolver este problema, se han adoptado diferentes enfoques. Para empezar se han mejorado los estándares gráficos para poder disponer de mejores animaciones y vídeos dentro de los exploradores. El resto del problema sólo puede resol -
18 Piensa en Java
verse incorporando la capacidad de ejecutar programas en el extremo cliente, bajo control del explorador. Esto se denomina programación del lado del c1ientc.
Programación del lado del cliente El diseño inicial de la Web, basado en una arquitectura servidor/explorador, permitía disponer de contenido interactivo, pero esa interacti vidad era completamente proporcionada por el servidor. El servidor generaba páginas estáticas para el explorador cliente, que si mplemente se encargaba de interpretarlas y mostrarlas. El lenguaje básico HTML (HyperText Markup Language) contiene una serie de mecanismos simpl es para la introducción de datos: recuadros de introducción de texto, casillas de verificación, botones de opción, listas normales y listas desplcgables, así como un botón que sólo podía programarse para bonar los datos del formulario o enviarlos al servidor. Ese proceso de envío se llevaba a cabo a través de la interfaz CGI (Common Gateway lntelface) incluida en todos los servidores web. El texto incorporado en el envío le dice a la interfaz CGI 10 que tiene que hacer. La acción más común, en este caso, consiste en ejecutar un programa ubicado en un servidor en un directorio normalmente llamado "cgi-bin" (si observa la barra de direcciones situada en la parte superior del ex plorador cuando pulse un botón en una página web, en ocasiones podrá ver las palabras "cgi-bin" como parte de la dirección). Estos programas del lado del servidor pueden escribirse en casi cualquier lenguaje. Perl es uno de los lenguajes más utilizados para este tipo de tareas, porque está diseñado específicamente para la manipulación de textos y es un lenguaje inte rpretado, por 10 que se puede instalar en cualquier servidor independientemente de cuál sea su procesador o su sistema operativo. Sin embargo, otro lenguaje, Python (WwHePython.org) se está abriendo camino rápidamente, debido a su mayor potencia y su mayor simplicidad. Hay muchos sitios web potentes en la actualidad diseñados estrictamente con CGI, y lo cierto es que con CGI se puede hacer prácticamente de todo. Sin embargo, esos sitios web basados en programas CGI pueden llegar a ser rápidamente bastante complicados de mantener, y además pueden aparecer problemas en lo que se refiere al tiempo de respuesta. El tiempo de respuesta de un programa CGl depende de cuántos datos haya que enviar, de la carga del servidor y de la red Internet (además, el propio arranque de un programa COI tiende a ser bastante lento). Los primeros diseñadores de la Web no previeron la rapidez con que el ancho de banda disponible iba a agotarse debido a los tipos de aplicaciones que la gente llegaría a desarrollar. Por ejemplo, es casi imposible diseñar de manera coherente una aplicación con gráficos dinámicos, porque es necesario crear un archivo GIF (Graphics !nterchange Formal) y enviarlo del servidor al cliente para cada versión de gráfico. Además, casi todos los usuarios hemos experimentado lo engorroso del proceso de validación de los datos dentro de un formulario enviado a través de la Web. El proceso es el siguiente: pulsamos el botón de envío de la página; se envían los datos al servidor, el servidor arranca un programa CGI que descubre un error, fonnatea una página HTM L en la que nos informa del error y devuelve la página al cliente; a continuación, es necesario que el usuario retroceda una página y vuelva a intentarlo. Este enfoque no sólo resulta lento sino también poco elegante. La solución consiste en usar un mecanismo de programación del lado del cliente. La mayoría de las computadoras de sobremesa que incluyen un explorador wcb son máquinas bastante potentes, capaces de realizar tareas muy com plejas; con el enfoque ori gina l basado en HTML estático, esas potentes máqui nas simplemente se limitan a esperar sin hacer nada, hasta que el servidor se digna a enviarles la siguiente página. La programación del lado del cliente pennite asignar al explorador web todo el trabajo que pueda llevar a cabo, con lo que el resultado para los usuarios es una experiencia mucha más rápida y más interactiva a la hora de acceder a los sitios web. El problema con las explicaciones acerca de la programación del lado del cliente es que no se diferencian mucho de las explicaciones relativas a la programación en general. Los parámetros son prácticamente idénticos, aunque la plataforma sea distinta: un explorador web es una especie de sistema operativo limitado. En último ténn ino, sigue siendo necesario diseñar programas, por lo que los problemas y soluciones que nos encontramos dentro del campo de la programación del lado del cliente son bastante tradicionales. En el resto de esta sección, vamos a repasar algunos de los principales problemas y técnicas que suelen encontrarse en el campo de la programación del lado del cliente.
Plug-íns Uno de los avances más significativos en la programación del lado del cliente es el desarrollo de lo que se denomina plugin. Se trata de un mecanismo mediante el que un programador puede añadir algún nuevo tipo de funcionalidad a un explorador descargando un fragmento de código que se inserta en el lugar apropiado dentro de l explorador. Ese fragmento de código le dice al explorador: "A partir de ahora puedes real izar este nuevo tipo de actividad" (sólo es necesario descargar el
1 Introducción a los objetos 19 plug-in una vez). Podemos añadir nuevas fannas de comportamiento, potentes y rápidas, a los exploradores mediante plug-ins, pero la escritura de unplug-in no resulta nada trivial, y por eso mismo no es conveniente acometer ese tipo de tarea como parte del proceso de construcción de un sitio \Veb. El va lor de un plug-in para la programación del lado del cliente es que pernlite a los programadores avanzados desarrollar extensiones y añadírselas a un explorador sin necesidad de pedir permiso al fabricante del explorador. De esta fonna , los plug-ins proporcionan una especie de "puerta trasera" que pennite la creación de nuevos lenguajes de programación del lado del cliente (aunq ue no todos los lenguajes se implementan como plug-ins).
Lenguajes de script Los plug-ins dieron como resultado el desarrollo de lenguajes de scrip! para los exploradores. Con un lenguaje de script, e l código fuente del programa del lado del cliente se integra directamente dentro de la página HTML, y el plllg-in que se encarga de interpretar ese lenguaj e se activa de manera automática en el momento de visualizar la página HTML. Los lenguajes de script suelen ser razonablemente fáci les de comprender, y como están formados simplemente por texto que se incluye dentro de la propia página HTML, se cargan muy ráp idamente como parte del acceso al servidor mediante el que se obtiene la página. La desventaja es que el código queda expuesto, ya que cualquiera puede verlo (y copiarlo). Generalmente, sin embargo, los programadores no llevan a cabo tareas extremadamente sofisticadas con los lenguajes de script, así que este problema no resulta particularmente grave. Uno de los lenguajes de scripl que los exploradores web suelen soportar sin necesidad de un p lllg-in es JavaScript (ellenguaje JavaScript sólo se asemeja de forma bastante vaga a Java, por lo que hace falta un esfuerzo de aprendizaje adicional para llegar a dominarlo; recibió el nombre de JavaScript simplemente para aprovechar el impulso inicial de marketing de Java). Lamentablemente, cada explorador web implementaba originalmente JavaScript de forma distinta a los restantes ex ploradores web, e incluso en ocasiones, de fonna diferente a otras versiones del mismo explorador. La estandarización de JavaScript mediante el diseño del lenguaje estándar ECMAScript ha resuelto parcialmente este problema, pero tuvo que transcurrir bastante ti empo hasta que los distintos exploradores adoptaron el estándar (el problema se complicó porque Microsoft trataba de conseguir sus propios objetivos presionando en favor de su lenguaje VBScript, que también se asemejaba vagamente a JavaScript). En general, es necesario llevar a cabo la programación de las páginas utilizando una especie de mínimo común denom inador de JavaScripr, si lo que queremos es que esas páginas puedan visualizarse en todos los tipos de exploradores. Por su parte, la solución de errores y la dep uración en JavaScript son un auténtico lío. Como prueba de lo dificil que resulta diseñar un problema complejo con JavaScript, sólo muy recientemente alguien se ha atrevido a crear un a aplicación compleja basada en él (Google, con GMail), y ese desarrollo requirió una dedicación y una experiencia realmente notables. Lo que todo esto nos sugiere es que los lenguajes de script que se emplean en los exploradores web están di señados, realmente, para resolver tipos específicos de problemas, principalmente el de la creación de in terfaces gráficas de usuario (GUT) más ricas e interactivas. Sin embargo, un lenguaje de script puede resolver quizá un 80 por ciento de los problemas que podemos encontrar en la programación del lado del cliente. Es posible que los problemas que el lector quiera resolver es tén incluidos dentro de ese 80 por ciento. Si esto es así, y teniendo en cuenta que los lenguaj es de scripl permiten realizar los desarrollos de fonna más fácil y rápida, probablemente sería conveniente ver si se puede resolver un tipo concreto de problema empleando un lenguaje de script, antes de considerar otras soluciones más complejas, como la programación en Java.
Java Si un lenguaje de scripl puede resolver el 80 por ciento de los problemas de la programación del lado del cliente, ¿qué pasa con el otro 20 por ciento, con los "problemas realmente dificiles"? Java representa una solución bastante popular para este tipo de problemas. No sólo se trata de un potente lenguaje de programación diseñado para ser seguro, inte¡plataforma e internacional, sino que continuamente está siendo ampliado para proporcionar nuevas características del lenguaje y nuevas bibliotecas que penniten gestionar de manera elegante una seri e de problemas que resultan bastante dificiles de tratar en los lenguajes de programación tradi cionales, como por ejemplo la concurrencia, el acceso a bases de datos, la programación en red y la infonuática distribuida. Java permite reso lver los problemas de programación del lado del cliente utilizando applets y Java Web Starl. Un applet es un mini-programa que sólo puede ejecutarse sobre un explorador web. El applet se descarga automáticamente como parte de la página web (de la misma forma que se descarga automáticamente, por ejemplo, un gráfico). Cuando se acti va el applet, ejecuta un programa. Este meca ni smo de ejecución automática forma parte de la belleza de esta solución: nos proporciona una fonna de distribuir automáticamente el software de cliente desde el servidor en el mismo momento en
20 Piensa en Java
que el usuario necesita ese software de cliente, y no antes. El usuario obtiene la última versión del software de cliente, libre de errores y sin necesidad de realizar complejas reinstalaciones. Debido a la forma en que se ha diseñado Java, el programador sólo tiene que crear un programa simple y ese programa funcionará automáticamente en todas las computadoras que dispongan de exploradores que incluyan un intérprete integrado de Java (lo que incluye la inmensa mayoría de máquinas). Puesto que Java es un lenguaje de programación completo podemos llevar a cabo la mayor cantidad de trabajo posible en el cliente, tanto antes como después de enviar solicitudes al servidor. Por ejemplo, no es necesario enviar una solicitud a través de Internet simplemente para descubrir que hemos escrito mal una fecha o algún otro parámetro; asimismo, la computadora cliente puede encargarse de manera rápida de la tarea de dibujar una serie de datos, en lugar de esperar a que el servidor genere el gráfico y devuelva una imagen al explorador. De este modo, no sólo aumentan de forma inmediata la velocidad y la capacidad de respuesta, sino que disminuyen también la carga de trabajo de los servidores y el tráfico de red, evitando así que todo Internet se ralentice.
Alternativas Para ser honestos, los applets Java no han llegado a cumplir con las expectativas iniciales. Después del lanzamiento de Java, parecía que los applets era lo que más entusiasmaba a todo el mundo, porque iban a permitir finalmente realizar tareas serias de programación del lado del cliente, iban a mejorar la capacidad de respuesta de las aplicaciones basadas en Internet e iban a reducir el ancho de banda necesario. Las posibilidades que todo el mundo tenía en mente eran inmensas. Sin embargo, hoy día nos podemos encontrar con unos applets realmente interesantes en la Web, pero la esperada migración masiva hacia los applets no llegó nunca a producirse. El principal problema era que la descarga de 10MB necesaria para instalar el entorno de ejecución JRE (Java Runtime Environment) era demasiado para el usuario medio. El hecho de que Microsoft decidiera no incluir el entorno JRE dentro de Internet Explorer puede ser lo que acabó por determinar su aciago destino. Pero, sea como sea, lo cierto es que los applets Java no han llegado nunca a ser utilizados de forma masiva. A pesar de todo, los applets y las aplicaciones Java Web Start siguen siendo adecuadas en algunas situaciones. En todos aquellos casos en que tengamos control sobre las máquinas de usuario, por ejemplo en una gran empresa, resulta razonable distribuir y actualizar las aplicaciones cliente utilizando este tipo de tecnologías, que nos pueden ahorrar una cantidad considerable de tiempo, esfuerzo y dinero, especialmente cuando es necesario realizar actualizaciones frecuentes. En el Capítulo 22, Interfaces gráficas de usuario, analizaremos una buena tecnología bastante prometedora, Flex de Macromedia, que permite crear equivalentes a los applels basados en Flash. Como el reproductor Flash Player está disponible en más del 98 por ciento de todos los exploradores web (incluyendo Windows, Linux y Mac), puede considerarse como un estándar de facto. La instalación O actualización de Flash Player es asimismo rápida y fácil. El lenguaje ActionScript está basado en ECMAScript, por lo que resulta razonablemente familiar, pero Flex permite realizar las tareas de programación sin preocuparse acerca de las especifidades de los exploradores, por 10 que resulta bastante más atractivo que JavaScript. Para la programación del lado del cliente se trata de una alternativa que merece la pena considerar.
.NETy C# Durante un tiempo, el competidor principal de los applets de Java era ActiveX de Microsoft, aunque esta tecnología requería que en el cliente se estuviera ejecutando el sistema operativo Windows. Desde entonces, Microsoft ha desarrollado un competidor de Java: la plataforma .NET y el lenguaje de programación C#. La plataforma .NET es, aproximadamente, equivalente a la máquina virtual Java (NM, Java Virtual Machine; es la plataforma software en la que se ejecutan los programas Java) y a las bibliotecas Java, mientras que C# tiene similitudes bastante evidentes con Java. Se trata, ciertamente, del mejor intento que Microsoft ha llevado a cabo en el área de los lenguajes de programación. Por supuesto, Microsoft partía con la considerable ventaja de conocer qué cosas habían funcionado de manera adecuada y qué cosas no funcionaban tan bien en Java, por lo que aprovechó esos conocimientos. Desde su concepción, es la primera vez que Java se ha encontrado con un verdadero competidor. Como resultado, los diseñadores de Java en Sun han analizado intensivamente C# y las razones por las que un programador podría sentirse tentado a adoptar ese lenguaje, y han respondido introduciendo significativas mejoras en Java, que han resultado en el lanzamiento de Java SE5. Actualmente, la debilidad principal y el problema más importante en relación con .NET es si Microsoft pennitirá portarlo completamente a otras plataformas. Ellos afIrman que no hay ningún problema para esto, y el proyecto Mono (www.gomono.com) dispone de una implementación parcial de .NET sobre Linux, pero hasta que la implementación sea completa y Microsoft decida no recortar ninguna parte de la misma, sigue siendo una apuesta arriesgada adoptar .NET como solución interplataforma.
Introducción a los objetos 21
Redes Internet e intranet La Web es la solución más general para el problema de las arquitecturas cliente/servidor, por lo que tiene bastante sentido utilizar esta misma tecnología para resolver un cierto subconjunto de ese problema: el problema clásico de las arquitecturas cliente/servidor internas a una empresa. Con las técnicas tradicionales cliente/servidor, nos encontramos con el problema de la existencia de múltiples tipos de computadoras cliente, así como con la dificultad de instalar nuevo software de cliente; los exploradores web y la programación del lado del cliente permiten resolver fácilmente ambos problemas. Cuando se utiliza tecnología web para una red de infonnación restringida a una empresa concreta, la arquitectura resultante se denomina intranet. Las intranets proporcionan un grado de seguridad mucho mayor que Internet, ya que podemos controlar fisicamente el acceso a los equipos de la empresa. En términos de formación, una vez que los usuarios comprenden el concepto general de explorador les resulta mucho más fácil asumir las diferencias de aspecto entre las distintas páginas y applets, por lo que la curva de aprendizaje para los nuevos tipos de sistemas se reduce. El problema de seguridad nos permite analizar una de las divisiones que parecen estarse formando de manera automática en el mundo de la programación del lado del cliente. Si nuestro programa se está ejecutando en Internet no sabemos en que plataforma se ejecutará y además es necesario poner un cuidado adicional en no diseminar código que contenga errores. En estos casos, es necesario disponer de un lenguaje interplatafonna y seguro, como por ejemplo, un lenguaje de script o Java. Sí nuestra aplicación se ejecuta en una intranet, es posible que el conjunto de restricciones sea distinto. No resulta extraño que todas las máquinas sean plataformas Tntel/Windows. En una intranet, nosotros somos responsables de la calidad de nuestro propio código y podemos corregir los errores en el momento en que se descubran. Además, puede que ya dispongamos de una gran cantidad de código heredado que haya estado siendo utilizado en alguna arquitectura cliente/servidor más tradicional, en la que es necesario instalar fi sicamente los programas cliente cada vez que se lleva a cabo una actualización. El tiempo que se pierde a la hora de instalar actualizaciones es, precisamente, la principal razón para comenzar a utilizar exploradores, porque las actualizaciones son invisibles y automáticas (Java Web Start también constituye una solución a este problema). Si trabajamos en una intranet de este tipo, la solución más lógica consiste en seguir la ruta más corta que nos permita utilizar la base de código existente, en lugar de volver a escribir todos los programa en un nuevo lenguaje. Al enfrentarse con este ampl io conjunto de soluc iones para los problemas de la programación del lado del cliente, el mejor plan de ataque consiste en realizar un análisis de coste-beneficio. Considere las restricciones que afectan a su problema y cuál sería la ruta más corta para encontrar una solución. Puesto que la programación del lado del cliente sigue siendo una programación en sentido tradicional, siempre resulta conveniente adoptar el enfoque de desarrollo más rápido en cada situación concreta. Ésta es la mejor manera de prepararse para los problemas que inevitablemente encontraremos a la hora de desarrollar los programas.
Programación del lado del servidor En nuestro análisis, hemos ignorado hasta ahora la cuestión de la programación del lado del servidor, que es probablemente donde Java ha tenido su éxito más rorundo. ¿Qué sucede cuando enviamos una solicitud a un servidor? La mayor parte de las veces, la solicitud dice simplemente "Envíame este archivo". A continuación, el explorador interpreta el archivo de la forma apropiada, como página HTML, como imagen, como UI1 applel de Java, como programa en lenguaje de scripl, etc. Las solicitudes más complicadas dirigidas a los servidores suelen implicar una transacción de base de datos. Una situación bastante común consiste en enviar una solicitud para que se realice una búsqueda completa en una base de datos, encargándose a continuación el servidor de dar formato a los resultados como página HMTL y enviar ésta al explorador (por supuesto, si el cliente dispone de un mayor grado de inteligencia, gracias a la utilización de Java o de un lenguaje de script, pueden enviarse los datos en bruto y formatearlos en el extremo cliente, lo que sería más rápido e impondría una menor carga de trabajo al servidor). Otro ejemplo: puede que queramos registrar nuestro nombre en una base de datos para unimos a un grupo o realizar un pedido, lo que a su vez implica efectuar modificaciones en la base de datos. Estas solicitudes de base de datos deben procesarse mediante algún tipo de código situado en el lado del cliente; es a este tipo de programas a los que nos referimos a la hora de hablar de programación del lado del cliente. Tradicionalmente, la programación del lado del cliente se llevaba a cabo utilizando Perl, Python, C++, o algún otro lenguaje para crear programas CG!, pero con el tiempo se han desarrol1ado otros sistemas más sofisticados, entre los que se incluyen los servidores web basados en Java que permiten realizar todas las tareas de programación del lado del servidor en lenguaje Java, escribiendo lo que se denomina servlets. Los servlets y sus descendientes, las páginas JSP, son dos de las principales razones por las que las empresas que desarrollan sitios web están adoptando Java, especialmente porque dichas tecnologías eliminan los problemas derivados de tratar
22 Piensa en Java
con exploradores que dispongan de capacidades diferentes. Los temas de programación del lado del servidor se tratan en Thinking in Enterprise Java en el sitio web www.MindView.net. A pesar de todo lo que hemos comentado acerca de Java y de Internet, Java es un lenguaje de programación de propósito general, que pennite resolver los mismos tipos de problemas que podemos reso lver con otros lenguajes. En este sentido, la ventaja de Java no radica sólo en su portabilidad, sino también en su programabilidad, su robustez, su amplia biblioteca estándar y las numerosas bibliotecas de otros fabricantes que ya están disponibles y que continúan siendo desarrolladas.
Resumen Ya sabemos cuál es el aspecto básico de un programa procedimental: definiciones de datos y llamadas a funciones. Para comprender uno de esos programas es preciso analizarlo, examinando las llamadas a función y util izando conceptos de bajo nivel con el fin de crear un modelo mental del programa. Ésta es la razón por la que necesitamos representaciones intermedias a la hora de diseñar programas procedimentales: en sí mismos, estos programas tienden a ser confusos, porque se utiliza una forma de expresarse que está más orientada hacia la computadora que hacia el programa que se trata de resolver. Como la programación orientada a objetos añade numerosos conceptos nuevos, con respecto a los que podemos encontrar en un lenguaje procedimental, la intuición nos dice que el programa Java resultante será más complicado que el programa procedimental equivalente. Sin embargo, la realidad resulta gratamente sorprendente: un programa Java bien escrito es, generalmente, mucho más simple y mucho más fácil de comprender que Wl programa procedimentaL Lo que podemos ver al analizar el programa son las definiciones de los objetos que representan los conceptos de nuestro espacio de problema (en lugar de centrarse en la representación realizada dentro de la máquina),junto con mensajes que se envían a esos objetos para representar las actividades que tienen lugar en ese espacio de problema. Uno de los atractivos de la programación orientada a objetos, es que con un programa bien diseñado resulta fácil comprender el código sin más que leerlo. Asimismo, suele haber una cantidad de código bastante menor, porque buena parte de los problemas puede resolverse reuti lizando código de las bibliotecas existentes. La programación orientada a objetos y el lenguaje Java no resultan adecuados para todas las situaciones. Es importante evaluar cuáles son nuestras necesidades reales y detenninar si Java permitirá satisfacerlas de fonna óptima o si, por el contrario, es mejor emplear algún otro sistema de programación (incluyendo el que en la actualidad estemos usando). Si podemos estar seguros de que nuestras necesidades van a ser bastante especializadas en un futuro próximo, y si estamos sujetos a restricciones específicas que Java pueda no satisfacer, resulta recomendable investigar otras alternativas (en particular, mi recomendación sería echarle un vistazo a Python; véase www.Python.org). Si decide, a pesar de todo, utilizar el lenguaje Java, al menos comprenderá, después de efectuado ese análisis, cuáles serán las opciones existentes y por qué resultaba conveniente adoptar la decisión que ftnalmente haya tomado.
Todo es un objeto "Si habláramos un lenguaj e diferente, percibiríamos un mundo algo distinto". Ludwig Wittgenstein (1889-1951) Aunque está basado en C++, Java es un lenguaje orientado a objetos más "puro". Tanto e++ como Java son lenguajes híbridos, pero en Java los diseñadores pensaron que esa hibridación no era tan importante con en C++. Un lenguaje híbrido pennite utilizar múltiples estilos programación; la razón por la que e++ es capaz de soportar la compatibilidad descendente con el lenguaje C. Puesto que e++ es un superconjunto del lenguaje C. incluye muchas de las características menos deseables de ese lenguaje, lo que hace que algunos aspectos del e++ sean demasiado complicados. El lenguaje Java presupone que el programador sólo quiere realizar programación oricntada a objetos. Esto quiere decir que. antes de empezar, es preciso cambiar nuestro esquema mental al del mundo de la orientación a objetos (a menos que ya hayamos efectuado esa transición). La ventaja que se obtiene gracias a este esfuerzo adicional es la capacidad de programar en un lenguaje que es más fácil de aprender y de utilizar que muchos otros lenguajes orientados a objetos. En este capítulo veremos los componentes básicos de un programa Java y comprobaremos que (casi) todo en Java es un objeto.
Los objetos se manipulan mediante referencias Cada lenguaje de programación di spone de sus propios mecanismos para manipular los elementos almacenados en memoria. En ocasiones. el programador debe ser continuamente consciente del tipo de manipulación que se está efectuando. ¿Estamos tratando con el elemento directamente o con algún tipo de representación indirecta (un puntero en C o C++), que haya que tratar con una sintaxis especial? Todo esto se simplifica en Java. En Java, todo se trata como un objeto, utilizando una única sintax is coherente. Aunque f,.olamas todo como un objeto) los identificadores que manipulamos son en realidad "referencias" a objetos. l Podríamos imaginarnos una TV (el objeto) y un mando a distancia (la referencia): mientras di spongamos de esta referencia tendremos una conexión con la televisión, pero cuando alguien nos dice "cambia de canal" o "baja el volumen", lo que hacemos es manipular la referencia, que a su vez modifica el objeto. Si queremos movemos por la habitación y continuar controlando la TV, llevamos con nosotros el mando a distancia/referencia, no la televisión . I Este punto puede suscitar enconados debates. Ilay personas que sostienen que "claramente se lrala de un puntero", pero esto esta presuponiendo una detenninada implemcntación subyacente. Asimismo. las referencias en Ja\a se parecen mucho mas sintacticamcnte a las referencias C++ que a los punteros. En la primera edición de este libro decidi utilizar eltcmlino "descriptor" porque las referencias C++ y las referencias Java tienen diferencias notables. Yo mismo provenía del mundo del lenguaje C++ y no quería confundir a los programadores de C++. que supon ía que constituirían la gran mayoría de personas interesadas en el lenguaje Java. En la segunda edición, decidí que "referencia" era eltémlino más comúnmente utilizado. y que cualquiera que proviniera del mundo de C++ iba a enfrentarse a problemas mucho mas graves que la tenninología de las referencias. por lo que no tenía sentido usar una palabra distinta. Sin embargo. hay personas que están en desacucrdo incluso con el ténnino "referencia". En un detemlinado libro. pude leer quc "rcsultu completamentc equ ivocado decir que Java soporta el paso por referencia". o que los identificadores de los objetos Java (de acuerdo con el autor del libro) son en realidad "referencias a objctos". Por lo que (continúa el autor) todo se pasa e" la práctica por valor. Según esle autor, no se efectúa un paso por referencia, sino quc se "pasa una referencia a objeto por \' * System.out.println (new Date ()) ; * */ /1/ , También podemos usar código HTML como en cualquier otro documento web, para dar faonato al texto nomlal de las descripciones: ji : object / Documenta t ion3. j ava
/ ** * Se puede inc!uso< / em> insertar una lista : * <01:;. * */ 11/ , -
Observe que, dentro del comentario de documentación, los asteriscos situados al principio de cada línea son ignorados por Javadoc, junto con los espacios iniciales. Javadoc refonnatea todo para que se adapte al estilo estándar de la documentación. No utilice encabezados como o
en el HTML embebido, porque Javadoc inserta sus propios encabezados y los que nosotros incluyamos interferirán con ellos. Puede utilizarse HTML embebido en todos los tipos de comentarios de documentación : de clase, de campo y de método.
Alg unos marcadores de ejemplo He aquí algunos de los marcadores Javadoc disponibles para la documentación de código. Antes de tratar de utilizar Javadoc de fonna seria, consulte la documentación de referencia de Javadoc dentro de )a documentación del IDK, para ver las diferentes formas de uso de Javadoc.
@see Este marcador pennite hacer referencia a la documentación de otras clases. Javadoc generará el código HTML necesario, hipervinculando los marcadores @see a los olros fragmentos de documentación. Las fom13s posibles de uso de este marcador son: @see nombreclase @see nombreclase-completamente-cualificado @see nombreclase-completamente-cualificado #nombre-método
Cada uno de estos marcadores añade una entrada "See Also" (Véase también) hipervinculada al archivo de documentación generado. Javadoc no comprueba los hipervínculos para ver si son válidos.
{@Iink paquete.clase#miembro etiqueta} Muy similar a @see, exceplO porque se puede ut ilizar en línea y emplea la etiqueta como texto del bipervínculo, en lugar de "See Also".
38 Piensa en Java
{@docRoot} Genera la mta relativa al direclOrio raíz de la doc umentaci ón. Resulta útil para introducir hipervínculos explícitos a páginas del árbol de documentación.
{@inheritDoc} Este indicador hereda la documentación de la clase base más próxima de esta clase, insertándola en el comentario del documento actual.
@version Tiene la fonna: ®Version información -versi ón
en el que información-versión es cualquier infonnación significativa que queramos incluir. Cuando se añade el indicador @ version en la línea de comandos Javadoc, la infonnación de vers ión será mostrada de manera especial en la documentac ión HTM L generada.
@author Tiene la fonna: @au thor información-autor
donde información-autor es, normalmente, nuestro nombre, aunque también podríamos incluir nuestra dirección de correo e lectrónico o cualquier otra información apropiada. Cuando se incluye el indicador @ author en la línea de comandos Javadoc, se inserta la infonnación de autor de manera especial en la documentación HTML generada. Podemos incluir múltiples marcadores de autor para incluir una lista de autores, pero es preciso poner esos marcadores de forma consecu tiva. Toda la información de autor se agrupará en un único párrafo dentro del código HTML generado.
@since Este marcador pem1ite indicar la versión de l código en la que se empezó a utilizar una característica concreta. En la documentación HTML de Java, se emplea este marcador para indicar qué versión del JDK se está utilizando.
@param Se utiliza para la documentación de métodos y tiene la fonna: @param nombre-parámetro descripción
donde nombre-parámetro es el identificador dentro de la lista de parámetros del método y descripción es un texto que puede continuar en las líneas siguientes. La descripción se considera tenninada cuando se encuentra un nuevo marcador de documentación. Puede haber múltiples marcadores de este tipo, nonnalmente uno por cada parámetro.
@return Se utiliza para documentación de métodos y su fonnato es el siguiente: @return descripción
donde descripción indica el significado del valor de retomo. Puede conti nuar en las lineas siguientes.
@throws Las excepciones se estudian en el Capítulo 12, Tratamiento de errores mediante ex.cepciones. Por resumir, se trata de objetos que pueden ser "generados" en un método si dicho método falla. Aunque sólo puede generarse un objeto excepción cada vez que invocamos un método, cada método concreto puede generar diferentes tipos de excepciones, todas las cuales habrá que describir, por lo que e l formato del marcador de excepción es: @throws nombre-clase-completamente-cualificado descripción
2 Todo es un objeto 39 donde nombre-c1ase-complelamente-cualijicado proporciona un nombre no ambiguo de una clase de excepción definida en alguna otra parte y descripción (que puede ocupar las sigu ientes lineas) indica la razón por la que puede generarse este tipo concreto de excepción después de la ll amada al método.
@deprecated Se utili za para indicar característi cas que han quedado obsoletas debido a la introducción de alguna otra característica mejorada. Este marca dor indicativo de la obsolescencia recomienda que no se utilice ya esa característica concreta, ya que es probable que en e l futuro sea eliminada. Un método marcado como @ de precated hace que el compi lador genere una advertenc ia si se usa. En Java SE5, el marcador Javadoc @ depreca ted ha quedado susti tuido por la ano/ación @ Dcpreca ted (hablaremos de este tema en el Capitulo 20, Anoraciones).
Ejemplo de documentación He aquí de nuevo nuestro primer programa Ja va, pero esta vez con los co mentarios de documentación inc luidos:
JI : object/HelloDate . java import java.util .*; / ** El primer programa de ejemplo del libro. * Muestra una cadena de caracteres y la fecha actual.
* @au thor Bruce Eckel * @author www.MindView.net * @version 4.0 */ public class HelloDate / ** Punto de entrada a la clase y a la aplicación. * @param args matriz de argumentos de cadena 1< @throws exceptions No se generan excepciones
*/ public static vOld main(String[] args) System.out.println {"Hello,
it ! s:
11 ) ;
System.out.println(new Date ()) ;
/ * Ou tput: HelIo, it's:
(5 5% match)
Wed Oet 05 14:39:36 MDT 2005
* /// , La primera línea del archivo utiliza un a técnica propia del autor del libro que consiste en incluir '//:' como marcador especial en la línea de comentarios que contiene el nombre del archivo fue ntc. Dicha línea contiene la infonn3ción de ruta del archivo (object indica este capítulo) seguida del nombre del archivo. La última línea también teml ina con un comentario, y éste ('///:- ') indica el final del listado de código fuente, lo que pem1ite actualizarlo automáti camente dentro del texto de este libro después de comprobarlo con un compilado r y ejecutarlo. El marcador 1* O utp ut: indica el comienzo de la salida que generará este archi vo. Con esta fanna concreta, se puede comproba r automáticamente para ve rifi car su precisión. En este caso, el texto (550/0 matc h) indica al sistema de pruebas que la salida se rá bastante distinta en cada sucesiva ej ecución de l programa, por lo que sólo cabe esperar un 55 por ciento de correlación con la salida que aquí se mu estre. La mayoría de los ejemplos del li bro que generan una salida contendrán dicha salida comentada de esta fonma, para que el lector pueda ver la salida y saber que lo que ha obtenido es correcto.
Estilo de codificación El estilo descrito en el manual de convenios de código para Java, Cade Convenlions jor rhe Java Programming Language 8, consiste en poner en mayúsc ula la primera letra del nombre de una clase. Si e l nombre de la clase está compuesto por varias Para ahorrar espacio tanto en el libro como en las presentaciones de los seminarios no hemos podido seguir todas las directrices que se marcan en ese texto. pero el lector podrá ver que el estilo empleado en el libro se ajusta lo máximo posible al estándar recomendado en Java.
¡¡
40 Piensa en Java
palabras, éstas se escriben juntas (es decir, no se emplean guiones bajos para separarlas) y se pone en mayúscula la primera letra de cada una de las palabras integrantes, como en: class AIITheColorsOfTheRainbow { II
...
Para casi todos los demás elementos, como por ejemplo nombres de métodos, campos (variables miembro) y referencias a objetos, el estilo aceptado es igual que para las clases, salvo porque la primera letra del identificador se escribe en minúscula, por ejemplo: class AIITheColorsOfTheRainbow ( int anlntegerRepresentingColors¡ void changeTheHueOfTheColor(int newHue )
II ... )
II .. . Recuerde que el usuario se verá obligado a escribir estos nombres tan largos, así que procure que su longitud no sea excesiva. El código Ja va que podrá encontrar en las bibliotecas Sun también se ajusta a las directrices de ubicación de llaves de apertura y cierre que hemos utilizado en este libro.
Resumen El objetivo de este capítulo es explicar los conceptos mínimos sobre Java necesarios para entender cómo se escribe un programa sencillo. Hemos expuesto una panorámica del lenguaje y algunas de las ideas básicas en que se fundamenta. Sin embargo, los ejemplos incluidos hasta ahora tenían todos ellos la fonna "haga esto, luego lo otro y después lo de más allá". En los dos capítulos siguientes vamos a presentar los operadores básicos utili zados en los programas Java, y luego mostraremos cómo controlar el flujo del programa.
Ejercicios Normalmente, distribuiremos los ejercicios por todo el capítulo, pero en éste estábamos aprendiendo a escribir programas básicos, por lo que decidimos dejar los ejercicios para el final. El número entre paréntesis incluido detrás del número de ejercicio es un indicador de la dificultad del ejercicio dentro de una escala de 1-10. Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tire Thinking in Java Annofafed Solution Guide, a la venta en wwv.~ MindView. nef. Ejercicio 1:
(2) Cree una clase que contenga una variable int y otra char que no estén inicializadas e imprima sus valores para verificar que Java se encarga de realizar una inicialización predetenninada.
Ejercicio 2:
(1) A partir del ejemplo HelloDate.java de este capítulo, cree un programa "helio, world" que simplemente muestre dicha frase. Sólo necesitará un método dentro de la clase, (el método "main" que se ejecuta al arrancar el programa). Acuérdese de defUlir este método como static y de incluir la lista de argumentos, incluso aunque no la vaya a utilizar. Compile el programa con javac y ejecútelo con java. Si está utilizando otro entorno de desarrollo distinto de JDK, averigüe cómo compilar y ejecutar programas en dicho entorno.
Ejercicio 3:
(1) Recopile los fragmentos de código relacionados con ATypeNa me y transfónnelos en un programa que se pueda compilar y ejecutar.
Ejercicio 4:
(1) Transforme los fragmentos de código DataOnly en un programa que se pueda com pilar y ejecutar.
Ejercicio 5:
(1) Modifique el ejercicio anterior de modo que los va lores de los datos en DataOnly se asignen e impriman en maine ).
Ejercicio 6:
(2) Escriba un programa que incluya e invoque el método storage( ) definido como fragmento de código en el capítulo.
2 Todo es un objeto 41 Ejercicio 7:
(1) Transfonme los fragmento s de código lncremenlable eo un programa funcional.
Ejercicio 8:
(3) Escriba un programa que demuestre que, independientemente de cuántos objetos se creen de una clase concreta, só lo hay
Ejercicio 9:
Ulla
única instanc ia de un ca mpo static concreto defmido dentro de esa clase.
(2) Escriba un programa que demuestre que el mecanismo automático de conversión de tipos funciona
para todos los tipos primitivos y sus envoltorios. Ejercicio 10 :
(2) Escriba un programa que im prima tres argumentos ex traídos de la línea de comandos. Para hacer esto,
necesitará acceder con el índice correspondiente a la matriz de objetos String extraída de la linea de comandos.
Ejercicio 11:
(1) Transfom1e el ejemplo AIITheColorsOfTheRainbow en un programa que se pueda compi lar y ejecutar.
Ejercicio 12:
(2) Localice el código para la segunda versión de HelloDate.java, que es el ejemplo simple de documentación mediante comentarios. Ejecute Javadoc con ese archivo y co mpruebe los resultados con su explorado r web.
Ejercicio 13:
( 1) Procese Documentationl.java, Documentalion2.java y Documentation3.java con Javadoc. Verifique la documentación resultante con su explorador web.
Ejercicio 14:
( 1) Añada una lista HTML de elementos a la documentación del eje rcicio anterior.
Ejercicio 15:
(1) Tome el programa de l Ejercicio 2 y ailádale comen tarios de documentación. Extraiga esos comentarios de documentación Javadoc para generar un archi vo HTML y visua líce lo con su exp lorador web.
Ejercicio 16:
( 1) En el Capítulo 5, Inicialización y limpieza, localice el ejemplo Overloading.java y añada documentación de tipo Javadoc. Extraiga los comentarios de documentación Javadoc para generar un archi vo HTML y vis ualícelo con su explorador web.
Operadores
En el nivel inferior, los datos en Java se manipulan utili zando operadores. Puesto que Java surgió a partir de C++, la mayoría de estos operadores les serán familiares a casi todos los programadores de C y C++. Java ha añadido también algunas mejoras y ha hecho algunas simplificaciones. Si está familiarizado con la sintaxis de e o e++, puede pasar rápidamente por este capítulo y el siguiente, buscando aquellos aspectos en los que Ja va se diferencie de esos lenguajes. Por el contrario, si le asaltan dudas en estos dos capítulos, repase el seminario multimedia Thinking in e, que puede descargarlo gratuitamente en www.MindView.nef. Contiene conferencias, presentaciones, ejercicios y soluciones específicamente diseñados para familiarizarse con los fundamentos necesarios para aprender Java.
Instrucciones simples de impresión En el capítulo anterior, ya hemos presentado la instrucc ión de impresión en Java: System . out. println ( "Un montón de texto que escribir " )
i
Puede observar que esto no es sólo un montón de texto que escribir (10 que quiere decir muchos movimientos redundantes de los dedos), sino que también resulta bastante incómodo de leer. La mayoria de los lenguajes, tanto antes como después de Java, han adoptado una técnica mucho más sencilla para expresar esta instrucción de uso tan común. En el Capítulo 6, Control de acceso se presenta el concepto de importación estática añadido a Java SES , y se crea una pequeña biblioteca que pennite simplificar la escritura de las instrucciones de impresión. Sin embargo, no es necesario comprender los detalJes para comenzar a utilizar dicha biblioteca. Podemos reescribir el programa del último capítulo empleando esa nueva biblioteca: jj: operators/HelloDate.java import java.util.·; import static net . mindview.util.Print.*;
public class HelloDate { pubIic static void main (String [] args) print ( "He lIo, it' s: "); print(new Date());
l'
Output,
{
(55 % match)
HelIo, it's: Wed Oct 05 14:39:05 MDT 2005
* 111 , Los resultados son mucho más limpios. Observe la inserción de la palabra clave static en la segunda instrucción import. Para emplear esta biblioteca, es necesario descargar el paquete de código del libro desde www. MindView.net o desde alguno de los sitios espejo. Descomprima el árbol de código y añada el directorio raiz de dicho árbol de código a la va riable de entorno CLASSPATH de su computadora (más ade lante proporcionaremos una introducción completa a la variable de ruta CLASSPATH, pero es muy probable que el lector ya esté acostumbrado a lidiar con esa variable. De hecho, es una de las batallas más comunes que se presentan al intentar programar en Java).
44 Piensa en Java
Aunque la utilización de net.rn indview.util.Pri nt simplifica en0n11emente la mayor parte del código, su uso no está justificado en todas las ocasiones. Si sólo hay unas pocas instrucciones de impresión en un programa, es preferible no incluir la instrucción import y escribir el comando completo System.out.println(). Eje rc ic io 1:
(1) Escriba un programa que emplee tanto la faona "corta" como la norlnal de la instrucción de impresión.
Utilización de los operadores Java Un operador toma uno o más argumentos y genera un nuevo valor. Los argumentos se incluyen de forma distinta a las llamadas nonnales a métodos, pero el efecto es el mismo. La suma, el operador más unaria (+), la resta y el operador menos unario (-), la multiplicación (*), la división (f) y la asignación (=) funcionan de fonna bastante similar a cualquier otro lenguaje de programación. Todos los valores producen un valor a partir de sus operandos. Además, algunos operadores cambian el valor del operando, lo que se denomina efecto colateral. El uso más común de los operadores que modifican sus operandos consiste precisamente en crear ese efecto colateral, pero resulta conveniente pensar que el valor generado también está disponible para nuestro uso, al igual que sucede con los operadores que no tienen efectos colaterales. Casi todos los operadores funcionan únicamente con primitivas. Las excepciones son '=', '=' y ' != ', que funcionan con todos los objetos (y son una fuente de confusión con los objetos). Además, la clase St r in g soporta '+' y '+='.
Precedencia La precedencia de los operadores define la fom13 en que se evalúa una expresión cuando hay presentes varios operadores. Java tiene una serie de reglas específicas que determinan el orden de evaluación. La más fácil de recordar es que la mu ltiplicación y la división se realizan antes que la suma y la resta. Los programadores se olvidan a menudo de las restantes reglas de precedencia, así que conviene uti lizar paréntesis para que el orden de evaluación esté indicado de manera explícita. Por ejemplo, observe las instrucciones (1) y (2):
11 : operators/Precedence.java public class Precedence { public static void main(String[] int x l , Y = 2, Z = 3; int a = x + y - 2/2 + z¡ int b = x +
(y
-
args)
2 )/(2 + z );
System.out.println("a
+
a + " b
/ /
(1 ) (2)
/ / =
"
+ b) i
1* Output: a
=
S b
=
1
* /// ,Estas instrucciones ti enen aspecto similar, pero la sa lida nos muestra que sus significados son bastante distintos, debido a la utili zación de paréntesis. Observe que la instrucción System.out.prilltln() incluye el operador '+'. En este contexto, '+' significa "concatenación de cadenas de caracteres" y, en caso necesario, "conversión de cadenas de caracteres." Cuando el compilador ve un objeto Stri ng seguido de '+' seguido de un objeto no String, intenta convertirlo a un objeto St ri ng. Como puede ver en la salida, se ha efectuado correctamente la conversión de int a String pam a y b.
Asignación La asignación se realiza con el operador =. Su significado es: " Toma el valor del lado derecho, a menudo denominado rvalor. y cópialo en e l lado izquierdo (a menudo denominado Ivalor)". Un rvalor es cualquier constante, variable o expresión que genere un valor, pero un lvalor debe ser una variable detenn inada, designada mediante su nombre (es decir, debe existir un espacio fisico para almacenar el va lor). Por ejemplo, podemos asignar un valor constante a una variable: a
=
4¡
3 Operadores 45 pero no podemos asignar nada a un valor constante, ya que una constante no puede ser un ¡valor (no podemos escribir 4 = a;). La asignación de primitivas es bastante sencilla. Puesto que la primitiva almacena el valor real y no una referencia a cualquier objeto, cuando se asignan primitivas se asigna el contenido de un lugar a otro. Por ejemplo, si escribimos a = b para primitivas, el contenido de b se copia en a. Si a continuación modificamos a, el valor de b no se verá afectado por esta modificación. Como programador es lo que cabría esperar en la mayoría de las situaciones. Sin embargo, cuando asignamos objetos, la cosa cambia. Cuando manipularnos un objeto lo que manipulamos es la referencia, asi que al as ignar" de un objeto a otro", lo que estamos haciendo en la práctica es copiar una referencia de un lugar a otro. Esto significa que si escribimos e = d para sendos objetos, lo que al final tendremos es que tanto e corno d apuntan al objeto al que sólo d apuntaba originalmente. He aquí un ejemplo que ilustra este comportamiento:
11 : operators / Assignment.java II La asignación de objetos tiene su truco. import static net.mindview.util.Print.*¡ class Tank { int level;
public class Assignment { public static void main (String [] args ) Tank tI = new Tank () ¡ Tank t2 = new Tank {) i tl.level = 9¡ t2.level = 47¡ print ( "I: tI.level: + tI.level + t2.level: + t2 .level ) i tI = t2 ¡ print {"2: tI.level: + tI.level + t2.level: + t2 .level ) ¡ tI.level = 27 i print ( "3: tI.level: + tI.level + t2.level: + t2 .level ) i
{
1* Output: 1: tl.level: 9, t2.level: 47 2: tI.level: 47, t2.level: 47 3 : tI.level: 27, t2.1evel: 27 * /// , -
La clase Tank es simple, y se crean dos instancias de la misma (11 y t2) dentro de main(). Al campo level de cada objeto Tank se le asigna un valor distinto, luego se asigna t2 a tl , y después se modifica tI. En muchos lenguajes de programación esperaríamos que tl y t2 fueran independientes en todo momento, pero como hemos as ignado una referencia, al cambiar el objeto t1 se modifica también el objeto t2. Esto se debe a que tanto tI como t2 contienen la misma referencia, que está apuntada en el mismo objeto (la referencia original contenida en tI, que apuntaba al objeto que tenía un valor de 9, fue sobreescrita durante la asignación y se ha perdido a todos los efectos; su objeto será eliminado por el depurador de memoria). Este fenómeno a menudo se denomina creación de alias, y representa Wla de las características fundamentales del modo en que Java trabaja con los objetos. Pero, ¿qué sucede si no queremos que las dos referencias apunten al final a un mismo objeto? Podemos reescribir la asignación a otro nivel y utilizar: tI.level
=
t2.level¡
Esto hace que se mantengan independientes los dos objetos, en lugar de descartar uno de ellos y asociar tI y t2 al mismo objeto. Más adelante veremos que manipular los campos dentro de los objetos resulta bastante confuso y va en contra de los principios de un buen diseño orientado a objetos. Se trata de un tema que no es nada trivial, así que tenga siempre presente que las asignaciones pueden causar sorpresas cuando se manejan objetos. Ejercicio 2 :
(1) Cree una clase que contenga
Wl
valor float y utilicela para ilustrar el fenómeno de la creación de alias.
46 Piensa en Java
Creación de alias en las llamadas a métodos El fenómeno de la creación de alias también puede manifestarse cuando se pasa un objeto a un método: /1 : operators / PassObject . java // El paso de objetos a los métodos puede no ser /1 lo que cabria esperar. import static net.mindview.util.Princ.*¡ class Letter char c¡
publi c class PassOb j ect { static void f (Letter y ) y.e ::: 'z'; public static void main (5cring[]
args ) {
Lecter x = new Letter () ¡ x.e = 'a' i print ( "l: x.e: + x. e ) ; f Ix l ; print ( " 2: x.e:
+ x.e ) ;
/ * Output: 1: x.e: a 2: x.e: z
* /// ,En muchos lenguajes de programación, el método f( ) haría una copia de su argumento Lettcr dentro del ámbito del método, pero aquí, una vez más, lo que se está pasando es una referencia. por lo que la línea:
lo que está haciendo es ca mbiar el objeto que está fuera de f( ). El fenómeno de creación de alias y su solución es un tema complejo del que se trata en uno de los su plementos en Línea di sponibles para este libro. Sin embargo, conviene que lo tenga presente desde ahora, con el fin de detectar posibles errores. Ejercicio 3 :
( 1) Cree una clase que contenga un va lor flo.! y utilícela para ilustrar e l fenómeno de la creación de alias durante las llamadas a métodos.
Operadores matemáticos Los ope radores matemáticos básicos son iguales a los que hay di sponibles en la mayoría de los lenguajes de programación: suma (+), resta (-), di visión (f), multiplicación (*) y módulo (%, que genera el resto de una divi sión entera). La división entera trunca en lugar de redondear el resultado. Java también utili za la notación abreviada de e/e++ que realiza una operación y una asignación al mismo tiempo. Este tipo de operación se denota medi ante un operador seguido de un signo de igual, y es coheren te con todos los operadores del lenguaje (allí donde tenga sentido). Por ejemplo, para sumar 4 a la variable x y asignar el resultado a x, uti lice: x += 4. Este ejemplo muestra el uso de los operadores matemáticos:
11 : eperaters / MathOps.java
JI Ilustra los operadores matemáticos. import java.util. *; impert static net . mindvie w. util.Print. * ; pubIic cIass MathOps public static void main (String [) args ) {
3 Operadores 47
II Crea un generador de núme r os aleatorios con una cierta semilla : Random rand = new Random {47 ) ¡ int i, j, k; II Elegir valor entre 1 y 1 0 0: j = rand . nextInt ( lOO ) + 1; print {"j 11 + j )¡ k = rand.nextInt ( lO O) + 1, print ( "k + k) ; i = j + k; print {"j + k + i )j i = j - k; print ( lIj - k + i )¡ i = k I j i print ( "k I j + i ); i = k * j; print ( "k * j + i ); i = k % j; print ( "k % j + i ) i j %= k· print ( "j %= k : " + j ) ¡ II Pruebas con números en coma flotante: float u, v, W¡ II Se aplica también a los de doble precisión v = rand.nextFloat () i print ( "v "+ v ) ¡ W = rand . nextFloat () ¡ print ( "w : 11 + w); u = v + w¡ print ( "v + w + u) ; u = v - w¡ print ( "v - w + u) ¡ u = v * w; pri n t ( IIV * W + u); U = v I w¡ print ( "v I w "+ u ) ¡ II Lo siguiente también funciona para char, I I byte , short , int, long y double: U
+= V;
print ( "u += V u - = v; print ( "u v u * = V; print ( "u *= v u 1= v; print ("u 1= v
1*
Output:
j 59 k 56 j + k 115 j k 3 k I j O k * j 3304 k % j 56 j %= k ; 3 v 0.5309454 w 0.0534 1 22 v + w 0.5843576 v
w
v
* w
0.47753322 0 .028358962
+ u) ; + u)¡ + u);
"
+ u) i
48 Piensa en Java v
/
U
+=
w
,
V
u v u *= v u /= v
9.940527 10.471473 9.940527 5.2778773 9 . 940527
*///>
Para generar números, el programa crea en primer lugar un objeto Random . Si se crea un objeto Random sin ningún argumento, Java usa la hora actua l como semilla para el generador de números aleatorios, y esto generaría una salida diferente en cada ejecución del programa. Sin embargo, en los ejemplos del libro, es importante que la salida que se muestra al final de los ejemplos sea lo más coherente posible, para poder verificarla con herramientas externas. Proporcionando una semilla (un va lor de inicialización para el generador de números aleatorios que siempre genera la misma secuencia para una detenninada semilla) al crear el objeto Random, se generarán siempre los mismos números aleatorios en cada ejecución del programa, por 10 que la salida se podrá verificar. l Para generar una salida más variada, pruebe a eliminar la sem illa en los ejemplos del libro. El programa genera varios números aleatorios de distintos tipos con el objeto Random simplemente invocando los métodos nextln!( ) y nextFloa!( ) (también se pueden invocar nextLong() o nexIDouble( El argumento de nextIn!() establece la cota superior para el número generado. La cota superior es cero, lo cual no resulta deseable debido a la posibilidad de una división por cero, por lo que sumamos uno al resultado.
».
Ejercicio 4:
(2) Escriba un programa que calcule la velocidad utilizando una distancia constante y un tiempo constante.
Operadores unarios más y menos El menos unario (-) y el más unario (+) son los mismos operadores que la suma y la resta binarias. El compilador deduce cuál es el uso que se le quiere dar al operador a partir de la fonna en que está escrita la expresión. Por ejemplo, la instrucción: x
=
-a¡
tiene un significado obvio. El compilador también podría deducir el uso correcto en: x
=
a * -b¡
pero esto podría ser algo confuso para el lector, por lo que a veces resulta más claro escribir: x
=
a * (-b)
¡
El menos unario invierte el signo de los datos. El más unario proporciona una simetría con respecto al menos unario, aunque no tiene ningún efecto.
Autoincremento y autodecremento Java, como e, dispone de una serie de abreviaturas. Esas abreviaturas pueden hacer que resulte mucho más fácil escribir el código; en cuanto a la lectura, pueden simplificarla o complicarla. Dos de las abreviaturas más utilizadas son los operadores de incremento y decremento (a menudo denominados operadores de autoincremento y autodecremento). El operador de decremento es -- y significa "disminuir en una unidad". El operador de incremento es ++ y significa "aumentar en una unidad". Por ejemplo, si a es un valor ¡ot, la expresión ++a es equivalente a (a = a + 1). Los operadores de incremento y decremento no sólo modifican la variable, sino que también generan el va lor de la misma como resultado. Hay dos versiones de cada tipo de operador, a menudo denominadas prefija y postfl)·a. Pre-incremenlO significa que el operador ++ aparece antes de la variable, mientras que post-incremento significa que el operador ++ aparece detrás de la variable. De fonna similar, pre-decremenlo quiere decir que el operador - - aparece antes de la variable y post-decremento significa que el operador - - aparece detrás de la variable. Para el pre-incremento y el pre-decremento (es decir, ++a o - -a), se realiza primero la operación y luego se genera el valor. Para el post-incremento y el post-decremento (es decir, a++ o - -a), se genera primero el valor y luego se realiza la operación. Por ejemplo: 1 El número 47 se utilizaba como "número mágico" en una universidad en la que estudié, y desde entonces 10 utilizo.
3 Operadores 49
JI : operators / Autolnc.java /1 Ilustra los operadores ++
y
-- o
i mport static net.mindview.util . Print.*; public class Autolnc p u blic static void main (String [J args ) { int i = 1; print ( tli : + i I ; print ( H+ +i + ++i ) i // Pre-incremento print ( lIi++ + i++ } ; // Post-incremento print ( "i : + i ) ; print ( "- - i + - - i I ; // Pre-decremento print ( lIi-+ i - - ) i // Post-decremento
print ( "i
+ i ) ;
/ * Output: i
1
++i i ++
,
2 2
i 3 - -i 2 i- 2 i : 1
* /// ,Puede ver que pa ra la fanna prefija, se obtiene el valor después de rea li zada la operación, mientras que para la fanna pOS1fija , se obtiene el valor antes de que la operación se realice. Estos son los únicos operadores, además de los de asignación, que tienen efectos colaterales. Modifican el operando en lugar de simplemente utilizar su valor. El operador de incremento es, precisamente, una de las explicaciones del por qué del nombre e++, que quiere decir "un paso más allá de C". En una de las primeras presentaciones reali zadas acerca de Java, Bi ll Joy (uno de los creadores de Ja va), dijo que "Java=C++- _" (C más más menos menos), para sugerir que Java es C++ pero si n todas las complejidades innecesarias, por lo que resulta un lenguaje mucho más simple. A medida que vaya avanzando a lo largo de l libro, podrá ver que muchas partes son más simples, aunque en algunos otros aspectos Java no resulta mucho más sencillo que C++.
Operadores relacionales Los operadores relacionales generan un resultado de tipo boolean. Eva lúan la relación existente entre los va lores de los opera ndos. Una expresión relaciona l produce el valor t ru e si la relación es cierta y false si no es cierta. Los operadores relacionales son: menor que «), mayor que (», menor o igual que «=), mayo r o igual que (>=), equi valente (=) y no equi valente (!=) . La equival encia y la no equi va lencia funcionan con todas las primitivas, pero las otras comparaciones no funcionan con el tipo boolean. Puesto que los va lores boolean sólo pueden ser true o fa lse, las relaciones "mayor que" y "menor que" no tienen sentido.
Comprobación de la equivalencia de objetos Los operadores relacionales = y != también funcionan con todos los objetos, pero su significado suele confundir a los que comienzan a programar en Java. He aquí un ejemplo:
JJ : operators J Equivalence.java public class Equivalence { public static void main (String[] args ) { Integer nI = new Integer (47 ) i Integer n2 = new Integer (47 ) i System. o ut . println (nl n2 ) ; System.out . println (nl != n2 ) i
50 Piensa en Java
} / * Output , f a l se t r ue
* /// , -
La instrucción System.out.println(n t = n2) imprimirá el resultado de la comparación booleana que contiene. Parece que la sal ida debería ser "tTue" y luego "false", dado que ambos objetos Integer son iguales, aunque el contenido de los objetos son los mi smos, las referencias no son iguales. Los operadores = y != comparan referencias a objetos, por lo que la salida realmente es "false" y luego ·'true". Naturalmente, este comportamiento suele sorprender al principio a los programadores. ¿Qué pasa si queremos comparar si el contenido de los objetos es equivalente? Entonces debemos utilizar el método especia l equals() disponib le para todos los objetos (no para las primiti vas, que funcionan adecuadamente con = y !~). He aquí la fonna en que se emplea:
11 :
operators / EqualsMethod.java
public class EqualsMethod { public static void main {String(] args ) { Integer nl = new Integer (47 ) ; Integer n2 = new Integer (47 ) ; System.out.println (nl.equals {n2 ) ¡
1* Output: true
* /// ,El resultado es ahora el que esperábamos. Aunque, en rea lidad, las cosas no son tan senci llas. Si creamos nuestra propia clase, como por ejemplo:
11: operators/EqualsMethod2.java 11 equals {) predeterminado no compara
los contenidos.
class Value int i¡
pub l ic class EqualsMethod2 { public stati c void main {String(] args ) { Value v1 = new Value () ¡ Value v2 = new Value () ¡ v1.i = v2.i = 10 0 ; System.out.println (vl.equal s( v 2) ¡
1* Output: false
* /// ,los resultados vuelven a confundirnos. El resultado es false, Esto se debe a que el comportamiento predetenninado de equals() consiste en comparar referencias. Por tanto, a menos que sustituyamos equals() en nuestra nue va clase, no obtendremos el comportamiento deseado. Lamentablemente, no vamos a aprender a sustituir unos métodos por otros hasta el capítu lo dedicado a la Reutilización de clases, y no veremos cuál es la forma adecuada de definir equ als() hasta el Capítulo 17, Análisis detallado de los contenedores, pero mientras tanto tener en cuenta el comportamiento de eq ua ls() nos puede ahorrar algunos quebraderos de cabeza. La mayoría de las clases de biblioteca Java implementan equals( ) de modo que compare el contenido de los objetos, en lugar de sus referencias. Ejercicio 5:
(2) Cree una clase denominada Dog (perro) que contenga dos objetos Strin g: name (nombre) y says (ladrido). En main(), cree dos objetos perro con los nombres "spot" (que ladre di ciendo " Ruf!1") y "scruffy" (que ladre dic iendo, "Wurf1 "). Después, muestre sus nombres y el sonido que hacen al ladrar.
3 Operadores 51 Ejercicio 6:
(3) Continuando con el Eje rci cio 5, cree una nueva referen cia Dog y asígnela al objeto de nombre "spot". Realice una comparación utili zando = y cqua ls() para todas las referencias.
Operadores lógicos Cada uno de los operadores lógicos AND (&&), OR (11) y NOT (!) produce un va lor boolean igua l a Irue o false basá ndose en la relació n lógica de sus argumentos. Este ejemplo utiliza los operadores re lacionales y lógicos:
JI: operators/Bool.java 1/ Operadores relacionales
y lógicos. import java.util. * ; import static net.mindview util.Print. * ¡
public class Bool { public static void ma in (String[]
args ) {
Random rand = new Random (47 ) ¡ int i = rand.next lnt (lOQ l i int j = rand,nextlnt ( l OO); print ( " i + i) ; print ( "j + j) ; print ( " i > j is " + (i > j) ) ; print( " i < is " + (i < j) ) ; print( " i >= j is + (i >= j) ) ; print( " i <= j is + (i <= j) ) ; j) ) ; print("i j is + (i print("i != j is + (i != j) ) ; // Tratar int como boolean no es legal en Java: // ! print ( ti i && j is + ( i && j) ) ; //! print (" i 11 j is " + (i 11 j) ) ; II ! print ( "!i is " + ti ) i print( " (i < la) && (j < la) is • ((i < 10) && (j < 10)) ); print( " (i < 10) 11 ( j < 10) is + ((i < 10) 11 (j < 10)) ) ;
/ 0 Output : i
58 55 i > j is true i < j is false i >= j is true i <= j is false i j is false i ! = j is true (i < 10) && (j < 10) is false (i < 10) 11 ( j < 10 ) is false j
0///,-
Sólo podemos aplica r AND, OR o NOT a valores de tipo boolean . No podemos emplear un valor que no sea boolea" como si fuera un va lor booleano en una expresión lógica como a diferencia de lo que sucede en e y C++. Puede ver en el ejemplo los intentos fallidos de realizar esto, desac ti vados mediante marcas de comentarios '''!' (esta si ntax is de comentarios penni te la eliminac ión automática de comentarios para facilitar las pruebas). Sin embargo, las expresiones subsiguientes generan valores de tipo boolean utilizando comparaciones relaciona les y a continuación reali zan operaciones lógicas con los resu ltados. Observe que un va lor boolean se convierte autom át icamente a una fonna de tipo tex to aprop iada cuando se les usa en lugares donde lo que se espera es un valor de tipo String.
52 Piensa en Java
Puede reemplazar la definición de valores int en el programa anterior por cualquier otro tipo de dato primitivo excepto boolean . Sin embargo, teni endo en cuenta que la comparación de números en coma flotante es muy estricta, un número que difiera de cualquier otro, aunque que sea en un valor pequeñísimo seguirá siendo distinto. Asimismo, cualquier número situado por encima de cero, aunque sea pequeñísimo, seguirá siendo distinto de cero.
Ejercicio 7:
(3) Escriba un programa que simule el proceso de lanzar una moneda al aire.
Cortocircuitos Al tratar con operadores lógicos, nos encontramos con un fenómeno denominado "cortocircuito". Esto quiere decir que la expresión se evaluará únicamente hasta que la veracidad o la falsedad de la ex presión completa pueda ser detenninada de forma no ambigua. Como resultado, puede ser que las últimas partes de una expresión lógica no lleguen a evaluarse. He aquí un ejemplo que ilustra este fenóm eno de cortocircuito.
11 : operators/ShortCircuit.java II Ilustra el comportamiento de cortocircuito II al evaluar los operadores lógicos. import static net . mindview.util.Print. * ; public class ShortCircuit { static boolean testl(int val) print("testl(" + val + ") " ) i print (" resu lt: " + (val < 1)) return val < 1;
i
static boolean test2 (in t val) { print (tttest2(" + val + ")"); print( tt result: 11 + (va l < 2)); return val < 2' static boolean test3 (int val) print ( " test3 (" + val + ti),,} ¡ print ( "result: I! + (val < 3}); return val < 3¡ public static void main(String[] args} boolean b = testl(O} && test2(2) && test3 (2); print ("expression is " + b);
1* Output: testl(O)
result: true test2(2) result: false expression is false
* ///,Cada una de las comprobaciones realiza una comparación con el argumento y devuelve true o fa lse. También imprime la información necesaria para que veamos que está siendo invocada. Las pruebas se utilizan en la expresión: testl (O)
&& test2 (2)
&& test3 ( 2 )
Lo natural sería pensar que las tres pruebas llegan a ejecutarse, pero la salida muestra que no es así. La primera de las pruebas produce un res ultado tr ue, por lo que continúa con la evaluación de la expresión. Sin embargo, la segunda prueba produce un resultado false. Dado que esto quiere decir que la expresión completa debe ser false, ¿por qué continuar con la evaluación del resto de la expresión? Esa evaluación podría consumir una cantidad considerable de recursos. La razón de que se produzca este tipo de cortocircuito es. de hecho, que podemos mejorar la velocidad del programa si no es necesario evaluar todas las partes de una expresión lógica.
3 Operadores 53
Literales Nomlalmente, cuando insertamos un valor literal en un programa, el compilador sabe exactamente qué tipo asignarle. En ocasiones, sin embargo, puede que ese tipo sea ambiguo. Cuando esto sucede, hay que guiar al compilador afíadiendo cierta infonnación adicional en la fonna de caracteres asoc iados con el va lor litera l. El código siguiente muestra estos caracteres:
/1 : operators / Literals.java import static net.mindview.util .Print.*¡ public class Literals { public static void main (String [] args ) { int i l = Ox2f¡ /1 Hexadecimal (minúscula ) print ( "i!: " + Integer.toBinaryString ( il )) i int i2 = OX2F¡ / 1 Hexadecimal (mayúscula) print (" i2: 11 + Integer. toBinaryString (i2) ) ; int i3 = 0177; j i Octal (cero inicial)
print ( It i3: " + Integer. toBinaryString (i3 l l ; char c = Oxffff¡ II máximo valor hex para char print {"c: 11 + Integer.toBinaryString (c )) ; byte b = Ox7f; II máximo valor hex para byte print ("b: 11 + Integer. toBinaryString (b) ) ; short s = Ox7fff¡ II máximo valor hex para short print { liS: 11 + Integer.toBinaryString {s )l ; long nI 200L¡ II sufijo long long n2 = 2001; II sufijo long (pero puede ser confuso) long n3 = 200; float f1 1; float f2 = IF ¡ /1 sufijo float float f3 = lf¡ II sufijo float double dI = Id; II sufijo double double d2 = ID; II sufijo double 1I (Hex y Octal también funcionan con long)
/ * Output: il: 101111 i2 : 101111 i 3: 1111111 c: 1111111111111111 b,
1111111
s: 111111111111111
' 111 ,Un carácter si tuado al final de un va lor literal pennite establecer su tipo. La L mayúscula o minúscula significa long (sin embargo, utilizar una I minúscula es confuso, porque puede parecerse al número uno). Una F mayúscula o minúscula significa float. Una D mayúscula o minúscula significa double. Los va lores hexadecimales (base 16), que funcionan con todos los tipos de datos enteros, se denotan mediante el prefijo O, seguido de 0-9 o a-f en mayúscula o minúscula. Si se intenta inicializar una variable con un va lor mayor que el máx imo que puede contener (independientemente de la fonna numérica del valor), el compilador dará un mensaje de error. Observe, en el código anterior, los va lores hexadecimales máximos pennitidos para char, byte y short. Si nos excedemos de éstos, el compilador transfoffilará automáticamente el valor a ¡nt y nos dirá que necesitamos una proyección hacia abajo para la asignación (definiremos las proyecciones posterionnente en el capítulo). De esta fonua, sabremos que nos hemos pasado del límite pennitido. O OX
Los valores octales (base 8) se denotan incluyendo un cero inicial en el número y utilizando sólo los dígitos 0-7. No existe ninguna representación literal para los números binarios en e, e++ o Java. Sin embargo, a la hora de trabajar con notación hexadecimal y octal, a veces resulta útil mostrar la fOffila binaria de los resultados. Podemos hacer esto fácilmen-
54 Piensa en Java
te con los métodos slalic loBinarySlring( ) de las clases lnleger y Long. Observe que, cuando se pasan tipos mas pequeños a Integer.toBinaryString(), el tipo se convierte automáticamente a int.
Ejercicio 8:
(2) Dem uestre que las notaciones hexadecimal y octal funcionan con los valores long. Utilice Long.loBinarySlring( ) para mostrar los resultados.
Notación exponencial Los exponentes utilizan una notación que a mí personalmente me resulta extraña: // : operatorsjExponents.java jI "e" significa "ID elevado a". public class Exponents { public static void main(String[] args) JI 'e' en mayúscula o minúscula funcionan igual: float expFloat : 1.3ge-43f¡ expFloat = 1.39E-43f¡ System.out.println(expFloac) ; double expDouble = 47e47ct¡ // 'd' es opcional double expDouble2 = 47e47; // automáticamente double System.out.println(expDouble) i
1* Output: 1.39E-43 4.7E48
* /// ,En el campo de las ciencias y de la ingeniería. °e' hace referencia a la base de los logaritmos nanlrales, que es aproximadamente 2,718 (en Java hay disponible un va lor double más preciso, que es Malb .E). Esto se usa en expresiones de exponenciación, como por ejemplo 1. 39 X e- 43 , que significa 1.39 X 2.71S--B . Sin embargo, cuando se inventó el lenguaje de programación FORTRAN, decidieron que e significaría "diez elevado a", lo cua l es una decisión extraña, ya que FORTRAN fue diseñado para campos de la ciencia y de la ingeniería, asi que cabría esperar que sus di señadores tendrían en cuenta lo confuso de introducir esa ambigüedad2 . En cualquier caso, esta costumbre fue también introducida en e, e++ y ahora en Java. Por tanto, si el lector está habituado a pensa r en e como en la base de los logaritmos naturales, tendrá que hacer una traducción mental cuando vea una expresión como 1.39 e-43f en Java; ya que qui ere decir 1.39 X 10- 43 . Observe que no es necesario utilizar el ca rácter sufijo cuando el compilador puede deducir el tipo apropiado. Con : long n3
200;
=
no existe ninguna ambigüedad, por lo que una L después del 200 sería superfluo. Sin embargo, con: tloat f4
=
le-43f¡ 11 10 elevado a
el compilador nonnalmente considera los números exponenciales como de tipo double, por lo que sin la f final, nos daría un error en el que nos inform aría de que hay que usar una proyección para convertir el valor double a noat.
Ejercicio 9:
(1) Visualice los números más grande y más pequeño que se pueden representar con la notación ex ponencial en el tipo noal y en el tipo double.
2 John Kirkham escribe: "Comencé a escribir programas infonnáticos cn 1962 en FORTRAN 11 en un IBM 1620. Por aquel entonces y a lo largo de las décadas de 1960 y 1970, FORTRAN era un lenguaje donde todo se escribía en mayúsculas. Probablemente, la razón era que muchos de los disposi livos de entrada eran antiguas unidades de teletipo que utilizaban el código Baudot de cinco bits que no disponía de minúsculas. La hE" en la notación exponencial era también mayúscula y no se confundía nunca con la base de los logaritmos naturales "e" que siempre se escribe en minúscula. La "E" simplemente quería decir exponencial. que era la base para el sistema de numeración que se estaba utilizando, que nonnahncnle era 10. En aquella época, los programadores también empleaban los números oCUlles. Aunque nunca vi que nadie lo utilizara, si yo hubiera visto un número octal en notación exponencial. habría considerado que cSlaba en base 8. La primera vez que vi un exponencial utiliu·mdo una "e" fue a finales de la década de 1970 y tambien a mí me pareció confuso: el problema surgió cuando empezaron a utilizarse minúsculas en FORTAN, no al principio. De hecho. disponíamos de funciones que podian usarse cuando se quisiera empicar la base de los logaritmos naturales, pero todas esas funciones se escribían en mayúsculas.
3 Operadores 55
Operadores bit a bit Los operadores bit a bit penniten manipular bits individuales en un tipo de datos entero primitivo. Para generar el resultado. los operadores bit a bit realizan operaciones de álgebra booleana con los bits correspondientes de los dos argumentos. Los operadores bit a bit proceden de la orientación a bajo nivel del lenguaje e, en el que a menudo se manipula el hardware directamente y es preciso configu rar los bits de los registros hardware. Ja va se diseii.ó originalmente para integrarlo en cod ificadores para te levisión, por lo que esta orientación a bajo nivel seguía teniendo sentido. Sin embargo, lo más probable es que no utilicemos demasiado esos ope radores bit a bit en nuestros programas. El operador bit a bit ANO (& ) genera un uno en el bit de salida si ambos bits de ent rada son iguales a uno; en caso contrario, genera un cero. El operador OR bit a bit (1) genera un uno en el bit de salida si alguno de los bits de entrada es un uno y genera cero sólo si ambos bits de entrada son cero. El operador bit a bit EXCLUS IVE OR o XOR (A) genera un uno en el bit de sal ida si uno de los dos bits de entrada es un uno pero no ambos. El ope rador bit a bit NOT (-. también denominado operador de complemento a lII/O) es lIn operador unario, que sólo adm ite un argumento (todos los demás operadores bit a bit son operadores binarios). El operado r bit a bit NOT genera el opuesto al bit de entrada, es uno si el bit de entrada es cero y es cero si el bit de entrada es uno. Los operado res bit a bit y los operadores lógicos utili zan los mismos caracteres. por lo que resulta útil recurrir a un tmco mnemónico para recordar cuál es el signi fi cado correcto. Como los bits son "pequeños" sólo se utiliza un ca rácter en los operadores bit a bil. Los operadores bit a bit pueden combinarse con el signo = para unir la operación y la asignación: & =. do res legítimos (puesto que - es un operador unari o. no se puede combinar con el signo =).
1= y A= son opera-
El tipo boolean se trata como un va lor de un único bit, por lo que es algo distinto de los otros tipos primitivos. Se puede realizar un a operación AN O, OR o XOR bit a bit. pero no se puede real izar una operación NOT bit a bit (presumiblemente, para evi tar la confusión co n la ope ración lógica NOT). Para los va lores booleanos, los operadores bit a bit tienen el mismo efec to que los operadores lógicos, salvo porque no se aplica la regla de cortocircuito. As imismo, las operaciones bit a bit con valores booleanos incluyen un operador lógico XOR que no fomla parte de la lista de operadores " lógicos". No se puede n emplear valores booleanos en expresiones de desplazamiento, las cuales vamos a describir a continuación. Ejercicio 10:
(3) Escriba un programa con dos va lores constantes, uno en el que haya un os y ceros binarios alternados, con un cero en el dígito menos significati vo, y el segundo con un va lor también alternado pero con un tino en el dígito menos significati vo (consejo: lo más fácil es usar constantes hexadeci mal es para es to). Tome estos dos valores y combínelos de ladas las fonnas posibles utilizando los operadores bit a bit, y visualice los resultados uti li za nd o Integer.toBin.ryStr ing().
Operadores de desplazamiento Los operadores de desplazamiento también sirven para manipular bits. Sólo se les puede utilizar con tipos primitivos en teros. El operador de desplaza mi ento a la izq ui erda «<) genera como resultado el operando situado a la izquierda del operado r después de desplazarlo hacia la izquierda el núm ero de bits especificado a la derecha del operador (i nsertando ceros en los bits de menor peso). El operador de desplaza miento a la derecha con signo (») genera como resultado el operando situado a la izq ui erda del operador después de desplazarlo hacia la derecha el número de bils es pecificado a la derecha del operador. El desplaza mi ento a la derecha co n signo» utili za lo que se denomina extensión de signo: si el va lor es positivo, se inse nan ceros en los bits de mayor peso; si el va lor es nega ti vo, se inse rtan UIlOS en los bits de mayor peso. Ja va ha ailadido también un desplazamiento a la derecha si n signo »>. que utiliza lo que denomina extensión con ceros: independientemente del signo. se insertan ceros en los bits de mayor peso. Este operador no existe ni en e ni C++. Si se desplaza un va lor de tipo charo byte o short, será convertido a int antes de que el desplazamiento tenga lugar y el resultado será de tipo ¡nt. Sólo se utilizarán los bits de menor peso del lado derecho; esto evita que se realicen desplazamientos con un número de posiciones superior al número de bits de un va lor int. Si se es tá operando con un valor long, se obtendrá un resultado de tipo long y sólo se empleará n los seis bits de menor peso del lado derecho. para así no poder desplazar más posiciones que el número de bits de un valor long. Los desplazamient os se pueden combinar con el signo igual «<= o »= o »>=). El Ivalor se sustimye por ell va lor desplazado de acuerdo co n lo que el rvalor marque. Existe un problema. sin embargo, con el desplazamiento a la derecha sin
56 Piensa en Java
signo combinado con la asignación. Si se usa con valores de tipo byte o sho rt, no se obtienen los resultados correctos. En lugar de ello, se tran sfom1an a ¡Dt y luego se desplazan a la derecha, pero a continuación se truncan al volve r a asignar los valores a sus variables, por lo que se obtiene - 1 en esos casos. El siguiente ejemplo ilustra esta situación:
/1: operatorsJURShift.java /1 Prueba del desplazamiento a la derecha sin signo. import stat i c net.mindview.util.Print.*¡ public class URShift public sta tic void main(String[] args) int i = - 1; print(Integer.toBinaryString(i)) ; i
»>= 10 i
print(Integer.toBinaryString(i)) ; long 1 = -1;
print(Long.toBinaryString{l)) ; 1 »>0: la;
print(Long.toBinaryString(l)) ; short s = - 1; print(Integer.toBinaryString(s)) ; s
:>:»=
la;
print(Integer.toBinaryString(s)) ; byte b = -1,
print(Integer . toBinaryString(b)) ; b »>= 10; print(Integer.toBinaryString(b)) ; b = -1 i
print(Integer.toBinaryString(b)) ; print(Integer.toBinaryString(b»>10)) ;
1* Output: 11111111111111111111111111111111 1111111111111111111111 1111111111111111111111111111111111111111111111111111111111111111 111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 11111111111111111111111111111111 1111111111111111111111
* /// , En el último desplazamiento, el valor resultante no se asigna de nuevo a b, sino que se imprime directamente, obteniéndose el comportamiento correcto.
He aquí un ejemplo que ilustra todos los operadores para el manejo de bits:
11: o perators/BitManipulation .java /1 Uso de los operadores bit a bit. import java.util . *¡ import static net.mindview.util.Print.*; public class BitManipulation ( public static void main(String[] args) Random rand = new Random (47 ); int i = rand.nex tlnt(); int j = rand.nextlnt(); printBinarylnt ( "-1", -1) i printBinarylnt ( " +1!l, +1) i
{
3 Operadores 57
int maxpos = 2147483647; printBinarylnt ("maxpos", maxpos) i int maxneg = -2147483648; printBinarylnt ("maxneg", maxneg); printBinarylnt (11 i 11, i ) ; printBinarylnt (11_1 ", -i) i printBinarylnt (11 - i", - i) i printBinarylnt (" j", j) i printBinarylnt (11 i & j", i & j) i printBinarylnt (" i j ", i I j); printBinarylnt (" i A j" I i A j); printBinarylnt("i c< S", i ce 5); printBinarylnt("i » 5", i » 5) j printBinarylnt("(-i} » 5 " , (-i) » 5 ) ; printBinarylnt("i
»>
printBinarylnt ( " {-i}
long 1
=
5", i »> 5",
»>
5);
(-i)
»> S);
rand.nextLong()¡
long ID = rand.nextLong()¡ printBinaryLong ( "-IL", -lL); printBinaryLong ("+lL", +lL ); long 11 = 9223372036854775807L; printBinaryLong ("maxpos", 11); long 11n = -92233720368s477s80BL¡ printBinaryLong ( "maxneg", l1n); printBinaryLong ( " 1 tl, 1) ¡ printBinaryLong(tl-1 11 , -1); printBinaryLong (It _1 11 , -1); printBinaryLong ("m", m) ¡ printBinaryLong (111 & m" 1 & mi; printBinaryLong (It 1 mi; m" 1 , printBinaryLong (11 1 '" m" 1 mi; printBinaryLong ( " 1 « 5" 1 « 51 ; printBinaryLong("1 » Sil, 1 » S); printBinaryLong(1I (-1) » S", {-l} » 5) i printBinaryLong( 1t 1 »:> 5", 1 :»> S) ¡ printBinaryLong("(-1) »:> S", (-1) »> S)¡ static void printBinaryInt(String s, int i) print (s + 11, int: 11 + i + ", binary: \n Integer.toBinaryString(i») ¡ static void printBinaryLong(String s, long 1) print (s + It, long: It + 1 + 11, binary: \n Long. toBinaryString (1l ) ¡
/*
Output: int: -1, binary: 11111111111111111111111111111111 +1, int: 1, binary: - 1,
1
maxpos, int: 2147483647, binary: 1111111111111111111111111111111 maxneg, int: -2147483648, binary: 10000000000000000000000000000000 i, int: -1172028779, binary: 10111010001001000100001010010101 -i, int: 1172028778, binary: 1000101110110111011110101101010
+
+
58 Piensa en Java -i,
int:
1172028779,
binary:
1000101110110111011110101101011 j, i i i
int:
1717241110,
binary:
1100110010110110000010 1 00010110 & j, int: 570425364, binary: 100010000000000000000000010100 I j, int: -25213033, binary: 11111110011111110100011110010111 j,
"
int:
-595638397,
binary:
110 111 0001111111010001 111 0000011 i
5,
«
int:
11 49784736,
binary:
1000100100010000101001010100000 i
»
5,
int:
-36625900,
binary:
111111 011101000 1 00 1 0001000010100 (-i)
»
S,
int : 36625899,
binary:
1 00010 111011011101111 01011 i
»>
5,
int:
97591828,
binary:
1011101 0001001000 1 0000 101 00 (-i)
»>
5,
int:
36625899,
binary:
10001011101101110111101011
Los dos métodos del final , printBinarylnt() y printBinaryLong( j, toman un valor int o long, respectivamente, y lo imprimen en formato binario junto con una cadena de caracteres descriptiva. Además de demostrar el efecto de todos los operadores bit a bit para valores int y long, este ejemplo también muestra los valores mínimo, máximo, + 1 Y - t para int y long para que vea el aspecto que tienen. Observe que el bit más alto representa el signo: O significa positivo y 1 significa negati vo. En el ejemplo se muestra la salida de la parte correspondiente a los valores in!. La representación binaria de los números se denomina complemento a dos con signo. Ejercicio 11:
(3) Comience con un número que tenga un uno binario en la posición más significati va (consejo: utilice una constante hexadecimal). Emplee el operador de desplazamiento a la derecha con signo, desplace el valor a través de todas sus posiciones binarias, mostrando cada vez el resultado con Integer.toBinaryString( ).
Ejercicio 12:
(3) Comience con un número cuyos dígitos binarios sean todos iguales a uno. A continuación desplácelo a la izquierda y utilice el operador de desplazamiento a la derecha sin signo para desplazarl o a través de todas sus posiciones binarias, visualizando los resultados con lnteger.toBinaryString().
Ejercicio 13:
(1) Escriba Wl método que muestre valores char en fonnato binario. Ejecútelo utilizando varios caracte-
res di ferentes.
Operador ternario if-else El operador ternario, también llamado operador condicional resulta inusual porque tiene tres operandos. Realmente se trata de un operador, porque genera un valor a diferencia de la instrucción if-else ordinaria, que veremos en la siguiente sección del capítulo. La expresión tiene la fomla: exp-booleana ? valorO : valor1
Si exp-booleana se evalúa como true, se evalúa valorO y el resultado será el valor generado por el operador. Si expbooleana es false , se evalúa valorl y su resultado pasará a ser el valor generado por el operador. Por supuesto, podría utilizarse una instrucción if-else ordinaria (que se describe más adelante), pero el operador ternario es mucho más compacto. Aunque e (donde se originó este operador) se enorgullece de ser un lenguaje compacto, y el operador ternario puede que se baya introducido en parte por razones de eficiencia, conviene tener cuidado a la hora de emplearlo de forma cotidiana, ya que el código resultante puede llegar a ser poco legible. El operador condicional es diferente de if-else porque genera un valor. He aquí un ejemplo donde se comparan ambas estructuras:
3 Operadores 59
JI : operators / TernarylfElse.java import static net.mindview.util.Print.*¡ public class TernarylfElse { static int ternary(int i) { return i < 10 ? i * 100
i
*
10i
static int standardlfElse ( int i) if(i < 10) return i
*
100 i
else
return i * 10; public static void main{String[] args) print(ternary{9))
j
print(ternary{lO))
i
print(scandardlfElse(9)) ;
print(standardlfElse(lO)) ;
/ * Output : 900 100 900 100
* /1/ ,Puede ver que el código de ternary( ) es más compacto de lo que sería si no dispusiéramos del operador temario: la versión sin operador temario se encuentra en standardlfElse( j. Sin embargo, standardlfElse( j es más fácil de comprender y además exige escribir muchos caracteres más. Así que asegúrese de ponderar bien las razones a la hora de elegir el operador ternario; nOffilalmente, puede convenir utilizarlo cuando se quiera configurar una variable con uno de dos valores posibles.
Operadores + y += para String Existe un uso especial de un operador en Java: los operadores + y += pueden usarse para concatenar cadenas, corno ya hemos visto. Parece un uso bastante natural de estos operadores, aún cuando no encaje demasiado bien con la fonna tradicional en que dichos operadores se emplean. Esta capacidad le pareció adecuada a los diseñadores de C++, por lo que se añadió a C++ un mecanismo de sobrecarga de e++ pudieran añadir nue vos significados casi a cualquier operador. Lamentablemente, la sobrecarga de operadores combinada con alguna de las otras restricciones de C++, resulta una característica excesivamente complicada para que los programadores la incluyan en el diseño de sus clases. Aunque la sobrecarga de operadores habría sido mucho más fácil de implementar en Java de lo que lo era en C++ (como se ha demostrado en el lenguaje C#, que sí que dispone de un sencillo mecanismo de sobrecarga de operadores), se consideraba que esta característica seguía siendo demasiado compleja, por lo que los programadores Java no pueden implementar sus propios operadores sobrecargados, a diferencia de los programadores de C++ y C#. operadores para que los programadores
El uso de los operadores para valores String presenta ciertos comportamientos interesantes. Si una expresión comienza con un valor String, todos los operandos que siguen también tendrán que ser cadenas de caracteres (recuerde que el compilador transforma automáticamente a String lada secuencia de caracteres encerrada entre comillas dobles).
11: ope rators /Stri ngOperators.java import static net.mindview.util.Print . *; public class StringOperators { public static void main{String[] args) int x ~ 0, y = 1, Z = 2; String s = II X , y, Z "¡ print{s + x + y + z );
{
60 Piensa en Java print (x + " " + s); / / Convierte x a String s += It (summed ) = "i / / Operador de concatenación print (s + (x + y + z »); print ( "" + x ) ; / / Abreviatura de Integer . toString ()
/ * Output: x , y, O
z 012
x, y,
x, y,
z
z (summed)
3
O
*111 ,Observe que la salida de la primera instrucción de impresión es ' 012 ' en lugar de sólo '3', que es lo que obtendria si se estuvieran sumando los valores enteros. Esto es porque el compilador Java convierte x, y y z a su representación String y concatena esas cadenas de caracteres, en lugar de efectuar primero la suma. La segunda instrucción de impresión convierte la variable inicial a String, por lo que la conversión a cadena no depende de qué es lo que haya primero. Por último, podemos ver el uso del operador += para añadir una cadena de caracteres a s, y el uso de paréntesis para controlar el orden de evaluación de la expresión, de modo que los valores enteros se sumen realmente antes de la visualización. Observe el último ejemplo de main() : en ocasiones, se encontrará en los programas un valor String vacío seguido de + y una primitiva, como fonna de rea lizar la conversión sin necesidad de invocar el método explicito más engorroso, (Integer.toString(), en este caso).
Errores comunes a la hora de utilizar operadores Uno de los errores que se pueden producir a la hora de emplear operadores es el de tratar de no incluir los paréntesis cuando no se está del todo seguro acerca de la fonna que se evaluará una expresión. Esto, que vale para muchos lenguajes también es cierto para Java. Un error extremadamente común en while Ix = y )
II
oo
e y e++ seria el siguiente:
{
••
El programador estaba intentando, claramente, comprobar la equivalencia (-) en lugar de hacer una asignación. En
ey
e++ el resultado de esta asignación será siempre true si y es distinto de cero, por lo que probablemente se produzca un bucle infinito. En Java, el resultado de esta expresión no es de tipo boolean, pero el compilador espera un valor boolean y no reali zará ninguna conversión a partir de un valor int, por lo que dará un error en tiempo de compilación y detectará el problema antes de que ni siquiera intentemos ejecutar el programa. Por tanto, este error nunca puede producirse en Java (la única posibilidad de que no se tenga un error de tiempo de compilación, es cuando x e y son de tipo boolean, en cuyo caso x = y es una expresión legal , aunque en el ejemplo anterior probablemente su li SO se deba a un error). Un problema similar en e y e++ consiste en utilizar los operadores bit a bit AND y OR en lugar de las versiones lógicas. Los operadores bit a bit AND y OR uti lizan uno de los caracteres (& o Il mientras que los operadores lógicos AND y OR utilizan dos (&& y 11). Al igual que con = y~, resulta fácil confundirse y escribir sólo uno de los caracteres en lugar de dos. En Java, el compi lador vuelve a evitar este tipo de error, porque no pennite emplear un determinado tipo de datos en un lugar donde no sea correcto hacerlo.
Operadores de proyección La palabra proyección (casI) hace referencia a la conversión explícita de datos de un tipo a otro. Java cambiará automáticamente un tipo de datos a otro cada vez que sea apropiado. Por ejemplo, si se asigna un valor entero a una variable de coma flotante, el compilador convertirá automáticamente el valor int a noat. El mecanismo de conversión nos pennite realizar esta conversión de manera explícita, o incluso forzarla en situaciones donde nonnalmente no tendría lugar. Para realizar una proyección, coloque el tipo de datos deseado entre paréntesis a la izquierda del valor que haya que convertir, como en el siguiente ejemplo:
3 Operadores 61 // : operators / Casting.java public class Casting public sta tic void main (String [] args ) { int i = 200; long 1n9 = (10ng ) i; 1n9 = i¡ JI "Ensanchamiento," por lo que no se requiere conversión long lng2 = ( long ) 2 00 ; lng2 = 20 0 ; l/ Una "conversión de estrechamiento ! l . i
= (int ) lng2; // Proyección requerida
) /// > Como podemos ver, resulta posible aplicar una proyección de tipo tanto a los valores numéricos como a las variables. Observe que se pueden también introducir proyecciones superfluas, por ejemplo, el compilador promocionará automáticamente un valor ¡ut a long cuando sea necesario. Sin embargo, podemos utili zar esas proyecciones superfluas con el fin de resaltar la operación o de clarificar el código. En otras situaciones, puede que la proyección de tipo sea esencial para que el código llegue a compilarse. En C y C++, las operaciones de proyección de tipos pueden provocar algunos dolores de cabeza. En Java, la proyección de tipos resulta siempre segura, con la excepción de que, cuando se realiza una de las denominadas conversiones de estrechamiento (es decir, cuando se pasa de un tipo de datos que puede albergar más infomlación a otro que no permite albergar tanta), se COITe el riesgo de perder infonnación. En estos casos, el compilador nos obliga a emplear una proyección, como diciéndo nos: "Esta conversión puede ser peligrosa, si quieres que lo haga de todos modos, haz que esa proyección sea explícita". Con una conversión de ensanchamiento, no hace falta una proyección explícita, porque el nuevo tipo pennitirá albergar con creces la infonnación del tipo anterior, de modo que nunca se puede perder infonnación. Java pennite proyectar cualquier tipo primitivo a cualquier otro, excepto en el caso de boolean, que no pennite efechlar ningún tipo de proyección. Los tipos de clase tampoco penniten efectuar proyecciones: para converti r uno de estos tipos en otro, deben existir métodos especiales (posterionnente, veremos que los objetos pueden proyectarse dentro de una/ami/ia de tipos; un Olmo puede proyectarse sobre un Árbol y viceversa, pero no sobre un tipo externo como pueda ser Roca).
Truncamiento y redondeo Cuando se realizan conversiones de estrechamiento, es necesario presta r atención a los problemas de truncamiento y redondeo. Por ejemplo, si efectuamos una proyección de un va lor de coma flotante sobre un valor entero, ¿qué es lo que haría Java? Por ejemplo, si tenemos el valor 29,7 y lo proyectamos sobre un int, ¿el valor resultante será 30 o 29? La respuesta a esta pregunta puede verse en el siguiente ejemplo: /1 : operato rs / CastingNumbers.java // ¿Qué ocurre c uando se proyecta un val o r float // o double sobre un val or entero? import static net.mindview.util.Print.*¡ public class CastingNumbers { public st atic void main (String[] args ) double aboye = 0.7, below = 0 .4; float fabove = D.7f, fbelow = D.4f¡ print ( " (int l above: " + (int ) above ) ; print {" (int l below: " + í int ) below ) i print ( II (int l fabove: + (int ) fabove ) ; print ( " (int l fbelow: " + (int ) fbelow ) ¡
1* Output: (int ) above: O (int ) below: O (int ) fabove: O (int ) fbelow: O * /// ,-
62 Piensa en Java
Asi que la respuesta es que al efectuar la proyección de float o double a un valor entero, siempre se trunca el correspondiente número. Si quisiéramos que el resultado se redondeara habría que utilizar los métodos round() de ja\'a.lang.Math: 11 : operators/RoundingNumbers . java
II Redondeo de valores float y double. import static net.mindview.ut i l.Print .*; public class RoundingNumbers { public static void main(String[) args) double aboye = 0 . 7, below = 0 .4 ; float fabove = 0 . 7f, fbelow = 0 .4f; print {"Math. round (aboye ) : ti + Math .round{above»; print ( "Math. round (below) : " + Math.round {below»; print("Math.round {fabove ) : + Math.round(fabove» i + Math. round (fbelow»; print ("Math.round{fbelow) : 11
1* Output: Math . round (above) : Math. round (be low) : Math. round (fab ove) Ma th. round (fbelow )
1 O : 1 : O
* ///,Puesto que round( ) es parte de ja\'a.lang, no hace falta ninguna instmcción adicional de importación para utilizarlo.
Promoción Cuando comience a programar en Java, descubrirá que si hace operaciones matemáticas o bit a bit con tipos de datos primitivos más pequeños que ¡nt (es decir, char, byte o short), dichos valores serán promocionados a ¡nt antes de realizar las operaciones, y el valor resultante será de tipo int. Por tanto, si se quiere asignar el resultado de nuevo al tipo más pequeño, es necesario emplear una proyección (y, como estamos realizando una asignación a un tipo de menor tamaño, perderemos infonnación). En general, el tipo de datos de mayo r tamaño dentro de una expresión es el que detennina el tamaño del resultado de esa expresión, si se multiplica un valor float por otro double, el resultado será double; si se suman un valor ¡nt y uno long, el resultado será long.
Java no tiene operador "sizeof" En C y C++, el operador sizeof() nos dice el número de bytes asignado a un elemento de datos. La razón más importante para el uso de sizeof( ) en C y C++ es la portabilidad. Los diferentes tipos de datos pueden tener di fe rentes tamaños en distintas máq uinas, por lo que el programador debe averiguar el tamaño de esos tipos a la hora de realizar operaciones que sean sensibles al tamaño. Por ejemplo, una computadora puede almacenar los enteros en 32 bits, mientras que otras podrían almacenarlos en 16 bits. Los programas podrían, así, almacenar valores de mayor tamaño en variables de tipo entero en la primera máquina. Como puede imaginarse, la portabilidad es un ve rdadero quebradero de cabeza para los programadores de C y C++. Java no necesita un operador sizeof( ) para este propósito, porque todos los tipos de datos tienen el mismo tamaño en todas las máquinas. No es necesario qu e tengamos en cuenta la portabilidad en este nivel, ya que esa portabilidad fanna parte del propio diseño del lenguaj e.
Compedio de operadores El siguiente ejemplo muestra qué tipos de datos primitivos pueden utilizarse con detenninados operadores concretos. Básicamente, se trata del mismo ejemplo repetido una y otra vez pero empleando diferentes tipos de datos primitivos. El archivo se compilará sin errores porque las líneas que los incluyen están desactivas mediante comentarios de tipo I/!. 11 : operators/A110ps.java
1I Comprueba todos los ope r adore s con todos los tipos de dat os primitivos
3 Operadores 63
/1
para mostrar cuáles son aceptables por el compilador Java.
public class AIIOps { 1/ Para aceptar los resultados de un test booleano: void f (boolean b) {} void boolTest(boolean x, boo!ean y) // Operadores aritméticos: //! x x * Yi x j y; x%y;
jj! x jj! x
//!x
X+Yi
ji! x
x
/ /!
y¡
X++;
jj! x--;
I/! x
=
+y;
jj! x = -y; 1/ Relacionales y lógicos: jj! f (x > y); j j! f (x >= y); jj! f(x < y); j j! f (x <= y); f (x y); f (x ! = y); f (!y);
x
=
x && y¡
x
=
x
1/ j j! x x x jj! jj! jj!
11
y;
Operadores bit a bit:
x x x x
x x x
-y; &
y; y; y; x « x » X
>>>
1;
1; 1;
// Asignación compuesta: jj ! x += y;
-= y; x *= y; jj! x j= y; jj! x %= y; JI! x «= 1; JI! x »= 1; I/! x »>= 1; x &= y¡ x "= y¡ x 1= y; l/Proyección: //! char e = (char) x; jj! byte b = (byte ) x; I/! short s = (short ) x; jI! int i = ( int ) x¡ jj! long 1 = (long )x ; jj! float f = (float)x; jj! double d = (doubl e )x; jj! x
JI!
void charTest(char x, char y) /1 Operadores aritméticos: x (c har ) (x * y); x = (char) (x j y);
64 Piensa en Java (char) (x % y);
x
x
(char) (x
x
(char) ( x
+
y); y) ;
x++; x--¡ x = (char) +y¡ x = (char) -y ¡ II Relacionales y lógicos: f (x > y); f (x >= y);
f (x < y); f (x <= y); f (x == y); f(x
!= y) ;
II! f (!x) ¡ II! f (x && y) ; II! f(x 11 y) ; II Operadores bit (char) -y ¡ x = (char) {x
a bit:
x=
&
y};
x x
=
x x x
(char ) (x « 1); (char) (x » 1) ¡ (char) (x »> 1); Asignación compuesta:
II X
(char) ( x 1 y); (char) (x • y);
+= y;
X
y;
X
*= y;
x
1=
x x x
y; %= y; «= 1 ;
x
»= 1 ; »>= 1 ; &= y;
X
A=
x
y¡
1= y; II Proyección: II! boolean bl
x
byte b
=
=
(boolean}x;
(byte)x;
short s = (short)x¡ int i = {int}x¡ long 1 = (long)x¡ float f = (float)x¡ double d = (double)x¡ void byteTest{byte x, byte y) II Operadores aritméticos: x
(byte) (x *
x
(byte) (x
x
(byte) (x % y);
x
(byte) (x + y) ;
x
(byte) (x
y);
I
y);
y) ;
x++; X--¡
x
=
x
=
II
(byte) + y; (byte ) - y;
Relacionales y lógicos:
f (x > y);
3 Operadores 65
f (x f (x f (x f (x f (x // ! //! // !
>; y ) ;
< y) ; <= y ) ; -- y ) ;
!= y) ;
f (! x l i
f (x && y ) ; f(x 11 y ) ; 1/ Operadores bit a bit: x (byte) -y; x
(byte) (x & y ) ;
x x
( byte ) ( x ( byte ) ( x
•
x
( byte ) ( x
«
1) i
x
( byte ) (x
»
1) i
( byte ) (x
»> 1 ) i
x
JI
y) ; y) ;
Asignación compuesta:
x += y; y; *= y ; x /; y ;
X
X
x %= Yi X «:::c 1;
x »= 1; x»>", 1; x &= y; x "'= y; x 1; y;
JI Proyección : JI ! boolean bl =
(boolean)x; char e = (char ) x; short s = (short ) x; int i
=
( int ) Xi
=
long 1
(l ong ) x;
=
float f
dauble d
( fleat ) X i
=
(double)x;
void shortTest (short x, short y ) { JI Operadores aritméticos: x (s hort ) (x * y ) ; x x
( short ) ( x ( short ) ( x
x
(short ) (x
% y); + y) ;
/
x
( short) ( x
y);
y ) ;
x ++ ; x--¡
x x
= =
JI
(short)+y¡ (short}-y ; Relacionales y lógicos :
f (x
>
f (x
>=
f (x
< y) ;
y) ;
y) ;
f ( x <= y ) ; f ( x ;= y ) ; f ( x != y); / / ! f (!x); // ! f (x && y ) ; // ! f (x 11 y);
JI
Operadores bit a bit :
66 Piensa en Java x x x x
(short)-y¡ Is hort ) Ix & y); Ishort) Ix y) ; Is hort ) Ix y ); x (short) ( x « 1 ) i x (short) (x » 1) i x (short) (x »> 1) i II Asignación compuesta: X += y; X -= y; x *= y; x 1= y; x %= y; x «= 1¡ x »= 1 ¡ x »>= 1; x &= y¡ x "= Y¡
x
A
1= y;
II Proyección: II! boolean bl
= (boolean } x¡ char c = (c har }x; byte b = Ibyte)x; int i = (i nt ) Xi long 1 = Ilong ) x; float f = I float ) x; double d = Idouble ) x;
void intTest (int x, int y ) { II Operadores aritméticos: x x * y¡ x x 1 y; x x % y; x x + y; x x y; x++¡ x - -¡
x x
+Yi
=
-Y¡ Relacionales y lógicos:
=
II f lx f lx flx f lx f lx f Ix
II ! II ! 11 '
II
x x x x
x x x
II
> y);
y) ; y) ; <= y ) ; -- y ) ; >=
<
! = y );
f I! x ) ; f Ix && y ) ; f Ix 11 y); Operadores bit a bit: -y¡
x & y; x 1 y; x y; x « 1; x » 1; x»> 1 i Asignación compuesta:
X + = Yi
3 Operadores 67 x y; x *= y; x / = y; x %= y;
x «= x :»=
1;
1i
X »>= 1;
x
&= Yi
x "'= Yi x 1= y;
// Proyección: (boolean ) x¡ // ! boolean bl char e = {char ) x¡
=
byte b
(byte ) x;
short s = (short ) x¡ long 1 = ( long ) x¡ float f
(float ) x;
=
double d
=
{double )x¡
void longTest ( long x, long y) 1/ Operadores aritméticos: x x x x x
x x x x x
* Yi / y; % y; + y;
- y;
X++¡
x--¡
x = +Yi X = -Vi
/ 1 Relacionales y lógicos: f f f f f f /
(x > y); (x
>=
(x
< y ) ;
y ) i
(x
<= y ) ;
(x
==
(x
/ !
//! // !
y ) ; != y ) ;
f ( !x ) ; f (x && y ) ; f (x 1 1 y ) ;
JI
Operadores b i t a bit: - Yi x & y; x y; x y; x « 1; x » 1; x x»:> 1; /1 Asignación compuesta: X += y; X y; X *= y; x / = y; x %= y;
x x x x x x
x «= 1¡ x »= 1; x »>= 1; x &= Yi x "= Yi
68 Piensa en Java
1, y;
x
II Proyección: II! boolean bl char c
=
=
(boolean)x ;
(char)x¡
byte b '
(byt e l x;
short s = (short) x¡ int i = (int) x¡ float f = (float ) x¡ double d = {double)x¡ void floatTest ( float X, float y) II Operadores aritméticos: X X * y¡
x I y; x % y; x + y;
x x x x
x - y;
x++¡ x--; X =
x
=
II f f f f f
+y¡ -y ¡ Relacionales y lógicos:
(x
>
y) ¡
(x >, yl; (x < yl; Ix <, yl; Ix " yl; f (x !, yl; II! f (! x l ;
II! II!
II
f (x && yl; f (x 1 1 yl; Operadores bit a bit: x -y;
II! II! x II! x II! x
x
&
y;
x
1
y; y;
x
II! x x« 1; II ! x x» 1; II! x x»> 1; II Asignación compuesta: x
+=
x
-
y¡ y¡ x *= y¡ x 1, y; x %= y¡ II! x «= 1¡
II!
x »=
1;
II! x »>= 1 ¡ II ! x &, y; II! x " y; II!xl,y;
II II!
Proyección ; boolean bl = (boolean )x¡ char c = (cha r ) x¡
byte b
=
(byte l x;
short s = (short)x¡ int i = (in t )x ¡
long 1 double d
(longl x; (double l x;
=
{
3 Operadores 69
void doubleTesc {double x, double y ) { // Operadores aritméticos: x x * Yi x x I y; x x % y¡ x x + y; x x y; X++¡
x--; x = +Yi =
X
-y;
/1 Relacionales
y lógicos:
f Ix > y l ; f Ix >= y l ;
f Ix < y l ;
Ix Ix Ix
yl; yl ; != y l ; II ! f I 'x l ; II ! f Ix && y l ; I I ! f Ix 1 1 yl; / / Operadores bit a bit: 11 ' x -y;
f f f
<= ==
// ! x
X&Yi
II ! x II ! x
x x
y; 1; x:>:> 1; x»> 1;
// ! x j i!
1 y;
x«
x
ji ! x /1 Asignación compuesta: x +"" y; X Yi x *= Yi x / = y; %= y¡ x « = 1; ji ! x »= 1; // ! x »>= 1;
x
JI !
II ! x II ! x II ! x
y; '= y;
&=
1= y; Proyecc ión: //1 boolean bl JI
=
{boo l ean ) x;
c har e = (char ) x; byte b = Ibyte l x; short s = (short ) x; int i = ( int ) x;
long 1 float f
Ilong l x;
=
=
( float ) Xi
)
111 ,Observe que boolean es bastante limitado. A una variable de este tipo se le pueden asignar los valores true y false, y se puede comprobar si el va lor es verdadero o falso, pero no se pueden sumar va lores booleanos ni realizar ningún otro tipo de operación con ellos. En char, byte y short, puede ver el efecto de la promoción con los operadores ari tméticos. Toda operación aritmética sobre cualquiera de estos tipos genera un resultado int, que después debe se r proyectado explícitamente al tipo original (una conve rsión de estrechamiento que puede perder información) para realizar la asignación a dicho tipo. Sin embargo, con los va lo-
70 Piensa en Java
res int no es necesari a una proyecc ión. porque todo es ya de tipo int. Sin embargo, no se crea que todas las operaciones son seguras. Si se multiplican dos va lores ¡nt que sean lo suficientemente grandes, se producirá un desbordamiento en el resultado, como se ilustra en el siguiente ejemplo:
JJ : operatorsJOverflow . java JJ ¡Sorpresa! Java permite los
desbordamientos.
public class Overflow { public static void main(String(} args ) int big = Integer.MAX_VALUE¡ System . out . println("big = " + big) ¡ int bigger = big * 4¡ System.out.println ( lIbigger = 11 + bigger ) ¡
J* Output : big = 214 748364 7
bigger * ///,-
=
-4
No se obtiene ningún tipo de error o advertencia por pane del compilador, y tampoco se genera ninguna excepción en tiempo de ejecución. El lenguaje Ja va es muy bueno, aunque no hasta ese punto. Las asignaciones compuestas no requieren proyecciones para char, byte o short, aún cuando estén realizando promociones que provocan los mismos resultados que las operaciones aritméticas directas. Esto resulta quizá algo sorprendente pero, por otro lado, la posibilidad de no incluir la proyección simplifica el código. Como puede ver, con la excepción de boolcan, podemos proyectar cualqu ier tipo primitivo sobre cualquier otro tipo primitivo. De nuevo, recalquemos que es preciso tener en cuenta los efectos de las conversiones de estrechamiento a la hora de realizar proyecciones sobre tipos de menor tamaño~ en caso contrario, podríamos perder información inadvertidamente durante la proyección. Ejercicio 14:
(3) Escriba un método que tome dos argumentos de tipo String y utilice todas las comparac iones boolean para comparar las dos cadenas de caracteres e imprimir los resultados. Para las comparaciones = y !=, realice también la prueba con equals( ). En maine ), invoque el método que haya escrito, utilizando varios objetos String diferentes.
Resumen Si tiene experiencia con algún lenguaje que emplee una sintaxis similar a la de C. podrá ver que los operadores de Ja va son tan similares que la curva aprendizaje es prácticamente nula. Si este capítulo le ha resultado dificil, asegúrese de echar un vistazo a la presentación multimedi a Thinking in C. disponible en wW)'l~ MindViel1Wel. Puede encontrar las sol uciones a lo~ ejercicios seleccionados en el documento elcctrónico The Thi"ki"g in Java Afmolllfed So/mio" Guide, que está disponible para la vcnta en \\"Il1ul/i"dl'iew.nel.
Control de . ., eJecuclon Al igual que las criaturas sensibles, un programa debe manipular su mundo y tomar decisiones durante la ejecución. En Java, las decisiones se toman mediante las instrucciones de control de ejecuc ión. Java utiliza todas las instrucciones de control de ejecución de e, por lo que si ha programado antes con e o C++, la mayor parte de la infonnación que vamos a ver en este capítulo le resultará fam il iar. La mayoría de los lenguajes de programación procedimental disponen de alguna clase de instrucciones de control, y suelen existir solapamientos entre los distintos lenguajes. En Java, las palabras clave incluyen if-else, while, do-whUe, for, return, break y una instrucción de selección denominada switch. Sin embargo, Java no soporta la despreciada instrucción goto (q ue a pesar de ello en ocasiones representa la fonna más directa de resolver ciertos tipos de problemas). Se puede continuar realizando un salto de estilo goto, pero está mucho más restringido que un goto típico.
true y false Todas las instrucciones condicionales utilizan la veracidad o falsedad de una expresión condicional para determinar la ruta de ejecución. Un ejemplo de expresión condicional sería a = b. Aquí, se utili za el operador condicional = para ver si el va lor de a es equivalente al va lor de b. La expresión devuelve true o false. Podemos utilizar cualquiera de los operadores relacional es que hemos empleado en el capí tulo anterior para escri bir una instrucción condicional. Observe que Java no permite utilizar un número como boolean , a diferencia de lo que sucede en e y e++ (donde la veracidad se asocia con valores disti ntos ce ro y la falsedad con cero). Si quiere emplear un va lor no boolean dentro de una prueba boolean, como por ejemplo if(a), deberá primero convertir el valor al tipo boolean usando una expresión condicional, como por ejemplo if(a != O).
if-else La instmcción if-else representa la fonna más básica de controlar el flujo de un programa. La cláusula elsc es opcional, por lo que se puede ver if de dos fonnas distintas: if (expresión-booleana ) instrucción
o if (expresión-booleana ) instrucción else instrucción
La expresión-booleana debe producir un resultado boolean. La instrucción puede ser una instmcción simple temlinada en punto y coma o una instrucción compuesta, que es un gmpo de instmcciones simples encerrado entre llaves. All í donde empleemos la palabra instrucción querremos decir siempre que esa instmcc ión pued e ser simple o compuesta. Co mo ejemplo de ir-else, he aquí un método teste ) que indica si un cierto valor está por encima, por debajo o es equivalente a un número objetivo:
72 Piensa en Java
//: control/lfElse.java import static net.mindview.util.Print.*; public class IfElse { static int result =
o;
static void test (int testval, int target) if(testval > target)
{
result = +1; else if(testval < target) result -1; else result O; /1 Coincidencia public sta tic void main (String [] args)
{
test(lO,5);
print(result) ; test(5,10);
print(result) ; test(S,5);
print (result) ;
/*
Output:
1
-1 O
* /// ,En la pal1e central de test( ), también puede ver una instnlcción "else if," que no es una nueva palabra clave sino una instnacción cisc seguida de una nueva instrucción ir. Aunque Java, como sus antecesores e y C++, es un lenguaje de "fonnato libre" resulta habitual sangrar el cuerpo de las instrucciones de control de flujo, para que el lector pueda detenninar fácilmente dónde comienzan y dónde tenninan .
Iteración Los bucles de ejecución se controlan mediante whil e, do~w h ile y for, que a veces se clasifican como instrucciones de iteración. Una detenninada instrucción se repite hasta que la expresión-booleana de control se evalúe como falseo La fonna de un bucle whil e es: while (expresi6n-booleanal instrucción
La expresión-booleana se evalúa una vez al principio del bucle y se vuelve a evaluar antes de cada suces iva iteración de la instrucción. He aquí un ejemplo si mple que genera números aleatorios hasta que se cumple una detenninada condición.
JJ: controlJWhileTest.java JJ Ilustra el bucle while. public class WhileTest { static boolean condition( ) boolean result = Math.random( ) < 0.99; System.out.print(result + tI, n); return result; public static void main(String[] args) { while(condition()) System.out.println(nInside 'while' " ); System. out. println (U Exited 'while''') i
J* (Ejecútelo para ver la salida) *JJJ :-
4 Control de ejecución 73 El metodo condition() utiliza el método random( ) de tipo sta tic de la biblioteca Math, que genera un valor double comprendido entre O y 1 (incluye 0, pero no l.) El valor result proviene del operador de comparación <, que genera un resultado de tipo boolean oSi se imprime un va lor boolean , automáticamente se obtiene la cadena de caracteres apropiada "true" o "false", La expresión condicional para e l bucle wbile dice: "repite las instrucciones del cuerpo mientras que condition() devuelva truc",
do-while La forma de do-while es do
instrucción while (expresión-booleana )
j
La única diferencia entre ""hile y do-while es que la instrucción del bucle do-while siempre se ejecuta al menos una vez, incluso aunque la expresión se evalúe como false la primera vez. En un bucle while, si la condición es false la primera vez, la instrucción nunca llega a ejecutarse. En la práctica, do-while es menos co mún que ",hUe.
for El bucle for es quizá la forma de iteración más habitualmente utilizada. Este bucle realiza una inicialización antes de la primera iteración. Después realiza una prueba condicional y, al final de cada iteración, lleva a cabo alguna fOffila de "avance de paso". La fonna del bucle for es: for(inicialización; expresión-booleana ; paso) instrucción
Cualquiera de las expresiones inicialización, expresión-booleana o paso puede estar vacía. La expresión booleana se comprueba antes de cada iteración y, en cuanto se evalúe como false, la ejecución continúa en la línea que sigue a la instrucción for oAl final de cada bucle. se ejecuta el paso. Los bucles for se suelen utilizar para tareas de "recuento";
/1: control/ListCharacters.java /1 Ilustra los bucles IIfor ll enumerando
II todas las letras minúsculas ASCII. public class ListCharacters { public statie void main(String[] args) { for{char c = O; C e 128; c++) if(Character.isLowerCase(c) ) System. out. println ("value: " + (int) c + character: 11 + el i
/, Output : value: 97 character: a value: 98 character: b value: 99 character: e value: 100 character: d value: 101 character: e value: 102 character: f value : 103 character: 9 value: 104 character: h value: 105 character: i va lue: 106 character: j * /// ,Observe que la variable e se define en el mismo lugar en el que se la utiliza, dentro de la expresión de control correspondiente al bucle for, en lugar de definirla al principio de main(). El ámbito de e es la instrucción controlada por foro
74 Piensa en Java
Este programa también emplea la clase "envo ltorio" java.lang.Character. que no sólo envuelve ellipo primili vo char dentro de un objeto, sino que también proporciona Olras utilidades. Aquí el mélOdo static isLowcrCase( ) se usa para detectar si el carácter en cuestión es una letra minúscula. Los lenguajes procedimental es tradicionales co mo e requieren que se definan todas las variables al comienzo de un bloque, de modo que cuando el compilador cree un bloque, pueda asignar espacio para esas va riabl es. En Java y C++, se pueden distribuir las declaraciones de variables por todo el bloque, definiéndolas en el lugar que se las necesite. Esto permite un estilo más natural de codificación y hace que el código sea más fácil de entender.
Ejercicio 1:
(1) Esc riba un programa que imprima los val ores comprendidos entre I y 100.
Ejercicio 2:
(2) Escriba un programa que genere 25 va lores int aleatorios. Para cada va lor, utilice una instrucción if-elsc para clasificarlo como mayor que, menor que o igual a un segundo va lor generado aleatoriamente.
Ejercicio 3:
(1) Modifique el Ejercicio 2 para que el código quede rodeado por un bucle while ·'infinito". De este modo, el programa se ejecutará basta que lo interrumpa desde el teclado (nomlalmente, pulsando Contro l-C).
Ejercicio 4:
(3) Escriba un programa que utilice dos bucles ror an idados y el operador de módulo (% ) para detectar e imprimir números primos (números en teros que no son di visibles por ningún número excepto por sí mismos y por 1).
Ejercicio 5:
(4) Repita el Ejercicio 10 del capitulo anterior, utilizando el operador ternario y una comprobación de tipo bit a bit para mostrar los unos y ceros en lugar de lnteger.toBinaryString( ).
El operador coma Anterionnente en el capítulo, hemos dicho que el operador coma (no el separador coma que se emplea para separar definiciones y argumentos de mélOdos) sólo ti ene un uso en Java: en la ex presión de control de un bucle foroTanto en la parte correspondiente a la inicialización como en la parte correspondiente al paso de la ex presión de control, podemos incluir una seri e de instrucciones separadas por comas, y dichas insrrucciones se evaluarán secuencialmente. Con el operador coma, podemos definir múltiples variables dentro de una instrucción for, pero todas ellas deben ser del mismo tipo:
11 :
control/CornmaOpe rat or . java
public class CommaOperator { public static void main (String [] args) { for(int i = 1, j = i + la; i < 5; 1++, j System.out.println("i = n + i + I! j ::
1* i i
i i
= U
i * 2) + j) i
Output: 1 j 11 2 j 4 3 j 6 4 j B
* ///,-
La definición int de la instrucción for cubre tanto a i como a j . La parte de inicialización puede tener cualquier número de defmiciones de /In mismo tipo. La capacidad de definir variabl es en una ex presión de control es tá limitada a los bucles foro No puede emplearse esta técnica en ninguna otra de las restantes instrucciones de selección o iteración. Puede ver que, tanto en la parte de iniciali zación co mo en la de paso, las instmcciones se evalúan en orden secuencial.
Sintaxis foreach Java SE5 introduce ulla sintaxi s for nueva. más sucinta, para utili zarl a co n matrices y cont enedores (hablaremos más en detalle sobre este tipo de objetos en los Capítulos 16, Matrices, y 17, Análisis detallado de los contenedores). Esta sintax is se denomina sintaxisjoreach (para todos), y quiere decir que no es necesario crear una variable int para efeCnl8f un recuento a través de lUla secuencia de elementos: el bucle for se encarga de generar cada elemenlO automáticamente.
4 Control de ejecución 75 Por ejemplo, suponga que tiene una matriz de valores float y que quiere seleccionar cada uno de los elementos de la matri z:
1/ : controljForEachFloat . java import java.util. *i public class ForEachFloat public static void main(String(]
Random rand
args)
{
= new Random(47)¡
float f [] far (int i
= new float [la] ; = O; i < 10 ; i+ + } f[i] = rand.nextFloat() i for ( float x f)
System.out.println(x)
i
/ * Output: 0 .72711575 0.399 82635 0 .5309454 0.0534122 0.16020656 0.57799 757 0 .18847865 0.4170137 0.5 1660204 0 .73734957 * jjj , -
La matriz se rellena utilizando el antiguo bucle foreach en la Iínca: f or (float x
fOf ,
porque debe accederse a ella mediante un índice. Puede ver la sintaxis
: f)
Esto define una variable x de tipo float y asigna secuencialmente cada elemento de fax.
Cualquier método que devuelve una matriz es buen candidato para emplearlo con la sintaxisforeach. Por ejemplo, la clase String tiene un método toCharArray() que devuelve una matriz de char, por lo que podemos iterar fácilmente a través de los caracteres de una matriz:
JI : control/ForEachString . java public c!ass ForEachString { public static void mai n (S tring[] argsJ for (char e : "An African Swallow". toCharArray () System.out.print(c + " " );
1* Output: A n
A f r i
e a n
s
w a 1 1 o w
* jjj ,-
Como podremos ver en el Capítulo 11, Almacenamiento de objetos , la sintaxisjoreach también funciona con cualquier objeto que sea de tipo Iterable. Muchas instrucciones for req uieren ir paso a paso a través de una secuencia de valores enteros como ésta: for (in t i
=
o¡ i
<
100; i++)
Para este tipo de bloques. la sintaxi sforeach no funcionará a menos que queramos crear primero una matri z de valores int. Para simplificar esta tarea, he creado un método denominado range() en net.mindview.utiI.Range que genera automáticamente la matriz apropiada. La intención es que range( ) se utilice como importación de tipo static:
11 : control/ForEachInt . java import static net.mindview.util.Range.*¡ import static net.mindview.util.Print.*¡
76 Piensa en Java public class ForEachInt { public static void main (S tring [] for {int
, range(10)) printnb (i + 11 " ) ;
print ()
i
args)
{
// 0 .. 9
i
for {int i
, range {5,
printnb (i + 11 " ) ; print () i for (int i : range(5, printnb(i + " "); print{)¡
10))
20,
// 5 .. 9
3))
II 5 . . 20 step 3
1* Output : O 1 2 3 4 5 6 7 8 9 5 6 7 8 9 58111417
* /// ,El método range( ) está sobrecargado, lo que quiere decir que puede utilizarse el mismo método con diferentes listas de argumentos (en breve hablaremos del mecanismo de sobrecarga). La primera fonna sobrecargada de range() empieza en cero y genera valores hasta el extremo superior del rango, sin incluir éste. La segunda fonTIa comienza en el primer valor y va hasta un valor menos que el segundo, y la tercera fonna incluye un valor de paso, de modo que los incrementos se realizan según ese valor. range( ) es una versión muy simple de lo que se denomina generador, que es un concepto del que hablaremos posteriormente en el libro. Observe que aunque range() pemlite el uso de la sintaxisforeach en más lugares, mejorando así la legibilidad del código, es algo menos eficiente, por lo que se está utili zando el programa con el fm de conseguir la máxima velocidad, conviene que utilice un perfilador, que es una herramienta que mide el rendimiento del código. Podrá obse rvar también el uso de priotnb() además de print(). El método printnb() no genera un carácter de nueva línea, por lo que pemlite escribir una línea en sucesivos fragmentos. La sinta~isfo,.each no sólo ahorra tiempo a la hora de escribir el código. Lo más importante es que facilita la lectura y comunica perfectamente qué es lo que estamos tratando de hacer (obtener cada elemento de la matriz) en lugar de proporcionar los detalles acerca de cómo lo estamos haciendo ("Estoy creando este índice para poder usarlo en la selección de cada uno de los elementos de la matri z"). Utilizaremos la sintaxi sforeach siempre que sea posible a lo largo del libro.
return Diversas palabras clave representan lo que se llama un salIO incondicional, lo que simplemente quiere decir qu e el salto en el flujo de ejecución se produce sin reali zar previamente comprobación alguna. Dichas palabras clave incluyen return, break, continue y una forma de saltar a una instrucción etiquetada de fonna similar a la instrucción goto de otros lenguajes. La palabra clave returD tiene dos objetivos: especifica qué valor devolverá un método (si no tiene un valor de retomo de tipo void) y hace que la ejecución salga del método actual devolviendo ese valor. Podemos reescribir el método lest( ) precedente para aprovechar esta característica:
1/ : control/lfElse2.java import static net.mindview.util.Print.*¡ public class IfElse2 { static int test (int testval, int target) if(testval > target } return +1; else if{testval < target ) return -1; el se return O¡ 1I Coincidencia
{
4 Control de ejecución 77 p u b l ic sta t ic void main (String[] args l { print ( test (1 0 , S I 1 ; print ( test (S, 1 0 » ; print ( test ( 5,5 )) ;
/ * Output : 1
-1
°*/1/ ;No hay necesidad de la cláusula else, porque el método no continuará después de ej ecutar una instrucción return. Si no inclu ye una instrucción returo en un método que devuel ve un valor void, habrá una instrucción returo implícita al fina l de ese método, así que no siempre es necesario incluir dicha instmcción. Sin embargo, si el método indica que va a devolver cualquier otro valor di stinto de void, hay que garantizar que todas las rutas de ejecución del códi go devuel van un
valor.
Ejercicio 6:
(2) Modifique los dos métodos teste ) de los dos programas anteriores para que admitan dos argumentos adicionales, begin y cnd, y para que se compruebe testval para ver si se encuentra dentro del rango comprendido entre begin y end (ambos incluidos).
break y continue También se puede controlar el flujo del bucle dentro del cuerpo de cualquier instrucción de iteración utili zando break y continue. break provoca la saLida del bucle sin ejecutar el resto de la instmcciones. La instrucción continue detiene la ejecución de la iteración actual y vuelve al principio del bucle para comenzar con la siguiente iterac ión . Este programa muestra ejemplos de break y continue dentro de bucles ror y while: // : control / BreakAndContinue.java // Ilustra las palabras clave bre ak y continue . import static net . mindvie w.util . Range .* ; public class BreakAndContinue { public static void main (String[ ] args ) { f or {int i = O; i < 10 0 ; i++ ) { if (i == 74 ) break; // Fuera del bucle if {i % 9 1= O) c o ntinu e; // Siguiente itera c i ó n System. o ut . print (i + " 11 ) ; System. ou t.println {) ; // Uso de f o reach: for (int i : range (l OO)) if ( i == 74 ) break; // Fuera de l b ucl e if {i % 9 ! = O) centi n ue; // Siguiente iteración System. e ut.print (i + 11 " ) ; System . out . print ln () ; int i = O; / / Un "bucle infinito ll while (t r ue ) {
:
i+ + ;
i nt j = i * 27 i i f ( j == 1269 ) b reak ; // Fuera del bucle f er if ( i % 10 ! = O) centi nue; // Principio del bucle System.out . print (i + 11 " ) ;
/ * Output:
78 Piensa en Java
o o
9 18 27 36 45 54 63 72 9 18 27 36 45 54 63 72 10 20 30 40
* /// ,En el bucle for, el va lor de i nunca llega a 100. porque la instrucción break hace que el bucle termine cuando ¡ va le 74. Nommlmente. utilizaremos una instrucción break como ésta sólo si no sabemos cuándo va a cumplirse la condición de terminación. La instrucción continue hace que la ejecución vuelva al principio del bucle de iteración (incrementando por tanto i) siempre que i no sea divisible por 9. Cuando lo sea, se imprimirá el valor. El segundo bucle for muestra el uso de la sintaxisforeach y como ésta produce los mismos resultados. Finalmente, podemos ver un bucle while " infinito" que se estaría ejecutando, en teoría, por siempre. Sin embargo, dentro del bucle hay una instmcción break que hará que salgamos del bucle. Además, podemos ver que la instrucción continue devuelve el control al principio del bucle sin ejecutar nada de lo que hay después de dicha instrucción contin ue (por tanto, la impresión sólo se produce en el segundo bucle cuando el va lor de i es divisible por 10). En la salida, podemos ver que se imprime el va lor O, porque O % 9 da como resultado O. Una segunda forma del bucle infutito es ror(;;). El compilador trata tanto while(true) como ror(;;) de la misma fonna , por lo que podemos uti lizar una de las dos fOffilas según prefiramos. Ejercicio 7 :
( 1) Modifique el Ejercicio 1 para que el programa temúne usando la palabra clave break con el va lor 99.
Intente utilizar return en su lugar.
La despreciada instrucción "goto" La palabra clave goto ha estado presente en muchos lenguajes de programación desde el principio de la Infomlática. De hecho, goto representó la génesis de las técnicas de control de programa en los lenguajes ensambladores: "Si se cumple la condición A, salta aquí; en caso contrario, salta allí". Si leemos el código ensamblador generado por casi todos los compiladores, podremos ver que el control de programa contiene muchos saltos (el compi lado r Java produce su propio "código ensamblador", pero este código es ejec utado por la máquina virtual Java en lugar de ejecutarse directamente sobre un procesador hardware). Una instmcción goto es un salto en el ni ve l de código fuente, yeso es lo que hizo que adquiriera una mala reputación. Si un programa va a saltar de un punto a otro, ¿no ex iste alguna fonna de reorganizar el código para que el flujo de control no tenga que dar saltos? La instrucción goto llegó a ser verdaderamente puesta en cuestión con la publicación de l famoso artículo "GOlo considered harmfuf' de Edsger Dijkstra, y desde entonces la caza del goto se ha convertido en un deporte Illuy popular, forzando a los defensores de esa instrucción a ocultarse cuidadosamente. Como suele suceder en casos como éste, la verdad está en el punto medio. El problema no está en el uso de goto, sino en su abuso, en detenninadas situaciones especiales goto representa. de hecho, la mejor fonna de estmcturar el flujo . Aunque goto es una palabra reservada en Java, no se utiliza en el lenguaje. Java no dispone de ninguna instrucción goto. Sin embargo, sí que dispone de algo que se asemeja a un salto, y que está integrado dentro de las palabras clave break y con tinue. No es un salto, sino más bien una fornla salir de una instrucción de iteración. La razón por la que a menudo se asocia este mecanismo con las discusiones relativas a la instrucción go to es porque utili za la misma técnica: una etiqueta. Una etiqueta es un identificador seguido de un carácter de dos puntos, como se muestra aquí: labell ,
El lÍnico lugar en el que una etiqueta resulta útil en Java es justo antes de una instrucción de iteración. Y queremos decir exactamente justo antes: no resulta conven iente poner ninguna instrucción entre la etiqueta y la iteración. Y la única razón para colocar una etiqueta en una iteración es si vamos a anidar otra iteración o una instmcción switc h (de la que hablaremos enseg uida) dentro de ella. Esto se debe a que las palabras clave break y continue nOn1lalmente sólo intemlmpirán el bucle actual, pero cuando se las usa como una etiqueta intemllnpen todos los bucles hasta el lugar donde la etiqueta se haya definido: labell ,
iteración-externa iteración-interna
4 Control de ejecución 79
/ / ... break ; / /
111
/ / ... con tinue ;
//
(2 )
/ / ..
JI
continue labell¡
(3)
/ / ... break labell;
//
14 1
En (1). la instrucción break hace que se sa lga de la iteración interna y que acabemos en la iteraci ón ex terna. En (2), la instrucción continue bace que vo lvamos al principio de la iteración interna. Pero en (3), la instrucción continue label) hace que se salga de la iteración imema y de la iteración ex terna, hasta situarse en labell . Entonces, continúa de hecho con la iteración, pero comenzando en la iteración externa. En (4), la instrucción break labell también hace que nos salgamos de las dos iteraciones has ta situamos en labell , pero sin volver a entrar en la iteración. De hecho, ambas iteraciones habrán finalizado. He aq uí un ejemplo de utilización de bucles for :
JI : control/LabeledFor.java / / Bucles ter con tlbreak eqtiquetado n y "con tinue etiquetado". import static net.mindview.util . Print. *¡
public class LabeledFor { public static void main(String[] args) int i = O; outer: II Aquí no se pueden incluir instrucciones for ( ; true i ) ( II bucle infinito inner: II Aquí no se pueden incluir instrucciones for ( ¡ i < 10; i++) { print ( " i = " + i ) ; if Ii
==
2)
{
print ( "continue " ); continue¡
} if I i
==
3)
(
print ( "break" ) ; i++; II En caso contrario, i nunca II se incrementa. break; if li
== 71
print ("continue oucer"); i++; II En caso contrario, i nunca II se incrementa. continue outer; if l i
== S I
print ("break outer" ) ; break outer; for (int k if l k
=
O; k
<
5; k++ } {
== 31 {
print ( " continue inner 11 ) continue inner;
;
80 Piensa en Java
/1 Aquí no se puede ejecutar break o continue para saltar a etiquetas
1* Output: i
=
O
continue inner i
=
1
continue inner i
=
2
continue i
=
3
break i
=
4
continue inner i
=
5
continue inner i
=
6
continue inner i
=
7
continue outer i
=
8
break outer
*/1/,Observe qu e break hace que salgamos del bucle for, y que la expresión de incremento no se ejecuta hasta el final de la pasada a través del bucle fOf o Puesto que break se salta la expresión incremento, el incremento se realiza directamente en el caso de i = 3. La instrucción continue outer en el caso de i = 7 también lleva al principio del bucle y también se salta el incremento, por lo que en este caso tenemos también que realizar el incremento directamente.
Si no fuera por la instrucción break outer no habría fonna de salir del bucle ex temo desde dentro de un bucle interno, ya que break por sí misma sólo permite sal.ir del bucle más interno (lo mismo cabría decir de continue). Por supuesto, en aquellos casos en que salir de un bucle impl.ique salir también del método, basta con ejecutar return. He aquí una demostración de las instrucciones break y continue etiquetadas con bucles while: ji : control/LabeledWhile.java / / Bucles while con "break etiquetado" y "continue etiquetado". import static net.mindview.util.Print. * ¡
public class LabeledWhile { public static void main{String[] int i = O; outer: while(true) print ( "Outer while loop"); ",hile (t rue ) { i++¡ print{lIi = " + i) ¡ if (i == 1) { print ( " continue") ; continue;
) if(i == 3) { print ( " continue outer") ¡ continue outer¡ if ( i == 5 ) print ("break 11 l ; break¡ if(i == 7)
argsl
{
4 Control de ejecución 81 print ("break oute r" ); break outer;
1*
Output:
Outer while loop i
=
1
continue i = 2 i
=
3
continue Quter Outer while loop i i
= =
4 5
break Outer while loop i = 6 i = 7 break outer
*///,Las mismas reglas siguen siendo ciertas para while : 1. Una instrucción contin ue nonnal hace que sa ltemos a la parte superior del bucle más interno y continuemos allí la ejecución.
2. Una instrucción con tin ue etiquelada hace que sa ltemos hasta la etiqueta y que volvamos a ejecutar el bucle si tuado justo después de esa etiqueta. 3. Una instrucción b reak hace que finalice el bucle. 4. Una instrucción break etiquetada hace que finalicen todos los bucles hasta el que tiene la etiqueta, incluyendo este último. Es importante recordar que la única razón para utilizar etiquetas en Java es si tenemos bucles anidados y queremos ejecutar una instrucción break o continue a través de más de un nivel. En el artículo "Goto considered harmfllf' de Dijkstra. la objeción específica que él hacía era contra la utili zac ión de etiquetas, no de la instrucción goto. Su observación era que el número de errores parecía incrementarse a medida que lo hacía el número de etiquetas dentro de un programa, y que las etiquetas en las instrucciones goto hacen que los programas sean más dificiles de analizar. Observe que la etiquetas de Java no presentan este problema, ya que están restringidas en cuanto a su ubicación y pueden utilizarse para transferi r el control de fonna arbitraria. También merece la pena observar que éste es uno de esos casos en los que se hace más útil una detemlinada característica del lenguaje reduciendo la potencia de la correspondiente instmcción.
switch La palabra clave switch a veces se denomina instrucción de selección. La instmcción switch permite seleccionar entre distintos fragmentos de código basándose en el valor de una expresión entera. Su fonna general es: switch(selector-entero) case valor-entero! case valor-entero2 case valor-entero3 case valor-entero4 case valor-enteroS // ... default: instrucción ;
{ instrucción¡ instrucción ¡ instrucción ¡ instrucción¡ instrucción ¡
break; break; break; break; break;
82 Piensa en Java
SeleCfOr-entero es una expresión que genera un va lor entero. La instrucción switch co mpara el res ultado de selecfOr-enrero
con cada valor-el1fero. Si encuentra un a coi ncidencia, ejecuta la correspondiente instrucción (una sola instmcción o múltiples instmcciones: no hace falta usar lla ves). Si no hay ningun a coi ncidencia, se ejec uta la instrucción de default. Observará en la defini ción anterior que cada case finali za con una instmcción break, lo que hace que la ejecución salte al final del cuerpo de la instrucción switch . Ésta es la forma convencional de construir una instrucción switch , pero la instnlcción break es opcional. Si no se inclu ye, se ejec utará el códi go de las instmcciones case si tuadas a continuac ión hasta que se encuentre una instmcc ión break. Aunque normalmente este comportamiento no es el deseado, puede resultar últil en ocasiones para los programadores expertos. Observe que la última instrucción, situada después de la cláusula default, no tiene una instrucción break porque la ejecución continúa justo en el lugar donde break haría que continuara. Podemos incluir, si n que ello represente un problema, una instmcción break al final de la cláusula default si consideramos que resulta importante por razones de estilo. La instmcción s,,'itch es una fonna limpia de implementar selecciones multi vía (es decir, selecciones donde hay que elegir entre diversas rutas de ejecución), pero requiere de un selector que se evalúe para dar un va lor entero, como int o charo Si se desea emplear, por ejemplo, una cadena de caracteres o un nllmero en coma flotante como selector, no funcionará en una instmcción switch. Para los tipos no enteros, es preciso emplear una serie de instm cciones ir. Al final del siguiente capítulo, veremos que la nueva característica enum de Java SE5 ayuda a sua viza r esta restricción, ya que los valores enum están diseiiados para funcionar adecuadamente con la instrucción switch . He aquí un ejemplo en el que se crean letras de manera aleatoria y se detennjna si son vocales o consonantes: //: control/VowelsAndConsonan ts.java /1 Ilustra la instrucción switch. import java . util .*; import static net.mindview . ucil . Print .* ; public class VowelsAndConsonants { public static void main(String[] args} Random rand = new Random(47) ¡ for(int i = O; i < 1 00; i++) int e = rand.nextInc(26) + 'a'; printnb ( (char) c + ", 11 + C + ": " ) ; switch(c) { case 'a': case 'e': case 'i': case ' o ': case ' u': print("vowel"l i break; case 'y': case 'w': print ("Sometimes a vowel"); break; defaul t: print (" consonant") ;
/ * Output : y, n, z, b, r, n, y, g, c, f, o,
121, Sometimes a vowel 11 0 , consonant 122, consonant 98, consonant 114: consonant 110, consonant 121 : Somecimes a vowel 103, consonant 99, consonanc 102, consonant 1110 vowel
4 Control de ejecución 83 w, 119: Sometimes a vowel z, 122: consonant
, /// ,Puesto que Ra ndom .nextln t(26) genera un valor comprendido entre O y 26, basta con sumar ' a' para generar las letras minúsculas. Los caracteres encerrados entre comillas simples en las instmcciones case también generan valores enteros que se emplean para comparación.
Observe cómo las instrucciones case pueden "apilarsc" unas encima de otras para proporcionar múltiples coincidencias para un detenllinado fragmento de código. Tenga también en cuenta que resulta esencial colocar la instrucción b reak al final de una cláusula case concreta; en caso contrario, el control no efectuaría el salto requerido y continuaría simplemente procesando el caso siguiente. En la instrucción : int e
= rand.nextInt(26)
+
'a'
i
Random. nextl nt( ) genera un valor int aleatorio comprendido entre O y 25, al que se le suma el valor ' a '. Esto quiere decir que 'a' se convierte automáticamente a int para real izar la suma. Para imprimir c como carácter, es necesario proyectarlo sobre el tipo char; en caso contrario, generana una salida de tipo entero. Ejercicio 8 :
(2) Cree una instrucción switch que imprima un mensaje para cada case, y coloque el switch dentro de un bucle fo r en el que se pruebe cada uno de los va lores de case. Incluya una instrucción break después de cada case y compruebe los resultados; a conti nuación, elimine las instrucciones brea k y vea lo que sucede.
Ejerc icio 9 :
(4) Una secllencia de Fibonacci es la secuencia de números 1, 1,2,3,5,8, 13,21,34, etc., donde cada número (a partir del tercero) es la suma de los dos anteriores. Cree un método que tome un entero como argumento y muestre esa cantidad de números de Fibonacci comenzando por el principio de la secuencia; por ejemplo, si ejecuta java Fibonacci 5 (donde Fi bonacci es el nombre de la clase) la salida seria: 1, 1. 2,3,5.
Ejercicio 10:
(5) Un nlÍmero vampiro tiene un número par de dígitos y se forma multiplicando una pareja de números que contengan la mitad del número de dígitos del resultado. Los dígitos se toman del número original en cualquier orden. No se permiten utilizar parejas de ceros finales. Ent re los ejemplos tendríamos: 1260 ~ 21 • 60 1827 ~ 21 • 87 2187~27' 8 1 Escriba un programa que detennine todos los números vampiro de 4 dígitos (problema sugerido por Dan Forhan).
Resumen Este capítulo concluye el estudio de las características fundamentales que podemos encontrar en la mayoría de los lenguajes de programación : cálculo, precedencia de operadores, proyección de tipos y mecanismos de selección e iteración. Ahora esta mos li stos para dar los siguientes pasos, que nos acercarán al mundo de la programación orientada a objetos. El siguiente capítulo cubrirá las importantes cuestiones de la inicialización y limpieza de objetos, a lo que seguirá, en el siguiente capirulo, el concepto esencial de ocultación de la implementación. Pueden encontrarse las soluciones a los ejercicios selecc ionados en el documento electrónico rhe Thi"ki"g in Java Am/Olllled So/mion Cuide. disponible para la venta en l\w\I:MindViel\:nel.
Inicialización y limpieza A medida que se abre paso la revolución infonllática, la programación "no segura" se ha convertido en uno de los mayores culpables del alto coste que tiene el desarrollo de programas. Dos de las cuestiones relativas a la seguridad son la inicialización y la limpieza. Muchos errores en e se deben a que el programador se olvida de inicializar una variable. Esto resulta especialmente habitual con las bibliotecas, cuando los usuarios no saben cómo inicializar un componente en la biblioteca, e incluso ni siquiera son conscientes de que deban hacerlo. La limpieza también constituye un problema especial, porque resulta fácil olvidarse de un elemento una vez que se ha tenninado de utilizar, ya que en ese momento deja de preocuparnos. Al no borrarlo. los recursos utilizados por ese elemento quedan retenidos y resulta fácil que los recursos se agoten (especialmente la memoria). C++ introdujo el concepto de constructor, un método especial que se invoca automáticamente cada vez que se crea un objeto. Java también adoptó el concepto de constructor y además di spone de un depurador de memoria que se encarga de liberar automáticamente los recursos de memoria cuando ya no se los está utilizando. En este capítulo, se examinan las cuestiones relativas a la inicialización y la limpieza, así como el soporte que Java proporciona para ambas tareas.
Inicialización garantizada con el constructor Podemos imaginar fácilmente que sería sencillo crear un método denominado in iti a lize( ) para todas las clases que escribiéramos. El nomb re es una indicación de que es necesari o invocar el método antes de utilizar el objeto. Lamen tableme nte, esto indica que e l usuario debe recordar que hay que in voca r ese método. En Java, el diseñador de un a clase puede garantizar la inicialización de todos los objetos proporcionando un constructor. Si una clase tiene un construclOr, Java invoca automáticamente ese constructor cuando se crea un objeto, antes incluso de que los usuarios puedan llegar a utili zarlo. De este modo, la inicializac ión queda garantizada. La siguiente cuestión es cómo debemos nombrar a este método, y existen dos problemas a este respecto. El primero es que cualquier nombre que usemos podría colisionar con otro nombre que quisiéramos emplear como miembro de la c lase. El segundo problema es que debido a que el compilador es responsable de invocar e l constructor. debe siempre conocer qué método invocar. La solución en e++ parece la más fácil y lógica, de modo que también se usa en Java: el nombre del constructor coincide con e l nombre de la clase. De este modo, resulta fácil in vocar ese método automáticamente durant e la inicialización. He aquí una clase simple con un constructor:
// : initializationfSimpleConstructor.java // Ilustración de un constructor simple. class Rock { Rock () { / f Éste es el constructor System.out.print(IIRock 11 ) j
public class SimpleConstructor { public static void main (String[] args) for(int i '" O; i < 10¡ i++)
{
86 Piensa en Java new Rack ()
i
1* Output: Rack Rack Rack Rock Rock Rack Rock Rack Rack Rack
* /// ,Ahora, cuando se crea un objeto: new Rock() i
se asigna el correspondiente espacio de almacenamiento y se invoca el constmctor. De este modo, se garantiza que el objeto está apropiadamente inicializado antes de poder utilizarlo. Observe que el estilo de codificación consistente en poner la primera letTa de todos los métodos en minúscula no se aplica a los constructores, ya que el nombre del constructor debe coincidir exactamente con el nombre de la clase. Un constmctor que no tome ningún argumcnto se denomina conslrucfOr predeterminado. Nonnalmente, la documentación de Java utiliza el ténnino constructor sin argumentos, pero el término "constructor predeterminado" se ha estado utilizando durante muchos ailos antes de que Ja va apareciera, por lo que prefiero uti lizar este últin10 ténnino. De todos modos, como cualquier otro método, el constructor puede también tener argumentos que nos penniten especificar cómo hay que crear el objeto. Podemos modificar fácilmente el ejemplo anterior para que el constructor tome un argumento:
11 : initialization/SimpleConstructor2.java
II Los constructores pueden tene r argumentos . class Rack2 ( Rack2(int i) System.out . print( "Rack
11
+ i
+
public class SimpleCanstructor2 { public sta tic void main (S tring[] for (int i = o¡ i < 8 i i++ ) new Rock2 ( i ) i
11
")
args)
i
{
1* Output : Rock O Rack 1 Rack 2 Rack 3 Rac k 4 Rack S Rack 6 Rock 7 * /// , Los argumentos dcl constructor proporcionan una forma de pasar parámetros para la inicialización de un objeto. Por ejemplo, si la clase Tree (árbol) tiene un constmctor que toma como argumento tm único número entero que indica la altura del árbol, podremos crear un objeto Tree como sigue: Tree t
=
new Tree (12)
i
/
I árbal de 12 metras
Si Tree(int) es el único constmctor del que di sponemos, el compilador no nos pennitirá crear un objeto Tree de ninguna otra forma. Los constructores eliminan una amplia clase de problemas y hacen que el código sea mas fácil de leer. Por ejemplo, en el fragmento de código anterior, no vemos ninguna llamada ex plícita a ningún método initialize() que esté conceptualmente separado del acto de creación del objeto. En Java, la creación y la inicialización son conceptos unificados: no es posible tener la una si n la otra. El constructor es un tipo de método poco usual porque no ti ene valor de retorno. Existe una clara diferencia entre esta circunstancia y los métodos que devuelven un valor de retorno vo id, en el sentido de que estos últimos métodos no devue lven nada. pero seguimos teniendo la opción de hacer que devuel van algo. Los constnlctores no devuel ven nada nunca, y no tenemos la opc ión de que se comporten de otro modo (la ex presión De\\' devuel ve una referencia al objeto recién creado, pero el constructor mismo no tiene un va lor de retomo). Si hubiera valor de retomo y pudiéramos seleccionar cuál es, el compilador necesitaría saber qué hacer con ese va lor de retomo.
Ejercicio 1:
(1) Cree una clase que contenga una referencia de tipo String no inicializada. Demuestre que esta referencia la inicializa Java con el va lor null.
5 Inicialización y limpieza 87
Ejercicio 2:
(2) Cree una clase con un campo String que se inicialice en el punto donde se defina, y otro campo quesea inicializado por el constlUclOr. ¿Cuál es la diferencia entre- las dos técnicas?
Sobrecarga de métodos U!1<) de las características más importantes en cualquier lenguaje de programación es el uso de nombres. Cuando se crea un objeto. se proporciona un nombre a un área de almacenamiento. Un método, por su paJ1e, es un nombre que sirve para designar una acción. Utilizamos nombres para referimos a todos los objetos y metodos. Una serie de nombres bien elegida crearú un sistema que resultará más fácil de entender y modificar por otras personas. En cierto modo, este problema se parece al acto dL' escribir literanlra: el objetivo es comunicarse con los lectores. Todos los problemas surgen a la hora de aplicar el concepto de matiz del lenguaje humano a los lenguajes de programación. A menudo. una misma palabra tiene diferentes significados: es lo que se denomina palabras polisémicas, aunque en el campo de la programación diríamos que están sobrecargadas. Lo que hacemos normalmente es decir "Lava la camisa", "Lava el coche" y '"Lava al pelTa": sería absurdo vernos forzados a decir "camisaLava la camisa". "cocheLava el coche" y "perroLava el pelTa" simplemente para que el oyente no se vea forzado a distinguir cuál es la acción que tiene que realizar. La mayoria de los lenguajes humanos son redundantes, de modo que podemos seguir detenninando el signiticado aún cuando nos perdamos algunas de las palabras. No necesitamos identificadores unívocos: podemos deducir el significado a partir del contexto. La mayoría de los lenguajes de programación (y C en panicular) exigen que dispongamos de un identificador unívoco para cada metodo (a menudo denominados.fimciones en dichos lenguajes). Así que no se pl/ede tener una función denominada print( ) para imprimir entt:'"ros y otra denominada igualmente print( ) para imprimir números c-n coma flotante, cada una de las funciones necesitará un nombre distintivo. En Java (yen e+-t-). hay otro factor que obliga a sobrecargar los nombres de los métodos: cl constructor. Puesto que el nombre del constructor está predetemlinado por el nombre de la clase, sólo puede haber un nombre de constmctor. Pero entonces. ¿qué sucede si queremos crear un objeto utilizando varias formas distintas? Por ejemplo, suponga que construimos una clase cuyos objetos pueden inicializarse de la fonna nomlal o leyendo la infonnación de un archivo. Harán falta dos constructores, el constructor predeterminado y otro que tome un objeto String como argumento, a través del cual suministraremos el nombre del archivo que hay que utilizar para inicializar el objeto. Ambos metodos serán constructo res, así que tendrán el mismo nombre: el nombre de la clasc. Por tanto, la sobrecarga de metodos resulta esencial para poder utilizar el mismo nombre de metodo con diferentes tipos de argumentos. Y. aunque la sobrecarga de metodos es obligatoria para los constructores. también resulta útil de manera general y puede ser empleada con cualquier otro método. He aquí un ejemplo que muestra tanto constmclores sobrecargados como metodos normales sobrecargados: / /: initialization/Overloadi ng. java // Ilustración del mecanismo de sobrecarga /.1 canto de constructores como de métodos normales. ~mporc scatic net.mindview.u ti l.Prin t.*· class Tree { ine height i Tree () { print ( 11 Plant i,-¡g a seedling") ¡ height =: O i Tree(int inicialHeight) height =: initialHeight¡ print ("Creating new Tree that is " + height + " feet. tall") i void info () print ("Tre e is
+
height +
void info(String s) print (s + ": Tree is
11
11
feet tall") ¡
+ height + " feet tall") ¡
88 Piensa en Java
public class Overloading { public static void main {String [ ] args ) { for (int i = Di i < 5; i++ } { Tree t = new Tree {i ) ; t. info () ; t. info ( lIoverloaded methad" ) ;
1/ Constructor sobrecargado: new Tree () ; /*
Output:
Creating new Tree that is O feet tall Tree is O feet tall
overloaded methad: Tree is O feet tall Creating new Tree that is 1 feet tall Tree is 1 feet tall overloaded methad: Tree is 1 feet tall
Creating new Tree that is 2 feet tall Tree is 2 feet tall overloaded methad: Tree is 2 feet tall Creating new Tree that is 3 feet tall Tree is 3 feet tall overloaded methad: Tree is 3 feet tall
Creating new Tree that is 4 feet tall Tree is 4 feet tall overloaded methad: Tree is 4 feet tall
Planting a seedling
./ // > Con estas definiciones, podemos crear un objeto Tree tanto a partir de una semilla, sin utilizar ningún argumento, como en fonna de planta criada en vivero, en cuyo caso tendremos que indicar la alnlra que tiene. Para soportar este comportamiento, hay un constructor predetenninado y otro que toma como argumento la altura del árbol. También podemos invocar el método info() de varias formas distintas. Por ejemplo, si queremos imprimir un mensaje adicional, podemos emplear info(String), mientras que utilizaríamos info() cuando no tengamos nada más que decir. Sería bastante extraño proporcionar dos nombres separados a cosas que se corresponden, obviamente. con un mismo concepto. Afortunadamente, la sobrecarga de métodos nos pennite utilizar el mismo método para ambos.
Cómo se distingue entre métodos sobrecargados Si los métodos tienen el mismo nombre. ¿cómo puede saber Java a qué método nos estamos refiriendo? Existe una regla muy simple: cada método sobrecargado debe tener una lista distintiva de tipos de argumentos. Si pensamos en esta regla durante un momento, vernos que tiene bastantes sentido. ¿De qué otro modo podría un programador indicar la diferencia entre dos métodos que tienen el mismo nombre, si no es utilizando las diferencias entre los tipos de sus argumentos? Incluso las diferenc ias en la ordenación de los argumentos son suficientes para distinguir dos métodos entre sí, aunque normalmente no conviene emplear esta técnica, dado que produce códi go dificil de mantener: 1/ : initialization/OverloadingOrder.java 1/ Sobrecarga basada en el orden de los argumentos. import static net.mindview.util.Print.*¡ public class OverloadingOrder static void f(String S, int i ) print ( " String : " + S + int:
u
+ i ) ¡
5 Inicialización y limpieza 89
static void f (int i, String s) { print (" int: 11 + i + ", String: 11 + s); public static void main.(String [] f ("String first", 11) i f {99, !tlnt first") i
args)
{
1* Output : String: String first, int: 11 int: 99, String: Int first *///,Los dos métodos f() tienen argumentos idénticos, pero el orden es distinto yeso es lo que los hace diferentes.
Sobrecarga con primitivas Una primitiva puede ser automáticamente convertida desde un tipo de menor tamaño a otro de mayor tamaiio, y esto puede inducir a confusión cuando combinamos este mecanismo con el de sobrecarga. El siguiente ejemplo ilustra lo que sucede cuando se pasa una primitiva a un método sobrecargado:
11: initialization/PrimitiveOverloading.java II Promoción de primitivas y sobrecarga. import static net .mindview.util . Print. *¡ public void void void void void void void
class PrimitiveOverloading { f1 (c har xl { printnb("f 1 (char) 11) i } f1 (byte x ) ( printnb("f1(byte) " ); } f1 (short x ) { printnb{"f1(short ) 11) i f1 (int x ) { printnb ( "f 1(int) ") ¡ } f1(long x ) ( printnb("fl(long) "); } f1 (fl oat x ) { printnb { "f1 (float ) ,, } i f1 (double x) { printnb ("fl (double) ");
void void void void void void
f2 (byte x) { printnb ( " f2 (byte ) " ); f2(short x l { printnb(Uf2(short) " }i f2 (int x) ( printnb("f2 (int) " ) ; } f2(long x) ( printnb("f2(long) "); } f2(float x ) { printnb(Uf2(float) ")i } f2(double x) (printnb("f2(double) ");
void void void void void
f3(short xl { printnb("f3(short) ")i f3(int x) { printnb( lt f3(int) tI)i } f3 (long x) ( printnb("f3 (long) " ); } f3 (f loat x l { printnb{ tl f3 (float ) 11); f3{double xl {printnb(lIf3{double) "),
void void void void
f4(int x ) { printnb ("f4(int ) ") i } f4 (long x) { printnb ( " f4 (long ) " ); f4 (float x l { printnb { fl f4 {float ) "); f4 (double x ) { printnb("f4 (double ) "),
void fS(long x l { printnb ( lIfS (long) " ) ¡ } void f5 ( float x ) { printnb ( "f5 ( float ) " ); void fS{double x l { printnb (" fS (double l "), void f6{float x l { printnb ( "f6 (float) 11 ) i void f6(double x l { printnb( lI f6(double) ") void f7 {double x l
{ printnb("f7(doublel
}
i
") ,
90 Piensa en Java void testConstVal() printnb (U 5: n); f1(S) ;f2(S) ;f3(S) ;f4(S) ;fS(S) ;f6(S) ;f7(S); print(); void testChar ()
{
char x = 'x' i printnb ("char:
")
i
fl(x) ;f2 (x) ;f3 (x ) ;f4 (x) ;fS(x) ;f6(x) ;f7(x); print();
void testByte () { byte x = Oi printnb (ltbyte: "); f1(x) ;f2 (x ) ;f3(x) ;f4 (x) ;fS(x) ;f6(x) ;f7(x); print(); void testShort ()
short x
=
{
O;
printnb (" short: n) i f1 (x ) ; f2 (x ) ; f3 (x ) ; f4 (x ) ; fS (x) ; f6 (x) ; f7 (x ); print () ; void testlnt (l
int x
=
{
O;
printnb ( " int: "); f1(x) ;f 2(x) ;f 3(x) ;f4 (x ) ; fS(x) ;f6(x) ;f7 (x) ; print();
) void testLong ()
{
long x = O; printnb(1I1ong :
It);
f1(x) ;f2 (x ) ;f3 (x ) ;f4 (x ) ;fS(x) ;f6(x) ;f7(x); print () ;
void testFloat ()
{
float x = O; printnb("float: "); f1 (x ) ; f2 (x) ; f3 (x ) ; f4 (x ) ; fS (x) ; f6 (x) ; f7 (x); print () ;
void testDouble () { double x = O; printnb ("doubl e: "); f1 (x) ; f2 (x ) ; f3 (x ) ; f4 (x ) ; fS (x ) ; f6 (x) ; f7 (x ); print () ;
public static void main (String [] args) PrimitiveOverloading p = new PrimitiveOverloading(); p.testConstVal() ;
{
p.testChar() ; p. testByte () ; p. testShort () ; p.testlnt() ; p. testLong () ; p. testFloat () i p. testDouble () ;
/ * Output: 50 f1 (int) f2(int) f3 (int) f4(int) fS ( long ) f6 ( float ) f7 (double ) charo f1 (cha r ) f2 (int) f3 (int) f4 (int) fS ( long ) f6 ( float ) f7 (double) byte o f1 (byte ) f2 (byte) f3 (short) f4 (i nc ) fS (long) f6 ( float ) f7 (double ) shorto f1(short) f2 (s hort ) f3(short) f4 (in t ) fS(long ) f6 ( float ) f7 (doub le ) into f1 ( int ) f2(int) f3 ( inc) f4 (i nt) fS(long) f6 ( float ) f7 (double) longo f1 (long) f2 ( long) f3 (long) f4 (long) fS (long) f6 (floa t ) f7 (double) floato f1(float ) f2(float) f3(float) f4(float) fS(float ) f6 ( float) f7 (double) doubleo fl(double) f2 (double ) f3(double) f4(double) fS (double) f6(double) f7(double)
*/// 0-
5 Inicialización y limpieza 91
puede ver que el valor constante 5 se trata como ¡ut, por lo que si hay disponible un método sobrecargado que lOma un objeto ¡ot. se utilizara dicho mélOdo. En todos los demás casos, si lo que tenemos es un tipo de datos más pequeño que el argu-
mento del método, di cho tipo de datos será promocionado. char produce un efecto ligeramente diferente, ya que, si no se encuentra una co rrespondencia exacta con cbar, se le promociona a int. ¿Qué sucede si nu estro argumenlO es mayor qu e el argumento esperado por el método sob recargado? Una modificación del programa ant eri or nos da la respuesta:
11: initialization/Demotion.java
JI
Reducción de primitivas y sobrecarga. import static net.mindview.util.Print.*;
public void void void void void void void
class Demotion { fl (char x) { print (Ufl {char} ji) ; fl (byte x) { print ("fl (byte)") ; fl (short x) { print (" fl (short) ") ; fl{int x) { print{ufl{int)")¡ } fl (long x) { print ( "fl (long)"); ) fl(float x) { print("fl(float) " ); fl (double x ) { print (ti fl (double) ti) ;
void void void void void void
f2 (char x ) { print {U f2 {char} U}; } f2 (byte x) { print ( " f2 (byte) " ) ; ) f2(short xl {print{ Uf2 {shortl " )¡ f2(int x) { print("f2(int)"); } f2 (long x) { print ( "f2 (long)"); ) f2(float x l { print("f2{floatl");
void void void void void
f3 (char x) { print ( "f3 (charl U) ; } f3 (byte x) { print ( " O (byte ) " ); ) f3{short xl {print(Uf3{short}")¡ f3(int x) {print("O(int) " ) ; } f3 (long x) { print ("O (long)");
void void void void
f4 (char xl print ( "f4 (char) 11); f4 (byte xl print ( "f4 (byte) 11 l ; f4(short xl {print ( Uf4 {short)U)¡ f4 (int xl { print{lIf4 ( int ) 11); }
void f5(char xl { print{tlf5(char) It} ¡ void f5(byte x) { print("f5(byte) "); void f5(short x) { print("f5(short)II); void f6{char xl void f6 (byte xl
print ("f6 {charl "); print {"f6 (byte) 11) ¡
void f7 (char xl
print{l'f7(charl ti) ¡
void testDouble() double x = o; print {Udouble argument : 11 l i fl (x) ; f2 ( (float) x) ; f3 ( (long) x) ; f4 ( (int) x ) ; f5 ( (s h ort) x) ; f6 ( (byte) x ) ; f7 ( (char) x) ; public static void main(String[] Demotion p = new Demotion()¡ p.testDouble() i
1* Output : double argument:
args)
{
92 Piensa en Java f1 (double ) f2 (float ) f3 ( long ) f4 (int ) f5 (short } f6 (byte } f7 (char )
* /// ,Aquí, los métodos admiten valores primitivos más pequeños. Si el argumento es de mayor anchura, entonces será necesario efectuar una conversión de estrechamiento mediante una proyección. Si no se hace esto, el compilador generará un mensaje de error.
Sobrecarga de los valores de retorno Resulta bastante habitual hacerse la pregunta: "¿Por qué sólo tener en cuenta los nombres de clase y las listas de argumentos de los métodos? ¿Por qué no distinguir entre los métodos basándonos en sus valores de retorno?" Por ejemplo, los siguientes dos métodos, que tienen el mismo nombre y los mismos argumentos, pueden distinguirse fácilmente: void f () ( ) int f () ( return 1;
)
Esto podría funcionar siempre y cuando el compilador pudiera detenninar inequívocamente el significado a partir del contexto, como por ejemplo en int x = f(). Sin embargo, el lenguaje nos permite invocar un método e ignorar el valor de retorno, que es una técnica que a menudo se denomina invocar un método por su efecto colateral, ya que no 110S preocupa el valor de retorno, sino que queremos que tengan lugar los restantes efectos de la invocación al método. Luego entonces, si invocamos el método de esta fonna: f () ;
¿Cómo podría Java detenninar qué método f( ) habría que invocar? ¿Y cómo podría determinarlo alguien que estuviera leyendo el código? Debido a este tipo de problemas, no podemos utilizar los tipos de los valores de retomo para distinguir los métodos sobrecargados.
Constructores predeterminados Como se ha mencionado anteriormente, un constructor predetenninado (también denominado "constructor sin argumentos" ) es aquel que no tie ne argumentos y que se uti liza para crear un "objeto predetenninado". Si creamos una clase que no tenga constructores, el compilador creará automáticamente un constructor predetenninado. Por ejemplo:
11 :
i n i t ializa t i o n / Defaul tCo n s tructo r. j a v a
c l ass Bird ()
public class Defau!tConstructo r pu blic stati c v o id main (Str i ng[ ] args l Bird b ~ new Bird () ; II Defau l t!
) / / / o-
La expresión new Bird ()
crea un nuevo objeto y llama al constructor predetemlinado, incluso aunque no se baya definido lino de manera explícita. Sin ese constructor predeternlinado no dispondríamos de ningún método al que invocar para construir el objeto. Sin embargo, si definimos algún constructor (con o sin argumentos), el compilador no sin/enlizará ningún constructor por nosotros:
11 :
initialization/ NoSynthesis.java
class Bird2 {
5 Inicialización y limpieza 93 Bü-d2 {int i ) {} 2ird2 1double d I
{}
!."'L:bl ':"c class NoSynthes i s { public s : atic void main (String [J al-gs ) { / / ! Bird 2 b = n ew Bird2 () ; / / No h ay p rede termi nado Bird2 b2 ne w Bird 2(1) ; Bird2 b3 '" r.e \,o¡ Bird2 ( 1. O) ; / I / ,-
Si c:,c ribimos: :..e.w Bi rd2 ()
el compilador nos indicara que no puede localizar ningílll constmctOr que se corresponda con la instrucción que hemos escr ito. Cuando no defi nimos explícitamente ningún cons tmctor. es como si el compi lador dijera: "Es necesario ut il izar algún constmcror. así que déjame definir uno por ti". Pero, si escribimos al menos constructor. el compilador dice: "Has escrito un constructor, así que tu sabrás 10 que estás haciendo: si 110 has incluido uno predeterminado es porque no quieres hacerlo".
Ejercicio 3:
(1) Cree ulla clase con un constructor predeterminado (uno que no tome ningún argumento) que imprima un mensaje. Cree UIl objeto de esa clase.
Ejerc ici o 4:
( 1) Aiiada un constructor sob recargado a l ejercic io anterior que admita un a rgumen to de tipo Strin g e
imprima la correspondiente cadena de cflracteres junto con el mensaje.
Ejercicio 5:
(2 ) Cree IIna clase denominada Dog con un método sobrecargado bark( ) (método ··ladrar"). Este método debe es tar sobrecargado basándose en diversos tipos de datos primi tivos y debe imprimir d iferel1les tipos de ladridos. gruñidos, etc .. dependiendo de la versión sobrecargada que se invoque. Escriba un método m aín ( ) que invoque todas las distintas versiones.
Ejercicio 6:
( 1) .\!lodilique e l ejercici o 3merior de modo que dos de tos métodos sobrecargados tengan dos argumentos (de dos tipos di stintos). pero en orden inverso LIno respecto del otro. Verifiquc que estas detíniciones funcionan.
Ejercicio 7:
(1) Crec una c lase ~ in ningún cünstruc(Qr y luego cree SI.?
un objeto de esa clase en main( ) para veriticnr que sintetiza automa¡ieamente el constructor predetcnninado.
La palabra clave this Si te nemo s dos objetos del mism o tip o llamados a y b. p0c! emo s preguntam os cómo es posible iIwocar un metodo pl'C'I( ) p,m-l ambos obje lOs (110fl.l: la palabra illgksa peel significa "pelar ulla fruta ". que en este ejemplo de programación es una banana ): ji :
initialization/Bana na Peel . java
class S a nana { vOld pee l l i nt i)
(! *
*1 )
public class BananaPe el { p u blic s:atic void main ( String [J
Ba nana a
al-gs )
{
new Banana() I b = ne w Banana() i a . peel tll i =
b . peel (2) ;
) I/! , -
Si sól o ha y un único método denominado peel( ), ¿cómo puede ese metoJo saber si está siendo llamado para el objcro a o para el objeto b?
94 Piensa en Java
Para poder escribir el código en una sintaxis cómoda orientada a objetos, en la que podamos "enviar un mensaje a un objeto" , el compilador se encarga de reali zar un cierto trabajo entre bastidores por nosotros. Existe un primer argumento secreto pasado al método peel(), y ese argumento es la referencia al objeto que se está manipulando. De este modo, las dos llamadas a métodos se convierten en algo parecido a: Banana.peel(a, 1) ¡ Banana.peel(b, 2) i
Esto se realiza internamente y nosotros no podemos escribir estas expresiones y hacer que el compilador las acepte, pero este ejemplo nos basta para hacernos una idea de lo que sucede en la práctica. Suponga que nos encontramos dentro de un método y queremos obtener la referencia al objeto aChlal. Puesto que el compilador pasa esa referencia secretamente, no existe ningún identificador para ella. Sin embargo, y para poder acceder a esa referencia, el lenguaje incluye una palabra clave específica: this. La palabra clave this, que sólo se puedc emplear en métodos que no sean de tipo static devuelve la referencia al objeto para el cual ha sido invocado el método. Podemos tratar esta referencia del mismo modo que cualquier otra referencia a un objeto. Recuerde que, si está invocando un método de la clase desde dentro de otro método de esa misma clase, no es necesario utilizar this, si no que simplemente basta con invocar el método. La referencia this actua l será utilizada automáticamente para el otro método. De este modo, podemos escribir: jj : initialization j Apricot.java public class Apricot {
void pick l) { / * ... * / void pit 11 { pick 11 ; / *
.. * / }
// / ,Dentro de pite ), podríamos decir Ihis.pick( ) pero no hay ninguna necesidad de hacerlo. ' El compilador se encarga de hacerlo automáticamente por nosotros. La palabra clave this sólo se usa en aque llos casos especiales en los que es necesario utilizar explícitamente la referencia al objeto aChla l. Por ejemplo, a menudo se usa en instrucciones return cuando se quiere devolver la referencia al objeto actual: jj : initialization/Leaf.java / / Uso simple de la palabra clave "this".
public class Leaf { int i = O¡ Leaf increment() i++¡ return this; void print () System.out.println("i
=
"
+
i) ¡
public static void main(String[] args) Leaf x = new Leaf()¡ x. increment () . increment () . increment () . print ()
i
j * Output: i
=
3
* ///,Puesto que increment() devuelve la referencia al objeto actual a través de la palabra clave this, pueden realizarse fácilmente l11ultiples operaciones con un mismo objeto. La palabra clave this también resulta úti l para pasar el objeto actual a otro método:
1 Algunas personas escriben obsesivamente this delante de cada llamada a método O referencia a un campo argumentando que eso hace que el código sea "mas claro y más explicito". Mi consejo es que no lo haga. Existe una razón por la que utilizamos los lenguajes de alto nivel, y esa razón es que estos lenguajes se encargan de hacer buena parte del trabajo por nosotros. Si incluimos la palabra clave this cuando no es necesario, las personas que lean el código se sentiran confundidas, ya que los demás programas que hayan leido en cualquier parte 110 wili=an las palabra clave Ihis de manera continua. Los programadores esperan que this sólo se use allí donde sea necesario. El seguir un estilo de codificación cohercllIe y simple pemlite ahorrar tiempo y dinero.
5 Inicialización y limpieza 95
in : tialization j Pass:'ngThis . java class Person { p'..lblic void eat (Apple apple) { Apple peeled = apple. get.Peeled () ; System. out. . prin':.ln ( "Yl.H~1my") ;
class ?eeler { static Apple peel \Apple apple l
/1 pelar retur n apple i
Pela da
/ /
c:ass P-.pple { .ll..pple getPeeled ()
{ return Peeler . peel ( this )
i
}
pub li c class PassingThis {
public static void ma in(String[] args) new Person {) . eat (ne w Apple () ) ;
/ * OU tput : Yummy
* /// :-
El objeto Apple (manzana) necesita invocar Peeler.peel( ), que es un metodo de utilidad ex temo que lleva a cabo una operación que. por alguna razón, necesita ser ex tema a Apple (quizá ese mélodo ex temo pueda ap lica rse a muchas clases distintas y no queremos repeti r el código). Para que el objeto pueda pasarse a si mismo al método extcmo. es necesari o emplear Ihis
Ejercicio 8:
(1) Cree ulla clase con dos melodos. Dent ro del primer método invoque a l segu ndo método dos veces: la primera vez sin util izar this y la segunda utilizando dic ha palabra c lave. Realic e este eje mp lo simpleme nte para ver cómo funciona el mecan ismo. no debe utilizar esta forma de invocar a los métodos en la práctica.
Invocación de constructores desde otros constructores Ulla clas~. existen ocasiones en las que co nviene im'oear a un construc tor desde dentro de otro para no tener que duplicar el código. Podemos cfcewar este tipo de ll amadas Uli li za ndú la pa labrtl cla,'c thi s.
CU:lmh) se escriben varios COllSlructor6 para
Normalmente. cuando escribi1l1o~ this. es en el sentido de "este objeto" o el "objeto m:tual", y esa palabra cla\-e genera, por si misma. la referencia al objeto actual. Dentro de UIl COllst rlH:tor. la palabra clave this toma un significado distinto cuando se la proporciona una li sta dt:' argumentos: realiza una ll amada explícita al const ructor que se co rrespo nda con esa li sta de argumentos. De este modo, di sponemos de una f0n113 se ncilla de in\'ocar a NroS constmctores:
1/ : in':'tializat.ion/E='lower . java.
JI Llamada a conSLructores con "chis" ':'mport static net.mindview.util.?rint.* public class FlOWe r { int petalCount : O; String s = " initial value"; Flower ( int petals ) { petalCount. = petals; Pl·int. ( " Construct.or wl int arg only, petaiCount: + petalCount ) i
96 Piensa en Java Flower (String ss ) { print ( "Constructor s :: ss;
w/ String arg only, s
"
+ ss ) ;
Flower (String s, int petals ) //1
this (petals ) ; this(s); JI
¡NO podemos realizar dos invocaciones!
this.s ~ Si ji Otro uso de "this" print (" String & int args 11) ;
) Flower ()
(
this ( "hi",
47 ) ;
print ( "default constructor (no args) " ) ; void printPetalCount ()
// ! this (11 ) ;
{
JI
¡NO dentro de un no-constructor! print ( lIpetalCount = " + petalCount + s = "+ s ) ;
public static void main (String{] args ) Flower x = new Flower () ; x.printPetalCount {) i
/ * Output: Constructor w/ int arg only, petalCount= 47 String & int args default constructor (no args) petalCount = 47 s = hi
*/ // ,El constructor Flower(String s, int petals) muestra que, aunque podemos invocar un constructor uti lizando this, no podemos invocar dos. Además, la llamada al constructor debe ser lo primero que hagamos, porque de lo contrario obtendremos un mensaje de error de compi lación. Este ejemplo también muestra otro modo de utilización de this. Puesto que el nombre del argu mento s y el nombre del miembro de datos s son iguales, existe una ambigüedad. Podemos resolverl a utilizando this.s, para dejar claro que estamos refiriéndonos al miembro de datos. Esta fanna de utilización resulta muy habitual en el código Java y se emplea en numerosos luga res del libro. En printPctalCount( ) podemos ver que el compilador no nos pennite invocar un constructor desde dentro de cualquier método que no sea un constructor.
Ejercicio 9:
(1) Cree una clase con dos constructores (sobrecargados). Utiliza ndo this, in voq ue el segundo constructor desde dentro del primero.
El significado de static Teniendo en mente el significado de la palabra clave this, podemos comprender mejor qué es lo que implica definir un método como static. Significa que no existirá ningún objeto this para ese método concreto. No se pueden invocar métodos no static desde dentro de los métodos static2 (aunque la in versa sí es posible), y se puede invocar un método static para la propia clase, sin especificar ningún objeto. De hecho, esa es la principal aplicación de los métodos static: es como si estuviéramos creando el equivalente de un método global. Sin embargo, los métodos globales no están pennitidos en Java, y el incluir el método static dentro de una clase pennite a los objetos de esa clase acceder a métodos static y a campos de tipo static. Algunas personas argumentan que los métodos estáticos no son orientados a objetos, ya que ti enen la semántica de un método global. Un método estático no envía un mensaje a un objeto, ya que no existe referencia this. Probablemente se trate de 2 El único caso en que esto puede hacerse es cuando se pasa al método stalic una referencia a un objeto (el método stalic también podría crear su propio objeto). Entonces, a travcs de la referencia (que ahora será. en la práctica, Ihis), se pueden invocar métodos no stlltiC y acccder a campos no static . Pero. norma lmente si quercmos hacer algo como esto, lo mejor es que escribamos un método no static nonnal y comente.
5 Inicialización y limpieza 97
arg,U1l10;;'1l10 correcto. y si So;;' encll~llIra al guna \t.~Z lIlili 7 entonces i se inicializará primero con el va lor O, y luego con el va lor 7. Esto es cierto para todos los tipos primitivos y también para las referencias a objetos, incluyendo aquellos a los que se inicialice de manera explíc ita en el punto en el que se los defina. Por esta razón, el compilador no trata de obligamos a inicializar los elementos dentro del constructor en ningún sitio concreto o antes de utilizarlos: la inicialización ya está garantizada.
Orden de inicialización Dentro de una clase, el orden de inicialización se detennina mediante el orden en que se definen las variables en la clase. Las defllliciones de variables pueden estar dispersas a través de y entre las definiciones de métodos, pero las variables se inicializan antes de que se pueda invocar cualquier método, incluso el constructor. Por ejemplo:
11 : initialization/OrderOfInitialization.java
II Ilustra el orden de inicialización. import static net.mindview.util.Print.*; II II
Cuando se invoca el constructor para crear un obje to Window, aparecerá el mensaje: class Window { Window(int marker) { print("Window(" + marker + tI ) " ) ;
class House Window wl House ()
new Window(l);
II
}
Antes del constructor
{
II Mostrar que estamos en el constructor: print ("House () " ) ; w3 = new Window(33); II Reinicializar w3 Window w2 = new Window(2); void f () { print ( " f () "); ) Window w3 = new Window(3) i
II
Después del constructor
II
Al final
public class OrderOfInitialization public static void main(String[] args) House h = new House() i h.f( ) i II Muestra que la construcción ha finalizado
1* Output: Window(l) Window (2) Window(3) House () Window(33) f ()
* //1 ,En l-louse, las definiciones de los objetos Window han sido dispersadas intencionadamente, para demostrar que todos ellos se inicializan antes de entrar en el constructor o de que suceda cualquier otra cosa. Además. \\'3 se reinicializa dentro del constructor. Examinando la sa lida, podemos ver que la referencia a w3 se inicializa dos veces. Una vez antes y otra durante la llamada al constructo r (el primer objeto será eliminado, por lo que podrá ser procesado por el depurador de memoria más adelante). Puede que esto no le parezca eficiente a primera vista, pero garantiza una inicialización adecuada: ¿qué sucedería si se definiera un constructor sobrecargado que no inicializara w3 y no hubiera una ü1icialización "predeterminada" para w3 en su definición?
106 Piensa en Java
Inicialización de datos estáticos Sólo existe una única área de a lmacenamiento para un dato de tipo sta tic, independientemente del número de objetos que se creen. No se puede aplicar la palabra clave statie a las va riables locales, así que sólo se aplica a los campos. Si un campo es una primiti va de tipo static y no se inicializa, obtendrá el valor inicial estándar correspondiente a su tipo. Si se trata de una referencia a un objeto, el valor predeterminado de inicia lización será null . Si desea colocar la iniciali zac ión en el punto de la defmición, será similar al caso de las variables no estáticas. Para ver cuándo se in iciali za e l almacenamiento de tipo stane, ha aquí un ejemplo: ji : initialization/Staticlnitialization .java // Especificación de valores iniciales en una definición de clase. import static net.mindview.util.Print.*;
class Bowl { Bowl (int marker) print {"Bowl (" + marker + " )") ; void fl (int marker) { print("fl(" + marker + ") " ) ;
class Table ( static Bowl bowll = new Bowl (l) ; Table () { print{"Table{) " ) ; bow12.fl(l) ;
void f2(int marker) print{"f2(" + marker + " ) " ); static Bowl bow12
= new
Bowl(2);
class Cupboard { Bowl bow13 = new Bowl(3); static Bowl bow14 = new Bowl (4) ; Cupboard () { print (11 Cupboard () " ) i bow14.fl(2) ;
void f3(int marker) print{"f3 (u + marker + ") sta tic Bowl bow15
= new
U);
Bowl(5);
public class Staticlnitialization { public static void main(String[] args) print("Creating new Cupboard() in main u ) ; new Cupboard () ; print ( " Creating new Cupboard () in main " ) ; new Cupboard () ; table. f2 (1) ; cupboard. f3 (1) ; static Table table = new Table(); static Cupboard cupboard = new Cupboard ();
5 Inicialización y limpieza 107
) / * Output, Bowl(l) Bowl (2) Table () f1 (1) Bowl(4) Bowl(S) Bowl(3) Cupboard () f1 (2) Creat ing new Cupboard() Bowl(3) Cupboard () f1 (2) creating new Cupboard() Bowl(3) Cupboard () f1 (2)
in main
in main
f2 (1)
f3 ( 1 )
* /// , 80\\'1 pennite visualizar la creación de una clase, mientras que Table y Cupboard tienen miembros de tipo static de 80\\'1 dispersos por sus co rrespondientes definiciones de clase. Observe que C upboard crea un objeto Bowl bowl3 no estáti co
antes de las definiciones de tipo static. Examinando la salida, podemos ver que la inicialización de static sólo tiene lugar en caso necesario. Si no se crea un objeto Table y nunca se hace referencia a Table.bowll o Table.bowI2, los objetos Bowl estáticos bowll y bowl2 nun ca se crearán. Sólo se inicializarán cuando se cree el primer objeto Table (cuando tenga lugar el primer acceso statie). Después de eso. los objetos static no se reinicializan.
El orden de inicialización es el siguiente: primero se inicializan los objetos estáticos, si es que no han sido ya ini cializados con una previa creación de objeto, y luego se inicializan los objetos no estáticos. Podemos ver que esto es así examinando la salida del programa. Para examinar main() (un método static), debe cargarse la clase Static\nitialization, después de lo
cual se inicializan sus campos estáticos tab le y cupboard, lo que hace que esas clases se carguen y, como ambas contienen objetos Bowl estáticos, eso bace que se cargue la clase Bowl . Por tanto, todas las clases de este programa concreto se cargan antes de que dé comienzo main(). Éste no es el caso usual , porque en los programas típicos no tendremos todo vinculado entre sí a través de valores estáticos, como sucede en este ejemplo. Para resumir el proceso de creación de un objeto, considere una clase Dog: 1. Aunque no utilice explícitamente la palabra clave static, el constructor es, en la práctica, un método sta tic. Por
tanto, la primera vez que se crea un objeto de tipo Dog, o la primera vez que se accede a un método estático o a un campo estáti co de la clase Dog, e l intérprete de Java debe localizar Dog.class, para lo cual analiza la ruta de clases que en ese momento haya definido (c1asspath). 2. A medida que se carga Dog.c1ass (creando un objeto Class, acerca del cual bablaremos posteriormente) se ejecu-
tan todos sus inicializadores de tipo static. De este modo, la inicialización de tipo static sólo tiene lugar una vez, cuando se carga por primera vez el objeto Class. 3. Cuando se crea un nuevo objeto con new Dog( ), el proceso de construcción del objeto Dog asigna primero el
suficiente espacio de almacenamiento para el objeto Dog en el cúmulo de memoria. 4. Este espacio de almacenamiento se rellena con ceros, lo que asigna automáticamente sus valores predeterminados a todas las primiti vas del objeto Dog (cero a los números y el equivalente para booloan y cbar); asimismo.
este proceso hace que las referencias queden con el valor null. 5. Se ejecutan las inicializaciones especificadas en el lugar en el que se definan los campos. 6. Se ejecutan los constructores. Como podremos ver en el Capítulo 7, Reutilización de las clases, esto puede implicar un gran número de actividades, especialmente cuando estén implicados los mecanismos de berencia.
108 Piensa en Java
Inicialización static explícita Java pemlile agrupar Olras inicializaciones estáticas dentro de una "cláus ul a" sta tic especial (en ocasiones denominada bloque estático) en una clase. El aspecto de esta cláusula es el siguiente: // : initialization/ Spoon. j ava public class Spoon { static int i; sta tic { i
:::: 47;
}
111 ,Parece ser un método. pero se trata sólo de la palabra clave sta tic seg uida de un bloque de código, Este código, al igual que otras inicializaciones estáticas sólo se ejecuta una vez: la primera vez que se crea un objclO de esa clase o la primera vez que se accede a un mjembro de tipo static de esa clase (incluso aunque nUllca se cree un objeto de dicha clase). Por ejemp lo:
/1 : initialization/ ExplicitStatic . java / / Inicialización static explícita con la cláusula "static". import static net.mindview.util.Print.*¡
class Cup { Cup (int marker ) print("Cup ( 1I + marker + 11 ) " ) ; void f (int marker) { print ( "f ( 1I + marker + 11) " ) ;
class Cups { static Cup cupl ¡ static Cup cup2 ¡ static { cupl new Cup ( l) ; new Cup ( 2 ) ; cup2 Cups()
print ( IICups () n);
public class ExplicitStatic { public static void main(String [] args) print {"Inside main() " ) ; Cups , cupl.f(99 ) ;
II
{
(1 )
}
II static cups cupsl II static cups cups2 1* Output:
new Cups ( ) ¡ new Cups () ;
II II
(2 ) (2 )
Inside main () Cup ( l) Cup(2) f (99)
*111,Los inicializadores static para Cups se ejecutan cuando tiene lugar el acceso del objeto estático cup] en la línea marcada con (1), o si se desactiva mediante un comentario la línea (1) y se quitan los comentarios que desactivan las líneas marcadas (2). Si se desactivan mediante comentarios tanto (1) como (2), la inicialización sta tic de Cups nunca tiene lugar. como
5 Inicialización y limpieza 109
puede verse a la salida. Asimismo, da igual si se eliminan las marcas de comentario que están desactivando a una y otra de las lineas marcadas (2) o si se eliminan las marcas de ambas líneas; la inicialización estática tiene lugar una sola vez. Ejercic io 13:
(l) Verifique las afinnaciones contenidas en el párrafo anterior.
Ejercic io 14:
( 1) Cree una clase con un campo estático Strin g que sea inicializado en el punto de definición, y otro campo que se inicialice mediante el bloque sta tic. Añada un método static que imprima ambos campos y demuestre que ambos se inicializan antes de usarlos.
Inicialización de instancias no estáticas Java proporciona una si ntaxis simi lar, denominada inicialización de instancia, para inicializar las variables estáticas de cada objeto. He aquí un ejemplo:
11 : initialization / Mugs.java II "Inicialización de instancia" en Java. import static net.mindview.util.Print.*; cl ass Mug { Mug ( int marker ) print { IIMug(1I + marker + 11 ) 11 ) ; void f ( int marker ) { print (11 f ( " + marker + " ) 11 ) ;
public class Mugs { Mug mug1; Mug mug2;
( mug1 = new Mug(l ) ¡ mug2 = new Mug ( 2 ) ¡ print (lImug1 & mug2 initialized" ) ¡
Mugs () print("Mugs () !I ) ; Mugs (int i ) print ( IIMugs (int ) 11 ) ; publi c s t a t i c vo i d main (Stri ng [ ] args l print ( " Inside main () It ) ; new Mugs () ; print ( IInew Mugs () c ompleted" ) i new Mugs (1 ) ¡ print ( IInew Mugs (1 ) c o mpleted" ) ¡
1* Output: Inside main ( ) Mug(l ) Mug(2) mug1 & mug2 initialized Mugs () new Mugs ( ) completed Mug ( l) Mug(2 ) mug1 & mug2 initialized Mugs ( int ) new Mugs ( l ) completed * // /,-
{
110 Piensa en Java
Podemos ver que la cláusula de inicializació n de instancia : mugl : new Mug {l ) ; mug2 = new Mug(2 ) ; print ( "mugl & mug2 initialized" ) ;
parece exac tamente como la cláusula de inicialización es tática, salvo porque falta la palabra clave statie. Esta sintaxis es necesaria para so portar la inicialización de clases internas anónimas (véase el Capítulo 10, Clases illlernas), pero también nos permite ga rantiza r que ciertas ope raciones tendrán lugar independientemente de qué co nstmctor ex plícito se invoque. Examinando la salida, podemos ver que la cláusula de inicia li zación de instancia se ejecuta antes de los dos constructores. Ejercicio 1 5:
( 1) Cree una clase con un campo String que se inicialice mediante una cláusula de inicialización de instancia.
Inicialización de matrices Una matri z es, simplemente, una secuencia de objetos o primitivas que son todos del mismo tipo y que se empaquetan juntos , utilizando un único nombre identificador. Las matrices se definen y usan mediante el operador de indexación I }. Para definir una referencia de una matri z, basta con incluir unos corchetes vacíos detrás del nombre del tipo: int[]
al;
También puede colocar los corchetes después del identificador para obtener exactamente el mismo resultado: int al [] ;
Esto concuerda con las expectati vas de los programadores de e y C++. Sin embargo, el primero de los dos estilos es una sintaxis más adecuada, ya que comunica mejor que el tipo que estamos defmiendo es una " matriz de variab les de tipo int". Es te estilo es el que emplearemos en el libro. El compilador no pernlite especificar el tamaño de la matriz. Esto nos retrotrae al problema de las "referencias" anteriormente comentado. Todo lo que tenemos en este punto es una referencia a una matriz (habiendo asignado el suficiente espacio de almacenamiento para esa referencia), sin que se haya asignado ningún espacio para el propio objeto matriz. Para crear espacio de almacenamiento para la matriz, es necesario escribir una expresión de inicialización. Para las matrices, la iniciali zación puede hacerse en cualquier lugar del código, pero también podemos utilizar una clase especial de expresión de inicialización que sólo puede emplearse en el punto donde se cree la matriz. Esta inicialización especial es un conjunto de va lores encerrados entre llaves. En este caso, el compilador se ocupa de la asignación de espacio (el equivalente de utilizar new) Por ejemplo: int 1] al :
( 1,
2,
3,
4,
5 );
Pero entonces, ¿por qué íbamos a definir una referencia a una matriz sin definir la propia matri z? int[l a2;
Bueno, la razón para definir una referencia sin definir la matri z asociada es que en Java es posible asignar una matri z a otra, por lo que podríamos escribir: a2
= al;
Lo que estamos haciendo con esto es, en realidad, copiar una referencia, como se ilustra a continuación:
JJ : initializationJArraysOfPrimitives .java import static net.mindview . ut il.Print.*¡ public class ArraysOfPrimitives { public static void main (St ring( ] argsl int [] al : ( 1, 2, 3, 4, 5 ); int [] a2; a2 = al i f or(int i O; i e a2.1ength; i++l
5 Inicialización y limpieza 111 a2[í] = a2[í] + 1; for {int i = O; i < al.length; i++) print (n al [" + i + "1 = " + al [i] )
i
} / , Output, al al al al al
[O] [1] [2] [3] [4]
2 3 4 5 6
, /// ,Como puede ver, a al se le da un valor de inicialización, pero a a2 no; a a2 se le asigna posterionnente un valor que en este caso es la referencia a otra matriz. Puesto que a2 y a 1 apuntan ambas a la misma matriz, los cambios que se realicen a través de a2 podrán verse en al . Todas las matrices tienen un miembro intrínseco (independientemente de si son matrices de objetos o matrices de primitivas) que puede consultarse (aunque no modificarse) para determinar cuántos miembros hay en la matriz. Este miembro es length. Puesto que las matrices en Java, al igual que en C y C++, comienzan a contar a partir del elemento cero, el elemento máximo que se puede indexar es length - 1. Si nos salimos de los límites, e y C++ lo aceptarán en silencio y pennitirán que bagamos lo que queramos en la memoria, lo cual es el origen de muchos errores graves. Sin embargo, Java nos protege de tales problemas provocando un error de tiempo de ejecución (una excepción) si nos salimos de los Iímites. 5 ¿Qué sucede si no sabemos cuántos elementos vamos a necesitar en la matriz en el momento de escribir el programa? Simplemente, bastará con utilizar new para crear los elementos de la matriz. Aquí, new funciona incluso aunque se esté creando una matriz de primitivas (sin embargo, new no pennite crear una primitiva simple que no fonne parte de una matri z): 11 : initiali z a ti on/ArrayNew . java 11 Crea ción de matrices con new. import java.util. *; import static net.mindview.util.Print.*; public class ArrayNew { public static void main{String[] args) int [] a; Random rand = new Random(47); a = new int [rand.nextlnt {20)] ; pri n t ( n length of a = 11 + a . length) ; print(Arrays.toString(a}} i / * Output : length of a = 18
[O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O]
, ///,El tamaño de la matriz se selecciona aleatoriamente utilizando el método Random.nextlnt(), que genera un valor entre cero y el que se le pase como argumento. Debido a la aleatoriedad, está claro que la creación de la matriz tiene luga r en tiempo de ejecución. Además, como la salida del programa muestra que los elementos de matriz de tipos primitivos se inicializan automáticamente con valores "vacíos" (para las variables numéricas y char, se inicializan con cero mientras que para variables boolean se inicializan con false). El método Arrays.toString( ), que fonna parte de la biblioteca estándar java.util, genera una versión imprimible de una matriz unidimensional. 5 Por supuesto, comprobar cada acceso de una matriz cuesta tiempo y código, y no hay manera de desactivar esas comprobaciones, lo que quiere decir que los accesos a matrices pueden ser una fuente de ineficiencia en los programas, si se producen en alguna sección critica. En aras de la seguridad en Últemct y de la productividad de los programadores, los diseñadores en Java pensaron que resultaba convenienle pagar este precio para evitar los errores asociados con las matrices. Aunque el programador pueda sentirse lenlado de escribir código para tTalar de hacer que Jos accesos a las matrices sean más eficientes, esto es una pérdida de tiempo, porque las optimizaciones automáticas en tiempo de compilación y en tiempo de ejecución se encargan de acelerar los accesos a las matrices.
112 Piensa en Java
Por supuesto, en este caso la matri z también podía haber sido definida e inicializada en la misma instrucción: int[]
a
=
new int[rand.nextlnt(20}];
Ésta es la forma preferible de hacerlo. siempre que se pueda. Si se crea una matriz que no es de tipo primitivo, lo que se crea es una matri z de referencias. Considere el tipo envoltorio Integer, que es una clase y no una primitiva:
JI : initialization/ArrayClassObj.java
JI Creación de una matriz de objetos no primitivos. import java.util.*; import static net . mindview.util.Print .* ¡ public class ArrayClassObj { public static void main(String(] args ) Random rand = new Random(47); Integer[] a = new Integer[rand.next lnt ( 20 }) i print ( " length of a = " + a .length) ; for(inc i = O; i < a . length¡ i++} a[i] = rand.nextlnt(500) i II Conversión automática print(Arrays . toString(al 1;
1* Output: (Sample ) length of a = 18 [55,193,36 1 ,461,429,368,200,22,207,288,128,51,89,
309,
278,
498,
361,
20]
* /// ,Aquí, incluso después de in vocar new para crear la matriz: Integer[]
a
= new
Integer(rand.nextInt(201]
i
es sólo una matriz de referencias y la inicialización no se completa basta que se inicialice la propia referencia creando un nuevo objeto Integer (mediante el mecanismo de conversión automática, en este caso): a[i]
=
rand . nextInt(500)
i
Sin embargo, si nos olvidamos de crear un objeto, obtendremos una excepción en ti empo de ejecución cuando tratemos de utilizar esa posición vacía de la matriz. También es posible inicializar matrices de objetos mediante una lista encerrada entre llaves. He aquí dos formas de hacerlo:
11 : initialization/ArrayInit.java II Inicialización de la matriz. import java.util. *; public class ArrayInit { public static void main(String[]
Integer []
a
=
args)
{
(
new Integer (1) , new Integer(2) , 3, II Conversión automática
}; Integer[] b = new Integer[] { new Integer (1) , new Integer (2) , 3, II Conversión automática
}; System.out.println(Arrays.toString(a» System.out.println(Arrays . toString(b»
1* Output: 11, [1,
2, 2,
*///,-
3] 3]
i
;
5 Inicialización y limpieza 113
En ambos casos, la coma final de la lista de inicializadores es opcional (esta característica pennite un manteni miento más fácil de las listas de gran tamaño). Aunque la primera forma es útil, es más limitada, porque sólo puede emplearse en el punto donde se deflne la matri z. podemos uti lizar las fonnas segunda y tercera en cualquier lugar, incluso dentro de una llamada a un método. Por ejemplo, podríamos crear una matriz de objetos String para pasa rl a a otro método main(), con el fin de proporcionar argumentos de línea de comandos altemativos a ese método main() :
1/ : initialization/DynamicArray.java 1/ Inicialización de la matriz . public class DynamicArray { public static void main (String [] args ) { Other .main (new String[] { "fiddle", "de",
class Other { public static void main (Str ing [) for (String s : args) System.out.print(s + " ") i
args)
Itdum"
})
i
{
1* Output: fiddle de dum *j jj ,La matriz creada para el argumento de Other.main() se crea en el punto correspondiente a la llamada al método, así que podemos incluso proporcionar argumentos alternativos en el momento de la llamada. Ejercicio 16:
( 1) Cree una matri z de objetos String y asigne un objeto String a cada elemento. Imprima la matriz utilizando un bucle for .
Ejercicio 17:
(2) Cree una clase con un constructor que tome un argumento String. Durante la construcción, imprima el argumento. Cree una matriz de refe rencias a objetos de esta clase, pero sin crear ningún obj eto para asignarl o a la matriz. Cuando ejecute el programa, obse rve si se imprimen los mensajes de iniciali zación correspondi entes a las llamadas al constructor.
Ejercicio 18:
( 1) Co mplete el ejercicio anterior creando objetos que asociar a la matriz de referencias.
Listas variables de argumentos La segunda forma proporciona una sin taxis cómoda para crear e invocar métodos que pueden producir un efecto sim ilar a las lisras variables de arglllne11los de C (conocidas con el nombre de "varargs" en C). Esto puede incluir un número desconocido de argumentos que a su vez pueden ser de tipos desconocidos. Puesto que todas las clases se heredan en última instanc ia de la clase raíz común Object (tema del que hablaremos más adelante en el libro), podemos crear un método que adm ite una matriz de Object e invocarlo del sigui ente modo:
/1: initialization/VarArgs.java II USO de la sintaxis de matriz para crear listas variables de argumentos . class A {} public class VarArgs { static void printArray(Object[) args) for(Object obj : args) System . out.print(obj + " tl) i System.out.println() i public static void main(String(] printArray(new Object[] {
args)
{
114 Piensa en Java new Integer ( 47 ) , new Float(3.14 ) ,
})
,
new Double (11.11 )
printArray (new Object[] {"ane", "two ll , "three" } ) i (new A () , new A () , new A () } ) ;
printArray ( new Object[]
/ * Output: ( Sample ) 47 3.14 11.11 ene
tWQ
three
A@la46e3 0 A@3e25a5 A@19821f
* /// ,Podernos ver que print() admi te una matriz de tipo Object, y recorre la matri z ut ilizando la sintaxisforeach imprimiendo cada objeto. Las clases de la biblioteca estándar de Java generan una salida más comprensible, pero los objetos de las cIases que hemos creado aquí imprimen el nombre de la clase, seguido de un signo de '@' y de un a serie de dígitos hexadecimales. Por tanto, el comportamiento predeternlinado (si no se define un método toString() para la clase, como veremos posterionn ente en el libro) consiste en imprimir el nombre de la clase y la dirección del objeto. Es posible que se encuentre con códi go anterior a Java SES escrito como el anteri or para generar li stas variables de argumentos. Sin embargo, en Java SE5, esta característica largo tiempo demandada ha sido finalmente añadida, por lo que ahora podemos emplear puntos suspensivos para definir una lista variable de argumentos, como puede ver en pr intArray() :
11:
initializationjNewVarArgs.java
I I USO de la sintaxis de matrices para crear listas variables de argumentos. public class NewVarArgs { static void printArray(Object. args) for (Object obj : args) System.out.print (obj + " " ) ; System.out.println( ) ;
{
public static void main (String(] args ) { II Admite elementos individuales: printArray(new Integer (47 ) I new Float (3.14) I new Double(11.11 )) ; printArray (47, 3.14F, 11.11 ) ; printArray ( "one", "two", "three" ) ; printArray (new A( ) , new A(), new A ()) ¡ II O una matriz: printArray l IObject [] ) new Integer[l{ 1 , 2 , 3 , 4 } ) , printArray () ¡ jI Se admite una lista vacía
1* Output: ( 75% ma tch ) 47 3.14 11.11 47 3.14 11.11 one two three A@1babSOa A@c3c749 A@150bd4d 1 2 3 4
* /// , Con varargs, ya no es necesari o escrib ir explícitamente la sintaxis de la matriz: el compi lador se encargará de comp letarla automática mente cuando especifiquemos varargs. Seguimos obteniendo una matriz, lo cual es la razón de que pr int() siga pudiendo uti lizar la sintaxisforeach para iterar a través de la matriz. Sin embargo, se trata de al go más que una simple conversión aut omática entre una lista de elementos y una matriz. Observe la penúltima línea del programa, en la que una matriz de elementos Integer (creados con la característi ca de conversión automática) se proyecta sobre una matriz O bj ect (para evitar que el compi lador genere una advertencia) y se pasa a printA rray(). Obviamente, el compilador determ ina que esto es ya una matri z, por lo que no realiza nin guna conversión con ella. De modo que, si tenemos un grupo de elementos, podemos pasarlos como un a lista, y si ya tenemos una matriz, se aceptará esa matriz C0l110 lista variable de argument os. La última línea del programa muestra que es posible pasar cero argumentos a una lista varmg. Esto resulta úti l cuando ex isten argumentos fina les opcionales:
5 Inicialización y limpieza 115
JI : initialization/OptionalTrailingArguments.java public class OptionalTrailingArguments { static void f (int required, String ... trailing ) System.out,print( ll r equired:
ti
+ required +
I!
n);
for(String s : trailing ) System . out .print(s + " " ); System.out.println() ; public static void main(String[] f (l ,
args)
{
"one") i
f (2, "two U f (O) ;
,
"three" );
/ * Output : requ ired: 1 ane requ ired : 2 two three requi red: o */// , Esto muestra también cómo se pueden utili zar varargs con un tipo especificado distinto de Object. Aquí, todos los varargs deben ser objetos String. Se puede utilizar cualquier tipo de argumentos en las listas varargs, incluyendo tipos primitivos. El siguiente ejemplo también muestra que la lista vararg se transforma en una matri z y que, si no hay nada en la lista, se tratará como una matri z de tamaño cero. JJ : initiaIizationJVarargType . java public cIass VarargType { static void f (Character ... args ) { System . out.print{args .getClass()) i System.out.println( " length " + args . length ); static void 9 (int ... args ) { System.out.print{args .getClass()) i System . out . println ( " length " + args.Iength ) ¡ public static void main (Stri ng []
args)
{
f ( 'a' ) ;
fl); g (1); g() ;
System.out.println("int[) : " + new int(O) .getClass{))¡
J* Output: class (Lj ava. lang . Character i Iength 1 class [Lj ava . Iang. Character i length O class [I Iength 1 class [I Iength O int [): class [I
* ///, El método getClass( ) es parte de Objeet, y lo ana lizaremos en detalle en el Capítulo 14, Información de lipos. Devuelve la clase de un objeto, y cuando se imprime esa clase, se ve una representación del tipo de la clase en forma de cadena de caracteres codificada. El carácter inicial ' 1' indica que se trata de una matriz del tipo situado a continuación. La ' 1' indica una primiti va ¡nt; para comprobarlo, hemos creado una matri z de iut en la última línea y hemos impreso su tipo. Esto permi te comprobar que la utili zación de varargs no depende de la característica de conversión automática, sino que utiliza en la práctica los tipos primitivos. Sin embargo, las listas vararg funcionan perfectamente con la característica de conversión automática. Por ejemplo :
116 Pien sa en Java
1/: initialization/AutoboxingVarargs.java public class AutoboxingVarargs { public static void f(Integer . . for(Integer i : args) System.out.print{i + " U);
args)
{
System.out.println{) ; public static void main(String[] args) f(new Integer(l), new Integer(2)); f(4, 5, 6, 7, 8, 9); f(lO, new Integer{ll)
I
12);
/* Output: 1 2 456789 10 11 12
* /// , Observe que se pueden mezclar los tipos en una misma lista de argumentos, y que la característica de conversión automática promociona selecti va mente los argumentos ¡nt a Integer. Las listas vararg compli can el proceso de sobreca rga, aunque éste parezca sufici entemente seguro a primera vista: // : initialization/OverloadingVarargs.java public class OverloadingVarargs { statie void f(Charaeter . .. args) System.out.print("first") ; for(Character e : args) System.out.print(" + e) i System .out.println () ; 11
statie void f(Integer.. args) System.out.print{"second") ; for(Integer i : args) System.out.print(II 11 + i); System.out.println() ; static void f{Long ... args) System.out.println{IIthird"l; publie statie void main (String [J args) f ( ' a' 'b' , 'e' l ;
{
I
f f f f
(1) ;
(2,
1);
(O) ; (aL) ;
II! f{)
i
11 No se compilará -- ambigüo
1* Output : first a b e seeond 1 seeond 2 1 seeond O third * /// ,En cada caso, el compil ado r está utili zando la caracterí stica de co nversión auto mática para determinar qué método sobrecargado hay que utili zar, e invocará el método que se ajuste de la fonna más específica.
5 Inicialización y limpieza 117 Pero cuando se invoca f() sin argumentos, el compilador no tiene fanna de saber qué método debe llamar. Aunque este error es comprensible, probablemente sorprenda al programador de programas cliente. podemos tratar de resolver el problema añadiendo un argumento no vararg a uno de los métodos:
JI : initialization/OverloadingVarargs2.java / / {CompileTimeError} (Won' t compile) public class OverloadingVarargs2 ( static void f(float i, Character ... args) System. out. printIn ( n f irst" ) i
{
static void f(Character ... argsl System.out.print("second" ) ; public static void main (String [] argsl
{
f (1, 'a'); f ('a ', 'b') i
}
/// ,El marcador de comentario {CompileTimeError} excluye este archivo del proceso de constmcción Ant del libro. Si lo compila a mano podrá ver el mensaje de error:
relerenee /0 I is ambiguous, bo/h me/hod l(floG/Java.lang. Charae/"" .. ) in Overloading Varargs2 and 111et!Jod fOava./ang.Characte¡: ..) in Ove¡·/oadingVarargs2 match Si proporciona a ambos métodos un argumento no-varO/'g, funcionará perfectamente:
11 :
initialization/OverloadingVarargs3.java
public class OverloadingVarargs3 { static void f(float i, Character ... args) System.out.println(Ufirst U) ; static void f (c har e, Character ... args) System.out.println(Usecond U) ; public static void main(String[]
args)
{
{
{
f (1, la t) ; f (t al, Ib l ) i
1* Output: first second ' /// ,Generalmente, sólo debe utilizarse una lista variable de argumentos en una única versión de un método sobrecargado. O bien, considere el no utilizar la lista variable de argumentos en absoluto. Ejercicio 19:
(2) Escriba un método que admita una matriz vararg de tipo Str ing. Verifique que puede pasar una lista separada por comas de objetos Stri ng o una matriz String[] a este método.
Ejercicio 20 :
(1) Cree un método main() que utilice varargs en lugar de la sintaxis main() normal. Imprima todos los elementos de la matriz args resultante. Pruebe el método con diversos conjuntos de argumentos de línea de comandos.
Tipos enumerados Una adición aparentemente poco importante en Java SES es la palabra clave enUDl, que nos facilita mucho las cosas cuando necesitamos agrupar y utilizar un conjunto de tipos enumerados. En el pasado, nos veíamos forzados a crear un conjun-
118 Piensa en Java
to de valores enteros constantes, pero estos conjuntos de valores no suelen casar muy bien con los conjuntos que se necesitan definir y son, por tanto, más arriesgados y dificiles de utilizar. Los tipos enumerados representan una necesidad tan común que C. e++ y diversos otros lenguajes siempre los han tenido. Antes de Java SES, los programadores de Java estaban obligados a conocer muchos detalles y a tene r mucho cuidado si querían emular apropiadamente el efecto de eoum . Ahora. Java dispone también de enum , y lo ha implementado de una manera mucho más completa que la que podemos encontrar en C/C++. He aquí un ejemplo simple :
11 : initialization/ Spiciness.java public enum Spiciness { NOT, MILO, MEDIUM, HOT, FLAMING } /1/ ,Esto crea un tipo enumerado denominado Spiciness con cinco valores nominados. Puesto que las instancias de los tipos enumerados son constantes, se suelen escribir en mayúsculas por convenio (si hay múltiples palabras en un nombre, se separan mediante guiones bajos). Para utilizar un tipo enum, creamos una referencia de ese tipo y la as ignamos una instancia:
11 : initialization/ SimpleEnumUse.java public class SimpleEnumUse { public static void main{String[] args ) Spiciness howHot = Spiciness.MEDIUM¡ System.out.println{howHot) i 1* Output: MEDIUM * /// ,El compilador añade automáticamente una seri e de características útiles cuando creamos un tipo eoum . Por ejemplo, crea un método toString() para que podamos visuali zar fácilmente el nombre de una instancia enum, y ésa es precisamente la forma en que la instrucción de impresión anterior nos ha penni tido generar la salida del programa. El compilador también crea un método ordinal( ) para indicar el orden de declaración de una constante enum concreta, y un método static values( ) que genera una matriz de valores con las constantes enum en el orden en que fueron declaradas:
11 : initialization/EnumOrder.java public class EnumOrder { public sta tic void main (S tring [] args ) { for {Spiciness s : Spiciness.values{)) System.out.println(s + ", ordinal 11 + s.ordinal()); 1* Output: NOT, ordinal O MILD, ordinal 1 MEDIUM, ordinal 2 HOT, ordinal 3 FLAMING, ordinal 4 * /// ,Aunque los tipos enumerados enum parecen ser un nuevo tipo de datos, esta palabra clave sólo provoca que el compilador realice una serie de actividades mientras genera una clase para el tipo enum, por lo que un enum puede tratarse en muchos sentidos como si fuera una clase de cualquier otro tipo. De hecho, los tipos enum son clases y tienen sus propios métodos. Una característica especialmente atractiva es la forma en que pueden usarse los tipos enum dentro de las instrucciones switch :
11 : initialization/Burrito.java public class Burrito { Spiciness degree¡ public Burrito (Spiciness degree ) { this. degree public void describe () { System. out. print ( "This burrito is " ) ¡
degree; }
5 Inicialización y limpieza 119 switch(degree) case NOT:
System.out.println(lInot spicy at all . tI) break¡
i
case MILD: case MEDIUM: System.out.println(lt a little hot."); break; case HOT: cas e FLAMING: defaul t: System. out. println ( tlmaybe too hot. 11 ) ;
public static void main(String[] argsl
Burrito plain = new Burrito (Sp iciness.NOT ). greenChile = new Burrito (S piciness.MEDIUM ) , jalapeno = new Burrito (Spiciness.HOT ) ;
plain . describe () ; greenChile.describe() ; jalapeno.desc ribe () ;
/ * Output: This burrito is not spicy at all. This burrito is a little hoto This bur rito is maybe too hot. */// ,Puesto que una instrucción switch se emplea para seleccionar dentro de un conjunto limitado de posibilidades, se complementa perfectamente con un tipo en um. Observe cómo los nombres enum indican de una manera mucho más clara qué es lo que pretende hacer el programa. En general, podemos utilizar un tipo enum como si fuera otra fOn1la de crear un tipo de datos, y limitamos luego a utilizar los resultados. En realidad, eso es lo importante, que no es necesario prestar demasiada atención a su uso, porque resulta bastante simple. Antes de la introducción de enum en Java SE5, era necesario realizar un gran esfuerzo para construir un tipo enumerado equivalente que se pudiera emplear de fOn1la segura. Este breve análisis es suficiente para poder comprender y utilizar los tipos enumerados básicos, pero examinaremos estos tipos enumerados más profundamente en el Capítulo 19, Tipos enumerados. Ejercicio 21 :
( 1) Cree un tipo enum con los seis lipos de billetes de euro de menor valor. Recorra en bucle los valores utilizando values() e imprima cada va lor y su orden correspondiente con ordinal().
Ejercicio 22 :
(2) Esc riba una instrucción switch para el tipo enum del ejercicio anterior. En cada case, imprima una descripci ón de ese billete concreto.
Resumen Este aparentemente elaborado mecanismo de inicialización, el constructor, nos indica la importancia crítica que las tareas de inicialización tienen dentro del lenguaje. Cuando Sjame Stroustrup, el inventor de C++, estaba diseñando ese lenguaje, una de las primeras cosas en las que se fijó al analizar la productividad en C fue que la inicialización inadecuada de las variables es responsable de una parte significativa de los problemas de programación. Este tipo de errores son dificiles de localizar, y lo mismo cabría decir de las tareas de limpieza inapropiadas. Puesto que los constructores nos pem'¡ten garantizar una inicialización y limpieza adecuadas (el compilador no pennitirá crear un objeto sin las apropiadas llamadas a un constructor), la seguridad y el control están garantizados. En C++. la destrucción también es muy importante, porque los objetos creados con new deben ser destruidos explícitamente. En Java, el depurador de memoria libera automáticamente la memoria de los objetos que no son necesarios, por lo que el método de limpieza equivalente en Java no es necesario en muchas ocasiones (pero cuando lo es, es preciso implementarlo explícitamente). En aquellos casos donde no se necesite un comportamiento simi lar al de los destructores, el mecanismo de depuración de memoria de Ja va simplifica enOn1lemente la programación y mejora también en gran medida la
120 Piensa en Java
seguridad de la gestión de memoria. Algunos depuradores de memoria pueden incluso limpiar otros recursos, como los recursos gráficos y los desc riptores de archivos. Sin embargo, el depurador de memori a hace que se incremente el coste de ejecución, resultando dificil evaluar adecuadamente ese coste, debido a la lentitud que históricamente han tenido los intérpretes de Ja va. Aunque a lo largo del tiempo se ha mejorado significativamente la velocidad de Java, un problema de la velocidad ha supuesto un obstáculo a la hora de adoptar este lenguaje en ciertos tipos de problemas de programación. Debido a que está garantizado que todos los objetos se construyan, los constructores son más complejos de lo que aquí hemos mencionado . En particular, cuando se crean nuevas clases utilizando los mecanismos de composición o de herencia, lambién se mantiene la garantía de constnlcción, siendo necesaria una cierta sintaxis adicional para soportar este mecanismo. Hablaremos de la composición, de la herencia y del efecto que ambos mecanismos tienen en los constnlctores en próximos capítulos. Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tlle Thinking in Java AnnOfafed Sofllfion Guide. que esta disponible para la venta en Inm:MindView.llel.
Control de acceso
El control de acceso (u ocultación de la implementación) trata acerca de "que no salgan las cosas a la primera". Todos los buenos escritores, incluyendo aquellos que escriben software, saben que un cierto trabajo no está tem1inado hasta después de haber sido reescrito. a menudo muchas veces. Si dejamos un fragmento de código encima de la mesa durante un tiempo y luego volvemos a él, lo más probable es que veamos una [anna mucho mejor de escribirlo. Ésta es una de las principales motivaciones para el trabajo de rediseño, que consiste en reescribir código que ya funciona con el fin de hacerlo más legible, comprensible y, por tanto, mantenible.' Sin embargo, existe una cierta tensión en este deseo de modificar y mejorar el código. A menudo, existen consum idores (programadores de cliente) que dependen de que ciertos aspectos de nuestro código cont inúen siendo iguales. Por tanto. nosotros queremos modificar el código, pero ellos quieren que siga siendo igual. Es por eso que una de las principales consideraciones en el diseño orientado a objetos es la de "separar las cosas que cambian de las cosas que pennanecen"'. Esto es particulannente importante para las bibliotecas. Los consumidores de una biblioteca deben poder confiar en el elemento que están utilizando, y saber que no necesitarán reescribir el código si se publica una nueva vers ión de la biblioteca. Por otro lado, el creador de la biblioteca debe tener la libertad de realizar modificaciones y mejoras, con la confianza de que el código del cliente no se verá afectado por esos cambios. Estos objetivos pueden conseguirse adoptando el convenio adecuado. Por ejemplo, el programador de la biblioteca debe aceptar no eliminar los métodos existentes a la hora de modificar una clase de la biblioteca, ya que eso haría que dejara de funcionar el código del programador de clientes. Sin embargo, la sintación inversa es un poco más compleja de resolver. En el caso de un campo, ¿cómo puede saber el creador de la biblioteca a qué campos han accedido los programadores de clientes? Lo mismo cabe decir de los métodos que sólo forman parte de la implementación de una clase y que no están para ser usados directamente por el programador de clientes. ¿Qué pasa si el creador de la biblioteca quiere deshacerse de una implementación anterior y sustituirla por una nueva? Si se modifica alguno de esos miembros, podría dejar de funcionar el código de algún programa cliente. Por tanto, el creador de la biblioteca tiene las manos atadas y no puede modificar nada. Para resolver este problema. Java proporciona especificadores de aCceso que penniten al creador de la biblioteca decir qué cosas están disponibles para el programa cliente y qué cosas no lo están. Los niveles de control de acceso, ordenados de mayor a menor acceso, son public, protected, acceso de paquete (que no ti enen una palabra clave asociada) y private. Leyendo el párrafo anterior, podríamos pensar que, como diseñadores de bibliotecas, conviene mantener todas las cosas 10 más "privadas" posible y exponer sólo aquellos métodos que queramos que el programa cliente utilice. Esto es CIerto, aunque a menudo resulta an tinatural para aquellas personas acostumbradas a programar en otros lenguajes (especialmente C) y que están acostumbradas a acceder a todo sin ninguna restricción. Cuando lleguen al final del capítulo, estas personas estarán convencidas de la utilidad de los controles de acceso en Java. Sin embargo, el concepto de biblioteca de componentes y el control acerca de quién puede acceder a los componentes de esa biblioteca no es completo. Sigue quedando pendiente la cuestión de cómo empaquetar los componentes para [onnar 1 Consulte RelaclOril/g: Improlling ,he Design 01 Exisling Code , de Martin Fowlcr, el al. (Addison·Wesley, 1999). Ocasionalmente. algunas personas argu· mentarán en contra de las lareas de rediseño, sugiriendo que tUl código que ya funciona es perfectamente adecuado, por lo que resulta una pérdida dc ticm· po tratar de rediseñarlo. El problema con esta fomla de pensar es que la parte del león en lo que se refiere al tiempo y al dinero consumidos por un proyecto no está en la escritura inicial del código. sino en su mantenimiento. Hacer el código más fácil de entender pernlite ahorrar una gran cantidad de dinero.
122 Piensa en Java una unidad de biblioteca cohesionada. Este aspecto se controla mediante la palabra clave package en Ja va. y los especificadores de acceso se verán afectados por el hecho de que una clase se encuentra en el mismo paquete o en otro paquete di stinto. Por tanto, para comenzar este capítulo, veamos primero cómo se incluyen componentes de biblioteca en los paquetes. Con eso, seremos capaces de entender completamente el significado de los especificadores de acceso.
package: la unidad de biblioteca Un paquete contiene un grupo de clases, organizadas conjuntamente dentro de un mismo espacio de nombres. Por ejemplo, existe una biblioteca de utilidad que fomla parte de la di stribución estándar de Java, organizada bajo el espacio de nombres java.util. Una de las clases de java.util se denomina ArrayList. Una fonua de utilizar un objeto Arr.yList consiste en especi ficar elllombre completo java.util.ArrayList.
JI : access / FullQualification.java public class FullQualification { public static void main (String[]
java.util.ArrayList list
args ) {
= new java.util.ArrayList( ) ;
)
/1 / ,Sin embargo, este procedimiento se vuel ve rápidamente tedioso, por lo que suele se r más cómodo utilizar en su lugar la palabra clave import. Si queremos importar una única clase, podemos indicar esa clase en la instrucción import:
JI : access / Singlelmport.java import java.util.ArrayList¡ public c!ass Singlelmport { public static void main (String [] args)
ArrayList list
=
{
new java.util .ArrayList ( ) i
)
111 ,Ahora podemos usar Array List sin ningún cualificador. Sin embargo, no tendremos a nuestra disposición ninguna de las otras clases de java.util. Para importar todas, basta con utili zar '*' tal como hemos visto en los ejemplos del libro. import java.util.*;
La razón para efectuar estas importaciones es proporcionar un mecanismo para gestionar los espacios de nombres. Los nombres de todos los miembros de las clases están aislados de las clases restantes. Un método f( ) de la clase A no coincidirá con un método f() que tenga la misma signatura en la clase B. ¿Pero qué sucede con los nombres de las clases? Suponga que creamos una clase Stack en una máquina que ya disponga de otra clase Stack escrita por alguna otra persona. Esta posibilidad de colisión de los nombres es la que hace que sea tan importante disponer de un control completo de los espacios de nombres en Java, para poder crear una combinación de identificadores unívoca para cada clase. La mayoría de los ejemplos que hemos visto hasta ahora en el libro se almacenaban en un único archivo y habían sido di senados para uso local, por lo que no nos hemos preocupado de los nombres de paquete. Lo cierto es que estos ejemplos sí estaban incluidos en paquetes: e l paquete predeterminado o " innominado". Ciertamente, ésta es una opción viab le y trataremos de utilizarla siempre que sea posible en el resto del libro, en aras de la simplicidad. Sin embargo, si lo que pretendemos es crear bibliotecas o programas que puedan cooperar con otros programas Java que estén en la misma máq uina, deberemos tener en cuenta que hay que evitar las posibles colisiones entre nombres de clases. Cuando se crea un archivo de código fuente para Java, normalmente se le denomina unidad de compilación (y también , en ocasiones, unidad de traducción). Cada lmidad de compilac ión debe tener un nombre que termine en .java, y dentro de la unidad de compilación puede haber UDa clase public que debe tener el mi smo nombre del archivo (i ncluyendo el uso de mayúsculas y minúsc ulas, pero excluyendo la extensión .java de l nombre del archivo). Sólo puede haber una clase public en cada unidad de compi lación; en caso contrario, el compilador se quejará. Si existen clases adicio nales en esa unidad de compilación, estarán ocultas para el mundo exterio r al paquete, porque no son public, y simplemente se tratará de clases "soporte" para la clase public principal.
6 Control de acceso 123
Organización del código Cuando se compila un archivo .j ava. se obtiene un archivo de salida para cada clase del archivo .java. Cada archivo de salida tiene el nombre de una de las clases del archi vo .java, pero con la extensión .class. De este modo, podemos llegar a obtener un gran número de archivos .class a partir de un número pequeño de archivos .java . Si el lector ha programado anteriom1ente en algún lenguaje compilado, esta rá acostumbrado al hecho de que el compilador genere a lgún fonnato intennedio (nannalmente un archi vo "obj") que luego se empaqueta con otros del mismo tipo utilizando un montador (para crear un arch ivo ejec utable) o un gestor de biblioteca (para crear una biblioteca). És ta no es la fonna de funciona r de Java. Un programa funcional es un conju nto de archivos .class, que se puede empaquetar y comprimir en un arch ivo JAR (Java ARrchi ve), utilizando el arch ivador jar de Java. El intérpre te de Java es responsable de localizar, ca rgar e interpretar2 estos archivos. Una biblioteca es un gmpo de estos archivos de clase. Cada archi vo fuente suele tener una clase public y un número arbitrario de c lases no públicas, por lo que no sólo existe un componente public para cada archivo fuente. Si queremos especificar que todos estos co mponentes (cada uno con sus propios arc hi vos .java y .c1ass separados) deben agruparse, podemos utilizar la palabra c lave package. Si usamos una instmcción package, debe aparecer como la primera línea no de comentario en el archivo. Cuando escribimos: package access;
estamos indicando qu e esta unidad de compilación es parte de una biblioteca denominada access. Dicho de otro modo, estamos especificando que el nombre de clase pública situado dentro de esta unidad de compilación debe integrarse bajo el "paraguas" correspondient e al nombre access, de modo que cualquiera que quiera usa r ese nombre deberá especi fi carlo por completo o utili za r la palabra clave import en combinación con access, utili za ndo las opciones que ya hemos mencionado anteriormente (observe que e l co nvenio que se empl ea para los nombres de paquetes Java consiste en emplear letras minúscu las, incl uso para las palabras intermedias). Por ejemp lo, suponga que e l nombre de un archi vo es MyClass.java . Esto quiere decir que sólo puede haber una clase public en dicho archivo y que el nombre de esa clase debe ser MyClass (respetando e l uso de mayúscu las y minúsculas):
11 : access/mypackage/MyClass.java package access.mypackage; public class MyClass {
//
} /// ,Ahora. si alguien quie re utilizar MyClass o cualquiera olra de las clases públicas de access, deberá emplear la palabra clave import para que estén disponibles esos nombres defi nidos en el paquete access. La alternativa consiste en especificar el nombre completamente cualificado:
11:
access/QualifiedMyClass.java
public class QualifiedMyClass { public static void main(String[] args) access.mypackage.MyClass m = new access.mypackage.MyClass();
La palabra clave import permi te que este ejemplo tenga un aspecto mucho más simple:
11 :
access/lmportedMyClass.java import access.mypackage.*¡
2 No hay ninguna característica de Java que nos obligue a utilizar un interprete. Existen compiladores Java de código nativo que generan un único archi* vo ejecutable.
124 Piensa en Java public class ImportedMyClass { public static void main (String[] args ) { MyClass m = new MyClass () ;
}
/// , Merece la pena tener presente que lo que las palabras clave package e import nos penniten hacer. como diseiiadores de bibliotecas, es dividir el espacio de nombres global único, para que los nombres no colisionen, independientemente de cuántas personas se conecten a Internet y comiencen a escribir clases en Java.
Creación de nombres de paquete unívocos El lector se habrá percatado de que, dado que un paquete nunca estará realmente "empaquetado" en un solo archivo, podrá estar compuesto por muchos archivos .class, por lo que el sistema de archivos puede llegar a estar un tanto abarrotado. Para evitar el desorden, una medida lógica que podemos tomar seria colocar todos los archivos .class correspondientes a un paquete concreto dentro de un mismo directorio; es decir, aprovechar la estmctura de archivos jerárquica del sistema operativo. Ésta es una de las forma s mediante las que Java trata de evitar el problema de la excesiva acumulación de archivos; vere mos esto de otra fOn1la cuando más adelante presentemos la utilidad jaro Recopilar los archivos de un paquete dentro de un mismo subdirectorio resuelve también otros dos problemas: la creación de nombres de paquete uní vocos y la localización de aquellas clases que puedan estar perdidas en algún lugar de la estructura de directorios. Esto se consigue codificando la ruta correspondiente a la ubicación del archivo .class dentro del nombre del paquete. Por convenio, la primera parte del nombre del paquete es el nombre de dominio Internet invertido del creador de la clase. Dado que está garantizado que los nombres de dominio Internet sean unívocos, si seguimos este convenio nuestro nombre de paquete será unívoco y nunca se producirá una col isión de nombres (es decir, salvo que perdamos el derecho a utilizar el nombre de dominio y la persona que 10 comience a utilizar se dedique a escribir código Java con los mismos nombres de ruta que usted utili zó). Por supuesto, si no disponemos de nuestro propio nombre de dominio, deberemos concebir una combinación que res ulte lo suficientemente improbable (como por ejemplo la combinación de nuestro nombre y apellidos) para crear nombres de paquete unívocos. Si ha decidido comenzar a publicar código Java, merece la pena que haga un requefio esfuerzo para obtener un nombre de dominio. La seg unda parte de esta solución consiste en establecer la correspondencia entre los nombres de paquete y los directorios de la máquina, de modo que cuando el programa Java se ejecute y necesita cargar el archivo .class, pueda localizar el directorio en el que ese archivo .class resida. El intérprete Java actúa de la fomla siguiente. Primero, localiza la variable de entamo CLASSPATH 3 (que se tija a través del sistema operativo y en ocasiones es definida por el programa de instalación que instala Java con una herramienta basada en Java en la máquina). CLASSPATH contiene uno o más directorios que se utilizan como raíces para buscar los archivos .class. Comenzando por esa raíz, el intérprete toma el nombre de paquete y sustituye cada pWltO por una barra inclinada para generar un nombre de ruta a partir de la raiz CLASSPATH (por lo que el paquete package roo.bar.baz se convertiria en foo\bar\baz o foo /bar/baz o, posiblemente, en alguna otra cosa, dependiendo del sistema operativo). Esto se concatena a continuación con las diversas entradas que se encuentren en la variable CLASS PATH . Será en ese subdirectorio donde el intérprete busque el archi vo .class que tenga un nombre que se corresponda con la clase que se esté intentando crear (también busca en algunos directorios estándar relativos al lugar en el que reside el intérprete Java). Para comprender esto, considere por ejemp lo mi nombre de dominio, que es MindView.net. Invirtiendo éste y pasándolo a minúsculas, net.mindview establece mi nombre global uní voco para mis clases (antiguamente, las extensiones com, edu, org, etc., estaban en mayúscu las en los paquetes Java, pero esto se modificó en Java 2 para que todo el nombre del paquete estuviera en minúsculas). Puedo subdi vidir este espacio de nombres todavía más creando, por ejemplo, una biblioteca denominada simple, por lo que tendré un nombre de paquete que será: package net.mindview.simple¡
Ahora, este nombre de paquete puede uti lizarse como espacio de nombres paraguas para los siguientes dos archivos:
3
Cuando nos refiramos a la variable de entamo, utilizaremos letras mayúsculas (CLASSPATH).
6 Control de acceso 125
JI : net / mindview / simple / Vector . java /1 Creación de un paquete . package net . rnindview . s i mp l e; public class Vector { public Vector () ( Syst.em . out . println ( Unet.mindview. simpl e . Vector
Ol
)
i
)
111 > Como hemos men cionado antes, la instrucción package debe ser la primera línea de no comentario dentro del código del archi vo. El segundo archivo tiene un aspecto parecido:
JI : net / mindview/ simple / List . java /1 Creación de un paquete . package net . mindview . simple ¡
public class List { public List 11 { Sys t em.out . prin t ln( "net . mindvie w. simp l e . Lis t" } ;
Ambos archivos se ubicarán en el siguiente subdirectorio de mi sistema: C: \ DOC \ JavaT\ net \ mindv iew\s i mple
Observe que la primera línea de comentario en cada archivo del libro indica la ubicación del directorio donde se encuentra ese archi vo dentro del árbol del código fuente; esto se usa para la herramienta automática de extracción de código que he empleado con el libro. Si examinamos esta ruta, podemos ver el nombre del paquete net.mindview.simple, pero ¿qué pasa con la primera pane de la ru ta? De esa parte se encarga la vari able de entorno CLASSPATH, que en mi máquina es: CLASSPATH= . ;O , \ JAVA\LIB;C, \ OOC \ JavaT
Podemos ver que la variable de en tomo CLASSPATH puede contener
W13
serie de rutas de búsqueda allemativas.
Sin embargo, ex iste una variación cuando se usan archivos JAR. Es necesario poner el nombre real del archi vo lAR en la vari able de mta, y no simplemente la ruta donde está ubi cado. Así, para un archi vo lAR denominado grape.jar, la variable de mta incluiría: CLASSPATH= . ;O, \ JAVA\ LIB;C, \ flavors \ grape.jar
Una vez que la variable de ruta de búsqueda se ha configurado apropiadamente, el siguiente archivo puede ubi ca rse en cualqu ier directorio:
11 : access / LibTest. j ava
II Utiliza la biblioteca. i mport net.mindview.simple . *¡ public class LibTest { public stat ic void main (String [J args ) { Vector v : new Vec t or( ) ; List 1 : new List {) ;
1* Output: net.mindvie w. simple . Vector net.mindview . simple . List
' /11 ,Cuando el compilador se encuentra con la instrucción import correspondiente a la biblioteca simple, comienza a explorar todos los directorios especificados por CLASSPATH, en busca del subdirectorio net/mindview/simple, y luego busca los archi vos compilados con los nombres apropiados (Vector.c1ass para Vector y List.c1ass para List). Observe que tanto las dos clases C0 l110 los métodos deseados de Vector y List tienen que ser de tipo public.
126 Piensa en Java La configuración de CLASSPATH resullaba tan en igmática para los usuarios de Java inexpertos (al menos lo era para mi cuando comencé con el lenguaje) que Sun ha hecho que en el kit JDK de las vcrsiones más recientes de Java se compOrte de fomla algo más inteligente. Se encontrará, cuando lo instale. que aunque no configure la variable CLASSPATH. podrá co mpilar y ejec utar programas Java básicos. Sin embargo. para compilar y ejecutar el paq uete de código fuente de este libro (disponible en 1I'1I'1I'.MindVie1l'. ne¡). necesi tará anadir a la variable CLASSPATH el directorio base del árbol de código. Ejercicio 1 :
(1) Cree una clase dent ro de un paquete. Cree una instancia de esa clase fuera de dicho paquete.
Colisiones ¿Q ué sucede si se importan do s bibliotecas mediante '*' y ambas incluyen los mismos nombres? Por ejemplo, suponga que un programa hace esto: impore net.mindview.simple.*; import java.util.*;
Puesto que java.util.* también contiene una clase Vector. esto provocaría una potencial colisión. Sin embargo, mientras que no lleguemos a escribir el código que provoque en efecto la colisión, no pasa nada. Resulta bastante conveniente que esto sea así, ya que de otro modo nos veríamos forzados a escri bir un montón de cosas para evitar colisiones que realmente nunca iban a suceder. La colisión sí que se producirá si ahora intentamos constmir un Vector: Vector v
=
new Vector {) ;
¿A qué clase Vector se refiere esta línea? El compilad or no puede saberlo, como tampoco puede saberlo el lector. Así que el compilador generará un error y nos obligará a se r más explíci tos. Si queremos utili zar e l Vecto r Java estándar, por ejemplo, deberemos escribir: java.util.Vector v
=
new java.util.Vector {) ;
Puesto que esto Uunto con la variable CLASSPATH) especifica completamente la ubicación de la clase Vector deseada, no existirá en realidad ninguna necesidad de emplea r la instrucción import java.util.*, a menos que vayamos a utilizar alguna Olra clase definida en java.util . Alternativamente, podemos utili zar la instmcción de importación de una única clase para preve nir las colisiones, siempre y cuando no empleemos los dos nombres qu e entran en colisión dentro de un mismo progra ma (en cuyo caso, no tendremos más remedi o que especificar completamente los nom bres). Ejercicio 2 :
( 1) Tome los fragmentos de cód igo de esta sección y transfónn elos en un programa para verificar que se producen las colisiones que hemos mencionado.
Una biblioteca personalizada de herramientas Armados con este conocimiento, ahora podemos crear nuestras propias bibliotecas de herrami erllas, para reducir o eliminar la esc ritura de códi go duplicado. Considere, por ejemplo, el alias que hemos estado utili zando para Sysle m.ou t.println(). con el fin de reducir la cantidad de infonnación tecleada. Esto puede ser parte de una clase denorninada Priot, de modo que dispondríamos de una instrucción estática de impresión bastante más legible: ji : net / mindview/ util / Print.java
JI JI
Métodos de impresión que pueden usarse sin cualificadores, empleando importaciones estáticas de Java SE5: package net.mindview . util; import java.io.*; public class Print JI Imprimir con una nueva línea: public static void print(Object obj) System.out .println{objl;
JI
Imprimir una nueva línea sola:
6 Controt de acceso 127 public static void print()
System.out.println(} ;
JI Imprimir sin salto de línea: public static void printnb (Object obj) System.out.print(obj) ;
{
/ / El nuevo printf () de Java SES (de el : public static PrintStream printf (String format, Obj ect. .. args) { return System . out.printf(format, args );
Podemos utilizar estas abreviaturas de impresión para imprimir cualquier cosa, bien con la inserción de una nueva línea (print( o sin una nueva línea (printnb(
»
».
Como habrá adivinado, este archivo deberá estar ubi cado en un directorio que comience en una de las ubicaciones definidas en CLASSPATH y que luego continúe con net/ mindview. Después de compilar, los métodos sta tic print() y printnb() pueden emplearse en cualquier lugar del sis tema utilizando una instrucción import sta tic:
11: access/PrintTest.java
II Usa los métodos estáticos de impresión de Print.java. import static net.mindview . util.Print.* ¡ public class PrintTest { public static void main(String[] args) print ( !tAvailable frem now on! It) ¡ print ( 1 00); print (100L); print(3.14159) ;
1* Output: Available frem now on! 100 100 3.14159
* /// , Un segundo componente de esta biblioteca pueden ser los métodos range() , que hemos presentado en el Capítulo 4. Con/rol
de la ejecución, y que penniten el uso de la sintaxisforeach para secuencias simples de enteros:
11 : net/mindview/util/Range .java
II II
Métodos de creación de matrices que se pueden usar sin cualificadores, con importaciones estáticas Java SES: package net.mindview.util¡ public class Range { II Generar una secuencia [O . . n) public static int[] range(int n) int [] result "" new int [n] ¡ for (i nt i = O ¡ i < n ¡ i++) result[i] = i¡ return resul t ¡
II Generar una secuencia [start .. end) public static int[] range (int start, int end) int sz = end - start¡ int [] result = new int [sz] ; for(int i = O; i < sz; i++) result[i] ~ start + i;
{
128 Piensa en Java return result;
/ 1 Generar una secuencia [start .. end) con incremento igual a step public static int [] range {int start, int end, int step ) { int sz = (end - start l/ step; int[] result = new int[sz); for (int i = O; i < 5Z; i++ l result[i) = start + (i * step ) ; return result;
A partir de ahora, cuando desarrolle cualquier nueva utilidad que le parezca interesante la podrá añadir a su propia biblioteca. A lo largo del libro podrá ver que cómo añadiremos más componentes a la biblioteca net.mindview.util
Utilización de importaciones para modificar el comportamiento Una característica que se echa en falta en Java es la compilación condicional que existe en e y que pemlite cambiar una variable indicadora y obtener un comportamiento diferente sin variar ninguna otra parte del código. La razón por la que dicha característica no se ha incorporado a Java es, probablemente, porque la mayor parte de las veces se utiliza en C para resolver los problemas interplataforma: dependiendo de la platafomla de destino se compilan diferentes partes del código. Puesto que Ja va está pensado para ser automáticamente un lenguaje interplatafornla no debería ser necesaria. Sin embargo, existen otras aplicaciones interesantes de la compilación condicional. Un uso bastante común es durante la depuración del código. Las características de depuración se activan durante el desarrollo y se desactivan en el momento de lanzar el producto. Podemos conseguir el mismo efecto modificando el paquete que se importe dentro de nuestro programa, con el fin de conmutar entre el código utilizado en la versión de depuración y el empleado en la versión de producción. Esta misma técnica puede utilizarse para cualquier código de tipo condicional. Ejercicio 3:
(2) Cree dos paquetes: debug y debu goff, que contengan una clase idéntica con un método debug(). La primera versión debe mostrar su argumento String en la consola, mientras que la segunda no debe hacer nada. Utilice una línea sta tic import para importar la clase en un programa de prueba y demuestre el efecto de la compilación condicional.
Un consejo sobre los nombres de paquete Merece la pena recordar que cada vez que creamos un paquete, estamos especificando implícitamente una estructura de directorio en el momento de dar al paquete un nombre. El paquete debe estar en el directorio indicado por su nombre, que deberá ser un directorio alcanzable a panir de la mta indicada en CLASSPATH. Experimentar con la palabra clave package puede ser algo frustrante al principio, porque a menos que respetemos la regla que establece la correspondencia entre nombres de paquete y rutas de directorio. obtendremos un montón de misteriosos mensajes en tiempo de ejecución que nos dicen que el sistema no puede encontrar una clase concreta, incluso aunque esa clase esté ahí en el mismo directorio. Si obtiene un mensaje como éste, desactive mediante un comentario la instrucción package y comp ruebe si el programa funciona, si lo hace, ya sabe dónde está el problema. Observe que el código compilado se coloca a menudo en un directorio distinto de aquel en el que reside el código fuente. pero la ruta al código compilado deberá seguir siendo localizable por la JVM utilizando la variable CLASSPATH.
Especificadores de acceso Java Los especificadores de acceso Java public, protected y private se colocan delante de cada definición de cada miembro de O un método. Cada especificador de acceso sólo controla el acceso para esa definición concreta.
la clase, ya sea éste un campo
Si no proporciona un especificador de acceso, querrá decir que ese miembro tiene "acceso de paquete". Por tanto, de una
fonna u otra. todo tiene asociado algún tipo de control de acceso. En las secc iones siguientes. vamos a analizar los diversos tipos de acceso.
6 Control de acceso 129
Acceso de paquete En los ejemplos de los capítu los anteriores no hemos utilizado especificadores de acceso. El acceso predetenninado no tiene asociada ninguna palabra clave, pero comúnmente se hace referencia a él como acceso ele paquete (y también, en ocasiones, "acceso amigable"). Este tipo de acceso significa que todas las demás clases del paquete actual tendrán acceso a ese miembro, pero para las clases situadas fuera del paquete ese miembro aparecerá como priva te. Puesto que cada unidad de compilación (cada archivo) sólo puede pertenecer a un mismo paquete, todas las clases dentro de una misma unidad de compilación estarán automáticamente disponibles para las otras mediante el acceso de paquete. El acceso de paquete nos permite agrupar en un mismo paquete una serie de clases relacionadas para que puedan interactuar fácilmente entre sí. Cuando se colocan las clases juntas en un paquete, garantizando así el acceso mutuo a sus miembros definidos con acceso de paquete, estamos en cierto modo garantizando que el código de ese paquete sea "propiedad" nuestra. Resulta bastante lógico que sólo el código que sea de nuestra propiedad disponga de acceso de paquete al resto del código que nos pertenezca. En cierto modo. podríamos decir que el acceso de paquete hace que tenga sentido el agrupar las clases dentro de un paquete. En muchos lenguajes. la forma en que se hagan las definiciones en los archivos puede ser arbitraria. pero en Java nos vemos impelidos a organizarlas de una fomla lógica. Además, podemos aprovechar la definición del paquete para excluir aquellas clases que no deban tener acceso a las clases que se definan en el paquete actual. Cada clase se encarga de controlar qué código tiene acceso a sus miembros. El código de los restantes paquetes no puede presentarse sin más y esperar que le muestren los miembros protected, los miembros con acceso de paquete y los miembros private de una detemlinada clase. La única fomla de conceder acceso a un miembro consiste en: l. Hacer dicho miembro public. Entonces, todo el mundo podrá acceder a él. 2. Hacer que ese miembro tenga acceso de paquete, por el procedimiento de no incluir ningún especificador de acceso, y colocar las otras clases que deban acceder a él dentro del mismo paquete. Entonces, las restantes clases del paquete podrán acceder a ese miembro. 3. Como veremos en el Capítulo 7, Rculili::ación de clases, cuando se introduce la herencia. una clase heredada puede acceder tanto a los miembros protectcd como a los miembros pubLic (pero no a los miembros priva te). Esa clase podrá acceder a los miembros con acceso de paquete sólo si las dos clases se encuentran en el mismo paquete. Pero, por el momento, vamos a olvidamos de los temas de herencia y del especificador de acceso protected . 4. Proporcionar métodos "de acceso/mutadores" (también denominados métodos "get/set") que pennitan leer y cambiar el valor. Éste es el enfoque más civilizado en ténninos de programación orientada a objetos, y resulta fundamental en JavaBeans, como podrá ver en el Capítulo 22, blle/faces gráficas de usuario.
public: acceso de interfaz Cuando se utiliza la palabra clave public, ésta quiere decir que la declaración de miembros situada inmediatamente a continuación suya está disponible para todo el mundo, y en particular para el programa cliente que utilice la biblioteca. Suponga que definimos un paquete desscrt que contiene la siguiente unidad de compilación: //: access/dessert/Cookie.java // Crea una biblioteca. package access.dessert; public class Cookie { public Cookie () ( System.out .println(IICookie constructor"); } void bite() { System.out.println("bite"); }
/// ,Recuerde que el archivo de clase producido por Cookie.java debe residir en un subdirectorio denominado dessert, dentro de un directorio access (que hace referencia al Capítu lo 6, Control de acceso de este libro) que a su vez deberá estar bajo uno de los directorios CLASSPAT H. No cometa el error de pensar que Java siempre examinará el directorio actual como uno de los puntos de partida de su búsqueda. Si no ha incluido un ... como una de las rutas dentro de CLASS PATH, Java no examinará ese directorio.
130 Piensa en Java
Si ahora creamos un programa que usa Cookie:
11 :
access/Dinner.java Usa la biblioteca. import access.dessert. * ;
II
public class Dinner { public static void main{String[] args) Cookie x = new Cookie(); II! x.bite(); II No puede acceder
1* Output: Cookie constructor * jjj ,podemos crear un objeto Cookie, dado que su constructor es public y que la clase también es public (más adelante, profundizaremos más en el concepto de clase pubHc). Sin embargo, el miembro bite() es inaccesible desde de Dinner.java ya que bite() sólo proporciona acceso dentro del paquete dessert, así que el compilador nos impedirá utilizarlo.
El paquete predeterminado Puede que le sorprenda descubrir que el siguiente código sí que puede compilarse, a pesar de que parece que no cumple con las reglas:
11:
II
access/Cake.java Accede a una clase en una unidad de compilación separada.
class Cake { public static void main (Stri ng [] args) Pie x = new Pie {) i x. f () ;
{
1* Output: Pie. f () *j jj,En un segundo archivo del mismo directorio tenemos:
11:
II
access/Pie.java La otra clase.
class Pie { void f {) { System.out.println{"Pie.f{) 11 ) i } jjj,-
}
Inicialmente, cabría pensar que estos dos archivos no tienen nada que ver entre sí, a pesar de lo cual Cake es capaz de crear un objeto Pie y de in vocar su método ro (observe que debe tener ' .' en su variable CLASSPATH para que los archivos se compilen). Lo que parecería lógico es que Pie y f() tengan acceso de paquete y no estén, por tanto, disponibles para Cake. Es verdad que tienen acceso de paquete, esa parte de la suposición es correcta. Pero la razón por la que están disponibles en Cake.java es porque se encuentran en el mismo directorio y no tienen ningún nombre explícito de paquete. Java trata los archivos de este tipo como si fueran implícitamente parte del "paquete predetem1inado" de ese directorio, y por tanto pro· porciona acceso de paquete a todos los restantes archivos situados en ese directorio.
private: ino lo toque! La palabra clave private significa que nadie puede acceder a ese miembro salvo la propia clase que lo contiene, utili zando para el acceso los propios métodos de la clase. El resto de las clases del mismo paquete no puede acceder a los miembros privados, así que el efecto resultante es como si estuviéramos protegiendo a la clase contra nosotros mismos. Por otro lado. resulta bastante común que un paquete sea creado por varias personas que colaboran entre sí, por lo que private pennite modificar libremente ese miembro sin preocuparse de si afectará a otras clases del mismo paquete.
6 Control de acceso 131 El acceso de paquete predetenninado proporciona a menudo un nivel adecuado de ocultación; recuerde que un miembro con acceso de paquete resulta inaccesible para todos los programas cliente que utilicen esa clase. Esto resulta bastante conveniente, ya que el acceso predetenninado es el que nomlalmente se utiliza (y el que se obtiene si nos olvidamos de añadir especificadores de control de acceso). Por tanto, 10 que normalmente haremos será pensar qué miembros queremos definir explícitamente como públicos para que los utilicen los programas cliente; como resultado, uno tendería a pensar que la palabra clave private no se utiliza muy a menudo, ya que se pueden realizar los diseños si n ella. Sin embargo, la realidad es que el uso coherente de priva te tiene una gran importancia, especialmente en el caso de la programación multihebra (como veremoS en el Capítulo 21, Concurrencia). He aquí un ejemplo del uso de priva te:
11 : access/IceCream.java II Ilustra la palabra clave "private". class Sundae { pri vate Sundae() {} static Sundae makeASundae() return new Sundae{)¡
public class IceCream { public static void main (Str ing[) args ) II ! Sundae x = new Sundae() ¡ Sundae x = Sundae.makeASundae();
Este ejemplo nos pennite ver un caso en el que private resulta muy útil: queremos tener control sobre el modo en que se crea un objeto y evitar que nadie pueda acceder directamente a un constructor concreto (o a todos eUos). En el ejemplo anterior, no podemos crear un objeto Sundae a través de su constructor; en lugar de ello, tenemos que invocar el método makeASundae( ) para que se encargue de hacerlo por nosotros 4 Cualquier método del que estemos seguros de que sólo actúa como método "auxiliar" de la clase puede ser defmido como privado para garantizar que no lo utilicemos accidentalmente en ningún otro lugar del paquete, lo que nos impediría modificar o eliminar el método. Definir un método como privado nos garantiza que podamos modificarlo libremente en el futuro. Lo mismo sucede para los campos privados definidos dentro de una clase. A menos que tengamos que exponer la implementación subyacente (lo cual es bastante menos habinlal de lo que podría pensarse), conviene definir todos los campos como privados. Sin embargo, el hecho de que una referencia a un objeto sea de tipo private en una clase no quiere decir que algún otro objeto no pueda tener una referencia de tipo publie al mismo objeto (consuhe los suplementos en línea del libro para conocer más detalles acerca de los problemas de los alias).
protected: acceso de herencia Para poder comprender el especificador de acceso protected, es necesario que demos un salto hacia adelante. En primer lugar, debemos tener presente que no es necesario comprender esta sección para poder continuar leyendo el libro hasta llegar al capítulo dedicado a la herencia (el Capítulo 7, Relllilización de clases). Pero para ser exhaustivos, hemos incluido aquí una breve descripción y un ejemplo de uso de proleeted. La palabra clave protected trata con un concepto denominado herencia, que toma una clase existente (a la que denominaremos clase base) y añade nuevos miembros a esa clase sin tocar la clase existente. También se puede modificar el comportamiento de los miembros existentes en la clase. Para heredar de una clase, tenemos que especificar que nuestra nueva clase amplía (ex tends) una clase existente, como en la siguiente línea: class Foo extends Bar { Existe otro efecto en este caso: puesto que el único constructor definido es el predetenninado y éste se ha definido como pl"ivate, se impedini que nadie herede esta clase (lo cual es un tema del que hablaremos más adelante).
4
132 Piensa en Java
El resto de la definición de la clase tiene el aspecto habitual. Si creamos un nuevo paquete y heredamos de una clase situada en otro paquete. los únicos miembros a los que tendremos acceso son los miembros públicos del paquete original (por supuesto, si la herencia se realiza dentro del mismo paquete. se podrán manipular todos los miembros que tenga n acceso de paquete). En ocasiones, el creador de la clase base puede tomar un miembro concreto y garantizar el acceso a las clases derivadas. pero no al mundo en generaL Eso es precisamente lo que hace la palabra clave protected . Esta palabra clave también proporciona acceso de paquete, es decir, las restantes clases del mismo paquete podrán acceder a los elementos protegidos. Si volvemos al archivo Cookic.java. la siguiente clase no puede invocar el miembro bite( ) que tiene acceso de paquete: // : access/ChocolateChip.java /1 No se puede usar un miembro con acceso de paquete desde otro paquete. import access.dessert.*; public class ChocolateChip extends Cookie ( public ChocolateChip ()
{
System. out .println ( "ChocolateChip constructor");
} public void chomp() lIt bite()¡ 1/ No se puede acceder a bite public static void main(String[] args ) ChocolateChip x = new ChocolateChip{); x. chomp () ¡
1* Output: Cookie constructor
ChocolateChip constructor
* /// ,Uno de los aspectos interesantes de la herencia es que, si existe un método bite( ) en la clase Cookie, también existirá en todas las clases que hereden de Coo ki e. Pero como bite() tiene acceso de paquete y está situado en un paquete di stinto, no estará disponible para nosotros en el nuevo paquete. Por supuesto, podríamos hacer que ese método fuera público, pero entonces todo el mundo tendría acceso y puede que no sea eso lo que queramos. S i modificamos la clase Cookie de la forma siguiente: 1/ : access/cookie2/Cookie.java package access.cookie2¡ public class Cookie { public Cookie ()
{
System.out.println("Cookie const r uctor " ) ¡ protected void bi te () { System.out.println{"bite") ;
ahora bite( ) estará d isponible para toda aque lla clase que herede de Cookic:
11: access /ChocolateChip2.java import access.cookie2.*; public class ChocolateChip2 extends Cookie { public ChocolateChip2 ()
{
System. out. println ( "ChocolateChip2 constructor") ¡
} public void chomp ()
{ bite ();
} // Método protegido
6 Control de acceso 133 public static void main(String[]
ChocolateChip2 x x.chomp() i
=
args)
{
new ChocolateChip2()
i
/ * Output : cookie constructor ChocolateChip2 constructor bite
' /11 ,Observe que, aunque bite( ) también tiene acceso de paquete, no es de tipo publico Ejercicio 4 :
(2) Demuestre que los métodos protegidos (protected) tienen acceso de paquete pero no son públicos.
Ejercicio 5 :
(2) Cree una clase con campos y métodos de tipo p ubli c, private, protected y con acceso de paquete. Cree un objeto de esa clase y vea los tipos de mensajes de compi lación que se obtienen cuando se intenta acceder a todos los miembros de la clase. Tenga en cuenta que las clases que se encuentran en el mismo directorio faonan parte del paquete "predetenrunado",
Ejercicio 6:
(1) Cree una clase con datos protegidos. Cree una segunda clase en el mismo archivo con un método que manipule los datos protegidos de la primera clase.
Interfaz e implementación El mecanismo de control de acceso se denomina a menudo ocultación de la implementación. El envolver los datos y los métodos dentro de la clase, en combinación con el mecanismo de ocultación de la implementación se denomina a menudo encapsulación. 5 El resultado es un tipo de datos con una serie de características y comportamientos. El mecanismo de control de acceso levanta una serie de fronteras dentro de un tipo de datos por dos razones importantes. La primera es establecer qué es lo que los programas cliente pueden usar o no. Podemos entonces diseñar como queramos los mecanismos internos dentro de la clase, sin preocuparnos de que los programas cliente utilicen accidentalmente esos mecanismos internos como parte de la interfaz que deberían estar empleando. Esto nos lleva directamente a la segunda de las razones, que consiste en separar la interfaz de la implementación. Si esa clase se utiliza dentro de un conjunto de programas, pero los programas cliente tan sólo pueden enviar mensajes a la interfaz pública, tendremos libertad para modificar cualquier cosa que no sea pública (es decir, los miembros con acceso de paquete, protegidos o privados) sin miedo de que el código cliente deje de funcionar. Para que las cosas sean más claras, resulta conveniente a la hora de crear las clases, siul8r los miembros públicos al principio, seguidos de los miembros protegidos, los miembros con acceso de paquete y los miembros privados. La ventaja es que el usuario de la clase puede comenzar a leer desde el principio y ver en primer lugar lo que para él es lo más importante (los miembros de tipo public, que son aquellos a los que podrá accederse desde fuera del archivo), y dejar de leer en cuanto encuentre los miembros no públicos, que f0n11an parte de la implementación interna.
11 : access/OrganizedByAccess.java public class OrganizedByAccess { public void publ () { / * */ .. •1 ... • 1 private void privl () { / * */ private void priv2 () { 1* */ private void priv3 () { /* */ public void pub2 () public void pub3 ()
{ /. { l'
private int i¡ /1
/! / ,-
5 Sin embargo, mucha gente utiliza la palabra encapsulación para referirse en exclusiva a la ocultación de la implementación.
134 Piensa en Java
Esto sólo facilita parcialmente la lectura, porque la interfaz y la implementación siguen estando mezcladas. En otras palabras, el lector seguirá pudiendo ver el código fuente (la implementación), porque está incluido en la clase. Además, el sistema de documentación basado en comentarios soportado por Javadoc no concede demasiada importancia a la legibilidad del código por parte de los programadores de clientes. El mostrar la interfaz al consumidor de una clase es, en realidad, trabajo del explorador de clases, que es una herramienta que se encarga de exarnjnar todas las clases disponibles y mostramos lo que se puede hacer con ellas (es decir, qué miembros están disponibles) en una forma apropiada. En Java, visualizar la documentación del IDK con un explorador web nos proporciona el mismo resultado que si utilizáramos un explorador de clases.
Acceso de clase En Java, los especificadores de acceso también pueden emplearse para detennjnar qué clases de una biblioteca estarán disponibles para los usuarios de esa biblioteca. Si queremos que una cierta clase esté disponible para un programa cliente, tendremos que utilizar la palabra clave public en la definición de la propia clase. Con esto se controla si los programas cliente pueden siquiera crear un objeto de esa clase. Para controlar el acceso a una clase, el especificador debe situarse delante de la palabra clave class, Podemos escribir: pUblic class Widget {
Ahora, si el nombre de la biblioteca es access, cualquier programa cliente podrá acceder a Widget mediante la instrucción import access.Widget¡
o import access.*¡
Sin embargo, existen una serie de restricciones adicionales: l . Sólo puede haber una clase pu blic por cada unidad de compilación (archivo). La idea es que cada unidad de compilación tiene una única interfaz pública representada por esa clase pública. Además de esa clase, puede tener tantas clases de soporte con acceso de paquete como deseemos. Si tenemos más de una clase pública dentro de una unidad de compilación, el compilador generará un mensaje de error. 2. El nombre de la clase public debe corresponderse exactamente con el nombre del archivo que contiene dicha unidad de compilación, incluyendo el uso de mayúsculas y minúsculas. De modo que para Widget, el nombre del archivo deberá ser Widget.java y no widget.java ni WIDGET.java . De nuevo, obtendremos un error en tiempo de compilación si los nombres no concuerdan.
3. Resulta posible, aunque no muy normal, tener una unidad de compi lación sin ninguna clase publico En este caso, podemos dar al archivo el nombre que queramos (aunque si lo denominamos de forma arbitraria confundiremos a las personas que tengan que leer y mantener el código). ¿Qué sucede si tenemos una clase dentro de access que sólo estamos empleando para llevar a cabo las tareas realizadas por Widget o alguna otra clase de tipo public de access? Puede que no queramos molestamos en crear la documentación para el programador de clientes y que pensemos que algo más adelante quizá queramos modificar las cosas completamente, eliminando esa clase y sustituyéndola por otra. Para poder disponer de esta flexibilidad, necesitamos garantizar que ningún programa cliente dependa de nuestros detalles concretos de implementación que están ocultos dentro de access. Para conseguir esto, basta con no incluir la palabra clave public en la clase, en cuyo caso tendrá acceso de paquete (dicha clase sólo podrá ser usada dentro de ese paquete). Ejercicio 7 :
(1) Cree una biblioteca usando los fragmentos de código con los que hemos descrito access y Widget. Cree un objeto Widget dentro de una clase que no forme parte del paquete access.
Cuando creamos una clase con acceso de paquete, sigue siendo conveniente definir los campos de esa clase como private (siempre deben hacerse los campos lo más privados posible), pero generalmente resulta razonable dar a los métodos el mismo tipo de acceso que a la clase (acceso de paquete). Puesto que una clase con acceso de paquete sólo se utiliza normalmente dentro del paquete, sólo hará falta defi nir como públicos los métodos de esa clase si nos vemos obligados a ello; además, en esos casos, el compilador ya se encargará de informamos.
6 Control de acceso 135 Observe que una clase no puede ser private (ya que eso haria que fuera inaccesible para todo el mundo salvo para la propia clase) ni protected. 6 Así que sólo tenemos dos opciones para especificar el acceso a una clase: acceso de paquete o public. Si no queremos que nadie tenga acceso a la clase. podemos definir todos los constructores como privados, impidiendo que nadie cree un objeto de dicha clase salvo nosotros, que podremos hacerlo dentro de un miembro de tipo static de esa clase. He aquí un ejemplo: ji : access/Lunch.java Ilustra los especificadores de acceso a clases. Define una clase como privada con constructores privados:
/1 /1
class Soupl { pri vate Soupl () () JI (1) Permite la creación a través de un método estático: public static Soupl makeSoup() return new Soupl();
class Soup2 { private Soup2() () /1 (2) Crea un objeto estático y devuelve una referencia JI cuando se solicita. (El patrón "Singleton " ) : private static Soup2 psi = new Soup2{) i public static Soup2 access {} { return psl¡ public void f ()
{}
1/ Sólo se permite una clase pública por archivo: public class Lunch { void testPrivate () { II ¡No se puede hacer! Constructor privado: II! Soupl soup = new Soupl() ; void testStatic() Soupl soup = Soupl.makeSoup(); void testSingleton{) Soup2. access () . f () ;
}
lit > Hasta ahora, la mayoría de los métodos devolvían void o un tipo primitivo, por lo que la definición: public static Soupl makeSoup() return new Soupl{) ;
puede parecer algo confusa a primera vista. La palabra Soupl antes del nombre del método (makeSoup) dice lo que el método devuelve. Hasta ahora en el libro, esta palabra normalmente era void, lo que significa que no devuel ve nada. Pero también podemos devolver una referencia a un objeto, que es lo que estamos haciendo aquí. Este método devuelve una referencia a un objeto de la clase Soupl. Las clases Soupl y Soup2 muestran cómo impedir la creación directa de objetos de una clase definiendo todos los constructores como privados. Recuerde que, si no creamos explícitamente al menos un constructor, se creará automáticamente el constructor predeterminado (un constructor sin argwnentos). Escribiendo el constructor predetenninado, garantizamos
6 En rea lidad, una e/ase ¡nlerna puede ser privada o protegida, pero se trata de un caso especial. Hablaremos de ello en el Capítulo 10, Clases internas.
136 Piensa en Java que no sea escrito automáticamente. Definiéndolo como privado nadie podrá crear un objeto de esa clase. Pero entonces, ¿cómo podrá alguien lIsar esta clase? En el ejemplo anterior se muestran dos opciones. En Soup 1. se crea un método static que crea un nuevo objeto SoupJ y devuelve una referencia al mismo. Esto puede ser úti l si queremos realizar algunas operaciones adicionales con el objeto Soupt antes de devol verlo, o si queremos llevar la cuenta de cuántos objetos Soupl se han creado (por ejemplo. para restringir el número total de objetos). Soup2 utiliza lo que se denomina un pmrón de disei)o, de lo que se habla en Thinking in Pallerns (with Java) en wl1'H'.Minc/View.nef. Este patrón concreto se denomina Solitario (s; ngleron) , porque sólo pennite crear Wl único objeto. El
objeto de la clase Soup2 se crea como un miembro static prívate de Soup2, por lo que existe un objeto y sólo uno, y no se puede acceder a él salvo a rravés del mérodo público access( ). Como hemos mencionado anterionnente, si no utili zamos un especificador de acceso para una clase, ésta tend rá acceso de paquete. Esto significa que cualquier otra clase del paquete podrá crear un objeto de esa clase, pero no se podrán crear objetos de esa clase desde fue ra del paquete (recuerde que todos los archivos de un mismo di rectori o que no tengan declaraciones package exp lícitas fonnan parte, implícitamente, del paquete predetenni nado de dicho directorio). Sin embargo, si un miembro estático de esa clase es de tipo publie, el programa cliente podrá seguir accediendo a ese miembro estático, aún cuando no podrá crear un objeto de esa clase.
Ejercicio 8:
(4) Siguiendo la fonna del ejemplo Lunch.java. cree una clase denominada ConnectionManager que gestione una matriz fija de objetos Conncetion. El programa cliente no debe poder crear explícitamente objetos Conncetíon, sino que sólo debe poder obtenerlos a través de un método estát ico de ConnectionManager. Cuando ConnectionManager se quede sin objetos, devolverá una referencia oul!. Pmebe las clases con un programa main() .
Ejercicio 9:
(2) Cree el sigui ente archivo en el directorio aeccss/loeal (denrro de su mta CLASSPATH):
11 access/local / PackagedClass.java package access.local¡ class PackagedClass { public PackagedClass() System.out.println("Creating a packaged class")
i
A continuación cree el siguiente archivo en un directorio distinto de aceess/local :
11 access/foreign/Foreign.java package access.foreign¡ import access.local.*¡ public class Foreign { public static void main(String[] args) { PackagedClass pe = new PackagedClass() ¡
Exp lique por qué el compilador crea un error. ¿Se reso lveria el error si la clase Forcign fuera parte del paquete access. local?
Resumen En cualquier relación, resulta importante definir una serie de fronteras que sean respetadas por todos los participantes. Cuando se crea una biblioteca. se establece una relación con el usuario de esa biblioteca (el programador de clientes), que es un programador como nosotros, aunque lo que hace es utili zar la biblioteca para constru ir una aplicación u otra biblioteca de mayor tamai'io. Sin una serie de reglas, los programas cliente podrían hacer lo que quisieran con lodos los miembros de una clase, aún cuan· do nosotros prefiriéramos que no manipularan algunos de los miembros. Todo estaría expuesto a ojos de todo el mundo.
6 Conlrol de acceso 137 En este capítulo hemos visto cómo se construyen bibliotecas de clases: en primer lugar, la fonna en que se puede empaquetar un grupo de clases en una biblioteca y. en segundo lugar, la fanna en que las clases pueden controlar el acceso a sus miembros. Las esti maciones indican que los proyectos de programación en e empiezan a fallar en algún punto entre las 50.000 y 100.000 líneas de código, porque e tiene un único espacio de nombres yesos nombres empiezan a colisionar, obligando a realizar un esfuerzo adicional de gestión. En Java. la palabra clave package, el esquema de denominación de paquetes y la palabra clave import nos proporcionan un control completo sobre los nombres, por lo que puede evitarse fácilmente el pro-
blema de la colisión de nombres. Existen dos razones para controlar el acceso a los miembros. La primera consiste en evirar que los usuarios puedan husmear en aquellas partes del código que no deben lOcar. Esas partes son necesarias para la operación interna de la clase, pero no fonnan parte de la interfaz que el programa cliente necesita. Por tanto, definir los métodos y los campos como privados constituye un servicio para los programadores de clientes, porque así éstos pueden ver fácilmente qué cosas so n impoI1antes para ellos y qué cosas pueden ignorar. Simplifica su comprensión de la clase. La segunda razón, todavía más importante. para definir mecanismos de control de acceso consiste en pennitir al diseñador de bibliotecas modificar el funcionamiento interno de una clase sin preocuparse de si ello puede afectar a los programas cliente. Por ejemplo. podemos diseñar al principio ulla clase de una cierta manera y luego descubrir que una cierta reestructuración del código podría aumentar enomlemente la velocidad. Si la interfaz y la implementación están claramente protegidas, podemos realizar las modificaciones sin forzar a los programadores de clientes a reescribir su código. Los mecanis1110S de control de acceso garantizan que ningún programa cliente dependa de la implementación subyacente de una clase. Cuando di sponemos de la capacidad de modificar la implementación subyacente, no sólo tenemos la libertad de mejorar nuestro diselio, sino también la libertad de cometer errores. lndependicntemente del cuidado que pongamos en la planificación y el diseño, los errores son inevitables. Saber que resulta relativamente seguro cometer esos errores significa que tendremos menos miedo a experimentar, que aprenderemos más rápidamente y que finalizaremos nuestro proyecto con mucha más antelación. La interfaz pública de una clase es lo que el usuario puede ver, así que constituye la parte de la clase que más importancia tiene que definamos "correctamente" durante la fase de análisis del diseño. Pero incluso aquí existen posibilidades de modificación. Si no definimos correctamente la interfaz a la primera podemos añadir más mélOdos siempre y cuando no eliminemos ningún método que los programas cliente ya hayan utilizado en su código. Observe que el mecanismo de control de acceso se centra en una relación (yen un tipo de comunicación) entre un creador de bibliotecas y los clientes externos de esas bibliotecas. Existen muchas situaciones en las que esta relación no está presente. Por ejemplo, imagine que todo el código que escribimos es para nosotros mismos o que estamos trabajando en estrecha relación con un pequeño grupo de personas y que todo lo que se escriba se va a incluir en un mismo paquete. En esas situaciones, el lipa de comunicación es diferente, y la rígida adhesión a una serie de reglas de acceso puede que no sea la solución óp tima. En esos casos, el acceso predetenninado (de paquete) puede que sea perfectamente adecuado. Las soluciones a los ejercicios seleccionados pueden encontrarse en el documento electrónico The Thillkillg ill JUnI AI/I/Ulafed SO{I/fioll CI/ide. disponible para la \"cnta en \\'\\'II'./LIil/dl'iell."ef.
Reutilización de clases Una de las características más atractivas de Java es la posibilidad de reutilizar el código. Pero, para ser verdaderamente revolucionario es necesario ser capaz de hacer mucho más que simplemente copiar el cód igo y modificarlo. Ésta es la técnica que se ha estado utilizando en lenguajes procedimental es como e, y no ha funcionado muy bien. Como todo lo demás en Java, la solución que este lenguaje proporciona gira alrededo r del concepto de clase. Reutilizando el código creamos nuevas clases, pero en lugar de crearlo partiendo de cero, usamos las clases existentes que alguien haya construido y depurado anteri omlcnte. El truco estri ba en utilizar las clases sin modificar el código existente. En este capítulo vamos ver dos fonnas de llevar esto a cabo. La primera de ellas es bastante simpl e. Nos limitamos a crear objetos de una clase ya existente dentro de una nueva clase. Esto se denomina composición, porq ue la nueva clase está compuesta de objetos de otras clases existentes. Con esto, simplemente estamos reu tili zando la funcionalidad del códi go, no su fonna. La segunda técnica es más sutil. Se basa en crear una nueva clase como un tipo de una clase existente. Literalm ente 10 que hacemos es tomar la fonna de la clase existente y añadirl a código sin modificarla. Esta técnica se denomina herencia, y el compi lador se encarga de realizar la mayor parte del trabajo. La herencia es una de las piedras angulares de la programación orientada a objetos y tiene una serie de implicaciones adicionales que analizaremos en el Capítulo 8, Poliformismo. Resulta que buena parte de la sintaxis y el comportamiento son similares tanto en la composición como en la herencia (lo que tiene bastante sentido, ya que ambas son fonnas de construir nuevos tipos a partir de otros tipos existentes). En este capítulo, vam os a presentar ambos mecanismos de reutilización de código.
Sintaxis de la composición Hemos utilizado el mecanismo de com posición de fonna bastante frecuente en el libro hasta este momento. Simplemente, basta co n colocar referencias a objetos dentro de las clases. Por ejemplo, suponga que qu eremos construir un objeto que almacene varios objetos String, un par de primitivas y otro objeto de otra clase. Para los objetos no primitivos, lo que hacemos es colocar referencias dentro de la nueva clase, pero sin embargo las primitivas se definen directamente:
11 : reusing/SprinklerSystem.java II Composición para la reutilización de código. class WaterSource { private String S; WaterSource () { System . out .println ("Wate rSource () ,, ) ; s = "Constructed"; public String toString ()
{ return s; }
public class SprinklerSystem { private String valvel, valve2, valve3, valve4¡
140 Piensa en Java new WaterSource () ¡ private WaterSource source pri vate int i i private float fi pu blic String toString () return IIvalvel + valvel + + II v alve2 + valve2 + + + valve3 + "va l ve3 + "valve4 + valve4 + " \ n" + "i = " + i + " " + "f = " + f + " " + 11 source = 11 + source ¡ public static void main (String[] args ) { SprinklerSystem sprinklers = new SprinklerSystem () ¡ System.out.println ( sprinklers ) ¡
1* Output: WaterSource ( ) valvel = null valve2 i = O f = 0.0 source
null valve3 Constructed
null valve4
null
* / //,Uno de los métodos definidos en ambas clases es especial : toString(). Todo objeto no primitivo tiene un método toString() que se in voca en aquellas situaciones especiales en las que el compilador quiere una cadena de caracteres String pero lo que tiene es un objeto. Así, en la expresión contenida en SprinklerSystem.toString(): 11
source =
11
+ source ¡
el compilador ve que estamos intentando añadir un objeto String ("source = 11) a un objeto WaterSource. Puesto que sólo podemos '"añadir" un objeto String a otro objeto String, el compilador dice "¡voy a convertir source en un objeto String in voca ndo toString() !". Después de hacer esto, puede combinar las dos cadenas de caracteres y pasar el objeto String res ultante a System.out.println() (o, de fonna equi valente, a los métodos estáticos print() y printnb() que hemos definido en este libro). Siempre que queramos permitir este tipo de comportamiento dentro de una clase que creemos, nos bastará con escribir un método toString( ). Las primitivas que son campos de una clase se inicializa n automát icamente con el valor cero, como hemos indicado en el Capítulo 2, Todo es un objeto. Pero las referencias a objetos se inicializan con el valor null, y si tratamos de invocar métodos para cualquiera de ellas obtendremos una excepción (un eITor en tiempo de ejecución). Afortunadamente, podemos imprimir una referencia null si n que se genere una excepción. Tiene bastante sentido que el compilador no cree un objeto predetenninado para todas las referencias, porque eso obligaría a un gasto adicional de recursos completamente innecesarios en muchos casos. Si queremos inicializar las referencias, podemos hacerlo de las siguientes fonnas:
J. En el lugar en el que los objetos se definan. Esto significa que estarán siempre inicializados antes de que se in voque el constructor. 2. En el constructor correspondiente a esa clase. 3. Justo antes de donde se necesite utili zar el objeto. Esto se suele denominar inicialización diferida. Puede reducir el gasto adicional de procesamiento en aquellas situaciones en las que la creación de los objetos resulte muy cara y no sea necesario crear el objeto todas las veces. 4. Utili za ndo la técnica de iniciaUzación de instancia. El siguiente ejemplo ilustra las cuatro técnicas: 11: reusing / Bath.java
I I Inicialización mediante constructor con composición. import static net.mindview.util . Print .* ¡ class Soap { private String Si
7 Reutilización de clases 141 Soap () ( print ("Soap()") i
s
= "Constructed";
public String toString ()
{ return s;
}
public class Bath { private String /1 Inicialización en el punto de definición: 51 :: I'Happy" , s2 = 11 Happy 11 , 53,
54;
private Soap castille; private int ii private float toy; public 8ath 1) { print (U Inside Bath () " ) 53 = "Joy"; toy = 3 .14f;
castille
= new
Soap()
i
i
// Inicialización de instancia: {i= 47;} public String toString () { if (54 == nulll / / Inicialización diferida: 54
=
IIJoy";
return
"sl
+ + + +
"52 IIs3
"54
"i =
11
+ i
51 52 53 54 +
" \n" "\n" "\n" "\nn "\n" +
+ + + +
+ + + +
"toy = " + toy + " \n " + "castille = " + castille; public static void main(String[] Bath b = new Bath(); print lb) ;
argsl
{
/* Output: Inside Bath () Soap () 51 Happy s2
Happy
53
Joy
54
i
Joy
:::: 47
toy = 3.14 castille Constructed
,///,Observe que en el constructor 8ath, se ejecuta una instrucción antes de que tenga luga r ninguna de las inicializaciones. Cuando no se reali za la iniciaLización en el punto de defi nición, sigue sin haber ninguna garantía de que se vaya a reali zar la inicialización antes de enviar un mensaje a una referencia al objeto, salvo la inevi table excepción en tiempo de ejecución. Cuando se invoca toString(), as igna un valor a s4 de modo que todos los campos estarán apropiadamente inicializados en el momento de usarlos. Ejercicio 1:
(2) Cree una clase simple. Dentro de una segunda clase, defina una referencia a un objeto de la segunda clase. Utilice el mecanismo de inicialización diferida para instanciar este objeto.
142 Piensa en Java
Sintaxis de la herencia La herencia es una parte esencial de Java (y lOdos los lenguajes orientados a objetos). En la práctica. siempre estamos uti-
liza ndo la herencia cada vez que creamos
UDa
clase, porque a menos que heredemos explícitamente de alguna otra clase.
es taremos heredando implícitamente de la clase raíz estándar Object de Java. La si ntaxis de composición es obvia, pero la herencia utiliza una sintaxis especial. Cuando heredamos, lo que hacemos es decir "esta nu eva clase es similar a esa antigua clase". Especificamos esto en el código situado antes de la llave de abertura del cuerpo de la clase. utilizando la palabra clave extends seguida del nombre de la clase base. Cuando hacemos esto, automáticamente obtenemos todos los campos y métodos de la clase base. He aquí una clase:
JI: reusing/Detergent.java
JI
Sintaxis y propiedades de la herencia. impore static net.mindview.util.Print.*¡
class Cleanser { private String s = "Cleanser"; public void append(String al { s += a¡ } public void dilute{) ( append(U dilute() "); public void apply () public void scrub ()
( append (" apply () " ); } ( append (" scrub () " ) ; }
public String toString () { return Si} public static void main(String[] args) Cleanser x = new Cleanser(); x . dilute(); x.apply(); x.scrub();
print (x l;
public class Detergent extends Cleanser { // Cambio de un método : public void scrub () { append(" Detergent.scrub()"); super.scrub() ¡ // Invocar version de la clase base // Añadir métodos a la interfaz: public void foam()
( appendl" foam() ");
// Probar la nueva clase: public static void main (String [] args) Detergent x = new Detergent(); x.dilute() ;
{
x.apply() ;
x.scrub(l; x.foam (); print (x l; print ("Testing base class:!I l ; Cleanser.main(argsl; / * Output: Cleanser dilute () apply () Detergent . scrub () scrub () f oam() Testing base class : Cleanser dilute () apply () scrub()
* /// ,Esto nos pem1ite ilustrar una serie de características. En primer lugar, en el método Cleanser append(). se concatenan cadenas de caracteres con S utilizando el ope rador +=. que es uno de los operado res, junto con que los diseñadores de Java "han sobrecargado" para que funcionen con cadenas de caracteres.
'+"
En segu ndo lugar. tanto eleanser co mo Detergent contienen un método maine ). Podemos crear un método maine ) para cada una de nuestras clases: esta técnica de colocar un método main() en cada clase pcm1ite probar fácilmente cada una de
7 Reutiliza ción de clases 143
ellas. V no es necesario el iminar el método maine ) cua ndo hayamos tenn inado: podemos dejarlo para las pruebas poste-
riores. Aún cuando tengamos muchas clases en un programa, sólo se invoca rá el método main() de la clase especificada en la línea de comandos. Por tanto, en este caso, cuando escribimos java Detergent, se invocará Detergent.main( ). Pero también podemos escribi r java Cleanser para invocar a Cleanser.main( ), aún cuando Cleanser no sea una clase pública. Incluso aunque una clase tenga acceso de paquete. si el método main() es público, también se podrá acceder a él. Aquí. podemos ver que Detergcnt.main() llama a C lea nser.main ( ) explícitamente, pasándole los mismos argumentos de la línea de comandos (sin embargo. podríamos pasarle cualquier matriz de objetos String). Es importante que todos los métodos de C leanser sean públicos. Recuerde que, si no se indica ningún especificador de acceso. los miembros adoptarán de f0n11a predetenninada el acceso de paquete, lo que sólo pennite el acceso a los otros miembros del paquete. Por tanto. dentro de este paquete. cualquiera podría usar esos métodos si no hubiera ningún especificador de acceso. Detergent. por ejemplo. no tendría ningún problema. Sin embargo. si alguna c lase de algún otro paquete fuera a heredar de Cleanser, sólo podría acceder a los miembros de tipo public. Así que, para permitir la herencia, como regla general deberemos definir todos los campos eomo private y todos los métodos como public (los miembros de tipo protecled también pemliten el acceso por parte de las clases derivadas; analizaremos este tema más adelante). Por supuesto. en algunos casos particulares será necesario hacer excepciones. pero esta directriz suele resultar bastante útil. Cleanser dispone de una serie de métodos en su interraz: append( ). dilute( ). apply( ). serub( ) y toString( ). Puesto que Detergent deril'a de Cleanser (gracias a la palabra clave extends). automáticamente obtendrá todos estos métodos como parte de su interfaz, aún cuando no los veamos explícitamente definidos en Detergent. Por tanto. podemos considerar la herencia como un modo de reutilizar la clase. Como podemos ver en scrub( ), es posible tomar un método que haya sido definido en la clase base y modificarlo. En este caso, puede también que queramos invocar el método de la clase base desde dentro de la nueva versión. Pero dentro de scrub( ), no podemos simplemente invocar a scrub( ). ya que eso produciría una llamada recursiva. que no es exactamente 10 que queremos. Para resolver este problema, la palabra clave super de Java hace referencia a la "supcrclase" de la que ha heredado la clase actual. Por tanto. la expresión supcr.scrub( ) invoca a la versión de la c lase base del método scrub( ). Cuando se hereda, no estamos limirados a utilizar los métodos de la clase base. También podemos añadir nuevos métodos a la clase derivada, de la misma forma que los añadiríamos a otra clase: definiéndolos. El método foam( ) es un ejemplo de esto. En Detergent.main( ) podemos ver que. para un objeto Detergent, podemos invocar todos los métodos disponibles en CIeanseO' osi C0l110 en Detergent (es decir, foam()). Ejemplo 2:
(2) Cree una nueva clase que herede de la clase Detergent. Sustinlya el método scrub() y añada un nuevo método denominado stcrilize( ).
Inicialización de la clase base Puesto que ahora tenemos dos clases (la clase base y la clase derivada) en lugar de una, puede resu ltar un poco confuso tratar de imaginar cuál es el objeto resultante generado por una clase derivada. Desde fuera, parece como si la nueva clase tuviera la misma interfaz que la clase base y. quizá algunos métodos y campos adicionales. Pero el mecan ismo de herencia no se limita a copiar la interfaz de la clase base. Cuando se crea un objeto de la clase derivada, éste contiene en su interior un subobjeto de la clase base. Este subobjeto es idéntico a 10 que tendríamos si hubiéramos creado directamente un objeto de la clase base. Lo que sucede, simplemente. es que. visto desde el exterior, el subobjeto de la clase base está envuelto por el objeto de la clase derivada. Por supuesto. es esencial que el subobjeto de la clase base se inicialice correctamente, y sólo hay una forma de garantizar esto: realizar la inicialización en e l cons tructor invocando al constructor de la clase base, que es quien tiene todos los conocimientos y todos los privilegios para llevar a cabo adecuadamente la inicialización de la clase base. Java inserta automáti camente llamadas al constructor de la clase base dentro del constructor de la clase derivada. El siguiente ejem plo mu estra este mecanismo en acción con tres niveles de herencia:
11 : reusing / Cartoon.java 11 Llamadas a constructores
durante la herencia.
144 Piensa en Java import static net.mindview.util.Print.*¡ class Art Art () { print ( "Art constructor" ) ; }
class Drawing extends Art { Drawing () { print ( "Drawing constructor ti
)
i
}
public class Cartoon extends Drawing { public Cartoon () { print ( "Cartoon constructor" ) i public static void main (String[] args ) { Cartoon x = new Cartoon () i
1* Output: Art constructor Drawing constructor Cartoon constructor */1/,Como puede ver, la construcción tiene lugar desde la base hacia "afuera", por lo que la clase base se inicializa antes de que los constructores de la clase derivada puedan acceder a ella. Incluso aunque no creáramos un constructor para Cartoon() , el compilador sintetizaría un constructor predeternlinado que in vocaría al constructor de la clase base.
Ejercicio 3:
(2) Demuestre la afinnación anterior.
Ejercicio 4:
(2) Demuestre que los constructores de la clase base (a) se in vocan siempre y (b) se invocan antes que los constructores de la clase deri vada.
Ejercicio 5:
(1) Cree dos clases, A y B, con constructores predeterminados (listas de argum ent os vacias) que impriman un mensaje infornlando de la construcción de cada objeto. Cree una nueva clase llamada e que herede de A, y cree un miembro de la clase B dentro de C. No cree un constructor para C. Cree un obj eto de la clase e y observe los resu ltados.
Constructores con argumentos El ejemplo anterior tiene constructores predetenn inados; es decir, que no tienen argumentos. Resulta fáci l para el compilador invocar estos constructores, porque no existe ninguna duda acerca de qué argumen tos hay que pasar. Si no existe un constructor predetenninado en la clase base. o si se quiere in vocar un constructor de la clase base que tenga argumentos, será necesario escri bir explícitamente una llamada al constmctor de la clase base uti lizando la palabra clave super y la lista de argumentos apropiada:
11: reusing/Chess.java II Herencia, constructores y argumentos. import static net.mindview.util.Print.*; class Game { Game {int i) print ( "Game constructor" ) i
class BoardGame extends Game { BoardGame (int i) { super (i ) ; print ( "BoardGame constructor" ) i
public class Chess extends BoardGame {
7 Reutilización de clases 145 Chess (1
{ super(ll) ;
print ( "Chess constructor lt ) ; public stacic void main(String(] Chess x = new Chess()¡
args)
{
/ * Output : Game constructor BoardGame constructor Chess constructor * /// > Si no se invoca el constructor de la clase base en BoardGame( ), el compilador se quejará de que no puede localizar un constructor de la fonna Ga me( ). Además. la llamada al constructor de la clase base debe ser la primera cosa que se haga en el constructor de la clase derivada (el compilador se encargará de recordárselo si se olvida de ello). Ejercicio 6 :
(1) Utilizando Chess.java. demuestre las afimlaciones del párrafo anterior.
Ejercicio 7 :
(1) Modifique el Ejercicio 5 de modo que A y B tengan constructores con argumentos en lugar de constTuctores predetenninados. Escriba un constructor para e que realice toda la inicialización dentro del constructor de C.
Ejercic io 8 :
(1) Cree una clase base que sólo tenga un constructor no predeterminado y una clase derivada que tenga un constructor predetenninado (sin argumentos) y otro no predetemlinado, En los constmctores de la clase derivada, invoque al constructor de la clase base,
Ejercic io 9 :
(2) Cree Wla clase denominada Root que contenga una instancia de cada una de las siguientes clases (que también deberá crear): Componentl, Component2 y Component3. Derive una clase Stem de Root que también contenga uoa instancia de cada "componente", Todas las clases deben tener constructores predeterminados que impriman un mensaje relativo a la clase,
Ejercic io 10:
(1) Modifique el ejercicio anterior de modo que cada clase sólo tenga constructores no predetenninados.
Delegación Una tercera relación, que no está directamente soportada en Java, se denomina delegación. Se eocuentra a caballo entre la herencia y la composición, por que lo que hacemos es incluir un objeto miembro en la clase que estemos construyendo (como en la composición), pero al mismo tiempo exponemos todos los métodos del objeto miembro en nuestra nueva clase (como en la herencia). Por ejemplo, una nave espacial (spaceship) necesita un módulo de control:
11:
reusing/SpaceShipControls . java
public void void void void void void void
class SpaceShipControls up(int ve1ocity) () down (int velocity) {} 1eft (int ve10cityl {) right (int velocity) {} forward (i nt velocity) {} back (int veloci ty ) {} turboBoost () {}
/// 0-
Una forma de conslruir la nave espacial consistiría en emplear el mecanismo de herencia:
/1 :
reusing /SpaceShip .java
public class SpaceShip extends SpaceShipControls { private String name; public SpaceShip(String name ) ( this.name = name; public String toString{) { return name; }
146 Piensa en Java pUblic static void main(String [] args) { SpaceShip protector = new SpaceShip ("NSEA Protector" proteceor.forward(lOO) ;
Sin embargo. un objeto SpaceShip no es realmcnle "un tipo de" objeto SpaccShipControls. aún cuando podamos, por ejemplo. "deeirle" al objeto SpaceShip que avance hacia adelante (forward ( )). Resu lta más preciso decir que la na ve espacial (el objeto SpaceShip) cOl/liel/e un módulo de control (objeto SpaceShipControls), y que, al mismo tiempo. todos los métodos de SpaceShipControls deben quedar expuestos en el objeto SpaceShip. El mecanismo de delegació n pemlite resolver este dilema: jj : reusingjSpaceShipDelegation.java
public class SpaceShipDelegation { private Sering name; private SpaceShipConerols controls new SpaceShipControls () ; public SpaceShipDelegation (S tring name) this.name = name; jj Métodos delegados: public void back (i nt velocity ) controls .ba c k (velocit y ) ;
public void down(int velocity ) controls.down(velocity) ; public void forward (int velocity) controls .forward (ve l ocity) ; public void left ( int velocity ) controls.left(velocity) ; public void right(int velocity ) controls.right (v elocity ) ; public void turboBoost() controls .turboBoost () ; public void up(int velocity ) controls.up(velocity ) ; public static void main (String (] args ) { SpaceShipDelegation protector = new SpaceShipDelegation ( "NSEA Protector" ) ; protector.forward(lOO) ;
Como puede ver los métodos se redirigen hacia el método controls subyacente, y la interfaz es, por tanto, la misma que con la herencia. Sin embargo, tenemos más control con la delegación, ya que podemos decidir proporcionar únicamente un subconjunto de los métodos contenidos en el objeto miembro. Aunque el lenguaje Java no soporta directamente la delegación, las herramientas de desarrollo sí que suelen hacerlo. Por ejemplo, el ejemplo anterior se ha ge nerado automáti camente utili za ndo el entorno integrado de desarrollo JetBrains Idea. Ejercicio 11:
(3) Modifique Detergent.java de modo que utilice el mecanismo de delegación .
7 Reutilización de clases 147
Combinación de la composición y de la herencia Resulta bastante común utilizar los mecanismos de composición y de herencia conjuntamente. El siguiente ejemplo muestra la creación de una clase más compleja utilizando tanto la herencia como la composic ión, junto con la necesaria inicialización mediante los constructores:
/1: reusing!PlaceSetting.java I Combinación de la composición
y la herencia. import static net.mindview.util.Print.*¡
class Plate { Plate (int i)
print ("Plate constructor")
i
class DinnerPlate extends Plate { DinnerPlate (int i) { super(i) ; print ("DinnerPlate constructor");
class Utensil ( Utensil(int i) print ("Utensil constructor");
class Spoon extends Utens il { Spoon{int i) { super (i) ¡ print ("Spoon constructor " ) ¡
class Fork extends Utensil { Fork lint il { super(i)¡ print ( " Fork constructor " ) i
class Knife extends Utensil { Knife (int i) { super(i) ¡ print ("Knife constructo r" )
i
// Una forma cultural de hacer algo: class Custom { Custom ( int i) { print ( "Custom constructor!! ) i
publ ic class PlaceSetting extends Custom { private Spoon SPi
148 Piensa en Java private Fork frk¡ private Knife kn¡ private DinnerP late pI; public PlaceSetting(int i) super (i + 1); sp = new Spoon(i + 2); frk = new Fork(i + 3}; kn = new Knife(i + 4); pI = new DinnerPlate{i + 5); print (" PlaceSetting constructor"); public static void main(String[] args) PlaceSetting x = new PlaceSetting(9);
/ * Output : Custom cons tructor Utensil constructor Spoon constructor Utensil constructor Fork constructor Utensil constructor Knife constructor PI ate constructor DinnerPlate constructor PlaceSetting constructor * /// , Aunque el compi lador nos obliga a inicializar la clase base y requiere que lo hagamos al principio del constructor. no se asegura de que inicialicemos los objetos miembro, así que es preciso prestar atención a este detalle. Resulta asombrosa la fonna tan limpia en que las clases quedan separadas. No es necesario siquiera disponer del código fuente de los métodos para poder reut ilizar ese código. Como mucho, nos basta con limitamos a importar un paquete (esto es cierto tanto para la herencia como para la composición).
Cómo garantizar una limpieza apropiada Java no tiene el concepto C++ de destructor, que es un método que se invoca automáticamente cuando se destruye un objeto. La razón es, probablemente, que en Java la práctica habitual es olvidarse de los objetos en lugar de destruirlos, pennitiendo al depurador de memoria reclama r la memoria cuando sea necesario. A menudo, esto resulta suficiente, pero hay veces en que la clase puede realizar determinadas actividades durante su tiempo de vida que obligan a realizar la limpieza. Como hemos mencionado en el Capítulo 5, Inicialización y limpieza, no podemos saber cuándo va a ser invocado el depurador de memoria, o ni siquiera si va a ser invocado. Por tanto, si queremos limpiar algo concreto relacionado con una clase, deberemos escribir explicitamente un método especial para hacerlo yaseguramos de que el programador de clientes sepa que debe invocar dicho método. Además, como se descri be en el Capítulo 12, Tratamiento de errores con excepciones, debemos protegemos frente a la aceleración de posibles excepciones incorporando dicha actividad de limpieza en una cláusula finall y. Considere un ejemplo de un sistema de diseño asistido por computadora que dibuje imágenes en la pantalla:
JJ : reusingJCADSystem.java
JI Cómo garantizar una limpieza adecuada. package reusing¡ import static net.mindview.util.Print.*¡ class Shape { Shape{int i) print (IIShape constructor") ¡ void dispose () { print ("Shape dispose H ) i }
7 Reutilización de clases 149
class Circle extends Shape { Circle (int i) super (i) ;
{
print{UDrawing Circle")
i
} void dispose () { print ("Erasing Circle"); super.dispose(} ;
class Triangle extends Shape { Trianglelint il ( super (i) ; print ( "Drawing Triangle 11)
i
} void dispose () { print (" Erasing Triangle");
super.dispose() ;
class Line extends Shape { private int start, end;
Line(int start, int end} super (start) j this.6tart = start¡ this.end = end¡ print ("Drawing Line: void dispose () { print ("Erasing tine:
super.dispose()
+ start +
" + end);
" + start +
" + end);
11
i
public class CADSystem extends Shape { private Circle C¡ private Triangle ti private Line [] lines = new Line [3) ; public CADSystem(int i) { super (i + 1); for{int j = O; j < lines.length; j++)
lines [j] = new Line{j j*j) i e = new Circle(1); t = new Triangle{1); print ("Combined constructor"); I
public void dispose () { print ("CADSystem . dispose () " } ; II El orden de limpieza es el inverso II al orden de inicialización: t.dispose() ; c.dispose() ; for(int i = lines.length - 1; i >= O; i--) lines[i] .dispose( ); super.dispose() ; public static void main (String [] args)
{
1SO Piensa en Java CADSystem x
new CADSystem(47);
try { /1 Código y tratamiento de excepciones. finally {
x.dispose{) ;
/* Output: Shape constructor Shape constructor Drawing Line: 0, O Shape constructor Drawing Line: 1, 1 Shape constructor Drawing Line:
2,
4
Shape constructor Drawing Circle
Shape constructor Drawing Triangle Combined constructor CADSystem.dispose() Erasing Triangle Shape dispose Erasing Circle Shape dispose Erasing Line: 2, 4 Shape dispose Erasing Line: 1, 1 Shape dispose Erasing Line: O, O Shape dispose Shape dispose
*///,Todo en este sistema es algún tipo de objeto Shape (que a su vez es un tipo de Object, puesto que hereda implícitamente de la clase raíz). Cada clase sustituye el método dispose( ) de Shape, además de invocar la versión de dicho método de la clase base utilizando super. Las clases Shape específicas, Circle, Triangle y Une, tienen constructores que "dibujan" las formas geométricas correspondientes, aunque cualquier método que se invoque durante la vida del objeto puede llegar a ser responsable de hacer algo que luego requiera una cierta tarea de limpieza. Cada clase tiene su propio método dispose() para restaurar todas esas cosas que no están relacionadas con la memoria y dejarlas en el estado en que estaban antes de que el objeto se creara. En maine ), hay dos palabras clave que no habíamos visto antes y que no va n a explicarse en delalle hasta el Capitulo le. Traramiento de errores mediante excepciones: try y finally . La palabra clave try indica que el bloque situado a continuación suyo (delimitado por llaves) es una región protegida, lo que quiere decir que se la otorga un tratamiento especial. Uno de estos tratamientos especiales consiste en que el código de la cláusula finally sihlada a continuación de esta región protegida siempre se ejecuta, independientemente de cómo se salga de bloque try (con el tratamiento de excepciones, es posible salir de un bloque try de di versas formas distintas de la normal). Aquí, la cláusula finally dice: "Llama siempre a dispose() para x, independientemente de lo que suceda". En el método de limpieza, (dispose(), en este caso), también hay que prestar atención al orden de Llamada de los métodos de limpieza de la clase base y de los objetos miembro, en caso de que un subobjelo dependa de otro. En general, hay que seguir la misma forma que imponen los compiladores de C++ para los destructores: primero hay que realizar toda la tarea de limpieza específica de la clase, en orden inverso a su creación (en generaL esto requiere que los elementos de la clase base sigan siendo viables). A continuación, se invoca el método de limpieza de la clase base, C0l110 se ilustra en el ejemplo. Hay muchos casos en los que el tema de la limpieza no constituye un problema, bastando con dejar que el depurador de memoria realice su tarea. Pero cuando hay que llevar a cabo una limpieza explícita se requieren grandes dosis de diligencia y atención, porque el depurador de memoria no sirve de gran ayuda en este aspecto. El depurador de memoria puede que no
7 Reutilización de ctases 151
llegue nunca a ser invocado y, en caso de que lo sea, podría reclamar los objetos en el orden que quisiera. No podemos confiar en la depuración de memoria para nada que no sea reclamar las zonas de memoria no utilizadas. Si queremos que las tareas de limpieza se lleven a cabo. es necesario definir nuestros propios métodos de limpieza y no emplear finalize() . Ejercicio 12:
(3) Aliada una jerarquía adecuada de métodos dispose() a todas las clases del Ejercicio 9.
Ocultación de nombres Si una clave base de Java tiene un nombre de método varias veces sobrecargado, redefinir dicho nombre de método en la clase derivada no ocultará ninguna de las versiones de la clase base (a diferencia de lo que sucede en e++). Por tanto, el mecanis mo de sobrecarga funci ona independientemente de si el método ha sido definido en este nivelo en tina clase base: // : reusing / Hide . java // La sobrecarga de un nombre de método de la clase base en una II clase derivada no oculta las versiones de la clase base. i mport static net.mindview . util.Print .* ; c lass Homer { char doh (char c) { print ("doh (char) ") ; return 'd';
float doh l float f l ( print ( "doh (float ) " ) ; return 1. Of;
class Milhouse {} class Bart extends Homer { void doh (Milhouse m) { print ( "doh (Milhouse ) " ) ;
public class Hi de { public sta t ic void main (Str i ng [) args) Bart b = new Bart {) i b . dohlll; b.doh( ' x '); b . doh l l.Of l ; b . doh (new Milhouse ()) i
{
1* Output : dohlfloa tl dOh(c har) doh Ifloa t I doh( Milhouse) */11 , Podemos ver que todos los métodos sobrecargados de Homer están disponibles en Bart. aunque Bart introduce un nuevo método sobrecargado (si se hiciera esto en C++ se ocultarían los métodos de la clase base). Como veremos en el siguiente capitulo, lo más común es sobrecargar los métodos del mismo nombre, utili zando exactamente la misma signatura y el mismo tipo de retomo de la clase de retomo. En caso contrario, el código podría resultar confuso (lo cual es la razón por la que C-++ oculta todos los métodos de la clase base. para que no cometamos lo que muy probablemente se trata de un error). Java SES ha añadido al lenguaje la anotación @Override, que no es una palabra clave pero puede usarse como si lo fuera. Cuando queremos sustituir un método, podemos añadir esta anotación y el compilador generará un mensaje de error si sobreca rgamos accidentalmente e l método en lugar de sustiruirlo:
152 Piensa en Java
JI : reusing / Lisa . java //
{CompileTimeError}
(Won'
t
compile )
class Lisa extends Homer { @Override void doh (Milhouse m) { System.out.println("doh (Milhouse J ");
}
///,El marcador {CompileTimeError} excluye el archivo del proceso de construcción con Ant de este libro, pero si lo compila a mano podrá ver el mensaje de error: methad does not override a methad fraID its superclass
La anotación @Override evitará, así, que sobrecarguemos accidentalmente un método cuando no es eso lo que queremos
hacer. Ejerc ici o 13:
(2) Cree una clase con un método que esté sobrecargado tres veces. Defina una nueva clase que herede de la anterior y añada una nueva versión sobrecargada del método. Muestre que los cuatro métodos están disponibles en la clase derivada.
Cómo elegir entre la composición y la herencia Tanto la composición como la herencia nos permiten incluir subobjetos dentro de una nueva clase (la composición lo hace de forma explícita, mientras que en el caso de la herencia esto se hace de fonna implícita). Puede que se esté preguntando cuál es la diferencia enrTe ambos mecan ismos y cuándo convi ene elegir entre uno y otro. Generalmente, la composición se usa cuando se desea incorporar la funcionalidad de la clase existente dentro de la nueya clase pero no su interfaz. En otras palabras, lo que hacemos es integrar un objeto para poderlo utilizar con el fm de poder implementar ciertas características en la nueva clase, pero el usuario de la nueva clase verá la interfaz que hayamos definido para la nueva clase en lugar de la interfaz de l objeto incrustado. Para conseguir este efecto, lo que hacemos es integrar objetos private de clases existentes dentro de la nueva clase. Algunas veces, tiene sentido permitir que el usuario de la clase acceda directamente a la composición de esa nueva clase. es decir. hacer que los objetos miembros sean públicos. Los objetos miembros utilizan la técnica de la ocultación de la implementación por sí mismos, así que no existe ningún riesgo. Cuando el usuario sabe que estamos ensamblando un conjunto de elementos. normalmente, puede comprender mejor la interfaz que hayamos definido. Un ejemplo seria un objeto car (cocbe):
JI: reusingfCar.java II Composición con objetos públicos. class Engine { public void start () {} public void rey 11 {) public void stop 11 {)
class Wheel { public void inflate (int psi ) {}
class Window { public void rollup 11 {) public void rolldown ( ) {)
class Door { public Window window = new Window()¡ public void open 11
{)
7 Reutilización de clases 153 public void elose (1
{)
public class Car { new Engine () ; public Engine engine public Wheel[) wheel new Wheel [4) ; public Door Ieft = new Door {) , right = new Door(); // 2-door public Car () { for (int i = Oi i < 4; i++ ) wheel [i] = new Wheel () ; public static void main{String[] Car car = new Car () ;
args ) {
car.left.window.rollup() ; car.wheel [O) .inflate (72) ;
Puesto que en este caso la composición de un coche fonna parte del análisis del problema (y no simplemente del diseño subyacente), hacer los miembros públicos ayuda al programador de clientes a entender cómo utilizar la clase, y disminuye también la complejidad del código que el creador de la clase tiene que desarrollar. Sin embargo, tenga en cuenta que éste es un caso especial y que, en general, los campos deberían ser privados. Cuando recurrimos almcc3nismos de herencia, lo que hacemos es tomar una clase existente y definir una ve rsión especial de la misma. En general, esto quiere decir que estaremos tomando una clase de propósito general y especial izándola para una necesidad concreta. Si reflexionamos un poco acerca de ello, podremos ver que no tendría ningún sentido componer un coche utilizando un objeto vehícu lo. ya que un coche no contiene vehícu lo, sino que es un vehículo. La relación eS-1II1 se expresa mediante la herencia, mientras que la relación tiene-un se expresa mediante la composición. Ejercicio 14:
(1) En Carojava añada un método service() a Engine e invoque este método desde main().
protected Ahora que ya hemos tomado contacto con la herencia. vemos que la palabra clave protected adquiere su pleno significado. En un mundo ideal, la palabra clave private resuharía suficiente, pero en los proyectos reales, hay veces en las que queremos ocultar algo a ojos del mundo pero pennitir que accedan a ese algo los miembros de las clases derivadas. La palabra clave protected es homenaje al pragmatismo, lo que dice es: "Este elemento es privado en lo que respecta al usuario de la clase, pero está disponible para cualquiera que herede de esta clase y para todo lo demás que se encuentre en el mismo paquete (protccted también proporciona acceso de paquete)"o. Aunque es posible crear campos de tipo protected (protegidos), lo mejor es definir los campos como privados; esto nos permitirá conservar siempre el derecho a modificar la implementación subyacente. Entonces, podemos permitir que los herederos de nuestra clase dispongan de un método controlado utilizando métodos protected :
JI: reusingfOrc.java JI La palabra clave protected. import static net.mindview.util.Print.*¡ clas s Villain private String name; protected void set (String nm ) { name = nm; } public Villain(String name ) { this.name = name; public String toString () { return "I'm a Villain and my name is " + name;
154 Piensa en Java public class Ore extends Villain { private int orcNumber; public Ore (String name, int orcNumber) super (name) ;
this.orcNumber = orcNumber; public void change (String name, int orcNumber) { set(name); /1 Disponible porque está protegido. this.orcNumber
=
orcNumber;
public String toString () { return "Ore " + orcNumber + ":
" + super. toString () ;
public static void main(String[] args) Ore ore,: new Orc{"Limburger", 12) i print (ore) i ore . change ("Bob", 19); print (ore) ;
1* Out p ut : Or e 1 2: l' m a Vi l lain and my name i s Li mburger Or e 19 : l ' m a Vi l la i n a n d my n a me i 5 Bob * /// , Puede ver que ehange() tiene a cceso a sel() porque es de tipo proleeled. Observe también la forma en que se ha definido el método loSlring() de O re en términos de loSlring() de la clase base. Ejercicio 15:
(2) Cree una clase denlro de un paquete. Esa clase debe estar dentro de un paquete. Esa clase debe contener un método protected . Fuera del paquete, trate de invocar el método protected y explique los resultados. Ahora defina otra clase que herede de la anterio r e in voque el método prolected desde un método de la clase derivada.
Upcasting (generalización) El aspecto más importante de la herencia no es que proporciona métodos para la nueva clase, sino la relación expresada entre la nueva clase y la clase base. Esta relación puede resumirse dic iendo que " la nueva clase es un lipo de la clase existente". Esta descripción no es simplemente una foona elegante de explicar la herencia, sino que está soportada directamente por el lenguaje. Por ejemplo, considere una clase base denominada [nstrument que represente instrumentos musicales y una clase derivada denominada Wind (instrumentos de viento). Puesto que la herencia garanti za que todos los métodos de la clase base estén disponibles también en la clase derivada, cualquier mensaje que enviemos a la clase base puede enviarse también a la clase derivada. Si la clase Inslrumenl tiene un método play() (tocar el instrumento), también lo tendrán los instrum entos de la clase Wind . Esto significa que podemos dec ir con propiedad que un objeto Wind es también un objeto de tipo Inslrumon!. El siguiente ejemplo ilustra cómo soporta el compilador esta idea.
11 : reusing / Wind.java II Herencia y generalización. class Instrument { public void play () {} static void tune (Instrument i ) {
/ / ... i. play () ;
II Los objetos instrumentos de viento II porque tienen la misma interfaz:
7 Reutilización de clases 155 public class Wind extends I nstrument ( public static void ma i n (String[] args )
Wind f lute = ne w Wi nd( ) ¡ Instr umen t. tune ( f lute l ; /1 Gene r ali zación
}
;;; ,Lo más interesante de este ejemplo es el método tune( ) (afi nar), que acepta una referencia a un objeto Instrument. Sin embargo, en Wind.main() al método tune( ) se le entrega una referencia a un objeto Wind. Dado que Java es muy estricto en lo que respecta a las comprobaciones de tipos, parece extraño que un método que acepta un detenninado tipo pueda aceptar también otro tipo distinto, hasta que nos demos cuenta de que un objeto Wind también es un objeto Instrument y de que no ex iste ningún método que tune() pudiera invocar para un objeto Instrumeot y que no se encuentre también en Wind . Dentro de tune(), el código funciona tanto para los objetos Instrument como para cualquier otra cosa derivada de Instrument, y el acto de convertir una referencia a un objeto Wind en otra referencia a Instrument se denomina upcasling (generalización).
¿Por qué generalizar? El ténnino está basado en la fonna en que se vienen dibujando tradicionalmente los diagramas de herencia de clase. Con la raíz en la parte superi or de la página y las clases derivadas distribuyéndose hacia abajo (por supuesto, podríamos dibujar los diagramas de cualquier otra manera que nos resultara útil), El diagrama de herencia para Wind.java será entonces:
Al reali zar una proyección de un tipo derivado al tipo base, nos movemos hacia arriba en el diagrama de herencia, y esa es la razón de que en inglés se utilice el término upcasling (up = arriba, casI = proyección. El upcas ling o generalización siempre resulta seguro, porque estamos pasando de un tipo más específico a otro más general. Es decir, la clase derivada es UD superconjunto de la clase base. Puede que la clase derivada contenga más métodos que la clase base, pero debe contener al menos los métodos de la clase base. Lo único que puede ocurrir con la interfaz de la clase durante la generalización es que pierda métodos, no que los gane, y ésta es la razón por la que el compilador pennite la generali zación sin efectuar ningún tipo de proyección explícita y sin emplear ninguna notación especial. También podemos realizar el inverso de la generali zación, que se denomina downcasling (especialización), pero esto lleva asociado un cierto dilema que examinaremos más en detalle en el siguiente capítulo, y en el Capítulo 14, Información de tipos.
Nueva comparación entre la composición y la herencia En la programación orientada a objetos, la fonna más habitual de crear y utilizar código consiste en empaquetar los datos y métodos en una clase y usar los objetos de dicha clase. También utilizamos otras clases existentes para construir nuevas ciases utilizando el mecanismo de composición. Menos frecuentemente, debemos utilizar el mecanismo de herencia. Por tanto, aunque al enseñar programación orientada a objetos se suele hacer un gran hincapié en el tema de la herencia, eso no quiere decir que se la deba usar en todo momento. Por el contrario, conviene emplearla con mesura, y sólo cuando esté claro que la herencia resulta útil. Una de las fonnas más claras de detenninar si debe utilizarse composición o herencia consiste en preguntarse si va a ser necesario recurrir en algún momento al mecanismo de generalización de la nueva clase a la clase base. Si es necesario usar dicho mecanismo, entonces la herencia será necesaria, pero si ese mecanismo no hace falta conviene meditar si verdaderamente hay que emplear la herencia. En el Capítulo 8, Polimotjisl/lo se proporciona una de las razones más importantes para utilizar la generalización, pero si se acuerda de preguntarse u¿ vaya necesitar generalizar en algún momento?" tendrá una buena fonna de optar entre la composición y la herencia. Ejercicío 16:
(2) Cree una clase denominada Amphibian (anfibio). A partir de ésta, defina una nueva clase den ominada Frog (rana) que herede de la anterior. Incluya una seríe de métodos apropiados en la clase base. En
156 Piensa en Java
main( ). cree un objeto Frog y realice una generalización a Amphibian, demostrando que lodos los métodos siguen funcionando. Ejercicio 17:
(1) Modifique el Ejercicio 16 para que el objeto Frog sustituya las definiciones de métodos de la clase base (propo rcione las nuevas definiciones utilizando las mismas signaturas de métodos). Observe 10 que sucede en main( ).
La palabra clave final La palabra clave de Java final tiene significados ligeramente diferentes dependiendo del contexto, pero en general quiere decir: "'Este elemento no puede modifi carse". Puede haber dos razones para que no queramos pennitir los cambios: diseño y eficiencia. Puesto que estas dos razones son muy diferentes entre sí, resulta bastante posible utilizar la palabra clave final de manera inadecuada. En las siguient es secciones vamos a ver los tres lugares donde final puede utilizarse: para los datos, para los métodos y para las clases.
Datos final Muchos lenguajes de programación disponen de alguna fonna de comunicarle al compilador que un elemento de datos es "constante". Las constantes son útiles por dos razones: 1. Puede tratarse de una constante de tiempo de compilación que nunca va a cambiar.
2. Puede tratarse de un valor inicializado en tiempo de ejecución que no queremos que cambie. En el caso de una constante de tiempo de compilación, el compilador está autorizado a "compactar" el va lor constante en todos aquellos cálculos que se le utilice; es decir, el cálculo puede realizarse en tiempo de compilación eliminando así ciertos cálculos en tiempo de ejecución. En Java, estos tipos de constantes deben ser primitivas y se expresan con la palabra clave final . En el momento de definir una de tales constantes, es preciso definir un valor. Un campo que sea a la vez static y final sólo tendrá una zona de almacenamiento que no puede nunca ser modificada. Cuando final se utiliza con referencias a objetos en lugar de con primitivas, el significado puede ser confuso. Con una primitiva, final hace que el valor sea constante, pero con una referencia a objeto lo que final hace constante es la referencia. Una vez inicializada la referencia a un objeto, nunca se la puede cambiar para que apunte a otro objeto. Sin embargo, el propio objeto sí que puede ser modificado; Java no proporciona ninguna manera para hacer que el objeto arbitrario sea constante (podemos, sin embargo, escribir nuestras clases de modo que tengan el efecto de que los objetos sean constantes). Esta restricción incluye a las matrices, que son también objetos. He ?quí un ejemplo donde se ilustra el uso de los campos final . Observe que, por convenio, los campos que son a la vez static y final (es decir, constantes de tiempo de compilación) se escriben en mayúsculas. utilizando guiones bajos para separar las palabras.
11: reusing/FinalData.java II Efecto de final sobre los campos. import java.util.*; import static net.mindview.util.Print.*; class Value { int i; II Acceso de paquete public Value(int i) {th is.i
i;
public class FinalData { private static Random rand = new Random(47)¡ private String id; public FinalData (String id) { this. id = id¡ } II Pueden ser constantes de tiempo de compilación: private final int valueOne = 9;
7 Reutilización de clases 157 private static final int VALUE_TWO = 99; /1 Constante pública tipica: public static final int VALUE_THREE = 39; JI No pueden ser constantes de tiempo de compilación: private final int i4 = rand.nextlnt(20); static final int INT_S = rand.nextlnt(20l i private Value vI = new Value(ll) i private final Value v2 = new Value(22)¡ private static final Value VAL_3 = new Value(33) i 1/ Matrices: private final int[] a = { 1, 2, 3, 4, 5, 6 }i public String toString () { return id + lO: !I + "i4 = " + i4 + ", INT_S "+ INT_5;
public static vold main (String [] args)
{
FinalData fd! = new FinalData("fdl"}; jI! fdl.valueOne++i /1 Error: no se puede modificar el valor fdl.v2.i++; /1 ¡El objeto no es constante! fdl.vl = new Value(9); II OK ~ - no es final for (int i =- O; i < fdl. a .length; i++) fdl.a[iJ++; II ¡El objeto no es constante! II! fdl.v2 = new Value(O); II Error: no se puede II! fdl.VAL_3 = new Value(l); II cambiar la referencia II! fdl.a =- new int [3] ; print (fd1) ; print ("Creating new FinalData"); FinalData fd2 new FinalData (" fd2") ; print (fd1) ; print (fd2) ;
1* Output: fd1, i4 = 15, INT 5 = 18 Creating new FinalData fd1, i4 15, INT_5 18 fd2, i4 = 13, INT 5 = 18
*111 ,Dado que va lueOne y VALUE_TWO son primitivas final con valores definidos en tiempo de compilación, ambas pueden usarse como constantes de tiempo de compilación y no se diferencian en ningún aspecto importante. VALUE_THREE es la fonna más típica en que podrá ver definidas dichas constantes: public que se pueden usar fuera del paquete, static para enfatiza r que sólo hay uo a y final para decir que se trata de una constante. Observe que las primitivas final static con valores iniciales constantes (es decir, constantes en tiempo de compilación) se designan con letras mayúsculas por convenio, separando las palabras mediante guiones bajos (al igual que las constantes en C, que es el lenguaje en el que surgió este convenio). El que algo sea final no implica necesariamente el que su va lor se conozca en ti empo de compilación. El ejemplo ilustra está inicializando i4 e lNT_5 en tiempo de ejecución, mediante números generados aleatoriamente. Esta parte del ejemplo también genera la diferencia entre hacer un va lor final estát ico o no estático. Esta diferencia sólo se hace patente cuando los valores se inicializan en tiempo de ejecuci ón, ya que el compilador trata de la misma manera los valores de tiempo de compilación (en muchas ocasiones, optimizando el códi go para eliminar esas constan tes). La diferencia se muestra cuando se ejecuta el programa. Observe que los valores de i4 para fd1 y fd2 son distintos, mientras que el valor para INT_5 no cambia porq ue creemos el segu ndo objeto FinalData. Esto se debe a que es estát ico y se inicializa una sola vez durante la carga y no cada vez que se crea un nuevo objeto. Las vari ables v1 a VAL_3 ilustran el significado de una referencia final . Como puede ver en main( ), el hecho de que v2 sea final no quiere decir que se pueda modificar su valor. Puesto que es una referencia, final significa que no se puede asociar v2 COIl un nuevo objeto. Podemos ver que la afinnación también es cierta para las matrices, que son otro de tipo de referencia (no hay nin guna fom1a que yo conozca de hacer que las referencias a una matri z sean final). Hacer las referencias final parece menos úti l que definir las primitivas como final.
158 Piensa en Java
Ejercicio 18:
(2) Cree una clase con un campo static final y un campo final y demuestre la diferencia entre los dos.
Valores final en blanco Java pennite la creación de valores finales en blanco, que son campos que se declaran como final pero que no se les pro~ porciona un valo r de inicialización. En todos los casos, el valor final en blanco debe ser inicializado antes de utilizarlo, y el compilador se encargará de hacer que esto sea asÍ. Sin embargo, los valores final en blanco proporcionan mucha más fle· xibilidad en el uso de la palabra clave final ya que, por ejemplo, un campo final dentro de una clase podrá con esto ser diferente para cada objeto y mantener aún así su carácter de inmutable. He aquí un ejemplo:
JI : reusing / BlankFi na l . j ava
JI
Campos final "en blanco".
cIass Poppet {
private int i¡ ii;
Poppet (int ii I { i
}
public cIass BlankFinal { private final int i = Di 1/ Valor final inicializado private final int j i II Va l or final en blanco private final Poppet Pi II Referencia final en blanco II Los valores final en blanco DEBEN inicializarse en el constructor: publie BlankFinal () { j 1i II Inicializar valor final en blanco
p
=
new Poppet (l ) ;
II
Inicializar referencia final en blanco
public BlankFinal {int x ) j Xi II Inicializar valor final en blanco p = new Poppet (x ) ; II Inicializar referencia final en blanco public static void main {String[] args ) { new BlankFinal {) ; new BlankFinal (47 ) i
Estamos obligados a realizar asignaciones a los valores final utilizando una expresión en el punto de definición de) campo o bien en cada constructor. De esa fonna, se garantizará que el campo final esté siempre inicializado antes de utilizarlo. Ejercicio 19:
(2) Cree una clase con una referencia final en blanco a un objeto. Realice la inicialización de la referencia final en blanco dentro de todos los constructores. Demuestre que se garantiza que el va lor final estará inicializado antes de utilizarlo, y que no se puede modificar una vez inicializado.
Argumentos final Java pennite definir argumentos final declarándolos como tales en la lista de argumentos. Esto significa que dentro del método no se podrá cambiar aquello a lo que apunte la referencia del argumento:
11 : reusing / FinalArguments.java Il uso de 11 final" con argumentos de métodos. class Gizmo { publie vo id spin ()
{}
public class FinalArguments { void with {final Gizmo g ) { II ! 9 = new Gizmo () ; II Ilegal -- 9 es final
7 Reutil ización de clases 159 .:~:j
el
wit~c~:IG:zmo
=
n e',,' ·; :;izmo {; ;
;; . sp:n í;'
g. 1/ 8K
9 r.o es :i"o.1
i
-Jo ici f (final i::-.1: i ) f i+-I'; } 1/ Ko se puede car:-.:ciar Las p~im~tivas f !nal s510 p ue den leerse: L:c. g; fí::-Ial '-n:. :' i { ret urn i 1" 1.; } rub1'..c static ~ol d mainl$tring[) args l { ?i¡:aLr..:::-g\.:.me"ts bf = ne"'" FinalArgume:-tts ¡;. ; _ _ . '.Vl.thout (nu ll \ ; ti .\';ith ~ null);
Lo" m~tod05 f( ) Y g( ) muestran lo que sucede cuando los argumento s primitivos son final : Se puede leer el argumento. pero no 1l10diticarlo. Es ta carac¡el'Ística se utili za principall11ell!e pam pasa r dato s a la:-; clases intemas anónimas. lo cua l es un tt'll1~! dcl que hablaremos en el C3pítulo 10. Clases imen/Us.
Métodos final Hay dos razones para utilizar métodos finaL. La primera es "'bloquear" el método para impedir que cualqu ie.r clase quc herede dc esta ca mbi e su sign ificado. Esto se hace por razones de discí10 cuando queremos aseguramos de que se retenga el Cl1lllpon allliemo de un méwdo duranre la herencia y que ese método pueda se r sustituido. La'lL'~ul1da razón por la que se ha sugerido en ('.1 pasado la utilización <,1(' los métodos tinal es la eficiencia. En las illlplcIllcllfill."ionl!s anteriores de Java, si definíamos un método como final. pemlitíamos al compi lador convenir rodas las Ihllnada::- a ese método en llamadas en línea. Cuando el comrilador veía una llamada a un método final. podía (a su discreción) sal!:lrse el modo 110nnal de inserta r el código correspondientc almccanismos de llamada a l método (insenar los arg umentos en la pi la. saltar al código del método y ejecutarlo, saltar hacia arras y eliminar de la pila los argumentos y tratar el vall)]" de retorno), para sus titui r en su lugar la llamada al mélodo por Wla copia del propio código contenido en el cuerpo del método. Esto e limina el ga~to adicional de recursos asociado a la llamada al método. Por s upuesto. si el método es de gran !alllai1o. el código empezará a cr..::cer ellomlem~nte y probablemente no detectemos ninguna mejora de velocidad por la uti· Ii.~:)c ibll de metodos en línea. ya qw: la mejora será insigniti~ante comparada con la call1idad de tiempo invertida dentro del
111CIOdo.
En 111:-. \ersiones más recicmes de J¿l\,a. la máquina virtual (en panicular. la IccnologÍ La clase base Shape establece la interfaz común para cualquier otra clase que herede de Shape; en ténninos conceptuales. representa a todas las formas que puedan dibujarse y borrarse, Cada clase deri vada sust itu ye estas definiciones con el fin de proporcionar un comportamiento distintivo para cada tipo específico de forma.
8 Polimorfismo 171 Random~napet,;ellerator es una espec ie de "fábrica" que genera una referencia a un objeto Shape aleatoriamente se leccionado cada vez que se invoca su método next( ). Observe que el upcasting se produce en las instrucciones return, cada una de las cuales toma una referencia a Ci rcle, Square o Triangle y la devu elve desde next() con el tipo de retorno, Shape. Por tanto, cada vez que se invoca next(). nunca tenemos la oprotunidad de ve r de qué tipo específico se trata. ya que siem-
pre obtenemos una referencia genérica a Shape. main() contiene una matri z de referencias Shape que se rellena mediante llamadas a RandomShapeGenerato r.next(). En este punto, sabemos que tenemos objetos Shape, pero no podemos ser más específicos (ni tampoco puede serlo el compilador). Sin embargo, cuando recorremos esta matriz e invocamos draw() para cada objeto, tiene lugar el comportamiento correspondiente a cada tipo especí flco, como por arte de magia, tal y como puede ver si analiza la salida que se obtiene al ejecutar el programa. La razón de crear las fonnas aleatoriamente es que así puede perc ibirse mejor que el compilador no puede tener ningún conocimiento especial que le pennite hacer las llamada s correctas en tiempo de compilación. Todas las J1amadas a draw() deben tener lugar mediante el mecanismo de acop lamiento dinámico.
Ejercicio 2:
(1) Añada la anotación @Overr ideal ejemplo de procesamiento de fonnas.
Ejercicio 3:
(1) Añada un nuevo método a la clase base de S hapes.java que imprima un mensaje, pero sin sustituirlo en las clases derivadas. Explique lo que sucede. Ahora, sustitúyalo en una de las clases derivadas pero no en las otras y vea lo que sucede. Finalmente, sustitúya lo en todas las clases deri vadas.
Ejercicio 4:
(2) Añada un nuevo tipo de objeto Shape a Shapes.j ava y verifique en main() que el polimorfismo funciona para el nuevo tipo al igual que para los tipos anteriores.
Ejercicio 5:
( 1) Paniendo del Ejercicio 1, añada un método wheels() a Cycle, que devue lva el número de ruedas. Modifique ride() para invocar wheels() y verifique que func iona el polimorfismo.
Ampliabilidad Vol va mos ahora al ejemplo de los instrumentos musicales. Debido al polimorfi smo, podemos añadi r al sistema todos los nuevos tipos que deseemos sin modificar el método tun e() . En un programa orientado a objetos bien diseñado, la mayoría de los métodos (o todos ellos) seguirán el método de t u ne() y sólo se comunicarán con la interfaz de la clase base. Ese lipo Instrument
void playO String whatO void adjustO
I Wind void playO String whatO void adjustO
Woodwind void playO String whatO
Percussion
void playO String whatO void adjustO
Brass
void playO void adjustO
Stringed void playO String whatO void adjustO
172 Pien sa en Java
de programas es extensible (ampliable) porque puede añadir nueva funcionalidad heredando nuevos tipos de datos a par~ tir de la clase base común . Los métodos que manipulan la interfaz de la clase base no necesitarán ser modificados para poder utili zar las nuevas clases. Considere lo que sucede si tomamos el ejemplo de los illstnullentos y añadimos más métodos a la clase base y una serie de clases nuevas. Puede ve r el diagra ma correspondiente al final de la página anterior. Todas estas nuevas clases funcionan correctamente con el método amiguo tune(), sin necesidad de modificarlo. Incluso si tune() se encontrara en un archivo separado y añadiéramos nuevos métodos a la interfaz de Instrument. tune() seguiría funcionando correctamente. si n necesidad de recompilarlo. He aquí la implementación del diagra ma: // : polymorphism/music3/Music3.java // Un programa ampliable. package polymorphism.music3¡ impor t polymorphism.music . Note¡ import static net . mindview.util.Print. * ¡ class Instrument { void play(Note n) { print("Instrument.play{) " + n); String what () { return It Instrument lO ¡ } void adj ust () { print ("Adj usting Instrument") ¡
class Wind extends Instrument { void play(Note n) {print("Wind .play {) String what () { return "Wind" i } void adjust() { print{"Adjusting Wind");
+ n) ¡
class Percussion extends Instrument { void play(Note n) {print("Percussion.play() String what () { return "Percussion "; } void adjust() { print("Adjusting Percussion lt ) ;
}
+ n) ¡ }
class Stringed extends Instrument { void playlNote ni { printl"Stringed . playll " + ni; String what () { return "Stringed"; } void adjust() { print("Adjusting Stringed U ) i
}
class Brass extends Wind { void play(Note n) { print(IIBrass . play() " + n) ¡ } void adj ust () { print (" Adjusting Brass 11) ;
class Woodwind extends Wind { void playlNote ni { printl"Woodwind.playll String what () { return IIWoodwind"; }
" + ni;
public class Music3 { // No importa el tipo, por lo que los nuevos /1 tipos añadidos al sistema funcionan bien : public static void tune I Instrument i l {
/ / ... i.playINote.MIDDLE_CI; public static void tuneAll (Instrument [] for{Instrument i : e)
e)
{
}
}
8 Polimorfismo 173 tune(i)
i
public static vold main(String[) args) // Upcasting durante la adición a la matriz: Instrument[] orchestra = { new Wind()
I
new Percussion () new Stringed ()
I
I
new Brass () ,
new Woodwind ()
); tuneAll(orchestral;
/ * Output: wind.play() MIDDLE_C Percussion.play() MIDDLE_C Stringed.play() MIDDLE_C Brass.play() MIDDLE_C woodwind.play() MIDDLE_C
* /// ,Los nuevos métodos son what( ), que devuelve una referencia Strin g con una descripción de la clase y adjust(), que proporciona a lguna fanna de ajustar cada instmmento. En ma in( ), cuando insertamos algo dentro de la matriz orc hestra, se produce automáticamente una generalización a Jnstrument. Podemos ver que e l método t une( ) es completamente ignorante de todos los cambios de código que han tenido lugar alrededor suyo, a pesa r de 10 cual sigue fu ncionando perfectamente. Ésta es, exactam ente, la funcionalidad que se supone que el polimorfismo debe proporcionar. Los cambios en el código no generan ningún problema en aquellas partes del programa que no deban verse afectadas. Dicho de otra fon11a , el polimorfismo es una técnica importante con la que el programador puede "separar" las cosas que cambian de las cosas que pennanecen. Ejercicio 6:
( 1) Modifique Music3.java de modo qu e what() se convierta en el método toString() del objeto raíz Object. Pruebe a imprimir los objetos (nstr ument utilizando System.out.pr intln() (sin efectuar ninguna proyección de tipo).
Ejercicio 7:
(2) Ailada un nuevo tipo de objeto lnstrument a Music3.java y verifique que e l polimorfismo funciona para el nuevo tipo.
Ejercicio 8 :
(2) Modifique Music3.java para que genere aleatoriamente objetos [ostrument de la misma forma que 10 bace Shapes.java.
Ejercicio 9:
(3) Cree una jerarqu íaa de herencia Rodent: Mouse, Gerbil, Hamster, etc (roedor: ratón, jerbe, hamster, etc.). En la clase base proporcione los métodos que son comunes para todos los roedores, y sustituya estos métodos en las clases derivadas para obtener diferentes comportami entos dependiendo del tipo específi co de roedor. Cree una matri z de objetos Rodeot , rellénela con diferentes tipos específicos de roedores e invoque los métodos de la clase base para ver lo que sucede.
Ejercicio 10:
(3) Cree una clase base con dos métodos. En el primer método, in voque el segundo método. Defina una c lase que herede de la anterior y sustituya el segundo método. Cree un objeto de la clase derivada, realice una generalización (lIpcasling) al tipo base y llame al primer método. Explique 10 que sucede.
Error: "sustitución" de métodos private He aq uí un ej emplo de error de un programa que se puede cometer de manera inadvertida:
11 : polymorphismJPrivateOverride.java
JI Intento de sustituir un método privado . package polymorphism¡ import static net . mindview.util.Print.*;
174 Piensa en Java
public class PrivateOverride { private void f () { print("private f () 11); public static void main(String[] args) PrivateOverride po = ne w Derived() j
po.r O;
class Derived extends PrivateOverride { public void r () { print ( "public r () " ) ; } / * Output , private f ()
* /// ,Podría esperar, razonablemente, que la salida fuera " public f( )", pero los métodos pri vados son automáticamente de tipo final , y están también ocultos a ojos de la clase deri vada. Por esta razón, el método f() de la clase derivada es, en este caso, un método completamente nuevo, ni siquiera está sobrecargado, ya que la versión de f( ) en la clase base no es visible en Derived .
El resultado de esto es que sólo los métodos no privados pueden ser sustituidos, así que hay que estar atento al intento incorrecto de sustituir métodos de tipo priva te, ya que esos intentos no generan ninguna advertencia del compilador, sino que el sistema no hará, seguramente, 10 que se espera. Para evitar las confusiones, conviene utilizar en la clase deri vada un nombre diferente al del método private de la clase base.
Error: campos y métodos static Una vez familiari zados con el tema del polimorfismo, podemos tender a pensar que todo ocurre polimórficamente. Sin embargo, las únicas llamadas que pueden ser po limórficas son las llamadas a métodos normales. Por ejemplo, si accedemos
a un campo directamente, ese acceso se resolverá en tiempo de compilación, como se ilustra en el siguiente ejemplo:1 // : polymorphism / FieldAccess.java // El acceso directo a un campo se determina en tiempo de compilaci6n. class Super { public int field = O¡ public int getField O { return field;
}
c lass Sub extends Super public int field = 1: public int getField () return field; } public int getSuperField () { return super. field¡
public class FieldAccess ( public static void main(String[] args ) { Super sup = new Sub(); // Upcast System.out.println(ltsup.field = n + sup.field + ", sup . getField () = n + sup. getField () ) : Sub sub = new Sub (} : System.out.println("sub.field = + sub . field + 11 sub. getField () + sub.getField () + ti, sub . getSuperField () = n + sub . getSuperField ()) ; I
I
Gracias a Randy Nichols por plantear esta cuestión.
8 Polimorfismo 175
/ * Output: 0, sup getField{) sup. field sub. field = 1, sub getField{)
1
1, sub.getSuperField {)
= O
,///:Cuando un objeto Sub se generaliza a una referencia Super, los accesos a los campos son resueltos por el compi lador, por lo que no son polimórficos. En este ejemplo, hay asignado un espacio de almacenamiento distinto para Super.field y Sub.lield. Por tanto, Sub contiene rea lmente dos campos denominados lield : el suyo propio y el que obti ene a partir de Super. Sin embargo, cuando se hace referencia al campo field de Super no se genera de fanna predetenninada una referencia a la versión almacenada en Super; para poder acceder al campo field de Super es necesario escribir explícitamente super.lield. Aunque esto último pueda parecer algo confuso. en la práctica no llega a plamearse casi nunca, por una razón: por regla general, se definen todos los campos como priva te, por lo que no se accede a ellos directamente, sino sólo como efecto secundario de la invocación a métodos. Además, probablemente nunca le demos el mi smo nombre de la clase base a un campo de la clase derivada, ya que eso resultaría muy confuso. Si un método es de tipo stane, no se comporta de fonna polimórfica:
JJ : polymorphismJStaticPolymorphism.java 11 Los métodos estáticos no son polimórficos. class StaticSuper { public static String staticGet() return "Base staticGet () u i public String dynamicGet () { return u Base dynamicGet () " ;
class StaticSub extends StatieSuper { publie statie String statieGet () { return "Derived statieGet(}"¡ public String dynamicGet () { return "Derived dynamicGet() u;
public class StaticPolymorphism { public statie void main (String[] args ) { StaticSuper sup : new StaticSub(); JJ Generalización System,out.println(sup.staticGet{)) ; System.out . println(sup.dynamicGet()) i
J* Output: Base staticGet() Derived dynamicGet()
, /// :Los métodos estáticos están asociados con la clase y no con los objetos individuales.
Constructores y polimorfismo Corno suele suceder, los constructores difieren de los otros tipos de métodos, también en lo que respecta al polimorfismo. Aunque los constructores no son polirnórficos (se trata realmente de métodos estáticos, pero la declaración static es implícita), tiene gran importancia comprender cuál es la fonna en que funcionan los constructores dentro de las jerarquías complejas y en presencia de polimorfismo. Esta compresión de los fundamentos nos ayudará a evitar errores desagradables.
176 Piensa en Java
Orden de las llamadas a los constructores Hemos hablado brevemente del orden de las llamadas a los constmclOres en el Capítulo 5, Inicialización y Iimpie:a, y también el Capítulo 7. Relllili:ación de clases, pero eso fue antes de introducir el concepto de polimorfi smo. El conslructor de la clase base siempre se in voca durante el proceso de construcción correspondiente a un a clase deri vada. Esta llamada provoca un desplazamiento automático hacia arriba en la jerarquía de herencia, invocándose un constructor para todas las clases base. Esto tien e basta nt e sen tido, porque el constructor tiene asignada una tarea especial: garantizar que el objeto se constmye apropiadamente. Una clase deri vada sólo tiene acceso a sus propios miembros y no a los de la clase base (aq uellos mi embros típicamente de tipo private). Sólo el constructor de la clase base dispone del conocimiento y del acceso adecuados para inicializar sus propios elementos. Por tanto, resulta esencial que se invoquen todos los constructores, en caso contrario, no podría co nst ruirse el método completo. Ésta es la razón por la que el compilador impone que se realice una llamada al constructor para cada parte de una clase derivada. Si no especificamos ex plícitamente una llamada a un constructor de la clase base dentro del cuerpo de la clase deri vada, el compilador invocará de manera automática el constructor predetenninado. Si no hay nin gún constmctor predetenninado, el compilador generará un error (en aquellos casos en que una determinada clase no tenga ningún constructor, el compilador sintetizará automáticamente un constructor predeterminado). Veamos un ejemplo que muestra los efectos de la composición, de la herencia y del polimorfismo sobre el orden de construcción:
1/: polymorphism/Sandwich . java / 1 Orden de las llamadas a los constructores. package polymorphism¡ import static net.mindview . util.Print.*¡ class Meal Meal {l print ( "Meal () " ) ¡
class Bread Bread () { print ( liBread ()
11 )
j
class Cheese Cheese () { print ( IICheese () " ) ¡
class Le t tuce Lettuce {) { print {"Lettuce {) " )
j
class Lunch extends Meal { Lunch () { print ( "Lunch () " ) ¡
class PortableLunch extends Lunch { PortableLunch (l print {"PortableLunch () 11 ) ¡ }
public class Sandwich extends PortableLunch private Bread b = new Bread () ¡ private Cheese e = new Cheese{ ) ¡ private Lettuce 1 = new Lettuce()¡ public Sandwich () { print ( " Sandwich () " ) ¡ public static void main (String [] args l { new Sandwich () ¡
1* Output:
8 Polimorfismo 177 Meal (1 Lunch!)
portabl eLunch () Bread (1 Cheese ()
Lettuce () Sandwich ()
. /// ,Este ejemplo crea una clase compleja a partir de otras clases y cada una de estas clases dispone de un constructor que se anuncia a sí mismo. La clase importante es Sandwich, que refleja tres niveles de herencia (cuatro si contamos la herencia implícita a partir de Object) y tres objetos miembro. Podemos ver en main() la salida cuando se crea un objeto Sandwich. Esto quiere decir que el orden de llamada a los constnlctores para un objeto complejo es el siguiente: 1. Se invoca al constructor de la clase base. Este paso se repite de fanna recursiva de modo que la raíz de la jerar-
quía se constmye en primer lugar, seguida de la siguiente clase derivada, etc., hasta alcanzar la clase si tuada en el nivel más profundo de la jerarquía. 2. Los inicial izadores de los miembros se invocan según el orden de declaración. 3. Se invoca el cuerpo del constmctor de la clase derivada. El orden de las llamadas a los constructores es importante. Cuando utilizamos los mecanismos de herencia, sabemos todo acerca de la clase base y podemos acceder a los miembros de tipo public y protected de la misma. Esto quiere decir que debemos poder asumir que todos los demás miembros de la clase base son vá lidos cuando los encontremos en la clase derivada. En UD método nonnal, el proceso de construcción ya ha tenido lugar, de modo que tod os los miembros de todas las partes del objeto habrán sido construidos. Sin embargo, dentro del constructor debemos poder estar seguros de que todos los miembros que utilicemos hayan sido construidos. La única fonna de ga rantizar esto es invocando primero al constmctor de la clase base. Entonces, cuando nos encontremos dentro del constructor de la clase derivada, todos los miembros de la clase base a los que queremos acceder ya habrán sido inicializados. Saber que todos los miembros son válidos dentro del constructor es también la razón de que, siempre que sea posible, se deban inicializar todos los objetos miembro (los objetos incluidos en la clase mediante los mecanismos de composición) en su punto de definición dentro de la clase (por ejemplo. b, e y I en el ejemplo anterior). Si se ajusta a esta práctica a la hora de programar, le será más fácil garantizar que todos los miembros de la clase base y objetos miembro del objeto actual hayan sido inicializados. Lamentablemente, este sistema no nos permite gestionar todos los casos, como veremos en la siguiente sección. Ejercicio 11:
(1) Añada una clase Pickle a Sandwich.j ava.
Herencia y limpieza Cuando se utili zan los mecanismos de composición y de herencia para crear una nueva clase, la mayor parte de las veces no tenemos que preocupamos por las tareas de limpieza; los subobjetos pueden nonnalmente dejarse para que los procese el depurador de memoria. Sin embargo, si hay algún problema relativo a la limpieza, es necesario actuar con diligencia y crear un método dispose() (éste es el nombre que yo he seleccionado, pero usted puede utili zar cualquier otro que indique que estamos deshaciéndonos del objeto) en la nueva clase. Y, con la herencia, es necesario sustituir dispose( ) en la clase deri vada si necesitamos realizar alguna tarea de limpieza especial que tenga que tener lugar como parte de la depuración de memoria. Cuando se sustituya dispose() en una clase heredada, es importante acordarse de invocar la versión de dispose() de la clase base, ya que en caso contrario las tareas de limpieza propias de la clase base no se llevarán a cabo. El siguiente ejemplo ilustra esta situación:
11 : polymorphism/ Frog. j ava /1 Limpieza y herencia. package polymorphism; import static net.mindview.util.Print.*; class Characteristic { private String S; Characteristic (String s ) { this.s = S;
178 Piensa en Java print ("Creating Characteristic " + s) i protected void dispose() print ( "disposing Characteristic " + s)
class Description { private String Si Description {String s} { this.s = Si print ("Creating Description " + s)
i
i
protected void dispose () { print ("disposing Description " + s);
class LivingCr eature { private Characteristic p new Characteristic("is alive " ) i private Description t = new Description("Basic Living Creature 'l) i Li vingCreature () { print (" Li vingCreature () ,, ) i protected void dispose () { print ( " LivingCreature dispose") i t.d i spose() i p.dispose () i
class Animal extends LivingCreature { private Characteristic p = new Characteristic ("has heart") i private Description t = new Description ("Animal not Vegetable"); Animal () { print ("Animal () "); } protected void dispose () { print ("Animal dispose"); t. dispose () ; p.dispose() i super.dispose() i
class Amphibian extends Animal { private Cha racteristic p = n e w Characteristic ( " can live in water " ) i private Description t = new Description ( " Both water and land"); Amphibia n () { print ("Amphibian () " ) ; protected void dispose() print ( "Amphibian dispose"); t.dispose() i
8 Polimorfismo 179
p.dispose() ; super.dispose() ;
public class Frog extends Amphibian { privace Characteristic p = new Characteristic ( UCroaks" ) ; private Description t = new Description("Eats Bugs") i public Frog () { print (11 Frog () 11 ) ; } protected void dispose () {
print ( 11 Frog dispose 11 ) ; t .dispose () ; p.dispose() ; super.dispose() ; public static void main (String [] args ) {
Frog frog
=
new Frog {);
print(IIBye! " ) ;
frog .dispose ()
i
/ * Output: Creating Characteristic i5 alive Creating Description Basie Living Creature LivingC reature () Creating Characteristic has heart Creating Description Animal nat Vegetable Animal ()
Creating Characteristic can live in water Creating Description 80th water and land Amphibian () Creating Characteristic Croaks Creating Description Eats Bugs Frog() Bye! Frog dispose disposing Description Eats Bugs disposing Characteristic Croaks Amphibian dispose disposing Description Both water and land disposing Characteristic can live in water Animal dispose disposing Description Animal not Vegetable disposing Characteristic has heart LivingCreature dispose disposing Description Basic Living Crea tu re disposing Characteristic is alive
*///,Cada clase de la jerarquía también contiene objetos miembro de los tipos Characteristic y Description, que también habrá que borrar. El orden de borrado debe ser el inverso del orden de inicialización, por si acaso uno de los subobjetos depende del otro. Para los campos, esto quiere decir el inverso del orden de declaración (puesto que los campos se inicializan en el orden de declaración). Para las clases base (siguiendo la nonna utilizada en e++ para los destructores), debemos real izar primero las tareas de limpieza de la clase derivada y luego las de la clase base, La razón es que esas tareas de limpieza de la clase derivada tuvieran que invocar algunos métodos de la clase base que requieran que los componentes de la clase base continúen siendo accesibles, así que no debemos destruir esos componentes prematuramente. Analizando la salida podemos ver que se borran todas las partes del objeto Frog en orden inverso al de creación. A partir de este ejemplo, podemos ver que aunque no siempre es necesario realizar tareas de limpieza, cuando se llevan a cabo es preciso hacerlo con un gran cuidado y una gran atención.
180 Piensa en Java
Ejerc ic io 12:
(3) Modifique el Ejercicio 9 para que se muestre el orden de iniciali zación de las clases base y de las clases deri vadas. Ahora aiiada objetos miembro a las clases base y deri vadas, y muestre el orden en que se lleva a cabo la inicialización durante el proceso de construcción.
Observe también en el ejemplo anterior que un objeto Fro g "posee" sus objetos miembro: crea esos objetos miembro y sabe durante cuálJto tiempo tienen que existir (tanto como dure el objeto F rog), de modo que sabe cuándo invocar el método dispose() para borrar los objetos miembro. Sin embargo, si uno de estos objetos miembro es compartido con otros objetos, el problema se vuelve más complejo y no podemos simplemente asumir que basta con invocar dispose(). En estos casos, puede ser necesario un recuento de referencias para llevar la cuenta del número de obj etos que siguen pudiendo acceder a un obj eto compartido. He aquí un ejemplo: //: polymorphism/ReferenceCoun t i ng . java // Limpieza de objetos miembro compartidos . import static net.mindview . util.P r int. * ¡ class Shared { private int refcount = O; pr i vate static long counter = O; private fi n a l l ong id = counter++; public Shared () { pr i nt ("Crea t ing " + this); public void addRe f () { refcount+ + i pro t ected void dispose () { if{ - - r efcoun t == O) print ("Disposing n + this) i public String toString()
{ return "Sha r ed " + id;
}
class Compos i ng { private Shared shared; private sta t ic long counter = O; private final long id = coun ter++; public Composing (Shared shared) { print ("Creating " + this); this.shared = shared¡ this . shared . addRef() ; protected void dispose() print ("disposing " + this) ¡ shared.dispose() ; public String toString ()
{ return "Composing " + id;
public class ReferenceCounting { public static void main (String [] args) { Shared shared = new Shared {} ¡ Composi ng[ ] composing = { ne w Compos i n g(shared), new Composing(shared}, n e w Composi ng{s h aredl, new Composing(shared}, new Compos i ng(shared) }¡ for(Composing c : composing) c.dispose() ; / * Output : Creating Shared O Creating Comp osing O Creating Composing 1
}
8 Polimorfismo 181 creating Composing 2 creating Composing 3 creating Composing 4 disposing Composing O disposing Composing 1 disposing Composing 2 disposing Composing 3 disposing Composing 4 Disposing Shared O
* /// ,El contador statie long counter lleva la cuenta del número de instancias de Shared que son creadas y también crea un valor para id. El tipo de eounter es long en lugar de int, para evitar el desbordamiento (se trata sólo de una buena práctica de programación: es bastante improbable que esos desbordamientos de contadores puedan producirse en ninguno de los ejemplos de eSle libro). La variable id es de tipo final porque no esperamos que cambie de valor durante el tiempo de vida del objeto. Cuando se asocia el objeto compartido a la clase, hay que acordarse de invocar addRef( ), pero el método dispose( ) llevará la cuenta del número de referencias y decidirá cuándo hay que proceder con las tareas de limpieza. Esta técnica requiere un cierta diligencia por nuestra parte, pero si estamos compartiendo objetos que necesiten que se lleve a cabo una determinada tarea de limpieza, no son muchas las opciones que tenemos. Eje rcicio 13:
(3) Añada un método finalize( ) a RefcrcnccCounting.java para verificar la condición de rerminación (véase el Capítu lo S,inicialización y limpieza).
Ejercici o 14:
(4) Modifique el Ejercicio 12 para que uno de los objetos miembro sea un objeto compartido. Utilice el método de recuento del número de referencias y demuestre que funciona adecuadamente.
Comportamiento de los métodos polimórficos dentro de los constructores La jerarquía de llamada a constructores plantea un dilema interesante. ¿Qué sucede si estamos dentro de un constmctor e invocamos un método con acoplamiento dinámico del objeto que esté siendo constmido? Dentro de un método nonnal, la llamada con acoplamiento dinámico se resuelve en tiempo de ejecución, porque el objeto no puede saber si pertenece a la clase en la que se encuentra el método o a alguna de las clases derivadas de la misma. Si invocamos un método con acoplamiento dinámico dentro de un constructor, también se utiliza la definición sustituida de dicho método (es decir, la definición del método que se encuentra en la clase actual). Sin embargo, el efecto de eSla llamada puede ser inesperado, porque el método sustituido será invocado antes de que el objeto haya sido completamente construido. Esto puede hacer que queden acuitas algunos errores realmente dificiles de detectar. Conceptualmente, la tarea del constructor es hacer que el objeto comience a existir (10 que no es una tarea trivial). Dentro de cualquier constructor, puede que el objeto completo sólo esté fonnado parcialmente, ya que de lo único que podemos estar seguros es de que los objetos de la clase base han sido inicializados. Si el constructor es sólo uno de los pasos a la hora de constmir un objeto de una clase que haya sido derivada de la clase correspondiente a dicho constructor, las partes derivadas no habrán sido todavía inicializadas en el momento en que se invoque al constructor actual. Sin embargo, una llamada a un método con acoplamiento dinámico se "adentra" en la jerarquía de herencia, invocando un método dentro de una clase derivada. Si hacemos esto dentro de un constructor, podríamos estar invocando un método que manipulara miembros que todavía no han sido inicializados, lo cual constituye una receta segura para que se produzca un desastre. Podemos ver el problema en el siguiente ejemplo:
JJ : polymorphismJPolyConstructors.java JJ Los constructores en presencia de pOlimorfismo JJ pueden no producir los resultados esperados. import static net.mindview.util.Print.*; class Glyph { void draw() { print(nGlyph.draw() "); } Glyph () {
182 Piensa en Java print ( "Glyph () bef o re draw () " ) ; draw (} ; pr i nt ( "Glyph () after draw () " ) i
class RoundGlyph extends Glyph private int radius '" 1; RoundGlyph ( int r ) { radius '" r; print ( "RoundGl}'Ph. RoundGlyph () void draw () { print ( "RoundGlyph. draw ()
I
radius
I
radius
11
11
+ radius ) ;
+ radius ) ;
public class PolyConstructors { public static void main (String []
args)
{
new RoundGlyph(S ) ;
/ * Output: Glyph () before draw () RoundGlyph . draw(), radius
= o
Glyph () after draw ()
RoundGlyph. RoundGlyph () ,radius
5
* ///,Glyph.draw() está diseñado para ser sustituido, lo que se produce en RoundGlyph. Pero el constructor de Glyph invoca este método y la llamada tennina en RoundGlyph.draw(), que parece que fuera la intención originaL Pero si examinamos la sal ida, podemos ver que cuando el constructor de Glyph invoca draw(), el valor de radius no es ni siquiera el valor inicial predetenninado de 1, sino que es O. Esto provocará, probablemente, que se dibuje en la pantalla un punto, o nada en absoluto, con lo que el programador se quedará contemplándolo tratando de imaginar por qué no funciona el programa. El orden de inicialización descrito en la sección anterior no está completo del todo, y ahí es donde radica la clave para resolver el misterio. El proceso real de inicialización es: 1. El almacenamiento asignado al objeto se inicializa con ceros binarios antes de que suceda ninguna otra cosa.
2. Los constructores de las clases base se invocan tal y como hemos descrito anteriormente. En este punto se invoca el método sustituido draw( ) (sí, se invoca antes de que llame al constructor de RoundGlyph ) y éste descubre que el valor de radius es cero, debido al Paso 1. 3. Los inicializadores de los miembros se invocan según el orden de declaración. 4. Se invoca el cuerpo del constructor de la clase derivada. La parte buena de todo esto es que todo se inicializa al menos con cero (o con lo que cero signifique para ese tipo de datos concreto) y no simplemente con datos aleatorios. Esto incluye las referencias a objetos que han sido incluidas en una clase a través del mecanismo de composición, que tendrán el valor null. Por tanto, si nos olvidamos de inicializar esa referencia, se generará una excepción en tiempo de ejecución. Todo lo demás tomará el valor cero, lo que usualmente nos sirve como pista a la hora de examinar la salida. Por otro lado, es posible que el programador se quede horrorizado al ver la salida de este programa: hemos hecho algo perfectamente lógico, a pesar de lo cual el comportamiento es misteriosamente erróneo, sin que el compilador se haya quejado (e++ produce un comportamiento más racional en esta situación). Los errores de este tipo podrían quedar ocultos fácilmente, necesitándose una gran cantidad de tiempo para descubrirlos. Como resultado, una buena directriz a la hora de implementar los constructores es: "Haz lo menos posible para garantizar que el objeto se encuentre en un estado correcto y, siempre que puedas evitarlo, no invoques ningún otro método de esta clase". Los únicos métodos seguros que se pueden invocar dentro de un constructor son aquellos de tipo final en la clase
8 Polimorfismo 183 base (esto también se aplica a los métodos privados, que son automáticamente de tipo final ). Estos métodos no pueden ser sustituidos Y no pueden, por tanto, darnos este tipo de so rpresas. Puede que no siempre seamos capaces de seguir esta direc-
triz, pero al menos debemos tratar de cumplirla. Ejercicio 15:
(2) Añada una clase Recta ngularGlyph a PolyCoostructors.java e ilustre el problema descrito en esta
sección.
Tipos de retorno covariantes Java SES añade los denominados tipos de retorno covarianles, lo que quiere decir que un método sustinlido en una clase derivada puede devo lver un tipo deri vado del tipo devuelto por el método de la clase base:
JI :
polymorphism/CovariantReturn .java
class Grai n { public String toString ()
{ rE"turn "Grain"
i
}
{ return "Wheat";
}
class Wheat extends Grain { public String toString ()
cIass MilI { Grain p r ocess ()
{ return new Grain () ¡ }
class WheatMil1 extends MilI { Wheat process () { return new Wheat {) ;
public class CovariantRetur n { public static void main{String[] MilI m = new Mill (} ¡ Grain 9 = m.process{}¡ System.out.println(g) ; m = new WheatMill {) i 9 = m.process()¡ System . out.println{g) ;
}
args ) {
/ * Output: Grain Wheat
*// /0La diferencia clave entre Java SE5 y las versiones anteriores es que en éstas se obligaría a que la versión sustituida de process( ) devolviera Grain, en lugar de Wheat, a pesar de que Wheat deriva de Graio y sigue siendo, por tanto. un tipo de retomo legítimo. Los tipos de retomo covariantes penniten utilizar el tipo de retorno Wheat más específico.
Diseño de sistemas con herencia Una vez que sabemos un poco sobre el polimorfismo, puede llegar a parecemos que todo debería heredarse, ya que el polimorfismo es una herramienta tan inteligente. Pero la realidad es que esto puede complicar nuestros diseños innecesariamente, de hecho, si decidimos utilizar la herencia como primera opción a la hora de utilizar una clase existente con el fin de formar otra nueva, las cosas pueden volverse innecesariamente complicadas. Una técnica mejor consiste en tratar de utilizar primero la composición, especialmente cuando no resulte obvio cuál de los dos mecanismos debería emplearse. La composición no hace que el diseño tenga que adoptar una jerarquía de herencia. Pero, as imismo, la composición es más flexible, porque permite seleccionar dinámicamente un tipo (y por tanto un compor-
184 Piensa en Java
tamiento), mientras que la herencia exige que se conozca un tipo exacto en tiempo de compilación. El siguien te ejemplo ilustra es to: jI : polymorphism/Transmogrify.java
/1 Modificación dinámica del comportamiento de un objeto /1 mediante la composición (el patrón de diseño basado en estados). import static net.mindview.util.Print.*¡ class Actor { public void act ()
{}
class HappyActor extends Act or { public void act () { print ( "HappyActor")
class SadActor extends Actor { public void act {) { print ( "SadActor ll
i
);
class Stage { private Ac t or actor = new HappyActor {); public void change () { actor = new SadActor () ; public void performPlay () { actor. act () ; }
public c l ass Transmogrify { public static void main (S tring (] args) Stage stage = new Stage()¡ stage.performPlay() ; stage.change() ; stage .performPlay() ;
{
/ * Output : HappyActor SadActor
' /1/ , Un objeto Stage contiene una referencia a un objeto Actor, que se inicializa para que apunte a un objeto HappyActor. Esto significa que performPlay() produce un comportamiento concreto. Pero, como una referencia puede redirigirse a un objeto distinto en tiempo de ejecución, podríamos almacenar una referencia a un objeto SadActor en actor, y entonces el comportamiento producido por performPlay() variaría. Por tanto, obtenemos una mayo r flexibilidad dinámica en liempo de ejecución (esto se denomina también patrón de diseño basado en estados, consulte Thinking in Patterns (with Java) en www.MindView.net) . Por contraste, no podemos decidir realizar la herencia de fonna diferente en tiempo de ejecución, el mecanismo de herencia debe estar perfectamente determinado en tiempo de compilación. Una regla general sería: "Utilice la herencia para expresar las diferencias en comportamiento y los campos para expresar las variaciones en el estado". En el ejemplo anterior se utilizan ambos mecanjsmos; definimos mediante herencia dos clases distintas para expresar la diferencia en el método act() y Stage utiliza la compos ición para permitir que su estado sea mod ificado. Dicho cambio de estado, en este caso, produce un cambio de comportamiento. Ejercicio 16:
(3) Siguiendo eJ ejempJo de Transmogrify.java, cree una cJase Starship que contenga una referencia AJertStatus que pueda indicar tres estados distintos. Incluya métodos para verificar los estados.
Sustitución y extensión Podría parecer que la forma más limpia de crear una jerarquía de herencia sería adoptar un enfoque "puro"; es decir, sólo los métodos que hayan sido establecidos en la clase base serán sustituidos en la clase derivada, como puede verse en este diagrama:
8 Polimorfismo 185
Shape draw() erase()
I Circle
draw() erase()
I Triangle
Square
draw() erase()
draw() erase()
Esto podría decirse que es una relación de tipo "es-un" porque la interfa z de una clase establece lo que dicha clase es. La herenc ia garant iza que cualqu ier clase derivada tendrá la interfaz de la c lase base y nada más. Si seguimos este diagrama, las c lases derivadas no tendrán nada más que lo que la interfaz de la clase base ofrezca.
Esto podría considerarse como una sustitución pura, porque podemos sustituir perfectamente un objeto de la clase base o un objeto de una clase deri vada y no nos hace falta conocer ninguna infomláción adicional acerca de las subcla ses a la hora de utilizarlas:
Habla con Shape
--------------------,..
Circle, Square, Une o un nuevo tipo de Shape
Mensaje
Relación
~es·u n "
En otras palabras, la clase base puede rec ibir cualquier men saje que enviemos a la clase deri vada, porque las dos tienen exactamente la mism a interfa z. Debido a esto lo que tenemos que hacer es generalizar a partir de la clase derivada, sin tener que preocuparnos de ver cuál es e l tipo exacto del objeto con el que estemos tratando. Todo se maneja medi ante el pol imo rfi smo. Cuando vemos las cosas de esta fom18, debe parece r que las relac iones puras de tipo "es-un" so n la fonna más lógica de implementar las cosas, y que cualquier otro tipo de diseño resulta confuso por comparación. Pero esta forma de pensar es un error. Tan pronto comencemos a pensar de esta foona, miraremos a nuestro alrededor y descubriremos que ampliar la interfaz (mediante la palabra clave extends) es la perfecta so lución para un problema concreto. Este tipo de so lución podría denominarse relación de tipo "es-coma-un", porque la clase deri vada es como la clase base: tiene la misma interfaz ele mental y tiene, además, ot ras características que req ui eren método s adicionales para implementarlas:
Uselul voi d I() void g()
Suponga qu e esto representa una interfaz compleja
MoreUselul
"Es-coma-un"
void I() void g() void u() void vO void w()
Ampliación de la interfaz
186 Piensa en Java
Aunque este enfoque también resulta útil y lógico (dependiendo de la situación) tiene una desventaja. La parte ampliada de la interfaz en la clase derivada no está disponible en la clase base, por lo que, una vez que efectuemos una generalización no podremos invocar los nuevos métodos: Habla con el objeto Useful
-_._-_._ . . . . . . . . . . . . . .
parte de Useful
Mensaje parte
eUseful
Si no estamos haci endo generalizaciones, no debe haber ningún problema, pero a menudo nos encontraremos en situaciones en las que necesitamos descubrir el tipo exacto del objeto para poder acceder a los métodos ampliados de dicho tipo. En la siguiente sección se explica cómo hacer esto.
Especialización e información de tipos en tiempo de ejecución Puesto que perdemos la información específica del tipo mediante el proceso de generalización (l/pcast, que consiste en moverse hacia arriba por la jerarquía de herencia), tiene bastante sentido que para extraer la infonnación de tipos; es decir, para volver a descender por la jerarquía de herencia, utilicemos un proceso de especialización (downcasf). Sin embargo, sabemos que una generalización siempre es segura, porque la clase base no puede tener una interfaz más amplia que la clase derivada; por tanto, se garantiza que todo mensaje que enviemos a través de la interfaz de la clase base será aceptado. Pero con una especialización no sabemos realmente si una determinada fonna, por ejemplo, es un círculo u otra cosa: también podría ser un triángulo, un cuadrado o algún otro tipo de forma. Para resolver este problema, tiene que haber alguna manera de garantizar que la especialización se efectúe de fonna correcta, de modo que no hagamos accidentalmente una proyección sobre el tipo inadecuado y luego enviemos un mensaje que el objeto no pueda aceptar. Si no podemos garantizar que la especialización se efectúe de manera correcta, nuestro programa no será muy seguro. En algunos lenguajes (como e++) es necesario realizar una operación especial para poder llevar a cabo una especialización de tipos de forma correcta, pero en Java todas las proyecciones de tipos se comprueban. Por tanto, aunque parezca que estemos utilizando simplemente una proyección de tipos nonnal, usando paréntesis, dicha proyección se comprueba en tiempo de ejecución para garantizar que se trate, de hecho, del tipo que creemos que es. Si no lo es, se obtiene una excepción ClassCastException. Este acto de comprobación de tipos en tiempo de ejecución se denomina información de tipos en tiempo de ejecl/ción (RTTI, rl/ntime type in/ormation). El siguiente ejemplo ilustra el comportamiento de RTTI: (( , polymorphism(RTTI . java
II Especialización en información de tipos en tiempo de ejecución (RTTI) . (( {ThrowsException} cIass UsefuI { public void f 1) {} public void g 1) {}
cIass MoreUseful extends Use fuI { pubIic void f 1) { } pubIic void gl) { } public void ul! {} public void vI! {} pubIic void w () { }
pubIic cIass RTTI { pubIic static void main{String[] args) Useful[] x " { new Use fuI () , new MoreUseful ()
};
{
8 Polimorfismo 187 x[O].f(); X[l].g () ;
/ 1 Tiempo de compilación: método no encontrado en Useful :
II !
x[l].u () ;
(( MoreUseful)x[l] ) . u ()
i
//
Especialización / RTTI
(( MoreUseful ) x[O J) .u () ;
1/
Excepción generada
}
111 ,Como en el diagrama anterior, MoreUseful amplía la interfaz de Usefu). Pero. como se tTata de una clase heredada también puede generalizarse a Useful. Podemos ve r esta generalización en acción durante la inicialización de la matriz x en m.iu(). Puesto que ambos objetos de la matriz son de clase Useful, podemos enviar los métodos f() Y g() a ambos. mientras que si tratamos de invocar u() (que sólo existe en MoreUseful), ob tendremos un mensaje de error en tiempo de com-
pilación . Si queremos acceder a la interfaz ampliada de un objeto MoreUseful, podemos tratar de efectuar una especialización. Si se trata del tipo correcto. la operación tendrá éxito. En caso contrario, obtendremos una excepción ClassCastException. No es necesario escribir ningtin código especial para esta excepción. ya que indica un error del programador que puede producirse en cualquier lugar del programa. La etiqueta de comentario {ThrowsExcept-ion} le dice al sistema de construcción de los ejemplos de este libro que cabe esperar que este programa genere una excepción al ejecutarse. El mecanismo RTTI es más comp lejo de 10 que este ejemplo de proyección simple pennite intuir. Por ejemplo, existe una fonna de ver cuál es el tipo con el que estamos tratando antes de efectuar la especialización. El Capítulo 14, Información de (ipos está dedicado al estudio de los diferentes aspectos de la infoonación de tipos en tiempo de ejecución en Java. Ejercicio 17:
(2) Utilizando la jerarquía Cyele del Ejercicio 1, aliada un método balance() a Unicyele y Bicyele, pero no a Tricycle. Cree instancias de los tres tipos y generalícelas para formar una matriz de objetos Cyclc. Trate de invocar balance() en cada elemento de la matriz y observe los resultados. Rea lice una especiali zación e invoque balance( ) y observe lo que sucede.
Resumen Polimorfismo significa "diferentes f0n11as". En la programación orientada a objetos, tenemos una misma interfaz definida en la clase base y diferentes fonnas que utilizan dicha interfaz: las diferentes versiones de los métodos dinámicamente acopIados. Hemos visto en este capítulo que resulta imposib le comprender. o incluso crear, un ejemplo de polimorfismo sin utilizar la abstracción de datos y la herencia. El polimorfismo es una característica que no puede anali zarse de manera aislada (a diferencia, por ejemplo, del análisis de la instrucción switch), sino que funciona de manera concertada, como parte del esquema global de relaciones de clases. Para usar el polimorfismo, y por tanto las técnicas de orientación a objetos, de manera efectiva en los programas, es necesario ampliar nuestra visión del concepto de programación, para incluir no sólo los miembros de una clase indi vidual, sino tambi én los aspectos comunes de las distintas c lases y las relaciones que tienen entre sÍ. Aunque esto requiere un esfuerzo significativo, se trata de un esfuerzo que merece la pena. Los resultados serán una mayor velocidad a la hora de desarrollar programas, una mejor organizac ión del código y la posibilidad de di sponer de programas ampliables, y un mantenimiento del código más eficiente. Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico rhe rhillkillg in Jav(I Annotafed So/lIti01l Guide, disponible para la venia en \IWII~ AfilldVhT\I ·. /¡e/.
Interfaces
Las interfaces y las clases abstractas proporcionan una fonna más estructurada de separar la interfaz de la implementación. Dichos mecanismos no son tan comunes en los lenguajes de programación. C++-, por ejemplo, sólo tiene soporte indirecto para estos conceptos. El hecho de que existan palabras clave del lenguaje en Java para estos conceptos indica que esas ideas fueron consideradas lo suficientemente importantes como para proporcionar un soporte directo. En primer lugar, vamos a exa minar el concepto de clase abstracta, que es una clase de ténnino medio entre una clase normal y una interfaz. Aunque nuestro primer impulso pudiera ser crear una interfaz, la clase abstracta constituye una herramienta importante y necesaria para construir clases que tengan algunos métodos no implementados. No siempre podemos utili zar una interfaz pura.
Clases abstractas y métodos abstractos En todos los ejemplos de "instmmentos musicales" del capítulo anterior,los métodos de la clase base Instrument eran siempre "ficticios". Si estos métodos llegan a ser invocados, es que hemos hecho algo mal. La razón es que lnstrument no tiene otro sentido que crear una intelfa= comlÍn para todas las clases derivadas de ella. En dichos ejemplos, la única razón para establecer esta interfaz común es para poder expresarla de manera diferente para cada uno de los distintos subtipos. Esa interfaz establece una forma básica, de modo que podemos expresar todo aquello que es común para todas las clases deri vadas. Otra fonna de decir esto sería decir que Instr ument es una clase base abstracta. o simplemente una clase abstracta. Si tenemos una clase abstracta como Instrument, los objetos de dicha clase específica no tienen ningún significado propio casi nunca. Creamos una clase abstracta cuando queremos manipular un conjunto de clases a través de su interfaz comú n. Por tanto, el propósito de lnstrument consiste simplemente en expresar la interfaz y no en una implementación concreta. por lo que no tiene sentido crear un objeto Instr ument y probablemente convenga impedir que el usua rio pueda hacerlo. Podemos impedirlo haciendo que todos los métodos de Instrument generen errores, pero eso retarda la infonnación hasta el momento de la ejecución y requiere que el usuario realice pruebas exhaustivas y fiables. Generalmente, resulta preferible detectar los problemas en tiempo de compilac ión. Java propo rciona un mecanismo para hacer esto denominado método abstracto. ¡ Se trata de un método que es incompleto: sólo tiene una declaración, y no dispone de un cuerpo. He aquí la si ntaxis para la declaración de un método abstracto: abstract void f();
Una clase que contenga métodos abstractos se denomina clase abstracta. Si una clase contiene uno o más métodos abstractos, la propia clase debe calificarse como abs tr aet, (en caso co ntrario, el compilador generará un mensaje de error). Si una clase abstracta está incompleta, ¿qué es 10 que se supone que el compi lador debe hacer cuando alguien [rate de insranciar un objeto de esa clase? El compilador no puede crear de manera segura un objeto de una clase abstracta, por lo que
I Para los programadores de C++, se trata del anftlogo a lasjimciones ¡'irrita/es puras de C++.
190 Piensa en Java
generará un mensaje de error. De es ta fonna. el compilador garal1liza la pureza de la clase abstracta y no es necesario preo· cupa rse de si se la va a utilizar correctamente. Si definimos un a clase heredada de una clase abstracta y queremos co nstruir objelos del nuevo tipo. deberemos proporcio· nar defini ciones de métodos para todos los métodos abstractos de la clase base. Si no lo hacemos (y podemos decidir no hacerlo). el1lonces la clase derivada será también abstracta. y el compilador nos obligará a calificar esa clase con la palab ra clave abstrael. Resulta posible definir una clase como abstracta sin incluir ningún método abstracto. Esto resulta útil cuando tenemos una clase en la que no tiene sentido ten er ningún método abstracto y. sin embargo. queremos evitar que se generen instancias de dicha clase.
La clase Instrument de l capítulo ant eri or puede lransfonnarse fácilmente en una clase abstracta. Sólo algunos de los méto· dos serán abstra cTOs, ya que definir una clase como abstracta no obli ga a que todos los métodos sean abstractos. He aquí el ejempl o modificado: abstraet lnstrument abstract void playO; String whatO ¡r ... '1} abstraet voi d adjustO;
exte ds Wind void playO String whatO void adjustO
exte nds
ext nds
Percussion
Stringed
voi d playO String whatO void adjustO
ext nds
extr nds Brass
Woodwind void playO String whatO
void playO String whatO void adjustO
void playO void adjustO
He aquí el ejemplo de la orquesta modificado para uti lizar clases y métodos abstractos:
/1 : interfaces / musie4 / Musie4.java 1/ Clases y métodos abstractos. package interfaees.musie4¡ import polymorphism.musie.Note¡ import statie net.mindview.util.Print.*; abstraet elass Instrument { private int i¡ /1 Storage allocated for eaeh publie abstraet void play {Note n)¡ publie String what () { return "Instrument" ¡ } publie abstraet void adjust () ;
class Wind extends Instrument public void play (Note n ) {
9 Interfaces 191 + n);
print ("Wind. play () public String what () public void adjust ()
{ return "Wind" i {}
}
class Percussion extends Instrument { public void play (Note n) { print(IIPercussion.playO " + n) i { return npercussion"; {}
public String what () public void adjust()
clasE Stringed extends Instrument { public void play (Note n ) { print (" Stringed. play () 11 + n) i public String what () public void adj ust ()
{ return "Stringed!1; {}
class Brass extends Wind { public void play{Note n) print (JI Brass. play () 11 + n) public void adjust()
i
{ print("Brass.adjust() n);
class Woodwind extends Wind { public void play (Note n) { print("Woodwind.play() " + n); public String what ()
{ return "Woodwind 11
i
public class Music4 { // No me preocupa el tipo, por lo que los nuevos tipos /1 añadidos al sistema seguirán funcionando: static void tune (Instrument i) {
// ... i.play{Note.MIDDLE_C)
i
static void tuneAll (Instrument [J tor (Instrument i e) tune (i) i
e)
{
public static void main(String[] args) II Generalización durante la inserción en la matriz: Instrument[] orchestra = { new Wind (), new Percussion(), new Stringed () , new Brass () , new Woodwind ()
}; tuneAll(orchestra)
i
192 Piensa en Java
} / * Output, Wind.play () MIDDLE_C Percussion.play () MIDDLE_C Stringed.play () MIDDLE_C
Brass.play () MIDDLE_C Woodwind.play () MIDDLE_C
* /// , Podemos ver que no se ha efecnlado ningún cambio, salvo en la clase base. Resulta útil crear clases y métodos abstractos porque hacen que la abstracción de una clase sea explícita, e ¡nfannan tanto al usuario como al compilador acerca de cómo se pretende que se utilice esa clase. Las clases abstractas también resultan útiles como herramientas de rediseño, ya que permiten mover fácilmente los métodos comunes hacia arriba en la jerarquía de herencia. Ejercicio 1:
(1) Modifique el Ejercicio 9 del capítulo anterior de modo que Rodent sea una clase abs tracta. Defina los
métodos de Rodent como abstractos siempre que sea posible. Ejercicio 2:
(1) Cree una clase abstracta sin incluir ningún método abstracto y verifique que no pueden crearse instancias de esa clase.
Ejercicio 3:
(2) Cree una clase base con un método print( ) abstracto que se sustituye en una clase deri vada. La versión sustituida del método debe imprimir el va lor de una variable int definida en la clase derivada. En el punto de defin ición de esta vari able, proporcione un va lor di stinto de cero. En el constructor de la clase base, llame a este método. En main( ), cree un objeto del tipo derivado y luego invoque su método print( ). Explique los resultados.
Ejercicio 4:
(3) Cree una clase abstracta sin métodos. Defllla una clase derivada y anádale un método. Cree un método estático que tome una referencia a la clase base, especialícelo para que apunte a la clase derivada e invoque el método. En main( ), demuestre que este mecanismo funciona. Ahora, incluya la declaración abstracta del método en la clase base, eliminando así la necesidad de la especialización.
Interfaces La palabra clave interface lleva el concepto de abstracción un paso más allá. La palabra clave abstract pennite crear uno o más métodos no definidos dentro de una clase: proporcionamos parte de la interfaz, pero sin proporcionar la implementación correspondiente. La implementación se proporciona de las clases que hereden de la clase actual. La palabra clave interface produce una clase com pletamente abstracta, que no proporciona ninguna implementación en absoluto. Las interfaces pemúten al creador determinar los nombres de los métodos, las listas de argumentos y los tipos de retomo, pero si n especificar ningún cuerpo de nin gull método. Una interfaz proporciona simplemente una fomla, sin ninguna implementación. Lo que las interfaces hacen es decir: "Todas las clases que implementen esta interfaz concreta tendrán este aspecto". Por tanto. cualquier código que utili ce una interfaz concreta sabrá qué métodos pueden invocarse para di cha interfaz yeso es todo. Por tanto, la interfaz se utiliza para establecer un "protocolo" entre las clases (algunos lenguaj es de programación orientados a objetos di sponen de una palabra clave denominada pr%col para hacer lo mi smo). Sin embargo, una interfaz es algo más que simplemente una clase abstracta llevada hasta el extremo, ya que permite rea li zar una varian te del mecanismo de " herencia múltiple" creando una clase que pueda generalizarse a más de un tipo base. Para crear una interfaz, utilice la palabra clave interface en lugar de class. Al igual que COIl una clase, puede añadir la palabra clave public antes de interface (pero sólo si dicha interfaz está definida en un archivo del mi smo nombre). Si no incluimos la palabra clave public, obtendremos un acceso de tipo paquete, porque la interfaz sólo será utilizable dentro del mismo paquete. Una interfaz también puede contener campos, pero esos campos serán im plíci tamente de tipo static y final. Para definir una clase que se adapte a una interfaz concreta (o a un gmpo de interfaces concretas), utilice la palabra clave implements que quiere decir: " La interfaz especifica cuál es el aspecto, pero ahora vamos a decir cómo fimciono". Por lo demás, la defini ción de la clase derivada se asemeja al mecanismo normal de herencia. El diagrama para el ejemplo de los instnlmentos musicales sería el siguiente:
9 Interfaces 193
Instrument void play(); String what(); void adjust();
imPleTents
impl ments
Wind void play() String what() void adjust()
Percussion void play() String what() void adjust()
ext nds
Stringed void play() String what() void adjust()
extf nds
Brass
Woodwind void play() String what()
imple~ents
void play() void adjust()
Podemos ver en las clases Woodwind y Brass que una vez que hemos implementado la interfaz, la implementación pasa a ser una clase normal que puede ampliarse de la forma usual. Podemos declarar explícitamente los métodos de una interfaz como public, pero esos métodos serán públicos aún cuando no lo especifiquemos. Por tanto, cuando implementemos una interfaz, los métodos de esa interfaz deben estar definidos como públicos. En caso contrario, se revertiría de f0n113 predeterminada al acceso de tipo paquete, con lo que estaríamos reduciendo la accesibilidad de los métodos durante la herencia, cosa que el compilador de Java no permite. Podemos ver esto en la versión modificada del ejemplo Instrumeot. Observe que todos los métodos de la interfaz son estrictam en te una declaración, que es lo único que el compilador pemite. Además, ninguno de los métodos de Instrument se declara corno public, pero de lodos modos son públicos de manera automática:
11 : interfaces / musicS / MusicS . java
II
Interfaces . package interfaces.mus icS¡ i mport p olymorphism.music.Not e ; i mport stati c net . mindview util . Print .* int erface I nstrumen t { II Constante de t i empo de compilación: int VALUE = 5; II static & final II No puede tener definiciones de métodos : voi d play (Not e n); II Automáticamente público voi d adjust() ¡
class Wind i mplements I nstrume nt { public v oid p l ay (Note n ) { print(t hi s +".p lay () "+ n) ¡ public String toString () { r eturn "Wind " ¡ } publ i c void adjust () {print (this + ". adjust () " ) ;
194 Piensa en Java class Percussion implements Instrument publ ic void play (Note n) { print (this + ".play() " + n); public String toString() { return uPercussion"; } public void adjust () { print (this + ". adjust () ") ;
class Stringed implements Instrument public void play (Note n) { print (this + ".play() " + n) i public String toString () { return UStringed" i } public void adjust () { print (this + ". adjust () ,,) ;
class Brass extends Wind { public String toString () { return uBrass"
i
}
class Woodwind extends Wind { public String toString () { return "Woodwind!l;
}
public class MusicS { II No le preocupa el tipo, por lo que los nuevos tipos II que se añaden al sistema seguirán funcionando: static void tune (Instrument i) {
// i.play(Note.MIDDLE_C) ; static void tuneAll (Instrument [] for(Instrument i : e) tune (i);
e)
{
public static void main (String [] args) { II Generalización durante la inserción en la matriz: Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed () , new Brass () , new Woodwind () ); tuneAll(orchestra) ;
1* Output: Wind.play() MIDDLE_C Percussion.play(} MIDDLE_C Stringed.play() MIDDLE_C Brass.play() MIDDLE_C Woodwind.play() MIDDLE_C * ///,En esta versión del ejemplo hemos hecho otro cambio: el método \Vhat() ha sido cambiado a toString(), dado que esa era la fOnTIa en que se estaba utilizando el método. Puesto que toString() forma parte de la clase raíz Object, no necesita aparecer en la interfaz. El resto del código funciona de la misma manera. Observe que no importa si estamos generalizando a una clase "nonnar' denominada Instrument. a una clase abstracta llamada lnstrurncnt, o a una interfaz denominada Instrument. El compor-
9 Interfaces 195 tamiento es siempre el mismo. De hecho. podemos ver en el método lune( ) que no existe ninguna c\idencia acerca de si Instrumcn t es una clase "nannal", una clase abstracta o una interfaz. Ejercicio 5:
(2) Cree una interfaz que contenga Ires métodos en su propio paquete. Implemente la interfaz en un paquete diferente.
Ejercicio 6:
(2) Demuestre que lodos los métodos de una ¡merfaz son automáticamente públicos.
Ejercicio 7:
( 1) Mod ifique el Ejercicio 9 del Capítu lo 8. Polimorfismo, para que Roden! sea una interfaz.
Eje rcicio 8:
(2) En polymorphism'sandwich.java. cree una interfaz denominada FastFood (con los métodos apropiados) y cambie Sandwich de modo que también implemente FastFood .
Eje rcicio 9:
(3) Rediseñe Music5.jav3 moviendo los métodos comunes de \Vind . Percussion y Stringed a una clase abstracta.
Eje rcicio 10:
(3) Modifique MusicS.java añadiendo una interfaz Pla)'ab le. Mueva la declaración de playO de lnstrument a Playable. Afiada Playable a las clases derivadas incluyéndola en la lista implements. Modifique tun e() de modo que acepte un objeto Playable en lugar de un objeto Instrument.
Desacoplamiento completo Cuando un método funciona con una clase en lu ga r de con una interfaz, estamos limitados a utili za r dicha clase o sus subclases. Si quisiéramos ap li car ese método a una clase que no se encontrara en esa jerarquía, no podríamos. Las interfaces relajan esta restri cción considerablemen te. Como resu ltado, permiten escribir código más reutil izab le. Por ejemplo. supo nga que disponemos de una clase Processor que ti ene sendos métodos Dame () y process( ) que toman una cierta entrada, la modifican y generan una salida. La clase base se puede amp li ar para crear diferentes tipos de objetos Processo r . En este caso, los subtipos de Processor modifican objetos de tipo String (observe que los tipos de retomo pueden ser covariantes, pero no los tipos de argumentos):
11: interfaces/classprocessor/Apply.java package interfaces.classprocessor¡ import java.util.*¡ import static net.mindview util.Print.*¡ class Processor { public String name() return getClass () . getSimpleName () ; Object process (Object input)
{ return input ¡ }
class Upcase extends Processor { String process (Obj ect inputl { l/Retorno covariante return ((Stringl input ) .toUpperCase ();
class Downcase extends Processor { String process (Object input ) { return «String) input) . toLowerCase () ;
class Splitter extends Processor { String process (Object input) { II El método split() divide una cadena en fragmentos: return Arrays. toString ( ( (String) input) . spl i t ( " tl » ¡
196 Piensa en Java public class Apply { public stacic void process (Processor p, Object s ) { print ( "Using Processor 11 + p. name () ) i print (p.process {s )) ;
public sta tic String s "Disagreement with beliefs i5 by definition incorrect"; public static void main (String [ ) args ) {
process (new Upcase () , s ) i process (new Downcase () , s) process (new Splitter() s) I
i i
1* Output: Using Processor Upcase DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT Using Processor Downcase disagreement witb beliefs ls by definition incorrect Using Processor Splitter [Disagreement, wich, beliefs, is, by, definition, incorrect] * /// ,El método Apply.process() toma cualquier tipo de objeto Processor y lo aplica a un objeto Object, imprimiendo después los resultados. La creación de un método que se comporte de fonna diferente dependiendo del objeto argumento que se le pase es lo que se denomina el patrón de diseño basado en estrategias. El método contiene la parte fija del algoritmo que hay que implementar, mientras que la estrategia cOOliene la parte que varia. La estrategia es el objeto que pasamos, y que contiene el código que hay que ejecutar. Aquí, el objeto Processor es la estrategia y en main( ) podemos ver cómo se aplican tres estrategias diferentes a la cadena de caracteres s. El método split( ) es parte de la clase String; toma el objeto String y lo divide utilizando el argumento como frontera, y devolviendo una matri z Stringll . Se utiUza aquí como forma abreviada de crear una matriz de objetos String. Ahora suponga que descubrimos un conjunto de filtros electrónicos que pudieran encajar en nuestro método Apply.process( ): / / : interfaces / filters / Waveform.java package interfaces.filters; public class Waveform { private static long counter; private final long id = counter++; public String t o String () { return "Waveform " + id; }
1/ / , // : interfaces / filters / Filter.java package interfaces.filters; public class Filter { public String name () return getClass () .getSimpleName () ; public Waveform process (Waveform input ) { return input;
/ / /> // : interfaces / filters / LowPass.java package interfaces.filters; public class LowPass extends Filter { double cutoff; public LowPass (double cutoff) { this . cutoff public Waveform process (Waveform input ) return input; // Dummy processing
cutoff; }
}
9 Interfaces 197
/1: interfacesjfilters/HighPass.java package interfaces.filtersi public class HighPass extends Filter { double cutoff; public HighPass (double cutoff) { this. cutoEf :: cutoff i } public Waveform process (Waveform input) { return input; }
///,1/: interfaces/filters/BandPass.java package interfaces.filtersi public class BandPass extends Filter { double lowCutoff, highCutoff; public BandPass (double lowCut,
double highCut)
{
lowCutoff = lowCut¡ highCutoff = highCut ¡ public Waveform process (Waveforrn input)
{ return input;
}
/// , Filter tiene los mismos elementos de interfaz que Processor, pero puesto que no hereda de Processor (puesto que el creador de la clase Filter no tenía ni idea de que podríamos querer usar esos objetos como objetos Processor). no podemos utilizar un objeto Filter con el método Apply.process( ), a pesar de que funcionaría. Básicamente, el acoplamiento entre Apply. process( ) y Processor es más fuerte de lo necesario y esto impide que el código de Apply.processO pueda reutilizarse en lugares que sería útil. Observe también que las entradas y salidas son en ambos casos de tipo Waveform . Sin embargo, si Processor es una interfaz, las restricciones se relajan lo suficiente como para poder reutili zar un método Apply. process( ) que acepte dicha interfaz. He aquí las versiones modificadas de Processor y Apply: //: interfaces/interfaceprocessor/Processor . java package interfaces.interfaceprocessor; public interface Proeessor { String name () ; Objeet process(Object input);
/// ,//: interfaces/interfaeeproeessor/Apply.java package interfaees.interfaceprocessor¡ import static net.mindview.util.Print .* ¡
public class Apply { public static void process(Proeessor p, Objeet s) { print ("Using Processor ti + p. name () ) ; print(p.process(s» ;
La primera fo nna en que podemos reutilizar el códi go es si los programadores de clientes pueden escribir sus clases para que se adapten a la interfaz, como por ejemplo: //: interfaces/interfaceproeessor/StringProcessor.java package interfaces.interfaeeprocessor; import java.util.*; publie abstraet class StringProcessor implements Processor{ publ ic String name () { return getClass () . getSimpleName () ¡ public abstraet String proeess(Objeet input); public static String s ~ "If she weighs the same as a duck, shets made of wood";
198 Piensa en Java
public static void main(String[] args) Apply.process(new Upcase(), s); Apply.process{new Downcase(), s); Apply.process(new Splitter(), s);
class Upcase extends StringProcessor { public String process (Object input) { / / Retorno covariante return ((String) input) . toUpperCase () ;
class Downcase extends StringProcessor { public String process (Object input)
{
return ((String ) input) . toLowerCase() ;
class Splitter extends StringProcessor { public String process (Object input ) { return Arrays. toStríng ( ( (String) input ) . split (It " )) ;
1* Output: Using Processor Upcase IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MACE OF WOOD Using Processor Downcase if she weighs the same as a duck, she's made of wood Using Processor Splitter [If, she, weighs, the, same, as, a, duck, , she's, made, of, wood]
* //1 , Sin embargo, a menudo nos encontramos en una situación en la que no podemos modificar las clases que queremos usar. En el caso de los filtros electrónicos. por ejemplo, la correspondiente biblioteca la hemos descubierto, en lugar de desarrollarla. En estos casos, podemos utilizar el patrón de diseiio adaprador. Con dicho patrón de diseño, lo que hacemos es escribir código para lOmar la interfaz de la que disponemos y producir la que necesitamos, como por ejemplo: //: interfaces/interfaceprocessor/FilterProcessor.java package interfaces.interfaceprocessor; import interfaces.fiIters.*¡ cIass FiIterAdapter implements Processor Filter filter¡ public FilterAdapter(Filter filter) { this.filter : filter¡ public String nameO { return filter.name() ¡ public Waveform process (Object input) { return filter.process((Waveform)input);
public class FilterProcessor { public static void main(String[] args) { Waveform w = new Waveform(); Apply.process(new FilterAdapter(new LowPass(l.O)), w); Apply.process(new FilterAdapter (new HighPass(2.0)}, w) ¡ Apply.process( new FilterAdapter{new BandPass{3 . 0, 4.0 )), w) i
9 Interfaces 199
} / * Output, Using Processor LowPass Waveform o
Using Processor HighPass Waveform o Using Processor BandPass Waveform O
* /// > En esta aplicación, el patrón de di seño de adaptación, el constmctorFilterAdapter, toma la interfaz que tenemos (Filter)
y produce un objeto que tiene la interfaz Processor que necesitamos. Observe también la utilización del mecanismo de delegación en la clase FilterAdapter. Desacoplar la interfaz de la implementación pennite aplicar las interfaces a múltiples implementaciones diferentes, con lo que el código es más reutilizable. Ejercicio 11:
(4) Cree una clase con un método que tome como argumento un objeto String y produzca un resultado en el que se intercambie cada pareja de caracteres contenida en el argumento. Adapte la clase para que fun-
cione con interfaceprocessor.Apply.process().
"Herencia múltiple" en Java Puesto que una interfaz no dispone de implementación (es decir, no hay ningún almacenamiento asociado con una interfaz) no hay nada que impida combinar varias interfaces. Esto resulta muy útil en ocasiones, como por ejemplo cuando queremos impleme ntar el concepto " una x es una a y una b y una c". En e++, este acto de combinar múltiples interfaces de clase se denomina herencia múltiple, y puede 1Iegar a resultar muy completo, porque cada clase puede tener una implementación. En Java, podemos hacer lo mismo, pero sólo una de las clases puede tener una implementación, por lo que los problemas de e++ no aparecen en Java cuando se combinan múltiples interfaces: Clase base
I
interfaz 1
abstracta o concreta
I I
Métodos de la clase base
I
interfaz 1
I
interfaz 2
interfaz 2
1 ...
.......
1 ...
I
intet n
I
I
interfaz n
I
En una clase deri vada , no estamos obligados a tener una clase base que sea abstracta o concreta (una que no tenga métodos abstractos). Pero si realizamos la herencia de algo que no sea una interfaz, sólo podemos heredar de una de esas clases; los restantes elementos base deberán ser interfaces. Hay que colocar todos los nombres de interfaz detrás de la palabra clave implements y separarlos mediante comas. Podemos incluir tantas interfaces como queramos y podemos realizar general izaciones (upeast) a cada interfaz, porque cada una de esas interfaces representa un tipo independiente. El siguiente ejemplo muestra una clase concreta que se combina con varias interfaces para producir una nueva clase:
11 : interfaces/Adventure.java II Interfaces múltiples . interface CanFight void fight () i
interface CanSwim void swim ( ) i
interface CanFly {
200 Piensa en Java voidfly(l;
class ActionCharacter { public void fight 11
{}
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly public void swim (1 {} public void fly (1 {}
public class Adventure { public public public public public
static void t (CanFight x ) { x. fight () i static void u(CanSwim x l { x.swim (} i static void v (CanFly xl ( x. fly (); ) static void w (ActionCha racter xl { x. f ight () ; static void main (String [] args) {
Hero h = new Hero( ) ;
II II v(hl; II w(hl; II t(hl;
u (hl;
Treat i t Treat it Treat it Treat it
as as as as
a CanFight a CanSwim a CanFly an ActionCharacter
) 111> Puede ver que Bero combina la clase concreta ActionCharacter con las interfaces CanFight, CanSwim y CanFly. Cuando se combina una clase concreta con illlerfaces de esta forma, la clase concreta debe expresarse en primer lugar y las interfaces indicarse a continuación (en caso contrario, el compilador nos dará un error). La signatura de fight() es igual en la interfaz CanFight y en la clase ActionCharacter. Asimismo, a fight() no se le proporciona una definición en Hero. Podemos amp liar una interfaz, pero lo que obtenernos entonces será otra interfaz. Cuando queramos crear un objeto, todas las definiciones deberán haber sido ya proporcionadas. Aunque "ero no proporciona explícitamente una defini ción para fight(), di cha definición está incl uida en ActionCharacter; por tanto, es posi ble crear objetos Hero. En la clase Adventure, podemos ver que hay cuatro métodos que toman argumentos de las distintas interfaces y de la clase concreta. Cuando se crea un objeto Hero, se le puede pasar a cualquiera de estos métodos, lo que significa que estará siendo generalizado en cada caso a cada una de las interfaces. Debido a la forma en que se diseñan las interfaces en Java, este mecanismo funciona sin que el programador tenga que preocuparse de nada. Recuerde que una de las principales razones para utilizar interfaces es la que se ilustra en el ejemplo anterior: para reali za r generalizaciones a más de un tipo base (y pode r disfrutar de la fl exibilidad que esto proporciona). Sin embargo, una segunda razón para uti li zar interfaces coincide con la raZÓn por la que utili zamos clases base abstrac tas: para impedir que el programador de clientes cree un objeto de esta clase y para establecer que sólo se trata de una interfaz. Esto hace que surja una cuestión: ¿debemos utili zar una interfaz o una clase abstracta? Si resulta posible crear nuestra clase base sin ninguna definición de método y sin ninguna vari able miembro, siempre son preferibles las interfaces a las clases abstractas. De hecho, si sabemos que algo va a ser una clase base, podemos considerar si resultaría conveni ente transformarla en interfaz (hablaremos más sobre este tema en el resumen del capítu lo). Ejercicio 12:
(2) En Adventure.java, añada una interfaz llamada CanClimb, sigu iendo el patrón de las otras interfaces.
Ejercicio 13:
(2) Cree una interfaz y herede de ella otras dos nuevas interfaces. Defina, mediante herencia múltiple, una tercera interfaz a partir de estas otras dos. 2
.2
Este ejemplo muestra cómo las interfaces evitan el denominado ··problema del rambo", que se presenta en el mecanismo de herencia mú lti ple de C++.
9 Interfaces 201
Ampliación de la interfaz mediante herencia podemos añadir fácilmente nuevas dec laraciones de métodos a una interfaz utilizando los mecanismos de herencia, y también podemos combinar va rias interfaces mediante herencia para crear una nueva interfaz. En ambos casos, obtendremos una interfaz llueva, como se ve en el siguiente ejemplo:
/1 : interfaces/HorrorShow.java JI Ampliación de una interfaz mediante herencia. interface Monster void menace () ;
interface DangerousMonster extends Monster { void destroy ()
i
interface Lechal void kill () ;
c!ass DragonZilla implements DangerousMonster { public void menace () {} public void destroy () {}
interface Vampire extends DangerousMonster, Lethal { void drinkBlood();
class VeryBadVampire implements Vampire { public void mena ce () {} pub1ic void destroy () {} pub1ic void kill () {} public void drinkB100d () {}
public class HorrorShow { static void u(Monster b) { b.menace(); static void v (DangerousMonster d) { d.menace() ; d.destroy () ; static void w(Letha1 1 )
{ l.kill( ) ;
}
public static void main(String[] args) DangerousMonster barney = new DragonZilla() u (barney ) i v (barney) i new VeryBadVampire() i Vampire vlad
i
u (v1 ad ) ; v(v1ad) ;
w (vladl ;
) /// ,Dangcrous~lonster es una extensión simple de Monster que produce una nueva interfaz. Ésta se implementa en DragonZilla.
202 Piensa en Java La siI1laxis empleada en Vampire sólo funciona cuando se heredan interfaces. Nonna lmente, sólo podemos utilizar extends con una única clase. pero extends puede hacer referencia a múltiples interfaces base a la hora de construir una nueva interfaz. Como puede ver, los nombres de interfaz es tá simplemente separados por comas. Ejercicio 14:
(2) Cree tres interfaces, cada una de ellas con dos métodos. Defina mediante herencia una nueva interfaz que combine las tres, añadiendo un nuevo método. Cree una clase implerncnrando la nueva interfaz y que también herede de una clase concreta. A continuación, escriba cuatro métodos, cada uno de los cuales tome una de las cuatro interfaces C01110 argumento. En main(), cree un objeto de esa clase y páselo a cada uno de los métodos.
Ejercicio 15:
(2) Modifique el ejercicio anterior creando una clase abstracta y haciendo que la clase derivada herede de ella.
Colisiones de nombres al combinar interfaces Podernos encontramos con un pequeño problema a la hora de implementar múltiples interfaces. En el ejemplo anterior, tanto CanFight como ActionCharacter tienen sendos métodos idénticos void fight( ). El que haya dos métodos idénticos no resulta problemático, pero ¿q ué sucede si los métodos difieren en cuanto a signatura o en cuanto a tipo de retorno? He aquí un ejemplo:
11 : interfaces/lnterfaceCollision.java package interfaces; interface interface interface class e {
Il void f(); } I2 int f(int i) ; int f (); } I3 public int f() { return 1 ;
} }
class C2 implements 11, 12 public void f () {} public int f (int i) { return 1; } II sobrecargado
class C3 extends C implements 12 { public int f (int i ) { return 1; } II sobrecargado
class C4 extends C implements 13 II I déntico. No hay problema: public int f () { return 1; }
II Los métodos sólo difieren en el tipo de retorno: II! class CS extends C implements 11 {} //! interface 14 extends Il, I3 {} ///,La dificultad surge porque los mecanismos de anulación, de implementación y de sobrecarga se entremezclan de forma compleja. Asimismo, los métodos sob recargados no pueden diferir sólo en cuanto al tipo de retorno. Si quitarnos la marca de comentario de las dos últimas líneas, los mensajes de error nos informan del problema:
lnterfaceCollisionjava:23: f( ) in e CanllO( implemenl f( ) in 11 ; attempting lO use incompatible relurn type foune!: inr required: void lntelfaceCollisionjava:24: Inlelfaces /3 ane! 11 are incompatible; bOlh dejinef( ), b1ll \Vith different relUrn Iype Asimismo, utilizar los mismos nombres de método en diferentes interfaces que vayan a ser combinadas suele aumentar, generalmente, la confusión en lo que respecta a la legibilidad del código. Trate de evi tar la utilización de nombres de método idénticos.
9 Interfaces 203
Adaptación a una interfaz Una de las razones más importantes para utili zar interfaces consiste en que con ellas podemos di sponer de múltiples implementaciones para una misma interfaz. En los casos más simples, esto se lleva a la práctica empleando un méwdo que acepta una inte rfaz, lo que nos deja total libertad y responsabilidad para implementar dicha interfaz y pasar nuestro objeto a dicho
método. Por tanto, uno de los usos más comunes para las interfaces es el patrón de diseño basado en estrategia del que ya hemos hablado: esc ribimos un método que realice ciertas operaciones y dicho método toma como argumento una interfaz que especifiquemos. Básicamente. lo que estamos diciendo es: " Puedes utilizar mi método con cualquier objeto que quieras, siempre que éste se adapte a mi interfaz". Esto hace que el método sea más flexible , general y reutili zable. Por ejemplo, el constructor para la clase Scanner de Java SES (de la que hablaremos más en detalle en el Capítulo 13 , Cadenas de caracteres) admite una interfaz Readable. Como veremos, Readable no es un argumento de ningún otro método de la biblioteca estándar de Java, fue creado pensando específicamente en Scanner, de modo que Scanner no tenga que restringir su argum ento para que sea una clase determinada. De esta forma , podemos hacer que Scanner funcione con más tipos de datos. Si creamos una nueva clase y queremos poder usarla con Scanner, basta con que la hagamos de tipo Readable, como por ejemplo:
/1: interfaces/RandomWords . java /1 Implementación de una interfaz para adaptarse a un método. import java.nio. * ; import java.util .* ; public class RandomWords implements Readable { private static Random rand = new Random(47) ; private static final char[) capitals = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray () ; private static final char [] lowers = "abcdefghijklmnopqrstuvwxyz".toCharArray() ; private static final char[] vowels = "aeiou lt • toCharArray () ; private int caunt; public RandomWords (int count) { this .count count ¡ } public int read (CharBuffer cb) ( if(count-- == O) return -1; // Indica el final de la entrada cb.append(capitals[rand.nextlnt(capitals.length)]) ; for(int i = O; i <: 4; i++) { cb.append(vowels[rand.nextlnt(vowels.length)]) ; cb . append(lowers[rand.nextInt(lowers.length)]) ; cb.append(" ti); return 10; /1 Número de caracteres añadidos public static void main (S tring [] args ) { Scanner S = new Scanner(new RandomWords(10»¡ while(s.hasNext(» System.out.println(s . next(») ; / * Output : Yazeruyac Fowenucor Goeazimom Raeuuacio Nuoadesiw Hageaikux Ruqicibui Numasetih
204 Piensa en Java
Kuuuuozog Waqizeyoy *///,-
La interfaz Readablc sólo requiere que se implemente un método read( ). Dentro de read( ), añadimos la infonnación al argumento C harBuffer (hay varias formas de hacer esto, consulte la documentación de CharB uffer), o devolvemos -] cuando ya no haya más datos de entrada. Supongamos que disponemos de una clase base que aún no implementa Readable. en este caso, ¿cómo podemos hacer que funcione con Scanner? He aquí un ejemplo de una clase que genera números en coma flotante aleatorios. ji: interfaces/RandornDoubles.java import java.util. *¡
public class RandornDoubles private static Random rand = new Random(47)¡ publ ie double next ()
( return rand . nextDouble ()
i
publ ic static void main(String[] argsl { RandomDoubles rd = new RandomDoubles(); for(int i := Di i < 7; i ++) System.out . print(rd.next() + 11
");
1* Output: 0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0 . 2613610344283964
*///,De nuevo. podemos utili zar el patrón de di seño adaptador, pero en este caso la clase adaptada puede crearse heredando e implementando la interfaz Readable. Por tanto, si utilizamos la heren cia pseudo-múltiple proporcionada por la palabra clave interface, produciremos una nueva clase que será a la vez Ra ndomDoubles y Readable:
11: interfaces/AdaptedRandornDoubles.java 11 Creación de un adaptador mediante herencia. import java.nio . *; import java.util. *¡ public class AdaptedRandornDoubles extends RandomDoubles implements Readable { private int count; public AdaptedRandornDoubles(int count} { this.count = count¡ public int read(CharBuffer cb) { if(count- - == O) return -1; String resul t = Double. toString (ne xt (» cb. append (resu lt) ; return result.length();
+
U
11
¡
public static void main (String [] args) { Scanner s = new Scanner(new AdaptedRandomDoubles(7» ¡ while(s.hasNextDouble () ) System.out.print(s.nextDouble() + i 11
"
)
1* Output: 0.7271157860730044 0.5309454508634242 0 .1 6020656493302599 0.18847866977771732 0.5166020801268457 0 . 2678662084200585 0 . 2613610344283964
* /// ,Puesto que podemos añadir de esta fomla Ulla interfaz a cualquier clase ex istente, podemos deducir que un método que tom e como argumen to una interfaz nos permitirá adaptar cualquier c lase para que funcione con dicho método. Aquí radica la verdadera potencia de utiliza r interfaces en lugar de clases.
9 Interfaces 205 Ejerci cio 16:
(3) Cree una clase que genere una secuencia de ca racteres. Adapte esta clase para que pueda utilizarse COlll0 entrada a un objeto Scanner.
Campos en las interfaces Puesto que cualquier campo que incluyamos en una interfaz será automáticamente de tipo static y final , la interfaz constituye una herramienta convenicmc para crear gm pos de valores constantes. Antes de Java SES, ésta era la única fonna de produci r el mismo efecto que con la palabra clave enum en e o C++. Por tanto, resulta habitual encontrarse con código anterior a la versión Java SES que presenta el aspecto sigui ente: ji : interfaces / Months.java
1/ Uso de interfaces para crear grupos de constantes. package interfaces; public interface Months int JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4, MAY = S, JUNE = 6, JULY 7, AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10, NOVEMBER = 11, DECEMBER = 12,
/// ,Observe la utilización del esti lo Java, en el que todas las letras están en mayúsc ulas (con guiones bajos para separar las dislintas palabras que formen un determinado identificador) en los casos de valores estáticos finales con inicializadores constantes. Los campos de una interfaz son automáticamente públicos, asi que el atributo public no se especifica explícitamente. Con Java SES. ahora disponemos de la palabra clave enum, mucho más potente y flexible, por lo que rara vez tendrá sentido que utilicemos interfaces para definir constantes. Sin embargo. quizá se encuentre en muchas ocasiones con esta técnica antigua a la hora de leer código heredado (los su plementos de este libro disponibles en Wlvlv.MindView.nef proporcionan una descripción completa de la técnica previa a Java SES para producir tipos enumerados utilizando interfaces). Puede encontrar más detalles sobre el uso de la palabra clave enum en el Capítulo 19, Tipos enumerados. Ejercicio 17:
(2) Demuestre que los campos de una interfaz son implícitamente de tipo sta tic y final .
Inicialización de campos en las interfaces Los campos defin idos en las interfaces no pueden ser valores "finales en blanco", pero pueden inicializarse con expresiones no constantes. Por ejemplo:
11 : interfaces / RandVals.java II Inicialización de campos de interfaz con II inicializadores no constantes. i mport java.util.*i public interface RandVals Random RAND = new Random (47 } ; int RANDOM_INT = RAND.nextInt (lO } ; long RANDOM_ LONG = RAND.nextLong () * 10; float RANDOM_FLOAT = RAND.nextLong() * 10; double RANDOM_DOUBLE = RAND.nextDouble ( ) * 10;
/// ,Puesto que los campos son estáticos, se inicializan cuando se carga por primera vez la clase, lo que tiene lugar cuando se accede por primera vez a cualquiera de los campos. He aquí una prueba simple:
11 : interfaces / TestRandVals.java import static net.mindview.util.Print.*; public class TestRandVals {
206
Piensa en Java public static void main (String [] args ) print (RandVals.RANDOM INT ) i print (RandVals.RANDOM LONG ) ; print {RandVals.RANDOM FLOAT ) i print (RandVals.RANDOM DOUBLE ) ;
(
/ * Output: 8 -32032247016559954 -8.5939291E18 5.779976127815049 *///,-
Los campos, por supuesto, no fonnan parte de la interfaz. Los valores se almacenan en el área de almacenamiento estático correspondiente a dicha interfaz.
Anidamiento de interfaces Las interfaces pueden anidarse dentro de clases y dentro de otras interfaces. 3 Esto nos revela una serie de características in teresantes: //: interfaces/nesting/Nestinglnterfaces.java package interfaces.nesting; class A interface B void f () ; public class Blmp implements B { public void f () {} private class Blmp2 implements B { public void f () {} public interface C { void f () ; class Clmp implements C { pUblic void f () {} private class Clmp2 implements C { public void f () {} private interface O { void f () ; private class Dlmp implements O { public void f () {} public class Dlmp2 implements O ( public void f () {}
public D getD () { return new DImp2 (); pri vate O dRef; public void receiveD (D d) { dRef = d; dRef. f () ;
}
] Grac ias a Martin Danner por hacer una pregunta a este respecto en un seminario.
9 Interfaces 207
interface E { interface G void f () ;
JI "public" redundante: public interface H { void f () ; void 9 () ;
JI
No puede ser private dentro de una interfaz:
li t private interface
1
{}
public class Nestinglnterfaces public class Blmp implements A.S public void f 1) {} class Clmp implements A.e { public void f () {}
// No se puede implementar una interfaz privada excepto JI dentro de la clase definitoria de dicha interfaz : // ! class Dlmp implements A . D {
II ! public void II ! }
f ()
{}
class Elmp implements E public void 9 1)
{}
class EGlmp implements E.G { public void f 1) {} clas s Elmp2 implements E { public void 9 1) {}
class EG implements E . G public void f ()
{}
public static void main (String [] A a = new A () i JI No se puede acceder a A.D: II! A.O ad = a.getOI );
JI
args)
{
S610 puede devolver a A.O:
II ! A.Olmp2 di2 = a . getOI ) ;
JI
No se puede acceder a un miembro de la interfaz:
II! a.getOI ) .f(); / / S610 otra A puede utilizar getD () : A a2 = new A () ; a2.receiveD(a.getD()) ;
}
111 ,La sintaxis para anidar una interfaz dentro de una clase es razonablemente amplia. Al igual que las interfaces no anidadas, las anidadas pueden lener visibilidad pública o con acceso de paquele. Como característica adicional, las interfaces también pueden ser privadas, como podemos ver en A.D (se necesita la misma sintaxis de cualificación para las interfaces anidadas que para las clases anidadas). ¿Para qué sirve una interfaz anidada privada? Podemos suponer que sólo puede implementarse como clase interna privada, como en DImp, pero A.DImp2 mues-
208 Piensa en Java
tra que también puede implementarse como clase pública. Sin embargo. A.Dlmp2 sólo puede utili zarse co mo ella misma. No se nos pennite mencionar el hecho de que implementa la interfaz privada D, por lo que implementar interfaces privadas es una fomla de forzar la defini ción de los métodos de dicha interfaz sin añadi r ninguna información de tipos (es decir, sin permitir ninguna generalización). El método getD() nos reve la un dato adicional acerca de las interfaces privadas: se trata de un método público que devuel~ ve una referencia a un interfaz privada. ¿Qué podemos hacer con el valor de retomo de este método? En main(), podemos ver varios intentos de utili za r el va lor de re tomo, todos los cua les fallan. La única cosa que funciona es entregar el valor de retomo a un objeto que tenga penniso para usarlo. qu e en este caso es orro objeto A, a través del método receiveD( ). La interfaz E mues tra que podemos ani dar unas interfaces dentro de otras. Sin embargo, las reglas acerca de las interfaces. en particular, qu e lodos los elementos de la interfaz tienen que ser públicos, se imponen aquí de manera estricta, por lo que una interfaz anidada dentro de otra será automáticamente pública y no puede nun ca definirse como privada. Nestinglnterfaces muestra las diversas fonnas en que pueden implementarse las interfaces anidadas. En particular, obser~ ve que, cuando implementamos una interfaz, no estamos obligados a implemenrar ninguna de las interfaces anidadas de n~ tro de ella. Asimismo, las interfaces pri vadas no pueden impl ementarse fuera de sus clases definitorias. Inicia lmente, pudiera parecer qu e estas característi cas sólo se hubieran miad ido para gara nti zar la coherencia si ntác tica, pero mi ex periencia es que una vez que se conoce una característica siempre se descubren ocasiones en las que puede resultar útil.
Interfaces y factorías El objeto principal de una interfaz es pem1itir la existencia de múltiples implement aciones, y una fonna típica de producir objetos que encajen con una interfaz es el denominado patrón de diselio de método factoría. En lugar de llamar a un cons~ tructor directamente. invocamos un método de creación en un objeto factoría que produce una impl ementación de la inter~ faz; de esta forma. en teoría, nuestro cód igo estará completamente ais lado de la implementac ión de la interfaz, haciendo así posib le intercambiar de manera transparente una implementación por otra. He aquí un ejemplo que muestra la estructura de l método factoría: // : interfaces/Factories.java import static net.mindview.util.Print.*; interface Service void methodl () ; void method2 () j
interface ServiceFactory Service getService();
class Implementationl implements Service { Implementationl () {} /1 Package access public void methodl () {print (" Implementationl methodl");} public void method2 () {print ( "Implementationl method2");}
class ImplementationlFactory implements ServiceFactory { public Service getService () ( return new Implementationl{);
class Implementation2 implements Service { Implementacion2 () {} /1 Acceso de paquete public void methodl () {print (" Implementation2 methodl n) j}
9 Interfa ces 209 public void method2 ()
{print (JI Implementation2 method2")
i }
class Implementation2Factory implements ServiceFactory { public Service getService () { return new Implementacion2();
public c!ass Factaries { public static void serviceConsumer(ServiceFactory face) Service s = fact . getService(); s.methadl() ; s.method2() ; public static void main(String[] args ) serviceConsumer(new ImplementationlFactory()) i // Las implementaciones son completamente intercambiables: serviceConsumer(new Implementation2Factory());
/ * Output: Imp lementacianl Implementatian! Implementation2 Imp lementation2
methadl mechod2 methadl method2
*///,Sin el método factoría, nuestro código tendría que especificar en algún lugar el tipo exacto de objeto Service que se estuviera creando, para poder invocar el constnlctor apropiado. ¿Para qué sirve añadir este nivel adicional de indirecci ón? Una razón común es para crear un marco de trabajo para el desarrollo. Suponga que estam os creando un sistema para juegos que pennita, por ejemplo, jugar tanto al ajedrez como a las damas en un mismo tablero.
JI: interfaces/Games.java II Un marco de trabajo para
juegos utilizando métodos factoría. import static net.mindview.util . Print.*¡
interface Game { boolean move(); } inter face GameFactory { Game getGame(); class Checkers implements Game { private int moves = O; private static final int MOVES = 3; public boolean move () { print ("Checkers move " + moves); return ++moves != MOVES;
class CheckersFactory implements GameFactory { public Game getGame() { return new Checkers() ¡
class Chess implements Game { private int moves = O; private static final int MOVES public boolean move () { print (ItChess move ti + moves); return ++moves != MOVES;
4;
210 Piensa en Java
class ChessFactory implements GameFactory { public Game getGame () { return new Chess () i
public clas9 Games { public static void playGame(GameFactory factory) Game s = factory getGame(); while(s.move())
{
public static void main(String[] args) playGame(new CheckersFactory{)); playGame (n ew ChessFactory());
/ * Output: Checkers move O Checkers move 1 Checkers move 2 Chess move O Chess move 1 Chess move 2 Chess move 3 * /// ,Si la clase Carnes representa un fragmenlO complejo de código, esta técnica permite reutilizar dicho código con diferentes tipos de juegos. Podemos fácilmente imaginar otros juegos más elaborados que pudieran beneficiarse a la hora de desarrollar este diseño. En el siguiente capítulo, veremos una fonna más elegante de implementar las factorías utili zando clases internas anónjmas. Ejercicio 18:
(2) Cree una interfaz Cycle, con implementaciones Unicycle, Bicycle y Tricycle. Cree fac torias para cada tipo de Cycle y el código necesario que utilicen estas factorías.
Ejercicio 19:
(3) Cree un marco de trabajo utilizando métodos factoría que pennita simular las operaciones de lanzar una moneda y lanzar un dado.
Resumen Resulta bastante tentador concluir que las interfaces resultan útiles y que, por tanto, siempre son preferibles a las clases concretas. Por supuesto, casi siempre que creemos una clase, podemos crear en su lugar una interfaz y una factoría. Mucha gente ha caido en esta tentación creando interfaces y factorías siempre que era posible. La lógica subyacente a este enfoque es que a lo mejor podemos necesitar en el futuro una implementación diferente, por lo que añadimos siempre dicho nivel de abstracción. Esta técnica ha llegado a convertirse en una especie de optimización de diseño prematura. La realidad es que todas las abstracciones deben estar motivadas por una necesidad real. Las interfaces deben ser algo que utilicemos cuando sea necesari o para optimizar el código, en lugar de incluir ese nivel ad icional de indirección en todas partes, ya que ello hace que aumente la complejidad. Esa complejidad adicional es significativa, y bacer que alguien trate de comprender ese código tan complejo sólo para descubrir al final que hemos añadido las interfaces "por si acaso" y sin una razón rea l, esa persona sospechará, con motivo, de todos los diseños que rea licemos. Una directriz apropiada es la que señala que las clases resultan preferibles a las ¡melfaces. Comience con clases y, si está claro que las interfaces son necesarias, rediseñe el código. Las interfaces son una herramienta muy conveniente, pero está bastante generalizada la tendencia a utili zarlas en demasía. Puede encontrar las soluciones a los ejercicios se leccionados en el documento electrónico Tire Thinking in Jam Annotated So/lItion Guide, disponible para la venta en wv,'w.J."indl1elOlet.
Clases internas
Resulta posible situar la definición de una clase dentro de la definición de otra. Dichas clases se llaman clases internas. Las clases internas constituyen una característica muy interesante, porque nos peml ite agrupar clases relacionadas y controlar la visibilidad mutua de esas clases. Sin embargo, es importante comprender que las clases internas son algo tota lmente distinto almccanismo de composición de l qu e ya hemos hablado. A primera vista, las clases internas parecen un simple mecanismo de ocultación de código: colocamos las clases dentro de otras clases. Sin embargo, como veremos, las clases internas sirven pa ra algo más que eso: la clase interna conoce los detalles de la clase contenedora y puede comunicarse con ella. Asimismo, el tipo de código que puede escribirse con las clases internas es más elegante y claro (aunque no en todas las ocasiones, por supuesto). Inicialmente, las clases internas pueden parecer extrañas y se requiere cierto tiempo para llegar a sentirse có modo al utilizarlas en los di seilos. La necesidad de las clases internas no siempre resulta obvia, pero después de describir la sintaxis básica y la se mántica de las clases internas, la sección "¿Para qué sirven las clases internas?" debería permitir que el lector se haga una idea de los beneficios de emplear este tipo de clases. Después de dicha sección, el resto del capítulo contiene un análisis más detallado de la sintaxis de las clases internas. Estas características se proporcionan con el fin de cubrir por completo el lenguaje, pero puede que no tengamos que usarlas nunca, o al menos no al principio. Asi pues, puede que el lector sólo necesite consultar las partes iniciales del capítulo dejando los análisis más detallados como material de referencia.
Creación de clases internas Para crear una clase interna, el procedimiento que se utiliza es el que cabría suponer: la definición de la clase se incluye dentro de otra clase contenedora:
11 : innerclasses / Parcell.java II Creación de clases internas. public class Parcel1 { class Contents { private int i = 11; public int value () { return i ¡ } cla ss Destination { privace String label; Destination(String whereTo ) label = whereTo¡ String readLabel ()
{ return label;
}
II La utilización de clases internas se asemeja II a la de cualquier otra cl ase, dentro de Pareel1:
212 Piensa en Java public void ship (String dest)
{
Contents e = new Contents(); Destination d = new Destination(dest); System.out.println(d.readLabel(» i public static void main(String[] args) Parcell p = new Pareell(); p. ship (UTasmania " )
i
/ * Output: Tasmania * /// , Las clases internas utili zadas dentro de ship( ) parecen c lases n0n11ales. Aquí, la única diferencia práctica es que los nombres están anidados dentro de Pareell. Pronto veremos que esta diferencia no es la única. Lo más nOlmal es que una clase ex terna tenga un método que devuelva una referencia a una clase interna, como puede verse en los métodos to( ) y contents( ): ji : innerclasses/Parce12.java
/1 Devolución de una referencia a una clase interna. public class Parcel2 class Contents { private int i : 11; public int value () { return i; } class Destination { private String label¡ Destination{String whereTo) label = whereTo¡ String readLabel {)
{ return label;
public Destination to (String s) return new Destination(s) ¡
{
public Contents contents() return new Contents()¡ public void ship{String dest) Contents c : contents{); Destination d : to{dest) ¡ System.out.println{d.readLabel())
i
public static void main(String[] args) Parcel2 p = new Parcel2() ¡ p.ship{"Tasmania " ) i Parce!2 q = new Parcel2{)¡ II Definición de referencias a clases internas: Parcel2.Contents e = q.contents{) i Parce!2.Destination d = q . to(IBorneo") i
1* Output: Tasmania * /// ,Si queremos construir un objeto de la clase interna en cualquier lugar que no sea dentro de un método 110 estático de la clase externa, debemos especificar el tipo de dicho objeto como NombreClaseExterna.NombreClaseJl7terna, corno puede verse en main( ).
10 Clases internas 213
Ejercicio 1:
( 1) Escriba una clase den ominada Outer que contenga una clase interna llamada Jon er . Añada un método a Outer que de vuel va un objeto de tipo Inn er . En main(), cree e inicialice una referencia a un objeto Jon er .
El enlace con la clase externa Hasta ahora, parece que las clases internas 5011 simplemente un esquema de organización de cód igo y de ocultación de nombres. lo cua l resulta útil pero no especialmente necesario. Sin emba rgo, las cosas son más complejas de lo que parecen, cuando se crea una clase interna, cada objeto de esa clase interna di spone de un enlace al objeto contenedor que lo ha creado. por lo cual puede acceder a los miembros de dicho objeto contenedor sin utilizar ninguna cualificación especial. Además, las c lases internas ti enen derechos de acceso a todos los e lementos de la clase contenedora.! El sigu iente eje mp lo ilustra esta caracleristica:
11 : innerclasses / Sequence . java II Almacena una secuencia de objetos. i nterface Selector { boolean end () ; Object current () ; v o id next ( ) ;
public class Sequence { private Object [] items; private int next = O; public Sequence (int size ) public void add (Object x ) if(next < items.length) items[next++]
items
new Object [size]; }
= Xi
private class SequenceSelector implements Selector private int i = O; public boolean end () { return i == items.length; publ ic Obj ect current ( ) { return i tems [i]; } public void next () { if (i < items . length ) i++; } public Selector selector () { return new SequenceSelector()
i
public static void main(String[] argsl { Sequence sequence = new Sequence(lO); for(int i = O; i < 10; i++} sequence.add {Integer . toString(i» ; Selector selector = sequence.selector( } ; while ( ! selector. end (l) { System.out.print {selector.current () + selector.next( ) ;
"
") i
1- Output: 0 1234 5 6 7 8 9
' /11 ,-
1 Esto difiere significati vamente del diseno de clases anidadas en C++, que simplemente se trata de un mecanismo de ocultación de nombres. No hay nin~ giln enlace al objeto contenedor ni nillgtin tipo de penni sos implícitos en C++.
214 Piensa en Java
La secuenc ia Sequence es simplemente una matriz de tamaño fijo de objetos Object con una clase envoltorio. In vocamos add( ) para aiiadir un nuevo objelO al final de la secuencia (si queda sitio). Para extraer cada uno de los objetos de la secuen_ cia, hay una interfaz denominada Selector. Éste es un ejemplo del patrón de diseño iterador del que hablaremos más en detalle posterionnente en el libro. Un Selector pennite ver si nos encontramos al final de la secuencia [ende )], acceder al objeto acnlal [current()] y desplazarse al objeto siguiente [next()] de la secuencia. Como Selector es una interfaz, otras clases pueden implementar la interfaz a su manera y otros métodos pueden tomar la interfaz como argumento, para crear código de propósito más general. Aquí. SequenceSelector es una clase privada que proporciona la funcionalidad Selector. En main(), podemos ver la creación de una secuencia, seguida de la adición de una serie de objetos de tipo String. A continuación, se genera un objeto Selector con una llamada a selector(), y este objeto se utiliza para desplazarse a tra vés de la secuencia y seleccionar cada elemento. A primera vista, la creación de SequenceSelector se asemeja a la de cualquier otra clase interna. Pero examinemos el ejemplo en más detalle. Observe que cada uno de los métodos [end(), current() y next()] bace referencia a items, que es una referencia que no forma parte de SequenceSelector, sino que se encuentra en un campo privado dentro de la clase contenedora. Sin embargo, la clase interna puede acceder a los métodos y campos de la clase contenedora como si fueran de su propiedad. Esta característica resulta muy cómoda, como puede verse en el ejemplo anterior. Así pues, una clase interna tiene acceso automático a los miembros de la clase contenedora. ¿Cómo puede suceder esto? La clase interna captura en secreto una referencia al objeto concreto de la clase contenedora que sea responsable de su creación. Entonces, cuando hacemos referencia a un miembro de la clase contenedora, dicha referencia se utiliza para seleccionar dicho miembro. Afortunadamente, el com pilador se encarga de resolver todos estos detalles por nosotros, pero resulta evidente que sólo podrá crearse un objeto de la clase interna en asociación con otro objeto de la clase contenedora (cuando, como vere mos pronto, la clase interna sea no estática). La construcción del objeto de la clase interna necesita de una referencia al objeto de la clase contenedora y el compilador se quejará si no puede acceder a dicha referencia. La mayor parte de las veces todo este mecanismo funciona sin que el programador tenga que intervenir para nada.
Ejercicio 2:
(1) Cree una clase que almacene un objeto String y que disponga de un método toString( ) que muestre esa cadena de caracteres. Añada varias instancias de la nueva clase a un objeto Sequence y luego visualícelas.
Ejercicio 3:
(1) Modifique el Ejercicio 1 para que Outer tenga un campo private String (inicializado por el constructor) e lnner tenga un método toString( ) que muestre este campo. Cree un objeto de tipo (nner y visualicelo.
Utilización de .this y .new Si necesita generar la referencia al objeto de la clase externa, basta con indicar el nombre de la clase externa seguido de un punto y de la palabra clave this. La referencia resultante tendrá automáticamente el tipo correcto, que se conoce y se comprueba en tiempo de compilación, por lo que no hay ningún gasto adicional en tiempo de procesamiento. He aquí un ejemplo que muestra cómo utilizar .this: ji : innerclasses/DotThis.java
1/ Cualificación del acceso al objeto de la clase externa. public class DotThis void f () { System.out.println("DotThis.f() ") i } public class Inner { public DotThis outer () { return DotThis.this¡ jj Un "this" haría referencia al "thisl! de Inner
public Inner inner () { return new Inner () ; public static void main (St ring [) args) { DotThis dt = new DotThis() ¡ DotThis.Inner dti = dt.inner() ¡
10 Clases internas 21 5 dti.outer () .f () ;
} 1* Output, Do tThis. f ()
* 111 ,Algunas veces, necesitamos decir a un objeto que cree otro objeto de una de sus clases internas. Para hacer esto es necesario proporcionar una referencia al objeto de la clase externa en la expresión oew, utilizando la sintaxis .ne\\', como en el siguiente ejemplo:
1/ : innerclasses/DotNew.java // Creación de una clase interna directamente utilizando la sintaxis .new. public class DotNew ( public class Inner {} public static void main(String(] args ) DotNew dn = new DotNew( ) ; DotNew.lnner dni = dn.new Inner()¡
Para crear un objeto de la clase intern a directamente, no se utiliza esta misma fa mla haciendo referencia al nombre de la clase externa DotNew como cabría esperar, sino que en su lugar es necesari o utili zar un objeto de la clase ex terna para crear un objeto de la clase intern a. como podemos ver en el ejemplo anterior. Esto resuelve también las cuestiones re lativas a los ámbitos de los nombres en la c lase interna, por lo que nun ca escri biríamos (porque, de hecho, no se puede) dO.De\\' DotNew.lnner( ). No es posible crea r un objeto de la clase interna a menos que ya se di sponga de un objeto de la c lase ex tern a. Esto se debe a que el objeto de la clase interna se conecta de manera transparente al de la clase ex tern a que lo haya creado. Sin embargo, si defini mos una clase anidada, (una clase intern a estática), entonces no será necesa ri a la refe renc ia al objeto de la clase externa. A continuación puede ver có mo se aplicaría
.Den'
a l eje mplo " Parcer':
11 : innerclasses/Parcel3.java
JI
Utilización de
.new para crear instancias de clases internas.
pu blic class Parcel3 cl ass Contents { private int i = 11¡ public int value () { return i ¡ } c lass Destination { private String label¡ Destination (String whereTo ) { label = whereTo¡ String readLabel () { return label ¡ }
}
public static void main{String[] args) Parcel3 p = new Parcel3{); JI Hay que usar una instancia de la clase externa II para crear una instancia de la clase interna: Parcel3.Contents e = p . new Contents(); Parcel3 . Destination d = p.new Destination(ITasmania");
}
111 > Ejercicio 4:
(2) Añada un método a la clase Sequ ence.Seq ue nceSelecto r que genere la re ferencia a la clase ex terna Sequ ence.
Ejercicio 5:
( 1) Cree una clase con una clase interna. En otra clase separada, cree una instancia de la clase interna.
216 Piensa en Java
Clases internas y generalización Las clases internas muestran su utilidad real cuando comenzam os a generalizar a una clase base y, en particular, a una inter_ faz. (El efecto de generar Wla referencia a una interfaz a partir de un objeto que la implemente es prácticamente el mismo qu e el de reali zar una generalización a una clase base). La razón es que entonces la clase interna (la implementación de la interfaz) puede ser no visible y estar no disponible, lo cual resulta muy útil para ocultar la implementación . Lo único que se obtiene es una referen cia a la clase base o a la interfaz. Podemos crear interfaces para los ejemplos anteriores:
11: innerclasses/Destination. java public interface Destination { String readLabel() i
} 111,Ij : innerclassesjContents . java public inter f ace Contents { int value () ;
} 1110Ahora Contents y Destination rep resentan interfaces disponibles para el programador de clientes. Rec uerde que una interfaz hace que todos sus miembros sean automáticamente públicos. Cuando obtenemos ulla referencia a la clase base o a la interfaz, es posible que no podamos averiguar el tipo exacto, como se muestra en el siguiente ejemplo:
JI :
innerclassesjTestParcel . java
class Parcel4 { private class PCont en t s implements Contents { private i n t i = 11; public int val ue () { return i; } protected c l ass PDestination implements De stination private String label; private PDestination (String whereTo) { label = whereTo¡ public String readLabel ()
{ return label;
public Destination destination (String s) return new PDestination(s);
{
public Contents contents() return new PContents();
publie class TestParcel { public static void main (String [] args) { parcel4 p = new PareeI4() i Contents c = p.contents(); Destination d = p . destination(UTasmania U ) ; IJ Ilegal -- no se puede acceder a la clase privada : JI! Parcel4 . PContents pe = p . ne w PConte n ts();
}
111 ,En Parcel4 hemos añadido algo nuevo. La clase interna PContents es priva te, así qu e sólo puede acceder a ella Parcel4 . Las clases normales (no internas) no pueden ser privadas o protegidas; sólo pueden tener acceso público o de paquete. PDestination es protegida, por lo que sólo pueden acceder a ella Parcel4, las clases contenidas en el mismo paquete (ya
10 Clases in lemas 217
que protected también proporciona acceso de paquete) y las clases que hereden de Parcel4. Esto quiere decir que el programador de clientes tiene un conocimiento de estos miembros y un acceso a los mismos restringido. De hecho, no podemos ni siquiera realizar una especialización a una clase interna privada (ni a una clase interna protegida, a menos que estemos usando una c lase que herede de ella), porque no se puede acceder al nombre, como podemos ver en class
TestParcel. Por tanto, las clases internas privadas proporcionan una forma para que los diseñadores de clases eviten completamente las dependencias de la codificación de tipos y oculten totalmente los detalles relativos a la implementación. Además. la extensión de una interfaz resulta inútil desde la perspectiva del programador de clientes, ya que éste no puede acceder a ningún método adicional que no forme parte de la interfaz pública. Esto también proporciona una oportunidad para que el compilador de Java genere código más eficiente. Ejercicio 6 :
(2) Cree una interfaz con al menos un método, dentro de su propio paquete. Cree una clase en un paquete separado. Añada una clase interna protegida que implemente la interfaz. En un tercer paquete, defina una clase que herede de la anterior y, dentro de un método, devuelva un objeto de la clase interna protegida, efecnlando una generalización a la interfaz durante el retorno.
Ejercicio 7:
(2) Cree una clase con un campo privado y un método privado. Cree una clase interna con un método que modifique el campo de la clase externa e invoque e l método de la clase externa. En un segundo método de la clase externa, cree un objeto de la clase interna e invoque su método, mostrando a continuación el efecto que esto tenga sobre el objeto de la clase externa.
Ejercicio 8:
(2) Detennine si una clase externa tiene acceso a los elementos privados de su clase interna.
Clases internas en los métodos y ámbitos Lo que hemos visto hasta ahora son los usos típi cos de las clases internas. En gene ral , el código que escribamos y el que podamos leer donde aparezcan clases internas estará compues to por clases internas "simples" que resulten fáciles de comprender. Sin embargo, las sintaxis de las clases internas abarca varias otras técnicas más complejas. Las clases internas pueden crearse dentro de un método o incluso dentro de un ámbito arbi trario. Existen dos razones para hacer esto: 1. Como hemos visto anterionllente, podemos estar implementando una interfaz de algún tipo para poder crear y devolver una referencia. 2. Podemos estar tratando de resolver un problema complicado y queremos crear una clase que nos ayude a encontrar la solución, pero sin que la clase esté públicamente disponible. En los siguientes ejemplos, vamos a modificar el código anterior para utilizar:
1. Una clase definida dentro de un método 2. Una clase definida dentro de un ámbito en el interior de un método 3. Una clase anónima que implemente una interfaz 4. Una clase anónima que amplíe una clase que disponga de un constructor no predeterminado 5. Una clase anónima que se encargue de la inicialización de campos 6. Una clase anónima que lleve a cabo la construcción utilizando el mecanismo de inicialización de instancia (las clases internas anónimas no pueden tener constmctores). El primer ejemplo muestra la creación de una clase completa dentro del ámbito de un método (en lugar de dentro del ámbito de otra clase). Esto se denomina clase interna local:
11 : innerclasses / ParcelS. java 11 Anidamiento de una clase dentro de un método. public class ParcelS { public Destination destination (String s) { class PDestination implements Destination private String label; private PDestination (String whereTo ) { label = whereTo ¡
218 Piensa en Java
public String readLabel ()
{ return label;
}
return new PDestination(s); public static void main (String [] args) { ParcelS p = new Parce15(); Destination d = p.destination{tlTasmania tl );
La clase PDestination es parte de destination( ) en lugar de ser parte de Parcel5. Por tanto, no se puede acceder a PDestination fuera de destination(). Observe la generalización que tiene lugar en la instrucción return: lo único que sale de destination( ) es una referencia a Destination, que es la clase base. Por supuesto, el hecho de que el nombre de la clase PDestination se coloque dentro de destination( ) no quiere decir que PDestinatioD no sea un objeto válido una vez que destination( ) tennina. Podemos utilizar el identificador de clase PDestination para nombrar cada clase interna dentro de un mismo subdirectorio sin que se produzcan colisiones de clases. El siguiente ejemplo muestra cómo podemos anidar clases dentro de un ámbito arbitrario. jj : innerclasses/Parcel6.java ji Anidamiento de una clase dentro de un ámbito. public class Parce16 { private void internalTracking(boolean b ) {
Hlbl
{
class TrackingSlip { private String id; TrackingSlip (String s) id = Si
{
String getSlip I I { return id;
}
TrackingSlip ts = new TrackingSlip ("s lip" ) ; String s = ts.getSlip() i
ji ¡NO se puede usar aquí! Fuera de ámbito: jj! TrackingSlip ts = new TrackingSlip("x tl ) ; public void track() { internalTracking(true}; public static void main{String[] args) { Parce16 p = new Parce16(); p. track ();
La clase TrackingSlip está anidada dentro del ámbito de una instrucción ir. Esto 00 quiere decir que la clase se cree condicionalmente; esa clase se compila con todo el resto del código. Sin embargo, la clase no está disponible fuera del ámbito en que está definida. Por lo demás, se asemeja a una clase nornlal. Ejercicio 9 :
(1) Cree una interfaz con al menos un método e implemente dicha interfaz definiendo una clase intema dentro de un método que devuelva una referencia a la interfaz.
Ejercicio 10:
(1) Repita el ejercicio anterior, pero definiendo la clase interna dentro de un ámbito en el interior de un método.
Ejercicio 11:
(2) Cree una clase interna privada que implemente una interfaz pública. Escriba un método que devuel va una referencia a una instancia de la clase interna privada, generalizada a la interfaz. Demuestre que la clase interna está completamente oculta, tratando de realizar una especialización sobre la misma.
10 Clases internas 219
Clases internas anónimas El siguiente ejemplo puede parecer un tanto extraño:
JI : innerclasses/Parce17.java // Devolución de una instancia de una clase interna anónima. public class Pareel7 public Contents contents() return new Contents () { / / Inserte una definición de clase private int i = 11¡ publ ic int value () { return i i }¡ /1 En este caso hace falta el punto y coma public static void rnain(String[] args) Pareel7 p = new Parce17()¡ Contents e = p.contents();
{
El método contents( ) combina la creación del va lor de retorno con la definición de la clase que representa dicho valor de retorno. Además, la clase es anónima, es decir, no tiene nombre. Para complicar aún más las cosas, parece como si estuviéramos empeza ndo a crear un objeto Contents, y que entonces, antes de lJegar al punto y co ma, dij éramos "Un momento: voy a introducir una definición de clase". Lo que esta extrai'ia sintax is significa es: "Crea un objeto de una clase anónima que herede de Contents". La referencia devuelta por la expresión De\\' se generalizará automáticamente a una referencia de tipo Contents. La sintax is de la clase interna anónima es una abreviatura de:
11: innerclasses/Parce17b.java II Versión expandida de Parce17.java public class Parce17b { class MyContents implements Contents private int i = 11; public int value () { return i i } public Contents contents () { return new MyContents () public static void main(String[] argsl { Parce17b p new Parce17b(); Contents c = p.contents();
j
En la clase interna anónima, Co ntents se crea utilizando un constructor predete rminado. El siguiente código muestra lo que hay que hacer si la clase base necesita un constructor con un argumento:
11 : innerclasses/ParceI8.java II Invocación del constructor de la clase base. public class Parcel8 { public Wrapping wrapping(int xl II Llamada al constructor de la clase base: return new Wrapping(x) { II Pasar argumento del constructor. public int value () { return super. value () * 47;
}i
II
Punto y coma necesario
public sta tic void main (String [] argsl
{
220 Piensa en Java
Parcele p = new Parcel8 () ; Wrapping w = p.wrapping (lO ) ;
Es decir, simplemente pasamos e l argumento apropiado al constructor de la clase base. C0l110 sucede aquí con la x que se pasa en oew Wr apping(x). Aunque se trata de una c lase nonnal con una implementación, W r ap ping se está usando tamo bién como "interfaz" con sus clases derivadas:
JI : innerclasses / Wrapping.java public class Wrapping { prívate int i; public Wrapping ( int x ) { i = x; } public int value () { return i; }
/// ,Como puede observar. \ Vr a p p in g liene un constructo r que requiere un argumento, para que las cosas sean un poco más inte-
resantes. El punto y coma si mado al final de la c lase intema anónima no marca el final del cuerpo de la c lase, sino el final de la expresión que co nten ga a la clase anónima. Por tanto, es una utili zac ión idéntica al uso del punto y coma en cua lquier otro lugar. También se puede realiza r la inicialización cuando se definen los campos en una clase anónima: // : innerclasses / ParceI9 . java // Una clase interna anónima que realiza la // inicialización . Versión más breve de ParceIS.java. public class Parcel9 { /1 El argumento debe ser final para poder utilizarlo /1 dentro de la clase interna anónima: public Destination destination (final String dest ) ( ret u rn new Destination () { private String label = dest; public String readLabel () { return label; } }; public static void main (String [] args l ( Parcel9 p = new ParceI9 () ; Destination d = p.destination ( IOTasmania
lO
)
;
Si estamos definiendo una clase interna anónima y queremos usar un objeto que está definido fuera de la clase interna anó~ nima. el com pilador requiere que la referencia al argumento sea fin a l, como puede verse en e l argumento de d es tin a tion(). Si nos olvidamos de hacer eslO, obtendremos un mensaje de error en tiempo de co mpilac ión. Mientras que es temos simplemente reali za ndo una asignación a un campo, la técnica empleada e n es te ejempl o resulta adecuada. ¿Pero qué sucede si necesitamos reali zar algún tipo de actividad similar a la de los constmclO res? No podemos disponer de un constmctor nominado dentro de una clase anónima (ya que la c lase no tiene ningún nombre), pero con el mecani smo de inicialización de instancia. podemos, en la práctica, crear un constructor para una clase interna anónima. de la fomla siguiente:
11 : innerclasses / AnonymousConstructor.java II Creación de un constructor para una clase interna anónima. import static net.mindview.util.Print.*; abstract class Base { public Base (int i) { print ( "Base constructor, i public abstract void f () ;
"
+ i);
10 Clases internas 221
pUblic class AnonyrnousConstructor { public static Base getBase(int i) return new Base (i) { { print(IIInside instance initializer") public void f () { print ( " In anonymous f () " ) ;
i
}
}; public stati c void main (String [] args) getBase(47) ; Base base base . f (1 ;
/ * Output : Base constructor,
i
{
= 47
rnside instance initializer In anonymous fe) * /// >
En este caso, la variable i no tenía porqué haber sido final. Aunque se pasa i al constructor base de la c lase anónima. nunca se utili za esa variable dentro de la clase anónima. He aq ui un ejemplo con inicialización de instancia. Observe que los argumentos de destination( ) deben ser de tipo final , puesto que se los usa dentro de la clase anónima: ji : innerclasses/ParcellO . java
JI USO de "inicialización de instancia" para realizar II la construcción de una clase interna anónima . public class ParcellO public Destination destination(final String dest, final float price) { return new Dest ination () { private int cost¡ II Inicialización de instancia para cada objeto : ( cost = Math.round(price); if (cost > 100) System . out . println ( "Over budget!"); private String label = dest; public String readLabel () { return label j
}
}; public static void main{String[] args) ( ParcellO p = new ParcellO(); Destination d = p.destination{"Tasmania", lOl.395F)
i
1* Output : Over budget!
* /1/ , Dentro del inicializador de instanc ia, podemos ver código qu e no podría ejecutarse como parte de un inicializador de campo (es dec ir, la instrucción if). Por tanto, en la práctica, un inicializador de instan cia es el constructor de una clase interna anónima. Por supuesto, esta solución está limitada : no se pueden sobrecargar los inicializadores de instancia, así que sólo podemos disponer de uno de estos constructores. Las clases imemas anónimas están en cierta medida limitadas si las comparamos con el mecani smo nonnal de herenci a, porque tiene n que extender Ull a clase o implementar una interfaz, pero no pueden hacer ambas cosas al mismo tiempo. Y, si implementam os una interfaz. sólo podemos implementar una.
222 Piensa en Java
Ejercicio 12:
( 1) Repita el Ejerci cio 7 ut ili zando una clase interna anónima.
Ejercicio 13:
( 1) Repita e l Ejercicio 9 utilizando una clase interna anónima.
Ejercicio 14:
( 1) Modifique interfaces/Ho r rorShow.j.v. para impl ementar Da nge rousMo nster y Va mpire utilizando clases anónimas.
Ejercicio 15:
(2) Cree una clase con un constructor no predetemli nado (uno que tenga argumentos) y sin ning ún cons· tmctor predetenn inado (es decir, un constructor sin argumenlos). Cree una segunda clase que tenga un método que devuel va una referenc ia a un objeto de la primera clase. Cree el objeto qu e hay que devolver defini endo Ulla clase intema anón ima que herede de la primera clase.
Un nuevo análisis del método factoría Observe que el ej emplo interfaces/ Facto r ies.java es mucho más atracti vo cuando se utilizan clases internas anónimas:
JI: innerc l asses/Factories . java import static net . mindview . util . Print .* ¡ interface Service void methodl () i void method2 () i
interface ServiceFactory Service getService( ) i
class Implementationl implements Se r vice { private Implementationl () {} public void methodl () {print ( " Implementationl methodl" ) ¡ } public void method2 () {print ( !I Implementationl method2" ) ;} public static ServiceFactory factory new ServiceFactory () { public Service getService () { return new Implementationl () ; }
}; class Implementation2 implements Service { private Implementation2 () { } public void methodl () {print ( " Implementation2 methodl" ) ¡ } public void method2 () {print ( " Implementation2 method2" ) ; } public static ServiceFactory factory new ServiceFactory () { public Service getService () { return new Implementation2( ) ; } };
public class Factories { public static void serviceConsumer(ServiceFactory fact ) Service s = fact.getService () ¡ s . methodl (} ; s.method2 () ; public static void main (String{) args ) { serviceConsumer ( Implementationl . factory ) ;
10 Clases internas 223
/1 Las implementaciones son completamente intercambiables : serviceConsumer{Implementation2.factory) i 1* Output: Implementationl rmplementationl Implementation2 Implementation2
methodl method2 methodl method2
* / 1/,Ahora. los constructores de hnplementationl e Implementation2 pueden ser pri vados y no hay ninguna necesidad de crear una clase nominada C01110 facrofi a. Además, a menudo sólo hace falta un úni co objeto fa ctoría , de modo qu e aquí lo hemos creado C0l110 un campo estáti co en la implementac ión de Service. Asimismo, la sintax is resultante es más clara. Tam bién podemos mejorar e l ej empl o interfaces/Games.java utilizando clases internas anónimas:
JI: innerclasses/Games . java II Utilización de clases internas anónimas con el marco de trabajo Game. import static net.mindview . util . Print .* interface Game ( boolean move() i } interface Game Factory ( Game getGame(); class Checkers implements Game { private Checkers () {} private int moves = O; private static final int MOVES = 3i public boolean move() { print ("Checkers move " + moves); re t urn ++moves != MOVESi public static GameFactory factory = new GameFactory() public Game getGame () { return new Checkers () i } };
class Chess implements Game private Chess() {} private int moves = O; private static final int MOVES = 4; public boolean move () print ("Chess move " + moves); return ++moves != MOVES; public static GameFactory factory = new GameFactory(} public Garne getGame () { return new Chess (); } };
public class Garnes { public static void playGame(GameFactory factory) Garne s = factory getGame(); while{s.move())
public static void main(String[] args) playGame(Checkers . factory) ; playGame(Chess . factory) ;
/* Output:
{
{
224 Piensa en Java
Checkers move O Checkers move 1 Checkers move 2 Chess move O Chess move 1 Chess move 2 Chess move 3
, /// ,Recuerde el consejo que hemos dado al final del último capítulo: U,Uice las clases con preferencia a las in/elfaces. Si su diseño necesita una interfaz, ya se dará cuenta de ello. En caso contrario, no emplee una interfaz a menos que se vea obli· gado. Ejercicio 16:
(1) Modifique la sol ución del Ejercicio 18 del Capitulo 9, Imelfaces para utilizar c lases internas anónimas.
Ejercic io 17:
(1) Modifique la solución del Ejercicio 19 del Capitulo 9, In/e/faces para utilizar clases internas anónimas.
Clases anidadas Si no es necesario disponer de una conexión entre el objeto de la clase ¡ntema y el objeto de la clase extema, podemos definir la clase interna como estática. Esto es lo que comúnmente se denomina una clase anidada. 2 Para comprende r el signi ficado de la palabra clave static cuando se la aplica a las clases internas, hay que recordar que el objeto de una clase intema normal mantiene implícitamente una referencia al objeto de la clase contenedora que lo ha creado. Sin embargo, esto no es cierto cuando definimos una clase interna corno estática. Por tanto, una clase anidada significa: 1. Que no es necesario un objeto de la clase externa para crear un objeto de la clase anidada. 2. Que no se puede acceder a un objeto no estático de la clase externa desde un objeto de una clase anidada. Las clases anidadas difieren de las clases internas ordinarias también en otro aspecto. Los campos y los métodos en las clases internas l10rnlales sólo pueden encontrarse en el nivel externo de una clase, por lo que las clases internas nomlales no pueden tener datos estáticos, campos estáticos o clases anidadas. Si.n embargo, las clases anidadas pueden tener cualquiera de estos elementos: ji : innerclasses/Parcel11.java ji Clases anidadas (clases internas estáticas).
public class Parcel11 { private static class ParcelContents implements Contents { private int i = 11; public int value() { return i¡ } protected static class ParcelDestination implements Destination private String label¡ private ParcelDestination (String whereTo ) { label = whereTo¡ public String readLabel () { return label ¡ } jj Las clases anidadas pueden contener otros elementos estáticos: pubEe statie void f () {) sta tic int x = 10; static class AnotherLevel { pubEe statie void f (1 {) static int x = 10;
2 Guarda cierto parecido con las clases anidadas de C++, salvo porque dichas clases no penniten acceder a miembros privados a dilercncia de lo que sueede en Java.
10 Clases internas 225 public static Destinatían destination(String s) return new ParcelDestination(s) i
{
public static Contents contents() return new ParcelContents() i public static void main(String[J args) Contents e = contents () i Destinatian d = destinatían ("Tasmania" ) i
}
111 ,En main(), no es necesario ningún objeto de Pareel1 ); en su lugar, utilizamos la sintaxis nornlal para seleccionar un mi embro estático con el que invocar los métodos que devuelven referencias a C ontents y Desti nation. Como hemos visto anterionnente en el capítulo. en una clase interna normal (no estática), el vínculo con el objeto de clase externa se utili za empleando una referencia this especiaL Una clase anidada no tiene referenc ia this especial, lo que hace que sea análoga a un mélodo eSlálico. Ejercicio 18 :
(1) Cree una clase que contenga una clase anidada. En ma in( ), cree una instancia de la clase anidada.
Ejercicio 1 9 :
(2) Cree una clase que contenga una clase interna que a su vez contenga otfa clase interna. Repita el proceso utilizando clases anidadas. Observe los nombres de los archivos .class generados por el compilador.
Clases dentro de interfaces Normalmente, no podemos incluir cualquier código dentro de una interfaz, pero una clase anidada puede ser parte de Ulla interfaz. Cualquier clase que coloquemos dentro de una interfaz será automáticamente pública y estática. Puesto que la clase es estática no viola las reg las de las interfaces: simplemente, la clase anidada se incluye dentro del espacio de nombres de la interfaz. Podemos incluso implementar la interfaz contenedora dentro de la clase contenedora de la fonna siguiente, como por ejemplo en: jJ: innerclasses/classInInterface . java jJ {main: Classlnlnterface$Test} public interface ClassInInterface void howdy () i class Test implements ClasslnInterface public void howdy () { System.out.println(HHowdy! ") i public static void main (String [J args) new Test () . howdy () i
{
1* Output: Howdy!
*//1,Resulta bastante útil anidar una clase dentro de una interfaz cuando queremos crear un código común que baya que emplear con todas las diferentes implementaciones de dicha interfaz. Anteriornlente en el libro ya sugeríamos incluir un método main( ) en todas las clases. con el fin de utilizarlo como mecanismo de prueba de estas clases. Una desventaja de esta técnica es la cantidad de código compilado adicional con la que hay que trabajar. Si esto representa un problema, pruebe a utilizar una clase anidada para incluir el código de prueba:
JI : innerclassesJTestBed.java JI Inclusión del código de prueba en una clase anidada . JI {main : TestBed$Tester} public class TestBed {
226 Piensa en Java public void f{) { System.out.println(tlf() 11); public statie elass Tester { public static v oid main (String [] args) { TestBed t = new TestBed(); t . f () ;
1* Output: f ()
* /// ,Esto genera una clase separada denominada Tcs tB cd$Tester (para ejecutar el programa, escribiríamos java TestBedSTestcr ). Puede utilizar esta clase para las pmebas, pero no necesitará incluirla en el producto final , bastará con borrar TestBed$Tester.class antes de crear el producto definitivo. Ejercicio 20 :
(1) Cree una interfaz que contenga una clase anidada. Implemente esta interfaz y cree una instancia de la clase anidada.
Ejercicio 21:
(2) Cree una interfaz que contenga una clase anidada en la que haya un método estático que invoque los métodos de la interfaz y muestre los resultados. Implemente la interfaz y pase al método una instancia de la implementación.
Acceso al exterior desde una clase múltiplemente anidada No importa con qué profundidad pueda estar anidada una clase interna: la clase anidada podrá acceder transparentemente a todos los miembros de todas la clases dentro de las cuales esté anidada, como podemos ver en el siguiente ejemplo:3
11 : innerclasses/MultiNestingAeeess.java II Las clases anidadas pueden acceder a todos los miembros de II todos los niveles de las clases en las que está anidada. elass MNA private void f () {} class A pri vate void 9 () {} publ ie elass B { void h() ( g (); f ();
publie elass MultiNestingAeeess { publie static void main{String[] MNA mna = new MNA () ; MNA.A mnaa = mna.new A{); MNA.A.B mnaab = mnaa.new B (); mnaab.h() ;
args ) {
Puede ver que en MNA.A. B. los métodos g() y f() son invocables sin necesidad de ninguna cualificación (a pesar del hecho de que son privados). Este ejemplo también ilust ra la sintaxis necesaria para crear objetos de clases internas múltiplemente anidadas cuando se crean los objetos en una clase diferente. La sintaxis ·'.new·' genera el ámbito correcto, por lo que no hace falta cualificar el nombre de la clase dentro de la llamada al constmctor. 3
Gracias de nuevo a Martin Danncr.
10 Clases internas 227
¿Para qué se usan las clases internas? Hasta este momento, hemos analizado buena parte de los detalles sintácticos y semánticos que describen la fanna de funcionar de las clases internas. pero esto no responde a la pregunta de para qué sirven las clases internas. ¿Por qué los diseñadores de Java se tomaron tantas molestias para añadir esta característica fundamental al lenguaje? Nonnalmente, la clase interna hereda de otra clase o implementa una interfaz y el código incluido en la clase interna manipula el objeto de la clase externa dentro del cual hubiera sido creado. Así pues, podríamos decir que una clase interna proporciona lm3 especie de ventana hacia la clase externa.
Una de las cuestiones fundamentales acerca de las clases internas es la siguiente: si simplemente necesitamos una referencia a una interfaz, ¿por qué no hacemos simplemente que la clase externa implemente dicha interfaz? La respuesta es que: "Si eso es todo lo que necesita, entonces esa es la manera de hacerlo". Por tanto, ¿qué es lo que distingue una clase interna que implementa una interfaz de una clase externa que implementa la misma interfaz? La respuesta es que no siempre disponemos de la posibilidad de trabajar con interfaces, sino que en ocasiones nos vemos forzados a trabajar con implementaciones. Por tanto, la razón más evidente para utilizar clases internas es la siguiente:
Cada clase in/erna puede heredar de una implementación de manera independiente. Por tamo, la clase interna no está limitada por el hecho de si la clase externa ya está heredando de una implementación. Sin la capac idad de las clases internas para heredar, en la práctica, de más de una clase concreta o abstracta, algunos problemas de diseño y de programación serían intratables. Por tanto, una forma de contemplar las clases internas es decir que representan el resto de la solución del problema de la herencia múltiple. Las interfaces resuelven parte de l problema. pero las clases internas permiten en la práctica una "herencia de múltiples implementaciones". En otras palabras, las clases internas nos pemliten en la práctica heredar de varios elementos que no sean in terfaces. Para analizar esto con mayor detalle, piense en una situación en la que tuviéramos dos interfaces que deban de alguna forma ser implementadas dentro de una clase. Debido a la flexibilidad de las interfaces, tenemos dos opciones: una única clase o una clase interna.
/1: innerclasses/MultiInterfaces.java
II
Dos formas de implementar múltiples interfaces con una clase. package innerclassesi interface A {} interface 8 {}
class X implements A, B {} class
y
implements A
8 makeB (1
JI
{
Clase interna anónima:
return new 8 (1
{};
public class Multilnterfaces static void takesA (A al {} static void takesB (8 b)
{}
public static void main (String [) argsl X x = new X (l i y Y = new Y () i takesA (x) i takesA (y) i takesB (x) i takesB(y . makeB()) i
{
228 Piensa en Java
Por supuesto. esto presupone qu e la estrucrura del código tenga sentido en ambos casos desde el punto de vista lógico. Sin embargo. nonnalmente dispondremos de algún tipo de directriz, extraída de la propia naturaleza del problema, que nos indi· cará si debemos utilizar una única clase o una clase interna. pero en ausencia de cualquier otra restricción, la técn ica utilizada en el ejemplo anterior no presenta muchas diferencias desde e l punto de vista de la imp lementación. Ambas so luciones funcionan adecuadamente. Sin embargo. si ten emos clases abstractas o concretas en lugar de interfaces, nos veremos obligados a utilizar clases internas si nuestra c lase debe imp lementar de alguna forma las otras clases de las que se quiere heredar:
/1: innerclasses/Multilmplementation.java II Con clases abstractas o concretas, las clases II internas son la única forma de producir el efecto II de la "herencia de múltiples implementaciones". package innerclasses¡ class D {} abstract class E {} class Z extends D { E makeE () ( return new E l)
{};
public class Multilmplementation static void takesDID d ) {} static void takesE(E e) {} public static void main(String[] Z z '" new Z () ; takesD{z) ; takesE(z.makeE{)) ;
}
args ) {
Si no necesitáramos resolver el problema de la '"herencia de múltiples implementaciones", podríamos escribir el resto del programa sin necesidad de utilizar clases internas. Pero con las clases internas tenemos. además. las siguientes características adicionales: 1. La clase interna puede tener múltiples instancias, cada una con su propia infonnación de estado que es independiente de la infornlación contenida en el objeto de la clase externa. 2. En una única clase externa, podemos tener varias clases internas, cada una de las cuales implementa la misma interfaz o hereda de la mis ma clase en una fornla diferente. En breve mostraremos un ejemp lo de este caso. 3. El punto de creación del objeto de la clase interna no está ligado a la creación del objeto de la clase externa. 4. No existe ninguna relación de tipo "es-un" potencialmente confusa con la clase interna, se trata de una entidad separada. Por eje mplo, si Seq uence.java no utilizara clases internas, estaríamos ob ligados a decir que "un objeto Sequ ence es un objeto Selecto r ", y sólo podría existir un objeto Selec to r para cada objeto Sequ ence concreto. Sin embargo, podríamos fácilmente pensar en definir un segundo método, reve rseSelecto r( ), que produjera un objeto Selecto r que se desp lazara en sentido inverso a través de la secuencia. Este tipo de flexibilidad sólo está disponible con las clases internas. Ejercicio 22 :
(2) Implemente reve rseSelecto r ( ) en Sequ ence.j ava.
Ejercicio 23 :
(4) Cree una interfaz U con tres métodos. Cree una clase A con un método que genere una referencia a U_ definiendo una clase interna anónima. Cree una segunda clase B que contenga una matriz de U. B debe tener un método que acepte y almacene una referencia a U en la matriz, un segundo método que configure una referencia en la matriz (especificada mediante el argumento del método) con el valor null, y un terce r método que se desplace a través de la matriz e invoque los mé todos de U. En main (), cree un grupo de objetos A y un único B. Rellene e l objeto B con referencias a U generadas por los objetos A. Utilice el objeto B para realizar llamadas a todos los objetos A. Elimine algunas de las referencia s U del objeto 8.
10 Clases internas 229
Cierres y retrollamada Un cierre (closure) es un objeto invocable que retiene infol1l13ción acerca del ámbito en que fue creado. Teniendo en cuenta esta definición, podemos ver que una clase intem8 es un cierre orientado a objetos, porque no contiene simplemente cada elemento de infonnación del objeto de la clase externa ("e l ámbito en que fue creado"). sino que almacena de manera automática una re ferencia que apunta al propio objeto de la clase externa, en el cual tiene penniso para manipular lodos los mi embros. incluso aunque sea privados. Uno de los argumentos más sólidos que se proporcionaron para incluir algún mecanismo de punteros en Java era el de permitir las re/rol/amadas (callbacks). Con una retrollamada, se proporciona a algún o tro objeto un elemento de infonnación que le pennite llamar al objeto o ri ginal en un momento posterior. Se trata de un concepto muy potente, como veremos más adelante. Sin embargo, si se implementa una retrollamada utilizando un puntero, nos veremos forzados a confiar en que el programador se comporte correctamente y no haga un mal uso del puntero. Como hemos visto hasta e l momento, el lenguaje Java tiende a ser bastante más precavido. por lo que 110 se ban incluido punteros en el lenguaje. El cierre proporcionado por la clase interna es una buena solución, bastante más flexible y segura que la basada en punteroS. Veamos un ejemplo:
11 :
i nnerclasses/Callbacks .java Utili za c i ón de clases internas para las retrollamadas package innerclassesi import static net . mindview . util .Print .*i
II
interface Incrementable void increment() i
II Muy simple class Calleel private int public void
para limitarse a implementar la interfaz: implements I ncrementab l e { i = Oi increment () {
i++i
print (i)
i
class Mylncrement { public void increment () print ("Other operation") static void f(Mylncrement mi) { mi. increment () ; }
i
II II
Si nuestra clase debe implementar increment() de alguna otra forma, es necsario utilizar una clase interna: class Callee2 extends Mylncrement private int i = Oi public void increment() super.increment(} i i++i
print (i)
i
private class Closure implements Incrementable { publ ic void increment () { II Especifique un método de la clase externa ; en caso II contrario, se produciría una recursión infinita: Callee2 . this.increment() i
Incrementable getCallbackReference(} return new Closure() i
230 Piensa en Java
elass Caller { private Inerementable eallbaekReferenee; Caller (Inerementable ebh) { eallbaekReferenee void 90 () { eallbaekReference. inerement (); }
cbh;
}
publie elass Callbaeks { publíe statíc voíd maín (String [] args) { Calleel el = new Calleel(); Callee2 e2 = new Callee2()¡ Mylnerement.f{c2)¡ Caller callerl new Caller{el)¡ Caller ealler2 = new Caller(e2.getCallbaekReferenee()); callerl. 90 () ¡ eallerl. 90 () ; caller2 .90 () ; caller2. 90 () ¡
/ * Output: Other operation 1 1 2
Other operatíon 2
Other operatíon 3
* /// ,Esto muestra también una di stinción ad icional entre el hecho de implementar una interfaz en una clase externa y el hecho de hacerlo en una clase interna. Calleel es claramente la solución más simple en ténninos de código. Callee2 hereda de Mylncrement, que ya dispone de un método increment( ) diferente que lleva a cabo alguna tarea que no está relacionada con la que la interfaz Incrementable espera. Cuando se hereda Mylncremcnt en Callee2, increment( ) no puede ser sustituido para que lo utilice Incrementable. por lo que estamos obligados a proporcionar una implementación separada mediante una clase interna. Observe también que cuando se crea una clase interna no se añade nada a la interfaz de la clase ex terna ni se la modifica de nin guna manera. Todo en Callee2 es privado salvo getCallbackReference(). Para permitir algún tipo de conexión con el mundo exterior, la interfaz Incrementable resulta esencial. Con este ejemplo podemos ve r que las interfaces penniten una completa separación cntre la interfaz y la implementación. La clase interna C losure implementa In crementable para proporcionar un engarce con Callee2 , pero un engarce que sea lo suficientemente seguro. Cualquiera que obtenga la referencia a Incrementable sólo puede por supuesto invocar increment() y no tiene a su dispos ición ninguna otra posibilidad (a diferencia de un puntero, que nos penni tiría hacer cualquier cosa). Caller toma una referencia a Incrementable en su constructor (aunque la captura de la refere ncia de retrollamada podría tener lugar en cualquier instante) y luego en algún momento posterior utiliza la referencia para efectuar una retro llamada a la clase Callee. El va lor de las retrollamadas radica en su flexibil idad; podemos decidir de manera dinámica qué métodos van a ser invocado en tiempo de ejecución. Las ven tajas de esta manera de proceder resultarán evidentes en el Capítul o 22, lntelfaces gráficas de usuario, en el que emplearemos de manera intensiva las retrolJamadas para implementar la funcionalidad GUl (Graphical User/I/te/face).
Clases internas y marcos de control Un ejemplo más concreto de l uso de clases internas es el que puede encontrarse en lo que de nominamos marco de control.
10 Clases internas 231
Un marco de lrabajo de una aplicación es una clase o un conjunto de clases diseñado para resolver un tipo concreto de problema. Para aplicar un marco de trabajo de una aplicación, lo que nonnalmente se hace es heredar de una o más clases y sustituir algunos de los métodos. El código que escribamos en los métodos sustituidos sirve para personalizar la solución general proporcionada por dicho marco de trabajo de la aplicación, con el fin de resolver nuestros problemas específicos. Se trata de un ejemplo del patrón de disclio basado en el método de plantillas (véase Thinking in Pallerns (with Java) en u'l\'w.A1indView.l1el). El método basado en plantillas contiene la estrucntra básica del algoritmo, e invoca uno o más métodos sustituibles con el fin de completar la acción que el algoritmo dictamina. Los patrones de diseño separan las cosas que no cambian de las cosas que sí que sufren modificación yen este caso el método basado en plantillas es la parte que pennanece invariable, mientras que los métodos sustituibles son los elementos que se modifican. Un marco de control es un tipo particular de marco de trabajo de apJjcación, que está dominado por la necesidad de responder a cierto suceso. Los sistemas que se dedican principalmente a responder a sucesos se denominan sistemas dirigidos por s/lcesos. Un problema bastante común en la programación de aplicaciones es la interfaz gráfica de usuario (GUI), que está casi completamente dirigida por sucesos. Como veremos en el Capítulo 22, ¡me'laces gráficas de usuario, la biblioteca Swing de Java es un marco de control que resuelve de manera elegante e l problema de las interfaces GUI y que utili za de manera intensiva las clases internas. Para ver la forma en que las clases internas permiten crear y utili zar de manera se ncilla marcos de control , considere un marco de control cuyo trabajo consista en ejecutar sucesos cada vez que dichos sucesos estén " listos". Aunque " listos" podría sign ificar cualqu ier cosa, en este caso nos basaremos en el dato de la hora actual. El ejemplo que sigue es un marco de control que no contiene ninguna infonnación específica acerca de qué es aquello que está controlando. Dicha infonnación se suministra mediante el mecanismo de herencia, cuando se implementa la parte del algoritmo correspondiente al método .ction(). En primer lugar, he aquí la interfaz que describe los sucesos de control. Se trata de una clase abstracta, en lugar de una verdadera interfaz, porque el comportamiento predeterminado consiste en llevar a cabo el control dependiendo del instante actual. Por tanto, parte de la implementación se incluye aquí:
11 : innerclasses/controller/Event.java II Los métodos comunes para cualquier suceso de control. package innerclasses.controller¡ public abstraet class Event { private long eventTime¡ protected final long delayTime¡ publie Event (long delayTime) { this.delayTime = delayTime¡ start () ; public void start() { II Permite la reinicialización eventTime = System.nanoTime() + delayTime¡ public boolean ready () { return System.nanoTime ()
>=
eventTime¡
public abstraet void action() ¡
///,El constructo r captura el tiempo (medido desde el instante de creación del objeto) cuando se quiere ejecutar el objeto Event, y luego invoca start( ), que toma el instante actual y añade el retardo necesario, con el fin de generar el instante en el que
el suceso tendrá lugar. En lugar de incluirlo en el constructor, start() es un método independiente. De esta forma, se puede reinicializar el temporizador después de que el suceso haya caducado, de manera que el objeto Event puede reutilizarse. Por ejemplo, si queremos un suceso repet iti vo, podemos invocar simplemente start() dentro del método action(). rcady() nos dice cuándo es el momento de ejecutar el método .ction( ). Por supuesto, ready() puede ser sust ituido en una clase derivada. con el fin de basar el suceso Event en alguna otra cosa distinta del tiempo. El siguiente archivo contiene el marco de control concreto que gestiona y dispara los sucesos. Los objetos Event se almacenan dentro de un objeto contenedor de tipo List (una lista de sucesos), que es un tipo de objeto que analizare-
232 Piensa en Java
mas en más detalle en el Capínllo 11 , Almacenamiento de objetos. Pero ahora lo único que necesitamos saber es que add() añade un objeto Event al final de la lista List, que size() devuelve el número de elementos de List, que la sintaxisforeach pennüe extraer objetos Event sucesivos de List, y que remove() elimina el objeto Event especificado de List.
11 :
innerclasses / controller / Controller.java
II El marco de trabajo reutilizable para sistemas de control. package innerclasses.controller¡ import java.util.*; public class Controller 1I Una clase de java.util para almacenar los objetos Event: private List eventList new ArrayList () i public void addEvent (Event c ) ( eventList.add (c ) i } public void run () ( while {eventList.size {) > O) II Hacer una copia para no modificar la lista II mientras se están seleccionando sus elementos: for {Event e : new ArrayList(eventList ) i f ( e . ready ( )) ( System.out.println {e) ; e.action {) ; eventList.remove {e ) i
El método run() recorre en bucle una copia de eventList, buscando un objeto Event que esté listo para ser ejecutado. Para cada uno que encuentra, imprime infonnación utilizando el método toString( ) del objeto, invoca el método action( ) y luego elimina el objeto Event de la lista. Observe que en este diseño, hasta ahora, no sabemos nada acerca de qué es exactamente lo que un objeto Event hace. Y éste es precisamente el aspecto fundamental del diseño: la manera en que "separa las cosas que cambian de las cosas que permanecen iguales". O, por utilizar un tém1ino que a mi personalmente me gusta, el "vector de cambio" está compuesto por las diferentes acciones de los objetos Event, y podemos expresar diferentes acciones creando distintas subclases de Event. Aquí es donde entran en juego las clases internas. Estas clases nos permiten dos cosas:
1. La implementación completa de un marco de control se crea en una única clase, encapsulando de esa forma lodas aquellas características distintivas de dicha implementación. Las clases internas se usan para expresar los múltiples tipos distintos de acciones [actionOJ necesarias para resolver el problema. 2. Las clases internas evitan que esta implementación sea demasiado confusa, ya que podemos acceder fácilmente a cualquiera de los miembros de la clase externa. Sin esta capacidad, el código podría llegar a ser tan complejo que terminaríamos tratando de buscar una alternativa. Considere una implementación concreta del marco de control di seilado para regular las funciones de un invernadero. 4 Cada acción es totalmente distinta: encender y apagar las luces, iniciar y detener el riego, apagar y encender los termostatos, hacer sona r alarn18s y reinicializar el sistema. Pero el marco de control está diseñado de tal manera que se aíslan fácilmente estas distintas secciones del código. Las clases internas permiten disponer de múltiples versiones derivadas de la misma clase base, Event, dentro de una misma clase. Para cada tipo de acción, heredamos una nueva clase interna ·Event y escribimos el código de control en la implementación de action(). Como puede supo ner por los marcos de trabajo para aplicaciones, la clase GreenhouseControls hereda de ControUer:
11 :
innerclasses/GreenhouseControls.java
11 Genera una aplicación específica del sistema
4 Por alguna razón, este problema siempre me ha resultado bastante grato de resolver; proviene de mi anterior libro C+ +/l1Side & Ow, pero Java permite obtener una solución más elegante.
10 Clases internas 233
JI
de control, dentro de una única clase. Las clases
JI internas permiten encapsular diferente funcionalidad 1/ para cada tipo de suceso. import innerclasses.concroller .* ; public class GreenhouseControls extends Controller { private boolean light = false; public class LightOn extends Event public LightOn(long delayTime) public void aetian () {
{ super (delayTime) ;
/1 Poner código de control del hardware aquí 1/ para encender físicamente las luces. light = true; public String toString()
{ return "Lighe is on" ;
}
public class LightOff extends Event public LightOff (long delayTime) public void aetian () {
{ super (delayTime);
JI
Poner código de control del hardware aquí para apagar físicame nte las luces . light = false¡
JI
public String toString {}
{ return "Light is off" ¡ }
private boolean water = false¡ public c l ass WaterOn extends Event public WaterOn (long delayTimel { super (delayTime) ¡ public void action () { II Poner el código de control del hardware aquí. water = true; public String toString(} return !1Greenhouse water is on"
i
public class WaterOff extends Event public WaterOff ( long delayTime) super(delayTimel¡ public void action () { II Poner el código de control del hardware aquí. water = false¡ public String toString() return "Greenhouse water is off"
i
private String thermostat = "Day" ¡ public class ThermostatNight extends Event public ThermostatNight (long delayTime) { super (delayTime) ; public void action() II Poner el código de control del hardware aquí. thermostat = " Night"; public String toString() return uThermostat on night setting lt
¡
234 Piensa en Java
public class ThermostatDay extends Event public ThermostatDay{long delayTime) { super (delayTime ) i public veid actien () // Poner el código de control del hardware aquí. thermostat = "Day" ¡ public String toString{) return IIThermostat on day setting"
i
} // Un ejemplo de action {) que inserta un // nuevo ejemplar de 51 misma en la línea de sucesos: public class Bell extends Event { public Bell (long delayTime) ( super (delayTime) ¡ public void action () { addEvent(new Bell(delayTime)); public String toString ()
{ return "Bing!"
i
public class Restart extends Event { private Event[] eventList¡ public Restart (long delayTime, Event [] eventList) { super {delayTimel ; this . eventLis t = eventList¡ for{Event e : eventListl addEvent (e) i public void actien () fer(Event e : eventList ) e.start(); // Re-ejecutar cada suceso. addEvent (el i start(); // Re-ejecutar cada suceso addEvent (t his ) i public String toString() return "Restarting system";
public static class Terminate extends Event { public Termina te (long delayTime) { super (delayTime) ; public void action () { System.exit(O) ¡ } public String toString () { return "Terminating" i
} ///,Observe que light, water y thermostat pertenecen a la clase externa GreenhouseControls. a pesar de lo cual las clases internas pueden acceder a dichos campos si n ninguna cualificación y sin ningún penniso especial. Asimismo, los método:t action() suelen requerir algún tipo de control del hardware. La mayoria de las clases Event parecen similares, pero BeJl y Restart son especiales. B.JI hace sonar una alarma y luego añade un nuevo objeto SeU a la lista de sucesos, para que vuelva a sonar posteriormente. Observe cómo las clases internas casi parecen un verdadero mecanismo de herencia múltiple. SeU y Restart tienen todos los métodos de Event y también parecen tener todos los métodos de la clase externa GreenhouseControls. A Restart se le proporciona una matri z de objetos Event y aquélla se encarga de añadirla al controlador. Puesto que Restart() es simplemente otro objeto Event, también se puede añadir un objeto Restart dentro de R.start.action() para que el sistema se reinicialice a sí mismo de manera periódica.
10 Clases internas 235
La siguie nte clase configura el sistema creando un objeto GreenhouseControls y añadiendo di versos tipos de objetos Event. Esto es un ejemplo del patrón de diseño Command: cada objeto de eventList es una solicitud encapsulada en forma de objeto:
1/ : innerclasses / GreenhouseController.java 1/ Configurar y ejecutar el sistema de control de invernadero. 11 {Args, sooo } i mport innerclasses.controller .* ¡
public class GreenhouseController public static void main (String[] args) GreenhouseControls gc = new GreenhouseControls( ) i /1 En lugar de fijar los va l ores, podríamos analizar JI información de con f iguración incluida JI en un archivo de tex to: gc . addEvent (gc.new Bell(900)) Event(] eventList = { gc .new ThermostatNight(O), gc.new LightOn(200), gc .new LightOf f (400), gc.new WaterOn(600), gc . new WaterOff(800 ) , gc . new ThermostatDay(1400)
i
}; gc.addEvent(gc . ne w Restart(2000, eventList)) if(args . length == 1) gc .addEvent( new GreenhouseControls . Terminate( new Integer(args[OI)) ) i gc . run () i
i
1* Output : Bing! Thermostat on night setting Light is on Light is off Greenhouse water is on Greenhouse water is off Thermostat on day setting Restarting system Terminating * 1// ,Esta clase inicializa el sistema, para que añada todos los sucesos apropiados. El suceso Restart se ejecuta repetidamente y carga cada vez la lista eventList en el objeto GreenhouseControls. Si proporcionamos un argumento de línea de comandos que indique los milisegundos, Restart tennillará el programa después de ese número de milisegundos especificado (esto se usa para las pruebas). Por supuesto, resulta más flexible leer los sucesos de un arclüvo en lugar de leerlos en el código. Uno de los ejercicios del Capítulo 18, E/S, pide, precisamente, que modifiquemos este ejemplo para hacer eso. Este ejemplo debería pemútir apreciar cuál es el va lor de las clases internas, especialmente cuando se las usa dentro de un marco de control. Sin embargo. en el Capítulo 22, ¡me/faces gráficas de usuario, veremos de qué [onna tan elegante se utili zan las clases internas para definir las acciones de una interfaz gráfica de usuario. Al tenninar ese capítulo, espero haberle convencido de la utilidad de ese tipo de clases.
Ejercicio 24:
(2) En GreenhouseControls.java , añada una serie de clases internas Event que pennitan encender y apagar una serie de ventiladores. Configure GreenhouseController.java para utilizar estos nuevos objetos Event.
236 Piensa en Java
Ejercicio 25:
(3) Herede de GreenhouseControls en CreenhouseControls.jav3 para ai1adir clases internas Event que pennitan encender y apagar una seri e de vapori zadores. Escriba una nueva ve rs ión de GreenhouseController.java para util izar estos nuevos objetos Event.
Cómo heredar de clases internas Puesto que el constructor de la clase interna debe efectuar la asociación como una referencia al objeto de la clase contene· dora, las cosas se complican ligeramente cuando fratamos de heredar de Ulla clase interna. El problema es que la referencia "secreta" al objeto de la clase contenedora debe inicializarse. a pesa r de lo cual en la clase derivada no hay ningún objeto predetenninado con el que asocia rse. Es necesario utili zar una sinrax is especial para que dicha asociación se haga de fonna explíc ita: // : innerclassesjlnheritlnner.java jI Heredando de una clase interna. class Withlnner class Inner {}
public class Inheritlnner extends WithInner.Inner II! Inheritlnner () {} lINo se compilará Inheritlnner (Withlnner wi) { wi. super () ¡ public static void main (S tring [) args) { Withlnner wi = new Withlnner() ¡ Inheritlnner ii = new Inheritlnner(wi) i
Puede ver que Inheritlnncr sólo amplía la clase interna, no la ex terna. Pero cuando llega el momento de crear un construc· tor, el predeterminado no sirve y no podemos limitarnos a pasar una referencia a un objeto contenedor. Además, es necesa· rio utihzar la si ntax is: enclosingClassReference.super() ¡
dentro del const ructor. Esto proporciona la referencia necesaria y el programa pod rá así compilarse.
Ejercicio 26:
(2) Cree una clase con una clase interna que tenga un constructor no predetenninado (uno que tom e argumentos). Cree una segunda clase con una clase interna que herede de la primera clase interna.
¿Pueden sustituirse las clases internas? ¿Qué sucede cuando creamos una clase interna, luego heredamos de la clase contenedora y redefinimos la clase interna? En otras palabras, ¿es posible "sustituir" la clase interna completa? Podría parecer que esta técnica resultaría muy útil , pero el "sustituir" un a clase interna como si fuera otro método cualquiera de la clase externa no tiene, en realidad, ningún efecto:
11 : innerclasses/BigEgg.java II No se puede sustituir una clase interna como si fuera un método. import static net.mindview.util.Print .* ¡ class E99 { private Yolk y¡ protected class Yolk { public Yolk() { print{"Egg Yolk() ");
} public Egg ()
{
print ("New Egg () " ) ; y = new Yolk () ;
}
10 Clases internas 237
public class BigEgg extends Egg { public class Yolk { publ ic Yolk () { print (" BigEgg Yolk (1 ") ;
public static void main (String [J new BigEgg () i
args)
{
/ * Output: New Egg () Eg9. Yolk () *///,El compilador sintetiza automática ment e el constructor predeterminado, y éste invoca al constructor predeterminado de la clase base. Podríamos pensa r que puesto que se está creando un objeto BigEgg, se utili zará la versión "sustituida" de Yolk, pero esto no es así, como podemos ver ana lizando la salida. Este ejemplo muestra qu e no hay ningún mecanismo mági co adicional relacionado con las clases internas que entre en acc ión al heredar de la clase externa. Las dos clases internas son entidades completamente separadas, cada una con su propio espacio de nombres. Sin embargo. lo que sigue siendo posible es heredar explícitamente de la clase interna:
JI: innerclassesjBigEgg2.java JI Herencia correcta de una clase
interna. import static net . mindview . util.Print.*¡
class Egg2 { protected class Yolk { public Yolk () { print (" Egg2 . Yolk () "); } public void fl) {print("Egg2.Yolk.f()");}
private Yolk y = new Yolk (); public Egg2 () { print ( "New Egg2 () "); } public void insertYolk{Yolk yy) { Y = yy¡ public void 9 () { y . f (); }
public class BigEgg2 public class Yolk public Yolk () { public void f ()
extends Egg2 { extends Egg2.Yolk print (" BigEgg2 . Yolk () "); } { print ("BigEgg2. Yolk. f () ") ;
public BigEgg2 () { insertYolk (new Yolk () ) ; public static void main (String [] args) { Egg2 e2 = new BigEgg2(); e2.g() ;
1* Output: Egg2 . Yolk () New Egg2 () Egg2 . Yolk () BigEgg2 . Yolk () BigEgg2 . Yolk.f() */ / /
>
Allora, BigEgg2.Yolk amplía explícitamente extends Egg2.Yolk y sustituye sus métodos. El método insertYolk( l pennite que BigEgg2 generalice uno de sus propios objetos Yolk a la referencia y en Egg2 , por lo que g( l invoca y.f( l, se utili za la vers ión sustituida de f() . La segund a llamada a Egg2.Yolk() es la llamada que el constructor de la clase base hace al constructor de BigEgg2. Yolk. Como puede ver, cuando se llama a g( l se utili za la versión sustituida de f( l·
238 Piensa en Java
Clases internas locales Corno hemos indicado anterionnen te. también pueden crearse clases internas dentro de bloques de código, nonnalmente dentro del cuerpo de un método. Una clase interna local no puede tener un especificador de acceso, porque no fonna pane de la clase externa, pero si que tiene acceso a las variables finales del bloque de código actual y a todos los miembros de la clase contenedora. He aquí un ejemplo donde se compara la creación de una clase interna local con la de una clase interna anónima:
11: innerclasses/LocallnnerClass.java
II Contiene una secuencia de objetos. import static net.mindview.util.Print.*; interface Counter int next () ;
public class LocallnnerClass { private int count = O; Counter getCounter (fi nal String name) { II Una clase interna local: class LocalCounter implements Counter public LocalCounter () { II La clase interna local puede tener un constructor print{IILocaICounter(}11) ; public int next () { printnb(name); II Acceso a variable local final return count++;
return new LocaICounter();
II Lo mismo con una clase interna anónima : Counter getCounter2 (final String name) { return new Counter() { II La clase interna anónima no puede tener un constructor II nominado , sino sólo un inicializador de instancia: { print ( 11 Counter () 11 ) ; public int next () { printnb(name)¡ II Acceso a una variable local final return count++; ) )
;
public static void main (String [] args) { LocallnnerClass lic = new LocallnnerClass(); Counter el = lic . getCounter(IILoeal inner "), c2 = lic.getCounter2 ( IIAnonymous inner " ); for(int i = O; i < 5; i++) print(el.next ()) ; for(int i = O; i < 5; i++) print (e2. next () ) ;
1* Output : LocalCounter () Counter ()
10 Clases internas 239
Local Local Local Local Local
inner O inner 1 inner 2 inner 3 inner 4
Anonyrnous Al1onyrnous
inner 5 inner 6
Anonymous inner 7 Anonymous inner 8 Anonymous inner 9
* /// ,Counter devuelve el siguiente valor de una secuencia. Está implementado como una clase local y como una clase interna anónima, teniendo ambas los mismos comportamientos y capacidades. Puesto que el nombre de la clase interna local no es accesible fuera del método, la única justificación para utilizar una clase interna local en lugar de una clase interna anónima es que necesitemos un constructor nominado y/o un constructor sobrecargado, ya que una clase interna anónima sólo puede utilizar un mecanismo de inicialización de instancia.
Otra razón para definir una clase interna local en lugar de una clase interna anónima es que necesitemos construir más de un objeto de dicha clase.
Identificadores de una clase interna Puesto que todas las clases generan un archivo .class que almacena toda la infonnación re lativa a cómo crear objetos de dicho tipo (esta infornlación genera una "metaclase" denominada objeto Class), podemos imaginar fácilmente que las cIases internas también deberán producir archivos .class para almacenar la infonnación de sus objetos Class. Los nombres de estos archivos Iclasses responden a una fórnlUla estricta: el nombre de la clase contenedora, seguido de un signo '$', seguido del nombre de la clase interna. Por ejemplo, los archivos .class creados por Locallnne r Class.java incluyen: Counter.class LocallnnerClass$l.class LocallnnerClass$lLocalCounter.class Locallnne rClass.class
Si las clases internas son anónimas, el compilador simplemente genera una serie de números para que actúen como identificadores de las clases internas. Si las clases internas están anidadas dentro de otras clases internas, sus nombres se añaden simplemente después de un '$' y del identificador o identificadores de las clases externas. Aunque este esquema de generación de nombres internos resulta simple y directo, también es bastante robusto y pennite traLar la mayoría de las sittlaciones. 5 Puesto que se trata del esquema estándar de denominación para Java, los archivos generados son automáticamente independientes de la platafomla (tenga en cuenta que el compilador Java modifica las clases internas de múltiples maneras para hacer que funcionen adecuadamente).
Resumen Las interfaces y las clases internas son conceptos bastante más sofisticados que los que se pueden encontrar en muchos lenguajes de programación orientada a objetos; por ejemplo, no podremos encontrar nada similar en C++. Ambos conceptos resuelven, conjuntamente, el mismo problema que C++ trata de resolver con su mecanismo de herencia múltiple. Sin embargo, el mecanismo de herencia múltiple en C++ resulta bastante dificil de uti lizar, mientras que las interfaces y las clases internas de Java son, por comparación, mucho más accesibles. Aunque estas funcionalidades son, por sí mismas, razonablemente sencillas, el uso de las mismas es una de las cuestiones fundamenta les de diseño, de fonna similar a lo que ocurre con el polimorfismo. Con el tiempo, aprenderá a reconocer
S Por otro lado, '$ ' es un mctacarácter de la shell Unix, por lo que en ocasiones podemos encontramos con problemas a la hora de listar los archivos .class. Resulta un tamo extraño este problema, dado que el lenguaje Java ha sido definido por Sun, una empresa volcada en el mercado Unix. Supongo que no tuvieron en cuenta el problema, pensando en quc los programadores se centrarian principahneme en los archivos de código fuente.
240 Piensa en Java
aquellas situaciones en las que debe utili zarse una interfaz o una clase interna. o ambas cosas. Pero almenas, en este pUnto del libro, sí que el lector debería sentirse cómodo con la sintaxis y la semántica aplicables. A medida que vaya viendo cómo se aplican estas funcionalidades, tenninará por interiorizarlas. Puede encontrm las soluciones a los ejercicios seleccionados en el documento electrónico rile Thinkil/g ;1/ )OI'Q Anllotaled SOllllioll GlIide, disponible para la venta en II'ww.MilldView."el.
Al macenamiento de objetos
Es un programa bastante simple que sólo dispone de una cantidad de objetos con tiempos de vida conocidos. En general, los programas siempre crearán nuevos objetos basándose en algunos criterios que sólo serán conocidos en tiempo de ejecución. Antes de ese momento, no podemos saber la cantidad ni el tipo exacto de los objetos que necesitamos. Para resolver cualquie r problema general de programación, necesitamos poder crear cualquier número de objetos en cualquier momento y en cualquier lugar. Por tanto, no podemos limitarnos a crear una referencia nominada para almacenar cada uno de los objetos: MiTipo unaReferencia¡
ya que no podemos saber cuántas de estas referencias vamos a necesitar. La mayoría de los lenguajes proporciona alguna mane ra de resolver este importante problema. Java dispone de varias formas para almacenar objetos (o más bien, referencias a objetos). El tipo soportado por el compilador es la matriz, de la que ya hemos hab lado antes. Una matriz es la fonna más efic iente de almacenar un gmpo de objetos, y recomendamos utilizar esta opción cada vez que se quiera almacenar un grupo de primitivas. Pero una matriz tiene un tamaño fijo y. en el caso más genera l, no podemos saber en el momento de escribir el programa cuántos objetos vamos a necesitar o si hará falta una fomla más sofisticada de almacenar los objetos, por lo que la restricción relativa al tamaí'io fijo de una matriz es demasiado limitanteo La biblioteca j ava.u til tiene un conjunto razonablemente completo de clases contenedoras para resolver este problema, siendo los principales tipos básicos List, Set, Q ueuc y Map (lista, conjunto. cola y mapa). Estos tipos de objetos también se conocen COD el nombre de clases de colección, pero como la biblioteca Java utiliza el nombre Collection para hacer referenci a a un subconjunto concreto de la biblioteca, en este texto utilizaremos el ténnino más general de ¡'contenedor". Los contenedores proporcionan fonnas so fi sticadas de almacenar los objetos, y con ellos podemos resolver un sorprendente número de problemas. Además de tener otras características (Set, por ejemplo, sólo almacena un objeto de cada valor mientras que Map es una matriz asociativa que pennite asociar objetos con otros objetos), las clases contenedoras de Java tienen la funcionalidad de cambiar automáticamente de tamai'io. Por tanto, a diferencia de las matrices, podemos almacenar cualquier número de objetos y no tenemos que preocupamos, mientras estemos escribiendo el programa, del tamaño que tenga que tener el contenedor. Aún cuando no tienen soporte directo mediante palabras clave de Java, I las clases contenedoras son herramientas fundamentales que incrementan de manera significativa nuestra potencia de programación. En este capítulo vamos a aprender los aspectos básicos de la biblioteca de contenedores de Java poniendo el énfasis en la ut ilización típica de los contenedores. Aquí, vamos a centramos en los contenedores que se uti lizan de mane ra cotidiana en las tareas de programación. Posterionnente, en el Capítulo 17, Análisis de/aliado de los cOnlenedores, veremos el resto de los contenedores y una serie de detalles acerca de su funcionalidad y de cómo utilizarlos.
!
Diversos lenguajes como PerL Payton y Ruby tienen soporte nativo para los contenedores.
242 Piensa en Java
Genéricos y contenedores seguros respecto al tipo Uno de los problemas de utili za r los con tenedores anteriores a Java SE5 era que el compilador penl1itía insertar un tipo inco~ rrecto dentro de un contenedor. Por ejemplo, considere un contenedor de objetos Apple que utilice el contenedor más bási~ co general, ArrayList. Por ahora. podemos considerar que ArrayLis t es "una matriz que se expande automáticamente". La utilización de una matTiz Ar ra yList es bastante simple: basta con crear una, insertar objetos utilizando addQ y acceder a ellos mediante getQ, utilizando un índice: es lo mismo que hacemos con las matrices, pero sin emplear corchetes.2 ArrayList también dispone de un método size( ) que nos pennite conocer cuánros elementos se han añadido, para no utili~ zar inadvertidamente índices que estén más allá del contenedor que provoquen un error (generando una excepción de tiem~ po de ejecución; hablaremos de las excepciones en el Capítulo 12, Tratamiento de errores medio1l1e excepciones). En este ejemplo. insertamos en el contenedor objetos App le y Orange y luego los extraemos. Nornlalmente, el compilador Java proporcionará una advertencia, debido a que el ejemplo no lisa genéricos. Aquí, empleamos una anotación de Java SES para suprimir esas advertencias del compilador. Las anotaciones comienzan con un signo de '@', y admiten un argumento; es ta anotación en concreto es @S uppress\Varnings y el argumento indica que sólo deben suprimirse las advertencias no comprobadas ("lIl1checked'):
11: holding/ApplesAndOrangesWichoutGenerics.java 1I Ejemplo simple de contenedor (produce advertencias del compilador). II {ThrowsException} import java . util.*; class Apple private static long counter; prívate final l ong id = counter++¡ public long id () { reCurn id; }
elass Orange {} public class ApplesAndOrangesWithoutGenerics @SuppressWarn íngs ( "unchecked" ) public static void main(String[] args) ArrayList apples = new ArrayList() ¡ for(int i = O; i < 3; i++)
apples.add(new Apple());
II No se impide añadir un objeto Orange: apples.add(new Orange(»; for(int i = O; i < apples.size()
( (Apple) apples .get
(i) ) . id
i
i++)
11 ;
II Orange sólo se detecta en tiempo de ejecución 1* (Execute to see output) * 1I 1:Hablaremos más en detalle de las anotaciones Java SE5 en el Capítulo 20, Anotaciones. Las clases Apple y Orange son diferentes; no tienen nada en común salvo que ambas heredan de Object (recuerde que si no se indica explícitamente de qué clase se está heredando, se hereda automáticamente de Object). Puesto que ArrayList almacena objetos de tipo Objcct, no sólo se pueden aliad ir objetos Apple a este contenedor utilizando el método add() de ArrayList, si no que también se pueden añadir objetos Orange sin que se produzca ningún tipo de advertencia ni en ti em ~ po de compilación ni en tiempo de ejecución. Cuando luego tratamos de extraer lo que pensamos que son objetos Apple uti~ li zando el método gct() de ArrayList, obtenemos una referencia a un objeto de tipo Object que deberemos proyectar sobre un objeto Apple. En tonces, necesitaremos encerrar toda la expresión entre paréntesis para forzar la evaluación de la proyec~ ción antes de invocar el método id( ) de Apple; en caso contrario. se producirá un erro r de sintaxis.
2 Éste es uno de Jos casos en los que la sobrecarga de operadores resultaría convenieOfe. Las clases contenedoras de C++ y C# producen una sintaxis [mIS limpia utilizando la sobrecarga de operadores.
11
Almacenamiento de objetos 243
En tiempo de ejecución, a l intentar proyectar el objeto Orange sobre un objeto Apple. obtendremos un error en la farnla de la excepción que antes hemos mencionado. En el Capínilo 20, Genéricos, veremos que la creación de clases utilizando los genéricos de Java puede resultar muy compleja. Sin embargo, la aplicación de c lases genéricas predefinidas suele resultar sencilla. Por ejemplo. para defmir un contenedor ArrayList en el que almacenar objetos Apple, tenemos que indicar ArrayList en lugar de sólo Arra:yList. Los corchetes angulares rodean los parámetros de lipo (puede haber más de uno) , que especifican el tipo o tipos que pueden almacena rse en esa instancia del contenedor. Con los genéricos ev itamos, en tiempo de compilación, que se puedan introducir objetos de tipo incorrecto en un contenedor.} He aquí de nuevo el ejemplo utilizando genéricos: ji : holding/ApplesAndOrangesWithGenerics . java import java.util. *¡
public class ApplesAndOrangesWithGenerics public static void main (String [] args) { ArrayList apples = new ArrayList(); for(int i = o; i < 3¡ i++) apples.add{new Apple(»; 1/ Error en tiempo de compilación: 11 apples.add(new Orange (»; for{int i == O; i < apples.size(); i++) System . out. println (apples. get (i) . id () ) ¡ 1/ Utilización de foreach: for(Apple c : apples) System.out.println(c.id( » ¡ / * Output:
Ahora el compilador evitará que introduzcamos un objeto Orange en apples, convirtiendo el error en tiempo de ejecución en un error de tiempo de compi lación. Observe también que la operación de proyección ya no es necesaria a la hora de extraer los elementos de la lista. Puesto que la lista conoce qué tipos de objetos almacena, ella misma se encarga de realizar la proyección por nosotros cuando invocamos get(). Por tanto. con los genéricos no sólo podemos estar seguros de que el compilador comprobará el tipo de los objetos que introduzcamos en un contenedor, sino que también obtendremos una si ntaxis más limpia a la hora de utilizar los objetos almacenados en el contenedor. El ejemplo muestra también que, si no necesitamos utilizar el índice de cada elemento, podemos utiliza r la sintaxi sjoreach para seleccionar cada elemento de la lista. No estamos limitados a almacenar el tipo exacto de objeto dentro de un contenedor cuando especificamos dicho tipo corno un parámetro genérico. La operación de generalización (upcasting) funciona de igual fonna con los genéricos que con los demás tipos:
11 : holding / GenericsAndUpcasting.java import java.util.*; class GrannySmith extends Apple {} class Gala extends Apple {} ) Al final del Capitulo 15. Gené/'icos, se incluye una explicación sobre la gravedad de este problema. Sin embargo, dicho capítulo también explica que los genéricos de Java resultan útiles para otras cosas además de definir contenedores que sean seguros con respecto al tipo de los datos.
244 Piensa en Java class Fuji extends Apple {} class Braeburn extends Apple {} public class GenericsAndUpcasting public static void main(String[] args} ArrayList apples = new ArrayList(}; apples.add(new GrannySmith()); apples.add(new Gala()}; apples.add(new Fuji()}; apples.add{new Braeburn(}}; for(Apple e : apples} System.out.println(c) ;
1* Output: (Samplel GrannySmith@7d772e Gala@11b86e7 Fuji @35ce36 Braeburn@757aef
* /// ,Por tanto, podemos aiiadir un subtipo de Apple a un contenedor que hayamos especificado que va a almacenar objetos Apple. La salida se produce utilizando el método toString() predetenninado de Object, que imprime el nombre de la clase seguido de una representación hexadecimal sin signo del código hash del objeto (generado por el método hashCode( )l. Veremos más detalles acerca de los códigos hash en el Capítulo 17, Análisis detallado de los contenedores. Ejercicio 1:
(2) Cree una nueva clase llamada Gerbil con una variable int gerbilNumber que se inicializa en el constructor. Añada un método denominado hop( ) que muestre el valor almacenado en esa variable entera. Cree una lista ArrayList y añada objetos Gerbil a la lista. Aho ra, utilice el método get() para desplazarse a través de la li sta e invoque el método hop( ) para cada objeto Gerbil.
Conceptos básicos La biblioteca de contenedores Java toma la idea de "almacenamiento de los objetos" y la divide en dos conceptos di stintos, expresados mediante las interfaces básicas de la biblioteca:
1. Collection : una secuencia de elementos individuales a los que se apli ca una o más reglas. Un contenedor List debe almacenar los elementos en la fom1a en la que fueron insertados, un contenedor Set no puede tener elementos duplicados y un contenedor Queue produce los elementos en el orden determinado por una disciplina de cofa (que nonnalmente es el mismo orden en el que fueron insertados). 2. Map : un grupo de parejas de objetos clave-valor, que permite efectuar búsquedas de va lores utilizando una clase. Un contenedor ArrayList pennite buscar un objeto utilizando un número, por lo que en un cierto sentido sirve para asociar números con los objelOs. Un mapa permite buscar un objeto utilizando otro objeto. También se le denomina matriz asociativa, (porque asocia objetos con otros objetos) o diccionario (porque se utili za para buscar un objeto valor mediante un objeto clave. de la misma fonTIa que buscamos una definición utili zando una palabra). Los contenedores Map son herramientas de programación muy potentes. Aunque no siempre es posible. deberíamos tratar de escribir la mayor parte del código para que se comunique con estas interfaces; asi mi smo, el único lugar en el que deberíamos especificar el tipo concreto que estemos usando es el lugar de la creación del contenedor. Así, podemos crear un contenedor List de la fonna siguiente: List apples
=
new ArrayList ();
Observe que el contenedor ArrayList ha sido general izado a un contenedor List, por contraste con la fonna en que lo habíamos tratado en los ejemplos anteriores. La idea de utilizar la interfaz es que, si posteriormente queremos cambiar la implementación, lo úni co que tendremos que hacer es efectuar la modifi cación en el punto de creación, de la fomla siguiente: List apples = new LinkedList(};
11 Almacenamiento de objetos 245 Así, nonna lmente crearemos un objeto de una clase concreta. lo generalizaremos a la correspondiente interfaz y luego utilizaremos la interfaz en e l resto de l código. Esta técnica no siempre sirve. porque algunas c lases di sponen de funcionalidad adicionaL Por ejemplo, LinkedList tiene métodos adicionales que no fonllan parte de la interfaz List mientras que TreeMap tiene métodos que no se encuentran en la interfaz Map. Si nec esita mos usar esos métodos, no podremos efectuar la operación de generalización a la interfaz más generaL La interfaz Collection generaliza la idea de secuencia: una forma de almacenar un grupo de objetos. He aquí un ejemplo simple que rellena un contenedor CoUection (representado aquí mediante un contenedor ArrayList) con objetos Illteger y luego imprime cada elemento en el contenedor resultante:
11 : holding/SimpleCollection java import java . util. * ¡ public class SimpleCollection public static void main(String[] argsl ( COl lection e = new ArrayList (); for (in t i = O¡ i < 10 ¡ i+ +) c . add(il ¡ II Autoboxing for(Integer i : el System out.print(i + 11 );
1* Output: 0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
. /// ,Puesto que este ejemplo sólo utiliza métodos Collection, cualquier objeto que herede de una clase de Collection funciona-
rá, pero ArrayList representa e l tipo más básico de secuencia. El nombre del método add() sugiere que ese método introduce un nuevo elemento en el contenedor Collection. Sin embargo, la documentación indica expresamente que add() "garanti za que este objeto Collection contenga el e lemento especificado". Esto es así para pennitir la existencia de contenedores Set, que añaden el elemento sólo si éste no se encuentra ya en el contenedor. Con un objeto ArrayList, o con cualquier tipo de List, add( ) siempre significa " insertar e l e lemento", porque las listas no se preocupan de si ex isten duplicados. Todas las colecciones pueden recorrerse utilizando la sintaxi sforeach, como hemos mostrado aquí. Más adelante en el capítulo ap renderemos acerca de un co ncepto más flexibl e denominado I!erador.
Ejercicio 2:
(1) Modifique SimpleCollection.java para utilizar un contenedor Set para c.
Ejercicio 3:
(2) Modifique innerclasses/Sequence.java de modo que se pueda añadir cualquier número de elementos.
Adición de grupos de elementos Existen métodos de utilidad en las clases de matrices y colecciones de java.util que añaden grupos de elementos a una colección. El método Arrays.asList( ) toma una matri z y una lista de elementos separados por comas (utilizando varargs) y lo transforma en un objeto List. Collections.addAII( ) loma un objeto Collection y una matriz o una lista separada por comas y aliade los elementos a la colección. He aquí un ejemplo donde se ilustran ambos métodos, así como el método addAII( ) más convencional que forma parte de todos los tipos Collection :
11 : holding/AddingGroups . java II Adición de grupos de elementos a objetos Collection. import java.util.*; publie elass AddingGroups public statie void main(String[] argsl { Collection eollection new ArrayList (Arrays . asList (1, 2, 3,4,5»; Integer[l morelnts " { 6,
7,
B,
9, 10 };
246 Piensa en Java collection.addAll (Arrays.asList (morelnts )) ; II Se ejecuta bastante más rápido, pero no se puede II construir una colección de esta forma: Collections.addAll (collection, 11, 12, 13, 14, 15 ) ¡ Collections.addAll (collection, morelnts) i II Produce una lista 11 respaldada 11 en una matriz: Listclnteger> list = Arrays.asList ( 16, 17, 18, 19, 2 0) ; list.set ( 1, 99 ) ¡ II OK - - modificar un elemento II list.add(21 ) ¡ II Error de ejecución porque la matriz II subyacente no se puede cambiar de tamaño.
El constructor de una colección puede aceptar otra colección con el fin de utilizarla para inicializarse a sí misma, así que podemos emplear Arrays.asLisl( ) para generar la entrada para el constructor. Sin embargo, Collections.addAII( ) se eje. cuta mucho más rápido y resulta igualmente sencillo construir el objeto Collection sin ningún elemento y luego invocar Collections.addAII( ), por lo que ésta es la técnica que más se suele utili za r. El método miembro Colleclion.addAII( ) sólo puede tomar como argumento otro objeto Colleclion, por lo que no es tan flexible como Arrays.asList( ) o Collections.addAII( J, que utili zan listas de argumentos variables. También es posible utilizar directamente la salida de Arrays.asListO como una lista, pero la representación subyacente en este caso es la matri z, que no se puede cambiar de tamaño. Si tratamos de añadir o borrar elementos en dicha lista, eso impli. caría cambiar el tamaño de la matriz, por lo que se obtiene un error "Unsupported Operation" en tiempo de ejecución. Una limitación de Arrays.asList() es que dicho método trata de adivinar cuál es el tipo de lista resultante, sin prestar aten· ción a la variable a la que la estemos asignando. En ocasiones, esto puede causar un problema :
11 : holding/AsListlnference.java I I Arrays.asList( ) determina el tipo en sí mismo. import java.util.*¡ elass class class class class class
Snow {} Powder extends Snow {} Light extends Powder { } Heavy extends Powder {} Crusty extends Snow { } Slush extends Snow {}
public class AsListlnference public static void main {String(] args l List c Snow> snow1 = Arrays.asList { new Crusty {) , new Slush () , new Powder ())
II 11 11 11 11 11
i
No se compilará: ListcSnow> snow2 = Arrays.asList { new Light{ ) , new Heavy ( ) ) ; El compilador dirá: found java.util.List required: java.util.ListcSnow>
1I Collections .addAll () no se confunde : ListcSnow> snow3 = new ArrayListcSnow> () ¡ Collections.addAll (snow3, new Light ( ), new Heavy {») II Proporcionar una indicación utilizando una
II especificación explícita del tipo del argumento: ListcSnow> snow4 = Arrays.cSnow>asList { new Light{), new Heavy {));
i
11 Almacenamiento de objetos 247 Al tratar de crear snow2, Arrays.asList( ) sólo di spone del tipo Powder, por lo que crea un objeto List en lugar de List, mientras que Collections.addAII() funciona correctamente, porque sa be, a partir del primer argumento, cuál es el tipo de destino. Como puede ver por la creación de snow4, es posible insertar una " indicación" en mitad de Arrays.asList(), para decirle al compilador cuál debe ría ser el tipo de destino real para el objeto List producido por Arrays.asList(). Esto se denomina especificación explícita del tipo de argumento. Los mapas son más complejos co mo tendremos oportunidad de ver, y la biblioteca estándar de Java no proporciona ninguna fonna para inicializarlos de manera automática. sa lvo a partir de los contenidos de otro objeto Map .
Impresión de contenedores Es necesario utilizar Arrays.toString( ) para generar una representación imprimible de una matri z, pero los contenedores se imprimen adecuadamente sin ninguna medida especial. He aquí un ejemplo que también nos va a pennitir presentar los contenedores básicos de Java:
11: holding/PrintingContaine r s.java
II Los contenedores se imprimen automáticamente. import java.util. * ; import static net.mindview . util.Print.*¡ public class PrintingContainers { static Collection fill(Collection collection) collection .add ( urat U) ; collection.add(Ucat U) ; col lection. add ( " dog 11) ; collection . add(Udog U) ; return collection¡ sta tic Map fill (Map map) map.put(IIrat", "Fuzzyll); map .put ( "cat U, "Rags"); map .put ( IIdog", "Bosco") ¡ map.put("dog", "Spot"); return map;
{
{
public static void main{String[1 args) print(fill(new ArrayList())); print(fill(new LinkedList())); print(fill(new HashSet())); print(fill(new TreeSet())); print(fill(new LinkedHashSet())); print(fill(new HashMap())); print(fill(new TreeMap())); print{fill(new LinkedHashMap())) i
1* Output: [rat, [rat, [dog, [cat, [rat,
cat, dog, dog] cat, dog, dog] cat, rat] dog, rat] cat, dog] {dog~S pot, cat=Rags, rat~Fuzzy} {cat ~Rags, dog=Spot, rat~Fuzzy} {rat~ Fuzzy, cat~Rags, dog=Spot}
*///,Este ejemplo muestra las dos categorías principales en la biblioteca de contenedores de Java. La distinción entre ambas se basa en el número de elementos que se almacenan en cada "posición" del contenedor. La categoría Collection sólo almace-
248 Piensa en Java na un e lemento en cada posición; esta categoría incluye el objeto List, que almacena un gnlpo de elementos en una secuen. cia especificada, el objeto Set, que no pemlite ailadir un elemento idéntico a otro que ya se encuentre dentro del conjunto y el objeto Queue. que sólo pennite insertar objetos en un "extremo" del contenedor y extraer los objclOs del arra "extremo" (en lo que a este ejemplo respecta se trataría simplemente de otra forma de manipu lar una secuenc ia, por lo que no lo hemos incluido). Un objeto Map, por su parte, almacena dos objetos, una clave y un valor asociado, en cada posición.
A la salida, podemos ver que el comportamiento de impresión predeterminado (que se implementa mediante el método toString() de cada contenedor) genera resultados razonablemente legibles. Una colección se imprime rodeada de corche. tes, estando cada elemento separado por una coma. Un mapa estará rodeado de llaves, asociándose las claves y los valores mediante un signo igual (las claves a la izquierda y los va lores a la derecha). El primer método fill() funciona con todos los tipos de colección, cada uno de los cuales se encarga de implementar el meto. do add() para incluir nuevos elementos. ArrayL ist y LinkedList son tipos de listas y, como puede ver a la salida, ambos almacenan los elementos en el mi smo orden en el que fueron insertados. La diferencia entre estos dos tipos de objeto no está sólo en la ve locidad de ciertos tipos de ope. raciones. sino también en que la clase LinkedList contiene más operaciones que ArrayList. Hablaremos más en detalle de estas operaciones posterionnente en el capítulo. HashSet, TreeSet y Linkedl-lashSet son tipos de conjuntos. La salida muestra que los objetos Set sólo pemliten almacenar una copia de cada elemento (no se puede introducir dos veces el mismo elemento), pero tamb ién muestra que las dife· rentes implementaciones de Set almacenan los elementos de manera distinta. HashSet almacena los e lementos utilizando una técnica bastante compleja que ana li zaremos en el Capítulo 17, Análisis detallado de los cOlllenedores, lo único que nece· sitamos saber en este momento es que dicha técnica representa la fonlla más rápida de extraer elementos y, como resultado, el orden de almacenamiento puede parecer bastante extraño (a menudo, lo único que nos preocupa es si un cierto objeto fom13 parte de un co njunto, y no el orden en que aparecen los objetos). Si el orden de almacenamiento fuera impar· tante, podemos utilizar un objeto TreeSet, que mantiene los objetos en orden de comparación ascendente, o un objeto LinkedHashSet, que mantiene los objetos en el orden en que fueron "'adidos. Un mapa (también denominado matriz asociativa) pennite buscar un objeto utilizando una clave, como si fuera una base de datos simple. El objeto asociado se denomina va/Dr. Si tenemos un mapa que asocia los estados ameri canos con sus capitales y queremos saber la capital de Ohio, podemos buscarla utilizando "Ohio" como clave, de fonna muy similar al proceso de acceso a una matriz uti li za ndo un índice. Debido a este comportamiento, un objeto mapa sólo admite una co pia de cada clave (no se puede introducir dos veces la misma clave). Map.put(key, val"e) añade un valor (el elemento deseado) y lo asocia con una clave (aquello con lo que buscaremos el elemento). Map.get(key) devuelve el va lor asociado con una clave. En el ejemplo anterior sólo se añaden parejas de clavevalor, sin real izar ninguna búsqueda. Ilustraremos el proceso de búsqueda más ade lante. Observe que no es necesario especificar (n i preocuparse por ello) el tamaño del mapa, porque éste cambia de tamaño automáticamente. Asimismo, los mapas sabe n cómo imprimirse, mostrando la asociación existente entre claves y valores. En el orden en que se malllienen las claves y va lores dentro de un objeto Map no es e l orden de inserción, porque la implemen· lación HashMap utiliza un algoritmo muy rápido que se encarga de controlar dicho orden. En el ejemplo se utilizan las tres versiones básicas de Map: HashMap, TreeMap y LinkedHashMap. Al igual que Has hSet, HashMap proporciona la técnica de búsqueda más rápida, no almacenando los elementos en ningún orden aparente. Un objeto Treel\1ap mantiene las claves en un orden de comparación ascendente, mientras que LinkedHashMap tiene las claves en orden de inserción sin deja r, por ello, de ser igual de rápido que HashMap a la hora de realizar búsquedas. Ejercicio 4:
(3) Cree una clase generadora que devuelva nombres de personajes (como objetos String) de su pelicula favo rita cada vez que invoque next( ), y que vue lva al principio de la lista de personajes una vez que haya acabado con todos los nombres. Uti lice este generador para rellenar una matriz, un ArrayList, un LinkedList, un HashSet, un LinkedHashSet y un TreeSet, y luego imprima cada contenedor.
List Las listas garantizan que los elementos se mantengan en una secuenci a concreta. La interfaz List añade varios métodos a Collection que penniten la inse rción y la eliminación de elementos en mitad de una lista.
11
Almacenamiento de objetos 249
Existen dos tipos de objetos Lis!: •
El objeto básico ArrayList, que es el que mejor permite acceder a los elementos de fanna aleatoria, pero que resulta más Icnto a la hora de insertar y eliminar elementos en mitad de una lista.
•
El objeto LinkedList, que proporciona un acceso secuencial óptimo. siendo las inserciones y borrados en mitad de una lista enormemen te rápidos. LinkedList resulta relati vamente lento para los accesos aleatorios. pero tiene muchas más func ionalidades que Ar r ayList.
El siguiente ejemplo se adelanta un poco dentro de la estructura del libro, utilizando UDa biblioteca del Capítulo 14, Información de tipos para importar t)'peinfo.pets. Se trata de una biblioteca que contiene una jerarquía de clases Pet (mascota), junto algunas herramientas para generar aleatoriamente objetos Pet. No es necesario entender todos los detalles en este momento, si no que basta con sabe r que existe: (1) una clase Pet y varios subtipos de Pet y (2) que el método Pets.arrayList() estático devuelve un método ArrayList lleno de objetos Pet aleatoriamente seleccionados:
11: hOlding/ListFeatures.java import typeinfo.pets.*¡ import java.util.*; import static net.mindview.util.Print.*¡ public class ListFeatures { public static void main(String[] args) Random rand = new Random(47)¡ List pets = Pets.arrayList{7); print ("1 : " + pets) ¡ Hamster h = new Hamster{)¡ pets.add(hl ¡ II Cambio de tamaño automático print ("2 : " + pets) i print("3: " + pets.contains(h»; pets.remove(h) ¡ II Eliminar objeto a objeto Pet p = pets.get(2); print ("4 : "+ P + " " + pets. indexOf (p) ) ¡ Pet cymric = new Cymric()¡ print ( "5: !l + pets. indexOf (cymric» ¡ print ("6 : " + pets. remove (cymric» i II Debe ser el objeto exacto: print (" 7: " + pets.remove(p» i print (118 : + petsl ¡ pets.add(3, new Mouse(») ¡ II Insertar en un determinado índice print (" 9: " + pets) ¡ List sub = pets.subList{l, 4); print ("subList: " + sub); print ( " 10: " + pets. containsAll (sub) ) ; Collections . sort (sub) ; II Ordenación de la colección print ("sorted subList: " + sub) i II El orden no es importante en containsAll(): print("ll: " + pets.containsAll(sub»; Collections.shuffle(sub, rand) i II Mezclar los elementos print ("shuffled subList: " + sub) ¡ print("12: " + pets.containsAll(sub); List copy = new ArrayList (pets) ; sub = Arrays.asList{pets . get(1), pets.get(4»; print("sub: " + sub); copy.retainAll(sub) ; print("13: " .. copy) i copy = new ArrayList (pets) i II Obtener una nueva copia copy.remove(2); II Eliminar según un índice print("14: " + copy) i copy.removeAll(sub); II Sólo elimina objetos exactos print (" 15: " .. copy);
250 Piensa en Java copy.set(l, new MauSe()); print("16: " + copy);
JI Sustituir
un elemento
copy.addAll(2, sub), // Insertar una lista en el medio print("17: " + copy); print ("18: " + pets. isEmpty () ) ; pets . clear() i /1 Eliminar todos los elementos print("19: print("20:
" + pets); " + pets.isEmpty())¡
pets.addAll(Pets.arrayList{4)) print(I'21:
+
i
pets);
Object[] o = pets.toArray(); print("22: " + 0[3]);
Pet[] pa • pets . toArray(new Pet[O]); print("23, " + pa[3] .id());
/ * OUtput: 1:
[Rat, Manx, Cymric, Mutt,
Pug, Cymric, Pug)
2:
(Rat, true
Pug,
3, 4, 5, 6, 7,
Manx,
Cymric, Mutt,
Cymric,
Pug, Hamster]
Cymric 2 -1 false true B, [Rat, Manx, Mutt, Pug, Cymric, Pug] 9, [Rat, Manx, Mutt, Mouse, Pug, Cymric, Pug]
subList: [Manx, Mutt, 10: true
Mouse)
sorted subList: [Manx, Mouse, Mutt] 11: true shuffled subList: [Mouse, Manx, Mutt1 12: true sub: (Mouse, Pug] 13, [Mouse, Pug] 14, [Rat, Mouse, Mutt, Pug, Cymric, Pug1 15, [Rat, Mutt, Cymric, Pug) 16, [Rat, Mouse, Cymric, Pug} 17, [Rat, Mouse, Mouse, Pug, Cymric, PugJ lB, false 19, [] 20, true 21, [Manx, Cymric, Rat, EgyptianMau] 22: EgyptianMau 23, 14
* ///,Hemos numerado las lineas de impresión para poder establecer la relación de la salida con el código fuente. La primera línea de salida muestra la lista original de objetos Peto A diferencia de las matrices. un objeto List pennite añadir o eliminar elementos después de haberlo creado y el objeto cambia automáticamente de tamaño. Ésa es su característica fundamental: se trata de una secuencia modificable. En la linea de salida 2 podemos ver el resultado de arladir un objeto Hamster. El objeto se ha añadido al final de la lista. Podemos averiguar si un objeto se encuentra dentro de la lista utilizando el método contains(). Si queremos eliminar un objeto. podemos pasar la referencia a dicho objeto al método remove( j. Asimismo, si disponemos de una referencia a un objeto, podemos ver en qué número de indice está al macenado ese objeto dentro de la lista utili zando indexOf( j, como puede verse en la linea de sa lida 4. A la hora de determinar si un elemento fonlla parte de una lista, a la hora de descubrir el índice de un elemento y a la hora de eliminar un elemento de una lista a partir de su referencia, se utiliza el método equals() (que forma parte de la clase raíz Objectj. Cada objeto Pet se define como un objeto único, por lo que, aunque ex istan dos objetos Cymric en la li sta, si creamos un nue vo objeto Cym ri c y lo pasamos a indexOf( j, el resultado será -1 (indicando que no se ha encontrando el obje-
11 Almacenamiento de objetos 251 10): asimismo. si tratamos de eliminar el ObjclO con remove( ), e l va lor devuelto será false . Para otras clases, el método equals( ) puede esta r definido de forma diferente; dos objetos String. por ejemplo, serán iguales si los co ntenidos de las cadenas de ca racteres son idénti cos. Así que, para evi tarnos sorpresas, es importante ser consc iente de que e l co mportami ento de un objeto List va ría dependiendo del comportamie nt o del método equals(). En las líneas de sa lida 7 y 8, podemos ver que se puede eliminar perfec tamente un objeto que se corresponda exactamente con otro obje to de la lista. También resulta posible insertar un elemento en mitad de la lista , C0l110 puede verse en la línea de salida 9 y en el código que la precede. pero esta operación nos pemlite resaltar un potencial problema de rendimi ento: para un objeto LinkedList, la inserción y e liminación en mitad de una lista son operaciones muy poco costosas (salvo por, en este caso, e l propio acceso aleatorio en mitad de la lista), mientras que para un obje to ArrayList se trata de una operación bastante cos tosa. ¿Quiere esto decir que nunca deberíamos insertar elementos en mitad de una lista ArrayList, y que por el contrario. deberíamos emplear un objeto LinkedList en caso de tener que llevar a cabo esa operación? De ninguna manera: simplemente signi fica que debemos tener en cuenta el potencial problema, y que si co menzamos a hacer numerosas inserciones en mitad de un objeto ArrayList y nuestro programa comien=a a ralenti:orse, podernos sospechar que el posible culpable es la implementación de la lista concreta que hemos elegido (la mejor fomla de descubrir uno de estos cuellos de botella, como podrá ver en el suplemento http://MindVielt:net/ Books/BetterJO\'lI, co nsis te en utili zar un perfilador). El de la optimización es un problema bastante complicado, y lo mejor es no preocuparse por é l hasta que veamos que es abso lutamente necesario (au nque comprender los posibles problemas siempre resulta útil). El método subList() pem1Íte crear fácilmente una sublista a partir de otra lista de mayor tamaño, y esto produce de fonna natural un resultado true cuando se pasa la subli sta a containsAIl( ) para ver si los elementos se encuentran en esa lista de mayor lamal10. También merece la pena recalcar que e l orden no es importante: puede ver en las líneas de salida 11 y 12 que al invocar los métodos CoUections.sort() y Collections,shuftle() (que ordenan y a leatorizan, respecti vamente, los elementos) con la subli sta sub , el resultado de containsAII() no se ve afectado. subList() genera una lista respaldada por la lista origi nal. Por tanto, los cambios efectuados en la lista devue lta se verán reflejados en la lista origi nal, y viceversa. El método retainAII() es, en la práctica, una operaci ón de " intersección de conjuntos", que en este caso conserva todos los elementos de copy que se encuentren tambi én en sub. De nuevo, e l comportamiento resultante dependerá del método equals( ). La línea de salida 14 muestra el resultado de eliminar un e lemento utili zando su número índice, lo cual resulta bastante más directo que elimi narl o mediante la referencia al objelO, ya que no es necesario preocuparse acerca del comportamiento de equals( ) cuando se utilizan índices. El método removeAII() también opera de manera distinta dependiendo del método equals(). Como su propio nombre sugiere. se encarga de elimi nar de la Lista todos los objetos que estén en e l argumento de tipo List. El nombre del método set( ) no resulta muy adec uado, debido a la posible confusión con la clase Set, un mejor nombre habría sido "replace" (sustituir) porque este método se encarga de sustituir e l elemento situado en el índice indicado (e l primer argumento) con el segundo argumento. La línea de salida 17 muestra que, para las listas. existe un método addAII() sob recargado que nos permite insertar la nueva lista en mitad de la lista ori gi nal , en lugar de limitamos a añadirla al final con el método addAII() incluido en Collection . Las lineas de salida 18-20 muestran el efecto de los métodos isEmpty( ) y eloar( ). Las líneas de salida 22 y 23 muestran cómo puede conve rtirse cualquier objeto Collection en una matri z utili za ndo tOArray( ). Se trata de un método sobrecargado, la versión sin argu mentos devuelve una matriz de Object. pero si se pasa una matriz del tipo de destino a la versión sobrecargada, se generará una matri z del tipo especificado (suponi endo que los mecanismos de comprobac ión de tipos no detecten ningún error). Si la matriz utilizada como argumento resulta demasiado pequeña pa ra al macenar todos los objetos de la lista List (como sucede en este ejemplo), toArray() crea rá un a nueva matriz del tamaI10 apropiado. Los objetos Pet ti enen un método id(), pudiendo ver en el ejemplo cómo se invoca di cho método para uno de los objetos de la matriz resultante.
Ejercicio 5;
Modifique ListFeatures.java para luili zar enteros (recuerde la característica de o/iloboxing) en lugar de objetos Pet, y explique las diferencias que haya en los resultados.
Ejercicio 6;
(2) Modifique ListFeatures,java para utili zar cadenas de caracteres en lugar de objetos Pet, y explique las diferencias que haya en los resultados.
252 Piensa en Java
Ejercicio 7:
(3) Cree una clase y construya luego una matriz inicializada de objetos de dicha clase. Rellene una liSta a partir de la matriz. Cree un subconj unto de la lista uti lizando subList(), y luego e limine dicho Subco njun. to de la lista.
Iterator En cualquier conte nedor. tenemos que tene r una forma de inse rtar elementos y de vo lver a ex traerlos. Después de todo, esa es la función princ ipal de un contenedor: almacenar cosas. En una lista, add( ) es una de las fo rm as de inse rtar elementos y gct( ) es una de las fonnas de extraerlos. Si queremos razonar a UD nivel más a lto, nos encontramos con un prob lema: necesitamos desarrollar el programa con el tipo exacto de contenedor para poder utilizarlo. Esto puede parecer una desventaja a primera vista. pero ¿q ué sucede si escribi. mas código para una lista y posterionnente descub rim os que sería conveniente ap licar ese mismo código a un conjunto? Suponga que quisiéramos escribir desde el principio, un fragmento de código de propósi to general, que no supiera con que tipo de contenedor está trabajando, para poderlo ut il izar con diferentes tipos de contenedores sin reescribir dicho código. ¿Cómo podríamos hacer esto? Podemos util izar el concepto de Iterador (otro patrón de di seño) para conseguir este grado de abstracción. Un iterador es un objeto cuyo trabajo consiste en desplazarse a través de una secuencia y seleccionar cada UIlO de los objetos que la como ponen, sin que el programa cliente tenga que preocuparse acerca de la estmctura subyacente a dicha sec uencia. Además, un iterador es lo que usualmente se denomina un objeto ligero: un objeto que resulta barato crear. Por esa ra zón, a menudo nos encont ramos con rest ricciones aparentemente ex trañas que afectan a los iterado res; por ejemplo, un objeto Iterator de Java sólo se puede desp lazar en una dirección. No so n muchas las cosas que podemos hacer con un objeto Iterator salvo: 1. Pedi r a una colección que nos devuelva un iterador utili zando un método iterator(). Dicho iterador estará preparado para devolver el primer elemento de la secuencia.
2. Obtener el siguiente objeto de la secuencia mediante nexl(). 3. Ver si hay más objetos en la secuencia utilizando e l método hasNexl(). 4. Eliminar el último elemento devuelto por el ilerador mediante remove( ). Para ver cómo funciona, podemos volver a utilizar las herramientas de la clase Pet que hemos tomado prestadas del Capitulo 14, Información de tipos.
11 : holding / Simplelteration.java import typeinfo.pets.*; import java.util.*; public class Simplelteration public static void main{String(] args ) List pets = Pets.arrayList (12 ) i Iterator it = pets.iterator () ; while (i t . hasNext () I (
Pet p = it.next()i System.out.print(p.id () +
"
.11
+ p +
"
ti ) i
System.out.println() ; 1I Un enfoque más simple, siempre que sea posible: for {Pet p : pets) System . out . print (p. id ( l + It: ti + P + "l ; System.out.println() ; I1 Un iterador también permite eliminar elementos: it = pets.iterator {); for(int i = Di i < 6; i++l { it.next() ; it.remove() ; System.out .println{pets)
i
11 Almacenamiento de objetos 253
/*
Output:
O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 8:Cymric 9:Rat lO:EgyptianMau
11:Hamster O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 8:Cymric 9:Rat lO:EgyptianMau 11 :Hamster
[Pug, Manx,
Cymric,
Rat,
EgyptianMau,
Hamster]
* // /,Con un objeto Iterator, no necesitamos preocuparnos acerca del número de elementos que haya en el contenedor. Los métodos hasNoxt( ) y next() se encargan de dicha tarea por nosotros. Si simplemente nos estamos desplazando hacia adelante por la lista y no pretendemos modificar el propio objeto List, la sintaxis/oreach resulta más sucinta. Un ¡teTador pennite también eliminar el último elemento generado por next( ), lo que quiere decir que es necesario invocar a next( ) antes de llamar a remove( ).~ Esta idea de toma r un contenedor de objetos y recorrerlo para realizar una cierta operación con cada uno resulta muy potente y haremos un extenso uso de ella a lo largo de todo el libro. Ahora consideremos la creación de un metodo display() que sea neutral con respecto al contenedor utilizado:
11 : holding/CrossContainerlteration.java import typeinfo.pets.*¡ import java . util .*¡ public class CrossContainerlteration public static void display(Iterator it)
whilelit.hasNext())
{
(
Pet p = it.next()¡ System.out . print (p.id()
+
". " + p +
11
"
)
;
System.out.println () i public static void main (String[] args ) { ArrayList pets = Pets.arrayList ( B) ; LinkedList petsLL = new LinkedList (pets l i HashSet petsHS = new HashSet (pets ) ; TreeSet petsTS = new TreeSet(pets ) ; display(pets.iterator (» ; display(petsLL.iterator(» i display (petsHS.iterator(» i display(petsTS . iterator (» i
1* Output: O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 4: Pug 6:Pug 3:Mutt l:Manx 5:Cymri c 7:Manx 2:Cymric O:Rat 5 :Cymric 2:Cymric 7:Manx l:Manx 3:Mutt 6:Pug 4:Pug O:Rat * /// ,Observe que display() no contiene ninguna infonnación acerca del tipo de secuencia que está recorriendo. lo cual nos muestra la verdadera potencia del objeto 1tcrator: la capacidad de separar la operación de recorrer una secuencia de la estructura subyacente de recorrer dicha secuencia. Por esta razón, decimos en ocasiones que los ¡teradores unifican el acceso a los contenedores.
4 remo\C() es un método "opciona l" (existen otros métodos también opcionales), lo que significa que no todas las implementaciones de It era lor deben implementarlo. Este tema se trata en el Capitulo 17, Alláli.~is detallado de los cOIl/elledo/'es. Los contenedores de la biblioteca estándar de Java si imple-
mentan cl método rcmove{ l. por lo que no es necesario preocuparse de este lema hasta que lleguemos a este capitulo.
254 Piensa en Java
Ejercicio 8 :
(1) Modifique el Ejercicio l para que utilice un iterador para recorrer la lista mientras se invoca hop().
Ejercicio 9 :
(4) Modifique innercl asses/Sequ cnce.j ava para que Sequ ence funcione con un objeto !terator en lugar de un objeto Selector.
Ejercicio 10:
(2) Modifique el Ejercicio 9 del Capitulo 8, PolimO/jismo para utili zar un objeto Ar r ayList para almace_ nar los objetos Rod ent y un iterador para recorrer la secuencia de objetos Rodent.
Ejercicio 11:
(2) Escriba un método que uti lice un iterador para recorrer una colección e imprima el resultado de toStrin g() para cada objeto del contenedor. Rellene lodos los diferentes tipos de colecciones con una serie de objetos y aplique el método que haya diseliado a cada contenedor.
Listlterator ListIterator es un subtipo más potente de Iterator que sólo se genera mediante las clases List. Mientras que Iterator sólo se puede desplazar hacia adelante, Listlterator es bidireccional. También puede generar los índices de los elementos siguiente y anterior, en re lación al lugar de la lista hacia el que el ¡terador está apuntando, permite sustituir el último ele· mento listado uti lizando el método set( ). Podemos generar un iterador Listlterator que apun te al principio de la lista iuvo· cando listIterator(), y también podemos crear un iterador Listlterator que com ience apuntando a un índice n de la lista invocando Iistlterator(n). He aquí un ejemplo que ilustra estas capacidades: // : holding/Listlteration.java import typeinfo.pets. * ¡ import java.util. * ; public class Listlteration public static void main (String [] args) { List pets = Pets.arrayList(S) ¡ Listlterator it = pets.listlterator{) ¡ while (it .hasNext ()) System.out.print(it .next() + ", " + it.nextlndex{) + " , " + it.previouslndex( ) + "¡ ti ) ¡ System.out.println() ; 1/ Hacia atrás: while(it.hasPrevious()) System.out.print(it.previous() .id() + " " ) ¡ System.out.println() i System.out.println(pets) ; it = pets.listlterator(3) ¡ while (it . hasNext () I {
it . next () ; it.set(Pets . randomPet(» ¡ System.out.println(pets)¡
1* Output: Rat, 1, O¡ Manx, 2, 1; Cymric, 3, 2¡ Mutt, 4, 3¡ Pug, 5, 4¡ Cymric, 6, 5; Pug, 7, 6¡ Manx, S, 7; 76543210
[Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx) [Rat, Manx, Cymr ic, Cymric, Rat, EgyptianMau, Hamster, EgyptianMau]
El método Pets.randomPet() se utili za para sustimir todos los objetos Pet de la lista desde la posición 3 en adelante. Ejercicio 12:
(3) Cree y rellene un objeto List. Cree un segundo objeto List del mismo tamaño que el primero y utilice sendos objetos Listlterator para leer los elementos de la primera lista e insertarlos en la segu nda en orden inve rso (pruebe a explorar varias fonnas distintas de resolve r este problema).
11
Almacenamienlo de objetos 255
LinkedList LinkedList también implementa la interfaz List básica, como ArrayList, pero realiza ciertas operaciones de famla más eficiente que Ar ra yL ist (la inserción y la eliminación en la mitad de la li sta). A la inversa, resulta menos eficiente para las operaciones de acceso aleatorio. LinkcdList también incluye métodos que pemliten lIsa r este tipo de objetos como una pila, como una cola o como una cola bidireccional. Algunos de estos métodos son alias o li geramente variantes de los otros, con el fin de disponer de nombres que resulten más familiares dentro del contexto de un uso específico (en particular en el caso de los objetos Queue, que se lIsan para implementar colas). Por ejemplo, getFirst() y element() son idénticos: devuelven la cabecera (primer elememo) de la lista sin eliminarlo Y generan la excepción NoSuchElementException si la lista está vaCÍa. peek() es una variante de estos métodos que devuelve nuJl si la lista está vacía. rem oveFi rst() y remove() también son idénticos: eliminan y devuel ven la cabecera de la lista , generando la excepción NoSuchE lement Excc ption para una lista vacía; poll() es una variante que devuelve nuJl si la lista está vacía. addFirst() inserta un elemento al principio de la lista. offer() es el mismo método que add () y addLast(). Todos ellos añaden un elemento al final de la lista. rem oveLas t() e limina y devuelve el último elemento de la lista. He aquí un ejemplo que muestra las similitudes y diferencias básicas entre todas estas funcionalidades. Este ejemplo no repite aquellos comportamientos que ya han sido ilustrados en ListFcatures.java:
1/: holding/LinkedListFeatures.java impor t typeinfo.pets.*¡ import java.util.*¡ import static net.mindview.util.Print.*¡ public class LinkedListFeatures ( public static void main(String[] args) { LinkedList pets = new LinkedList(Pets.arrayList(S» ¡ print (pets) ¡ II Idénticos : print ("pets. getFirst (): " + pets. getFirst () ) ¡ print ( "pets. element () : " + pets. element () ) ¡ II Sólo difiere en el comportamiento con las listas vacías: print(Upets.peek(}; 11 + pets.peek(»); II Idénticos; elimina y devuelve el primer elemento: print ( "pet s. remove () : + pets.remove{» ¡ print ("pets. removeFirst (): " + pets. removeFirst () ) ; 11 Sólo difiere en el comportamiento con las listas vacías: print("pets.poll(), " + pets.poll()); print (pets) ¡ pets.addFirst(new Rat(»; print("After addFirst(): " + pets); pets.offer(Pets.randomPet(» i print("After offer(): ti + pets); pets.add(Pets.randomPet(» ¡ print (" After add () : + pets) ¡ pets.addLast(new Hamster(}} i print("After addLast(}: It + petsl; print ("pets. removeLast () : 11 + pets. removeLast () ) ; 11
1* Output: [Rat, Manx, Cymric, Mutt, Pug) pets.getFirst(): Rat
256 Piensa en Java pets.element(): Rat pets.peek(): Rat pets.remove(): Rat pets.removeFirst(): Manx pets.poll(): Cymric [Mutt, PugJ After addFirst (): (Rat, Mutt, PugJ After offer(): [Rat, Mutt, Pug, CymricJ After add(): [Rat, Mutt, Pug, Cymric, Pug] After addLast(): [Rat, Mutt, Pug, Cymric, Pug, pets.removeLast(): Hamster
HamsterJ
* ///> El resu ltado de Pets .• rr.yList( ) se entrega al constructor de LinkedList con el fID de rellenar la lista enlazada. Si analiza la interfaz Queue, podrá ver los métodos element(), offer(), peek( l, poll() y remove() que han sido aüadidos a LinkedList para poder disponer de la implementación de una cola. Más ade lante en el capítulo se incluyen ejemplos Completos del manejo de colas. Ejercicio 13:
(3) En el ejemplo innerclasscs/GreenhouseController.java, la clase Controllcr utiliza un objeto ArrayList. Cambie el código para util izar en su lugar un objeto LinkedList y emp lee un iterador para recorrer el co njunto de sucesos.
Ejercicio 14:
(3) Cree un objeto vacío LinkedList< lnteger>. Ut ilizando un iterador ListIterator. añada valores enteros a la lista insel1ándolos siempre en mitad de la misma.
Stack Una pila (sTack) se denomina en ocasiones "contenedor de tipo LLFO" (Iasl-in,first-o lll, el último en en trar es el primero en salir). El último elemento que pongamos en la "parte superior" de la pila será el primero que tengamos que sacar de la misma, como si se tratara de una pila de platos en una cafetería. LinkedList tiene métodos que implementan de fonna directa la funcionalidad de pila, por lo que también podríamos usar una lista enlazada LinkcdList en lugar de definir una clase con las característ icas de una pila. Sin embargo, definir una clase a propósito permite en ocasiones clarificar las cosas:
11: net/mindview/util/Stack . java II Definición de una pila a partir de una lista enlazada. package net.mindview.util; import java.util . LinkedList; public class Stack { private LinkedList storage = new LinkedList(); public void push (T v) { storage. addFirst (v); } public T peek() { return storage.getFirst(}; } public T pop () { return storage. removeFirst () ; public boolean empty () { return storage. isEmpty () i } public String toString() { return storage.toString();
///,Esto nos pennite introducir el ejemplo más sim ple posible de defin ición de una clase mediante genéricos. La que sigue al nombre de la clase le dice al compilador que se trata de un tipo paramelrizado y que el parámetro de ti po (que será su s~ tituido por un tipo real cuando se utili ce la clase) es T . Básicamente, lo que estamos diciendo es: "Estamos definiendo una pila Stack que almacena objetos de tipo TOO. La pila se implementa utili zando un objeto LinkedList, y también se define dic ho objeto LinkedList para que almacene el tipo T. Observe que push() (el método que introduce objetos en la pila) toma un objeto de tipo T , mientras que peek() y pop() devuel ven el objeto de tipo T . El método peek( ) devuelve el elemento superior de la pila sin eliminarlo, mientras que pop() extrae y devuelve dicho elemento superior. Si lo único que queremos es disponer del comportamiento de pila, el mecanismo de herencia resulta inapropiado, porque generaría una clase que incluiría el resto de los métodos de LinkedList (en el Capítulo 17, Análisis derallado de los contenedores, podrá ver que los diseñadores de Java 1.0 cometi eron este error al crear java.utiI.Stack).
11
He aquí una se ncilla demos trac ión de esta nueva clase
Almacenamiento de objetos 257
St~lck :
1/: holding/StackTest.java import net.mindview.util.*¡ public class StackTest { public static void main(String[] args)
Stack stack
=
{
new Stack()
for(String s
"My dog has fleas".split(" stack.push(s) ;
i lO»
while(!stack . empty() ) System.out.print(stack.pop()
+
"
") i
/ * Output : fleas has dog My
, /// ,Si quiere utilizar esta clase Stack en su propio código, tendrá que especificar completamente el paquete (o ca mbiar el nOI11bre de la clase) cuando cree una pila: en caso contrario, probablemente entre en colisión con la clase Stack del paquete java.lItil. Por ejemplo, si importamos java.util.* en e l ejemplo anterior. deberemos usar los nombres de los paquetes para evitar las colisiones.
11: holding/StackCollision . java import net.mindview . util.*¡ public class StackCollision public static void main (St ring [] args) ( net . mindview.util.Stack stack = new net.mindview.util.Stack() i for(String s : "My dog has fleas".split(" lO»~ stack.push(s) i while(!stack.empty() ) System.out . print(stack . pop() + " lO ) ¡ System . out . println() i java .util.Stack stack2 new java.util.Stack() i for(String s "My dog has fleas".split(II ti » stack2.push(s) ¡ while(!stack2.empty() ) System.out.print(stack2.pop() + " lO) i
1* Output: fleas has dog My f leas has dog My ' / / / >
Las dos clases Stack tienen la misma interfaz. pero no existe ninguna interfaz común Stack en java.util, probable mente porque la clase original java.utiI.Stack. que es taba di señada de una fonna inadecuada, ya tenía ocu pado el nombre. Aunque java.utilStack existe, LinkedList permite obtener una clase Stack mejor, por lo que resulta preferible la técnica basada en net.mindview.utiI.Stack. Tamb ién podemos controlar la selección de la implementación Stack Hpreferida" utili zando una instmcción de importación explícita: import net.mindview.util.Stack¡
Ahora cualquier referenci a a Stack hará que se selecc ione la versión de net.mindview.util , mientras que para seleccionar java.util.Stack es necesario emplear una cualificación completa. Ejercicio 15:
(4) Las pilas se utilizan a menudo para evaluar expresiones en lenguajes de programación. Utili zando net.mindview.utiI.Stack, evalúe la siguiente expresión, donde '+' significa "introducir la letra siguiente en la pila" mientras que '-' significa " extraer la parte superior de la fila e imprimirlo": "+U+n+c---+e+r+t---+a-+i-+n+t+y---+ -+r+u--+I+e+s---"
258 Piensa en Java
Set Los objetos de tipo Set (conjuntos) no penniten almacenar más de una instancia de cada objeto. Si tratamos de añadir más de una instancia de un mismo objeto, Set impide la duplicación. El uso más común de Set cons iste en comprobar la pene~ llencia, para poder preguntar de una manera sencilla si un detenninado objeto se encuentra dentro de un conjunto. Debido a esto, la operación más importante de un conjunto suele ser la de búsqueda, así que resulta habinJaI seleccionar la imple~ mentación HashSet, que está optimizada para realizar búsquedas rápidamente. Set tiene la misma interfaz que Collection , por lo que no existe ninguna funcionalidad adicional, a diferencia de los dos tipos distintos de List. En lugar de ello, Set es exactamente un objeto ColleetioD, salvo porque tiene un comportamiento distinto (éste es un ejemplo ideal del uso de los mecanismos de herencia y de polimorfismo: penniten expresar diferentes comportamientos). Un objeto Set detemlina la pertenencia basándose en el "valor" de un objeto, lo cual constituye un tema relativamente complejo del que hablaremos en el Capítulo 17, Análisis detallado de los conrenedores. He aquí un ejemplo que utiliza un conjunto HashSet con objetos Integer:
11:
holding/SetOflnteger.java import java . util. *i
public class SetOflnteger public static void main{String [] argsJ { Random rand = new Random(47); Set intset = new HashSet(); for(int i = Di i < 10000; i++) intset.add(rand.nextlnt(30)) ; System.out.println(intset ) ;
1* Out put: [15, 8, 23, 16, 28, 20, 25, 10,
7, 5,
22, O]
9,
21,
6,
1,
29,
14, 24, 4,
19, 26,
11, 18,
3, 12,
27,
17, 2,
13,
,/ // ,En el ejemplo, se añaden diez mil números aleatorios entre O y 29 al conjunto, por lo que cabe imaginar que cada valor tendrá muchos duplicados. A pesar de ello, podemos ver que sólo aparece una instancia de cada valor en los resultados. Observará también que la salida no tiene ningún orden específico. Esto se debe a que HashSet utiliza el mecanismo de hash para ace lerar las operaciones; este mecanismo se analiza en detalle en el Capítulo 17, Análisis detallado de los contenedores. El orden mantenido por un conjunto HashSet es diferente del que se mantiene en un TreeSet o en un LinkedHashSet, ya que cada implementación almacena los elementos de forma distinta. TreeSet mantiene los elementos ordenados en una estructura de datos de tipo de árbol rojo-negro, mientras que HashSet utiliza una función de hasll. LinkedHashSet también emplea una función hash para acelerar las búsquedas, pero parece mantener los elementos en orden de inserción utilizando una lista enlazada. Si queremos que los resultados estén ordenados, una posible técnica consiste en utilizar un conjunto TreeSet en lugar de HasbSet:
11:
holding/SortedSetOflnteger.java import java.util. *;
public class SortedSetOflnteger public static void main(String[] args) { Random rand = new Random (47); SortedSet intset = new TreeSet() i for(int i = o; i < 10000; i++) intset.add(rand.nextlnt(30)} ; System.out.println(intset) ;
1* [O,
Output: 1, 2, 3, 4,
5,
16,
17,
20,
, /// ,-
18,
19,
6,
7,
21,
8, 22,
9, 23,
1 0, 24,
11, 12, 13, 14, 15, 25,
26,
27,
28,
29]
11 Almacenamiento de objetos 259
Una de las operaciones más comunes que tendremos que realizar es comprobar la pertenencia al conjunto de un determinado miembro usando contains( ). pero hay otras operaciones que nos recuerdan a los diagramas Venn que enseñan en el colegio:
1/ : holdingfSetOperations.java import java.util. * ; import static net.mindview.util.Print.*
public class SetOperations { public stacic void main(String(]
args)
{
Set setl = new HashSec () ; Collections.addAll(setl, ti
A BCD E F G H 1 J
K L". spl i t ("
11)) i
setl . add ( "M" ) i
print ("H: " + setl. contains (" H" ) ) ; print ("N: It + setl. contains (UN") ) ; Set set2 = new HashSet(); Collections.addAll(set2, "H 1 J K L".split { 1I "¡)i
print{"set2 in setl: " + setl.containsAll {set2 »; setl. remove ( "H" ) ; print("setl: " + setl ) ; print ( "set2 in setl: " + setl.containsAll(set2»; setl.removeAll{set2) ; print(lIset2 removed fram setl: " + setl); eallections.addAll(setl, "X y Z".split(" 11»; print("'X Y Z' added to setl: " + setl); / * Output : H: true N: false set2 in setl: true seU,
ID,
K,
e, B, L, G, 1, M, A, F, J, El
set2 in setl: false set2 removed from setl: [D, C, a, G, M, A, F, El 'X y Z' added to setl: [Z, D, C, a, G, M, A, F, Y, X, El
*1110Los nombres de los métodos resultan bastante descriptivos, y existen unos cuantos métodos adicionales que podrá encon-
trar en el JDK. Generar ulla lista de elementos diferentes puede resultar muy útil en detenninadas ocasiones. Por ejemplo, suponga que quisiera enumerar todas las palabras contenidas en el archivo SetOperations.java anterior. Con la utilidad net.mindview. TextFile que presentaremos más adelante en el libro, podremos abrir un archivo y almacenar su contenido en un objeto Set:
11: holding/UniqueWords.java import java.util .*; import net.mindview.util.*; public class OniqueWords { public static void main (String [] args) { Set words = new TreeSet( new TextFile ("SetOperations.java" , " \\ w+" »; System.out.println(wordsl i
1* Output: (A, S, e, eollections, D, E, F, G, H, HashSet, I, J, K, L, M, N, Output, Print, Set, SetOperations, String, X, Y, Z, add, addAII, added, args, class, contains, containsAll, false, frem, holding, impert, in, java, main, mindview, net, new, print, public, remove, removeAll , removed, set!, set2, split, static, to, true, util, voidl * 111 ,-
260 Piensa en Java
TextFile hereda de List. El constnlctor de TextFile abre el arch ivo y lo descompone en palabras de acuerdo Con la expresión regular "\\ W+", que significa " una o más letras" (las expresiones regulares se presentan en el Capítulo !J. Cadenas de caracteres). El resultado se entrega al constructor de TreeSet. que aii.ade el contenido del objelO List al conjun. lo. Puesto que se lIata de un objeto TreeSet. el resultado está ordenado. En este caso, la reordenación se realiza /exicogra. .ficomente de modo que las letras mayúsculas y minúsculas se encuentran en gnlpos separados. Si desea reali zar una ordenación alfa bética, puede pasar el comparador Slring,CASE_INSENS ITIVE_ORDER (un comparador es un objeto que establece un orden) al constructor TreeSet:
/1 : holding / UniqueWordsAlphabetic . java 11 Generación de un listado alf a bético. import java . util .* ; import net . mindview.util .* ; public class UniqueWordsAlphabe tic public static void main (String[) arg s ) Set word s = ne w Tr eeSet (String.CASE_I NSENS IT IVE_ORDER) ; words . addAll ( new Text File ( ISetOperations . java " , n\\ w+ ,,» i System . out . pr i n tl n( words) i
1* Output : (A, add, addAll, added, args, B, C, c l ass, Coll ections, conta i ns, containsAll, D, E, F, f alse, f rom , G, H, HashS e t, holdi ng, I , i mpor t , i n , J , java, K, L, M, ma i n, mi n d view, N, net, new, Output, Pr int, public, remove, removeAll, removed , Set, set1 , set2, SetOperations, split, static, String , to, true , util, void, X, Y, Z) * /// ,Los comparadores se anali zarán en el Capítulo 16, Mafl·;ces. Ejercicio 16:
(5) Cree un obj eto Sel con todas las vocales, Ut il izando el archivo UniqueWords,java, cuente y muestre el número de vocales en cada palabra de entrada, y muestre también el número total de vocales en el archi· va de entrada.
Map La posibilidad de estab lecer correspondencias entre unos objetos y otros puede ser enonnemente potente a la hora de resol· ver ciertos problemas de programación. Por ejemplo, consideremos un programa que pemlite examinar la aleator iedad de la clase Random. Idealmente. Random debería producir la distribución perfectamente aleatoria de números, pero para com· probar si es to es así debería generar numerosos números aleatorios y llevar la cuenta de cuáles caen dentro de cada uno de los rangos definidos. Un obj eto Map nos pennite resolver fác ilmente el problema ; en este caso, la clase será el número ge ne· rada por Random y el va lor será el número de veces que ese número ha aparecido:
1/ : holding / Statistics . java
11
Ejemplo simple de HashMap . import java.util . *;
public class Statistics public static void main (String [) argsl { Random rand = new Random (47); Map m = new HashMap( ) ; for(int i = O; i < 1 0000; i ++) { 11 Generar un numero ent r e O y 20: int r = rand.nextlnt(20); Integer freq = m. get(r ) ; m. pu t {r, f req == null ? 1 freq + 1); System . out . println (m) ;
11 Almacenamiento de objetos 261
/ * Output: {15=497, 4=481, 19=464, 8=468, 11=531, 16=533, 18=478, 3=508, 7=471, 2=489, 13=506, 9=549, 6=519, 1=502, 14=477, 10=513, 5=503, 0 =481)
12=521,
17=509,
' 111 ,En main(), la característi ca de oUloboxing convie rte e l va lor int aleatoriamente generado en una referencia a Integer qu e puede utilizarse con el mapa HashMap (no pueden utili zarse primiti vas en los contenedores). El método get() devuelve null si la clave no se encuentra ya en el contenedor (lo que quiere decir que es la primera vez que se ha encontrado ese número concreto) . En caso contrario, el método gct() devuelve el va lor Intcger asociado con esa clave. el cual tendremos que incrementar (de nuevo, la ca racterística de auroboxing simplifica la ex presión, pero en la práctica se lleva n a cabo las necesarias conversiones hacia y desde Integer). He aquí un ejemplo que nos pennite utilizar la descripción de String para buscar objelOs Peto También nos muestra cómo podemos comprobar si un determinado objelO Map contiene una c lave o un va lor utilizando los métodos containsKey( ) y containsValue( ):
11 ,
holding/PetMap.java import typeinfo.pets.·¡ import java.util.·; import static net.mindview.util.Print.*¡
public class PetMap { public static void main(String[] args) { Map petMap = new HashMap() petMap. put ("My Cat", new Cat ("Molly") ) i petMap. put ( "My Dog", new 009 ("Ginger " ) ) ; petMap.put(ltMy Hamster", new Hamster ( ltBosco" » ; print (petMap) i Pet dog = petMap.get {"My 009"); print (dog) ¡ print {petMap. containsKey ("My Dog"» i print(petMap.containsValue(dogJ) ;
i
/ * Output: {My Cat=Cat Molly, My Hamster=Hamster Bosco, My Dog=Oog Ginger} Dog Ginger true true
' 111 ,Los mapas, al igual que las matrices y las colecciones, pueden cxpa ndirse fácilmente para que sean multidimcnsionales: basta con definir un obj cto Map c uyos va lores sean también mapas (y los va lores de esos olros mapas pueden se r, a su vez, otros contenedores o inc luso otros mapas). Así, resulta bastante fácil combinar los contenedores para generar estmctliras de datos muy potentes. Por ejem plo, suponga que queremos llevar una lista de personas que tien en múltiples masco tas, en ese caso. lo único que necesitaremos es un objeto Map< Person, List< Pet»:
11 , holding / MapOfList.java package holding; import typeinfo.pets.*¡ import java.util. * ¡ import static net.mindview . util.Print. · ¡ public class MapOfList { public static Map, siga el ejemplo de Uni qu eWords.java para crear un programa que lleve la cuenta del número de apariciones de cada palabra en un archi vo. Ordene los resultados uti lizando Collecti ons.so rt() proporcionando como segundo argumento Strin g.CASE_INSENS ITI VE_ O RD ER (para obtener una ordenación alfabét ica), y muestre los resultados.
Ejercicio 22:
(5) Modifique el ejercicio anterior para que ut il ice una clase que contenga un campo de tipo Stri ng y un campo contador para almacenar cada una de las di ferentes palabras, así como un conjunto Set de estos objetos con el fin de mantener la lista de palabras.
Ejercicio 23 :
(4) Partiendo de Statistics.j a va, cree un programa que ejecute la prueba repetidamente y compru ebe si hay algún número que tienda a aparecer más que los otros en los resultados .
Ejercicio 24:
(2) Rellene un mapa LinkedH as hM a p con claves de tipo Strin g y objetos del tipo que prefiera. Ahora extraiga las parejas, ordénelas según las claves y vue lva a insertarlas en el mapa.
Ejercicio 25 :
(3) Cree un objeto M ap el contador asociado con dicha palabra; esto es, en la práctica, la ubicación dentro del archivo en la que encontró dicha palabra.
Ejercicio 26 :
(4) Tome el mapa resultan te del ejercicio anterior y ordene de nuevo las palabras, tal como aparecían en el archivo origina l.
Queue Una cola (queue) es normalmente un contenedor de tipo FIFO (jirst-in,first-out, el primero en entrar es el primero en sa lir). En otras palabras, lo que hacemos es insertar elementos por un o de los extremos y extraerlos por el otro, y el orden en que insertemos los e lementos coincidirá con el orden en que estos serán extraídos. Las colas se utilizan comúnmente como un mecanismo fiab le para transferir objetos desde un área de un programa a otro. Las colas son especialmente importantes en la programación concurrente, corno veremos en el Capítulo 21, Concurrencia, porque permüen transferir objetos con seguridad de una a otra ta rea.
LinkedList di spone de métodos para soportar el comportamiento de una cola e implementa la interfaz Q ueue, por lo que un objeto Li nkcd List puede utilizarse como implementación de Q ueu • . Generalizando un objeto LinkedList a Q ueue, este ejemplo uti liza los métodos específicos de gestión de colas de la interfaz Queue: 11: holding /QueueDemo .java
II Generalización de un objeto LinkedList a un objeto Queue . import java.util. * ¡ public class QueueDemo public static void printQ (Queue queue) while(queue . peek() != null) System.out.print(queue.remove() + System.out.println(} ¡
{ 11
11);
public static void main(String[] args) { Queue queue = new LinkedList(); Random rand = new Random(47); for(int i :: O; i < 10; i++) queue.offer(rand.nextlnt(i + ID}); printQ(queue) ; Queue qc = new LinkedList (); for (c har e : 11 Brontosaurus" . toCharArray () ) qc . offer(c) ; printQ (qc) ;
1* Output:
264 Piensa en Java 8 1 1 1 5 14 3 1 O 1 B ron t o s a u r u s
* ///,offe . . () es uno de los métodos específicos de Queue; este método inserta un elemento al final de la cola, siempre que sea posible. o bien devuehe el valor fa lse. Tanto peek() como element() devuelven la cabecera de la cola sin eliminarla, pero peek() devuel ve null si la cola está vacia y element() genera NoSuch Element Exccption . Tanto poll() como remOVe() eliminan y de vuelven la cabecera de la cola, pero poll() devue lve null si la cola está vacía, mientras que removc() genera NoS ueh Element Exce plion. La característica de allloboxil1g convierte automáticamente el resultado int de ne xtlnt() en el objeto Int ege r requerido por q ueue. y el va lor ehar e en el objeto C har ae ter requerido por q c. La interfaz Queue limita el acceso a los métodos de Li nkedList de modo que sólo están disponibles los métodos apropiados. con lo que estaremos menos tentados de utilizar los métodos de LinkedList (aqui, podríamos proyectar de nuevo qu eue para obtener un objeto LinkedList, pero al menos nos resultará bastante más complicado uti lizar esos métodos). Observe que los métodos específicos de Queue propo rcionan una funcionalidad completa y autónoma. Es decir, podemos disponer de una cola ut ilizable sin ninguno de los métodos que se encuentran en Collection . que es de donde se ha heredado. Ejercicio 27:
(2) Esc ri ba una clase denominada Command que contenga un objeto Strin g y que tenga un método oper ati on( ) que imprima la cadena de caracteres. Escriba una segunda clase con un método que rellene un objeto Queue con objetos Command y devuelva la cola rellena. Pase el objeto Queue relleno a un método de una tercera clase que consuma los objetos de la cola e invoque sus métodos oper ation ( ).
PriorityQueue El mecanismo FIFO (Firsl-ill. firsl-ou t) describe la disciplina de gestión de colas más común. La disciplina de gestión de colas es lo que decide. dado un grupo de elementos existentes en la cola. cuál es el que va a continuación. La disciplina HFO dice que el siguiente elemento es aquel que haya estado esperando durante más tiempo. Por el contrario. una cola COI1 prioridad implica que el elemento que va a continuación será aquel que tenga una necesidad mayor (la prioridad más alta). Por ejemplo, en un aeropuerto, puede que un cliente que está en medio de la cola pase a ser atendido inmed iatamente si su avión está a punto de salir. Si construimos un sistema de mensaje ría, algunos mensajes serán más importantes que otros y será necesario tratar esos mensajes antes, independientemente de cuándo hayan llegado. El contenedor Pr iori tyQ ueue ha sido ailadido en Java SES para proporcionar una implementación automática de este tipo de comportamiento. Cuando ofrecemos. como método offer( ). un objeto a una cola de tipo Priorit)'Q ueue, dicho objeto se ordena dentro de la cola. 5 El mecanismo de ordenación predetcnninado utili za el orden lIatural de los objetos de la cola, pero podemos modificar dicho elemento proporcionando nuestro propio objeto Compar ator. La clase Priori tyQ ueue garantiza que cuando se invoquen los métodos peek(), poll( ) o ,'emove(), el elemen to que se obtenga será aquél que tenga la prioridad más alta. Resulta trivial implementar una cola de tipo Priori tyQ ueue que funcione con tipos predefinidos como Illteger. Str ing o C ha r aeter. En el siguiente ejemplo, el primer conjunto de valores son los valores aleatorios del ejemplo anterior. con lo cual podemos ver cómo se los extrae de manera diferente de la cola PriorityQueue:
11 : holding/PriorityQueueDemo.java impore java.util.*; public class PriorityQueueDemo public static void main(String[] args) PriorityQueue. algoritmos de colas con prioridad suelcn realizar la ordenación durantc la inserción (mantC'ni~IllI\.'
una estnJctura de memoria quc se conoce con el nombre de cúmulo). pcro también puedc perfectamente seleccionarse el elemento más impol1ante C'n ~I momento de la extracción . La elección del algoritmo podría tener su importancia si la prioridad de los objeto:>. puede modificarse mientra s estos C:>.tán óperando en la cola.
11 Almacenamiento de objetos 265 i = o; i <:: 10; i++ ) priorityQueue , offer {rand.nextlnt ( i
for ( fnt
+
10 » ;
QueueDemo. printQ {priorityQueue } ; List ints = Arrays . asList (25, 22, 20 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, I
25) ;
priorityQueue = new PriorityQueue (ints ) ; QueueDemo. printQ (priorityQueue ) i priorityQueue
=
new PriorityQueue (
ints. s i ze (), Collections. reverseOrder () ) priorityQueue.addAll(intsJ i QueueDemo.printQ(priorityQueuel
i
i
String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION " i ListcString> serings = Arrays.asList {fact . split("" )} ; PriorityQueue stringPQ = new PriorityQueue (strings) ; QueueDemo .p rintQ(stringPQ) ; stringPQ = new PriorityQueue ( serings. si ze () , Collections . reverseOrder ( ) ) i seringPQ . addAll ( serings ) ; QueueDemo . prineQ(stringPQ) ;
Set charSet = new HashSet(); for (char e fact.toCharArray( » charSet.add ( c ) ; II Autoboxing PriorityQueue characterPQ new PriorityQue ue< Character> (charSet) i QueueDemo.printQ(characterPQ) i
1* Output : O 1 1 1 1 1 3 5 8 14 1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 A A B e e e D D E E E F H H 1 1 L N N o o o o s s S T T U U U W wU U U T T S S S o o o o N N L 1 1 H H F E E E D o e e e B A A A B e o E F H 1 L N o S T U W , /// ,Como puede ver, se permiten los duplicados. los valores menores tienen la prioridad más alta (en el caso de String. los espacios también cuentan como va lores y tienen una prioridad superi or a la de las letras). Para ver cómo podemos verificar la ordenac ión propo rcio nando nuestro propio objeto compa rador, la tercera llamada al co nS(nlclor de PriorityQueue y la segunda llamada a PriorítyQueue utilizan el comparador de orden inverso generado por Collectiol1s,revcrseOrder( ) (añadido en Ja va SE5). La última sección aiiadc un conjunto HashSet para el iminar los objetos Character duplicados, simplemente con el fin de hacer las cosas un poco más interesantes. Integer. String y Charader funcionan con PriorityQuclIc porque estas clases ya tienen un orden natural predefinido. Si quere mos utilizar nuestra propia clase en una cola PriorityQueue. deberemos incluir una funcionalidad adicional para generar una ordenación natural. o bien proporcionar nuestro objeto Comparator. En el Capíhl lo 17. Análisis de/allado de los cOllfenedores se proporciona un ejemplo más sofi sticado en el que se ilustra este mecani smo. Ejercicio 28 :
(2) Rellene la cola PriorityQueue (ut ili za ndo offer()) con va lores de tipo Double creados utilizando java.utiI.Random. y luego elimine los elementos con poll( ) y visualícelos.
Ejercicio 29:
(2) Cree una clase simple que herede de Object y que no contenga ningún nombre y demuestre qu e se pueden aiiadir múltiples elementos de di cha clase a una cola PriorityQuclIe. Este tema se explicará en detalle en el Capintlo 17. Análisis detallado de los contenedores.
266 Piensa en Java
Comparación entre Collection e Iterator CoUection es la interfaz raíz que describe las cosas que ti enen en común todos los co ntenedores de secuencias. Podríamos considerarla como una especie de " interfaz incidental", que surgió debido a la existencia de aspectos comunes entre las otras interfaces. Además, la clase java.util.AbstractCollection proporciona una implementación predeterminada de Collection, para poder crear un nuevo subtipo de AbstractCollection sin duplicar innecesariamente el código. Un argumento en favor de disponer de una interfaz es que ésta nos permite crear código genérico. Escribiendo como una interfaz en luga r de como una implementación, nuestro código puede aplicarse a más tipos de objetos. 6 Por tanto, si escri. bimos un método que admita un método Collection, dicho método podrá aplicarse a cualquier tipo que implemente CoUection, y esto permite implementar Collection en cualquier clase nueva para poderla usar con el método que hayamos escrito. Resulta interesante resaltar, sin embargo, que la bibl ioteca estándar e++ no dispone de ninguna clase base común para sus contenedores: los aspectos comunes entre los contenedores se consiguen utili za ndo iteradores. En Java, podría pare· cer adecuado seguir la técnica utilizada en e++ y expresar los aspectos comunes de los contenedores utilizando un iterador en lugar de una colección. Sin embargo, ambos enfoques están entrelazados, ya que implementar Collection también impli. ca que deberemos proporcionar un método iterator() :
11 : holding/lnterfaceVs l terator . java import typeinfo.pets.*; import java .util.*; public class InterfaceVslterator public static void display(Iterator it) while(it.hasNext())
{
(
Pet p = it.next(); System . out.print (p .id () +
11.11
+ p + 11
11 );
System.out.println() ; public static void display(Collection pets) for(Pet p : pets) System.out.print(p.id() + 11. 11 + P + 11 " )i System.out.println() ; public static void main (String [] args) { List petList = Pets.arrayList(B); Set petSet = new HashSet (petList ) ; Map petMap = ne w LinkedHashMap{); String [] names = ("Ralph, Eric, Robín, Lacey, "Britney, Sam, Spot, Fluffy " ) .split( " , 11 ); for(int i = O; i < names.length; i++) petMap.put (names [i], petList.get (i)); display{petList) ; display(petSet) ; display(petList.iterator()) ; display(petSet . iterator()) i System.out.println (petMap) ; System.out.println(petMap.keySet()) ; display(petMap.values()) ; display(petMap . values() .iterat or()) i
ti
+
1* Output: O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 4:Pug 6:Pug 3:Mutt l:Manx 5:Cymric 7:Manx 2:Cymríc O:Rat fi Algunas personas defienden la creación automática de una interfaz para toda posible combi nación de métodos en una clase: en ocasiones, defienden que se haga esto para lodas las clases. En mi opinión. una interfaz debería tener un sign ificado mayor que una mera duplicación mecánica de combi naciones de métodos, por lo que suelo preferir esperar y ver qué valor añadiría una interfaz antes de crearla.
11 Almacenamiento de objetos 267 O:Rat l:Manx 2:Cymric 3 : Mutt 4:Pug 5:Cymric 6 : Pug 7:Manx 4:PU9 6:Pug 3:Mutt l:Manx 5:Cymric 7:Manx 2:Cymric O:Rat {Ralph~Rat,
Eric=Manx, Robin=Cymric, Lacey=Mutt, Britney=Pug, Sam=Cymric, Spot=Pug,
Fluffy=Manx} [Ralph, Eric, Robin, Lacey, Britney, Sam, Spot, Fluffyl O:Rat l: Manx 2:Cymric 3:Mutt 4:Pug S:Cymric 6:Pug 7 :Manx O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug 5 : Cymric 6:Pug 7:Manx
- /// ,Ambas versiones de display() funcionan con objetos Map y con subtipos de Collection, y tanto la interfaz Collection como lterator penniten desacoplar los métodos display(). sin forzarles a conocer ningún detalle acerca de la implementación concreta del contenedor subyacente. En este caso, los dos enfoques se combinan bien. De hecho, Colleetion lleva el concepto un paso más allá porque es de tipo Iterable, Y por tanto, en la implementación de display(Collection) podemos usar la estmcturajóreac/¡, lo que hace que el código sea algo más limpio. El uso de Iterator resul ta muy recomendable cuando se implementa una clase externa, es decir, una que no sea de tipo Collection, ya que en ese caso resultaría dificil o incómodo hacer que esa clase implementara la interfaz Collection. Por ejemplo. si creamos una implementación de Collection heredando de una clase que almacene objetos Pet, deberemos implementar todos los métodos de Collection, incluso aunque no necesitemos utilizarlos dentro de l método display( ). Aunque esto puede llevarse a cabo fácilmente heredando de AbstractCollection . estaremos forzados a implementar iterator( ) de todas fomlas,j unto con size(), para proporcionar los métodos que no están implementados en AbstractCollcction. pero son uti lizados por los otros métodos de AbstraetColleetion: jI : holdingfCollectionSequence.java import typeinfo.pets.*; import java.util.*;
public class ColleccionSequence extends AbstractCollection private Pet(] pets = Pets . createArray(B); public int size( ) { return pets.length; } public Iterator iterator () { return new Iterator () { private int index = o; public boolean hasNext ( ) return index < pets.length¡ public Pet next () { return pets [index++J i public void remove () { // No implementado throw new UnsupportedOperationException (} }
i
}; pu blic stati c void main (String (] args ) { Col lectionSequence e = new CollectionSequence ( ); In t erfaceVslterator.display (c ) i InterfaceVslterator.display(c.iterator( )) ;
1* Output: O:Rat l:Manx 2:Cymric 3:Mutt 4:Pug S : Cymric 6:Pug 7:Manx O:Rat l : Manx 2:Cymric 3 : Mutt 4 : Pug S:Cymric 6:Pug 7 : Manx - /// , El método remo\'c() es una "operación opcional", de la que hablaremos más delante en el Capítulo 17, Análisis detallado de los conrenedores. Aquí, no es necesa rio implementarlo y, si lo invocamos, generará una excepción. En este eje mplo, podemos ver que si implementamos Collection. también implementamos iterator(); además, vemos que implementar únicamente iterator() sólo requiere un esfuerzo ligeramente menor que heredar de AbstractCollection . Sin embargo, si nuestra clase ya hereda de otra clase, no podemos heredar de AbstractCollcction. En tal caso, para implemen-
268 Piensa en Java
tar Collection sería necesario implementar todos los métodos de la inte rfaz. En este caso. resultaría mucho más senci llo heredar y añadir la posibilidad de crear un ¡terador:
11 : ho lding / NonCollectionSequenee.java import typeinfo.pets.*¡ import j ava.util.*; class PetSequence protected Pet(] pets
Pets.createArray (8 ) ¡
publie class NonCollectionSequenee extends PetSequence { public Iterator iterator () { return new Iterator () { private int index = O; public boolean hasNext ( ) return index < pets . length¡ publ ic Pet next () { return pets [index++] ¡ publ ic void remove () { / I No implementado throw new UnsupportedOperationException()
i
};
publie static void main (String[) args) { NonCollectionSequenee ne = new NonCollectionSequence () InterfaceVslterator.display(nc.iterator {) ;
i
1* Output: O:Rat l:Manx 2:Cymric 3 :Mutt 4:Pug 5:Cymric 6 : Pug 7:Manx */ // ,Generar un objeto Iterator es la forma con acop lamiento más débil para conectar una secuencia con un método que consuma esa secuencia; además, se imponen muchas menos restricciones a la clase correspondi ente a la sec uencia que si implementamos Collectio n. Ejercicio 30:
(5) Modifique CollectionSeq uence.j.va para que no herede de AbstractCollection , sino que implemente Collection.
La estructura foreach y los iteradores Hasta ahora, hemos utili zado principalmente la si ntaxis foreach con las matrices, pero dicha simaxis también fUllciona con cua lquier objeto de tipo ColJection . Hemos visto algunos ejemplos en los que empleaba ArrayList, pero he aquí una demostración general:
/ 1 : holding / ForEachCollections.java II All eollections work with foreach. import java.util.*; public elass ForEachCollections publie static void main{String() args ) { Collection es = new LinkedList {) ¡ Collections.addAll {es, "Take the long way home" . split {" " ) i for (String s : es) System. out. print (" '" + s + '" ,,) i / * Output: ' Take' 'the' ' long'
* /// ,-
'way'
'home '
11 Almacenamiento de objetos 269 Como es es una colección, este código dem uestra que todos los objetos Collectioll permiten emplear la estmcturajoreach. La razón de que este método funcione es que en Java SES se ha introducido una nueva interfaz denominada Iterable que contiene un método iterator( ) para generar un objeto Iterator. y la interfaz Iterable es 10 que la cstrucrura foreach utilí · za para desp lazarse a través de una secuencia. Por tanto, si creamos cualquier clase que implemente Iterable, dicha clase podrá se r utilizada en un a instmcciónJoreach: // : holding / lterableClass. j ava /1 Anything Iterable works with foreach. i mport java.util.*; public class IterableClass implements Iterable pro tected String [] words = ( "And that is how " + "we know the Earth to be banana-shaped . " ) . spIit ( 11 " ) ¡ pubIic Iterator i terator () { return new Iterator () { private int index = O; public boolean hasNext ( ) { return index < words.length¡ public String next () { return words [index++ 1 ¡ public void remove () { / / No implementado throw new UnsupportedOperationException{) ; }
}; public static void main (String[] args ) for {String s : new IterabIeClass (») System. out. print (s + 11 " ) ¡ / * Output: And that is how we know the Earth to be banana-shaped.
* /// ,El método iterator() devuelve una instancia de una implementación interna anónima de Iterator que dev uelve cada palabra de la matri z. En main(), podemos ver que lterableClass funciona perfectamente en una instrucciónforeach. En Java SES, hay varias clases que se han definido co mo Iterable, principalment e todas las clases de tipo Collection (pero no las de tipo Map ). Por ejemplo, este códi go muestra todas las variables del entorno del sistema operativo: // : holding / EnvironmentVariables. j ava i mport java.util.*¡ public class EnvironmentVariables public static vo id main (String[] args ) for {Map.Entry entry: System.getenv () .entrySet {» System. out. printIn (entry. getKey () + ": 11 + entry.getVaIue ()} ¡
/ * (Execute to see output ) */// :-
System.getenv()' devuelve un objeto Map, entrySet() produce un conjunto de elementos Map.Entry y un conjunto (Set) de tipo Iterable. por lo que se le puede usar en un bucleforeach. La instrucción.loreach fun ciona con una matri z o cualquier cosa de tipo Iterable, pero esto no quiere decir que una matri z sea automáti camente de tipo Iterable, ni tampoco que se produzca ningún tipo de meca nismo de autoboxing: 7 Esto no estaba disponible antes de Java SE5. porque se pensaba que estaba acoplado de una manera demasiado estrecha con el sistema operativo, lo que violaba la regla dc "cscribir los programas una vez y ejecutarlos en cualquier lugac'·. El hecho de que sc haya incluido ahora sugiere que los diseñadores de Java han dccidido ser más pragmáticos.
270 Piensa en Java
11: holding/ArrayIsNot I terable . java import java.util. * ; publie elass ArrayIsNotIterabl e statie void test (Iterable ib) for(T t , i b) System . out.print(t + 11 " ) ;
{
public static void main (String [] args) { test (Arrays .asList(l, 2, 3)); String [] strings = { "A", " B II , "e" }; II Una matri z f un ciona eon f o rea c h, pe ro no e s de tipo Iterab le : I I ! test (strings) i II Ha y que convert ir l a exp líci t a me nte al t ipo Iterable: test{ Arrays . as List(st r i ng s )) i
1* Out put : 1 2 3 A B e * /// , Al tratar de pasar una matriz como argumento de tipo Iter able el programa falla. No hay ningún tipo de conversión automática a Iterable; sino que debe realizarse de forma manual. (3) Modifique polyrnorphism/shapelRa ndomShapeGenerator.java para hacerlo de tipo Iterable. Tendrá que añadir un constructor que admita el número de elementos que queremos que el iterador genere antes de pararse. Verifique que el programa funciona.
Ejercicio 3\:
El método basado en adaptadores ¿Qué sucede si tenemos una clase existente que sea de tipo Iterable, y queremos añadir una o más formas nuevas de utilizar esta clase en una instrucción foreach? Por ejemplo, suponga que queremos poder decidir si hay que iterar a través de una lista de palabras en dirección directa o inversa. Si nos limitamos a heredar de la clase y sustituimos el método iterator(), estaremos sustituyendo el método existente y no dispondremos de la capacidad de opción. Una solución es utili zar lo que yo denomino Método basado en adaptadores. Cuando se dispone de una interfaz y nos hace falta otra, podemos resolver el problema escribiendo un adaptador. Aqui, lo que queremos es añadir la capacidad de generar un iterador inverso, pero sin perder el iterador directo predetenninado. así que no podemos limitarnos a sustituir el método. En su lugar, lo que hacemos es añadir un método que genere un objeto Iterable que pueda entonces utilizarse en la instrucción fo reach. Como puede ver en el ejemplo siguiente. esto nos pennite proporcionar múltiples fonnas de usar joreac/¡ :
11 : holding / AdapterMethodIdiom.java II El método basado en adap tadores permite uti l izar II foreaeh con tipos adiciona l es de objetos i terables . import java.util .* ; elass ReversibleArrayList extends ArrayList { public ReversibleArrayList (Collection e) { super (e) i public Iterable reve rsed () { return new It e rable () { publie Iterator iterator () return new Iterator () { int eurrent = size () - 1; public boolean hasNext () { return cur rent > - 1 ; public T next() { return get(eu rrent- - ); } public void remove ( ) { liNo impl e ment a do throw new UnsupportedOperationExce p tion () ; )
};
}
11 Almacenamie nto de objetos 271 ) );
public class AdapterMethodldiom { public static void main(String[] args) ReversibleArrayList ral = new ReversibleArrayList( Arrays.asList(OITo be or not ta be".split(" ")) // Toma el iterador normal vía iterator(): ter (S tring s : ral) System.out.print (s + 11 " ) i System.out.println () ; JI Entregar el objeto Iterable de nuestra elección for{String s : ral.reversed() ) System.out.print (s + 11 " ) ;
) i
; * Output: To be or not ta be be ta not or be To
* /1/ ,,i nos limitamos a poner el objeto ral dentro de la instmcción/oreac/¡, obtenemos el iterador directo (predeterminado). Pero i invocamos reverscd() sobre el objeto, el comportamiento será distinto. Jtiliza ndo esta técnica, podemos añadir dos métodos adaptadores al ejemplo lterableClass.java : /1: holding / MultilterableClass.java /1 Adición de varios métodos adaptadores. import java.util. *¡ public class MultilterableClass extends IterableClass ( public Iterable reversed () ( return new Iterable () ( public Iterator iterator () return new Iterator () ( int current = words.length - 1; public boolean hasNext () { return current > -1; } public String next () { return words (current- -] ¡ } public void remove () { // No implementado throw new UnsupportedOperationException() ¡ ) };
) }; public Iterable randomized () ( return new Iterable () ( public Iterator iterator () List shuffled = new ArrayList (Arrays.asList (words »; Collections.shuffle(shuffled, new Random (47 » ¡ return shuffled.iterator () ¡ )
); public static void main (String(] args ) { MultilterableClass mic = new MultilterableClass( ) ; for(String s : mic.reversed( » Systern.out.print (s + ¡ 11
"
)
272 Piensa en Java System.out.println () i for (String s mic.randomized (» System.out.print (s + 11 11 ) i System . out.println () i f o r (String s : mie l System.out.print (s + 11 11 ) i / * Output: banana-shaped. be to Earth the know we how lS that And is banana-shaped. Earth that how the be And we know to And that lS how we know the Earth to be banana-shaped.
* /// ,Observe qu e el segundo método, random(). no crea su propio Iterator sino que se limita a devolver el correspondiente a la lista List mezclada . Puede ver a la salida que el método Collcctions.shuffle() no afecta a la matri z original, sino que sólo mezcla las referencias en shuffled. Esto es así porque el método randomized() empaqueta en el nue vo objeto Arra)'List el res ultado de Arrays.asList(). Si mezcláramos directamente la lista generada por Arrays.asList() se modificaría la matri z subyacente. como puede ver a continuación :
/1 : holding / ModifyingArraysAsList.java import java.util .* ¡ publie elass ModifyingArraysAsList publie stat i c void main(Stri ng[] a r gs) Random rand = n ew Random(47) i Integer[)
ia = { 1,
2,
3,
4,
5,
6,
7,
8,
9,
10 };
List listl = new ArrayList (Arrays . asList (ia»; System.out.println ( IIBefore shuf f ling: 11 + listl ) i Colleetio ns.shuffle(listl, rand ) i System.out.println("After shuff ling : " + listl) ¡ System. out . pri n tln ( " array : + Ar rays . toS t ring(i a »; List list2 = Arrays . asList{ia ) i System.out.println {"Before shuffling: " + list2 ) i Colleetions.shuffle (list2, rand ) i System.out.println ( "After shuffling: " + list2 ) i System . out. println ("array: " + Arrays. toString (ia ) ) ¡
1* Output: Before shuffling: (1, 2, 3, 4, 5, 6, 7, 8, 9, la] After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9) array: [1, 2 , 3, 4, 5, 6, 7, 8, 9, la] Before shuffling: (1, 2, 3, 4, 5, 6, 7, 8, 9, la] After shuffling: (9, 1, 6, 3 , 7, 2, 5, ID, 4, 8] array'
[9,
1,
6,
3,
7,
2,
5,
10,
4,
8)
* /// ,En el primer caso, la salida de Arrays,asList( j se entrega al constructor de ArrayList( j , y esto crea una lista ArrayList que hace referencia a los elementos de ia. Mezclar estas referencias no modifica la matri z. Sin embargo, si uti li zamos directamente el resultado de Arrays.asList(ia), el mezclado moditica el orden de ia. Es importante tener en clIenta que Arrays.asLis t() genera un objeto List que utili za la matri z subyacente corno su implememación fisica. Si hacemos algo a ese objeto List que lo modifique y no queremos que la matri z original se vea afectada, entonces será necesario reali zar una copia en otro contenedor. Ejercicio 32:
(2) Siguiendo el ejemplo de MultilterableClass, a"ada métodos reversed() y randomized() a NonCollcctionSequencc,java. Haga también que No nCollectionSequence implemente Iterable y muestre que las distin tas técnicas funcionan en las instnlcciones foreach.
11 Almacenamiento de objetos 273
Resumen Java proporciona varias fannas de almacenar objetos: 1. Las matrices asocian índices numéricos con los objetos. Almacenan objetos de un tipo conoc ido, así que no es necesario proyectar el resultado sobre ningún otro tipo a la hora de buscar un objeto. Pueden ser multidimcnsionales y también almacenar tipos primitivos. Sin embargo, su tamaiio no puede modificarse después de haberlas creado.
2. Las colecciones (Collection) almacenan elementos independientes, mientras que los mapas (Ma p ) almacenan parejas asoc iadas. Utilizando los genéricos de Java, especificamos el tipo de objeto que hay que almacenar en los contenedores, con el fin de no introduci r un tipo incorrecto en un contenedor y también para no tener que efectuar una proyección de los elementos en el momento de extraerlos de un contenedor. Tanto las colecciones como los mapas cambian automáticamente de tamaño a medida que añadimos más elementos. Los contenedores no permiten almacenar tipos primilivos, pero el mecanjsmo de autoboxillg se encarga de producir las primitivas a los tipos envoltorio almacenados en el contenedor. 3. Al igual que una matriz, una lista (List) también asocia índices numéricos con objetos; por tanto, las matrices y las listas son contenedores ordenados. 4. Ut ilice una lista ArrayList si tiene que realizar numerosos accesos aleato rios; pero, si lo que va a hacer es un gra n número de inserciones y de borrados en mitad de la lista, utili ce LinkedList. S. El comportamiento de las colas (Queue) y de las pilas se obtiene mediante LinkedList. 6. Un mapa (M ap ) es una fonna de asocia r los objetos no con valores enteros. sino con olros objelos. Los mapas HashMap están diseñados para un acceso rápido, mientras que un mapa TreeMap mantiene sus claves ordenadas y no es tan rápido como HashMap. Un mapa LinkedHashMap mantiene sus elementos en orden de inserción, pero proporciona un acceso rápido con mecanismos de ¡lCIS/¡. 7. Los conjuntos (S el) sólo acep lan objetos no duplicados. Los conjuntos Hash.Sel proporcionan las búsquedas más rápidas. miemras que TreeSet mantiene los elementos en orden. LinkedHashSet mantiene los elementos en orden de inserción. 8. No hay necesidad de utilizar las clases antiguas Vector, Hashtable y Stack en los nuevos programas. Resulta útil examinar un diagrama simplificado de los contenedores Java (sin las clases abstractas ni los componentes antiguos). En este diagrama se incluyen únicamente las interfaces y clases que podemos enCOlHrar de fonna habitual.
r-------.
r-------.
.-------.
: Jterator :-. ------------: Collection :-. ------------: ~-------..
Genera
~-- ¡¡ ---..
, r-----,..----,
Genera
Gene,.
"- ~ -"
:
,____ L _____ ,
:Li;t¡-t~;at~r:.-- ------: L~st: : S~t : :Q~;u~:
" _______ "
Map
'---Lf---"
.- ~-" "- ~-- "
I Tree~ap I
. .
,..------------1 ,..--- ,-----1I • 1,' ~~~~
,
•
,
r-~----,
PriorityQueue
I Has~~~g i-~:e~Set I : Comparable :......... : Comparator :
.----------"
~----------.
r LinkedHashSet I
Utilidades Collections Arrays
Taxonomía simple de los contenedores Como puede ver, sólo hay realmente cuatro componentes contenedores básicos: Map, List, Set y Queue, y sólo dos o tres implementaciones de cada uno (las implementaciones de java.util.concurrent para Queue no están incluidas en este diagrama). Los contenedores que más habinlalmente se utili zan son los que tienen líneas gmesas de color negro a su alrededor.
274 Piensa en Java
Los recuadros punteados representan interfaces, mientras que los recuadros de línea continua son clases normales (concre. tas). Las líneas de puntos con flecha s huecas indican que una clase concreta está implementando una interfaz. Las flechas rellenas muestran que una clase puede generar objetos de la clase a la que apunta la flecha. Por ejemplo, cualquier objeto Collection puede generar un objeto Iterator, y un objeto List puede generar un objeto Li,lIterator (además de un objeto Iterator nonma!. ya que List hereda de Collection). He aquí un ejemplo que muestra la diferencia en métodos entre las distintas clases. El código concreto se ha tomado del Capítulo 15. Genéricos; simplemente lo incluimos aquí para poder generar la correspondiente salida. La salida también muestra las interfaces que se implementan en cada clase o interfaz. jj: holdingjContainerMethods.java import net.rnindview.util.*; public class ContainerMethods public static void main{String[] args) { ContainerMethodDifferences.rnain{args} i j * Output: (Sample) Collection: {add, addAII, clear, contains, containsAII, equals, hashCode, isEmpty, iterator, remove, removeAII, retainAII, size, toArray] Interfaces in Collection: [Iterable] Set extends Collection , adds: [] Interfaces in Set: [Collectionl HashSe t extends Set, adds: [J Interfaces in HashSet: [Set, Cloneable, Serializablel LinkedHashSet extends HashSet, adds: [] Interfaces in LinkedHashSet: (Set, Cloneable, Serializablel TreeSet extends Set, adds: [pollLast, navigableHeadSet, descendingIterator, lower, headSet, ceiling, pollFirst, subSet, navigableTailSet, comparator, first, floor, last, navigableSubSet, higher, tailSet] Interfaces in TreeSet: [NavigableSet, Cloneable, SerializableJ List extends Collection, adds: [listlterator, indexOf, get, subList, set, lastIndexOf] Interfaces in List: (Collection] ArrayList extends List, adds: [ensureCapacity, trirnToSize] Interfaces in ArrayList: [List, RandomAccess, Cloneable, Serializable] LinkedList extends List, adds: [pollLast, offer, descendingIterator, addFirst, peekLast, removeFirst, peekFirst, removeLast, getLast, pollPirst, pop, polI, addLast, removeFirstOccurrence, getFirst, element, peek, offerLast, push, offerFirst, removeLastOccurrence] Interfaces in LinkedList: [List, Deque, Cloneable, Serializable] Queue extends Collection, adds: [offer, element, peek, polI] Interfaces in Queue: (Collectionl PriorityQueue extends Queue, adds: [comparator] Interfaces in PriorityQueue: [Serializablel Map: (clear, containsKey, containsValue, entrySet, equals, get, hashCode, isEmpty, keySet, put, putAII, remove, size, valuesl HashMap eXi:.ends Map, adds: [J Interfaces in HashMap: [Map, Cloneable, Serializable] LinkedHashMap extends Has~~ap, adds : [] Interfaces in LinkedHashMap: [Map] SortedMap extends Map, adds: [subMap, comparator, firstKey, last Key, headMap, tailMapJ Interfaces in SortedMap: [Map] TreeMap extends Map, adds: [descendingEntrySet, subMap, pollLastEntry, lastKey, floorEntry, lastEntry, lowerKey, navigableHeadMap, navigableTailMap, descendingKeySet, tailMap, ceilingEntry, higherKey, pollFirstEntry, comparator, firstKey, fl oor Key, higherEntry, firstEntry, navigableSubMap, headMap, lowerEntry, ceilingKey] Interfaces in TreeMap: [NavigableMap, Cloneable, Serializable]
. /// ,-
11 Almacenamiento de objetos 275 Como puede ver. todos los conjuntos, excepto TreeSet, tienen exactamente la misma interfaz que Collection. List y Collection difieren significativamente, aunque List requiere métodos que se encuentran en Collection . Por otro lado, los métodos de la interfaz Queue son autónomos; los métodos de Collection no son necesarios para crear una implementación de Queue funcional. Finalmente, la única intersección en tre Map y Collection es el hecho de que un mapa puede generar colecciones uti lizando los métodos entrySet() y values( ). Observe la interfaz de marcado java.util.RandomAccess, que está asociada a ArrayList perno no a LinkedList. Esta interfaz proporciona información para aquellos algoritmos que quieran modificar dinámicamente su comportamiento depend iendo del uso de un objeto List concreto. Es verdad que es ta organización parece un poco extraña en lo que respecta a las jerarquías orientadas a objetos. Sin embargo, a medida que conozca más detalles acerca de los contenedores en java.util (en particular, en el Capitulo 17, Análisis detallado de los contenedores), verá que existen otros problemas más importantes que esa estructura de herencia ligeramente extraña. Las bibliotecas de contenedores han constituido siempre un problema de diseño realmente dificil ; resol ver estos problemas implica tratar de satisfacer un conjunto de fuerzas qu e a menudo se oponen entre sí. Como consecuencia, debemos preparamos para llegar a cienos compromisos en algunos momentos. A pesar de estos problemas, los contenedores de Java son helTamientas fundamenta les que se pueden utilizar de fonna cotidiana para hacer nuestros programas más simples, más potentes y más efectivos. Puede que tardemos un poco en aCOSUlnlbramas a algunos aspectos de la biblioteca, pero lo más probable es que el lector comience rápidamente a adquirir y utilizar las clases que componen esta biblioteca. Puede encontrar las solucioncs a los ejercicios la venta en WII'lI'.Alindr'iew.net.
se leccionado~
en el documento electrónico rile Thinláng in JaI'tl AnnOfafed So/utioll Guide, disponible para
Tratamiento de errores • mediante excepciones La fi losofia básica de Java es que "el código erróneo no será ejecutado". El momento ideal de detectar un error es en tiempo de compilación, antes incluso de poder ejecutar e l programa. Sin embargo, no IOdos los errores pueden detectarse en tiempo de compilación. El resto de los problemas deberán ser gestionados en tiempo de ejecución, utilizando algún lipo de fanna lidad que pemlita que quien ha originado el error pase la infonnación apropiada a un receptor que sabrá cómo hacerse cargo de las dificultades ap ropiadamente. Una de las fannas más potentes de incrementar la robustez del código es disponer de un mecanismo avanzado de recuperación de errores. La recupe ración de errores es una de las preocupaciones principales de todos los programas que escribimos, pero resulta especialmente importante en Java. donde lino de los objetivos principales consiste en crear componentes de programas para que otros los utilicen. Para crear lIn s;sfema robusfo, cada componente f;ene que ser robusto. Al proporcionar un modelo coherente de informe de elTores utilizando excepciones, Java pennite que los componentes comuniquen los problemas de manera fiable al cód igo cliente. Los objetivos del tratamiento de excepciones en Java son simplificar la creación de programas fiables de gran envergadura utili zando menos código de lo habitual y llevar esto a cabo con una mayor confianza en que nuestra apl icación no va a encontrarse con errores no probados. El tema de las excepciones no resulta demasiado difici l de aprender y e l mecanismo de excepciones es una de esas características que proporcionan beneficios inmediatos y de gran importancia a cualquier pro-
yecto. Puesto que el tratamiento de excepciones es la única forma oficial en la que Java infonna acerca de los CITO res, y dado que dicho mecanismo de tratamiento de excepciones es impuesto por el compi lador Java, no son demasiados los ejemplos que podríamos escribir en este libro sin antes estudiar el tratamiento de excepciones. En este capítulo, se presenta el código que es necesario escribir para gestionar las excepciones adecuadamente, mostrándose también cómo podemos ge nerar nuestras propias excepciones si alguno de nuestros métodos se mete en problemas.
Conceptos El lenguaje e y otros le nguaj es anteriores di sponían a menudo de múltiples esquemas de tralamiento de elTores. y dichos esquemas se solían estab lecer por convenio y no como parte del lenguaje de programación. NOITna(mente, lo que se hacía era devolver un valor especial o co nfigurar una va riable ind icadora, y se suponía que el receptor debía examinar el valor o la variable y detennmar que algo iba mal. Sin embargo, a med ida que fuero n pasando los años. se descubrió que los programadores que uti lizaban una biblioteca tendían a pensar en sí mismos como si fueran inmunes al erro r; era casi como si dijeran: "Sí, puede que a otros se les presenten elTores, pero en mi código no hay errores". Por tanto, de f0IT118 bastante natura l, los programadores tendían a no comprobar las condiciones del elTo r (y además, en ocasiones, esas condiciones de error eran demasiado tontas como para comprobarlas ]). Si fuéramos tan exhaustivos como para comprobar si se ha producido un error cada vez que invocamos un método, el código se convertiría en una pesad illa ilegible. Puesto que los programadores
!
El programador en
e puede. por ejemplo. consultar el valor de retomo de printfO.
278 Piensa en Java
podían, a pesar de todo, construir sus sistemas con estos lenguajes. se resistían a admitir la realidad: que esta técnica de gestión de errores representaba una limitación importante a la hora de crear programas de gran envergadura que fueran robus_ tos y mantenibles. La solución consiste en eliminar la naturaleza casual del tratamiento de errores e imponer una cierta fonnalidad. Este modo de proceder tiene, en la práctica, una larga historia, porque las implementaciones de los mecanismos del tratamiento de excepciones se remontan a los sistemas operativos de la década de 1960, e incluso a la instmcci6n "on erro r goto" de BASIe. Pero el mecanismo de tratamiento de excepciones de C++ estaba basado en Ada, y el de Java se basa principalmen_ te en C++ (aunque se asemeja más al de Object Pascal). La palabra "excepción" hace referencia a algo que no tiene lugar de la forma acostumbrada. En el lugar donde aparece un problema, puede que no sepamos qué hacer con él, pero de lo que sí podemos estar seguros es de que no podemos conti. nuar como si no hubiera pasado nada; es necesario pararse y alguien, en algún lugar, tiene que saber cómo responder al error. Sin embargo. no disponemos de suficiente infonnación en el contexto actual como para poder corregir el problema, así que lo que hacemos es pasar dicho problema a otro contexto superior en el que haya alguie n cualificado para tomar la decisión adecuada. El otro beneficio importante derivado de las excepciones es que tienden a reducir la complejidad del código de gestión de errores. Sin las excepciones, es necesario comp robar si se ha producido un error concreto y resolverlo en múltiples lugares del programa. Sin embargo, con las excepciones ya no hace falta comprobar los errores en el punto donde se produce la llamada a un método, ya que la excepción garantiza que alguien detecte el error. Además, sólo hace falta tratar el problema en un único sit io, en lo que se denomina la rutina de Iratamiento de excepciones. Esto nos ahorra código y permite también separar el código que describe lo que queremos hacer durante la ejecución normal, de ese otro código que se ejecuta cuando las cosas van mal. En general, la lectura, la escritura y la depuración del código se bacen mucho más claras con las excepciones que cuando se utiliza la antigua fonna de tratar los errores.
Excepciones básicas Una condición excepcional es un problema que impide la continuación del método o ámbito actuales. Resulta importante distinguir las condiciones excepcionales de los problemas nonnales, en los que disponemos de la suficiente información dentro del contexto actual, como para poder resolver de alguna manera la dificultad con que nos hayamos encontrado. Con una condición excepcional, no podemos continuar con el procesamiento, porque no disponemos de la infonnación necesaria para tratar con el problema en el contexto actual. Lo único que podemos hacer es saltar fuera del contexto actual y pasar dicho problema a otro contexto de orden superior. Esto es lo que sucede cuando generamos una excepción. La división representa un ejemplo sencillo: si estamos a punto de dividir por cero, merece la pena comprobar dicha condición, pero ¿qué implica que el denominador sea cero? Puede que sepamos, en el contexto del problema que estemos intentando resolver en ese método concreto, cómo tratar con un denominador cero. Pero si se trata de un valor inesperado, no podremos tratar con él, y deberemos por tanto generar una excepción en lugar de continuar con la ruta de ejecución en la que estuviéramos. Cuando generamos una excepción suceden varias cosas. En primer lugar, se crea el objeto excepción de la misma fonna que cualquier otro objeto Java: en el cúmulo utilizando la instrucción new. A continuación. se detiene la ruta actual de ejecución (aquella que ya no podemos continuar) y se extrae del contexto actua l la referencia al objeto excepción. En este punto, el mecanismo de tratamiento de excepciones se hace cargo del problema y comienza a buscar un lugar apropiado donde con· tinuar ejecutando el programa. Dicho lugar apropiado es la rutina de Iratamiento de excepciones, cuya tarea consiste en recuperarse del problema de modo que el programa pueda intentar hacer otra cosa o simplemente continuar con lo que estuviera haciendo. Como ejemplo simple de generación de excepciones vamos a considerar una referencia a un objeto denominada t. Es posi· ble que nos pasen una referencia que no haya sido inicializada, así que podríamos tratar de comprobarlo antes de invocar un método en el que se utilice dicha referencia al objeto. Podemos enviar la infomlación acerca del error a otro contexto de orden superior, creando un objeto que represente dicha información y extrayéndolo del contexto actual. Este proceso se denomina generar una e.xcepción. He aquí el aspecto que tendría en el código: Hit :: null)
throw new NullPointerException();
12 Tratamiento de errores mediante excepciones 279
Esto genera la excepción, lo que nos pemlite, en el contexto actual, olvidarnos del problema: dicho problema será gestionado de manera transparente en algún otro lugar. En breve veremos dónde exactamente se gestiona la excepción. Las excepciones nos penniten pensar en cada cosa que hagamos como si se tratara de una transacción, encargándose las excepciones de proteger esas transacciones: " .. .la premisa fundamental de las transacciones es que hacía falta un mecanismo de tratamiento de excepciones en la informática distribuida. Las transacciones son el equivalente informático de los contratoS legales. Si algo va mal, detenemos todos los cálculos"2. También podemos pensar en las transacciones como si se tratara de un sistema integrado para deshacer acciones, porque (con un cierto cuidado) podemos establecer varios puntos de recuperación en cualquier programa. Si una parte del programa falla, la excepción se encarga de "deshacer" las operaciones basta un punto estable conocido dentro del programa. Uno de los aspectos más importantes de las excepciones es, que si sucede algo realmente grave, no penniten que el programa continúe con la ruta de ejecución ordinaria. Este tema ha constituido un grave problema en lenguajes como e y C++; especialmente en C, donde no había ninguna fonna de impedir que el programa continuara por una ruta de ejecución en caso de aparecer un problema, de modo que era posible ignorar los problemas durante un largo tiempo y acabar en un estado totalmente inadecuado. Las excepciones nos penniten (entre otras cosas) obligar al programa a detenerse y a infonnamos de qué es lo que ha pasado o, idealmente, obligar al programa a resolver el problema y volver a un estado estable.
Argumentos de las excepciones Al igual que sucede con cualquier objeto en Java, las excepciones siempre se crean en el cúmulo de memoria utilizando new, lo que asigna el correspondiente espacio de almacenamiento e invoca un constructor. Existen dos constructores en todas las excepciones estándar. El primero es el constructor predetenninado y el segundo toma un argumento de cadena de caracteres para poder aportar la información pertinente a la excepción: throw new NullPointerException(tlt
:::o
null ll )
i
Esta cadena de caracteres puede posteriormente extraerse utilizando varios métodos, como tendremos oportunidad de ver. La palabra clave throw produce una serie de resultados interesantes. Después de crear un objeto excepción mediante new, le proporcionamos la referencia resultante a throw. En la práctica, el objeto es "devuelto" desde el método, aun cuando dicho tipo de objeto no es normalmente lo que estaba diseñado que el método devolviera. Una fonna bastante simplificadora de considerar el mecanismo de tratamiento de excepciones es como si fuera un tipo distinto de mecanismo de retomo, aunque no debemos caer en la tentación de llevar esa analogía demasiado lejos, porque podríamos meternos en problemas. También podemos salir de ámbitos de ejecución ordinarios generando una excepción. En cualquiera de los casos, se devuelve un objeto excepción y se sale del método o ámbito donde la excepción se haya producido. Las similitudes con el proceso normal de devolución de resultados por parte de un método tenninan aquí, porque e/lugar a/ que se vuelve es completamente distinto de aquel al que volvemos en una llamada normal a Ull método (volvemos a una rutina apropiada de tratamiento de excepciones que puede estar alejada muchos niveles dentro de la pila del lugar en el que se ha generado la excepción). Además, podemos generar cualquier tipo de objeto Throwable, que es la clase raíz de las excepciones. Normalmente, lo que haremos será generar una clase de excepción distinta para cada tipo diferente de error. La información acerca del error está representada tanto dentro del objeto excepción como, implícitamente, en el nombre de la clase de excepción, por lo que alguien en el contexto de orden superior puede determinar qué es lo que hay que hacer con la excepción (a menudo, la única infonuación es el tipo de excepción, no almacenándose ninguna información significativa dentro del objeto excepción).
Detección de una excepción Para ver cómo se detecta una excepción, primero es necesario entender el concepto de región protegida. Se trata de una sección de código que puede generar excepciones que esté seguida por el código necesario para tratar dichas excepciones. 2 Jim Gray. ganador del premio Tunng Award por las contribuciones que su equipo ha realizado al tema de las transacciones. Las palabras eslán tomadas de una entrevista publicada en www.acmq/leue.org.
280 Piensa en Java
El bloque try Si nos encontram os dentro de un método y generamos una excepción (o si otro método al que invoquemos dentro de éste genera una excepción), dicho método temlinará al generarse la excepción. Si no queremos generar (con throw) la excep_ ción para sal ir del método. podemos definir un bloque especial denrro de dicho método para capturar la excepción. Este bloque se denomina Moque Iry (proba r) porque lo que hacemos en la práctica es "probar" dentro de ese bloque las diversas ll amadas a métodos. El bloque try es un ámbito de ejecución ordinari o precedido por la palabra clave try: try {
JI
Código que podría generar excepciones
Si qui siéramos comprobar cuidadosamente los errores en un lenguaje de programación que no tu vie ra mecanismos de tratamiento de excepciones, tendríamos que rodear todas las llamadas a métodos con cód igo de preparación y de comprobación de errores, incluso si in vocáramos el mismo método en varias ocasiones. Con e l tratamiento de excepciones incluimos todo dentro del bloque t ry y capturamos lOdas las excepciones en un único lugar. Esto significa que el código es mu cho más fácil de escribir y de leer, porque e l objetivo del código no se ve confundido con los mecanismos de comprobación de errores.
Rutinas de tratamiento de excepciones Por supuesto, la excepción generada debe acabar siendo tratada en algún lugar. Dicho " lugar" es la rutina de tratamiento de excepciones. existiendo una de dichas rutinas por cada tipo de excepción que queramos captu rar. Las rutinas de tratamienlO de excepciones es tán situadas inmediatamente a continuación del bloque try y se denotan mediante la palabra cla ve eateh : try {
II Código que podría generar excepciones catch(Tipol idl) { // Tratamiento de las excepciones de Tipol catch (Tipo2 id2) { // Tratamiento de las excepciones de Tipo2 catch (Tipo3 id3 ) { // Tratamiento de las excepciones de Tipo3
//
etc",
Cada clá usula catch (nuina de tratamiento de excepciones) es como un pequei'io método que toma un único argumento de un tipo concreto. El identifi cador (idl , id2 , etc.) puede utilizarse dentro de la mtina de tratamiento de excepciones exactamente igual que un argumento de un método. En ocasiones. nunca utilizamos e l identificador, porque el tipo de la excepción nos proporciona ya suficiente infomlación como para poder tratar con e ll a, pero a pesa r de todo el identificador debe estar preseme. Las rutinas de tratamiento de excepciones deben aparecer inmediatamente después del bloque try. Si se genera una excepción, el mecanismo de tratamiento de excepciones trata de buscar la primera mtina de tratamiento cuyo argumento se ajuste a l tipo de la excepción. A con tinuación, entra en la clá usul a ca tch, y la excepción se considera tratada. La búsqueda de rutinas de tratamiento se detiene en cuanto finalizada la cláusu la catch . Sólo se ejecutará la c láusula catch que se ajuste al tipo de la excepción; estas cláusula s no son como las instmcciones switch, en las que hace fa lta una instmcción break después de cada cláusula case para impedir que las restantes cláusul as se ejecuten. Observe que, dentro del bloque try, puede haber di stintas llamadas a metodos que generen la misma excepción, pero sólo hace falta una úni ca rutina de tratamiento.
Terminación y reanudación Existen dos modelos básicos en la teoría de tratamiento de excepciones. Java so pona el modelo denominado de termina-
ción ,3 en el que asumimos que e l error es tan crítico que no hay forma de volver a l lugar en e l que se generó la excepción. J
Como la mayoria de los lenguajes. incluyendo C+---. C#. Python, O, etc.
12 Tratamiento de errores mediante excepciones 281
Quienquiera que generara la excepción, decidió que 110 había ninguna manera de salvar la situación, por lo que no desea que volvamos a ese punto en el que la excepción fue generada. La alternativa se denomina reanudación. Esta alternativa implica que la mtina de tratamiento de excepciones debe hacer algo para rectificar la si tuación, después de 10 cual se vuelve a intentar ejecutar el método fallido , presumiendo que esa segunda vez tendrá éxito. Si queremos utili za r la técnica de reanudación. quiere decir que esperamos podemos con tinuar co n la ejecución después de que la excepción sea tratada. Si quiere disponer de un comportamiento de reanudación en Java, no genere una excepción cuando se encuentre con error. En lugar de ello. invoque un método que canija el problema. Alternativamente, coloque el bhJque tr)' dentro de un bucle while que continlle vo lviendo a entrar en el bloque t ry hasta que el resultado sea sarisfactorio. Históricamente, los programadores qu e utilizaban sistemas operativos donde se admitía el tratamiento de excepciones con reanudación terminaron uti lizando código basado en el mecanismo de tenllinación y evitando emplear la reanudación. Por tan to. aunque la reanudación pueda parecer atractiva a primera vista, no resulta demasiado útil en la práctica. La razón principal es. probablemente, el acop lamiento resultante: las mtinas de tratamiento con reanudación necesitan saber dónde se ha generado la excepción, y necesitan también contener código no genérico específico de cada ubicación donde la excepción se genere. Esto hace que el código sea difícil de escribir y de mantener. especialmente en sis temas grandes en los que las excepciones puedan generarse en muchos puntos di stintos.
Creación de nuestras propias excepciones No tenemos porqué limitamos a utili zar las excepciones Java existentes. La jerarquía de excepciones de Java no puede prever todos los errores de los que vayamos a quere r ¡nfonnar, así que podemos crear nuestros propios errores para indicar un problema especial con el que nuestra biblioteca pueda encontrarse. Para crear nuestra propia clase de excepción, debemos hereda r de una clase de excepción existente, preferiblemente de una cuyo significado esté próximo al de nuestra propia excepción (aunque a menudo esto no es posible). La fonl1a más tri vi al de crear un nuevo tipo de excepción consiste, simplemente, en permitir que el compilador cree el constructor predetenllinado por nOSO LTOS, por lo que no hace fa lta prácticamente ningún código: //: exceptions/InheritingExceptions.java // Creac ión de nuestras propias excepciones. class SimpleExceptian extends Exception {} public class InheritingExceptions { public void f() throws SimpleException System .out.println( "Throw SimpleException from f() " ) i throw new SimpleExcepti on(); public sta tic void main (String [] args) { InheritingExceptions sed = new InheritingExceptions() try {
i
sed. f (1;
catch(SimpleException e) System. out. println ( "Caught i t! ")
i
/ * Out put: Throw SimpleException fram f() Caught it! ' /// ,El compilador crea un constmctor predetemlinado, que de forma automática (e invisible) invoca al constructor predeterminado de la clase base. Por supuesto, en este caso no obtenemos un constmctor SimpleException(String), pero en la práctica dicho constructor 110 se usa demasiado. Como veremos, lo más importante acerca de una excepción es el nombre de la clase, por lo que la mayor parte de las veces ulla excepción como la que aquí se muestra se rá perfectamente adecuada.
282 Piensa en Java
Aqui, el resu ltado se imp rim e en la consola, donde se captura y se compru eba automáticamente utili za ndo el sistema de visualización de la sa lida de programas de este libro. Sin embargo. también podríam os enviar la infomlación de error a la salida de error estándar escribiendo en System. er r . Nomlalmentc, suele se r mejor envia r aquí la infonnación de error que a S~'stem.out. que puede estar redirigido. Si se envía la salida a System.er r, no sera redirigida junto con System.out. por lo que es más probable que el usuario vea la infonnación. También pode mos crea r una clase de excepción que ten ga un constructor con un argumento St ri ng:
JI : exceptions/FullConstructors.java class MyException extends Exception { public MyException () {} public MyException (String msg)
{ super (msg) ;
public cI ass FullConstructors ( public statie void f() throws MyException { System. out. println ( II Throwing MyException fram f () 11 ) ; throw new MyException() j
public statie void g() throws MyException ( System.out.println( IIThrowing MyException fram g{) " ); throw new MyException ( " Originat ed in 9 () 11 ) ;
public statie void main(String[) args ) try { í () ;
catch(MyException el ( e.printStackTrace(System.out)
i
} try { g(); catch (MyException el
{ e.printStackTrace{System . out) ;
/* Output: Throwing MyException fram f() MyException at FullConstl"uctors. f (FullConstructors . java: 11)
at FullConstructors.main(FullConstructors.java:19) Throwing MyException from g() MyExcepeion: Originated in g() ae FullConstructors.g(FullConstructors.java:15) at FullConstructors main(FullConstructors .java:2 4)
El código añadido es de pequeño tamano: do; eonstmetores qoe definen la fomla en que se crea MyException . En el segundo const ru ctor, se in voca explícilamente el constructor de la clase base con un argum ento String utilizando la palabra cla\e super. En las rutinas de tratamiento, se invoca uno de los metodos Throwable (de donde se hereda Ex«plio n): prinIStackTrace() . Como puede \'er a la salida. esto genera infonnación acerca de la secuencia de Inétodos que fueron ¡mocados hasta llegar al punt o en que se generó la excepción. Aquí, la información se envía a System.out. sie ndo automáticamente captu rada y mostrada a la salida . Sin embargo, si invocamos la vers ión predetenninada: e.printStackTrace() ;
la ¡nfonnación va a la salida de error es tándar.
12 Tratamiento de errores mediante excepciones 283
Ejercicio 1:
(2) Cree una clase con un método main() que genere un objeto de la clase Exception dentro de un bloque tr)'. Proporcione al co nstmctor de Exception un argumento String. Capture la excepción dentro de una cláusula catch e imprima el argumento String. Aii.ada ulla cláusula finaUy e imprima un mensaje para demostrar que pasó por alli.
Ejercicio 2:
(1) Defina una referencia a un objeto e inicialícela con nul!. Trate de invocar un método a través de esta referencia. Ahora rodee el código con una cláusula try-catch para capturar la excepción.
Ejercicio 3:
(1) Escriba código para generar y ca pnlfar Ulla excepción ArraylndexOutOffioundsException (indice de matri z fuera de límites).
Ejercicio 4:
(2) Cree su propia clase de excepción utilizando la palabra clave extends. Escriba un constructor para dicha clase que tome un argumento String y lo almacene dentro del objeto como una referencia de tipo String. Escriba un método que muestre la cadena de caracteres almacenada. Cree una cláusula try-catch para probar la nueva excepción.
Ejercicio 5:
(3) Defina un comportamiento de tipo reanudación utiliza ndo un bucle while que se repita hasta que se dej e de generar una excepción.
Excepciones y registro También podemos registrar la sa lida utili zando java.util.logging. Aunque los detalles comp letos acerca de los mecanismos de registro se presentan en el suplemento que puede encontrarse en la dirección htfp:/IMindView. netIBooksIBetterJava, los mecanismos de registro básicos son lo suficientemente sencillos como para poder utilizarlos aquí.
11 : exceptions / LoggingExceptions.java II Una excepción que proporciona la información a través de un registro . import java.util.logging .*; import java.io.*; class LoggingException extends Exception { private static Logger logger = Logger .getLogger("LoggingException U ) ; public LoggingException () ( StringWr iter trace = new StringWriter(); printStackTrace(new PrintWriter(trace»; logger . severe(trace.toString(» ;
public class LoggingExceptions { public static void main(String[] args) try { throw new LoggingException () i catch (LoggingException e ) { System.err.println ( "Caught " + el;
} try { throw new LoggingException(); catch (LoggingException el { System.err.println(UCaught " + el
i
/ * Output: (85% match) Aug 30, 2005 4:02:31 PM LoggingException SEVERE : Logging Ex ception at LoggingEx ceptions.main(LoggingExceptions.java :1 9 ) Caught LoggingException
284 Piensa e n Java Aug 30, 2005 4:02:31 PM LoggingException SEVERE: LoggingException at LoggingExceptions.main{LoggingExceptions.java:24) Caught LoggingException
*///,El método estático Logger.getLoggcr() crea un objeto Logger asociado con el argumento String (usualment e, el nombre del paqucle y la clase a la que se refieren los errores) que envía su salida a System.err. La forma más fácil de escribir en un objeto Logger consiste simple mente en invoca r el método asociado con el nivel de mensaj e de regi stro: aquí utilizamos severe(). Para producir el objeto String para el mensaje de registro. convendría di sponer de la traza de la pila correspon· diente al lugar donde se generó la excepción, pero printStackTrace() no genera un objeto String de fonna predetennina_ da. Para obtener un objeto String, necesitamos usar e l método sobrecargado printStackTrace() que toma un objeto java.io.PrintWriter como argumento (todo esto se ex plicará con detalle en el Capítulo 18, E/S). Si entregamos al construc· tor de Print\Vriter un objeto java.io.StringWritcr, la salida puede extraerse como un objeto String invocando toString(). Aunque la técnica utili zada por LoggingException resulta muy cómoda. porque integra toda la infraestructura de registro dentro de la propia excepción, y funciona por tamo automáticamente sin intervención del programador de clientes, lo más común es que nos veamos en la situación de capturar y regi strar la excepción generada por algún otro. por lo que es nece· sa rio generar el mensaje de regi stro en la propia mtina de tratamiento de excepciones:
11: exceptions/LoggingExceptions2.java /1 Registro de l as excepciones capturadas . import java .util.logging.*¡ import java.io. *¡ public class LoggingExceptions2 private static Logger logger = Logger.getLogger(ULoggingExceptions2") ; static void logException (Except i on e) { StringWriter trace = new StringWriter()¡ e.printStackTrace{new PrintWriter(trace)); logger.severe(trace.toString()) ; public static void main(String(] args) try { throw new NullPointerException(); catch (NullPointerException e) { logException(e) ;
1* Output: (90% match) Aug 30, 2005 4:07:54 PM LoggingExceptions2 logException SEVERE: java.lang .NullPointerException at LoggingExceptions2.main{LoggingExceptions2.java:16)
* ///,El proceso de creación de nuestras propias excepciones puede llevarse un paso más allá. Podemos ai'ladir constructores y mi embros adi cionales:
11:
exceptions/ExtraFeatures.java Mejora de las clases de excepción. import static net.mindview.util.Print.*;
JI
class MyException2 extends Exception { private int x¡ public MyException2 () {) public MyException2 (String msg) { super (msg) ¡ public MyException2 {String msg, int x l { super (msg) ;
12 Tratamiento de errores mediante excepciones 285 this.x =
Xi
public int val () { return Xi public String getMessage () { return "Detail Message: "+
X
+ "
"+ super.getMessage();
public class ExtraFeacures { public static void f(} throws MyException2 print ("Throwi ng MyException2 from f () ,,) i throw new MyException2() i public static void g() throws MyException2 { print ("Throwing MyException2 from 9 () ") i chrow new MyExcepcion2 ("Originated in 9 () ") public static void h{) throws MyException2 { print ("Throwing MyException2 from h () ") i throw new MyException2 ( "Originated in h (l ", public static void main (String [] try {
args)
i
4 7) ;
{
f () ;
catch (MyException2 e) { e.printStackTrace(System.out)
i
try g(); catch (MyException2 el { e.printStackTrace(System.out)
i
} try { h(); catch (MyException2 el { e.printStackTrace(System.out) i System.out.println{ue.valO = " + e.val(»
i
/* Output: Throwing MyException2 from f() MyException2: Detail Message: O null at ExtraFeatures.f(ExtraFeatures.java:22) at ExtraFeatures.main(ExtraFeatures.java:34) Throwing MyException2 from g() MyException2 : Detail Message: O Originated in g() at ExtraFeatures.g(ExtraFeatures.java:26l at ExtraFeatures.main(ExtraFeatures.java:39) Throwing MyException2 from h() MyException2: Detail Message: 47 Originated in h() at ExtraFeatures.h(ExtraFeatures.java:30l at ExtraFeatures.main(ExtraFeatures.java:44) e. val () = 47
*///,Se ha añadido un campo x junto con un mélOdo qu e lee di cho va lor y un constructor adicional qu e lo inicializa. Además, Throwable.getMessagc( ) ha sido sustituido para generar un mensaje de detalle más interesante. getMcssage() es un método simi lar a toString() que se utiliza para las clases de excepción.
286 Piensa en Java
Puesto que una excepción no es más que otro tipo de objeto, podemos continuar este proceso de mejora de nuestras clases de ex tensión. Recuerde, sin emba rgo. que todo este trabajo adic ional puede no servir para nada en los programas de c lien~ te que utili cen nuestros paquetes, ya que dichos programas puede que simplemente miren cuál es la excepción que se ha generado y nada más (ésa es la forma en la que se utili zan la mayoría de las excepciones de la biblioteca Java).
Ejercicio 6:
( 1) Cree dos clases de excepción, cada una de las cuales realice su propia tarea de registro te. Demuestre que dichas clases funcionan.
Ejercicio 7:
(1) Modifique el Ejercicio 3 para que la cláusula ealeh registre los resultados.
automáticamen~
La especificación de la excepción En Java, debemos tratar de informar al programador de clientes, que invoque nuestros métodos, acerca de las excepciones que puedan ser generadas por cada método. Esto resulta conveni ente porque quien realiza la in vocación puede saber así exactamente qué código necesita escribir para caphlrar todas las excepciones potenciales. Por supu esto, si el código fuente está disponible, el programador de clientes podría examinarlo y buscar las instrucciones throw, pero puede qu e una biblioteca se di stribuya sin el correspondiente código fuente. Para evitar que esto constituya un problema, Java proporciona una si ntaxis (y nos obliga a usar esa sintaxis) para permitirnos informar al programado r de clientes acerca de cuál es son las excepciones que el método genera, de modo que el programador pueda tratarlas. Se trata de la especificación de excepciones que forma parte de la declaración del método y que aparece después de la lista de argumentos. La especificación de excepciones utili za una palabra clave adicional, throws, seguida de todos los tipos potenciales de excepciones. Por tanto, la definición de un método podría tener el aspecto siguiente: void f()
throws TooBig. TooSmall, DivZero { j j ...
Sin embargo, si decimos void f ()
{ 11
significa que el método no genera ninguna excepción (salvo por las excepciones heredadas de RuntimeException , qu e pue ~ den generarse en cualquier lugar sin necesidad de que exista una especi ficación de excepciones; hablaremos de estas excep~ ciones más adelante) . No podemos proporcionar información falsa acerca de la especificación de excepciones. Si el código dentro del método provoca excepciones y ese método no las trata, el compilador lo detectará y nos infonnará de que debemos tratar la excepción o indicar, mediante una especificación de excepción, que dicha excepción puede ser devuelta por el método. Al imponer que se utilicen especificaciones de excepción en todos los lugares, Java garanti za en tiempo de compilación que exista una cierta corrección en la definición de las excepciones. Sólo hay una manera de que podamos proporcionar infonnación falsa: podemos declarar que generamos una excepción que en realidad no vayamos a genera r. El compilador aceptará lo que le digamos y forzará a los usuarios de nuestro método a tratar esa excepción como si rea lmente fuera a ser generada. La única ventaja de hacer esto es di sponer por adelantado de una forma de añadi r posteriormente la excepción; si más adelante decidimos comenzar a generar esa excepción, no tendremos que real izar cambios en el código existente. También resulta importante esta característi ca para crear interfaces y cIases bases abstractas cuyas clases derivadas o implementaciones puedan necesitar generar excepciones. Las excepciones que se comprueban y se imponen en tiempo de compilación se denominan excepciones comprobadas.
Ejercicio 8:
(1) Escriba una clase con un método que genere una excepción del tipo creado en el Ejercicio 4. Trate de compilarlo si n incluir una especificación de excepción para ver qué es lo que di ce el compilador. Añada la especificación de exce pción apropiada. Pruebe la clase y su correspondiente excepción dentro de una cláusula try-ealeh .
Cómo capturar una excepción Resulta posible crear una rutina de tratamiento que capture cualquier tipo de excepción. Para hace r esto, podemos capturar el tipo de excepción de la clase base Exception (existen otros tipo de excepciones base, pero Exception es la base que resu lta apropiada para casi todas las acti vidades de programación):
12 Tratamiento de errores mediante excepciones 287 c atch ( Exception e ) ( System.out.println ( "Caught an exception");
Esto pennitirá capturar cualquier excepción, por lo que si usamos este mecanismo convendrá ponerlo al final de la lista de rutinas de tratamiento, con el fin de evitar que se impida entrar en acción a otras rutinas de tratamiento de excepciones que puedan estar situadas a continuación. puesto que la clase Exception es la base de todas las clases de excepc ión que tienen importancia para el programador, no vamos a obtener demasiada infonnación específica acerca de la excepción concreta que hayamos capturado, pero podemos invocar los métodos incluidos en su lipo base Throwable: String getMessage() String gctLocalizedMessage()
Obtiene el mensaje de detalle o un mensaje ajustado para la configuración de idioma uti lizada. String toString() Devuelve una descripción corta del objeto Throwable, incluyendo el mensaje de detalle, si es que existe uno. voi d printStackTrace( ) void printStackTracc(PrintStream) void pri ntStackTraceGava.io.PrintWriter) Imprime el obje to Throwable y la traza de pila de llamadas de dicho objeto. La pila de llamadas muestra la sec uencia de llamadas a métodos que nos han conducido hasta el lugar donde la excepción fue generada. La primera versión imprime en la salida de error es tánda r, mientras que la segu nda y la tercera im primen en cualquier salida que elijamos (en e l Capitulo 18, E/S, veremos por qué ex isten dos tipos de !lujos de salida). Th rowa ble filllnStackTrace()
Registra infomlación dentro de este objeto Throwable acerca del estado actual de los marcos de la pila. Resulta útil cuando una aplicación está volviendo a generar un error o una excepción (hablaremos de esto en breve). Además, también tenemos acceso a otros métodos del tipo base de Throwablc que es Object (que es e l tipo base de todos los objetos). El que más útil puede resultar para las excepciones es getClass(), que devuelve un objeto que representa la clase de este objeto. A su vez, podemos consultar este objeto Class para obtene r su nombre mediante getNarnc(), que incluye infonnación o getSimpleName(), que sólo devuelve el nombre de la clase. He aqui un ejemplo que muestra el uso de los métodos básicos de Exception : // : exceptions / ExceptionMethods.java /1 Ilustración de los métodos de Exception. import static net.mindview.util.Print.*; public class ExceptionMethods { public static void main {String[]
args )
{
try { throw new Exception ( "My Excepti o n" ) ; catch (Exception e) { print ("Caught Exception"); print ( "getMessage ( ) :" + e. getMessage ( ) ) ; print ( "getLocalizedMessage () : 11 + e.getLocalizedMessage( )) ; print ("toString () :" + e ) ; print ( "printStackTrace () : " ) ; e.printStackTrace(System.out) ;
1* Output: Caught Exception g etMessage() :My Exception getLocalizedMessage () : My Exception toString() :java.lang.Exception: My Exception
288 Piensa en Java printStackTrace() : java . lang.Exception: My Exception at ExceptionMethods.main(ExceptionMethods.java:8)
Podemos ve r que los métodos proporcionan sucesivamente más infomlación. de hecho, cada uno de ellos es un supercon_ junto del anterior.
Ejercicio 9:
(2) Cree tres nuevos tipos de excepciones. Escriba una clase co n un método que genera las tres. En main(), llame al método pero utilice una única clá usula catch que penllita capturar los tres tipos de excep_ ciones.
la traza de la pila También puede accederse directam ente a la información proporcionada por printStackTrace() , utilizando getStackTrace() . Este método devuelve una matriz de elementos de traza de la pila, cada un o de los cuales representa un marco de pila. El elemento cero es la parte superior de la pila y se corresponde con la última in vocac ión de método de la secuencia (el punto en que este objeto Throwable fue generado). El último elemento de la matriz (la parte inferior de la pila) es la primera invocación de método de la secuencia. Este progra ma proporciona una ilustración simple: // : exceptions/WhoCalled.java // Acceso mediante programa a la información de traza de la pila. public class WhoCalled { static void f () { /1 Generar una excepción para rellenar la traza de pila. try { throw new Exception(); catch (Exception e) { for(StackTraceElement ste : e.getStackTrace()) System . out.println{ste.getMethodName{)) ;
static void g() { fl); ) static void h () { g (); ) public static void main (String [] args) { f(); System. out .println ( " - - -- - - - - - --- - -- - - - - -- - - - - - - - - - - -" ) ; g(); System.out.println("--------------------------------") ; h();
/ * Output: f
main f g
main
Aquí, nos limitamos a Impnm ir el nombre del método, pero también podríamos imprimir el objeto completo StackTraceElement , que contiene información adic iona l.
12 Tratamiento de errores mediante excepciones 289
Regeneración de una excepción En ocas iones. con viene regenerar una ex cepción que hayamos capturado, particularmente cuando se utiliza Exception para captu rar todas las excepciones. Puesto que ya di spo nemos de la re ferencia a la excepción actual , podemos simplemente volver a ge nerar dicha re ferencia : cacch(Exception e) { System . out . println(IIAn exception was thrown lt ) ; throw e; La regeneración de una excepción hace que ésta pase a las rutinas de tratamiento de excepciones del siguiente co ntexto de ni ve l superior. Las c láusulas catch adicional es inc luidas en el mismo bloque try seguirán ignorándose. Además, se preserva toda la infonnación acerca del objeto de excepción, por lo que la nnina de tratamiento en el contexto de ni vel superior que capnlre ese tipo excepción específico podrá extraer toda la información de dicho objeto. Si nos limitamos a regenerar la excepción actual. la información que imprimamos acerca de dicha excepc ión en printStackTrace() será la relativa al origen de la excepción, no la relati va al lugar donde la hayamos regenerado. Si queremos insertar nueva infonnación de traza de la pila podemos hacerlo invocando fillInStackTrace(), que devuelve un objeto Throwable que habrá generado insertando la infonnación de pila actual en el antiguo objeto de excepción. He aquí un ejemplo: jj : exceptions/Ret h r owing . java
11 Ilustración de fil l InStackTra ce() pub l ic class Rethrowing { public s t atic voi d f{) t hrows Excep tion { System . out . println( lI ori ginating the e xception i n f() " ); throw new Exception ( " t h rown from f () " ) i public static void g() throws Exception { try { f II ; catch (Exc e ption e l { System . out . prin t ln (" Inside 9 () ,e . pri n tStackTrace () ") ; e . printStackTr ace(System.ou t ) ; throw e¡
public static void h() throws Exception { try { fll;
catch(Exception e) System. out. println (" Inside h () , e . printStackTrace () ") ; e . printStackTrace(System.out) ¡ throw (Ex cep ti on) e. f i ll I nStackTrace();
public static void ma i n (String [] argsl { try { gil; ca t ch(Ex ception e) Sys t em. out . pri n tln (IImain: printStackTrace () 11) e . printStackTrace(System . outl; try hll; catch(Exception el Sys t em.out . println(lImain : printStackTrace() 11) e . printStackTrace(System . out) ;
i
i
290 Piensa en Java
/ * Output: originating the exception in f () Inside g {) ,e.printStackTrace () thrown froID f () at Rethrowing.f {Rethrowing.java:7 ) at Rethrowing.g (Rethrowing.java:ll )
j ava.lang.Exception:
at Rethrowing.main(Rethrowing.java:29)
main: printStackTrace () java .1an9. Exception: thrown froID f () at Rethrowing.f (Rethrowing.java:7 ) at Rethrowing.g (Rethrowing . java:ll l at Rethrowing.main (Rethrowing. java:29 ) originating che exception in f () Inside h () ,e.printStackTrace () java.lang.Exception : thrown fr oro f () at Rethrowing.f (Rethrowing.java:7 ) at Rechrowing.h (Rethrowing.java:2 0) at Rethrowing.main(Rethrowing.java:35 ) maln: printStackTrace {) java.lang.Exception: thrown fram f () at Rethrowing.h(Rethrowing.java:24) at Rethrowing.main(Rethrowing.java:35 )
La línea donde se invoca filllnStackTrace() será el nu evo punto de origen de la excepción. También resulta posible volver a generar una excepción distinta de aquella que hayamos capturado. Si hacemos esto, obte· nemos un efecto similar a cuando usamos filllnStackTrace() : la infonnación acerca del lugar de origen de la excepción se pierde, y lo que nos queda es la información correspondiente a la nueva instrucción throw:
/1 : exceptions / RethrowNew.java /1 Regeneración de un objeto distinto de aquél que fue capturado. class OneException extends Exception { public OneException (String s ) { super (s ) i
c lass TwoException extends Exception { public TwoException(String s ) { super (s}
i
public class RethrowNew { public static void f( ) throws OneException { System. out. println ( ltoriginating the exception in f () " ) ; throw new OneException ( " thrown from f () " ) i public static void main(String [] args ) { try { try ( f () ;
catch (OneException e ) { System.out.println( "Caught in inner try, e. printStackTrace ( ) " ) ; e . printStackTrace(System.out) i throw new TwoException ( .. from inner try"); catch(TwoException e) System.out .println (
12 Tratamiento de errores mediante excepciones 291
"Caught in outer try, e.printStackTrace() "); e.printStackTrace(System.out l ;
1* Output: originating the exception in f() Caught in inner try, e.printStackTrace() OneException : thrown from f() at RethrowNew. f {RethrowNew.java: 15) at RethrowNew. main {RethrowNew. java: 20) Caught in outer try, e.printStackTrace() TwoException: fram inner try at RethrowNew. main {RethrowNew. java: 25) * /// ,La excepción final sólo sabe que pro viene del bloque try interno y no de f(). Nunca hay que preocuparse acerca de borrar la excepción anterior, ni ningw1a otra excepción. Se trata de objetos basados en el cúmulo de memoria que se crean con new, por lo que el depurador de memoria se encargará automáticamente de borrarlos.
Encadenamiento de excepciones A menudo, nos interesa capturar una excepción y generar otra, pero manteniendo la informac ión acerca de la excepción de origen; este procedimiento se denomina encadenamiento de excepciones. Antes de la aparición del JDK 1.4, los programadores tenían que escribir su propio código para preservar la información de excepción original, pero ahora todas las subclases de Throwable tienen la opción de admitir un objeto causa dentro de su constnlctor. Lo que se pretende es que la causa represente la excepción original, y al pasa rla lo que hacemos es mantener la traza de la pila corres pondiente al origen de la excepción, incluso aunque estemos generando una nueva excepción. Resulta interesante observar que las úni cas subclases de Throwable que proporcionan el argumento causa en el constnlCtor son las tres clases de excepción fundamental es Error (utilizada por la máquina virtual JVM para informar acerca de los errores del sistema), Exception y RuntimeException . Si queremos encadenar cualquier otro tipo de excepción, tenemos que hacerlo utilizando el método initCause( ), en lugar del constructor. He aquí un ejemplo que nos pennite añadir dinámicamente campos a un objeto DynamicFields en tiempo de ejecución:
11 : exceptions/DynamicFields.java II Una clase que añade dinámicamente campos a s í misma. II Ilustra el mecanismo de encadenamiento de excepciones. import static net.mindview.util.Print.*¡ class DynamicFieldsException extends Exception {} public class DynamicFields ( private Object [] [] fields¡ public DynamicFields(int initialSize) fields ::: new Object [initialSizel [2] ¡ for{int i ::: O; i < initialSize¡ i++) fields[i]
=
new Object[]
{ null, null };
public String toString () { StringBuilder result = new StringBui lder {); for(Object[] obj , fields) resul t . append (obj [O] ) ;
resul t . append ( ":
11 );
resul t. append (obj [1] ) ;
result.append ( U\n U) ; return result . toString() ¡
{
292 Piensa en Java
private int hasField (String id ) { far ( int i = O; i < fields.length; if (id.equa1s ( fie1ds[iJ [OJ ))
i++ )
return i· return -1; private int
getFieldNumber(String id ) throws NoSuchFieldException { int fieldNum = hasField {id ) i if ( fieldNum == - 1 ) throw new NoSuchFieldException () ; return fieldNuID; private int makeFie1d (String id) { far(int i = O; i < fields.length; i++l i f (fie1ds [iJ [OJ == null) { fie1ds [iJ [OJ = id; return i;
// No hay campos vacios. Añadir uno : Object[J[J tmp = new Object[fie1ds.1ength + 1J[2J; far {int i = O; i < fields.length; i++l tmp [iJ = fie1ds [iJ ; far(int i = fields.length; i < tmp.length; i ++ } tmp[iJ = new Object[J { null, null j;
fields = tmpi // Llamada recursiva con campos expandidos: return makeField (id ) ; public Object getField(String id) throws NoSuchFieldException return fields [getFieldNumber(id)] [1];
public Object setField (String id, Object value) throws DynamicFieldsException ( if(va1ue == nu11 ) {
J J La mayoría de las excepciones no tienen un constructor con "causa".
JI JI
En estos casos es necesario emplear initCause () , disponible en todas las subclase de Throwable. DynamicFieldsException dfe = new DynamicFieldsException( ) i dfe.initCause(new NullPointerException( )} ¡ throw dfe¡ int fieldNumber = hasField (id ) ¡ if (fieldNumber == -1 ) fieldNumber makeField ( id } i Object result = null¡ try {
result = getField(id) i IJ Obtener valor anterior catch (NoSuchFieldException e) { J J Utilizar constructor que admite la "causa": throw new RuntimeException(e) i fields [fieldNumber] [1] = value¡ return result¡ public static void main(String[] argsJ
12 Tratamiento de errores mediante excepciones 293
DynamicFields df print (df ) ;
new DynamicFields (3 ) ;
try {
df.setField {"d", "A value ter d" ) i df.setField ( nnumber", 47 ) ; df. setField ( "number2 " , 48 ) i print (df ) ;
df . setField ( "d", "A new value fer d" ) ; df. setField ( "number3" , 11 ) ; print ( lIdf: " + df ) ; print ( "df.getField {\ "d\ U) u + df.getField ( "d" )) ; Object field = df.setField ( "d", null ) ; II Excepción catch (NeSuchFieldException e l { e.printStackTrace (System . out ) i catch (DynamicFieldsException e ) e.printStackTrace (System.out) i
1* Output: null: null null: null nu ll: null d: A value for d number: 47 number2: 48 df: d: A new value for d number: 47 number2: 48 number3: 11 df.getField ( "d" l : A new value tor d DynamicFieldsException at DynamicFields.setField (DynamicFields.java:64 ) at DynamicFields.main (DynamicFields.java:94 l Ca used by: java.lang . NullPointerException at DynamicFields.setField (DynamicFields . java:66 ) ... 1 more
* /!/ , Cada objeto DynamicFiclds contiene una matriz de parejas Object-Object. El primer objeto es el identificador del campo (de tipo String), mientras que el segundo es el valor del campo, que puede ser de cualquier tipo, salvo una primiti va no envuelta en otro tipo de objeto. Cuando se crea el objeto, tratamos de adivinar cuántos campos vamos a necesitar. Cuando invocamos setField(), dicho método localiza el campo existente que tenga dicho nombre o crea un nue vo campo, colocando a continuación el valor en él. Si se queda sin espac io, se añade nuevo espacio creando una matriz de longitud igual a la anterior más uno y copiando en ella los antiguos elementos. Si tratamos de almacenar un valor null , se genera una excepción DynamicFieldsException creando una excepción y utili zando initCause() para insertar una excepción NullPointerException como la causa. Como valor de retomo, setField() también extrae el antiguo valor situado en dicbo campo utili zando getField( ), que podría generar la excepc ión NoSuchFieldException (no existe un campo con dicho nombre). Si el programador de clientes invoca getField(), entonces será responsable de trata r la excepción NoSuchFieldException, pero si esta excepción se genera dentro de setField(), se tratará de un error de programación, por lo que NoSuchFieldException se convierte en una excepción RuntirneException utili zando el constructor que admite un argwnento de causa. Como podrá obervar, toString() utiliza un objeto StringBullder para crear su resultado. Hablaremos más en detalle acerca de StringBuilder en el Capítulo 13, Cadenas de caracreres, pero en general conviene utilizar este tipo de objetos cada vez que estemos escribiendo un método toString() que implique la utili zación de bucles como es el caso aquí.
294 Piensa en Java
Ejercicio 10:
(2) Cree una clase con dos métodos. f( 1 y g( l. En g( l. genere una excepción de un nuevo tipo det,nido por el usuario. En f(). invoque a g(), capture su excepción y, en la cláusula catch. genere una excepción diferente (de un segundo tipo también definido por el usuario). Compruebe el código en main( ).
Ejercicio 11:
(1) Repita el ejercicio anterior, pero dentro de la cláusula catch , envuelva la excepción g() dentro de una
excepción RuntimeException.
Excepciones estándar de Java La clase Java Throwable describc todas las cosas que puedan generarse como una excepción. Existen dos tipos generales de objetos Throwable ("tipos de "= "que heredan de"). Error representa los errores de tiempo de compilación y del sistema de los que no tencmos que preocupamos de capturar (salvo en casos muy especiales). Exception es el tipo básico que puede generarse desde cualquiera de los métodos de la biblioteca estándar de Java. así como desde nuestros propios métodos y también cuando sc producen enores de ejecución. Por tanto. el tipo base que más interesa a los programadores de Ja\'a es usualmente Exception. La mejor fonna de obtener una panorámica de las excepciones consiste en examinar la documentación dcl JDK. Conviene hacer esto al menos una vez para tencr una idea de las distintas excepciones, aunque si lo hace se dará cuenta pronto de que no existen diferencias muy grandes entre una excepción y otra, salvo en lo que se refiere al nombre. Asimismo, el número de excepciones en Java continua creciendo; es por ello que resulta absurdo tratar de imprimirlas todas en un libro. Cualquier nueva biblioteca que obtengamos de un fabricante de software dispondrá probablemente, asimismo, de sus propias excepciones. Lo importante es comprender el concepto y qué es lo que debe hacerse con las excepciones. La idea básica es que el nombre de la excepción represente el problema que ha tenido lugar y que ese nombre de excepción trate de ser autoexplicativo. No todas las excepciones están definidas en java.lang; algunas se defmen para dar soporte a otras bibliotecas como util. Det e io, lo cual pueue deducirse a partir del nombre completo de la clase correspondiente o de la clase de la que heredan. Por ejemplo. todas las excepciones de E/S beredan de java.io.lOException.
Caso especial: RuntimeException El primer ejemplo de este capítulo era: i f (t == nulll throw new NullPointerException{) ;
Puede resultar un poco atenador pensar que debemos comprobar si todas las referencias que se pasan a un método son iguales a ouU (dado que no podemos saber de antemano si el que ha realizado la invocación nos ha pasado una referencia válida). Afortunadamente, no es necesario realizar esa comprobación manualmente; dicha comprobación forma parte del sistema de comprobación estándar en tiempo de ejecución que Java aplica automáticamente. y si se realiza cualquier llamada a una referencia null, Java generará automáticamente la excepción NullPointerException . Por tanto, el anterior fragmento de código resulta siempre superfluo, aunque si que puede resultar interesante realizar otras comprobaciones para protegerse frente a la aparición de una excepción NuLlPointerException. Existe un conjunto completo de tipos de excepción que cae dentro de esta categoría. Se trata de excepciones que siempre son generadas de fomla automática por Java y que no es necesario incluir en las especificaciones de excepciones. Afortunadamente, todas estas excepciones están agrupadas. dependiendo todas ellas de una única clase base denominada RuntimeException, que constituye un ejemplo perfecto de herencia: establece una familia de tipos que tienen detem1Ínadas características y comportamientos en común. Asimismo, nunca es necesario escribir una especificación de excepción que diga que un método puede generar RuntimeException (o cualquier tipo heredado de RuntimeException), porque se trata de excepciones no comprobadas. Puesto que estas excepciones indican errores de programación, nonnalmente no se suele capnlrar una excepción RuntimeException. sino que el sistema las trata automáticamente. Si nos viéramos obligados a comprobar la aparición de este tipo de excepciones, el código sería enormemente lioso. Pero. aunque nonnalmenre no vamos a capturar excepciones RuntimeException. sí que podemos generar este tipo de excepciones en nuestros propios paquetes. ¿Qué sucede cuando no capturamos estas excepciones? Puesto que el compilador no obliga a incluir especificaciones de excepción para estas excepciones, resulta bastante posible que lila excepción RuntimeException ascienda por toda la jerar-
12 Tratamiento de errores mediante excepciones 295
qu ía de métodos sin ser captllrada. hasta llegar al método maine ). Para ve r lo que sucede en este caso, trale de ejecutar el siguiente ejemplo:
11 : exceptions / NeverCaught . java II Lo que sucede al ignorar una excepción RuntimeException . 11 {ThrowsException} public class NeverCaught { static void f () { throw new RuntimeException( "From f () static void 9 ()
ti) i
{
f II ;
public static void main (String[] args ) { g il ;
Como puede ver, RuntimeException (o cualquier cosa que herede de ella) es un caso especial. ya que el compilador no requiere que incluyamos una especificación de excepción para estos tipos. La salida se envía a System .crr: Exception in thread "main" java .lang. RuntimeException: From f () at NeverCaught.f{NeverCaught.java:7) at NeverCaught.g(NeverCaught.java:lOl at NeverCaught.main(NeverCaught.java:13)
Por tanto, la respuesta es: si una excepción RuntimeException llega hasta main() sin ser capturada. se invoca printStackTracc() para dicha excepción en el momento de sa lir del programa. Recuerde que sólo las excepciones de tipo RuntimeException (y sus subclases) pueden ser ignoradas en nuestros programas, ya que el compilador obliga exhaustivamente a tratar todas las excepciones comprobadas. El razonamiento que explica esta fonna de actuar es que RuntimeException representa un error de programación, que es: 1. Un error que no podemos anticipar. Por ejemplo, una referencia oull que escapa a nuestro contTo!.
2. Un error que nosotros, como programadores, deberíamos haber comprobado en nuestro código (como por ejemplo una excepción ArraylndexOutOfBoundsExccption , que indica que deberíamos haber prestado atención al tamaño de una matriz). Una excepción que tiene lugar como consecuencia del punto l suele convertirse en un problema del tipo especificado en el punto 2. Como puede ver, resulta enonnemente beneficioso disponer de excepciones en este caso, ya que nos ayudan en el proceso de depuración . Es interesante observar que el mecanismo de tratamiento de excepciones de Java no tiene un único objetivo. Por supuesto, está di se ñado para tratar esos molestos errores de ejecución que tienen lugar debido a la acción de fuerzas que escapan al control de nuestro código, pero también resulta esencial para ciertos tipos de errores de programación que el compilador no puede detectar. Ejercicio 12:
(3) Modifique innerclasses/Sequence.java para que genere una excepción apropiada si tratamos de introducir demasiados elementos.
Realización de tareas de limpieza con finally A menudo. existe algún fragmento de código que nos gustaría ejecutar independientemente de si la excepción ha sido generada dentro de un bloque try. Usualmente, ese fragmento de código se relaciona con alguna operación distinta de la de recuperación de memoria (ya que esta operación es realizada automáticamente por el depurador de memoria). Para conseguir este efecto, utili zamos una cláusula finally4 después de todas las rutinas de tratamiento de excepciones. La estructura completa de una sección de tratamiento de excepciones se ría, por tanto: " El mecanismo de tratamiento de excepciones de cabo estas tarcas de limpieza.
e++ no dispone de
la cláusula finally, porque depende de la utilización de destructores para llevar a
296 Piensa en Java try {
// La región protegida: actividades peligrosas // que pueden generar A, B o e catch lA al) { 11 Rutina de tratamiento para la situación A catch (B b1) { 11 Rutina de tratamiento para la situación B catch (C el) { 11 Rutina de tratamiento para la situación e final l y { 11 Actividades que tienen lugar en todas las ocasiones
Para demostrar que la cláusula fin aLl y siempre se ejecuta, pruebe a ejecutar este programa: //: exceptions/FinallyWorks.java
II La cláusula finally siempre se ejecuta. class ThreeException extends Exception {} public class FinallyWorks { static int count = Di public static void main(String[] args) { while(true) try { /1 El post-incremento es cero la primera vez: if(count++ == Ql throw new ThreeException () i System.out.println(tlNo exception n ) ; catch (ThreeException e l { System. out .println ( "ThreeException ll )
;
finally {
System.out.println(nIn finally clause n ); if (count == 2) break; II fuera del bucle "while"
1* Output: ThreeException In finally clause No exception In finally clause
*111,Analizando la salida, podemos ver que la cláusula fi nall y se ejecuta se haya generado o no una excepción. Este programa también nos indica cómo podemos tratar con el hecho de que las excepciones en Java no nos penniten continuar con la ejecución a partir del punto donde se generó la excepción, como ya hemos indicado anteriormente. Si incluimos nuestro bloque try en un bucle, podremos establecer una condición que habrá que satisfacer antes de continuar con el programa. También podemos añadir un contador estático o algún otro tipo de elemento para pennitir que el bucle pruebe con varias técnicas diferentes antes de darse por vencido. De esta forma, podemos proporcionar un mayor nivel de robustez a nuestros programas.
¿Para qué sirve finally? En un lenguaje que no tenga depuración de memoria y que no tenga llamadas automáticas a destructores,5 la cláusula fin ally es importante porque pem1ite al programador garantizar que se libera la memoria, independientemente de lo que suceda en S Un destructor es una función que siempre se invoca cuando un objeto deja de ser utilizado. Siempre sabemos exactamente dónde y cuándo se invoca al destructor. C++ dispone dc llamadas automáticas a destnLctores, mientras que e#, que se parece más a Java, dispone de un mecanismo que hace po~ibl e que tenga lugar la destrucción automática .
12 Tratamiento de errores mediante excepciones 297
el bloq ue try. Pero Ja va dispone de un depurador de memoria, por lo que la liberación de memoria casi nunca es un problema. Asimismo, no dispone de ningún destructor al que in vocar. Por tanto, ¿cuándo es necesario utili zar finally en Java? La cláusula finally es necesaria cuando tenernos que restaurar a su estado original alguna afro cosa distinta de la propia memoria. Se trata de algún tipo de tarea de limpieza que se encargue, por ejemplo, de cerrar un archivo abierto o una conexión de red, de borrar algo que hayamos dibujado en la pantalla o incluso de accionar un conmutador en el mundo exterior, tal como se ilustra en el siguiente ejemplo:
/1 : exceptions/Switch.java import static net.mindview util . Print.*¡ public class Switch { private boolean state = false; public boolean read () { return state; } public void on () ( state = true¡ print (this ) ; public void off () ( state = false; print(this ) ; public String toString () { return state ? "on" : "off";
/// ,/1 : exceptions / OnOffExcept ionl.java public class OnOffExcept i on l extends Exception {} 11 1:1/ : exceptions/OnOffExcept ion2.java public class OnOffException2 extends Exception {} ///:-
JI: exceptions/OnOffSwitch . java // ¿Por qué usar finally? public class OnOffSwitch { private static Switch sw = new Switch () i public static void f () throws OnOffExceptionl,OnOffException2 {} public static void main (String [] args ) { try {
sw.on () j // CÓdigo que puede generar excepciones ..
f {) , sw.off l) ,
catch (OnOffExceptionl e l { System. out. println ( "OnOffExceptionl" l ; sw.offl) ,
catch(OnOffException2 e l { System . out. println ( "OnOffException2") ; sw.off {) ,
1* Output: on o ff
, /// , Nuestro objetivo es asegurarnos de que el conmutador esté cerrado cuando se complete la ejecución de main(), por lo que situamos sw.off() al final del bloque try y al final de cada rutina de tratamiento de excepciones. Pero es posible que se genere alguna excepción que no sea capturada aquí, en cuyo caso no se ejecutaría sw.off(). Sin embargo, con finally podemos incluir el código de limpieza del bloque try en un único lugar:
JI : exceptionsfWithFinally . java // Finally garantiza que se ejecuten las tareas de limpieza . public class WithFinally
298 Piensa en Java static Switch sw ~ new Switch(); public static void main (S tring [] args) { try { sw.on() i // Código que puede generar excepciones ... OnOffSwitch.f() ; catch(OnOffException1 el { System.out.println(nOnOffException1") ; catch(OnOffException2 e l { System.out .println("OnO ffEx ception2") ; finally { sw . off();
/ * Output:
Aquí, la llamada a sw.off( ) se ha desplazado incluyéndola en un único lugar, donde se garantiza que será reali zada indepen· dientemente de lo que suceda. Incluso en aquellos casos en qu e la excepción no es capturada en el conjunto actual de cláusulas catch, finally se ejecuta· ría antes de que el mecanismo de tratamiento de excepciones continúe buscando una rutina de tratamiento adecuada en el siguiente ni vel de orden superior: // : exceptions/AlwaysFinally.java // Finally siempre se ejecuta . import static net.mindview.util.Print. * ¡ class FourException extends Exception {} public class AlwaysFinally { public static void main(String[] args) print("Entering first try block " ); try { print ( "Entering second try block " ) ; try { throw new FourException()¡ finally { print{"finally in 2nd try block"); catch (FourException e) { System.out.println( " Caught FourException in 1st try block"); finally { System.out.println("finally in 1st try block");
/ * Output : Entering first try block Entering second try block finally in 2nd try block Caught FourException in 1st try block finally in 1st try block
* /// , -
La instrucción finally también se ejecutará en aquellas situaciones donde estén implicadas instrucciones break y continuc. Observe que la cláusula finally junto con las instrucciones break y continue etiquetadas elimina la necesidad de una ins~ trucción goto en Java.
12 Tratamiento de errores mediante excepciones 299
Ejercicio 13:
(2) Modifique el Ejercicio 9 a¡;adiendo una cláusula fin.U y. Verifique que la cláusula finally se ejecuta, incl uso cuando se genera una excepción NulLPointcrExce ption .
Ejercicio 14:
(2) Demuestre que OnOffSwitch.java puede fallar, generando una excepción RuntimeException dentro del bloque try.
Ejercicio 15:
(2) Demuestre que WithFinally,java no falla , generando una excepción RuntimeException dentro del bloque try.
Utilización de finally durante la ejecución de la instrucción return Puesto que ulla cláusula finall y siempre se ejecuta, resulta posible vo lver desde múltiples puntos dentro de un método sin dejar por e llo de garanti zar que se realicen las tareas de limpieza importantes :
JJ : e x c e ptions JMultipleReturns . java import s t atic net . mindvie w. util . Print .* ; public class MultipleRetu r ns { public static void f ( int i ) print ( " Initialization that requires cleanup " ) ; try { print ( " Point 1" ) ; i f {i == l ' return; print (" Poi n t 2 " ) i i f {i == 2) return ; print (" Point 3" ) i i f ( i == 3 ) return; print ( "End'" i return; finally { print { "Performing cleanup" } ;
public static void main (String [) for ( int i = 1; i <= 4; i++ ) f (i l ;
1* Output: Initialization that Poi nt 1 Performing cleanup Initialization that Po int 1 Po int 2 Performing cleanup Initialization that Po int 1 Point 2 Po int 3 Perfo r mi ng cleanup Initiali za tion that Po int 1 Po int 2 Point 3
args)
{
requires cleanup
requires cleanup
requires cleanup
requires c l eanup
End Performi ng cleanup
* /// , Podemos ver, en la salida del ejemplo, que no importa desde dónde vol vamos, ya que siempre se ejecuta la c láu sula finally .
300 Piensa en Java
Ejercic io 16:
(2) Modifique reusing/CA DSystem.java para demostrar que si volvemos desde un punto siruado en la mitad de una estmctura try- fin all y se seg uirán ejecutando adecuadamente las tareas de limpieza.
Eje rcicio 17:
(3) Modifique polymorphism/Frog.j ava para que utilice la estrucrura try-fi nally con el fin de garantizar que se lleven a cabo las tareas de limpieza y demuestre que esto funciona incluso si ejecutamos una instmcción retu r n en mitad de la estructura try-finall y.
Un error: la excepción perdida Lamentablemente, existe un fallo en la implementación del mecanismo de excepciones de Java. Aunque las excepciones Son una indicación de que se ha producido una crisis en el programa y nunca deberían ignorarse. resulta posible que una excepción se pierda si n más. Esto sucede cuando se utili za un a con figuración concreta con una cláusula fi na lly:
11 : exceptions / LostMessage.java II Forma de perder una excepción. class VerylmportantException extends Exception public String toString () { return nA very important exception!";
class HoHumException extends Exception public String toString() { return "A trivial exception" i
public class LostMessage { void f () throws VerylmportantException throw new VerylmportantException () ; void dispose () throws HoHumException throw new HoHumException () i public static void main (String [] args ) { try ( LostMessage 1m = new LostMessage () ; try ( 1m. f () ; finally lm.dispose () ; catch (Exception e ) { System.out.println (e )
i
1* Output: A trivial exception
- /// ,Podemos ve r, analizando la salida, que no existe ninguna prueba de que se haya producido la excepción Vcry lmport ant Exception, que es simplemente sustituida por la excepción HoHumE xce ption en la cláusula finall y. Se trata de un fallo imponante, puesto que implica que puede pe rderse completamente una excepción, y además puede perderse de una fomla bastante más sutil y dificil de de tectar que en el ejemplo anterior. Por co ntraste, e++ considera como un erro r de programación que se genere una segunda excepc ión antes de que la primera haya sido tratada. Quizá, una futura versión de Java solventará este problema (por otro lado, no rma lmente lo que haremos será encerrar cualqu ier mé todo que genere una excepción, C01110 es el caso de dispose( ) en el ejemplo anterior, dentro de una cláusula try-ca tch).
12 Tratamiento de errores mediante excepciones 301
Una forma todavía más simple de perder una excepción consiste en volver con return desde dentro de una cláusula finally :
11:
exceptions/ExceptionSilencer.java
public class Ex ception Silencer { public static void main(S t ring[] args) try { throw n ew RuntimeException(} ¡ f inally { 1I La utili zación de t r eturn t dentro de un bloque f inally I1 h ará que se pie r da la excepción g e n e rada . return¡
Si ejecutamos este programa, veremos que no produce ninguna salida, a pesar de que se ha generado una excepción. Ejercicio 18:
(3) Añada un segundo nivel de pérdida de excepciones a LostMessage.java para que la propia excepción HoHum Exception sea sustituida por una tercera excepción.
Ejercicio 19:
(2) Solucione el problema de LostMessage.java protegiendo la llamada contenida en la cláusula finally .
Restricciones de las excepciones Cuando sustituimos un método, sólo podemos generar aquellas excepciones que hayan sido especificadas en la versión del método correspondiente a la clase base. Se trata de una restricción muy útil. ya que implica que el código que funci one con la clase base funcionará también automáticamente con cualquier objelO derivado de la clase base (lo cual , por supuesto, es un concepto fundamental den tro de la programación orientada a objetos), incluyendo las excepciones. Este ejemplo ilustra los tipos de restricciones impuestas a las excepciones (en tiempo de compilación):
11 :
e x cep ti ons/Stormy l nning . java Los método s sustituidos sólo puede n generar l as exc e pcione s especificadas en sus v e rsione s de la clase base , II o e x cep ciones de r ivadas de las exc epcione s de la clase base .
II II
class Basebal l Excep t ion extends Ex ception {} class Foul extends BaseballExce ption {} class Strike extend s BaseballException {} abstract c l ass Inning { public Inning() throws BaseballException {} public void event() throws Basebal l Exception II No tiene por qué generar nada public abstract void atBat{) throws Strike, Faul¡ publ ic void walk () {} liNo genera ninguna excepción comprobada
class StormEx ception e x t e nds Ex c ep tion {} class RainedOut e xt ends StormExc ep t i an {} class PopFoul extends Foul {} int erface Storm { pub li c v o i d e v ent () throws RainedOut¡ public v oid rainHard() throws RainedOut¡
public class Stormyl nning extends Inning implements Storm {
302 Piensa en Java
II II
Se pueden añadir nuevas excepciones para los constructores, pero es necesario tratar con las excepciones del constructor base: public Stormylnning() throws RainedOut, BaseballException {} public Stormy lnning (String s) throws Foul, BaseballException {} II Los métodos normales deben adaptarse a la clase base: II! void walk() throws PopFoul {} // error de compilación // Una interfaz NO PUEDE añadir excepciones a los métodos // existentes en la clase base: ji! public void event() throws RainedOut {} 1/ Si el método no existe ya en la clase // base, la excepción es válida: public void rainHard() throws RainedOut {} // Podemos definir no generar ninguna excepción, // aun cuando la versión base lo haga: public void event I I {} // Los métodos sustituidos pueden generar excepciones heredadas: public void atBat() throws PopFoul {} public static void main{String[] args) { try ( Stormylnning si = new Stormylnning{) ¡ si.atBat() i catch IPopFoul e l { System. out . println ( ti Pop foul") i catch (RainedOut e) { System . out. println ( tlRained out ti) i catch{BaseballException e) { System. out. println ( "Generi c baseball exception") ¡ // Strike no se genera en la versión derivada. try ( // ¿Qué sucede si generalizamos? Inning i = new Stormylnning()¡ i.atBat() i // Hay que capturar las excepciones de la 1/ versión del método correspondiente a la clase base: catch(Strike el { System. out. println ( " Strike i catchlFoul el { System. out. println ( " Foul") ¡ catch (RainedOut el { System. out _printIn ("Rained out ") i catch(Baseball Exception e) { System.out .printIn ( "Generic baseball exception") i tl
)
}
///,En Inning, podemos ver que tanto el constructor como el método event() especifican que generan una excepción, pero que nunca lo hacen. Esto es legal, porque nos pem1ite obligar al usuario a capturar cualquier excepción que podamos añadir en las versiones sustituidas de event(). La misma idea puede aplicarse a los métodos abstractos como podemos ver en atB at(). La interfaz Storm es interesante porque contiene un método (eve nt()) que está definido en Inning, y otro método que no lo está. Ambos métodos generan un nuevo tipo de excepción, Rain edOu t. Cuando Storm ylnning amplía (ex tend s) Inning e implementa (implements) Storm, podemos ver que el método event() de Storm /la puede cambiar la interfaz de excepciones de cvcnt() definida en Inning. De nuevo, esto tiene bastante sentido porque en caso contrario nunca sabríamos si estamos capturando el objeto correcto a la hora de trabajar con la clase base. Por supuesto, si un método descrito en una interfaz no se encuentra en la clase base, como es el caso de r ainHard(), no existe ningún problema en cuanto a las excep· ciones que genere.
12 Tratamiento de errores mediante excepciones 303
La restricción relativa a las excepciones no se aplica a los constructores. Podemos \'cr en Stormylnning que un constructor puede generar todo aquello que desee. independientemente de lo que genere el constructor de la clase base. Sin embargo. puesto que siempre hay que invocar el constructor de la clase base de una forma o de otra (aquí se invoca el constructor predetenninado de manera automática). el conslructor de la clase derivada deberá declarar todas las excepciones del constructor de la clase base en su propia especificación de excepciones. Un cons tructor de la clase derivada no puede capturar las excepciones generadas por su constructor de la clase base. La razón por la que Stormyln nin g.walk( ) no podrá compilarse es que genera una excepción. mientras que lnning.walk() no lo hace. Si se pennitiera esto. entonces podríamos escribir código que in vocara a lnning.walk() y que no tuviera que tratar ninguna excepción, pero entonces, cuando efectuáramos una sustitución y empleáramos un objeto de una clase derivada de Inning. podrían generarse excepciones. con Jo que nuestro código fallaria. Obligando a los métodos de la clase derivada a adaptarse a las especificaciones de excepciones de los métodos de la clase base. se mantiene la posibilidad de sustituir los objetos. El método sustituido eVf'nt() muestra que la versión de un método definido en la clase derivada puede elegir no generar nin guna excepción, incluso a pesa r de que la versión de la clase sí las genere. De nuevo, no pasa nada por hacer esto, ya que no dejarán de funcionar aquellos programas que se hayan escrito bajo la suposición de que la versión de la clase base genera excepciones. Podemos aplicar una lógica similar atBat( ), que genera PopFoul. una excepción que deriva de la excepción Fo ul generada por la versión de la clase base de atBat(). De esta fonna, si escribimos código que funcione con Inning y que invoque at8at( ). deberemos capturar la excepción Fou!. Puesto que PopFoul deri va de Foul. la rutina de tratamien10 de excepciones también pennitirá capturar PopFoul . El último punto de interés se encuentra en main(). Aquí, podemos ver que. si estamos tratando co n un objeto que sea exaclamente del tipo Stormylnning, el compilador nos obligará a capturar únicamente las excepciones que sean específicas de esa clase, pero si efectuamos una generali zación al tipo base. entonces el compilador (cOlTectamente) nos obligará a capturar las excepciones de l tipo base. Todas estas restricciones penniten obtener un código de tratamiento de excepciones mucho más robusto 6 . Aunque es el compilador el que se encarga de imponer las especificaciones de excepciones en los casos de herencia, esas especificaciones de excepciones no fomlan parte de la signatura de un método. que está compuesta sólo por el nombre del método y los tipos de argumentos. Por tanto, no es posible sobrecargar los métodos basándose solamente en las especificaciones de excepciones. Además, el hecho de que exista un a especificac ión de excepción en la ve rsión de la clase base de un método no quiere decir que dicha especificación deba existir en la versión de la clase derivada del método. Esto difiere bastante de las reglas nomlales de herencia, según las cuales un método de la clase base deberá también existir en la clase derivada. Dicho de otra fonna, la "interfaz de especificación de excepciones" de un método concreto puede estrecharse durante la herencia y cuando se realizan sustituc iones. pero lo que no puede es ensancharse~ se trata, precisamente. de la regla opuesta a la que se ap lica a la interfaz de una clase durante la herencia. Ejerci cio 20 :
(3) Modifique Storm ylnning.j ava añadiendo un tipo excepción UmpireArgument y una serie de métodos que generen esta excepción. Compruebe la jerarquía modificada.
Constructores Es importante que siempre nos hagamos la pregunta siguiente: "Si se produce una excepción, ¿se limpiará todo apropiadamente?"' La mayor parte de las veces, podemos eslar razonablemente seguros, pero con los constructores existe un problema. El constructor sitúa los objetos en un estado inicial seguro, pero puede realizar alguna operac ión (como por ejemplo abrir un archivo) que no revierta hasta que el usuario termine con el objeto e invoque un método de limpieza especial. Si generamos una excepción desde dentro de un constructor, puede que estas tareas de limpieza no se lleven a cabo apropiadamente. Esto quiere decir que debemos tener un especial cuidado a la hora de escribir los constructores. Podíamos pensar que la cláusula finally es una solución. Pero las cosas no son tan simples. porque finaUy lleva a cabo las tareas de limpieza lodas las \'eces. Si un constructor fa lla en mitad de la ejecución, puede que no haya tenido tiempo de crear alguna parte del objeto que será limpiado en la cláusu la linally. 6 El estándar ISO e++ ha añndido unas restricciones similares. que obligan a que las excepciones de los métodos derivados sean iguales a las excepciones gen~ntdas por los métodos de la clase base. o al menos a que deri\'en de ellas. Éste es uno de los casos en los que C++ es capaz de comprobar las especificaciones de excepciones en tiempo de compIlación.
304 Piensa en Java
En el siguiente ejemplo. se crea una clase denominada InputFile que abre un archivo y pemlite leerlo línea a línea. Utiliza las clases FileReader y BufferedR •• der de la biblioteca estándar E/S de Java que se analizará en el Capítulo 18, E/S Estas clases son lo suficientemente simples como para que el lector no tenga ningún problema a la hora de comprender los fun_ damentos de su utilización:
11: exceptions/lnputFile.java
II Hay que prestar a las excepciones en los constructores. import java . io.*; public class InputFile private BufferedReader in; public InputFile(String fname)
throws Exception {
try (
= new BufferedReader (new FileReader(fname»; Otro código que pueda generar excepciones catch(FileNotFoundException el { System.out.println("Could not open " + fname) ¡ II No estará abierto, por lo que no hay que cerrarlo. throw e¡ catch(Exception e l II Todas las demás excepciones deben cerrarlo in
II
try { in. close () ; catch (IOException e2) { System.out .println(lIin.close() unsuccessfuI U ) ; throw e¡ II Regenerar finally ( II ¡ j i No cerrarlo aquí!!!
public String getLine () String Si
{
try ( s = in.readLine()¡ catch(IOException el throw new RuntimeException("readLine()
failed ll
);
return s; public void dispose()
try ( in.close (); System.out .println ("dispose() successful"); catch (IOException e2) { throw new RuntimeException (" in. clase () failed 11) ;
El constructor de InputFile toma un argumento String, que representa el nombre del archivo que queremos abrir. Dentro de un bloque Iry, crea un objeto FileReader utili zando el nombre del archivo. Un objeto "ileReader no resulta particularmente útil hasta que lo empleemos para crear otro objeto BufferedReader (para lectura con buffer). Uno de los beneficios de InputFile es que combina las dos acciones. Si el constnlctor de FileReader falla, generará una excepción FileNolFoundException , que indicará que no se ha encontrado el archivo. Éste es el único caso en el cual no querernos cerrar el archivo, ya que no hemos llegado a poder abrirlo. Cualquier aIra cláusula catch deberá cerrar el archivo, porque estará abierto en el momento de entrar en dicha cláusula catch (por supuesto, el asunto se comp lica s i hay más de un método que pueda generar una excepción FileNolFoundExceplion. En dicho caso, normalmente, habrá que descomponer las cosas en varios bloques Iry). El rnéto-
12 Tratamiento de errores mediante excepciones 305
do c1ose() puede generar una excepción, así qu e lo encerramos dentro de una ctáusula try y tratamos de capulrar la excepción aú n cuando ese método se encuentre dentro del bloque de otra cláusula catch: para el compi lador de Java se trata simplemente de un par adicional de símbolos de llave. Después de realizar las operaciones locales, la excepción se vuelve a generar. lo cual resulta apropiado porque este constructor ha fallado y no queremos que el método que ha hecho la in vocación asuma que el objeto se ha creado apropiadamente y es válido. En este ejem plo, la cláusula finally no es. en modo alguno, el lugar donde ce rrar el archivo con c1osc(), ya que eso haría que el archi vo se cerrara cuando el constructor completara su ejecución. Lo que queremos es que el archi vo continúe abierto mientras dure la vida útil del objeto InputFile. El método getLinc() devuelve un objeto String que contiene la siguiente linea del archivo. Dicho método invoca a readLinc(), que puede generar una excepción, pero dicha excepción es capturada, por lo que gctLinc( ) no genera excepción alguna. Uno de los problemas de diseño relati vos a las excepciones es el de si debemos tratar una excepción completam ente en es te nivel. si sólo debemos tratarla parcialmente y pasar la misma excepción (u otra distinta) al ni vel siguiente, o si debemos pasar la excepción directamente al siguiente nivel. Pasar la excepción directamente, siempre que sea apropiado, puede simplificar bastante el programa. En nu estro caso, el método getUnc() cOl/vierfe la excepc ión al tipo RuntimeException para indicar que se ha producido un error de programación. El método dispose() debe ser llamado por el usuario cuando ya no se necesite el método InputFile. Esto hará que se liberen los recursos del sistema (como por ejemplo los descriptores de archivo) que estén siendo utilizados por los objetos BuffercdReader y/o FileReader. Evidentemente, no queremos hacer esto hasta que hayamos tenninado de utili zar el objeto InputFile. Podríamos pensar en incluir dicha funcionalidad en un método finalize() , pero como hemos dicho en el Capítulo 5, Inicialización y limpieza. no siempre podemos estar seg uros de que se vaya a llamar a finalize() (e, incluso si estuviéramos seguros de que va a ser llamado, lo que no sabemos es cuándo). Ésta es una de las desve ntajas de Java: las tareas de limpieza (excepn13ndo las de memoria) no ti enen lugar automáticamente, por lo que es preciso infonllar a los programadores de clientes de que ellos son los responsables. La fonna más segura de utilizar una clase que pueda ge nerar una excepción durante la construcción y que requiera que se lleven a cabo tareas de limpieza consiste en emplear bloques try anidados:
11:
ji
exceptions jCleanup . java Forma de garantizar la apropiada limpieza de un recurso.
public class Cleanup public static void main (St ring [] args) { try { InputFile in = new InputFile ("Cleanup.java") i try { String Si int i == 1 i while((s = in.getLine()) != null) i II Realizar aquí el procesamiento línea a línea. ca tch (Except ion e) { System.out.println("Caught Exception in main") i e.printStackTrace(Syscem.outl; finally { in . dispose() i catch(Exception el System.out . println("InputFi l e construction failed lt }
i
1* Output: dispose() successful
, /// ,Examine cu idadosamente la lógica utilizada: la construcc ión del objeto InputFile se encuentra en su propio bloque try. Si di cha construcción falla, se entra la clá usula catch extema y no se invoca el método dispose(). Sin embargo, si la construcción tiene éxito, entonces hay que asegurarse de que el objeto se limpie, por lo que inmediatamente después de la construcción creamos un nuevo bloque try. La cláusula finally que lle va a cabo las tareas de limpieza está asociada con el bloque
306 Piensa en Java
try interno; de esta fomla , la cláusul a finally no se ejec uta si la construcción fa lla, mientras que siempre se ejecuta si la construcción ti ene éx ito. Esta técnica general de limpieza debe utili zarse aún cuando el constmctor no genere ninguna excepción. La regla básica es: justo después de crear un objeto que requiera limpieza, Lncluya una estmctura try-finally:
11 : exceptions / CleanupIdiom.java
II
Cada objeto eliminable debe estar seguido por try-finally
class NeedsCleanup { II Construction can't fail private static long counter = 1; private final long id = counter++¡ publ ie void dispose () { System.out.println ( "NeedsCleanup " + id + 11 disposed" ) ;
class ConstructionException extends Exception {} class NeedsCleanup2 extends NeedsCleanup { II La construcción no puede fallar : public NeedsCleanup2() throws ConstructionException {}
public class CleanupIdiom { public static void main (String [] args) { II Sección 1: NeedsCleanup ncl = new NeedsCleanup( ) ;
try (
11 ... finally ncl.dispose() ;
II II
Sección 2: Si la construcción no puede fallar, NeedsCleanup nc2 new NeedsCleanup{)¡ NeedsCleanup nc3 = new NeedsCleanup();
podemos agrupar los objetos:
try (
11 finally nC3.dispose {) ; nc2.dispose ( ) ;
II II
II
Orden inverso al de construcción
Sección 3: Si la construcción puede fallar,
hay que proteger cada uno:
try ( NeedsCleanup2 nc4 = new NeedsCleanup2{);
try ( NeedsCleanup2 ncS
= new
NeedsCleanup2();
try (
11 finally ncS.dispose()
i
catch (ConstructionException e) System . out .println(e) i finally ( nc4 . dispose ( ) ;
{
II
const ru ctor de ncS
12 Tratamiento de errores mediante excepciones 307
catch (ConstructionException e l { // constructor de nc4 System.out.println (e l ;
/ * Output: NeedsCleanup NeedsCleanup NeedsCleanup NeedsCleanup NeedsCleanup
1
disposed
3 disposed 2 disposed
5 disposed 4 disposed
*/// ,En main( ), la sección l nos resulta bastante se ncilla de entender: incluimos una estructura tr y-fi nall y después de un objeto eliminable. Si la constmcción del objeto no puede fallar, no es necesario incluir ninguna cláusula catch . En la sección 2,
podemos ver que los objetos con constructores que no pueden fallar pueden agruparse tanto para las tareas de construcción como para las de limpieza. La sección 3 muestra cómo tratar con aquellos objetos cuyos constmctores pueden fallar y que necesitan limpieza. Para poder manejar adecuadamente esta situación, las cosas se complican, po rque es necesario rodear cada construcción con su propia estructura try-catch, y cada construcción de objeto debe ir seguida de un tr y-fin all y para garantizar la limpieza. Lo com plicado del tratamiento de excepciones en este caso es un buen argumento en favor de la creación de constructores que no puedan fallar, aunque lamentablemente esto no siempre es posible. Observe que si dispose() puede generar una excepción, entonces serán necesarios bloques tr)' adicionales. Básicamente, lo que debemos hacer es pensar con cuidado en todas las posibi 1idades y protegernos frente a cada lIna. Ejercicio 21:
(2) Demuestre que un constTuctor de una clase derivada no puede capturar excepciones generadas por su constnlClOr de la clase base.
Ejercicio 22 :
(2) Cree una clase denominada FailingCo nstr uctor con un constructor que pueda fa\lar en mitad del proceso de constmcción y generar una excepción. En ma in(). escriba el código que pemlita protegerse apropiadamente frente a este faUo.
Ejercicio 23 :
(4) Afiada una clase con un método d ispose( ) al ejercicio anterior. Modifique FailingCo nstructor para que el constnlctor cree uno de estos objetos eliminables como un objeto miembro, después de lo cual el constructor puede generar una excepción y crear un segundo objeto miembro eliminable. Escriba el código necesario para protegerse adecuadamente contra los fallos y verifique en ma in() que están cubiertas todas las posibles situaciones de fa\lo.
Ejercicio 24:
(3) Allada un método dispose( ) a la clase Faili ngConstr uctor y escriba el código necesario para utilizar adecuadamente esta clase.
Localización de excepciones Cuando se genera una excepción, el sistema de tratamiento de excepciones busca entre las mtinas de tratamiento "más cercanas", en el orden en que fueron escritas. Cuando encuentra una correspondencia, se considera que la excepción ha sido tratada y no se continúa con el proceso de búsqueda. Localizar la excepción correcta no requiere que haya una correspondencia perfecta entre la excepción y su rutina de tratamiento. Todo objeto de una clase derivada se corresponderá con una rutina de tratamiento correspondiente a la clase base, como se muestra en este ejemplo: // : exceptions/Human.java / / Captura de jerarquías de excepciones. class Annoyance extends Exception {} class Sneeze extends Annoyance {} public class Human {
308 Piensa en Java public static void main (String [] args) { 11 Capturar el tipo exacto: try ( throw new Sneeze() ¡ catch(Sneeze sl { System.out. println (lICaught Sneeze ll ) ; catch (Annoyance a) { System.out.println(IICaught Annoyance ll
)
i
11 Capturar el tipo base: try ( throw new Sneeze()¡ catch (Annoyance al { System. out. println ( 11 Caught Annoyance 11 ) ¡
1* Output: Caught Sneeze Caught Annoyance * /// , La excepción Sneeze será capturada por la primera cláusula catch con la que se corresponda, que será por supuesto la primera. Sin embargo, si eliminamos la primera cláusula catch, dejando sólo la cláusula catch correspondiente a Annoyance, el código seguirá funcionando porque se está capturando la clase base de Sneeze. Dicho de otra forma, catch(Annoyance a) penniti rá capturar Ulla excepción Annoyance o cualquier clase derivada de ella. Esto resulta útil porque si dec idimos añadir más excepciones derivadas a un método, no será necesario cambiar el código de los programas cliente, siempre y cuando el cliente capnlre las excepciones de la clase base. Si tratamos de "enmascarar" las excepciones de la clase derivada, incluyendo primero la cláusula catc h correspondiente a la clase base, como en el siguiente ejemplo: try ( throw new Sneeze(); catch (Annoyance al {
// ... catch (Sneeze s)
// el compilador nos dará un mensaje de error, ya que verá que la cláusula catch correspondiente a Sncezc nunca puede ejecutarse. Ejercicio 25:
(2) Cree una jerarquía de excepciones en tres niveles. Ahora cree una clase base A con un método que genere una excepción de la base de nuestra jerarquía. Herede una clase B de A y sustituya el método para que genere una excepción en el nivel dos de la jerarquía. Repita el proceso, heredando una clase e de B. En main(), cree un objeto e y generalícelo a A, invoque el método a continuación.
Enfoques alternativos Un sistema de tratamiento de excepciones es un mecanismo especial que permite a nuestros programas abandon ar la ejecución de la secuencia nonna! de instrucciones. Ese mecanismo especial se utiliza cuando tiene lugar una "condi ción excepcionar', tal que la ejecución nonnal ya no es posible o deseable. Las excepciones represe ntan condiciones que el método actual no es capaz de ges tionar. La razón por la que se desarrollaron los sistemas de tratamiento de excepciones es porque la técnica de gestionar cada posible condición de error producida por cada llamada a fun ción era demasiado onerosa, lo que hacía que los programadores no la implementaran. Como resultado, se terminaba ignorando los errores en los programas. Merece la pena recalcar que incrementar la comodidad de los programadores a la hora de tratar los errores fue una de las principales motivac iones para desa rrotlar los sistemas de tratam iento de excepciones.
12 Tratamiento de errores mediante excepciones 309
Una de las directrices de mayor importancia en el tratamiento de excepciones es "no captures una excepción a menos que sepa qué hacer con ella". De hecho, uno de los objetivos más importantes del tratamjento de excepciones es quitar el código de tratamiento de errores del punto en el que los errores se producen. Esto nos pennite concentramos en lo que queremos conseguir en cada sección del código, dejando la manera de tratar con los problemas para ulla sección separada del mismo código. Como resultado, el código principal no se ve oscurecido por la lógica de tratamiento de errores, con lo que resulta mucho más fácil de co mprender y de mantener. Los mecanismos de tratamiento de excepciones también tienden a reducir la cantidad de código dedicado a estas tareas. pemlitiendo que una rutina de tratamiento dé servicio a múltip les lugares posibles de generación de errores. Las excepciones comprobadas complican un poco este escenario, porque nos fuerzan a añadir cláusulas catch en lugares en los que puede que no estemos listos para gestionar un error. Esto puede dar como resultado que no se traten ciertas excepciones: try {
II ... hacer algo útil } catch IObligatoryException e l
{} / / Glub!
Los programadores (incluido yo en la primera edición de este libro) tienden a hacer lo más simple y a capturar la excepción olvidándose luego de tratarla; esto se hace a menudo de forma inadvertida, pero una vez que lo hacemos, el compilador se queda satisfecho, de modo que si no nos acordamos de revisar y corregir el código, esa excepción se pierde. La excepción tiene lugar, pero desaparece todo rastro de la misma una vez que ha sido capturada y se deja sin tratar. Puesto que el compilador nos fuerza a escribir código desde el principio para tratar la excepción, incluir una rutina de tratamiento vacía parece la solución más simple, aunque en realidad es lo peor que podemos hacer. Horrorizado al damlc cuenta de que yo había hecho precisa mente esto, en la segunda edición del libro "corregí" el problema imprimiendo la traza de la pila dentro de la mtina de tratamiento (como puede verse apropiadamente en varios ejemplos de este capítulo). Aunque resulta útil trazar el comportamiento de las excepciones, esta forma de proceder sigue indicando que realmente no sabemos qué hacer con esa excepción en dicho punto del código. En esta sección, vamos a estudiar algunos de los problemas y algunas de las complicaciones derivadas de las excepciones comprobadas, y vamos a repasar las opciones que tenemos a la hora de tratar con ellas. El tema parece bastante simple, pero no sólo resulta complicado sino que también es motivo de controversia. Hay personas que sostienen finnemente los argumentos de ambos bandos y que piensan que la respuesta correcta (es decir, la suya) resulta tremendamente obvia. En mi opinión, la razón de que den estas posiciones tan vehementes es que resulta bastante obvia la ventaja que se obtiene al pasar de un lenguaje con un pobre tratamiento de los tipos, como el previo ANSI e a un lenguaje fuertemente tipado con tipos estáticos (es decir, comprobados en tiempo de compilación) como e++ o Java. Cuando se hace esa transición (como hice yo mi smo), las ve ntajas resultan tan evidentes que puede parecer que la comprobación estática de tipos es siempre la mejor respuesta a la mayoría de los problemas. Mi esperanza con las líneas que siguen es que, al relatar mi propia evolución, el lector pueda ver que el valor absoluto de la comprobación estática de tipos es cuestionable; obviamente, resulta muy útil la mayor parte de las veces, pero hay un línea tremendamente difusa a partir de la cual esa comprobación estática de tipos comienza a ser un estorbo y un problema (una de mis citas favoritas es la que dice "todos los modelos son erróneos, aunque algunos de ellos resultan útiles").
Historia Los sistemas de tratamiento de excepciones tienen su origen en sistemas como PLII y Mesa, y posteriormente se incorporaron en CLU, Smalltalk, Modula-3, Ada, Eiffel, C++, Python, Java y los lenguajes post-Java como Ruby y C#. El di seño de Java es similar a C++-, excepto en aquellos lugares en los que los diseñadores de Java pensaron que la técnica usada en C++ podría causar problemas. Para proporcionar a los programadores un marco de trabajo que estuvieran más di spuestos a utilizar para el tratamiento y la recuperación de errores, el sistema de tratamiento de excepciones se añadió a e++ bastante tarde en el proceso de estandarización, promovido por Bjame Stroustrup, el autor original del lenguaje. El modelo de las excepciones de e++- proviene principalmente de CLU. Sin embargo, en aquel entonces existían otros lenguajes que también soportaban el tratamiento de excepciones: Ada, Smalltalk (ambos tienen excepciones, pero no tienen especificaciones de excepciones) y Modula-3 (que incluía tanto las excepciones como las especificaciones).
310 Piensa en Java
En su artículo pionero 7 sobre el tema, Liskov y Snyder observaron que uno de los principales defectos de los lenguajes tipo e, que infomlan acerca de los errores de manera transitoria es que:
..... toda im'ocaciólI debe ir seguida de una prueba incondicional para determinar cuál ha sido el resultado. Este requisito hace que se desarrollen programas dificiles de leer y que probablemente también 5011 poco ejicie11les, lo que tiende a desanimar a los programadores a la hora de .,·eliali:ar y tratar las excepciones. " Por tanto. uno de los motivos originales para desarrollar sistemas de tratamiento de excepciones era eliminar este requisito, pero con las excepciones comprobadas en Java nos encontramos precisamente con este tipo de código. Los autores continúan diciendo:
.. ... si se requiere que se asocie el texto de tilla rutina de tratamiento a la invocación que genera la excepción, el resultado serán programas poco legibles en los que las expresiones estarán descompuestas debido a la presencia de las rutinas de tratamiento. " Siguiendo el enfoque adoptado en CLU, Stroustrup afinnó, al diseñar las excepciones de e ++, que el objetivo era reducir la cantidad de código requerida para recuperarse de los errores. En mi opinión, estaba partiendo de la observación de que los programadores no solían escribir código de tratamiento de errores en e debido a que la cantidad y la colocación de dicho código en los programas era muy dificil de manejar y tendía a distraer del objetivo principal del programa. Como resultado, los programadores solian abordar el problema de la misma manera que en e. ignorando los errores en el código y utilizando depuradores para localizar los problemas. Para usar las excepciones, había que convencer a estos programadores de e de que escribieran código "adiciona!", que normalmente no escribirían. Por tanto, para hacer que pasen a adoptar una fonna más eficiente de tratar los errores, la cantidad de código que esos programadores deben "ai'iadir" no debe ser excesiva. Resulta importante tener presente este objetivo de diseño inicial a la bora de eliminar los efectos que las excepciones comprobadas tienen en Java.
e++ tomó prestado de CLU una idea adicional: la especificación de excepción, mediante la cual se enuncian programáticamente en la signatura del método las excepciones que pueden generarse como resultado de la llamada al método. La especificación de excepciones tiene, en realidad, dos objetivos. Puede querer decir: "Puedo generar esta excepción en mi código, encárgate de tratarla", Pero también puede significar: "Estoy ignorando esta excepción que puede producirse como resultado de mi código, encárgate de tratarla'". Hasta ahora, nos estamos centrando en la parte que dice "encárgate de tratarla" a la hora de examinar la mecánica y las sintaxis de las excepciones, pero lo que en este momento concreto nos interesa es el hecho de que a menudo ignoramos las excepciones que se producen en nuestro código, y eso es precisamente lo que la especificación de excepciones puede indicar. En C++, la especificación de excepciones no fornla parte de la infonnación de tipo de una función (la signatura). La única comprobación que se realiza en tiempo de compilación consiste en garantizar que las especificaciones de excepciones se utilizan de manera coherente, por ejemplo, si una función o un método generan excepciones, entonces las versiones sobrecargadas o derivadas también deberán generar esas excepciones. A diferencia de Java, sin embargo, no se realiza ninguna comprobación en tiempo de compilación para detenninar si la función o método va a generar en realidad dicha excepción. o si la especificación de excepciones está completa (es decir, si describe con precisión todas las excepciones que puedan ser generadas). Esa va lidación sí que se produce, pero sólo en tiempo de ejecución. Si se genera una excepción que viola la especificación de excepciones, el programa e++ invocará la función de la biblioteca estándar unexpected(). Resulta interesante observar que. debido al uso de plantillas, las especificaciones de excepciones no se utilizan en absoluto en la biblioteca estándar de C++. En Java, existen una serie de restricciones que afectan a la forma en que pueden emplearse los genéricos Java con las especificaciones de excepciones.
Perspectivas En primer lugar. merece la pena observar que es el lenguaje Java el que ha inventado las excepciones comprobadas (inspiradas claramente en las especificaciones de excepciones de e++ y en el hecho de que los programadores e++ no suelen ocuparse de las mismas). Sin embargo, se trata de un experimento que ningún lenguaje subsiguiente ha incorporado.
7 Barbara Liskov y Alan Snydcr. Exceplion Handling in CLU. IEEE Transactions on Sonware Enginecring. Vol. SE-5, No. 6. Noviembre 1979. Este antculo no c~ta disponible en Internet. sino sólo en copia impresa. por lo que tendrá que encargar una copia a tra\cs de su biblioteca.
12 Tratamiento de errores mediante excepciones 311
En segundo lugar, las excepciones comprobadas parecen ser algo "evidentemente bueno" cuando se las contempla dentro de ejemplos de nivel introductorio y en pequeños programas. Según algunos autores, las dificultades más sutiles comienzan a aparecer en el momento en que los programas crecen de tamaño. Por supuesto. el tamaiio de los programas no suele incrementarse de manera espectacular de la noche a la mañana. sino que lo más normal es que los programas vayan creciendo de tamaño poco a poco. Los lenguajes que puedan no ser adecuados para proyectos de gran envergadura, se utilizan sin problema para proyectos de pequeño tamaño. Pero esos proyectos crecen y. en algún punto, nos damos cuenta de que las cosas que antes eran manejables ahora son relativamente dificiles. A eso es a lo que me refería al comentar que los mecanismos de comprobación de tipos pueden llegar a hacerse engorrosos: en particular. cuando esos mecanismos se combinan con el concepto de excepciones comprobadas. El tamaño del programa parece ser una de las cuestiones principales. Y esto es, en sí mismo, un problema porque la mayoría de los análisis tienden a utilizar como ilustración programas de pequeilo tamaí1o. Uno de los diseñadores de C# escribió
que: "El examen de programas de pequeiio lamaiio nos I/e\'a a la conclusión de que imponer el uso de especificaciones de excepciones podría mejorar tanto la productividad del desarrollador como la calidad de código, pero la experiencia con los grandes proyecfOs de desarrollo software sugiere un resultado completamente distinto: 11110 menor productividad y un incremento en la calidad del código que es, como mucho. poco significativo, .. 8 En referencias a las excepciones no capturadas, los creadores de CLU escribían: "Pensamos que era poco realista exigir al programador que proporcionara rutinas de tratamiento en aquellas situaciones en las que no es posible llevar a cabo ninguna acción con verdadero significado. "9 A la hora de explicar por qué una declaración de función sin ninguna especificación significa que la función pueda generar
cualquier excepción en lugar de ninguna excepción. Stroustrup escribe: "Sin embargo, eso requeriria que se incluyeran especificaciones de excepción para casi todas lasfimciones. haría que fuera necesario efectuar muchas recompilaciones y d{ficultaría la cooperación con el software escrito en otros lenguajes, Esto animaría a los programadores a subvertir los mecanismos de tratamiento de excepciones y a escribir código espúreo para suprimir las excepciones. Proporcionarla un falso sentido de seguridad a las personas que no se hubieran dado cuenta de la excepción, .. 10 Precisamente, con las excepciones comprobadas en Java podemos ver que se produce precisamente esta reacción: tratar de subvertir las excepciones. Martin Fowler (autor de UAfL Distilled, Refactoring y Analysis Patlerns) escribía en cierta ocasión lo siguiente:
·· ... en conjunto. creo que las excepciones son buenas. pero las excepciones compmbadas en Java callsan más problemas de los que resuelven. ., Actualmente, lo que opino es que el paso más importante dado por Java fue unificar el modelo de información de errores, de modo que de todos los errores se informa utilizando excepciones, Esto no sucedía en C++. porque. debido a la compatibilidad descendente con C, seguía estando disponible el modelo de limitarse a ignorar los elTores, Pero, cuando disponemos de un mecanismo de infonne de errores coherente con excepciones, las excepciones pueden utilizarse si se desea y. en caso contrario, se propagarán al siguiente nivel superior (la consola u otro programa contenedor). Cuando Java modificó el modelo e++ para que las excepciones fueran la única fonna de infom1ar de los errores, la imposición WrapCheckedExccption.throwRuntimeException( ) contiene código que genera diferentes tipos de excepciones. Éstas se capturan y se envuel ven dentro de objetos RuntimeException , asi qu e se convierten en la "causa" de dichas excepciones. En TurnOffChecking, podemos ver que es posible in vocar throwRuntimeException( ) sin ningún bloque try porque el método no genera ninguna excepción comprobada. Sin embargo, cuando estemos listos para capturar las excepciones, seguiremos teniendo la posibilidad de capturar cualquier excepción que queramos poniendo nuestro código dentro de un bloque try. Comenzamos capturando todas las excepciones que sabemos explícitamente que pueden emerger del código incluido dentro del bloque try; en este caso. se captura primero SomeOtherException . Finalmente, se captura RuntimeException y se genera con throw el resultado de getCa use() (la excepción envuelta). Esto extrae las excepciones de origen, que pueden se r entonces tratadas en sus propias cláusulas catch. La técnica de envolver una excepción comprobada en otra de tipo RuntimeException se utili zará siempre que sea apropi ado a lo largo del resto del libro. Otra solución consiste en crear nuestra propia subclase de RuntirneException . De esta forma, no es necesari o capturarla, pero alguien puede hacerlo si así lo desea. Ejercicio 27 :
( 1) Modifique el Ejercicio 3 para convertir la excepción en otra de tipo RuntimeException .
Ejercicio 28:
( 1) Modifique el Ejercicio 4 de modo que la clase de excepción personali zada herede de RuntimeException, y muestre que el compi lador nos pem1ite no incluir el bloque try.
Ejercicio 29:
( 1) Modifique todos los tipos de excepción de Stormylnning.java de modo que extiendan RuntimeException, y muestre que no son necesarias especificaciones de excepción ni bloques try. Elimine los comentarios 'II! ' y muestre cómo pueden compi larse los métodos sin especificaciones.
Ejercicio 30:
(2) Modifique Human.java de modo que las excepciones hereden de RuntimeException. Modifique maine ) de modo que se utilice la técnica de TurnOffChecking.java para tratar los diferentes tipos de excepc iones.
12 Tratamiento de errores mediante excepciones 315
Directrices relativas a las excepciones Utilice las excepciones para: 1. Tratar los problemas en el nivel apropiado (evite cap turar las excepciones a menos que sepa qué hacer con ellas).
2. Corregir el problema e invocar de nuevo el método que causó la excepción. 3. Corregir las cosas y continuar, sin volver a ejecutar el método. 4. Calcular algunos resultados altemativos en lugar de aquellos que se supone que el método debía producir. 5. Hacer lo que se pueda en el contexto actua l y regenerar la misma excepción, entregándosela a un contexto de nivel superior. 6. Hacer lo que se pueda en el contexto actual y generar una excepción diferente, entregándosela a un contexto de nivel superior. 7. Terminar el programa. 8. Simplificar (si el esquema de excepciones utilizado hace que las cosas se vuelvan más complicadas, entonces será muy molesto de utilizar). 9. Hacer que la biblioteca y el programa sean más seguros (esto es una inversión a corto plazo de cara a la depuración y una inversión a largo plazo en lo que respecta a la robustez de la aplicación)
Resumen Las excepciones son una parte fundamental de la programación Java; no es mucho lo que puede hacerse si no se sabe cómo trabajar con ellas. Por esa razón, hemos decidido introducir las excepciones en este punto del libro; hay muchas bibliotecas (corno las de E/S, mencionadas anterionnente) que no pueden emplearse sin tratar las excepciones. Una de las ventajas del tratamiento de excepciones es que nos permite concentramos en un cierto lugar en el problema que estemos tratando de resolver, y tratar con los errores relati vos a dicho código en otro lugar. Y, aunque las excepciones se suelen explicar como herramientas que nos penniten informar acerca de los errores y recuperarnos de ellos en tiempo de ejecución, no es tan claro con cuánta frecuencia se implementa ese aspecto de "recuperación", como tampoco está muy claro si resulta siempre posible. Mi percepción es que la recuperación es posible en no más del I O por cierto de los casos, e incluso en esas sihlaciones sólo consiste en devolver la pila a un estado estable conocido, más que reanudar el procesamiento del programa. Pero, sea esto verdad o no, lo importante es que el valor fundamental de las excepciones radica en la función de "informe de errores". El hecho de que Java insista en que se ¡nfoone de todos los errores mediante las excepciones es lo que le proporciona a este lenguaje una gran ventaja respecto a otros lenguajes como C++. que permite infonnar de los errores de distintas manera o incluso no infonnar en absoluto. Disponer de un sistema coherente de infom1e de errores implica que no tenemos ya por qué hacemos la pregunta de "¿se nos está colando algún error por alguna parte?" cada vez que escribamos un fragmento de código (siempre y cuando, no capturemos las excepciones para luego dejarlas sin tratar). Como podrá ver en futuros capítulos, al pennitirnos olvidarnos de esta cuestión (aunque sea generando una excepción de tipo RuntimeException), los esfuerzos de diseño e implementación pueden centrarse en otras cuestiones más interesantes y complejas. Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tlle Thinking in Java Annorated So/mion Guide, disponible para
la venta en \n~'W.MindVie\\'.nel.
Cadenas de caracteres La manipulación de las cadenas de caracteres es probablemente una de las actividades más comunes en la programación . Esto resulta especialmente cierto en los sistemas web, en los que Java se ut ili za ampliamente. En este capítul o, vamos a examinar más en detalle la que constituye, ciertamente, la clase más comúnmente utilizada de todo el lenguaje, String, junto con sus utilidades y clases asoc iadas.
Cadenas de caracteres inmutables Los objetos de clase String son inmutables. Si examina la documentación del JDK referente a la clase String, verá que todos los métodos de la clase que parecen modificar una cadena de caracteres, 10 que hacen, en realidad, es devolve r un objeto String co mp letamente nuevo que contiene dicha modificación. El objeto String original se deja sin modificar. Considere el siguiente cód igo: //: strings/lrnmutable .java import static net . mindview util .Pr int .* public class Immutable { public static String upcase (Stri ng s) return s.toUpperCase{); public static void main (String [] args) String q = "howdy"; printlq); II howdy String qq = upcase(q); printlqq); II HOWDY printlq); II howdy
{
{
/* Out put: howdy HOWDY howdy
- 111> Cuando se pasa q a upcase( ) se trata en realidad de una copia de la referen cia a q. El objeto al que esta referencia está conectado pennanece en una única ubicación física. Las referencias se copian a medida que se las pasa de un si lio a a Iro. Examinando la definición de upcasc( ), podemos ver que la refe rencia que se le pasa ti ene un nombre s, y que di cha referencia existe sólo mientras que se ejecuta el cuerpo de upcase(). Cuando se completa upcase( ), la referencia local s desaparece. upcasc( ) devuelve el resultado, que es la cadena original con todos los caracteres en mayúscula. Por supuesto, lo que se devue lve en rea lidad es una referencia al resultado. Pero lo cierto es que la referencia que se devuelve apunta a un nuevo objeto, dejándose sin modi ficar el objeto al que apuntaba la referencia q original. Este comportamiento es n0011ahnente el que deseamos. Suponga que escribimos:
318 Piensa en Java String s = "asdf"; String x = Immutable.upcase(s};
¿Realmente queremos que el método upcase( ) mod(fique el argumento? Para el lector del código, los argumentos suelen aparecer como fragmentos de infon11ación proporcionados al método, no como algo que haya que modificar. Esta garantía es impol1ante. ya que hace que el código sea más fácil de escribir y comprender.
Comparación entre la sobrecarga de '+' y StringBuilder Puesto que los objetos String son inmutables, podemos establecer tantos alias como queramos para un objeto String concreto. Puesto que lm objeto String es de sólo lectura, no hay ninguna posibilidad de que una referencia modifique algo que pueda afectar a otras referencias. La inmutabilidad puede presentar problemas de rendimiento. Un ejemplo claro es el operador'+' que está sobrecargado para Jos objetos String. La palabra "sobrecargado" significa que hay una operación a la que se le ha proporcionado UIl significado adicional cuando se la usa con una clase concreta (los operadores '+' y '+=' para objetos String son los únicos operadores sobrecargados en Java, y el lenguaje no pemlite que el programador sobrecargue ningún otro operadorV El operador '+' nos pemlite concatenar cadenas de caracteres:
JJ: stringsJConcatenation.java public class Concatenation { public static void main (Str ing [] argsl { String mango = "mango" i String s = "abe" + mango + "def" + 47; System.out.println(s) ;
J* Output: abcmangodef47 * ///,Queremos tratar de imaginar cuál sería la forma en que este mecanismo funciona. El objeto String "abe" podría tener un método append( ) que creara un nuveo objeto String que contuviera "abe" concatenado con el contenido de mango. El nuevo objeto String crearía entonces otro objeto String que añadiera "der', etc. Esto podría funcionar. pero requiere la creación de un montón de objetos String simplemente para componer esta nueva cadena de caracteres. lo que conduciría a que hubiera varios objetos String intermedios a los que habría que aplicar posteriormente los mecanismos de depuración de memoria. Sospecho que los diseñadores de Java intentaron en primer lugar esta solución (lo cual constituye una de las lecciones aplicándose al diseño software: en realidad no sabemos nada acerca de un sistema hasta que lo probamos con código y obtenemos algo que funcione). También sospecho que descubrieron que esta solución presentaba un rendimiento inaceptable. Para ver lo que sucede en realidad podemos descompilar el código anterior utilizando la herramienta javap, que está incluida en el JDK. He aquí la línea de comandos necesaria: javap -e Concatenation
El indicador -c genera el código intennedio NM. Después de quitar las partes que no nos interesan y editar un poco los resultados. he aquí el código intemledio relevante: public static void main(java.lang.String[]); Code: Stack=2, Locals=3, Args slze~l o: ldc #2; J/String mango
I C++ pernlite al programador sobrecargar los operadores a voluntad. Como esto puede ser a menudo bastante complicado (I'éase el Capínilo 10 de Thinkillg in C+ +. rEdición. Prenticc Hall , 2000), los diseñadores de Java consideraron que era una característica "indeseable" que no habia que incluir en Java. Resulta gracioso que al final terminaran ellos mismos recurriendo a la sobrecarga de operadores y, lo que todavía resulta mas irónico, se da la circunstancia de que la sobrecarga de operadores resultaría mucho mas fácil en Java que en C++. Esto es lo que sucede en Python (I'éa~e WlVW.Prr}¡oll.org) y CII, que disponen de mecanismos tanto de depuración de memoria como de sobrecarga sencilla de los operadores.
13 Cadenas de caracteres 319 2:
astare 1 new #3; I/class StringBuilder dup
3: 6, 7: 10: 12: 15: 16:
invokevirtual #6; / /StringBuilder append: (Stri ng ) aload 1 invokevirtual #6¡ / /StringBuilder append: (String)
19,
lde #7; //String def
21: 24: 26: 29: 32: 33: 36:
invokevirtual #6; bipush 47
37: 40:
invokespecial #4;
//StringBuilder."lt: ()
ldc #5; I/String abe
/ /StringBuilder append: (String)
invokevirtual #8; //StringBuilder append: (I) invokevirtual #9; / /StringBuilder. toString: () astore_2 getstatic #10; / /F ield System. out: PrintStream; aload 2 invokevirtual #11; JI PrintStream.println: (St ringl return
Si tiene expe riencia con el lenguaje ensamblado r, puede que este código le resulte fami liar: las instrucciones como dup e invokevirtual son los equivalentes en la máquina virtual Ja\'3 (NM) al lenguaje ensamblador. Si nunca ha visto lenguaje ensamblador. no se preocupe: lo que hay que observar es que el compi lador introduce la clase java.lang.StringBuilder. No había ninguna mención de StringBuilder en el código fuente. pero el compi lador ha decidido utilizarlo por su cuenta, porque es mucho más eficiente. En este caso, el compilador crea un objeto StringBuilder para crear la cadena de caracteres s. y llama a append( ) cuatro veces, una para cada uno de los fragmentos. Finalmente, in voca a toString() para generar el resultado, que almacena (con a,lore_2) como ,. Antes de dar por supuesto que lo que hay que hacer es uti lizar cadenas de caracteres por todas partes y que el compilador se encargará de que todo sea eficiente. examinemos un poco más en detalle lo que el compilador está haciendo. He aquí un ejemplo que genera un resultado de tipo String de dos maneras: utilizando cadenas de caracteres y realizando la codificación de fonna manual con StringBuilder: jj: stringsjWhitherStringBuilder.java
public class WhitherStringBuilder public String implicit (String [1 fields) String result = ""; for(int i = O¡ i < fields.length¡ i++) result += fields [i] ¡ return result¡ public String explicit (String (] fields) { StringBuilder result ~ new StringBuilder{) ¡ for(int i = O; i < fields.length; i++) result .append(fields [iJ); return result.toString{);
Ahora, si ejecutamos javap -c WitherSlringBuilder. podemos ver el código (simplificado) para los dos métodos diferentes. En primer lugar, impLicit( ): public java.lang.String implicit(java.lang.String[]); Code: O, lde #2; / /String 2, astore - 2 3, iconst O 4, istore - 3 5, iload_ 3
320 Piensa en Java 6: 7:
aload 1 arraylength
8:
if_icmpge 38
11:
new #3; I/class StringBuilder
14:
dup
15: 18: 19: 22: 23: 24: 25: 28:
invokespecial #4; alead 2 invokevirtual #5; alead 1 iload 3
31:
32: 35:
38: 39:
aaload invokevirtual #5; invokevirtual #6 i astare 2 iinc 3, 1 goto 5 aload 2 areturn
// StringBuilder."":() // StringBuilder.append: ()
// StringBuilder.append: () / / StringBuilder. toString: ()
Observe las líneas 8: y 35: , que forman un bucle. La línea 8: rea li za una "comparación entera de tipo mayor o igual que" con los operandos de la pila y salta a la línea 38: cuando el bucle se ha terminado. La línea 35: es una instrucción de salto que vuelve al principio del bucle, en la línea 5:. Lo más importante que hay que observar es que la construcción del objeto StringBuilder tiene lugar dentro de este bucle, lo que quiere decir que obtenemos un nuevo objeto StringBuilder cada vez que pasemos a través del bucle. He aqui el código intennedio correspondiente a explicit(): public java.lang.String explicit(java.lang.String[]); Cede: O: new #3; / /cla ss StringBuilder
3,
dup
4: 7: 8: 9: 10: 11: 12: 13: 16: 17: 18 : 19: 20: 23, 24: 27:
invokespecial #4; astore 2 iconst O istore 3 iload 3 aload 1 arraylength lf lcmpge 30 alead 2 aload 1 iload 3 aaload invokevirtual #5;
30:
31: 34:
/ / StringBuilder." " : ()
/1
StringBuilder.append: ()
pop iinc 3, 1 goto 10 aload 2 invokevirtual #6; areturn
jj StringBuilder.toString: ()
No sólo es el código de bucle más corto y más simple, sino que además el método sólo crea un único objeto StringBuilder. La creación de un objeto StringBuilder explícito también nos pemlite preasignar su tamaño si d isponemos de infonnación adiciona l acerca de lo grande que debe ser, con lo cual no es necesario vo lver a reasignar constantemente el buffel: Por tanto, cuando creamos un método toString( ), si las operaciones son lo suficientemente simples como para que el com· pilador pueda figurarse el sólo cómo hacerlas, podemos generalmente confiar en que el compilador construirá el resultado de una fonna razonab le. Pero si hay bucles, conviene utili zar explícitamente un objeto StringBuUder en el método toString( ), como se hace a continuación:
11 :
stringsjUsingStringBuilder.java
13 Cadenas de caracteres 321 import java . util.*; public class UsingStringBuilder public static Random rand = new Random(47)¡ public String toString () { StringBuilder result = new StringBuilder(" (11); for{int i = Oi i < 25; i++) {
result.append(rand,nextInt{lOO)) ; result .append (",
")
j
result .delete (result .length () -2,
result.append("l ")
result .length () ) ;
i
return result.toString(); public static void main(String(]
UsingStringBuilder usb System.out.println(usb)
args)
{
= new UsingStringBuilder(); i
/ * Output: [58, 55, 93, 11, 22, 4]
61,
6 1,
29,
68,
O,
22,
7,
88,
28,
51,
89,
9,
78,
98,
61,
20,
58,
16,
40,
*///,Observe que cada parte del res ultado se añade con una instmcción append( ). Si tratamos de seguir un atajo y hacer algo como append(a + ": " + e), el compilador saldrá a la palestra y comenzará a construir de nuevo más objetos StringBuilder. Si tiene duda acerca de qué técnica utilizar, siempre puede ejecutar javap para comprobar los resultados. Aunque StringBuilder dispone de un conjunto completo de métodos, incluyendo insert(), replace(), substring( ) e incluso reverse( l, los que generalmente se usan son append( ) y toString( ). Observe el uso de delcte( ) para eliminar la última coma y el último espacio antes de añadir el corchete de cierre. StringBuilder fue introducido en Java SE5. Antes de esta versión, Java utili zaba StringBuffer, que garantizaba la seguridad en lo que respecta a las hebras de programación (véase el Cap ítulo 21 , Concurrencia) y era, por tanto, significativa mente más caro en términos de recursos de procesamiento. Por tanto, las operaciones de manejo de caracteres en Java SE5/6 deberían ser más rápidas. Ejercicio 1:
(2) Analice SprinklerSystem.toString( ) en reusing/SprinklerSystem.java para descubrir si escri bir el
método toString( ) con un método StringBuilder explícito pennitiría abarrar operaciones de creación de objetos StringBuilder.
Recursión no intencionada Puesto que los contenedores estándar Java (al igual que todas las demás clases) heredan en último término de Object, todos ellos contienen un método toString( ). Este método ha sido sustituido para que los contenedores puedan generar una representación de tipo String de sí mismos, incluyendo los objetos que almacenan. ArrayList.toString( ), por ejemplo, recorre los elementos del objeto Arr.yList y llama a toString() para cada uno de ellos: ji : stringsjArrayListDisplay.java impo rt generics.coffee.*¡ import java.util.*¡
public class ArrayListDisplay public static void main(String[] args) { ArrayList coffees = new ArrayList() new CoffeeGenerator(lO)) for (Coffee e coffees.add(c}¡ System . out.println{coffees) i /* Output:
i
322 Piensa en Java [Americano 0, Latte 1, Americano 2, Mocha 3, Mocha 4, Breve S, Americano 6, Latte 7, Cappuccino 8, Cappuccino 9]
* /// ,Suponga que quisiéramos que el método toString( ) im primiera la dirección de la clase. Parece que tendría bastante do hacer referencia simplemente a this :
sen ti~
jj : stringsjlnfiniteRecursion . java jj Recursión accidental.
// {RunByHand} import java.util.*; public class InfiniteRecursion public String toString () { return 11 In f initeRecursion address:
" -+ this -+ " \n ";
} public static void main (Str ing [] args ) { List v = new ArrayList () ; f or(int i = O; i < la; i-+-+) v.add(new InfiniteRecursion() ) ; Sys tem. out .println (v) ;
}
11/ , Si creamos un objeto InfiniteRecursion y luego lo imprimimos, obtendremos una secuencia muy larga de excepciones. Esto también se produce si colocamos los objetos InfiniteRecursion en un contenedor ArrayList e imprimimos dicho contene· dar como aquí se muestra. Lo que está sucediendo es una conversión automática de tipos para las cadenas de caracteres. Cuando decimos: "InfiniteRecursion address:
11
-+ this
El compilador ve un objeto String seguido de un símbolo '+ ' y algo que no es un objeto Slring, por lo que trata de convertir Ihis a String. El compilador reali za esta conversión llamando a toString(), que es lo que produce una llamada recursiva. Si queremos impri mi r la dirección del objeto, la solución es llamar al método toString() de Object. y hace precisamente eso. Por tanto, en lugar de especi ficar Ihis, lo que tendríamos que escribir es super.toString().
Ejercicio 2:
(1) Corrija el error de lnfiniteRecursion.java.
Operaciones con cadenas de caracteres He aquí algunos de los métodos básicos disponibles para objetos String. Los métodos sobrecargados se resumen en una única fila :
Método
Argumentos, sobrecarga
Constructor
Sobrecargado: predetenninado, String, StringBuilder, StringBuffer, matrices char, matrices byte.
l USO
Creación de objetos String.
Número de caracteres en el objeto String.
length( ) charAt( )
índ ice ¡nt
El carácter char en un a posición dentro del objeto String.
getChars( ), gelBytes( )
El principio y el final del que hay que copiar, la matriz a la que hay que copiar, un índice a la matriz de dest ino.
Copia caracteres o bytes en una matriz ex· tema.
13 Cadenas de caracte res 323
Argumentos, sobrecarga
Método IOCbarArray( )
Uso Genera un char[] que contiene los caracteres contenidos en el objeto String.
equals( ), equals-lgnoreCase( )
Un objeto String con el que comparar.
Una comprobación de igualdad de los contenidos de dos objetos String.
compareTo( )
Un objeto St rin g con el que comparar.
El resultado es negativo, cero o positivo dependiendo de la ordenación lexicogrática del objeto String y del argumento. ¡Las mayúsculas y minusculas no so n iguales!
contains( )
Un objeto CharSequence que buscar.
E! resultado es true si el argumento está contenido en el objeto String.
contentEquals( )
Un objeto CbarSequence o St rin gB uffer con el que comparar..
El resultado es true si hay una co rrespondencia exacta con el argumento.
equals lgnoreCase( )
Un objeto String con el que comparar..
El resultado es true si los contenidos son iguales, sin tener en cuenta la diferencia entre mayúsculas y minúsculas.
rcgionM atcbes( )
Desplazamiento dentro de este objeto String, el otro objeto Stri ng junto con su desplazamiento y la longitud para comparar. La sobrecarga aiiade la posibilidad de "ignorar mayúsculas y minúsculas".
Un resultado booleano que indica si la región se corresponde.
starlSWith( )
Objeto String con el que puede comenzar. La sobreca rga aiiade el desplazamiemo dentro de un argumento.
Un resultado booleano que indica si el objeto String comienza con el argumento.
endsWith( )
Objeto Slring que puede ser sufijo de este objeto String.
Un resu ltado booleano que indica si el argumento es un sufijo.
indexOf( ), lastlndexOf( )
Sobrecargado: char, char e índice de inicio, String. String e índice de inicio.
Devuel ve - 1 si el argumento no se encuen tra dentro de este objeto String; en caso contrario, devuelve el índice donde empieza el argumento. lastlndexOf( ) busca hacia atrás el final.
Sobrecargado: índice de micio: Índice de inicio + índice de fin.
Devuelve un nuevo objeto String que conliene el conjunto de caracteres especificado.
concat( )
El objeto String que haya quc concatenar.
Devuel ve un nuevo objeto StriDg que cOllliene los caracteres del objeto String original seguidos de los caracteres del argumento.
replace( )
El antiguo canlcter que hay que buscar, el nuevo carácter con el que hay que reemplazarlo. También puede reemp lazar un objeto CharSequeoce con otro objeto CharSequence.
Devuelve un nuevo objeto String en el que se han efectuado las sustituciones. Utiliza el antigua objeto String si no se encuentra ninguna correspondencia.
substring( ) (también subScquencc(
»
toLowerCase( ) toUpperCase( )
Devuelve un lluevo objeto String en el que se habrán cambiado todas las letras a minúsculas o mayúsculas. Utiliza el antiguo objeto String si no es necesario realizar ningún ca mbio.
trim( )
Devuelve un nuevo objeto String eliminando los espacios en blanco de ambos extremos. Utiliza el antiguo objeto String si no es necesario realizar ningún cambio.
324 Piensa en Java
Método
Argumentos, sobrecarga
Uso
valueDr( l
Sobrecargado: Object, charl! . charll y desplazamiento y recuento, boolean. charo int. long, noat. double.
Devuclvc un objeto String que contiene una representación en fonna de caracteres del argumento. Produce una y sólo una referencia a un objeto String por cada secuencia de caracteres distinta.
intern( )
Puede ver que todo método de Strin g devuelve, cuidadosamente. un nuevo objeto Strin g cuando es necesario cambiar los contenidos. Observe también que, si los contenidos no necesitan modificarse. el método se limita a devol ver una referencia al objeto String original. Esto ahorra espacio de almacenamiento y recursos de procesamiento. Los métodos Strin g en los que están implicadas expres;ones regulares se explican más adelante en este capítulo.
Formateo de la salida Una de las características más esperadas que ha sido finalmente incorporada a Java SES es el formateo de la sa lida al estilo de la instrucción printf() de C. No sólo pemúte esto simplificar el código de salida. sino que también proporciona a los desarrolladores Java una gran capacidad de control sobre el fomlato a la alineación de esa sa lida.:!
printfO La función printf() de e no ensambla las cadenas de caracteres en la forma en que lo hace Java, sino que toma una única cadena deformato e inserta valores en ella. efectuando el f0n11ateo a medida que lo hace. En lugar de utili za r el operador sobrecargado .+. (que el lenguaje C no sobrecarga) para concatenar ellexto entrecomillado y las varia bles. printf( ) utiliza marcadores especiales para indicar dónde deben insertarse los datos. Los argumentos que se insenan en la cadena de formato se indican a continuación mediante una lista separada por comas. Por ejemplo: printf("Row 1:
[%d %fl\n", x, y ) ¡
En tiempo de ejecución. el valor de x se inserta en °/od y el va lor de y se inserta en % f. Estos contenedores se denominan especificadores de formato Y. además de decirnos dónde se debe insertar el valor, también nos infonnan del tipo de va riable que hay que insertar y de cómo hay que fonnatearla. Por ejemplo. el marcador '% d' anterior dice que x es un entero. mientras que ' Olor dice que y es un valor de punto flotante (float o double).
System,out.format( ) Java SES introdujo el mélodo format( l, disponible para los objelos PrintStream o PrintWriter (de los que hablaremos más en deta lle en el Capítulo 18. EIS). enlre los que se incluye System.o"t. El método format() está modelado basándose en la función printf() de C. Existe incluso un método printf( ) que pueden utilizar aquellos que se sientan nos tálgicos, que simplemente invoca format( ). He aquí un ejemplo simple:
JJ :
strings / SimpleFormat. j ava
public class SimpleFormat { public static void main (String (J args ) { int x ::: 5¡ double y : 5.332542, JJ A la antigua usanza: System. o ut .println ( "Row 1, [" + x + " + y + "] " ) ; /1 A la nueva usanza: System. out. format ( "Row 1, [\d UI \ n", x, y ) ,
// o 2 Mark Welsh ha ayudado en la creación de esta sección. asi como en la sección "Análi~is de la entrada".
13 Cadenas de caracteres 325 Systern.out.printf(tlRow 1:
[%d %fl\n",
X,
y);
1* Output : Row 1, Row 1, Row 1,
[5 5. 332542J [5 5.3325421 [5 5. 332542J
* /// ,Puede ver que format() y printf() son equivalentes. En ambos casos, hay una única cadena de ronnata, seguida de un argumento para cada especificador de fonnato .
La clase Formatter Toda la nueva funcionalidad de fonnateo de Java es gestionada por la clase Formatter del paquete java.utU. Podemos COllsiderarFormatter como una especie de traductor que convierte la cadena de fonnato y los datos al resultado deseado. Cuando se crea un objeto Formatter, se le indica a dónde queremos que se manden los resultados. pasando esa infon11ación al constmctor: //: strings/Turtle.java import java.io.*; import java.util.*; public class Turtle private String name; private Formatter f; public Turtle (String name, Formatter f) this.name : name; this.f = fi
{
public void move (int x, int y) { f. format ("%s The Turtle is at (%d, %d) \n", name, x, y); public static void main(String[] args) PrintStream outAlias = System.out¡ Turtle tommy = new Turtle ("Tommy", new Formatter(System.out)) ¡ Turtle terry = new Turtle ("Terry", new Formatter(outAlias))¡ tommy.move(O,O) ; terry.move{4,8)¡ tommy.move(3,4) ¡ terry.move{2, 5) ¡ tommy.move(3,3) ¡ terry.move(3,3) ¡
/* Output: Tommy Terry Tommy Terry Tommy Terry
The The The The The
Turtle Turtle Turtle Turtle Turtle The Turtle
is is is is is is
at at at at at at
(O, O)
(4,8) (3,4) (2,5) (3,3) (3,3)
* /// > Toda la salida representada por tommy va a System.out mientras que la representa por terry va a un alias de Systcm.out. El constructor está sobrecargado para admitir di versas ubicaciones de sa lida, pero las más útil es son PrintStream (como en el ejemplo), OulputStre.m y File. Veremos más detalles sobre esto en el Capitu lo 18, En/rada/salida.
Ejercicio 3: (1) Modifique Turtle.java de modo que envie toda la salida a System.err.
326 Piensa en Java
El ejemplo anterior uti liza un nuevo especificador de fommto, -%s'. Este marcador indica un argumento de tipo Stri ng y es un ejemplo del tipo más simple de especificador de fonnato : uno que sólo tiene un tipo de conversión.
Especificadores de formato Para controlar el espaciado y la alineación cuando se insertan los datos, hacen falta especificadores de fomlato más elaborados. He aquí la sintaxis más general: %[i nd ice_argumentoJ [indicadores] [anchura] [.precisión] conversión
A menudo, será necesario controlar el tamaño mínimo de un campo. Esto puede rea lizarse especificando una anchura. El objeto Form atte r garantiza que un campo tenga al menos una anchura de un cierto número de caracteres, rellenándolo con espacios en caso necesario. De manera predeterminada, los datos se justifican a la derecha, pero esto puede cambiarse incluyendo ;-' en la sección de indicadores. Lo contrario de la anchura es la precisión, que se utili za para especificar un máximo. A diferencia de la anchura, que es aplicable a todos los tipos de conversión de datos y se comporta de la misma manera con cada uno de elJos, precisión tiene un significado distinto para los diferentes tipos. Para las cadenas de caracteres, la precisión especifica el número máximo de caracteres del objeto Str ing que hay que imprimir. Para los números en coma flotante , precisión especifica el número de posiciones decimales que hay que mostrar (el valor predeterminado es 6), efecntando un redondeo si hay más dígitos o añadiendo más ceros al final si hay pocos. Puesto que los enteros no tienen parte fraccionaria, precisión no es aplicable a ellos y se generará una excepción si se utili za el argumento de precisión con un tipo de conversión entero. El siguiente ejemplo utili za especificadores de formato para imprimir una factura de la compra: // : strings/Receipt.java import java . util .*¡ public c lass Receipt private double cotal = O; private Formatter f = new Formatter (S yscem.out ); public void printTitle () { f . format( "% - 15 s %56 %lOs\n", "Item", "Qty", "Price ll )¡ f.format ( "%-15s %56 %lOs\n ", tI _ _ _ _ _ " ) ¡ public void print (St ring name, int qty, double price) { f.format ( "%-15.1Ss %5d %lO .2f \n", name, qty, price); total += price; public void printTotal() f.format ("%-15s %5s %10.2f\n", "Tax", " ", tocal*0.06); f.format("%-15s %5s %10s\n", "", "", "-----"); f. format (" %-155 %55 %10. 2f\n", "Total", total * 1.06); public static void main{String[] arg6) Receipt receipt = new Receipt(); receipt.printTitle{) ; receipt. print ("Jack' s Magic Beans", 4, 4.25); receipt. print ( " Princess Peas", 3, 5.1); receipt .pri nt("Three Bears Porridge", 1, 14.29); receipt.printTotal{) ;
1* Output: Item Jack's Magic Be Princess Peas Three Bears Por Tax
Qty 4 3 1
Price 4.25 5.10 14.29 1. 42
13 Cadenas de caracteres 327
25.06
Como puede ver, el objeto Fo rmatter propo rciona un considerable grado de control entre el espaciado y la alineación, con una notación bastante concisa. Aquí, las cadenas de fannato se copian simplemente con el fin de producir el espaciado apropiado. Ejercicio 4:
(3) Modifique Receipt.java para que todas las anchuras estén controladas por un único conjunto de va lores constantes. El objetivo es poder cambiar fácilmente una anchura modificando un único valor en un determinado lugar.
Conversiones posibles con Formatter Éstas son las conversiones con las que más frecuentemente nos podremos encontrar:
Caracteres de conversión d
Entero (como decimal)
e
Carácter Uni code
b
Valor booleano
s
Cadena de caracteres
f
Coma flotante (como decimal)
e
Coma fl otantes (en notación cientifica)
x
Entero (como hexadecima l)
h
Código IwsJ¡ (as hexadeci mal )
%
Literal " %"
He aquí un ejemplo que muestra estas conversiones en acción: jj: strings;Conversion .java import java.math.*¡ import java.util.*¡ public class Conversion public static void main{String [] args) { Formatter f = new Formatter(System.out)¡ char u = 'a I ¡ System.out.println(tlu = 'a' ti) ¡ f. format ( tls: %s\n", u); JJ f.format("d: %d\n", u) i f.format( " c: %c\n", u) ¡ f.format("b: %b\n", u) ¡ 11 f.formatl"f, %f\n", u); JI f.format("e: %e\n", u) ¡ JI f.format ( "x: %x\n", u); f.format{"h: %h\n", u); int v = 121; System. out. println ("v
121") ;
328 Piensa en Java f.format {"d: %-d \ nll, v ) ; f.format ( "c: %c \ n", v ) ; f.format. ( l1b: %b \ nl1, v ) ; f.format {"s: %s \ n", v ) ¡ 11 f.format l "f, H\n", v ) ; II f.format(lIe: %-e \ n", v}; f.format("x: %x\n", v ) ¡ f.format("h: %h\n", v); Biglnteger w = new Biglnteger(1150000000000000 1' }¡ System.out.println ( "w = new Biglnteger (\ "50000000000000 \ " ) " ) ; f. format ( lid: %d \ nll, w) ; II f.format ( "c: %-c \ n", w) ; f.format ( lIb: %b \ n", w ) ; f.format {"s: %s \ n", w) ; II f.format("f: %f \ n", w) ; II f.format {"e: %e \ n", w ) ; f . format ( " x: %-x \ n ", w) ; f.format ( "h: %h\n", w) ; double x = 179.543; System.out.println(lI x = 179.543 " ); 1/ f.format("d: %d\n " , x); 1/ f.format("c : %c\n", x); f.format("b: %b\n", xl; f.format{"s: %s\n", xl; f.format {"f: %-f \ n", x l ; f.format(lIe: %e\nll, x l ; II f.format ( lIx: %x \ n ll , x l ; f.format ( "h: %-h \ n", x l ¡ Conversion y = new Conversion () ; System . out.println ( "y = new Conversion () " ) ; II f. format ( lid : %d\ n ti, y ) ; II f.format ( "c: %c \ n", y l ; f.format ( "b: %b \ n", y ) ; f.format ( lIs: %s \ n", y ) ; 11 f. format 1" f , H \ n", y ) ; I I f.format("e: %-e\n", y}; 1/ f.format{"x: %x\n", y); f.f o rmat("h: %h \ nll, y ) ; boolean z = false; System.out.println ( "z = false" ) ; 1/ f.format ( "d: %d \ n", z ) ; II f.format ( "c: %c \ n", z ) ; f.format ( "b: %b \ n", z ) ; f . format ( "s: %s \ n", z ) ; /1 f.format l "L H \ n", z ) ; II f.format ( "e: %e \ n", z ) ; II f.format ( "x: %x \ n", z ) ; f.format {"h: %h \ n", z ) ;
1* Output: (Sample ) u = 'a' s: a e: a b: true
13 Cadenas de caracteres 329 h, 61 v = 121 d,
121
e: y b:
true
s:
121 79 79
x: h,
w d,
=
new Biglnteger("SOOOOOOOOOOOOO" ) 50000000000000
true s, 50000000000000 x, 2d79883d2000 b:
h:
8842ala7
x = 179.543 b:
true
s: 179.543 f, 179.543000 e: 1 . 795430e+02 h: lef462c y = new Conversion () b: true
s: Conversion@9cab16 h:
9cab16
z
= false
b:
false
s: false h,
4d5
, /// ,Las líneas comentadas muestran conversiones que no so n vá lidas para ese tipo de variables concreto, ejecutarlas harían que se generará una excepción. Observe que la conve rsión ' b' funciona para cada una de las variables anteriores. Aunque es vá lida para cualquier tipo de argumento. puede que no se comporte C0l110 cabría esperar. Para las primitivas boolean o los objetos de tipo boolean, el resultado será true o false, según corresponda. Sin embargo, para cua lquier otro argumento, siempre que el tipo de argumento no sea nulJ. el resultado será siempre truco Incluso el va lor numérico de cero, que es sinónimo de false en muchos lenguajes (incluyendo C), generará true. de modo que tenga cuidado cuando utilice esta conversión con tipos no booleanos. Existen otros tipos de conversión y otras opciones de especificador de fonnato más extraños. Puede consultarlos en la documentación del JDK para la clase Formatter. Ejercicio 5:
(5) Para cada uno de los tipos básicos de conversión de la tabla anterior, escriba la expresión de formateo más compleja posible. Es decir, utilice todos los posib les especificadores de fornlato disponibles para dicho tipo de conversión.
String.format( ) Java SE5 también ha tomado prestado de e la idea de sprintf(), que es un método que se utiliza para crear cadenas de caracteres. String.format( ) es un método estático que toma los mismos argumentos que el método format() de Formatter pero devuelve un objeto String. Puede resultar útil cuando sólo se necesita invocar una vez a format( ):
11 : strings/DatabaseException.java public class DatabaseException extends Exception { public DatabaseException ( int transactionID, int queryID, String messagel { super(String.format{ti (t%d, q%d) %SU, transactionID, queryID, message});
330 Piensa en Java public static void main(String(] args) try ( throw new DatabaseException(3, 7, "Write failed"); catch(Exception el { System.out . println(e) ;
1* Output: DatabaseException:
(t3, q7) Wri te failed
, ///,Entre bastidores, tod o lo que Striog.format() hace es iostanciar el objeto Forma!!er y pasarle los argumentos que hayamos proporcionado, pero utili zar este método puede resultar a menudo más claro y más fácil que hacerlo de fonna manual.
Una herramienta de volcado hexadecimal Como segundo ejemplo, a menudo nos interesa examinar los bytes que componen un archivo binario utilizando formato hexadecimal. He aquí una pequeña utilidad que muestra una matriz binaria de bytes en un formato he xadecimal legible, utilizando Striog.format() :
11 : net/m i ndview/util/ He x .java pack age ne t . mindview.u t il; impo r t java . io. * ¡ public class Hex { public static String f ormat (byte (] data) { StringBui l der result = new StringBuilde r() ; int n = O; for (byte b , data) { i f (n % 16 == O) r e sult.append(String.format( U%05X: n»); r e sult . app end(String .format( " %02X ", b ) ; n ++ ;
if(n % 16 = = O)
r esult . append(U\n U);
result.append{ u\n U) ; return resul t .toSt ri ng() i public static void main(String[] args) throws Exception if(args . length == O) II Comprobar mostrando este archivo de clase: System.out.println( format(BinaryFile.read{UHex class U») i el se System . out.println( format{BinaryFile.read{new File{args[O] » » ;
/ , Output: 00000 , CA FE 00010 , 00 23 00020, 00 27 00030, 00 2C 0004 O, 31 08 00050, 36 00
(Sample) BA DA DA 00 00 37
BE 00 00 2D 32 07
00 02 28 08 DA 00
00 00 00 00 00 38
00 22 29 2E 33 DA
31 08 DA DA 00 00
00 00 00 00 34 12
52 24 02 02 DA 00
DA 07 00 00 00 39
00 00 2A 2F 15 OA
05 25 08 09 00 00
00 DA 00 00 35 33
22 00 2B 30 DA 00
07 26 DA 00 00 3A
* ///,Para abrir y leer el archi vo binario, este programa presenta otra ut ilidad que se presentará en el Capítulo 18, En/radal salida: net.mindview.utiI.BinaryFile. El método read( ) devuelve el archi vo completo como una matri z de tipo byte.
13 Cadenas de caracteres 331
Ejercicio 6 :
(2) Cree una clase que contenga campos inl. long, noal y double, Cree un método loSlring( ) para esta clase que utilice String.format(), y demuestre que la clase funciona correctamente.
Expresiones regulares Las expresiones regulares han sido durante mucho tiempo parte integrante de las utilidades estándar Unix como sed yawk,
y de lenguajes como Pylhon y Perl (algunas personas piensan incluso que las expresiones regu lares son la principal razón del éxito de Perl) . Las herramientas de manipulación de cadenas de caracteres estaban anterionnente delegadas a las clases Slring. Slrin gB uffer y StringTokenizer de Java. que disponian de funcionalidades relativamente simples si las comparamos con las expresiones regulares. Las expresiones regulares son herramientas de procesamiento de texto potentes y flexibles. Nos pCn11iten especificar. mediante programas, patrones complejos de texto que pueden buscarse en una cadena de entrada. Una vez descubiertos estos patrones, podemos reaccIOnar a su aparición de la fonna que deseemos. Aunque la sintaxis de las expresiones regulares puede resultar intimidante al principio. proporcionan un lenguaje compacto y dinámico que puede emplearse para resolver todo tipo de tareas de procesamiento, comparación, selección, edición y verificación de cadenas de una fonna general.
Fundamentos básicos Una expresión regu lar es una fonna de describir cadenas de ca racteres en ténnmos generales, de modo que podemos decir: "Si una cadena de caracteres contiene estos elementos, entonces se corresponde con lo que estoy buscando", Por ejemplo, para decir que un número puede estar o no precedido por un signo menos, escribimos el signo menos seguido de un signo de interrogación de la fonna siguiente: -?
Para describir un entero. diremos que está compuesto de uno o más digitos. En las expresiones regulares, un dígito se describe mediante '\d', Si tiene experiencia con las expresiones regulares en otros lenguajes, obsenrará inmediatamente la diferencia en la fomla de gestionar las barras inclinadas, En otros lenguajes, '\\' significa: '"Quiero insertar una barra incli nada a la izquierda normal y corriente (literal) en la expresión regular, No le asignes ningún significado especial", En Java, '11' significa: "Estoy insertando una barra inclinada de expresión regu lar, por lo que el siguiente carácter tiene un significado especia!". Por ejemplo, si queremos indicar un dígito, la cadena de la expresión regular será '\\d' , Si deseamos insertar una barra inclinada literal, tendremos que escribir '\\\\', Sin embargo, elementos tales como de nueva línea y de tabulación utilizan una barra inclinada simple: '\n\t', Para indicar " una o más apariciones de la expresión precedente'\ se utiliza un símbolo '+', Por tanto, para decir "posiblemente un signo menos seguido de uno o más dígitos", escribiríamos:
-?\\d+ La fonna más simple de utilizar las expresiones regulares consiste en utilizar la funcionalidad incluida dentro de la clase String. Por ejemplo, podemos comprobar si un objeto String se corresponde con la expresión regular anterior: ji:
strings/lntegerMatch.java
public class IntegerMatch { public static void main(String[] args) { System.out.println("-1234".matches("-?\\d-t")) ; System.out.println(15678 I ,matches("-?\\d-t")) i System.out.println("+911",matches(II-?\\d+ II ) ) ; System.out.println("+911".matches("(-I\\-t)?\\d+")) ; / * Output: true true false true
*/ / /
>
332 Pien sa en Java
Las primeras dos ex pres iones se corres pond en, pero la tercera comi enza con un '+', que es un número legí timo pero que no se aj usta a la ex presión regul ar. Por tanto, necesitamos un a fonna de decir "puede comenza r con un + o un -". En las expresiones regulares. los parén tesis tienen el efecto de agrupar una ex pres ión. y la barra verti cal '1' signifi ca OR (di syunción). Por lanto. ( - 1\\ + ) 7
qui ere decir que esta parte de la cadena de caracteres puede se r un .~ . o un '+' o nada (debido al '? ' ). Pues to qu e el carácter '+ ' ti ene un signifi cado especial en las ex presiones regul ares. es necesario introducir la secuencia de esca pe '\\' para que aparezca como un ca rácter normal dentro de la expresión. Una herram ienta útil de ex presiones regul ares incorporada en String es split(), que signjfi ca " partir esta cadena de caracteres juslO donde se produ zcan las co rrespondencias con la ex presión regul ar indicada". /f : strings f Splitting.java import java.util. * ¡ pub l ic class Splitting ( public static String knights "Then, when you have found the shrubbery, you must " + "cut down the mightiest tree in the forest... It + "with ... a herring! " ¡ public static void split(String regex) System.out.println( Arrays . toString{knights.split{regex) ) ) ; public static void main (String [] args) { split ( " II ) ¡ II No tiene porqué contener caracteres regex split ( " \\ w+" ) ¡ II Caracteres no pertenecientes a una palabra split(lIn \\ W+ ") ¡ 1/ 'n ' seguida de caracteres no II pertenecientes a una palabra / * Output : [Then" when, you, have, found, the, shrubbery" you, must, cut, down , the, migh tiest, tree, in, the, foresto .. , with .. . , a, herring!] [Then, when, you, have, found, the, shrubbery, you, must, cut, down, the, mightiest, tree, in, the, forest, with, a, herring] (The, whe, you have found the shrubbery, you must cut dow, the mightiest tree i, the forest. .. wi th. .. a herring!]
* /// , En primer lugar, observe que puede utili zar caracteres nomlales como expresiones regulares, una expresión regul ar no tiene porqué co nt ener ca racteres especiales. como podemos ver en la primera llamada a split(), que simple mente efectúa la parti ción de acuerdo con los espacios en blanco. La segunda y tercera llamadas a split() utili zan '\W ', que represent a caracteres que no pertenezcan a palabras (la versión en minúsculas, " w', representa un carácter perteneciente a una palabra); podrá ver que los signos de puntuación han sido eliminados en el segundo caso . La tercera llamada a split( ) dice. " la letra n seguida de uno o más caracteres que no pertenezcan a palabras". Podrá ver que los patrones de di visión no aparece n en el resultado. Una versión sobrecargada de String.split( ) nos pennite limitar el número de di visiones que hayan de producirse. La última de las herramieOlas de expresiones regulares incorporada en String es la de sustitución . Podemos sustiulir la primera apari ción o todas elJas: // : strings / Replacing . java import static net.mindvie w.util.Print. * ¡ public class Replacing { static String s = Splitting . knights ¡ public static void main(String[] args) print (s. replaceFirst ( " f \ \ w+ I t , tt located " ) ) ¡
13 Cadenas de caracteres 333 print(s.replaceAll("shrubbery!treelherring","banana ll
/ * Output: Then, when yau have forest ... with . .. a Then, when you have forest ... with ... a
)
i
located the shrubbery, you must cut down the mightiest tree in the herring! found the banana, you must cut down the mightiest banana in the banana!
* /// , La primera expresión se corresponde con la letra f seguida de uno o más caracteres de palabras (observe que el carácter w está en minúscula esta vez). Sólo sustituye la primera correspondencia que encuentra, por lo que la palabra "found" ha sido sustituida por la palabra "Iocated". La segunda exp resión se corresponde con cualquiera de las tres pa labras separadas por las barras verticales que representan la operación OR, y sustituye todas las correspondencias que encuentra. Más adelante veremos que las expresiones regulares que no son de tipo Strin g disponen de herramientas de sustitución más potentes; por ejemplo, se pueden in vocar métodos para llevar a cabo las sustituciones. Las expresiones regulares que no son de tipo String también son significativamente más eficientes cuando hace falta utilizar la ex presión regular más de un a vez.
Ejercicio 7:
(S) Utilizando la documentación de java. util.regex.Pattern como referencia, escriba y pruebe una expresión regu lar de prueba que compruebe una frase para ver si comienza con una let ra mayúscula y tenn ina con un punto.
Ejercicio 8:
(2) Divida la cadena Splitting.knights por las pa labras "the" o "you".
Ejercicio 9:
(4) Utilizando la documentación de j ava. util.regex.Pattern como referencia, sustituya todas las vocales de Splitting. knights por guiones bajos.
Creación de expresiones regulares Podemos comenzar a aprender ex presiones regulares con un subconjunto de las estrucUlras posibles. En la documentación del JDK correspondiente a la clase Pattern de java.util.regex podrá encontrar la lista completa de las estrucUlras que pueden emplearse para construir las expresiones regulares.
Caracteres B
El ca rácter específico B
\xhh
Carácter con el valor hexadecimal Oxhh
luhh hh
El ca rácter Unicode con la representación hexadecimal Oxh hh h
II
Tabulador
In
Nueva línea
Ir
Retomo de carro
Ir
Avance de página
le
Escape
La potencia de las expresiones regu lares comienza a hacerse patente cuando se definen clases de caracteres. He aquí algunas fonnas típ icas de crear clases de caracteres, junto con algunas clases predefinidas:
334 Piensa en Java
Clases de caracteres Cua lqui er carácter
label
Cualquiera de los caracteres a , b o (' (lo mi smo que a lblc)
I' abel
Cualquier carácter excepto a , b y e (negación)
la-zA-ZI
Cualquier carácter de la a a la z o A a la Z (rango)
labcl hij ll
Cualquiera de
la-z&& lhijll
Puede ser h. i o j (intersección)
Is
Un carácter de espaciado (espacio. tabulador, nueva linea, avance de pág ina. retomo de carro)
IS
Un carácter que no sea de espaciado (I"\s))
Id
Un dígito numérico (0-91
ID
Un carácter que no sea un dígito 1"'0-91
\w
Un carácter de palabra (a-zA-Z_0-91
\W
Un carácter que no sea de pa labra ("'\wl
a.b.c,b,i.j (lo mismo que . lblelbliÜ) (unión)
Lo que se muestra aquí es sólo un ejemplo; consultando la págma de documentación del JDK correspondiente a java.u til.regex. Pattern podrá conocer todos los posibles patrones de expresiones regulares.
Operadores lógicos XV
X seguido de Y
XIY
XoY
(X)
Un grupo de captllra. Puede referirse posteriormente al i-ésimo grupo capturado en la expresión mediante \i.
Localizadores de contorno
,
Comi enzo de línea
$
Fin de linea
lb
Frontera de palabra
\B
Frontera de no palabra
IG
Fin de la correspondencia anterior
Por ejemplo, cada una de las siguientes ex presiones pennite localiza r la secuencia de caracteres "Rudo lph": jj , strings j Rudolph.java public class Rudolph { public static veid main (String [] args l { fer (String pattern : new String [] { "Rudolph",
13 Cadenas de caracteres 335 "[rRludolph", "[rR] [aeioul [a-z]ol.*", "R.*" }) System. out .println (II Rudo lph" . mat c hes (pattern ) ) ;
/ * Ou tput: t rue true true t r ue
* /// , Por supuesto, el objetivo no debe ser crear la expresión regular más complicada sino la que sea más simple y baste para realizar la tarea que tengamos entre manos. Una vez que comience a escribir expresiones regulares, podrá ver cómo a menudo conviene referirse a los ejemplos de código escritos anterionnente, para facilitar la escritura de nuevas expresiones regu-
lares.
Cuantificadores Un cuantificador describe la forma en que un patrón absorbe el texto de entrada:
•
Avaricioso: los cuantificadores son avariciosos a menos que se los modifique de alguna manera. Un expresión avariciosa trata de encontrar el máximo número posible de correspondencias para el patrón indicado. Una causa bastante común de problemas consiste en suponer que el patrón sólo se corresponderá con el primer grupo de caracteres, cuando lo cierto es que se trata de un patrón avaricioso y continuará procesando texto hasta que baya logrado establecer una correspondencia con la cadena de caracteres más larga posible.
•
Reluctante: especificado con un signo de interrogación, este cuantificador hace que la correspondencia se establezca con el número mínimo de caracteres necesario para satisfacer el patrón. También se denomina perezoso, de correspondencia mínima o no avaricioso.
•
Posesivo: en la actualidad, este lipo de cuantificador sólo está di sponible en Java (no en otros lenguajes) y es más avanzado, por lo que es posible que no lo utilice al principio. A medida que se aplica una expresión regular a una cadena de caracteres, la expresión genera múltiples estados para poder retroceder si la correspondencia falla. Los cuantificadores posesivos no conservan dicbos estados intennedios, evitando así el retroceso. Pueden utilizarse para impedir que una expresión regular quede fuera de control y también para hacer que se ejecute de manera más eficiente. Avaricioso
I Reluctante
Posesivo
Correspondencia con
X?
X??
X?+
X, una o ninguna
X*
X*?
X*+
X, cero o más
X+
X+?
X++
X, una o más
X{n}
X{n}?
X{n}+
X, exactamente n veces
X{n,}
X{n,}?
X{n, }+
X, al menos n veces
X{n,m}
X{n,m} ?
X{n,m} +
X, al menos n pero no más de m veces
Recuerde que la expresión 'X' necesitará a menudo encerrarse entre paréntesis para que funcione de la fomla deseada. Por ejemplo:
abe ... podría parecer que se debería corresponder con la secuencia 'abc' una o más veces, y si la aplicamos a la cadena de entrada ' abcabcabc' , obtendremos de hecho tres correspondencias. Sin embargo, lo que la expresión dice en realidad es: "locali za ' ab' seguido de una o más apariciones de 'c '''. Para buscar correspondencias con la cadena completa 'abc' una o más veces, debemos decir:
336 Piensa en Java (abc)+
Resulta bastante fácil equivocarse al utilizar expresiones regulares; se trata de un lenguaje completamente ortogonal a Ja va, que funciona sobre éste y que presenta diferencias con el lenguaje de programación.
CharSequence La interfaz denominada C harScquence establece una definición generalizada de una secuencia de caracteres abstraída de las clase CharBuffer, String, StringBuffer o StringBuilder: interface CharSequence charAt{int i) i length () ; subSequence(int start, int end) toString() ¡
i
Dichas clases implementan esta interfaz. Muchas operaciones con expresiones regu lares toman argumentos de tipo CharSequence.
Pattern y Matcher En general, lo que se hace es compilar objetos de expres ión regular en lugar de emplea r las utilidades String, que so n bastante limitadas. Para ello, importamos java.util.regex, y luego compilamos una expresión regu lar utilizando el método static Patter n.compile( j . Esto genera un objeto Pattern basado en su argumento Strin g. Para util izar el objeto Pattern, lo que se hace es invocar el método matcher( ), pasándole la cadena de caracteres que queremos buscar. El método matcher( ) genera un objeto Matcher, que tiene un conjunto de operaciones de entre las cuales podemos elegir (puede consultar todas las operaciones en la documentación del JDK correspondiente a .util.regex.Matcher). Por ejemplo, el método replaceAII() sustituye todas las correspondencias por el argumento que se proporcione. Vamos a ver un primer ejemplo: la clase sigui ente puede utili zarse para probar expresiones regulares con una cadena de entrada. El primer argumento de la línea de comandos es la cadena de entrada en la que hay que buscar las correspondencias, seguida de una o más expresiones regulares que haya que aplicar a la entrada. En Un ix/Linux, las exp resiones regulares deben estar entrecomilladas en la línea de comandos. Este programa puede resultar úti l para probar expresiones regulares mientras las construimos con el fin de comprobar que esas expresiones establecen las correspondencias deseadas.
11 :
strings/TestRegularExpression.java
II Permite probar con facilidad expresiones regulares. II {Args : abcabcabcdefabc abc+ "(abc)+" " (abc ){2,}" } 11
11
import java.util.regex. *¡ import static net.mindview.util.Print.*¡ public class TestRegularExpression public static void main{String[] args) if(args.length < 2) { print ( "Usage: \njava TestRegularExpression "characterSequence regularExpression+") ¡ System.exit{Q) ;
+
print(IIInput: \"" + args[Ol + "\"") i for (String arg : args) { print (" Regular expression: \"" + arg + "\"" ) i Pattern p = Pattern.compile(arg) i Matcher m = p.matcher {args[O]); while(m.find()) ( print("Match \ "" + m.group() + "\" at positions " + m.start() + 11_" + (m .end () 1));
13 Cadenas de caracteres 337
} l ' Output, Input: "abcahcahcdefabc" Regular expression: "ahcabcabcdefahc" Match "ahcabcabcdefabc" at positions 0-14
Regular expression:
"abc+"
Match "abe" at positions 0-2 Match "abe" at positions 3-5
Match lIabc" at positions 6-8 Match "abe" at positions 12-14
Regular expression: " (abe) +" Match lIabcabcabc" at positions 0-8 Match "abe" at positions 12-14 Regular expression: " (abc){2 ,}u Match "abcahcabc " at positions 0-8
' 111 ,Un objeto Pattcrn representa la versión compi lada de una expresión regular. Como hemos visto en el ejemplo anterior, podemos utilizar el método matcher() y la cadena de entrada para generar un objeto Matcher a partir del objeto Pattern compilado. Pattern también tiene un método estático: static boolean matches{String regex,
CharSequence input)
para comprobar si regex se corresponde con el objeto input de tipo CharSequence utilizado como entrada, y un método split() que genera una matriz de tipo String después de descomponer la entTada según las correspondencias es tabl ecidas con la exp resión regular regex. Podemos generar un objeto Matcher invocando Pattern.matcher() con la cadena de entrada como argum ento. Después el objeto Matchcr se utiliza para acceder a los resultados, utili za ndo métodos para evaluar si se establecen o no diferentes tipos de correspondencias: boolean boolean boolean boolean
matches() lookingAt() f ind () find(int start)
El método matches( ) tendrá éxito si el patrón se corresponde con la cadena de entrada completa. mientras que lookingAt( ) tendrá éx ito si la cadena de entrada, comenzando por el pri ncipio, pe rmite establecer una correpondencia con el patrón. Ejercicio 10:
(2) Para la frase ·'Java now has expresiones regulares" evalúe si las siguientes expresiones pennitirán localizar la correspondencia: . . Java \8reg. *
n.w\s+h(ali)s s? s' s+
S(4}
S(l} . s(a,3}
Ejercicio 11 :
(2) Apl ique la expresión regular (?i)
(( A laeiouJ)
I (\s+ laeiouJ)) \ w+? laeioul \b
a "Arline ate eight apples and one orange while Anita hadn' t
any"
find( ) Matchcr.find() puede utilizarse para descubrir múltiples correspondencias de patrón en el objeto CharSequence al cual se aplique, Por ejemp lo:
338 Piensa en Java
JI: strings/Finding.java import java.util.regex.*¡ import static nec.mindview.util.Print.*;
public class Finding { public static void main (String [] args) { Matcher ID = Pattern.compile{"\\w+"l .matcher(UEvening is full of the linnet's wings ll ) ;
while (m. Eind () ) princnb(m.group() + "
")i
print() ; int i :: O i
whilelm.find(i)) printnb (m .group () i++;
+ 11
11);
/ * Output: Evening is full of the linnet s wings Evening vening ening ning ing ng 9 i5 is s full full ull 11 1 of of f
the the he e linnet linnet innet nnet net et t
s s wings
w~ngs
ings ngs 9S s
* /// ,El patrón '\\W+' divide la entrada en palabras. find() es como un iterador, que se desplaza hacia adelante a través de la cadena de caracteres de entrada. Sin embargo, la segunda versión de find() puede aceptar un argumento entero que le dice cuál es la posic ión del carácter en el que debe comenzar la búsqueda; esta versión rein icializa la posición de búsqueda con el valor de su argumento, como puede ver analizando la salida.
Grupos Los grupos son expresiones regulares delimitadas por paréntesis y a las que luego se puede hacer referencia utilizando su número de grupo. El grupo O indica la expresión completa, el grupo 1 es el primer grupo entre paréntesis, etc. Por tanto, en A(B(C))D
existen tres grupos: el grupo O es ABCD, el grupo 1 es BC y el ¡''fUpO 2 es C. El objeto Malcher dispone de mélodos para proporcionamos información acerca de los grupos: public int gro up Co unl( ) devuelve el número de grupos que hay en el patrón. El grupo O no se incluye dentro de este recuento. public Slring group( ) devuelve el grupo O (la correspondencia completa) de la operación anterior de establecimiento de correspondencias (por ejemplo, find ( )). public String gro up(inl i) devuelve el número de grupo indicado dentro de la operación de establecimiento de correspondencias anterior. Si esa operación ha tenido éxito, pero el grupo especificado no se corresponde con ninguna parte de la cadena de entrada, devuelve el valor null . public inl slarl(in t group) devuelve el índice de inicio del grupo encontrado en la operación anterior de establecimiento de correspondencias. public int end(int group) devuelve el índice del último carácter, más uno, del grupo encontrado en la anterior operación de establecimiento de correspondencias.
He aquí un ejemplo: // : strings/Groups.java import java.util.regex.*¡ import static net.mindview.util.Print.*¡ public class Groups { static public final String POEM
13 Cadenas de caracteres 339 "Twas brillig,
and the slithy toves\n " +
"Did gyre and gimble in the wabe . \n" + "All mimsy were the borogoves , \n" + "And the mame raths outgrabe. \n\nll + "Beware the Jabberwock, rny son, \n" + "The jaws that bite, the claws that catch. \ n" + IIBeware the Jubjub bird, and shun\n" + "The fru mious Bandersnatch ." i public static void main(String[] argsl Matcher ID ~
Pattern. compile (" (?ml (\ \8+1 \ \8+ ( (\ \8 +1\ \ 8+ (\ \8+11 $" 1 .matcher (POEM) i
while(m.find(11 ( for (int j = O; j <= m.groupCount{); j++) printnb(" [" + m.group(j) + lO] " ) ; print ()¡
1* Output : [the slithy taves] [the] [sl ithy taves] [slithy] [taves] [in the wabe.l [in] [t he wabe.] [the] [wabe.l
[were the borogoves,] [werel [the borogoves,] [the] [borogoves,] [mame raths out grabe .] [mame] [raths out grabe .] [raths] [out grabe . [Jabberwo ck, my son,] [Jabberwock,] [my son,] [rny] [son,] [cl aws that catc h. ] [c laws] [that catc h.] [that] [catch . ] [bird , and shun] [bird,] [and shun] [and] [shunl [The frum ious Bandersnatch.] [Thel [frumious Bandersnatch.] [ frumi ousl [Bandersnatch.]
*///,Este poema es la primera parte de "Jabberwocky", de Lewis Carroll extraído del libro A través del espejo. Puede ver que el patrón de expresión regular tiene una serie de grupos entre paréntesis, compuestos de cualquier número de caracteres que no sea de espaciado ('18+' ) seguido de cualquier número de caracteres de espaciado ('Is+'). El objetivo es capturar las tres últimas palabras de cada línea, el final de una línea está delimitado por '$'. Sin embargo, el comportamiento normal consiste en hacer corresponder "$' con el final de la secuencia de entrada comp leta, por lo que es necesario decir expLícitamente a la expresión regular que preste atención a los caracteres de nueva línea desde dentro de la entrada. Esto se consigue con el indicador de patrones '(?m)' al principio de la secuencia (los indicadores de patrones los veremos enseguida). Ejercicio 12:
(5) Moditique Groups.java para contar todas las palabras que no empiecen con una letra mayúscul a.
start( ) y end( ) Después de una operación de establecimiento de correspondencias que haya pennitido encontrar al menos una correspondencia, start() devuelve el índice de inicio de la correspondencia anterior, mientras que cnd() devuelve el índice del último carácter de la correspondencia mas uno. Al invocar start() o end() después de una operación de localización de correspondencias que no haya tenido éxito (o antes de intentar una operación de localización de correspondencias), se genera la excepción lllegalStateException. El siguiente programa también ilustra los métodos matcbes() y lookingAt( )3 jj : stringsjStartEnd.java import java. uti l.regex.*¡ import static net.mindview.util.Print.*¡ public class StartEnd { public static String input "As long as there is injustice, whenever a \ n" + "Targathian baby cries out, wherever a distress \ n" + "signal sounds among the stars ... We I 11 be there. \ n" + "This fine ship, and this fine crew ... \ n" + "Never give up! Never surrender!"¡ 1 El texto indicado es una cita de uno de los discursos del Comandante Taggart en Gala-cy Quest.
340 Piensa en Java private static class Display { private boolean regexPrinted = false; priva te String regex; Display (String regex) { this . regex = regex; void display(String message) { if (! regexPrinted) { print (regex) ; regexPrinted = true; print (message);
static void examine(String s, String regex) Display d new Display(regex); Pattern p = Pattern.eompile(regex); Matcher m = p.matcher(sl; whilelm.find() ) d.display("findO '" + m. group() + '" start = "+ m.start{) + " end = " + m.end(»; if(m.lookingAt{» II No reset(} necessary d. display (" lookingAt () start = " + m.start() + " end = " + m. end(»; if(m.matches(» II No reset() necessary d. display ("matehes () start + rn.start() + " end = " + m.end(»; public statie void mai n {String[] args) for(String in : input.split("\n"» { print (" input : ti + in); for{String regex : new String[) {H\\w*ere\\w*" , "\\w*ever", "T\\W+", "Never. * ?!"}) examine {in, regex) ;
1* Output: input : As long as there is injustice, whenever a \w *ere\w* findO 'there' start ::: 11 end = 16 \w *ever find() 'whenever' start 31 end = 39 input : Targathian baby cries out, wherever a distress \w*ere\w* find () 'wherever' start 27 end 35 \w*ever find () 'wherever' start 27 end 35 T\w+ find () 1 Targathian' start O end = 10 lookingAt() start = O end 10 input: signal sounds among the stars . . . We'll be there. \w*ere\w * find() ' there' start = 43 end = 48 input: This fine ship, and this fine crew . . . T\w+ find () 'This ' start = O end = 4 lookingAt{) start = O end = 4 input : Never give up! Never surrender! \w *ever f ind () 'Never' start O end = 5 f ind () 1 Never' start 15 end = 20
13 Cadenas de caracteres 341 lookingAt() start = O end = 5 Never. *?! find() 'Never give up!' start O end = 14 find() 'Never surrender!' start 15 end = 31 looki ngAt () start O end = 14 matches () start = O end = 31
-/1/,Observe que find () permite localizar la expresión regula r en cualquier lugar de la entrada, mientras que lookingAt() y matchcs() sólo tienen éxito en la búsqueda si la expresión regu lar se corresponde desde el pri nc ipio de la entrada. Mientras que matc hes( ) sólo tiene éx ito en la búsqueda si loda la en trada se corresponde con expresión regular, lookingAt()4 tiene éxito en la búsq ueda au nque sólo se corresponda la expresión reg ula r con la primera parte de la entrada. Eje rcici o 13:
(2) Modifique Start End.java pa ra que ut ilice Gro ups.PO EM como entrada, pero siga produciendo resultados positivos para find (), lookingAt( ) y matches( ).
Indicadores de Pattern Hay un método compile( ) altemativo que acepta indi cadores que afectan al co mportamiento de búsqueda de co rrespondencias: Pattern Pattern.compile{String regex,
int flag)
donde flag puede ser una de las siguientes constantes de la clase Pattero :
Efecto
Indicador de compilación l'attc rn .CANO N_EQ
Se cons idera que dos caracteres se corresponden si y sólo si sus descomposiciones canónicas completas 10 hacen. Por ejemplo, la expresión " u003F' se corresponderá con la cadena o?, cuando se especifique este indicador. De manera predeterminada. la búsqueda de correspondencias no tiene en cuen ta la equivalencia canónica.
Patte rn.CASE_INSENS ITl VE
Por omisión. la búsqueda de correspondencias sin distinción de mayúsculas y minúsculas presupone que sólo se están util izando caracteres del conjunto de caracteres US-ASC II. Este indicador permite establecer una correspondencia con el patrón sin tener cn cuenta mayúsculas o minúsculas. Puede habilitarse la búsqueda de correspondencias Unicode sin distinción de mayúsculas y minúsc ulas especificando el indicador UN ICOD E_ CASE en conjunción con este indicador.
(?i)
Patte rn .COM M ENTS (?x)
En es te modo. se ignoran los caracteres de espaciado, y también se ignoran los comentarios incnlstados que comicncen con # hasta el final de la línea. También puede habilitarse el modo de líneas Unix mediante la expresión de indicador incnlstado.
Pattern.DOTALL (?s)
En este modo, la expresión'.' se corresponde con cualquier carácter, incluyendo el tenninador de línea. Por omisión, la expresión'.' no se corresponde con los tenninadores de línea.
Pattern.M ULTI U NE (? m)
Patte rn.UN ICOD E_CASE (?u)
Pancrn.UNIX _ LI NES (?d)
-
-En el modo multilínea. las expresiones
'A' y 'S' se corresponden con el principio y el final de una línea. respectivamente. 'A' también se corresponde con el principio de la cadena de entrada y 'S' lo hace con el final de la cadena de entrada. De manera predetenninada. estas expresiones sólo se corresponden con el principio y el final de la cadena de entrada completa.
La correspondencia sin distinción de mayúsculas y mi núsculas, si está habi litada por el indicador CASE_lNSENSITIV E. se realiza de manera coherente con el estándar Unicode. De manera predetenninada, la correspondencia sin distinción de mayúsculas y minúsculas presupone que sólo se están buscando correspondencias con ca racteres del conjunto de caracteres US-ASCI 1. En este modo, sólo se reconoce el tenni nador de línea '\n ' en el comportamien to de ;.', ,'" y '$ '.
No sé por qué dieron este nombre a dicho método ni a qué refiere ese nombre. Pero resulta reconfortante saber que quienquiera que sea que inventa esos nombres de métodos tan poco intuitivos continúa empleado en SUIl y que su aparente politica de no revisar los diseños de código sigue estando vigente. Perdón por el sarcanno, pero es que este tipo de cosas empiezan a cansar después de unos cuamos años.
4
342 Piensa en Java
De especial utilidad entre todos estos indicadores son Pattern.CASE_INSENSITlVE, Pattern.MULTlLlNE y Pattern.COMMENTS (que resulta útil para mejorar la cla ridad y/o con propósitos de dcoumentación). Observe que el comportamiento de la mayor parte de los indicadores puede obtenerse también insertando en la expresión regular los caracteres entre paréntesis que se muestran en los indicadores de la tabla, j usto antes del lugar donde se quiera que ese modo tenga efecto. También podemos combinar el efecto de estos y otros indicadores mediante una operación "OR"
el'):
11 : strings / ReFlags.java import java.util.regex. * ; public class ReFlags { public static void main (String [) args ) { Pattern p = Pattern . comp i le ( lI"'java lt , Pattern . CASE_ I NS ENSITIVE I Pattern . MULTILINE ) ; Matcher m = p.matcher ( Itjava has regex \ nJava has regex \ n ll + IIJAVA has pretty good regular expressions \ n ll + "Regular e x pressions are in Java lt ) ; while (m.find () ) Sys t em . out .prin tln {m.group {)) i
1* Output: java Java J AVA
* /// ,Esto crea un patrón que se corresponderá con las líneas que comiencen por "java," "Java," "JAVA," etc., y que intentará buscar una correspondencia con cada línea que fonne parte de un conjunto multilínea (correspondencias que comienzan al principio de la secuencia de caracteres y a continuación de cada terminador de línea contenido dentro de la secuencia de caracteres). Observe que el método group() sólo devuel ve la porción con la que se ha establecido la correspondencia.
split( ) split() divide una cadena de caracteres de entrada en una matri z de objetos Str ing, utili zando como delimitador la expresión regular. String[] String[]
split {CharSequence input ) split {CharSequence input, int limit )
Ésta es una forma cómoda de descomponer el texto de entrada utili zando una frontera di visori a común : 11 : strings /Sp litDemo.java import j ava.util.regex.*; import java . util.*. import static net . mindview . util.Print. * ¡ publi c c lass SplitDemo { public static void main (String [] args ) { String input = "This! !unusual use! ! o f e x c l amation! !poi n ts tl print (Arrays . toStri ng ( Pattern . compile ( II ! ! 11 ) . split(input ))) ¡ II Hacer sólo las t r es p r imeras : print(Arrays . toString ( Pattern . compile ( " !!" ) .split (input, 3 ))) ¡
1* Output : [This, unusual us e , o f e xclamation, pointsl [This, unusual use, of exclamation! !points]
* /// ,-
;
13 Cadenas de caracteres 343
La segunda forma de split( ) limita el número de di visiones que pueden tener luga r. Ejercicio 14:
(1) Escriba de nuevo SplitDemo utilizando String.split( ).
Operaciones de sustitución Las expresiones regulares resultan especialmente útiles para sustituir texto. He aquí los métodos disponibles: rcplaceFirst(String rcplacement) sustituye por replacement la primera parte que se corresponde de la cadena de caractercS.
replaceAII(StriDg replacement) sustituye por replacemeDt todas aquellas partes que se correspondan en la cadena de caracteres de entrada. appendReplacement(StringBuffer sbuf, String replacement) reali za sustituciones paso a paso en sbuf, en lugar de sustinJir sólo la primera o todas ellas, como sucede con replaceFirst() y rcplaceAII(), respecti vamente. Éste es un método muy importante, porque permite invocar métodos y realizar otros tipos de procesamiento para generar la cadena de sustitución replacement (replaceFirst() y replaceAII() sólo pueden utilizar cadenas de caracteres fija s para la sustitución). Con este método, podernos separar los grupos mediante programa y crear potentes rutinas de sustitución. appendTail(StringBuffcr sbuf, String replacement) se invoca después de UDa o más invocaciones del método appendReplacement() para copiar el resto de la cadena de caracteres de entrada. He aquí un ejemplo que muestra el uso de todas las operaciones de sustitución. El bloque de texto comentado al principio del programa es extraído y procesado con expresiones regulares para usarlo como entrada en el resto del ejemplo:
/1: strings/TheReplacements.java import java.util.regex. * ¡ import net.mindview.util . *¡ import static net.mindview.util.Print. * ¡ /*! Here's a block of text to use as input to the regular express ion matcher. Note that we'll first extract the block of text by looking tor the special delimiters, then process the extracted block. ! * I public class TheReplacements public static void main(String(] args) throws Exception { String s ::: TextFile. read ( UTheReplacements. java") ¡ II Establecer correspondencia con el bloque de texto con II comentarios especiales mostrado anteriormente: Matcher mlnput ::: Pattern.compile("I\\*! (.*) !\\ * /". Pattern.DOTALL) .matcher(s) ¡
if(mlnput.find()) s ::: mlnput.group(l)¡ II Capturado por paréntesis Sustituir dos o más espacios por un único espacio: s ::: s.replaceAII(to {2, }II, " "); II Eliminar dos o más espacios al principio de cada II línea. Hay que habilitar el modo MULTILíNEA: s = s.replaceAII("{?m) '" +", ""J¡ print(s) ¡ s = s. replaceFirst ( " [a eiou] ". " (VOWEL1J 11) ; StringBuffer sbuf ::: new StringBuffer{); Pattern p ::: Pattern. compile (11 [aeiou] ") ¡ Matcher m = p.matcher(s); II Procesar la información de localización a medida II que se realizan las sustituciones:
II
while (m . find ())
344 Piensa en Java m.appendReplacement {sbuf, m.group () .toUpperCase ()) i // Insertar el resto del texto: m.appendTail (sbuf) i print (sbuf ) ; / * Output: Here's a block of text to use as input to the regular expression matcher. Note that we'll first extract the block of text by looking for the special delimiters, then process the extracted block. H{VOWEL1 ) rE' s A blOck Of tExt tO UsE As InpUt tO thE rEgUlAr ExprEssIOn mAtchEr. NOtE thAt wE'll fIrst ExtrAct thE blOck Of tExt by lOOkIng fOr thE spEcIAl dEllmItErs, thEn prOcEss thE ExtrActEd blOck.
*// / ,-
El archivo se abre y se lee utilizando la clase TextFiJe de la biblioteca net.mindview.util (el código correspondiente se mostrará en el Capitulo 18, E/S). El método estático read() lee el archivo completo y lo devuelve como objeto Slrin g. mlnput se crea para corresponderse con todo el texto (observe los paréntesis de agrupamiento) comprendido entre '{*! ' y ' !*/'. Después, los conjuntos de más de dos espacios seguidos se reducen a un único espacio y se eliminan todos los espacios situados al principio de cada línea (para hacer esto en todas las líneas y no sólo al principio de la cadena de entrada, es necesario habilitar el modo multilínea). Estas dos sustituciones se realizan con el método equivalente (pero más cómodo, en este caso) replaceAII() que fom1a parte de Slring. Observe que, como cada sustitución sólo se emplea una vez en el programa, no se produce ningún coste adicional por hacerlo así en lugar de precompilar la operación en fonna de un objeto Pattern . replaceFirst() sólo sustituye la primera correspondencia que encuentre. Además, las cadenas de sustitución en replacefirst() y replaceAII() son simplemente literales, por lo que si queremos realizar algún procesamiento en cada sustitución, no nos sirven de ninguna ayuda. En dicho caso, tendremos que utilizar appendReplacement(), que nos pennite escribir cualquier código que queramos para reali zar la sustitución. En el ejemplo anterior, se selecciona y procesa un grupo (en este caso, poniendo en mayúscula la vocal encontrada por la expresión regular) a medida que se constmye el objeto sbuf resultante. Nonnalmente, lo que haremos será recorrer toda la entrada y hacer todas las sustituciones y luego invocar a appendTail(), pero si queremos simular replaceFirst() (o una operación de "sustitución de n apariciones"), basta con hacer la sustitución una vez y luego invocar appendTail() para insertar el resto de la infonnación en sbuf. appendReplacement() también nos permite hacer referencia directamente en la cadena de sustitución a los grupos capulradas mediante la notación "$g", donde 'g' es el número de grupo. Sin embargo, este método sólo sirve para tareas de procesamiento simples y no nos varía los resultados deseados en el programa anterior.
reset( ) Podemos aplicar un objeto Matcher existente a una nueva secuencia de caracteres utili zando los métodos reset(): // : strings / Resetting . java import java.util.regex.*; publi c class Resetting { public static void main(String[] args) throws Exception Matcher m = Pattern.compile {tI {frbl {aiu] [gx] ti ) .matcher(lIfix the rug with bags ll ) ; while (m.find () ) System . out.print {m. group {) + 11 ti) i System.out . println( ) ; m.reset ( "fix the rig with rags ll ) ; while(m.find() ) System.out.print (m.group () + 11 11 ) ; / * Output:
13 Cadenas de caracteres 345 fix rug bag fix rig rag
' 111 ,rcsct() sin ningún argum ento bace qu e Matcher se situe al princi pio de la secuencia actu al.
Expresiones regulares y E/S en Java La mayoría de los ejemplos vistos hasta ahora mostraban la apli cación de las expresiones regulares a cadenas de caracteres es táticas. El siguiente ejemplo muestra una forma de aplicar expresiones regulares a la búsqueda de corres pondencias en un archivo. Inspirada en la utilidad grep de Unix, JGrep.java lOma dos argum entos: un nombre de archi vo y la ex presión regular con la que se qui ere buscar correspondencias. La salida muestra cada línca en la que se ha detec tado una correspondencia y la posición o posiciones de las correspondencias dentro de la línea: // : strings/JGrep.java / / Una versión muy simple del programa "grep". II {Args, JGrep.java " \\b[Ssctl\\w+"} import java.util.regex. * ¡ import net.mindview . util .* ¡ public class JGrep { public static void ma i n{Stri ng[] args) throws Exception i f (args . leng t h < 2) { System.out . println(ItUsage : java JGrep file regex " ) ¡ Sy stem . exit(O) ¡ Pattern p Pattern . compile (args (1 ]) ; /1 Iterar a través de las lineas del archivo de entrada: int index Oi Matcher m p . matcher ("") ; for(String line : new TextFile(args[O))) m. reset (line) ; while (m. find()) System.out.println(index++ + 11: " + m.group() +" : " +m.start());
1* Output: (Sample) O, strings: 4 L simple: 10 2, the: 28 3, Ssct: 26 4, class: 7 5, static: 9 6, String: 26 7, throws: 41 8, System: 6 9, System: 6 10, compile: 24 I I , through: 15 12, the: 23 13, the: 36 14, String: 8 15, System : 8 16, start: 31 ' 111,El archivo se abre como un objeto net.mindview.util.TcxtFilc (del que hablaremos en el Capitulo 18. E/S). que lee las lineas del archivo en un contenedor tipo ArrayList. Esto significa que podemos utilizar la sintaxi s foreach para iterar a través de las líneas almacenadas en el objeto TextFile.
346 Piensa en Java
Aunque es posible crear un nuevo objelO Matcher dentro del bucle for, resulta ligeramente más óptimo crear un objeto vacio Matcher fuera del bucle y utili zar el método reset() para asignar cada linea de la entrada al objeto Matcher. El resul_ tado se analiza con tind(). Los argumentos de prueba abren el archivo JGrep.java para leerlo como entrada y buscan las palabras que comiencen por ¡Ssct¡ . Puede aprender mucho más acerca de las expresiones regulares en A1astering Regular E).pressions, 2 u Edición, por Jeffrey E. F. Friedl (O'Reilly, 2002). Hay también numerosas introducciones a las expresiones regulares en Internet, y también se puede encontrar a menudo infonnación útil en la documentación de lenguajes tales como Perl y Python. Ejercicio 15:
(5) Modifique JGrep.java para aceptar indicadores como argumentos (por ejemplo, Pattern.CASE_ INSENS ITIVE, Pattern.M ULT ILl NE).
Ejercicio 16:
(5) Modifique JGr.p.java para aceptar un nombre de directorio o un nombre de archivo como argumento (si se proporciona un directorio, la búsqueda debe extenderse a todos los arcluvos de directono). Consejo: puede generar una lista de nombres de archivo con: File (]
files
=
new File (" . ") .listFiles () ;
Ejercicio 17:
(8) Escriba un programa que lca un archivo de código fuente Java (tendrá que proporcionar el nombre del archivo en la línea de comandos) y muestre todos los comentarios.
Ejercicio 18:
(8) Escriba UD programa que lea un archivo de código fuente Java (tendrá que propo rcionar el nombre del archivo en la línea de comandos) y muestre todos los literales de cadena presentes en el código.
Ejercicio 19:
(8) Utilizando los resultados de los dos ejercicios anteriores, escriba un programa que examine el código fuente Java y genere todos los nombres de clases utilizados en un programa concreto.
Análisis de la entrada Hasta ahora, resultaba relativamente complicado leer datos de un archivo de texto legible o desde la entrada estandar. La solución usual consiste en leer una línea de texto, extraer los elementos y luego utilizar los diversos métodos de análisis sintáctico de Integer, Double, etc., para analizar los datos:
11:
strings/SimpleRead.java impert java.io.·;
public class SimpleRead public static BufferedReader input = new BufferedReader( new StringReader("Sir Robin of Camelot\n22 1.61803")); public static void main(String (] args) { try ( System.out.println("What is your name?"); String name = input. readLine () ; System.out.println(name) ; System.out.println( "How old are you? What is your favorite double?"); System.out . println(l!(input: shapeList = Arrays.asList( new Circle(), new Square(), new Triangle() ) ;
for(Shape shape : shapeList ) shape.draw() ¡
/* Output : Circle . draw () Square. draw () Triangle. draw () *///,La clase base contiene un método draw() que utili za indirectamente toString() para imprimir un identificador de la clase, pasando this a System.out.println() (observe que toString() se declara como abstracto para obligar a las clases herederas a sustituirlo, y para imped ir la instantación de un objeto Shape simple). Si un objeto aparece en una expresión de concatenación de cadenas (donde están involucrados '+' y objetos String), se invoca automáti camente el método toString() para generar una representación de tipo Slring de dicho objelo. Cada un a de las clases derivadas sustituye el método toString() (de Object) de modo que draw() termine (po limórficamente) imprimiendo algo distinto en cada caso. En este ejemplo, la generalización tiene lugar cuando se coloca la fonna geométrica en el contenedor List. Durante la generalización a Shape, el hecho de que los objetos sean lipos específicos de Shape se pierde. Para la matriz se (rata simplemente de objetos Shape. En el momento en que se extrae un elemento de la matriz, el contenedor (q ue en la práctica almacena todos los elementos como si fueran de tipo Object) proyecta automáticamente el resultado sobre un objeto Shape. Éste es el tipo más básico del mecanismo RTTI , porque todas las proyecciones de tipos se comprueban en tiempo de ejecución para comprobar Su corrección. Eso es lo que RTTI significa: el tipo de los objetos se identifica en tiempo de ejecución . En este caso, la proyección RTTl sólo es parcial : el objeto Object se proyecta sobre Shape, y no so bre Circle, Sq ua re o Trian gle. Eso es debido a que lo único que sabemos en este punto es que el contenedor List está lleno de objetos Shape. En tiempo de compilac ión, esto se impone mediante el contenedor y el sistema genéri co de Java, pero en tiempo de ejecución es la proyección la que garanti za que esto sea así. Ahora es cuando entra en acción el polimorfismo y se detennina el código exacto que ejecutará el objeto Shape vie ndo si la referencia corresponde a un objeto Circle, Square o Triangle. Y, en general, as í es como deben ser las cosas. Lo que queremos es que la mayor parte de nuestro código sepa lo menos posible acerca de los tipos específicos de los objetos. debiendo limitarse a trata r con la representación general de una fami lia de objetos (en este caso, Shape). Como resu ltado. el código será más fácil de escribir, de leer y de mantener, y los diseños serán más senci llos de implementar, comprender y
14 Información de tipos 353 modificar. Por ello. el polimosrfismo es objetos.
UIlO
de los objetivos generales que se persiguen con la programación orientada a
¿Pero qué sucede si tenemos un problema especial de programación que resulta más fácil de resolver si conocemos el tipo exacto de una referencia genérica? Por ejemplo, suponga que queremos pem1itir a nuestros usuarios que resalten todas las fom1as geométricas de un cierto tipo concreto, asignándolas un color especial, de esta fonn8. pueden localizar todos los triángulos de la pantalla resaltándolos. 0 , por ejemplo. imagine que nuestro método necesita "rotar" una lista de fomlas geométricas. pero que no tiene sentido rotar un círculo, por lo que preferimos saltarnos los círculos al implementar la rotación de todas las formas. Con RTT l, podemos preguntar a una referencia de tipo Shape cuál es el tipo exacto al que está apuntand o. lo que nos permite seleccionar y aislar los casos especiales.
El objeto Class Para comprender cómo funciona el mecanismo RTTI en Java. primero tenemos que saber cómo se represe nta la infonnación de tipos en tiempo de ejecución. Esto se lleva a cabo mediante un tipo de objeto especial denominado objeto Class. que contiene infonnación acerca de la clase. De hecho. el objeto Class se utiliza para crear todos los objetos ""nonnales" de una clase. Java implementa el mecanismo RTIl utilizando el objeto C lass. incluso si lo que estamos haciendo es algo como una proyecc ión de tipos. La clase C lass también pennite otra se rie de fonnas de utilización de RTTI. Existe un objeto Class para cada clase que fonne parte del programa. En otras palabras, cada vez que escribimos y compilamos una nueva clase. también se crea un determinado objeto Class (y ese objeto se almacena en un archivo .class de nOI11bre idéntico). Para crear un objeto de esa clase. la máquina virtual Java (JVM) que esté ejecutando el programa utiliza un subsistema denominado cargador de clases. El subsistema cargador de clases puede comprender, en la práctica, una cadena de cargadores de clases. pero sólo existe un cargador de clases primordial, que forma parte de la implementación de la NM. El cargador de clases primordial carga las que se denominan clases de confian:::a, que incluyen las clases de las interfaces API de Java, y esa carga se reali za normalmente desde el di sco local. Usualmente no es necesario tener cargadores de clases adicionales en la cadena, pero si tenemos necesidades especiales (como por ejemplo, cargar clases de alguna manera especial para dar soporte a aplicaciones de servidor web, o descargar clases a través de una red). entonces disponemos de una manera de enlazar cargadores de clases adicionales. Todas las clases se carga n en la JVM dinámicamente, cuando se utiliza la clase por primera vez. Esto sucede cuando el programa hace referencia a un miembro estático de dicha clase. Resu lta que el constructor también es un método estático de una clase, aún cuando no se utilice la palabra clave sta tic para el constructor. Por tanto, el crear un nuevo objeto de di cha clase utilizando el operador Dew también cuenta como una referencia a un miembro estático de la clase. Por tanto. los programas Java no se cargan por completo antes de comenzar la ejecución, sino que se van cargando los distintos fragmentos del programa a medida que so n necesarios. Esto difiere de muchos lenguajes tradic ionales. El mecani smos de carga permite conseguir un tipo de comportamiento que resulta muy dificil, o incluso imposible, de obtener con un lenguaje estático de carga como pueda ser C++. El cargador de clases compnteba primero si el objeto C lass de dicho tipo está cargado. Si no lo está, el cargador de clases predetem1inado localiza el archivo .class con dicho nombre (un cargador de clases adicional podría. por ejemplo. extraer el código intenuedio de una base de datos en lugar de hacerlo de un archivo). A medida que se carga e l código intennedio correspondiente a la clase, dicho código se verifica para garantizar que no esté corrompido y que no incluya código Java mal fonnado (ésta es una de las líneas de defensa de los mecanismos de seguridad de Java). Una vez que e l objeto C lass de dicho tipo se encuentra en memoria se le utiliza para crear todos los objetos de dicho tipo. He aquí un programa que ilustra esta fom1a de actuar:
JI : typeinfo / SweetShop.java JI Examen de la forma en que
funciona el cargador de clases. import static net.mindview .uti l.Print.*;
class Candy { static { print ("Loading Candy");
}
354 Piensa en Java class Gum { static { print ("Loading Gum");
}
class Cookie { static { print (IILoading Cookie ll ) ;
}
public class SweetShop { public static void main (String (] args) print (11 inside main " ); new Candy () ; print(IIAfter creat ing Candy " ); try ( Class. forName ("Gum") i catch (ClassNotFoundExcep tion e) { print ("Couldn' t find Gum") ;
{
print ( " After Class. f orName (\ "Gum\ " ) 11 ) ; new Cookie () ; print("After creating Cookie lt ) ;
/ * Output : inside main Loading Candy After creating Candy Loading Gum After Class. forName ( "Gum") Loading Cookie After creating Cookie
* ///, Cada una de las clases de Candy, Gum y eookic tiene una cláusula statie que se ejecuta cuando se carga la clase por primera vez. Se imprimirá la infonnación para decimos cuándo tiene lugar la carga de esa clase. En maine ), las creaciones de objetos están mezcladas con instmcciones de impresión, como ayuda para detemlÍnar el instante de la carga. Podemos ver, a partir de la salida, que cada objeto Class sólo se carga cuando es necesario, y que la inicinlizHción de tipo static se realiza durante la carga de la clase. Una línea particulal111ente interesante es: Class. forName ( "Gum " ) ;
Todos los objetos elass pertenecen a la clase elass. Un objeto elass es como cualquier otro objeto, por lo que se puede obtener y manipular Wl3 referencia a él (es to es lo que hace el cargador). Una de las fonnas de obtener una referencia al objeto Class es el método estático forName(), que toma un argumento de tipo String que contiene el nombre textua l (¡tenga cuidado con la ortografía y el uso de mayúsculas!) de la clase concreta de la cual se quiera obtener una referencia. Elmétodo devuelve una referencia de tipo Class, que en este ejemplo se ignora; la llamada a forName() se rcali za debido a su efec10 secundario que consiste en cargar la clase Gum si no está ya cargada. Durante el proceso de carga se ejecuta la cláusula sta tic de Gum . En el ejemplo anterio r, si elass.forName( ) falla porque no puede encontrar la clase que estemos intentando cargar. generará una excepción ClassNotFoundException. Aqui, simplemente nos limitamos a infonnar del problema y a continuar. pero en otros programas más sofisticados podríamos intentar resolver el problema dentro de la mtina de tratamiento de excepciones. Siempre que queramos utilizar la infonnación de tipos en tiempo de ejecución, debemos primero obtener una referencia al objeto elass apropiado. elass.forName( ) es una fo mla cómoda de hacer esto, porque no necesitamos un objeto de di cho tipo para obtener la referencia Class. Sin embargo, si ya disponemos de un objeto del tipo que nos interesa. podemos ext raer la referencia Class invocando un método que fomJa parte de la clase raíz Objeet: gctelass( ). Este mecanismo de vuel\'e la referencia Class que representa el tipo concreto de objeto. Class dispone de muchos métodos interesantes; el sigu iente es un ejemplo que ilustra algunos de ellos:
14 Información de tipos 355
11 : typeinfo ltoys/ToyTest.java 11 Prueba de la clase Class. package typeinfo.toys; import static net.mindview.util.Print. * ; interface HasBatteries {} interface Waterproof {} interface Shoots {} class Toy 1/ Desactive con un comentario el constructor predeterminado 11 siguiente para ver NoSuchMethodError de (*1 * ) Toy () {} Toy l int i l
{}
class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots { FancyToy 11 { super 111; }
public class ToyTest { static void print l nfo (Class ce) { print( "Class name: u + ce.getName() + u is interface? [ " + cc.islnterface() + I']U); print (ti Simple name: " + ce. getSimpleName () ) ; print ( "Canonical name : " + cc. getCanonicalName (l ) ; public static void main(String[] args) Class e = null i
{
try {
c o:: Class. forName ( "typeinfo. toys. FaneyToytl) ; catch (ClassNotFoundException e) { print ( "Can I t find FancyToy"); System.exit(1) ¡ printlnfo(c) i for (Class face : c.getlnterfaces() printlnfo(face) ; Class up = c.getSuperclass() ¡ Object obj = null¡ try {
11 Requiere un constructor predeterminado: obj = up.newlnstance()¡ cateh (InstantiationException el { print ("Cannot instantiate"); System. exi t (l) ; eatch(IllegalAccessException el { print(IICannot access") ¡ System.exit(1) ¡ printlnfo(obj .getClass(»;
1* Output: Class name: typeinfo. tays. FancyToy is interface? [false] Simple name: FancyToy Canonical name : typeinfo.toys.FancyToy Class name: typeinfo. tays. HasBatteries i5 interface? [true] Simple name: HasBatterie5
356 Piensa en Java
Canonical name : typeinfo.toys.HasBatteries Class name: typeinfo. toys. Waterproof is interface? [true] Simple name: Waterproof Canonical name : typeinfo.toys .Waterproof Class name: typeinfo . toys . Shoots is interface? [true] Simple name: Shoots Canonical name : typeinfo.toys.Shoots Class name: typeinfo . toys . Toy is interface? (false] Simple name: Toy Canonical name : typeinfo.toys.Toy
*///,FancyToy hereda de Toy e implementa las interfaces HasBatteries, Waterproof y Shoots. En main( ), se crea una referencia Class y se la inicializa para que apunte al objeto Class FancyToy Class utili zando forName() dentro de un bloque Iry apropiado. Observe que hay que utilizar el nombre completamente cualificado (incluyendo el nombre del paquete) en la cadena de caracteres que se pasa a forName( ). printlnfo() utiliza geIName() para generar el nombre de clase completamente cualificado, y geISimpleName() y geICanonicaIName() (introducidos en Java SE5) para generar el nombre sin el paquete y el nombre completamente cualificado. respectivamente. Como su propio nombre indica, islnterface() nos dice si este objeto Class representa un interfaz. Por tanto, con el objeto Class podemos averiguar casi todo lo que necesitemos saber acerca de un determinado tipo. El método Class.getlnlerfaces( ) invocado en m.inO devuelve una matriz de objetos Class que representa las interfaces contenidas en el objeto Class de interés. Si tenemos un objeto Class, también podemos preguntarle cuál es su clase base directa utilizando geISuperclass(). Este método devuelve una referencia Class que podemos, a su vez, consultar. Por tanto, podemos detenninar la jerarquía de cIases completa de un objeto en tiempo de ejecución. El método newlnstance( ) de Class constituye una forma de implementar un "constructor virtual" que nos pennite decir: "No sé exactamente de qué tipo eres, pero crea una instancia de tí mismo de todas formas". En el ejemplo anterior, up es simplemente una referencia Class de la cual no se conoce ninguna infoffi1ación de tipos adicional en tiempo de compilación. Y cuando creamos una nueva instancia, obtenemos como resultado una referencia Object. Pero dicha referencia apunta a un objeto Toy. Por supuesto, antes de poder enviar a ese objeto ningún mensaje diferente de los que admite Object, es necesario in vestigar un poco acerca del objeto y efectuar algunas proyecciones de tipos. Además, la clase que se crea con newlnstance() debe disponer de un constructor predeterminado. Posterionnente en este capítulo, veremos cómo crear objetos dinámicamente de una cierta clase utilizando cualquier constructor, empleando para ello la API de reflexión de Java. Ejercicio 1:
(1) En ToyTest.java, desactive mediante un comentario el constmctor predetenninado de Toy y explique lo que sucede.
Ejercicio 2:
(2) Incorpore un nuevo tipo de interfaz en ToyTest.java y verifique que dicha interfaz se detecta y se muestra adecuadamente.
Ejercicio 3:
(2) Anada Rhomboid a Shapes.java. Cree un objeto Rhomboid y generalícelo a Shape, y vuelva a especializarlo a Rhomboid . Trate de especializarlo a un objelO Circle y vea lo que sucede.
Ejercicio 4:
(2) Modifique el ejercicio anterior para que utilice instanceof con el fin de comprobar el tipo, antes de efectuar la especialización.
Ejercicio 5:
(3) Implemente un método rotate(Shape) en Shapes.java, que compmebe si está girando un círculo (y, en caso afirmativo, no realice la operación).
Ejercicio 6:
(4) Modifique Shapes.java para que permita "resaltar" (acti vando un indicador) todas las formas de un tipo concreto. El método toString( ) para cada objeto derivado de Shape debe indicar si dicho objeto Shape está ··resaltado".
Ejercicio 7:
(3) Modifique SweelShop.java para que la creación de cada tipo de objeto esté controlada por un argumento de la línea de comandos. En otras palabras, si la línea de comandos es "java SweetShop Candy", entonces sólo se creará el objeto Candy. Observe cómo se pueden controlar los objetos Class que se cargan, utilizando argumentos de la línea de comandos.
14 Información de tipos 357 Ejercici o 8 :
(5) Escriba un método que tome un objeto e imprima de manera recursiva todas las clases presentes en la jerarquia de ese objeto.
Eje rcici o 9 :
(5) Modifique el ejercicio anterio r de modo que utilice C lass.getDeclaredFields( ) con el fin de mostrar también infom13ción acerca de los campos contenidos en cada clase.
Eje rcici o 10:
(3) Escriba un programa para determinar si una matriz de char es un tipo primitivo o un verdadero
obje~
too
Literales de clase Java proporciona una segunda fom13 de generar la referencia al objeto Class: el Iileral de clase. En el programa anterior, dicho literal de clase tendría el aspecto: FancyToy.class;
lo que no sólo es más simple. sino también más seguro ya que se comprueba en tiempo de compilación (y no necesita, por tanto, colocarse dentro de un bloque try). Asimismo, puesto que elimina la llamada al melado forNal11c( ). es también más eficiente. Los literales de c lase funcionan tanto con las clases nonnales COlT'IO con las interfaces. matrices y tipos primitivos. Además, existe un campo estándar denominado TVPE en cada un a de las clases envoltorio de los tipos primitivos. El campo TYPE produce una referencia a l objeto C lass correspondiente al tipo primitivo asociado, de modo que:
.... equIvIIInee .... boolcan.cla ss
Boolean.T"PE
char.cJass
C haracter.TYPE
byte.class
Byte.TYPE
short. cl ass
Short.TYPE
int.cJass
Integer.TYPE
long.cJass
Long.TYPE
float.class
Float.TYPE
double.cJass
Double.TYPE
\'oid.cJass
Void.TYPE
En mi opinión, es mejor utilizar las versiones ".class" siemp re que se pueda. ya que son más coherentes con las clases normales. Es interesante observar que al crear una referencia a un objeto Class utilizando ·'.class·' no se inicializa automáticamente el objeto C lass . La preparación de una clase para su uso consta, en realidad. de tres pasos diferentes :
1. Carga, que es realizada por el cargador de clases. Este proceso localiza el código intennedio (que usualmente se encuentra en el disco. dentro de la ruta de clases, aunque no tiene porque ser necesariamente así) y crea un objeto Class a partir de dicho código intem1edio. 2. A1onlClje. La fase de montaje verifica el código intenncdio de la clase. asigna el campo de almacenamiento para
los campos estáticos y. en caso necesario, resuelve todas las referencias que esta clase haga a otras clases.
3. Inicialización. Si hay una superclase, es preciso inicializarla, ejecutando los inicializadores de tipo static y los bloques de inicialización de tipo static. La inicialización se retarda hasta que produce la primera referencia a un melodo estático (e l cons tructor es implícitamen te de tipo sta tic) o a un campo estát ico no cons tante:
358 Piensa en Java //: typeinfojClasslnitialization.java import java.util. *; class Initable statie final int staticFinal = 47; statie final int staticFina12 = Classlnitialization.rand.nextlnt(lOOOl; static { System . out .println ("Initializing Initable");
class Initable2 { statie int staticNonFinal = 147; sta tic { System.out.println(lIInitializing Initable2");
class Initable3 { static int staticNonFinal = 74; statie { System.out.println(ltlnitializing Initable3");
public class Classlnitialization { public static Random rand = new Random(47 ) ; pUblic static void main(String[] args) throws Exception Class initable = Initable.class; System .out. println ("A fter creating Initable ref");
II No provoca la inicialización: System.out.println(Initable.staticFinal) ; II Provoca la inicialización: System.out.println(Initable.staticFinal2) i II Provoca la inicialización: Systern.out.println(Initable2.staticNonFinal) i Class initable3 :: Class. forName ( 11 Initable3 11 ) ; System .out. println ( "After creating Initable3 ref") System.out.println(Initable3.staticNonFinal) i
i
1* Output: After creating Initable ref 47
Initializing Initable 258
Initializing Initable2 147
Initializing Initable3 After creating Initable3 ref 74
* ///,En la práctica, la inicialización es lo más tardía posible. Analizando la creación de la referencia ¡nHable, podemos ver que usar sim plemelllc la sintaxis .class para obtener una referencia a la clase no provoca la inicialización. Sin embargo. Class.forName( ) inicializa la clase inmediatamente para generar la referencia C lass, como puede ver analizando la creación de i"ilable3 . Si un va lor final estático es una "constante de tiempo de compilación", tal como Jnitable.staticFinal, dicho valor puede leerse sin que ello haga que la clase Initable se inicialice. Sin embargo, definir un campo como estático y final no garanti-
14 Información de tipos 359 za este comportamiento: al acceder a Initable.sta tic Fina l2 se fuerza a la inicial ización de clase, porque dicho campo no puede ser una constante de tiempo de compi lación. Si un campo estático no es de tipo fin al, acceder al mismo requiere siempre que se ejecute la fase de montaje (para asignar el espacio de almacenamiento para e l campo) y también la de inicialización (para inicializar dicho espacio de almacenamienw) antes de que el va lor pueda ser leído, como puede ver analizando el acceso a Ini table2 .staticNo n Final.
Referencias de clase genéricas Una referencia C lass apunta a un objeto C lass, que genera instancias de las clases y contiene todo el código de los métodos correspond ientes a dichas instancias. También contiene los valores estáticos de dicha clase. Por tanto, una referencia Class realmente indica el tipo exacto de aquello a lo que está apuntando: un objeto de la clase C lass. Sin embargo, los di señadores de Java SES vieron la oportunidad de hacer esto un poco más específico, pennitiendo restringir el tipo de objeto C lass al que la referencia C lass apunta, utilizando para ello la sintaxis genérica. En el siguiente ejemplo, ambos tipos de sintaxis son correctos:
/1: typeinfo/GenericC lassReferences.java public class GenericClassReferences public static void main{String[] args) Class intClass = int.class; Class genericlntClass int.class¡ genericlntClass = Integer .class¡ II Lo mismo intClass = double . class¡ II genericlntClass = double . class¡ II Ilegal
La referencia de clase nonnal no genera ninguna advertencia de compilación. Sin embargo, puede ver que la referen cia de clase nomlal puede reasignarse a cualqui er otro objew Class, mientras que la referencia de clase genérica sólo puede asignarse a su tipo declarado. Utilizando la sintaxis genérica, permilimos que el compilador imponga comprobaciones adicionales de los tipos. ¿Qué sucede si queremos relajar un poco las restricciones? lnicialmente, parece que deberíamos ser capaces de hacer algo como lo siguiente: Class genericNumberClass
=
int.class¡
Esto parece tener sentid o. porque Integer hereda de Number. Sin embargo, este método no funciona, porque el objeto Class Integer no es una subclase del objeto C lass Number (puede parece r que esta di stinción re sulta demasiado su til ; la analizaremos co n más detall e en el Ca pítulo 15 . Genéricos). Para relajar las restri cciones al utili zar referencias Class genéricas, yo personalmente empleo el comodín , que fonna parte de los ge néricos de Java. El símbolo del comodín es ' ?', e indica "cualquier cosa". Por tanto. podemos añadir comodines a la referencia Class de l ejemplo anterior y genera r los mismos resultados: jI : typeinfo / WildcardClassReferences.java
public class WildcardClassReferences { public static void main (String[} args) Class intClass = int.class; intClass = double . class¡
En Java SE5, se prefiere utilizar Class en lugar de Class, aún cuando ambos son equivalemes y la referencia Class normal. como hemos visto, no genera ninguna advertencia del compilador. La ventaja de Class es que indica que no es tamos pasando una referencia de clase no específica simplemente por accidente o por ignorancia, sino que hemos elegido la versión no especifica.
360 Piensa en Java
Para crear una referencia C lass que esté restringida a un detemlinado tipo o a cualquiera de sus stlbripos, podemos combinar un comodín con la palabra clave ex tends para crear un limite. Por lanlO, en luga r de decir simplemente C lass. lo que diríamos seria:
11: typeinfo/BoundedClassReferences.java public class BoundedClassReferences { public static void main(String[] args) Class bounded = int.class; bounded = double.class; bounded = Number.class; II o cualquier otra cosa derivada de Number. )
/11 ,La razón de aiiadir la si ntax is genérica a las refe rencias Class estriba únicamente, en real izar una comprobación de los tipos en tiempo de compilación, de modo que si hacemos algo incorrecto lo detectaremos un poco antes. No es posible realizar nada realmente deslnlctivo con las referen cias Class normales, pero si cometemos un error no podremos detectarlo hasta el tiempo de ejecución, lo que puede resultar incómodo. He aquí un ejemplo donde se utiliza la si ntaxis de clases genéricas. El ejemplo almacena una referencia de clase y luego genera un contenedor List relleno con objelOs generados mediante newlnstance( ):
11 : typeinfo/FilledList.java import java.util.*; class Countedlnteger private static long counter; private final long id = counter++; public String toString() ( return Long. toString (i d ) ¡
public class FilledList { private Class type¡ public FilledList (Class type) { this. type public List create (int nElements) { List result = new ArrayList();
type;
)
try {
for(int i = O; i < nElements; i++) result.addCtype.newlnstance()) ; catch (Exception e) ( throw new RuntimeException(e); return resul t; public static void main(String[] args) ( FilledList fi = new FilledList (Countedlnteger.class) ; System . out.println(fl.create(15)) ;
1* Output: [O,
1,
2,
3,
4,
S,
6,
7,
8,
9,
lO,
11,
12,
13,
14J
* /// ,Observe que esta clase debe asumir que cualquier tipo con el que trabaje dispondrá de un constnlctor predetenninado (uno que no tenga argumentos), obteniéndose una excepción si no es éste el caso. El compilador no genera ningún tipo de advertencia para este programa. Cuando utilizamos la sintaxis genérica para los objetos Class sucede algo interesante: ncwInsta nce( ) devolverá el tipo exacto del objeto. en lugar de simplemente un objeto básico O bj ec t como vimos en ToyTes t.java. Esto resulta un tanto limitado :
14 Información de tipos 361
JI : typeinfo / toys / GenericToyTest.java JI Prueba de la clase Class. package typeinfo.toys¡ public class GenericToyTest public static void main(String[] args ) throws Exception { Class ftClass = FancyToy.classi /1 Produce el tipo exacto: FancyToy fancyToy = ftClass.newlnstance() i Class up = ftClass.getSuperclass{) i
JI
Esto no se compilará:
= ftClass.getSuperclass{); Sólo produce Object:
/1 Class up2
JI
Object obj
=
up . newlnstance () ;
}
/ // , Si obtenemos la supcrclase, el compilador sólo nos pcnnitirá decir que la referencia a la superclase es "alguna clase que es superclase de FancyToy" , co mo podemos ver en la expresión C lass. No aceptará una declaración de Class. Esto parece un poco ex traño, porque getS up ercl ass( ) devue lve la clase base (no una interfaz) y el compilador conoce en tiempo de compilación lo que esa clase es: en este caso, Toy.c lass, no simplemente "al guna superclase de FancyToy". En cualquier caso, debido a la vaguedad, el valor de retorno de up. new lnsta nce( ) 110 es de un tipo preciso, sino sólo de tipo Obj cct.
Nueva sintaxis de proyección Java SE5 también ha añadido una sintaxis de proyección para utilizarla con las referencias Class, nos referimos al método cast( ):
/1 : typeinfolclassCasts.java class Building {} class House extends Building {} public class ClassCasts { public static void main(String[] argsl ( Building b = new House(); Class houseType = House.class; House h = houseType. cast (b ) ¡ h = (House ) b¡ 1/ o haga simplemente esto. )
///,El método cast() toma el objeto proporcionado como argumento y lo proyecta sob re el tipo de la referencia Class. Por supuesto, si exa minamos el código anterio r parece que es demasiado trabajo adicional, si lo comparamos con la última línea de maine ), que hace exactamente lo mismo. La nueva sintaxis de proyección resulta útil en aquellas situaciones en las que no podemos utili zar una proyección ordinaria. Esto sucede, usualmente, cuando estamos escribiendo código genérico (de lo que hablaremos en el Capítulo 15, Genéricos), y hemos almacenado un a referencia Class que queremos utilizar en algún momento posterior para efectuar la proyecc ión. Este caso no resulta muy frecuente ; de hecho, sólo he podido encontrar una única ocasión en la que cast( ) se use dentro de la biblioteca de Ja va SE5 (concretamente en co m.sun.mir ro r.utiI.Declara tion Filter). Hay otra nueva funcionalidad que 110 se Uliliza en absoluto cnla biblioteca Java SE5: C lass.asS ubclass(). Este método permite proyectar el objeto de clase sob re un tipo más específico.
Comprobación antes de una proyección Hasta ahora, hemos visto varias fonuas de RTTI , inclu yendo:
362 Piensa en Java
J. La proyección clás ica, por ejemplo, "(Shape);' que utili za RTTl para asegurarse de que la proyección es correcta. Esto ge nerará ClassCastException si se ha realizado una proyección incorrecta. 2. El objeto Class representativo del objeto. Podemos consultar el objeto Class para obtener información útil en tiempo de ejecución. En C++. la proyección clásica "(Shape)" no utiliza mecanismos RTTI. Simplemente le dice al compilador que trate el objeto como si fuera del tipo indicado. En Java, sí realiza la comprobación de tipos. esta proyección se denomina a menudo "especialización segura en lo que respecta a tipos". La razón de utilizar el ténnino "especialización" se basa en la disposición históricamente utilizada en los diagramas de jerarquías de clases. Si la proyección de Circle sobre Shape es una generali zación, entonces la proyección de Shape sobre Circle es un a especialización. Sin embargo, puesto que el compilador sabe que un objeto Circle es también de tipo Shape, permite que se realicen libremente asignaciones de generalización, si n que sea obligatorio incluir una sintaxis de proyección específica. El compilador no puede saber, dado un objeto Shape, de qué tipo concreto es ese objeto; podría ser exactamente de Shape, o podría ser un subtipo de Shape, como Circle, Square, Triangle o algún olro tipo. En ti empo de compilación, el compilador sólo ve un objeto Shape. Por tanto, no nos permitirá que realicemos una asignación de especialización sin utilizar una proyección específica. con la que le decimos al compilador que disponemos de información adicional que nos permite saber que se trata de un tipo concreto (el compilador comprobará si dicha especialización es razonable, por lo que no nos permitirá efectuar especializaciones sobre un tipo que no sea realmente una subclase del anterior). Existe un tercer mecanismo de RTTI en Java. Se trata de la palabra clave instanceof, que nos dice si un objeto es una instancia de un tipo concreto. Devuelve un valor de tipo boolean, as í que esta palabra clave se utiliza en fomla de pregunta, como en el fragmento siguiente: if(x instanceof Oog) (( Dog ) x ) .bark( ) ;
La instrucción ir comprueba si el objeto x pertenece a la clase Dog antes de proyectar x sobre Dog. Es importante utilizar instanceof antes de una especialización cuando no dispongamos de otra in[onnación que nos indique el tipo del objeto; en caso contrario, obtendremos una excepción ClassCastException . Nonnalmente, lo que estaremos tratando de localiza r es un determinado tipo (por ejemplo, para pintar de púrpura todos los triángul os), pero podemos fácilmente seleccionar todos los objetos utilizando instanceof. Por ejemplo, suponga que disponemos de una familia de clases para describir mascotas, Pet, (y sus propietarios, una característica que nos será útil en un ejemplo posterior). Cada individuo (I ndividual ) de la jerarquia tiene un identificador id y un nombre opcional. Aunque las clases que siguen heredan de Individual, existen ciertas complejidades en la clase Individual, por lo que mostraremos y explicaremos di cho código en el Capítulo 17, Análisis detallado de los contenedores. En realidad, no es imprescindible anali zar el código de Individual en este momento; lo único qu e necesitamos saber es que podemos crear un individuo con o sin nombre, y que cada objeto Individual tiene un metodo ¡d() que devuelve un identificador unívoco (creado mediante un simple recuent o de los objetos). También hay un método toString( ); si no se proporciona un nombre para un objeto Individual, toString() sólo genera el nombre simple del tipo. He aqui la jerarquía de clases que hereda de Individual : // : typeinfo/pets/Person.java package typeinfo.pets; public class Person extends Individual { public Person{String name } ( super(name);
) /// ,// : typeinfo/pets/Pet.java package typeinfo pets; public class Pet extends Individual { public Pec(String name) { super(name); public Pet () { super () ; )
///,// , typeinfo/pets/Dog.java
14 Información de tipos 363 package typeinfo pets;
public class 009 extends Pet { public Dog (String name ) { super(name ) ¡ public Dog () { super () ; }
1/ 1,-
11 , typeinfo/pets/Mutt.java package typeinfo.pets i public class Mu tt extends 009 { public Mutt (String name ) { super (name) ;
public Mutt ()
{ super () ; }
111 >
1/ : typeinfo / pets/Pug . java package typeinfo.pets¡ public class Pug extends 009 { public Pug (String name ) { super (name ) public Pug () { super () ; }
i
1/ 1 ,-
JI :
typeinfo / pets / Cat.java
pac kage typeinfo.pets i pu blic cIass Cat extends Pet { public Cat (String name ) { super (name ) ;
public Cat ()
{ super () ; }
1/ 1 , ji : typeinfo / pets / EgyptianMau . java package typeinfo.pets; public class EgyptianMau extends Cat { public EgyptianMau (String name ) { super (name ) ;
public EgyptianMau (1
{ super () ; }
1/ 1 ,// : typeinfo / pets / Manx.java pac kage typeinfo.pets; public cIass Manx extends Cat { public Manx (String name ) { super (name ) ;
public Manx (1
{ super () ; }
111 , // : typeinfo / pets / Cymric . java package typeinfo.pets; public class Cymric extends Manx { public Cymric (String name ) ( super (name ) ;
public Cymric (1
{
super () ; }
111 ,-
JI : typeinfo / pets / Rodent . java package typeinfo.pets; public class Rodent extends Pet {
364 Piensa en Java
public Rodent(String name) public Rodent (1 { super 11 ;
super (name) ; }
111> //: typeinfo/pets/Rat.java package typeinfo . petsi public class Rat extends Rodent { public Rat (String name) { super (name) ; public Rat (1 { super (1; )
111,11: typeinfo/pets/Mouse.java package typeinfo.pets; public class Mouse extends Rodent { public Mouse (String name) { super (name) ; public Mouse (1 { super (1; )
111,/1: typeinfo/pets/Hamster .java package typeinfo.pets; public class Hamster extends Rodent { public Hamster(String name) { super (name) ¡ public Hamster () { super (); }
111,A continuación, necesitamos una fanna de crear aleatoriamente diferentes tipos de mascotas, y por comodidad, vamos a crear matrices y listas de mascotas. Para pemlitir que esta herramienta evolucione a través de varias implementaciones diferentes. vamos a definir dicha herramienta como una clase abstracta:
1/ : typeinfo/pets/PetCreator.java // Crea secuencias aleatorias de objetos Peto package typeinfo.pets¡ import java.util.*¡ public abstract class PetCreator { private Random rand = new Random(47)¡ II La lista de los diferentes tipos de Pet que hay que crear: public abstract ListeClasse? extends Pet» types() ¡ public Pet randomPet () { / / Crear un obj eto Pet aleatorio int n = rand.nextlnt (types (l .size())¡ try { return types () . get (n) . newlnstance () ; catch (InstantiationException e) { throw new RuntimeException{e); catch{IllegalAccessException el throw new RuntimeException(e);
public Pet(] createArray{int size) Pet[] result = new Pet[size]; for(int i = O; i < size; i++) result[iJ = randomPet() i return resul t; public ArrayListePet> arrayList (int size) { ArrayListePet> result = new ArrayListePet>(); Collections.addAll(result, createArray(size));
14 Información de tipos 365 return result¡
}
111> El método abstracto getTypes() deja para las clases derivadas la tarea de obtener la lista de objetos Class (esto es una variante del patrón de diseño basado en el método de las plantillas). Observe que el tipo de clase se especifica como "cualquier cosa derivada de Pet", por lo que newlnstance() produce un objeto Pet sin requerir ninguna proyección. randomPet() realiza una indexación aleatoria en el contenedor de tipo List y utiliza el objeto Class seleccionado para generar una nueva instancia de dicha clase con Class.newlnstance(). El método createArray() utiliza randomPet( ) para rellenar una matriz y arrayList() emplea a su vez createArray() . Podemos obtener dos tipos de excepciones al llamar a newInstance(). Analizando el ejemplo, podrá ver que es ta s excepciones se tratan en las cláusulas catch que siguen al bloque try. De nuevo, los nombres de las excepciones son casi autoexplicativos e indican cuál es el problema (1llegalAccessException está relacionado con una vio lación del mecanismo de seguridad de Java, en este caso si el constructor predetenninado es de tipo private). Cuando derivamos una subclase de PetCreator, lo único que necesi tamos su ministrar es el contenedor List de todos los tipos de mascotas que queremos crear mediante randomPet() y los otros métodos. El método getTypes() normalmente devolverá, simplemente, una referencia a un lista estática. He aquí una implementación utilizando forName():
11 : typeinfo/pets/ForNameCreator.java package typeinfo.pets¡ import java.util.*¡ public class ForNameCreator extends PetCreator { private static List public void CQunt (String type)
(
Integer quantity = get(type); if(quantity == null) put (type, 1); else put(type, quaneity + 1);
public static void countPets (PetCreator creator) { PetCounter counter= new PetCounter(); for(Pet pee : creator . createArray(20)) II Enumerar las mascotas individuales : printnb (pet. getClass () . getSimpleName () + if(pet instanceof Pet) counter.count(UPet") ; if(pet instanceof Dog) counter.count(OIDog " ) ; if(pet instanceof Mute ) counter.count ( "Mutt U ) ; if(pet instanceof Pug) couneer . count ( "Pug" ) ; if(pet instanceof Cat) counter.count {IICat") ; if(pet instanceof Manx ) counter. count ("EgyptianMau") ; if(pet instanceof Manx) counter. count ( "Manx " ) ; if(pet instanceof Manx) counter. count ("Cymric") ; if(pet instanceof Rodent) counter . count ( "Rodent 11) ; if(pet instanceof Rat ) counter. count (" Rat" ) ; if(pet instanceof Mouse ) counter. count ( "Mouse" ) ; if(pet instanceof Hamster ) counter . count ( "Hamster" ) ; )
II Mostrar las cantidades: print (); print (counte r ) ; public static void main(String[) args) countPets(new ForNameCreator());
u
U);
14 Información de tipos 367 } / * Output o Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt Mutt cymric Mouse Pug Mouse Cymric { Pug =3, Cat=9, Hamster=l, Cymric=7, Mouse=2, Mutt=3, Rodent=5, Pet=20, Manx=7, EgyptianMau=7, 00g=6, Rat=2}
*/// 0En countPets(), se rellena aleatoriamente un a matri z con objetos Pet utilizand o un objeto PetCreator. Después, cada obj eto Pet de la matriz se comprueba y se recuenta util izando instanceof. Existe una pequeña restri cción en la utili zación de instanceof: podemos comparar únicamente con un tipo nominado, y no con un objeto Class. En el ej emplo anterior, podría parecer que resulta tedioso esc ribir todas esas expresiones instanceof, y efectivamente lo es. Pero no hay ninguna forma inteligente de automatizar instanceof creando una matriz de objetos Class y realizando la co mparación de di chos objetos (aunque, si sigue leyendo, verá que existe una alternati va). Sin embargo, esta restricción no es tan grave como pudiera parecer, porque más adelante veremos que si un di seño nos exige escribir una gran cantidad de expresiones instanceof probablemente eso signifique que el di seño no está bien hecho.
Utilización de literales de clase Si reimplernentamos la clase PctCreator usando literales de clase, el resultado es mucho más limpio en muchos aspectos:
11 : typeinfo/pets / LiteralPetCreator.java II Utili z ación de literales d e clase. package typeinfo . pets; import java . util. · ; public class LiteralPetCreator extends PetCreator { 11 No hac e f alta b l oque t r y. @SuppressWarn ings{"unchecked n ) public static final List arrayList (int size) { return creator .arrayList (size) ¡ }
111,Esto proporciona también una indirección para acceder a randomPet( ), createArray( ) y .rrayList(). Puesto que PetCount.countPets() toma como argumento PetCreator, podemos probar fácilmente la clase de LiteralPetCreator (mediante el envoltorio anteriormente definido): //: typeinfo/PetCount2 . java import typeinfo.pets.*¡ public class PetCount2 { public static void main(String[] args) PetCount.countPets(Pets.creator) ; /*
(Execute to see output) *///:-
La salida es igual que la de PetCount.java.
Instanceof dinámico El método Class.islnstance() proporciona una ronlla para probar dinám icamente el tipo de un objeto. Por tanto, podemos el iminar todas esas tediosas instrucciones instanceof de PetCount.java : //: typeinfo/PetCount3.java // Utilizaci6n de islnstance() import typeinfo.pets.*¡ import java.util.*¡ import net.mindview.util.*; import static net.mindview.util.Print.*¡ public class PetCount3 { static class PetCounter extends LinkedHashMap,Integer:> { public PetCounter () { super (MapData.map (LiteralPetCreator. allTypes, O)); public void count (Pet pet) ( // Class .islnstance {) elimina instrucciones instanceof: for(Map.Entry,Integer:> pair : entrySet()) if (pair .getKey() . islnstance (pet)) put(pair.getKey(), pair.getValue() + 1); public String toString ()
14 Información de tipos 369 StringBuilder resul t = new StringBuilder (" {") ; for(Map.Entry,Integer> pair entrySet ()) {
result.append(pair.getKey() .getSimpleName(»
i
result,append("=") i
result.append(pair.getValue(» ; resul t . append ( ", ") i result.delete{result.length()-2, result. append ( "} ") ;
result.length(»;
return result.toString();
public static void main (String [J
PetCounter petCount
=
args)
{
new PetCounter();
for (Pet pet : Pets.createArray(20» { printnb (pet . getClass () . getSimpleName () petCount.count(pet) ;
+
I!
" );
print (); print(petCount) ;
/ * Output: Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat
EgyptianMau Hamster EgyptianMau Mutt Mutt Cymric Mouse Pug Mouse Cymric {Pet =20, Dog=6, Cat=9, Rodent=5, Mutt=3, Pug=3, EgyptianMau=2, Manx=7, Cymric=5, Rat=2, Mouse=2, Hamster=l}
* 1I loPara contar todos los tipos diferentes de objetos Pet, se precarga el mapa PetCounter Map con los tipos de LiteraIPetCreator.aIlTypes. Esto utili za la clase net.míndvíew.utíl.MapData, que toma un objeto Iterable (la li sta allTypes) y un valor constante (cero, en este caso) y rellena el mapa con claves tomadas de allTypes y valores iguales a cero). Sin precargar el contenedor de tipo Map, lo que haríamos sería contar los tipos que se generan aleatoriamente y no los tipos base como Pet y Cato Como puede ver, el método islnstance() ha eliminado la necesidad de utili za r expresiones instanceof. Además, esto significa que podemos añadir nuevos tipos de Pet si mplemente cambiando la matriz LiteraIPetCreator.types; el resto del programa no necesita modificación (al revés de lo que sucedía al utili zar expresiones instanceof). El método toString( ) ha sido sobrecargado para obtener una salida más legible que siga correspondiendo con la salida típica que podemos ver a la hora de imprimir un contenedor de tipo Map .
Recuento recursivo El mapa en PetCounO.PetCounter estaba precargado con todas las diferentes clases de objetos Peto En lugar de sobrecargar el mapa, podemos utili za r Class.ísAssignableFrom() y crear una herramienta de propósito general que no esté limitada a recontar objetos Pet:
11 : net/mindview/util/TypeCounter.java 11 Recuenta instancias de una familia de tipos . package net.mindview.util; import java . util . *; public class TypeCounter extends HashMap type)
{
Integer quantity = get(type); put(type, quantity == null ? 1 : quantity Class superClass = type.getSuperclass() if(superClass != null && baseType.isAssignableFrom(superClass» countClass(superClass) ;
+
1)
i
i
public String toString () { StringBuilder result = new StringBuilder (" {") ;
for(Map.Entry,Integer> pair : entrySet{» result.append(pair.getKey() . getSimpleName(»; result.append("= " ) ; result.append(pair.getValue(» ; resul t. append (", "); resul t . delete (resul t .length () - 2, result .length () ) ; result.append("} " l; return result.toString();
El método count() obtiene el objeto Class de su argumento y utili za isAssignableFrom() para reali zar una comprobación en tiempo de ejecución con el fin de veri ficar que el objeto que se le haya pasado pertenece verdaderamente a la jerarquía de clases que nos interesa. countClass() incrementa primero el contador correspondiente al tipo exacto de la clase. Después, si baseType es asignable desde la superclase, se invoca a countClass( ) recursivamente en la superclase.
11 : typeinfo/PetCount4.java import typeinfo.pets .* ; import net . mindview.util .* ; import static net.mindview.util . Print.*; public class PetCount4 { public static void main(String[] args) { TypeCounter counter = new TypeCounter(Pet.classl; for(Pet pet : Pets.createArray(20» { printnb (pet. getClass () . getSimpleName () + " "); counter.count(pet) ; print (); print (counter) ;
1* Output: (Sample) Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt Mutt Cymric Mouse Pug Mous e Cymr ic {Mouse=2 , 00g =6 , Manx=7 , EgyptianMau=2, Rodent=S, Pug =3, Mutt=3, Cymric=S, Cat=9, Hamste r =l , Pet=20, Rat=2} * /// , Como puede ver anal izando la salida, se cuentan ambos tipos base así como los tipos exactos. Ejercicio 11:
(2) Añada Gerbil a la biblioteca typeinfo.pets y modifique todos los ejemplos del capítulo para adaptarlos a esta nueva clase.
Ejercicio 12:
(3) Utilice TypeCounter con la clase CoffeeGenerator.java del Capitulo 15, Genéricos.
14 Información de tipos 371 Ejercicio 13 :
(3) Ulilice TypeCounter con el ejemplo RegisteredFactories.java de esle capítulo.
Factorías registradas Unos de los problemas a la hora de crear objetos de la jerarquía Pet es el hecho de que cada vez que añadimos un nuevo lipa de objelo Pel a la jerarquía lenemos que acordamos de añadirlo a las entradas de LiteraIPetC r eator.java. En aquellos sistemas donde tengamos que añadir un gran número de clases de fanna habitual. esto puede llegar a se r problemático. podríamos pensa r en añadir un inicializador estático a cada subclase, de modo que el inicializador añadiera su clase a una lista que se conservara en algún lugar. Desafortunadamente, los inicializadores estáticos sólo se invocan cuando se carga por primera vez la clase, así que tenemos el típico problema de la gallina y el huevo: el generador no tiene la clase en su lista, por lo que nunca puede crear un objeto de esa clase, así que la clase no se cargará y no podrá ser incluida en la lista. Básicame nte, podemos obligarnos a crear la lista nosotros mismos de manera manual (a menos que queramos escribir una herramienla que analice el código fuenle y luego genere y compile la liSia). Por lanto. lo mejor que podemos hacer. probablemente, es colocar la lista en algún lugar central lo suficientemente obvio. Seguramente, el mejor lugar será la clase base de la jerarquía de clases que nos interese. El otro cambio que vamos a hacer aquí es diferir la creación del objeto, dejándoselo a la propia clase, utilizando el parrón de diseño denominado método de/acloría. Un método de factoría puede invocarse polimórficamente y se encarga de crear por nosotros un objeto del tipo apropiado. En esta versión muy simple, el método factoría es el método create() de la interfaz Factor y:
11: typeinfo/factory/Factory . java package typeinfo.factory; public interface Factory<:T> { T create (); }
111:-
El parámelro genérico T pennite a create() devolver un lipa diferenle por cada implemenlación de Factory. ESlo hace uso también de los tipos de retomo covariantes. En este ejemplo, la clase base Part contiene un contenedor List de objetos factoría. Las factorías correspondientes a los lipos que deben generarse medianle el método createRan dom( ) se " registran" ante la clase base añadiéndolos a la liSia partFactori es:
11 :
typeinfo / RegisteredFactories.java
1/ Registro de factorías de clases en la clase base. import typeinfo.factory.*; import java.util .* ; class Part { public String toString () { return getClass() .getSimpleName{); static List<:Factory<:? extends Part» partFactories new ArrayList<:Factory<:? extends Part»(); static { II Collections.addAll( ) genera una advertencia "unchecked generic / I array creation ... for varargs parameter". partFactories.add(new FueIFilter.Factory()) j partFactories.add(new AirFilter.Factory()); partFactories . add(new CabinAirFilter.Factory()); partFactories.add(new OilFilter.Factory()); partFactories.add(new FanBelt.Factory()); partFactories.add{new PowerSteeringBelt.Factory()); partFactories.add(new GeneratorBelt.Factory()); private static Random rand = new Random{47) j public static Part createRandom () { int n = rand.nextlnt{partFactories.size()); return partFactories. get (n) . create () ;
372 Piensa en Java
class Filter extends Part {}
class FuelFilter extends Filter // Crear una factoría de clases para cada tipo específico: public static class Factory implements typeinfo.factory.Factory public FuelFilter create() { return new FuelFilter() i
class AirFilter extends Filter { public static class Factory implements typeinfo.factory.Factory public AirFilter create{) { return new AirFilter( );
class CabinAirFilter extends Filter {
public sta tic class Factory implements typeinfo.factory . Factory public CabinAirFilter create () {
return new CabinAirFilter();
class oilFilter extends Filter { public static class Factory implements typeinfo .factory.Factory public OilFilter crea te () { return new Oi lFil ter ()
i
class Belt extends Part {} class FanBelt extends Belt public static class Factory implements typeinfo.factory.Factory public FanBelt create() ( return new FanBelt()
i
class GeneratorBelt extends Belt ( public static class Factory implements typeinfo.factory.Factory public GeneratorBelt crea te () ( return new GeneratorBelt();
class PowerSteeringBelt extends Belt { public static class Factory implements typeinfo . factory.Factory public PowerSteeringBelt create () { return new PowerSteeringBelt();
14 Información de tipos 373
public class RegisteredFactories { public static void main(String [] argsl { far(int i = O; i < 10; i++) System out.println(Part.createRandom());
/ * Output: GeneratorBelt CabinAirFilter Gene ratorBelt
AirFilter PowerSteeringBelt
CabinAirF ilter FuelFilter PowerSteeringBelt powerSteeringBelt
FuelFilter
*///,No todas las clases de la jerarquía deben instanciarse; en este caso, Filter y Belt son simplemente clasificadores, por lo que no se crea ninguna instancia de ninguno de ellos, sino sólo de sus subclases. Si una clase debe ser creada por createRandom(), contendrá una clase Factory ¡ntema. La única forma de reutilizar e l nombre Factory, como hemos vis to
antes, es mediante la cualificación typeinfo.factory.Factory. Aunque podemos utilizar Colleetions.addAIl( ) para añadir las factorías a la lista, el compilador se quejará, generando una advertencia relati va a la "creación de una matriz genérica" (lo que se supone que es imposible, como veremos en el Capítulo 15, Genéricos), por lo que hemos preferido invocar add(). El método createRandom() selecciona aleatoriamente un objeto factoría de partFactories e invoca su método create() para generar un nuevo objeto Part. Ejercicio 14:
(4) Un constructor es un tipo de método de factoría. Modifique RegisteredFactories.j ava para que en lugar de utilizar una factoría explícita, el objeto clase se almacene en el contenedor List, uti li zándose newlnstanee() para crear cada objeto.
Ejercicio 15:
(4) Implemente un nuevo PetCre.tor utíli zando factorías registradas y modifique el método envoltorio de la sección "Utili zación de literales de clase" para que emplee este nuevo objeto en lugar de los otros dos. Haga los cambios necesarios para que el resto de los ejemplos que utilicen Pets.java sigan funcionando correctamente.
Ejercicio 16:
(4) Modifique la jerarquía Coffee del Capítulo 15, Genéricos, para utilizar jerarquías registradas.
instanceof y equivalencia de clases Cuando tratamos de extraer iufonnación sobre los tipos, existe una diferencia imporlante entre ambas fonnas de instanccof (es decir, instanceof o isInst.nee(), que produce resultados equivalentes) y la comparación directa de los objetos Class. He aquí un ejemplo que ilustra la diferencia: 11 : typeinfo/Fami1yVsExactType.java II La diferencia entre instanceof y
los objetos clase package typeinfo¡ import static net.mindview.uti1 . Print.*¡ c1ass Base {} c1ass Derived extends Base {} pub1ic c1ass Fami1yVsExactType static void test (Object x) { print (IITesting x of type + x. getClass () ) ; print ("x instanceof Base + (x instanceof Base»;
374 Piensa en Java print (" x instanceof Deri ved "+ (x instanceof Deri ved) ) ; print ("Base . islnstance (xl "+ Base. class. islnstance (x l) ;
print("Derived.islnstance{x) " + Derived.class.islnstance(x)) ; print{"x.getClass() == Base.class " + (x.getClass()
==
Base.class));
print("x.getClass() == Derived.class (x .getClass () == Derived.class));
11
+
print (!1 x. getClass () . equals (Base . class)) n + (x .getClass () .equals (Base.class))) i print("x.getClass() .equals(Derived.class)) " + (x . getClass () . equals (Deri ved. class) ) ) ; public static void main (String [] argsl test (new Base()); test(new Derived());
{
/ * Output: Testing x of type class typeinfo.Base x instanceof Base true x instanceof Derived false Base. islnstance (x) true Derived. islnstance (x) false x.getClass() == Base.class true x.getClass() == Derived.class false x.getClass () .equals(Base.class ) true x.getClass () .equals (Derived.class » false Testing x of type class typeinfo.Derived x instanceof Base true x instanceof Derived true Base. islnstance (x ) true Derived.islnstance(x) true x.getClass() == Base.class false x.get Class() == Derived.class true x .getClass () . equals (Base. class )) false x.getClass() .equals (Derived.class )) true
* ///,El método teste ) realiza una comprobación de tipos con su argumento, utilizando ambas fonnas de instanceof. Después, obtiene la referencia al objeto Class y emplea = y equals() para comprobar la igualdad de los objetos Class . Como cabría esperar, instaneeoC e islnstanee() producen exactamente los mismos resultados, al igual que equals() y =. Pero las pruebas muestran que se obtienen diferentes conclusiones. Basándose en el concepto de tipos, instanceof dice: "¿Perteneces a esta clase o a una clase derivada de ésta?". Sin embargo. si comparamos los objetos Class utilizando =, no entran en juego los conceptos de herencia: o son tipos exactamente iguales o no lo son.
Reflexión: información de clases en tiempo de ejecución Si no conocemos el tipo concreto de un objeto, el mecanismo RTIl nos los dirá. Sin embargo, existe una limitación: el tipo debe ser conocido en tiempo de compilación, para poder detectarlo utilizando RTfI y para poder hacer algo úti l con la información. Dicho de otro modo, el compilador debe conocer todas las clases con las que estemos trabajando. A primera vista, esto no parece que sea una limitación importante, pero suponga que nos entregan una referencia a un objeto que no se encuentra en nuestro espacio de programa. De hecho, suponga que la clase del objeto no está ni siquiera disponible para nuestro programa en tiempo de compilación. Por ejemplo, suponga que extraemos una se rie de bytes de un archivo de disco o de una conexión de red, y nos dicen que esos bytes representan una clase. Dado que esta clase aparece después de que el compilador haya generado el código de nuestro programa, ¿cómo podríamos utilizar esta clase? En un entorno de programación tradicional, este escenario parece un poco futurista. Sin embargo, a medida que nos desplazamos hacia un mundo de programación más amplio, aparecen casos de gran importancia en los que lo que sucede es pre-
14 Información de lipos 375
cisamente esto. El primero de esos casos es la programación basada en componentes. en la que constnlimos los proyectos utilizando herramientas RAD (Rapid Applica/ion Deve/opmen/. desarrollo rápido de aplicaciones) dentro de un entorno IDE (Integrafed Developmenr Environmem, entorno integrado de desarrollo). que fanna parte de una herramienta de generación de aplicaciones. Se trata de un enfoque visual para la creación de programas, mediante el que se desplazan hasta un fonnulario una serie de iconos que representan componentes. Estos componentes se configuran entonces estableciendo algunos de SUS va lores durante el desa rrollo. Esta configuración en tiempo de diseño requiere que todos los componentes sean instanciables, que expongan hacia el exterior partes de sí mismos y que pennitan que sus propiedades se lean y se modifiquen. Además. los componentes que gestionan sucesos GUI (Graphica/ User IIl/elface) deben exponer la información acerca de los métodos apropiados, de modo que el entamo lOE pueda ayudar al programador a la hora de sustituir dichos métodos de tratamiento de sucesos. La reflexión proporciona el mecanismo para detectar los métodos di sponibles y generar los nombres de los métodos. Java proporciona una estrucrura para la programación basada en componentes mediante JavaBeans (este tema se describe en el Capitulo 22, Imelfaces gráficas de usuario). Otra razón importante para descubrir la infonnación de clases en tiempo de ejecución es tener la posibilidad de crear y ejecutar objetos en platafomlas remotas, a través de una red. Esto se denomina invocación remota de melodos (RM-I, RemOle Method Invocation), y pennite a un programa Java tener objetos distribuidos entre muchas máquinas. Esta distribución puede tener lugar por diversas razones. Por ejemplo, quizá estemos realizando una tarea que requiera cálculos intensivos y, para acelerar las cosas, podemos intentar descomponerla y asignar panes del trabajo a las máquinas que estén inactivas. En otras situaciones, puede que queramos colocar el código que gestiona tipos concretos de tareas (por ejemplo, "reglas de negocio" en una arquitectura cliente/servidor multinivel) en una máquina concreta, de modo que la máquina se convierta en un repositorio común que describa dichas acciones y que pueda ser fáci lmente modificado para que los cambios afecten a todo el sistema (se trata de un concepto bastante interesante, ya que la máquina existe exclusivamente para facilitar la modificación del software). En la misma línea, la informática d istribuida también soporta la utilización de hardware especializado que puede resultar adecuado para una tarea concreta, por ejemplo, en inversiones de matrices. pero inapropiado o demasiado caro para la programación de propósito general. La clase C lass sopona el concepto de reflexión, junto con la biblioteca java.lang.rcOcct que contiene las clases Field, Mcthod y Constructor (cada una de las cuales implementa la interfaz Member). Los objetos de es tos tipos son creados por la máquina JVM en tiempo de ejecución para representar el miembro correspondiente de la clase desconocida. Entonces, podemos utilizar los objetos Constructor (constructores) para crear nuevos objetos, los métodos get( ) y set() para leer y modificar los campos asociados con los objetos Field y el método invoke() para invocar un método asociado con un objeto Method. Además, podemos invocar los métodos de utilidad getFields( ). getMethods( ). gctConstructors( ), etc., con el fin de obtener como resultado matrices de objetos que representen los campos, métodos y constructores (puede averiguar más detalles examinando la clase Class en la documentación del JDK). Así, la información de clase para objetos anónimos puede determinarse completamente en tiempo de ejecución y no es necesario tener ninguna infonnación en tiempo de compilación. Es importante comprender que no hay ninguna especie de mecanismo mágico en la reflexión. Cuando se utili za la refl ex ión para interactuar con un objeto de un tipo desconocido, la máquina NM simplemente examinará el objeto y comprobará que pertenece a una clase concreta (al igual que con el mecanismo RTTl normal). Antes de poder hacer nada con él, es necesario cargar el objeto Class. Por tanto, el archivo .class para ese tipo concreto deberá seguir estando disponible para la JVM, bien en la máquina local o a través de la red. Por tanto, la verdadera diferencia entre RTf 1 y la reflexión es que, con la RTf!. el compilador abre y examina el archivo .c1ass en tiempo de compilación. Dicho de otra fonna, podemos invocar tod os Jos métodos de un objeto de la forma "nornlal". Con el mecanismo de reflexión, el archivo .c1ass no está disponible en tiempo de compilación, sino que el que lo abre y examina es el entorno de tiempo de ejecución.
Un extractor de métodos de clases Nom13 lmente, no vamos a necesitar utilizar las herramientas de reflexión directamente, pero sí que pueden resultar útiles cuando necesitemos crear código más dinámico. La reflexión se ha incluido en el lenguaje para soportar otras características de Java, como la serialización de objetos y JavaBeans (ambos temas se tratan posterionnente en el libro). Sin embargo, hay ocasiones en las que resulta muy úti l extraer dinámicamente la infonnación acerca de una clase. Consideremos el caso de un extractor de métodos de clases. Examinando el código fuente de la definición de una clase o la documentación del JDK, sólo podemos conocer los métodos definidos o sustiruidos dentro de dicha definición de clase. Pero puede haber otra docena de métodos disponibles que procedan de las clases base. Localizar estos métodos es muy tedioso
376 Piensa en Java y requiere mucho ti empo ]. Afortunadamente, el meca nismo de reflexión proporciona una fonna de escribir una herramien_
tas simple que nos muestre automáticamente la interfaz completa. He aquí la fonna en que funciona : jj: typeinfojShowMethods.java jj Utilización de la reflexión para mostrar todos los métodos de una clase, jj incluso aunque los métodos estén definidos en la clase base.
II {Args, ShowMethods} import java.lang.reflect. * ; import java.util.regex.*; import static net.mindview.util.Print.*; public class ShowMethods { private static String usage "usage:\nll + "ShowMethods qualified.class.name\n ll + "To show all methods in class or:\n" + "ShowMethods qualified.elass.name word\n ll + "To search for methods involving 'word ' ''; private static Pattern p = Pattern . compile{II\\w+\\ ." )¡ public statie void main {String (] args) { if (args .length < 1)
{
print(usage) ¡ System.exit (O) ¡ int lines = O; try {
Class c = Class.forName{args[O]); Method(] methods = e . getMethods() i Construetor(] ctors = e.getConstruetors(); if(args.length == 1 ) {
for(Method method methods) print{ p. mateher (met hod. toString () ) . replaeeAll (" " ) ) i for{Construetor etor : ctors) print {p . mateher (etor. toString () ) . replaceAll (II") ) ¡ lines = methods.length + ctors.length¡ else {
for(Method method : methods) if(rnethod.toStringll .indexOf(args[l])
!= -1)
{
print( p. matcher (method. toString () ) . replaceAll (" 11) ) ¡ lines++¡ for(Construetor etor : etors) if(ctor.toString() . indexOf(args[l])
!=
-1 ) {
print(p.mateher( etor.toString(») .replaeeAll(""»); lines++¡
eateh(ClassNotFoundExeeption e) print ( liNo such class: " + el ¡
I Especialmente en el pasado. Sin embargo. SUIl ha mejorado enonnemente su documentación HTML sobre Java, por lo que ahora es más fácil consultar los métodos de las clases base.
14 Información de tipos 377
} /* Output, public public public public public public public
static void main(String[]) native int hashCode() final native Class getClass () final void wait(long,int ) throws InterruptedException final void wait() throws InterruptedEx ception final native void wait (long) throws InterruptedException boolean equals{Object)
public String toString( ) public fina l native void notify{) public final native void notifyAll() public ShowMethods()
*/ / /
>
Los métodos getMethods( ) y getConstructors( ) de Class devuel ven una matri z de tipo Method y una matriz de tipo Constructor, respectivamente. Cada una de estas clases tiene métodos adicionales para diseccionar los nombres, argumentoS y va lores de retomo de los métodos que representan. Pero también podemos utili zar toString(), como se hace en el ejemplo, para producir una cadena de ca racteres con toda la signatura del método. El res to del código extrae la información de la línea de comandos, determina si una signatura concreta se corresponde con la cadena buscada (ut ilizando indexOf( » y elim ina los cualificadores de los nombres utilizando expresiones regulares (presentadas en el Capítulo 13, Cadenas de caracteres). El resultado producido por Class.forName() no puede ser conocido en tiempo de compilación, y por tanto toda la infonnación de signaturas de métodos se está ex traye ndo en tiempo de ejecución. Si analiza la documentación del JDK sobre el mecanismo de reflexión, verá que existe el suficiente soporte como para poder realizar una invocación de un método sobre un objeto que sea totalmente desconocido en ti empo de compilación (más adelante en el libro se proporcionan ejemplos de esto). Aunque inicialmente pueda parecer que no vamos a llegar nunca a necesitar esta funcionalidad , el va lor de los mecanismo de reflexión puede resultar ciertamente so rprendente. La salida anterior se genera mediante la linea de comandos: java ShowMethods ShowMethods
Puede ver que la salida incluye un constructor predetenninado público, aún cuando no se haya definido ningún conslmctor. El co nstmctor que vemos es el que el compilador sinteti za de forma automát ica. Si luego ejecutamos ShowMethods con una clase no pública (es decir, acceso de paquete), el constructor predeterminado sintetizado no aparecerá en la salida. El constructor predetenninado sinteti zado recibe automáticamente el mismo acceso que la clase. Otro experimento interesante consiste en invocar java ShowMethods java.lang.String con un argumento adicional de tipo char, int, String, etc. Esta herramienta puede ahorramos mucho tiempo mientras programamos, en aq uellos casos en los que no reco rdemos si una clase dispone de un método concreto y no tengamos ganas de examinar el índice o la jerarquía de clases en la docu mentación del JDK, o bien si no sabemos, por ejemplo, si dicha clase puede hace r algo con, por ejemplo, objetos de tipo Color. El Ca pítulo 22, In/e/faces gráficas de usuario , contiene una vers ión GUl de este programa (personalizada para extraer información para componentes Swing), por lo que puede dejar ese programa ejecutándose mientras esté escri biendo código para poder realizar búsquedas rápidas. Ejercicio 17:
(2) Modifique la ex presión regular de ShowMethods.java para eliminar también las palabras clave native y final (consejo: utilice el operador OR "').
Ejercicio 18:
(1) Defina ShowMethods como una clase no pública y verifique que el constructor predetenninado sintetizado no aparece a la salida.
Ejercicio 19:
(4) En ToyTest.java, utilice la ren exión para crear un objeto Toy utilizando el constructor no predetenninado.
Ejercicio 20:
(5) Examine la interfaz de java.lang.Class en la documentación del IDK que podrá encontrar en hllp:l/java.sun.com. Esc riba un programa que tome el nombre de una clase como un argumento de la línea de comandos, y luego utilice los métodos Class para volcar toda la infonnación di sponible para esa clase. Compruebe el programa con una cJase de la biblioteca estándar y con una clase que usted mismo defina.
378 Piensa en Java
Proxies dinámicos El patrón de diseño Proxy es uno de los patrones de diseño básicos. Se trata de un objeto que insertamos en lugar del obje· te ureal" para proporcionar operaciones adicionales o diferentes, estos objetos nonnalmente se comunican con un objeto
"real", de manera que un proxy actúa típicamente como un intennediario. He aquí un ejemplo trivial para mostrar la estmc· tura de un proxy: jj : typeinfo/SimpleProxyDemo.java import static net.mindview . util.Print.*;
interface Interface { void doSomething() ¡ void somethingElse(String arg) ¡
class RealObject implements Interface { public void doSomething () ( print ( ldoSomething" ) ; public void somethingElse (St ring arg) ( print("somethingElse + arg) i 11
class SimpleProxy implements Interface { private Interface proxied¡ public SimpleProxy (Interface proxied) ( this.proxied = proxied; public void doSomething() print{"SimpleProxy doSomething") ¡ proxied.doSomething() ; public void somethingElse (String arg) { print (IISimpleProxy somethingElse 11 + arg ); proxied . somethingElse (arg) i
class SimpleProxyDemo { public static void consumer (Interface iface) iface.doSomething() ; iface. somethingElse ( "bonobo lt ) ;
(
public static void main(String[] args) ( consumer{new RealObject(»; consumer(new SimpleProxy(new RealObject(»);
1* Output: doSomething somethingElse bonobo SimpleProxy doSomething doSomething SimpleProxy somethingElse bonobo somethingElse bonobo , /// ,Puesto que consumer() acepta una Interface, no puede saber si está conteniendo un objeto real RealObj ect o un Proxy, porque ambos implementan Interface. Pero el Proxy, que se ha insertado entre el cliente y el objeto ReaIObjec!, reali za operaciones y luego invoca el método idéntico de RealObjec!.
14 Información de tipos 379
Un prOJ.y puede ser útil siempre que queramos incluir operaciones adicionales en un lugar distinto que el propio "objeto fea!" , y especialmente cuando queramos poder cambiar fácilmente entre una situación en la que se usen esas operaciones adicionales Y otra en la que no se empleen, y viceversa (el objeto de utilizar patrones de diseño consiste en encapsular los cambios, así que sólo si se tienen que efectuar modificaciones para justificar el uso de un patrón). Por ejemplo, ¿qué sucede si quisiéramos controlar las Llamadas a los métodos del objeto RealObject , o medir la ca rga de procesamiento asociada a dichas llamadas? Este tipo de código no conviene incorporarlo en la aplicación, por lo que un proxy nos pennite añadirlo y eliminarlo fácilmente. El concepto pro.\y dinámico de Java lleva el concepto de pro.\ y un paso más allá, tanto porque crea el objeto dinámicamente cuanto porque gestiona dinámicamente las llamadas a los métodos para los cuales hemos insertado un pro.\ y. Todas las llamadas realizadas a un proxy dinámico se redirigen a un único gestor de invocaciones, cuya tarea consiste en descubrir qué es cada llamada y en decidir qué hacer con ella. He aquí el programa SimpleProxyDemo.java reescrito para utilizar un prD.\y dinámico:
JI : typein f o/SimpleDynamicProxy . java import java .lang.reflect. *¡ class DynamicProxyHandler implements InvocationHandler private Obj ect praxied¡ public Dynami cProxyHandler (Object proxied) { this .proxied = proxied¡ publ i c Object invoke(Object proxy, Method method, Obj e ct[) args) throws Throwable { System.out.println(" **** proxy: " + proxy.getClass( ) + ", methad: 11 + methad + " , args : 11 + args) ¡ if(args != null) for {Object arg : args) System . out .println(" 11 + arg) ¡ return methad .invoke(proxied, args);
class SimpleDynamicProxy { public static void consumer{Interface iface) iface.doSomething{) ¡ iface.somethingElse{lbonobo") ¡
{
public static void main{String[] args) RealObject real = new RealObject() ¡ consumer (real) i IJ Insertar un proxy y llamar de nuevo: Interface proxy = ( Interface )Proxy.newProxyInstance( Interface.class.getClassLoader() , new Class [J { Interface. class }, new DynamieProxyHandler(real)); consumer(praxy) ¡
1* Output: (95% match) doSamething somethingElse bonabo **** proxy: class $ProxyO, methad: public abstraet void Inter faee.doSomething (), args: null doSomething **** praxy: class $ProxyO, methad: public abstraet vaid Interface. samethingElse (java .lang.String) , args: [Ljava.lang.Object¡@42e816 banaba
380 Piensa en Java
somethingElse bonobo
, /// ,Para crear un proxy dinámico se invoca el método estáti co Proxy.ncwProxyInstance(), que requiere un cargador de clases (generalment e, podemos pasa rle un cargador de clases de un objeto que ya haya sido cargado), una lista de interfaces (no clases ni clases abstractas) que queramos que el proxy implemente y una implementación de la interfaz InvocationHandler (gestor de in vocaciones). El prmy dinámico rediri girá todas las llamadas al gestor de invocaciones, de modo que al cons· tmclOr para el gcslO r de in vocac iones usualmente se le entrega la refe rencia al objeto " real" para que pueda redirigirle las solicintdes una vez que haya terminado de llevar a cabo su tarea intermediaria. Al método invoke( ) se le pasa el obj eto prmy. en caso de qu e necesitemos distinguir de dónde viene la solicitud, (aunque en muchos casos esto no nos preocupará). Sin embargo, tenga cuidado cuando in voq ue métodos del pro.\y dentro de invoke(), porque las llamadas a tra vés de la interfaz se redirigen a través del proxy. ~n
general. lo que haremos será reali za r la operación intermediario y luego usa r Method.invoke() para red irigir la soliciJd hacia el objeto real pasá ndole los argume nt os necesarios. Puede que esto parezca a primera vista algo limitado, co mo si ólo se pudieran realizar operaciones genéricas. Sin embargo, podemos filtrar ciertas llamadas a métodos, dejando pasa r las tras directamente: 11: typeinfo/SelectingMethods . java
II Búsqueda de métodos concretos en un proxy dinámico. import java.lang.reflect. * ; import static net.mindview.util.Print .* ; class MethodSelector i mplements InvocationHandler private Object proxied; public MethodSelector{Object proxied) ( this.proxied = proxied; public Object invoke{Object proxy, Method method, Object(] args) throws Throwable { if (method . getName () . equals ("interesting") ) print ( nproxy detected the interesting method"); return method. invoke (proxied , args) i
interface SomeMethods void boringl(); void boring2(); void interesting(String arg); void boring3 () ;
class Implementation implements SomeMethods { public void boringl () { print ( lI boringl " ) i } public void boring2() ( print("boring2") i } public void interesting (String arg) { print (" interesting " + arg); public void boring3 ()
( print (Uboring3") ;
class SelectingMethods ( public static void main(String(] args) { SomeMethods proxy= (SomeMethods)Proxy.newProxylnstance ( SomeMethods . class . getClassLoader() , new Class[] { SomeMethods.class }, new MethodSelector(new Implementation() )) ;
14 Información de tipos 381 proxy.boringl()
i
proxy.boring2() ; proxy. interesting ("bonoba")
i
proxy.boring3 {) ; / * Output: boringl boring2 Proxy detected the interesting methad
interesting banaba boring3
* /// ,Aquí. simplemente examinamos los nombres de los métodos, pero también podríamos examinar los aspeclOs de la signatura del método, incluso podríamos buscar va lores concretos de los argumentos. El proxy dinámico no es una herramienta para utilizarla todos los días. pero pennite reso lver ciertos tipos de problemas muy elegantemente. Puede obtener más detalles acerca del patrón de diseño Pro.\y y de otros patrones de diseño en Thinking in Palterns (,'éase H'H'wA1indView.net) y Design Palterns, de Erich Gamma el al. (Addison- Wesley, 1995). Ejercicio 21:
(3) Modifique SimpleProx yDemo.java para que mida los tiempos de llamada a los métodos.
Ejercicio 22:
(3) Modifique SimpleDynamicProxy.java para que mida los tiempos de llamada a los métodos.
Eje rci cio 23:
(3) Dentro de invoke( ) en SimpleDyna mic Prox y.ja va, trate de imprimir el argumento proxy y explique lo que sucede.
P royecto :2
Escriba un sistema utilizando proxies dinám icos para implementar transacciones, donde el proxy se encargue de confirmar /0 transacción si la llamada realizada al objeto real tiene éxito (no genera ninguna excepción), debiendo anular /a transacción si la llamada falla. La continnación y anu lación deben funcionar como un archivo de texto ex temo, que se encuentra fuera del control de las excepciones Java. Tendrá que prestar atención a la atomicidad de las operaciones.
Objetos nulos Cuando se utili za el valor predetenninado null para indicar la ausencia de un objeto, es preciso comprobar si las referencias son iguales a null cada vez que se utiliza. Esta labor puede llegar a ser muy tediosa y el código resultante es muy complejo. El problema es que nuH no tiene ningún comportamiento propio, salvo generar una excepción NullPointerExcepti on si se intenta hacer algo con el valor. Algunas veces. resulta úti l introducir la idea de un objeto nu/0 3, que aceptará mensajes en lugar del objelO al cual "represema", pero que devo lverá valores indicando que no existe ahí ningún objeto "real". De esta fomla, podcmos asumir que todos los objetos son vá lidos y no tenemos porqué desperd iciar tiempo de programación comprobando la igualdad con nuJl (y leyendo el código resultante). Aunque resulta di vertido imaginarse un lenguaje de programación que cree automáticamente objetos nulos por nosotros, en la práctica no tiene sentido usarlos en todas partes; en ocasiones, será adecuado realizar las comprobaciones de valor l1ull, en otros casos podremos asumir razonablemente que no vamos a encontrarnos con el valor null, e incluso, en otras ocasiones, será perfectamente aceptable detectar las aberraciones a través de NullPoin terExceptio n. El lugar donde los objetos nulos parecen ser más útiles es en "el lugar más próximo a los datos", con objetos que representen entidades cn el espacio del problema. Como ejemplo simple, muchos sistemas dispondrán de una clase Per sono y hay situaciones en el código en las que no di sponemos de una persona real (o sí di sponemos de ella. pero no tenemos todavía toda la infomlación acerca de dicha persona), por lo que tradicionalmente utilizaríamos una referencia null y comprobaríamos si las referencias son nulas. En lugar de ello, podemos crear un objeto nulo, pero aún cuando el objeto nulo responderá a lodos los mensajes 2 Los proyeclOs son sugerencias que pueden utilizarse. por cjcmplo. como uabajo!> dc clasc. Las soluc iones a los proyectos no se incluyen en la guía de soluciones . Descubieno por Bobby Woolfy Bmce Andcrson. Puede verse como un caso especial del patrón de diseño basado en eslr(l/egill. Una variante del ObjelO t/erador Nlllo. que hace que la interacción a lrav¿s de los nodos en una jerarquía complle~la sea transparente para el cliente (el cliellle puede elllonces utilizar la misma lógica para iterar a través de lajer.:¡rquía compuesta y a traves de los nodos hoja).
.3
Nlllo es el patrón de disello de
382 Pien sa en Java
a los que el objeto " real" respondería, sigue siendo necesario disponer de una manera de co mprobar si hay valores nul os. La forma más simple de hacer esto consiste en crear una interfaz de marcado: //: net/mindview/util/Null . java package net . mindview.util¡ public interface Null {) 111,-
Esto permite a insta nceof detectar el objeto nulo, y lo más importante, no requi ere que afiadamos un mérodo is Null() a todas nuestras clases (lo cual sería, después de todo. simplemente otra forma de utilizar mecanismos RTT I, ¿por qué no utili zar la funcionalidad integrada en su lugar?). // : typeinfo/Person.java // Una clase con un objeto Null . import net.mindview.util.*¡ class Person { public final String first¡ public final String last; public final String address¡ II etc. public Person(String first, String last, String address){ this.first = first¡ this.last = last¡ this.address = address¡ public String toString () { return "Person: " + first + "
"
+ last + "
" + address;
public sta tic class NullPerson extends Person implements Null private NullPerson() { super("None", "None", "None") ¡ public String toString() { return "NullPerson"; } public static final Person NULL = new NullPerson() ¡
/! 1 ,En general. el objeto nu lo se rá un objeto simple (no una serie de objetos agrupados en contenedores), por lo que aquí se crea como una instancia estática final. Esto fun ciona porq ue Perso" es inmutable: sólo podemos fijar los va lores en el co nstructor, y luego leer d ichos valores, pe ro no modi fi carlos (porq ue los propios String son inherentemente inmutables). Si desea modificar un objeto NullPerson , sólo puede susti tuirl e con un nuevo objeto Perso n oObserve que dispo nemos de la opción de detectar el genérico Null o el más específico NullPerson utilizando instanceof, pero como se trata de un va lor simp le también podemos utilizar equals( ) o incluso = pa ra com parar co n Pcrson.NULL. Aho ra suponga que estuviéramos en la época dorada de las empresas de Internet y que alguien hubiera invertido una gran cantidad de dinero en una maravi llosa idea que hub iéramos ten ido. imagine que estamos listos para recl utar personal pero que. mientras espe ramos a que las vacantes sean cubiertas. podemos util iza r objetos nul os Perso" para asignarlos a cada puesto de lrabajo (Posítioll): // : typeinfo/Position.java class Position { private String title¡ private Person person¡ public Position (String jobTitle, title = jobTitle¡ person = employee; if(person == null) person = Person.NULL¡ public Position{String jobTitle) title = jobTitle;
Person employeel
{
14 Información de tipos 383
person
=
Person . NULL;
public String getTitle() { return title; public void setTitle (String newTitle) { title = newTitle¡
publ ic Person getPerson () { return person; public void setPerson(Person newPerson) { persan = newPerson; i f {pe rs on == null) person = Person.NULL;
public String toString(} return "Position :
" + title + "
It
+ person¡
} 111 > Con Position , no ten emos necesidad de crear un objeto nulo, porque la existencia de Person.NULL implica un objeto Position nulo (es posible que más adelante descubramos que sí se necesita añadir un objeto nulo explícito para Position. pero hay una regla qu e dice que siempre debemos implementar la solución más simple que funcione en nuestro primer diseño, y esperar a que algún aspecto del programa requiera que añadamos la característica adicional, en lugar de asumir desde el principio que esa característica es necesaria).4
La clase Staff puede ahora buscar los objetos nulos a la hora de rellenar las vacantes:
1/ : typeinfo/Staff. java import java . util .*¡ public class Staff extends ArrayList { public void add{String title, Person person) { add(new Position{title, person) ) ¡ public void add{String . titles) for{String title : titles) add{new Position(title)) ¡ public StaffIString . .. titlesl ( addltitles); } public boolean positionAvailable (String title) { for(Position position this) if (pos ition.getTitle () .equals(title ) && position.getPerson() == Person.NULL) return true; return false¡ public void fillPosition{String title, Person hire) for(Position position this) if(position.getTitle() .equals (title) && position.getPerson() == Person.NULL) position. setPerson (hire) ; return¡
{
throw new RuntimeException( "Position " + title + " not available"); public static void main (Str ing [1 args) { Staff staff = new Staff("President", "CTOII, "Marketing Manager", "Product Manager", "Project Lead", "Software Engineer", 4
Esa tendencia a implementar la solución más simple posible es una de las recomendaciones de Extreme Programmillg (XP).
384 Piensa en Java
"Software Engineer", "Software Engineer", "Software Engineer", "Test Engineer", "Technical Writer"); staff.fillPosition("President", new Person(IOMe", "Last", "The TOp, Lonely At"))¡ staff. fillPosition ("Project Lead", new Person( nJanet", "Planner", "The Burbs")); if(staff.positionAvailable("Software Engineer"l) staff. fillPosition ("Software Engineer", new Person ( "Bob", "Coder", "Bright Light City")); System.out.println(staffl ¡
1* Output: [Position: President Person: Me Last The Top, Lonely At, Position: CTO NullPerson, Position: Marketing Manager NullPerson, Position: Product Manager NullPerson, Position: Project Lead Person: Janet Planner The Burbs, Position: Software Engineer Person: Bob Coder Bright Light City , Position: Software Engineer NullPerson, Position: Software Engineer NullPerson, Position: Software Engineer NullPerson, Position: Test Engineer NullPerson, Position: Technical Writer NullPerson] * /// > Observe que sigue siendo necesario comprobar la existencia de objetos nulos en algunos lugares, lo cual no difiere Illucho de comprobar la igualdad con el valor null, pero en otros lugares (como en las conversiones toString( ), en este caso). no es necesario realizar comprobaciones adicionales; podemos limitarnos a asumir que todas las referencias a objetos son válidas. Si estamos trabajando con interfaces en lugar de con clases concretas. es posible utilizar un objeto DynamicProxy para crear automáticamente los objetos nulos. Suponga que tenemos una interfaz Robot que define un nombre. un modelo y una lista List que describe lo que el Robot es capaz de hacer. Operation contiene una descripción y un comando (es un tipo del patrón de disetio basado en comandos):
11 : typeinfo/Operation.java public interface Operation String description() ¡ void command () ;
/// , Podemos acceder a los servicios de un objeto Robot invocando operations( ): // : typeinfo /Robot.java import java . util .* ; import net.mindview.util.*; public interface Robot String name () ; String model () ; List operations() ¡ class Test { public static void test (Robot r) { if(r instanceof Null) System.out.println(" (Null Robot} ") ¡ System . out.println("Robot name: + r . name(}); System. out . println ("Robot model: " + r. model () } ; for(Operation operation : r.operations(}) { System.out . println{operation . description()) ; operation.command() ; 11
} /l/o-
14 Información de tipos 385
Esto incorpora tambi én una clase ani dada para realizar las pruebas. Ahora podemos crear un objeto Robot espec iali zado:
/1: typeinfojSnowRemovalRobot . java import java.util. * ; public class SnowRemovalRobot implements Robot { private String name; public SnowRemovalRohot (String name) { this . name "" name;} publ ic S t ring name () { return name ; } publ ic String model () { return "SnowBot Se ries 11 " i } public Lisc operations () { return Arrays . asList{ new Operatian() {
public String description() return name + " can shove l snow";
public void cornmand{) System . out .prin tln(name + " s h oveling snow" );
}, n e w Operatian () public String description () return name + " can chip ice";
public void command() System.out.println(name }
+
I!
chippi ng ice " );
}, new Operation() public String desc ri ption(} return name + " can clear the roof"; public void command() System . out.println(name + " cl e aring roof " ) i
1; public static va id main (String [J args) { Robot . Test.test(new SnowRemovalRobot("Slusher")}
i
/ * Output: Robot na me: Slusher Robot model : SnowBot Series 11 Slusher can shove l snow Slusher shoveling snow Slushe r can chip ice Slushe r chipping ice Slusher can clear the roof Slushe r clearing roof */// , Existirá n presum iblemente muchos tipos diferentes de objetos Robot , y lo que nos gustaría es que cada objeto nulo hiciera algo especial para cada tipo de Robot; en este caso, incorporar infonnación acerca del tipo exacto de Robot que el objeto nul o representa. Esta información será capturada por el pro.\y dinámico: 11 : t ypeinfo/NullRobot.java /1 Util i zac i ón de un p r oxy d inámico para c rear un objeto nulo .
386 Piensa en Java import java.lang.reflect.*; import java.util.*; import net.mindview.util.*¡ class NullRobotProxyHandler implements InvocationHandler private String nullName; private Robot proxied = new NRobot(); NullRobotProxyHandler(Class type ) nullName = type .getSimpleName () + Ir NullRobot"; private class NRobot implements Null, Robot { public String name() { return nullName¡ } public String model () { return nullName ¡ } public List operations () { return Collections.emptyList{);
public Object invoke{Object proxy, Method method, Object[] args) throws Throwable { return method. invoke {proxied, args) ¡
public class NullRobot public sta tic Robot newNullRobot{Class type) return (Robot)Proxy.newProxylnstance( NullRobot.class .getClassLoader() , new Class[] { Null.class, Robot.class }, new NullRobotProxyHandler(type» i public static void main(String(] args) Robot [) bots = { new SnowRemovalRobot ( "SnowBee"), newNullRobot{SnowRemovalRobot.classl
}; for(Robot bot : bots) Robot.Test.test(bot) ;
1* Output : Robot name: SnowBee Robot model: SnowBot Series 11 SnowBee can shovel snow SnowBee shoveling snow SnowBee can chip ice SnowBee chipping ice SnowBee can clear the roof SnowBee clearing roof [Null Robot) Robot name: SnowRemovalRobot NullRobot Robot model: SnowRemovalRobot NullRobot * /// > Cuando se necesita un objeto Robot nulo, simplemente se invoca newNullRobot( ), pasándole al método el tipo de Robot para el que queremos que actúe como proxy. El prox)' satisface los requisitos de las interfaces Robot y Null. y proporciona el nombre específico para el que actúa como pro.\)'.
14 Información de tipos 387
Objetos maqueta y stubs Hay dos tipos \ ariantes del objeto nulo: el objeto maq/leta y el SI/lb. Al igual que el objeto nu lo. ambos tipos de objetos se utilizan en lugar del objeto "real" que empleará el programa terminado. Sin embargo. tanto el objeto maqueta como el sllIb pretenden se r objetos vivos que entregan infonl18Ción real en lugar de ser un sustituto un poco más inteligente de null, como es el caso del objeto nulo. La diferenc ia entre el objeto maqueta y un s/IIb es bastante sutil. Los objetos maqucla tienden a ser ligeros (poco complejos) y tienen capacidad de auto-comprobación. y usualmente se crean muchos de ellos para gestionar di stintas situaciones de prueba. Los swbs son típicamente más pesados y a menudo se reutilizan entre una pmeba y otra. Los stubs pueden configurarse para cambiar de comportamiento. dependiendo de cómo se los invoque. Por tanto. un slIIb es un objeto sofisti cado que lleva a cabo una de esas tareas; mientras que para hacer esas mismas ta reas con objetos maqueta lo que nomlalmente haríamos es crear muchos objetos maqueta pequClios y simples. Ejercicio 24:
(4) Aliada objetos nulos a RegistercdFactories.java.
Interfaces e información de tipos Un objetivo clave de la palabra clave interface es pemlitir al programador aislar componentes. reduciendo así el acopIamiento. Si escribimos el código basándonos en interfaces. conseguimos este objetivo. pero con la información de tipos es posible saltarse los controles: las interfaces no So n una garantía de desacoplamiento. He aquí un ejemplo comenzado con una interfaz:
11 : typeinfo / interfacea / A.java package typeinfo.interfacea¡ public interface A { void f 1) ;
} 111 ·Esta mterfaz se implementa a continuación y podemos '"er fácilmente cómo saltamos los controles para obtener el tipo real de implementación:
11 : typeinfo / lnterfaceVio lation.java 11 Sorteando una interfaz. import typeinfo.interfacea.*; class B implements A public void f 1) public void 9 1)
{} {}
public class InterfaceViolation { public static void main (String [) args) { A a = new B () ¡ a . f 11 ; 11 a.g()¡ 11 Error de compilación System.out . println(a.getClass() .getName()); ifla instanceof E) { B b =
lB) a;
b . gl) ;
1* Output:
Uti li zando RTfl, descubrimos que a ha sido implementado como R. Proyectando sobre R, podemos invocar un método que no se encuentre en A.
388 Piensa en Java
Esto es perfectamellle legal y aceptable. pero puede que no queramos que los programadores de clientes hagan esto, ya que esto les da la opornmidad para acoplarse más estrechamente con nuestro código de lo que querríamos. En otras palabras, podríamos pensar que la palabra clave interface nos está protegiendo. pero en realidad no es así, y el hecho de que utili ccmas B para implementar A en este caso es algo de dominio público. 5 Una solución consiste simplemente en decir que los programadores serán los responsab les si deciden utilizar la clase real en lugar de la interfaz. Esto es probablemente razonable en muchos casos, pero si ese " probablemente" no es suficiente, Conviene apli car otros controles más estrictos. La técnica más sencilla consiste en uti lizar acceso de paquete para la implemclllación, de modo que los clientes situados fuera del paquete no puedan verla: jj : typeinfo/packageaccess/HiddenC.java package typeinfo.packageaccess; import typeinfo.interfacea .* ¡ import static net.mindview.util.Print.*;
class C implements A { public void f () { print ( "public C. f () " ) ; public void g{) { print("public C.g()"); void u()
{ print("package C.u()");
)
protected void v () { print ( "protected c. v () " ) ; priva te void w() { print("private C .W {) H) ¡ }
public class HiddenC { public static A makeA ()
{ return new C () ¡ }
) /// , La única parte pública de este paquete, HiddenC, produce una interfaz A cuando se la invoca. Lo que es interesante acerca de este ejemplo es que incluso si devolviéramos un objeto C desde ma keA( ), seguiríamos sin poder utilizar ninguna otra cosa distinta de A desde fuera del paquete, ya que no podemos nombrar e fuera del paquete. Ahora, si tratamos de efectuar una especialización sobre fuera del paquete:
11 :
e, no podemos hacerlo, porque no hay ningún tipo
'C' disponible
typeinfo/Hiddenlmplementation.java
II Sorteando el acceso de paquete. import typeinfo . interfacea.*¡ import typeinfo.packageaccess.*¡ import java.lang.reflect.*; public class Hiddenlmplementation public static void main{String(] args) throws Exception ( A a = HiddenC.makeA{); a . E () ; System. out. println (a. getClass () . getName () ) ¡ II Error de compilación: no se puede encontrar el símbolo 1* if(a instanceof C) { e e = (C) a; c. 9 () ;
ICI:
*/ II ¡Caramba! La reflexión nos permite invocar g(): callHiddenMethod (a, "glI ); II ¡E incluso métodos que son menos accesibles!
5 El caso mas famoso cs el sistema operativo Windo\Vs. que tenía una API publica con la que se suponía que había que desarrollar programas y un conjunto no publicado pero visible de funciones que podiamos descubrir e invocar. Para resolver los problemas. los programadores utilizaban las funcione s ocuttas de la API , lo que forzó a Microsoft a mumcnerlas como si flleran pane de la API pública. Esto se convini6 en una fuente de grandes costes y de enonne trabajo para la empresa.
14 Información de tipos 389 callHiddenMethod(a, callHiddenMethod (a, callHiddenMethod (a,
"u");
"v"); "w");
static void callHiddenMethod(Object a, String methodName) throws Exception { Method 9 = a. getClass () . getDec!aredMethod (methodName) ; g.setAccessib l e{true ) ; g. invoke (a) ;
/* Output: public c. f () typeinfo.packageaccess.C public c . 9 () package c. u () protected C. v () prívate C. w ()
*///,Como puede ver, sigue siendo posible meterse en las entrañas e invocar lodos los métodos utili zando el mecanismo de reflexi ón. ¡Incluso los Inétodos pri vados! Si se conoce e l nombre del método, se puede in vocar setAccessible(tru e) sobre el objeto Meth od para hacerlo ¡nvocable, como podemos ver en caIlHiddenMethod(). Podríamos pensar que es posible impedi r esto distribuyendo sólo el código compilado, pero no es un a solución. Basta con ejecutar j a va p, que es el descompilador incluido en el J DK. He aqu í la línea de comandos necesari a: j avap -pri vate e
El indi cador -pri vate especifica que deben mostrarse todos los m iembros, incluso los privados. He aquí la salida que se obtiene: class typeinfo.packageaccess.C extends java.lang.Object impl e ments typeinfo.interfacea.A typeinfo . packageaccess . C{) ; public void f () ; public void g(); void u () ; protected void v() i private void w() i
Por tanto, cua lquiera puede obtener los nombres y signaturas de los métodos más pri vad os e invoca rl os. ¿Qué sucede si implementamos la interfaz con un a clase int ern a privada? He aqu í un ej empl o:
11: typeinfo/lnnerlmplementation . java II Las clases interna privadas no pueden ocultarse del mecanismo de II reflexión. import typeinfo.interfacea.*; import static net . mindview.util.Print.*¡ class InnerA { private static class e implements A { public void f (1 { print ( " public c. f () "1 ; public void 9 (1 { print ("public c . 9 () ") ; void u() ( print("package e.u()"I ; ) protected void v () { print ("protected c . v () ") ; private void w() { print("private C.w() "); } public static A makeA ()
{ return ne w C(); }
390 Piensa en Java
public class Innerlmplementation public static void main(String[] args) chrows Exception { A a = InnerA.makeA()¡ a. f 1) ; Syscem.out . println(a . getClass(} . getName()) ¡ II La reflexión sigue permitiendo entrar en la clase privada: Hiddenlmplementation . callHiddenMethod (a, "g") ¡ Hiddenlmplementation . callHiddenMethod (a "u " ) ¡ Hidde nlmplementation . callHiddenMethod (a "v") ¡ Hi ddenlmplemen ta t ion . callHi ddenMet hod (a "w") ¡ I
I
I
1* Oucput : public e. f l) I nne rA$C p u blic e . g 11 package C. u() protected C. v () private C.w () ' jjj >
Esta solución no nos ha penni tido ocultar nada a ojos del mecanismo de reflexión. ¿Qué sucedería con una clase anónima?
11 : typeinfo / Anonyrnouslmplementation.java II Las c lases internas anónimas no pueden ocultarse del mecanismo de II reflex i ón. import typeinfo . interfacea . *¡ import stat i c net . mindvi ew.util.Print.*¡ class AnonyrnousA { public static A makeA () return new A () { publi c void f 11 ( print I "public e. f 11 ,, ) ; public void g il ( print I "publi c e . g il " ) ; v o id u 11 ( print I "package e. u 1) " ) ; } protected void v () { print ( "prot ected C. v () " ) ¡ private void w() { print ( "private C.w () " ) i }
};
public class Anonyrnouslmplementation { publi c stat ic void main (String[) args ) throws Ex ception { A a = AnonymousA.makeA () i a. f 11 ; System . ou t . println (a.getClass () .getName ()) ¡ II La reflexión sigue pudi e ndo entrar en la clase anónima : Hiddenlmplementation.caIIHiddenMethod (a, "g" ) ; Hiddenlmplementation. callHi ddenMethod (a , "u" ) ; Hi ddenlmplementation. callHiddenMethod (a, "VII ) ; Hidden l mpleme nt a tion. cal l Hidde nMethod ( a, IIW tl ) ;
1* Output: public e . f l) AnonymousA$l pub lic e . g l) package C. u () protected C. v () private C. w() ' //j , -
14 Información de tipos 391 Parece que no existe ningu na faffila de impedir que el mecanismo de reflexión entre e invoque los métodos qu e no tienen acceso público. Esto también se cumple para los campos. inc luso para los campos pri vados:
JI : typeinfo / ModifyingPrivateFields.java import java.lang.reflect.*i class WithPrivateFinalField private int i = 1; private final String s = "1 'm totally safe" i private String 52 = "Am 1 safe?" i public String toString () { return 11 i = 11 + i + ", 11 + S + 11 + 52;
public class ModifyingPrivateFields { public static void main(String[] argsl throws Exception { WithP r ivateFinalField pf = new WithPrivateFinalField(); System . out . println(pf) i Field f
= pf. getClass () . getDeclare d Field (" i " ) ;
f . setAccessible(true) ; System . out.println("f . ge t 1nt( pf l:
" + f . g et1 nt( pf l);
f . set ln t(pf,47};
System . out.println(pf) ; f = p f. getC l ass() .get DeclaredField("s"); f . setAccessible(true) ; System.out . println("f . get(p f } ,
" + f . get(pf}};
f . se t (pf, "No , you' r e not ! " ) ; Sy st em.out.println(pf ) ; f = pf. getClass () . getDeclaredF ield ( 11 s2 11 1 ; f . s etAccess i bl e (true) ; Sys tem. out.p r int ln ( lIf. g e t( pf ) : 11 + f. g e t( pf )); f . set (pf, "No, you' re not! " ) ; System.out . println(pf ) i
/ * Outp ut : i
=
1, 1 'm totally safe, Am I saf e?
f .getInt (pf) , 1 = 47, I ' m totally sa f e, Am I safe? f .get (pf) : 1 'm totally sa f e i = 47, I ' m totally safe, Am I safe? f.get(pf) : Am 1 safe? i = 47, I' m tota ll y safe, No , you 're not !
i
*/// , Sin embargo, los campos de tipo final sí que están protegidos frente a los cambios. El sistema de tiempo de ejecución acepta los intentos de cambio sin quejarse, pero no se produce cambio alguno. En general , todas estas violaciones de acceso no constituyen un problema grave. Si alguien utiliza una de estas técnicas para invocar métodos que se han marcado como privados o con acceso de paquete (lo cual indica claramente que no deberían invocarse), entonces es dificil que esas personas puedan quejarse si decidimos cambiar posterionnente algunos aspectos de esos métodos. Por otro lado, el hecho de que siempre exista una puerta trasera para entrar en una clase nos pennite resolver ciertos tipos de problemas que en otro caso serían dificiles o imposibles, y los beneficios del mecanismo de reflexión son, por regla general, incuestionables. Ejercicio 25:
(2) Defina una clase que contenga métodos privados, protegidos y con acceso de paquete. Escriba código para acceder a dichos métodos desde fuera del paquete de la clase.
392 Piensa en Java
Resumen RTTI nos pennite descubrir la infonnación de tipos a partir de una referencia anónima a una clase base. Es por ello que se presta a una inadecuada utilización por los usuarios menos expertos, ya que resulta más fácil de comprender que las llamadas polimórficas a métodos. Para las personas que tienen experiencia previa en lenguajes procedimentales, resulta difícil organizar los programas en conjuntos de instrucciones switch. Este tipo de estmctura puede implementarse fácilmente con RTTI perdiéndose así el importante valor que el polimorfismo añade al desa rrollo y el mantenimiento del código. La intención de la programación orientada a objetos es utili zar llamadas polimórficas a métodos siempre que se pueda y RTTI sólo cuando no haya más remedio. Sin embargo, las llamadas polimórficas a métodos, tal como está prevista en el lenguaje, requiere que tengamos control de la definición de la clase base, porque en algún punto dentro del proceso de extensión del programa podemos llegar a descubrir que la clase base no incluye el método que necesitamos. Si la clase base proviene de una biblioteca desalTollada por algún otro programador, una so lución es RTTI: podemos heredar un nuevo tipo y ailadir el método adicional que necesitamos. En el resto del código podemos entonces detectar ese tipo concreto que hemos añadido y llamar a ese método especial. Esto no destmye el polimorfismo y la extensibilidad del programa, porque el aiiadir un nuevo tipo no requiere que andemos a la caza de instmcciones switch en nuestro programa. Sin emba rgo, cuando añadamos código que dependa de la nueva funcionalidad aiiadida, nos veremos obligados a util izar RTTI para detectar el tipo concreto que hayamos definido. Añadir una funcionalidad en una clase base puede implicar que, a cambio de obtener exclusivamente un beneficio en esa clase concreta, todas las demás clases derivadas de la misma deberán ca.rgar con un esqueleto de método completamente carente de significado. Esto hace que la interfaz sea menos clara y resulta bastante molesto para aque llos que se ven obligados a sustituir métodos abstractos cuando derivan otra clase a partir de esa clase base. Por ejemplo, considere una jerarquía de clases que representa instrumentos musicales. Suponga que desea limpiar las válvulas de las boquillas de todos los instrumentos apropiados de su orquesta. Una opción es incluir un método IimpiarValvula() en la clase base Instrumento, pero esto resulta confuso, porque implicaría que los instrumentos de Percusión, Cuerda y Electrónicos también tienen boquillas y vá lvulas. RTTI proporciona una solución mucho más razonable, porque nos pennile colocar el método en la clase específica donde resulta aprop iado (Viento, en este caso). AlmLsmo tiempo, podemos descubrir que existe una solución más lógica, que en este caso consistiría en incluir un método prepararlnstrumento(). Sin embargo, puede que no veamos esa solución cuando estemos tratando por primera vez de resolver el problema y, como consecuencia, podríamos asumir erróneamente que es necesario utilizar RTTI. Finalmente, RTTI pennite en ocasiones resolver problemas de eficiencia. Suponga que nuestro código utili za apropiadamente el polimorfismo, pero resulta que uno de los objetos reacciona a este código de propósito general de una manera terriblemente poco eficiente. Podemos detectar ese tipo concreto de objeto utilizando RTTI y escribir código específico para mejorar la eficiencia. No caiga en la tentación, sin embargo, de estructurar sus programas demasiado pronto pensando en la eficiencia. Se trata de una trampa bastante tentadora. Lo mejor es conseguir primero que el programa funcione y luego decidir si está funcionando lo suficientemente rápido. Sólo entonces deberemos abordar los problemas de eficiencia con una herramienta de perfilado (consulte el suplemento en htrp://MindView.net/Books/Be((erJava). También hemos visto que el mecanismo de reflexión abre un nuevo mundo de posibilidades de programación, pemlitiendo un esti lo de programaci ón mucho más dinámico. Existen programadores para los que la naturaleza dinámica del mecanismo de reflexión resulta bastante perturbadora. El hecho de que podamos hacer cosas que sólo pueden comprobarse en tiempo de ejecución y de las que sólo se puede informar mediante el mecanismo de excepciones, parece, para las mentes cómodamente acostumbradas a la seguridad de las comprobaciones estáticas de tipos, algo bastante pernicioso. Algunas perso nas sostienen incluso, que el introducir la posibilidad de una excepción en tiempo de ejecución es una indicación clara de que dicho tipo de código debe evitarse. En mi opinión, esta sensación de seg uridad no es más que una ilusión, siempre hay cosas que pueden suceder en tiempo de ejecución y que pueden gene rar excepciones, incluso en un programa que no contenga ningún bloque try ni ninguna especificación de excepción. En lugar de ello, en mi opinión, la existencia de un modelo coherente de infonnación de errores nos permite escribi r código dinámico utilizando los mecanismos de reflexión. Por supuesto, merece la pena tratar de escribir código que pueda comprobarse estáticamente ... siempre que se pueda. Pero creo que el código dinámico es una de las características más importantes que diferencia a Java de otros lenguajes como C++.
Ejercicio 26:
(3) Implemente un método IimpiarV.lvula() como el desc rito en este resumen.
Puede encontrar las soluciones a los ejercicios seleccionados en el documento electrónico Tll e Thillki"g il1 1am AI1/1o/ated SO/liriO" Gllide, disponible para la venta en l\'\\'W.A lind J'ie\\:l1et.
Genéricos
Los métodos y clases ordinarios funcionan con tipos específicos: con tipos primitivos o con clases. Si lo que queremos es escribir código que pueda utilizarse con un tipo más amplio de tipos, esta rigidez puede resultar demasiado restrictiva. I Una de las fonnas en que los lenguajes orientados a objetos penniten la generalización es a través del polimorfismo. Por ejemplo, podemos escribir un método que tome un objeto de una clase base como argumento, y luego utilice dicho método con cualquier clase derivada de dicha clase base. Con ello, el método será algo más general y podrá ser utilizado en más lugares. Lo mismo cabe decir dentro de las clases: en cualquier lugar donde utilicemos un tipo específico, un tipo base proporcionará mayor flexibilidad. Por supuesto, podemos extender todas las clases salvo aquellas que hayan sido definidas como finales 2 , po r lo que esta flexibilidad se obtiene de manera automática la mayor parte de las veces. En ocasiones, limitarse a una única jerarquía puede resultar demasiado restrictivo. Si el argumento de un método es una interfaz en lugar de una clase, las limitaciones se relajan de modo que ahora se incluirán todas aquellas clases que implementen la interfaz, incluyendo clases que todavía no hayan sido desarrolladas. Esto proporciona al programador de clientes la opción de implementar una interfaz para adaptarse a nuestra clase o método. Con esto, las interfaces nos penlliten establecer un vínculo entre jerarquías de clases, siempre y cuando tengamos la opc ión de crear una nueva clase para implementar ese vínculo. Algunas veces, incluso una interfaz resulta demasiado restrictiva. Las interfaces siguen requiriendo que nuestro código funcione con esa interfaz concreta. Podríamos escribir código todavía más gene ral si el lenguaje nos permitiera decir que ese código funciona con "algún tipo no especificado", en lugar de con una interfaz o clase específicas. En esto se basa el concepto de genéricos, un cambio de los más significativos en Java SES. Los genéri cos implementan e l concepto de tipos parametrizados, que penlliten crear componentes (especialmente contenedores) que resultan fáciles de util.izar con múltiples tipos. El término "genérico" significa " peI1eneciente o apropiado para grandes grupos de clases". La intención original de los genéricos en los lenguajes de programación era dotar al programador de la mayor capacidad expresiva posible a la hora de escribir clases o métodos, relajando las rest ricciones que afectan a los tipos con los que esas clases o métodos pueden funcionar. Como veremos en este capítulo, la implementación de los genéricos en Java no tiene un alcance tan grande; de hecho, podríamos cuestionamos si el término "genérico" resulta siquiera apropiado para esta funcionalidad de Java. Si no ha visto antes ningún mecanismo de tipos parametrizados, los genéricos de Java le parecerán, probablemente, una mejora sustancial del lenguaje. Cuando se crea una instancia de un tipo parametrizado, el lenguaje se encarga de realizar las proyecciones de los tipos por nosotros y la corrección de los tipos se garantiza en tiempo de compilación. Evidentemente, parece que este mecanismo es toda una mejora. Sin embargo, si el lector ya tiene experiencia con algún mecanismo de tipos parametrizados, como por ejemplo en C++, encontrará que no se pueden hacer con los genéricos de Java todas las cosas que cabría esperar. Mientras que utilizar un tipo genérico desarrollado por alguna otra persona resulta bastante sencillo, a la hora de crear nuestros propios genéricos nos I Quiero dar las gracias a Angelika Langer por su liSIa de preguntas frecuentes Jm'G Geller¡cs FAQ (véase II'wl\:!allge,:camefOl.de), así como por sus otros escritos (hechos en colaboración con Klaus Kreft). Esos trabajos han resultado enonnementc valiosos de cara a la preparación de este capítulo. 2 O clases que dispongan de un consfructor privado.
394 Piensa en Java encontraremos con diversas sorpresas. Uno de los aspeclOs que trataremos de explicar en este capínllo son los motivos por los que la funciona lidad se ha imp lementado en Java en la manera en que se ha hecho. No queremos decir que los genéricos de Java sean inútiles. En muchos casos, consiguen que el código sea más directo e incluso más elegante. Pero, si el leclOr ha utilizado ante riomlente algún lenguaje donde esté implemenrada una versión más pura de los genéricos, puede que la solución de Java le desilusione. En este capítulo. vamos a examinar tanto las fortalezas como las debilidades de los genéricos de Java, con el fin de que el lec tor pueda utilizar esta nueva funcionalidad de manera más efectiva.
Comparación con C++ Los diseñadores de Java han dejado claro que buena parte de la inspiración del lenguaje proviene de C++. A pesa r de ello, resulta perfectamente posible enseñar a programar en Java sin hacer apenas referencia a C++. y en este libro hemos intentado hacerlo así, salvo en aquellos casos en los que la comparación puede faci litar entender mejor el lenguaje. Los genéricos requieren que realicemos una comparación más detallada con C++ por dos razones. En primer lugar, comprender ciertos aspectos de las plantillas C++ (la principal inspiración de los genéricos, incluyendo su sintaxis básica) nos pennitirá entender los fundamentos de l concepto, así como (y esto es particularnlente importante) las limitaciones que afectan a lo que se puede hacer con los genéricos de Java, y los motivos subyacentes de la existencia de esas limitaciones. El objetivo último es que el lector comprenda claramente dónde están los límites. porque entendiendo esos límites se puede llegar a ser un programador más eficiel1le. Sabiendo lo que no puede hacerse. podemos emplear mejor aquellas cosas que sí podemos hacer (en parte porque no nos vernos obligados a perder tiempo rompiéndonos la cabeza contra una pared). La segunda razón es que existen muchas concepciones erróneas en la comunidad Java acerca de las plantillas C++, y estos conceptos erróneos pueden aumentar nuestra confusión acerca del objetivo de los genéricos. Por tanto, vamos a introducir unos cuantos ejemplos de plantillas C++ en este capítulo, aunque tratando siempre de limitar al máximo las explicaciones acerca del lenguaje C++.
Genéricos simples Una de las razones iniciales más fuertes para introducir los genéricos era crear clases de contenedores, de las que ya hemos hablado en el Capítulo 11 , Almacenamiellfo de objetos (hablaremos más acerca de estas clases en el Capítulo 17. Análisis detallado de los contenedores). Un contenedor es un lugar en el que almacenar objetos mientras trabajamos con ellos. Aunque esto también es cierto para las matrices, los contenedores tienden a ser más fl exibles y sus características son distintas a las de las matrices simples. Casi todos los programas requieren que almacenemos un grupo de objetos mientras los utilizamos, por lo que los contenedores son una de las bibliotecas de clases más inherentemente reutilizables. Examinemos una clase que almacena un único objeto. Por supuesto. la clase podría especificar el tipo exacto del objeto de la fonna siguiente :
11 : generics/Holderl.java class Automobile {} public class Holderl private Automobile a¡ public Holderl(Automobile a) {this . a Automobile get {) { return a¡ }
a;}
///> Pero esta herramienta no es muy reutili zable, ya que no puede emplearse para almacenar ninguna otra cosa. Preferiríamos no tener que escribir una nueva clase de este estilo para cada tipo con el que nos encontremos. Antes de Java SES , lo que haríamos simplemente es hacer que la clase almacenara un objeto de tipo Object:
11 : generics/Holder2.java public c!ass Holder2 {
15 Genéricos 395 private Object a¡ public public public public
Holder2(Object al { this . a = a; } void set(Object al { this.a = a¡ } Object get () { return a¡ } static void main(String[] args) {
Holder2 h2 = new Holder2{new Automobile()); Automobile a = (Automobile)h2.get(); h2.set("Not an Automobile"); String s = (String) h2 .get ();
h2 . set{1); // Se transforma automáticamente en Integer Integer x
=
(Intege r )h2 .get () ;
Ahora, la clase Holder2 puede almacenar cualquier cosa y, en este ejemplo, un único objeto Hold er2 almacena tres tipos distintos de objetos. Hay algunos casos en los que queremos que un contenedor almacene múltiples tipos de objetos, pero lo más normal es que sólo coloquemos un tipo de objeto en cada contenedor. Una de las principales motivaciones de los genéricos consiste en especificar el tipo de objeto que un contenedor almacena, y hacer que dic ha especificación quede respaldada por el compilador. Por tanto, en lugar de emplear Object, lo que querríamos es poder utilizar un tipo no especificado, lo que podremos decidir en algún momento posterior. Para hacer esto, incluimos un parámetro de tipo entre corchetes angulares después del nombre de la clase y luego, al utili zar la clase, sustituimos ese parámetro por un tipo real. Para nuestra clase contenedora an terior, la técni ca consistiría en lo siguiente, donde T es el parámetro de tipo:
11: generics/Holder3.java public class Holder3 private T a¡ public Holder3 (T al
public void set(T al public T get(1
{ this.a
{ this.a
{ return a;
= a; } =
a¡ }
}
public static void main(String[] argsl Holder3 h3 = new Holder3{new Automobile()) ¡ Automobile a = h3.get()¡ II No hace falta proyección II h3.set("Not an Automobile") ¡ II Error // h3.set(11;
// Error
Ahora, cuando creemos un objeto Holder3, deberemos especificar el tipo que queramos almacenar en el mismo, utilizando la sintaxis de corchetes angulares, como puede verse en main(). Sólo podemos introducir en el contenedor objetos de dicho tipo (o de alguno de sus subtipos, ya que el principio de sustinlción sigue funcionando con los genéricos). Y al ex traer del contenedor un va lor, dicho valor tendrá automáticamente el tipo correcto. Ésta es la idea fundamental de los genéricos de Java: le decimos al compi lador qué tipo queremos usar y el compilador se encarga de los detalles. En genera l, podemos tratar los genéricos como si fueran otro tipo más que en lo único que se diferencia de los tipos nonnales es en que tiene parámetros de tipo. Pero, como veremos en breve, podemos ut ilizar los genéricos simplemeOle nombrándolos junto con su lista de argumentos de tipo.
Ejercicio 1:
(1) Utilice HolderJ con la biblioteca typeinfo.pets para demostrar que un objeto Holder3 que se haya especificado para almacenar un tipo base, también puede almacenar un tipo derivado.
Ejercicio 2:
(1) Cree una clase contenedora que almacene tres objetos del mismo tipo, junto con los métodos para almacenar y extraer dichos objetos y un constructor para inicializar los tres.
396 Piensa en Java
Una biblioteca de tuplas Una de las cosas que a menudo hace falta hacer es devolver múltiples objetos de una llamada a método. La instmcción return sólo pennite especificar un único objeto, por lo que la respuesta consiste en crear otro objeto que almacene los múltiples objetos qu e queramos devo lver. Por supu esto, pode mos escribir un a clase especial cada vez que nos encontremos COn esta situación, pero con los genéricos es posible resolver el problema de tilla vez y ahorrarnos un montón de esfuerzo en el fulUro. Almisl110 tiempo estaremos garantizando la seguridad de los tipos en tiempo de compil ación. Este concepto se denomina tupla, y consiste simplemente en un grupo de objetos que se envuelven juntos dentro de otro obje to úni co. El receptor del objeto estará autorizado a leer los elementos, pero no a introducir otros nuevos (este concepto también se conoce como Objeto de tramlerel1cia de datos o Mensajero). Las tuplas pueden ten er, nom1almente, cualqui er longi tud, y cada objeto de la nlpla puede tener un tipo distinto. Sin embargo, lo que 110S interesa es especificar el tipo de cada objeto y garantiza r que, cuando el receptor lea los va lores, obtenga el tipo correcto. Para tratar con el problema de las múltiples longinldes, podemos crear múltiples tuplas diferentes. He aquí una que almacena dos objetos: // : net/mindview/ util / TwoTuple.java package net.mindview.uti l ; public class TwoTuple public final A first¡ public final B second¡ public TwoTuple (A a, B b ) { first = a¡ second public String toString () { + second + " ) It; return " ( " + first + )
b;}
/// ,El cons tmctor captura e l objeto que hay que almacenar y toString() es una función de utilidad que pennite mostrar los valores de ull a lista. Observe que una tupla co nserva implícitamente sus elementos en orden. Al examinar el ejemplo por primera vez, podria pensarse que viola los principios comunes de seguridad en la programación Java. ¿No deberían se r first y second pri vados, y no debería accederse a ellos úni camente con los métodos denominados getFirst( ) y getSecond( )? Considere la seguridad que se obtendría en dicho caso: los clientes podrían seguir leyendo los objetos y hacer lo que quisieran con los mismos, pero no podrían asignar first o second a ninguna otra cosa. La declaración final nos proporciona esa misma seguridad, pero la fonna empleada en el ejemplo es más corta y más simple. Otra observación de di seiio imponante es que puede que queramos permitir a un programador de clientes que haga apuntar a first o seco nd a algún otro objeto. Sin embargo, es más seguro dejar el eje mplo tal cual está, y limitarse a obliga r al usuari o a crear un nuevo objeto TwoTuple si desea di sponer de uno que tenga diferentes elementos. Las tuplas de mayor longitud puede crearSe med iante herencia. Co mo podemos ver, añadir más parámetTos de tipo resulla bastante si mple: // : net / mindview / util/ThreeTuple.java package net.mindview.util¡ public class ThreeTuple extends TwoTuple { public final C third¡ publie ThreeTuple lA a, B b, e e l { super (a, b ) ; third = c¡ public String toString () return It ( " + first +
{ " + second +
}
/// , / / : net / mindview / util / FourTuple.java package net.mindview.util¡
"
+ third +" ) It ¡
15 Ge néricos
397
public class FourTuple extends ThreeTuple public final D fourth¡ public FourTuple(A a, B b , e e, D d) super (a, b, el j fourth = d; public String toString () { + second + return "(" + first + ", third + 11 I 11 + fourth + ") 11 i
" +
}
;;;,jI: net/mindview/util/FiveTuple.java package net.mindview.util;
public class FiveTuple extends FourTuple { public f inal E fifth; public FiveTuple(A a, super(a, b , e, d); fi f th = e¡
B b,
public String toSt r ing ()
e
E el
{
{
return ,,(" + first + ", third + ",
e, D d,
" + fourth +
+ second + 11 I
"
",
+ fifth +
11
+ ") "
j
}
;;;,Para utili zar una tupla, simplemente definimos la tupla de la longitud adecuada como valor de retomo para nuestra función y luego la cream os y la devolvemos en la instrucción return :
1/: generics/TupleTest . java import net . mindview.util .* ¡ elass Amphibian {} elass Vehiele {}
public class TupleTest static TwoTuple f() /1 El mecanismo de autoboxing convierte int en Integer: return new TwoTuple ("hi", 4 7) ; static ThreeTuple 9 () { return new ThreeTuple( new Amphibian(), "hi " , 47); static FourTuple h() return new FourTuple( new Vehicle(), new Amphibian (}, "h i n, 47) i static FiveTuple k() return new FiveTuple ( new Vehicle(), new Amphibian(), "hi " , 47, 11 . 1) ¡ public static void main(String[] args} TwoTuple t t si ~ f();
398 Piensa en Java System.out.println(ttsi) i JI ttsi.first = "there"¡ // Error de compilación: System.out.println(g()) ; System.out.println(h()) ; System.out.println{k()) i
final
/ * Output : (80% match) (hi,
47 )
(Amphibian@l f6a7b9, hi, 47 ) (Vehicle@3Sce36, Amphibian@757aef, hi, 47 ) (Veh icle@9ca b16, Amphibian@la46e30, hi, 47,
11.1 )
* ///,Gracias a los genéricos, podemos crear fácilmente cualquier tupla para devolver cualquier grupo de tipos, simplemente escribiendo la expresión correspondiente. Podemos ver cómo la especificación final en los campos públicos impide que sean reasignados después de la construcción. puede observarlo viendo cómo falla la instrucción ttsi.first = " there". Las expresiones new son demasiado complejas. Posterionnente en el capítulo veremos cómo simplificarlas utilizando méto~ dos genéricos. Ejercicio 3 :
(1 ) Cree y pruebe un genéri co Si,Tuple con seis elementos.
Ejercicio 4 :
(3) Reescriba innerclasses/Sequence.java utili zando genéricos.
Una clase que implementa una pila Exami nemos algo ligeramente más complicado: la típica estructura de pila. En el Capítulo 11 , Almacenamiento de objelos, vi mos cómo implementar una pila utili zando un contenedor LinkedList en la clase net.mi ndview. utiI.Stack. En dicho ejemplo, podemos ver que LinkedList ya dispone de los métodos necesarios para crear una pila. La clase Stack que implementaba la pila se construyó componiendo una clase genérica (Stack { private static cIass Node U item¡ Node next¡ Node () { item = null ¡ next Node (U item, Node next ) this.item item; this.next = next ¡ boolean end ()
nuH;
{ return i tem "'''' null
}
&&
next
==
null ¡ }
private Node top = new Node () ¡ II Indicador de fin public void push(T item) { top '" new Node (item, top) ; } pUbli c T pop ()
(
T result = top.item¡ if ( !top.end ())
top = top. next i return resul ti
15 Genéricos 399
public static void main (String[] args ) LinkedStack lss ~ new LinkedStack () ; for (String s
: "Phasers on stun!".split ( U
It } )
lss.push (s ) i String Si while { {s = lss.pop {) ) != null ) System . out.println (s ) ;
/ * Output: stun! on Phasers *///,La clase interna Node también es un genérico y ti ene su propio parámetro de tipo. Este ejempl o hace uso de un indicador de fin para detemünar cuándo la pila está vacía. El indicador de fin se crea en e l momento de construir el contenedor LinkedStack, y cada vez que se invoca push( ) se crea un nuevo objeto Node y se enlaza con el objeto Node anterior. Cuando se invoca a pop(). siempre se devuelve top.item, y luego se descarta el objeto Node actua l y nos desplazamos al sigui ente, excepto cuando encontramos el indicador de fin , en cuyo caso no noS desplazamos. De esa forma, si el cliente continúa in vocando pope ), obtendrá como respuesta valores nuH para indicar que la pila está vaCÍa.
Ejercicio 5:
(2) Elimine el parámetro de tipo en la clase Node y modifique el resto del código en LinkedStack.java para demostrar que una clase interna ti ene acceso a los parámetros de tipo genérico de su clase ex terna.
RandomList Como ejemplo adicional de contenedor, suponga que querernos di sponer de un tipo especial de lista que seleccione aleatoriamente uno de sus elementos cada vez que invoquemos select( ). Al hacer esto, podemos co nstruir una herramienta que funcione para todos los objetos, así que utili zamos genéricos: // : generics /RandornList.java import java . util.*; public class RandomList prívate ArrayList storage = new ArrayList(); private Random rand = new Random(47); public void add(T item) { storage.add(item); } public T select () { return storage.get(rand .nextlnt (storage .size ())); public static void main (String [] args) { RandornList rs = new RandomList() ; tor (String s: ("The quick brown fox jumped over " + "the 1azy brown dog " ) . sp1it(rt " )) rs.add(s) ; for{int i = O; i < 11; i++ ) System. out . print (rs. select () + 11
");
/ * Output : brown over fox quick quick dog brown The brown lazy brown
* /// ,-
Ejercicio 6:
( 1) Utilice RandornList con dos tipos adicionales además del que se muestra en main( ).
Interfaces genéricas Los genéricos también funcionan con las interfaces. Por ejemplo, un generador es una clase qu e crea objetos. En la práctica, una especiali zac ión del patrón de diseño basado en el método defactoria, pero cuando pedimos a un generador que cree
400 Piensa en Java
un nuevo objeto no le pasamos ningún argumento, al contrario de lo que sucede con un método de factoría. El generador sabe cómo crear nuevos objetos sin ninguna infonnación adicional. Típicamente, un ge nerador simplemente define un método, el método que produce nuevos objetos. Aquí, lo denominaremos next( ) y lo incluiremos en las utilidades estándar:
1/: net/mindview/util/Generator.java II Una interfaz genérica. package net.mindview.util; public interface Generator { T next (); }
11/:-
El tipo de retomo de next() se parametriza como T . Como puede ver, la utilización de genéricos con interfaces no es diferente de la util izac ión de genéricos con clases. Para ilustra r la implementación de un objeto Generator, necesitaremos al gunas clases. He aquí una jerarquía de ejemplo:
11: generics/coffee/Coffee.java package generics.coffee; public class Coffee { private static long counter ~ O; private final long id = counter++¡ public String toString () { return getClass () . getSimpleName () + " " + id¡ }
///,-
11: generics/coffee/Latte.java package generics.coffee; public class Latte extends Coffee {}
111:-
//: generics/coffee/Mocha.java package generics.coffee; public class Mocha extends Coffee {}
11/ :-
/1: generics/coffee/Cappuccino.java package generics.coffee; public class Cappuccino extends Coffee {}
111:-
11: generics/coffee/Americano.java package generics.coffee¡ public class Americano extends Coffee {} /1/://: generics/coffee/Breve.java package generics . coffee; public class Breve extends Cof fee {} /1/:-
Ahora, podemos implementar un objeto Generator que genera aleatori amente diferentes tipos de objetos Coffee:
11: generics/coffee/CoffeeGenerator.java /1 Generar diferentes tipos de objetos Coffee: package generics.coffee; import java.util.*; import net.mindview.util.*¡ public class CoffeeGenerator implements Generator, Iterable { private Class[] types = { Latte.class, Mocha.class, Cappuccino. class, Americano. class, Breve. class, }; private static Random rand = new Random(47); public CoffeeGenerator () {} II Para iteración:
15 Genéricos 401 private int size = o; sz; ) public CoffeeGenerator (int sz) { size public Coffee next () { try ( return (Coffee) types (rand. next lnt (t-ypes .length) ] . newlnstance () ; // Informar de errores del programador en tiempo de ejecución: catch(Exception el { throw new RuntimeException(el i
class Coffeelterator implements Iterator int count = size; public boo!ean hasNext () { return count :> Oi } public Coffee next () { count--¡ return CoffeeGenerator . this.next() i public void remove() { JI No implementado throw new UnsupportedOperationException();
) ); public Iterator iterator() return new Coffeelterator(); public static void main(String[] args) { CoffeeGenerator gen = new CoffeeGenerator() for(int i = O; i < Si i++l System.out . println{gen.next() l; for(Coffee e : new CoffeeGenerator(S)) System.out.println{c) ;
i
1* Output: Americano O Latte 1 Americano 2 Mocha 3 Mocha 4 Breve 5 Americano 6 Latte 7 Cappuccino 8 Cappuccino 9 *111,La inte rfaz Generator parametrizada garanti za que next( ) devuel va el tipo definido en el parámetro. CoffeeGenerator también implementa la interfaz Iterable, por lo que se le puede usar en una instrucciónforeach. Sin embargo, requiere un "indicador de fin" para saber cuándo parar, y esto se crea utilizando el segundo constructor. He aquí una segunda implementación de Generator, que esta vez se utili za para generar números de Fibonacci:
/1 : generics/Fibonacci.java II Generar una secuencia de Fibonacc i. import net.mindview.util.*; public class Fibonacci implements Generator private int count = O; public Integer next() { return fib(count++); } pri vate int f ib (int n ) { if(n < 2) return 1; return fib(n-2) + fib (n-1);
402 Piensa en Java
p u b lic stat i c v o id main (String [ ] args l Fibonacci gen = new Fibonacci () ; f o r {int i :: O; i < 18; i++ l System. o ut.print (gen.next () + " 11 ) ;
/ * Output: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 * jjj ,-
Aunque estamos trabajando con va lores int tanto dentro como fuera de la clase, el parámetro de tipo es Intege r . Esto nos plantea una de las limitaciones de los gené ricos de Java. No se pueden utilizar primitivas como parámetros de tipo. Sin embargo, Java SES ha añadi do, afortunadamente, la fu ncionalidad de conversión automática entre primitivas y tipos envoltorio, para poder efectuar las conversiones fácilmente. Podemos ver el efecto en este ejemplo porque los va lores in t se utili zan, en general, en la clase de manera transparente. Podemos ir un paso más allá y crear un generador de Fibonacci de tipo Iterable. Una opción consiste en reimp lementar la clase y añadir la interfaz Iterabl e. pero no siempre tenemos control sobre el código original, y no merece la pena reesc ribir código a menos que nos veamos obligados a hacerlo. En lugar de ello, podemos crear un adaptador para obtener la interfaz deseada; este patrón de diseño ya fue presentado anteriomlente en el libro. Los adaptadores pueden implementase de múltiples fomlas. Por ejemplo, podemos utili zar el mecanismo de herencia para generar la clase adaptada:
11 : generics / IterableFibonacci.java 11 Adaptar la clase Fibonacci para hacerla
de tipo Iterable.
import java.util.*¡ public class IterableFibonacci extends Fibonacci implements Iterable private int n¡ public IterableFibonacci ( int count l n = c ount¡ public Iterator iterator( ) return new Iterator () { public boolean hasNext () { return n > Oi } public Integer next ( ) ( n--;
return IterableFibonacci.this . next ( ); public v o id remo ve () { li No implementado throw new UnsupportedOperationException ()
i
} }; public static void main (String [] args ) { for ( int i : new IterableFibonacci(18 } ) System.out.print ( i + 11 11 ) ;
1* Output: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 * jjj ,Para utili zar IterableFibonacci en una instrucción foreach, hay que proporcionar al constructor un límite para que hasNexl() sepa cuándo devolver false.
Ejercicio 7:
(2) Uti lice el mecanismo de composición en lugar del mecanismo de herencia para adaptar Fibonacci con el fm de hacerla de tipo Iterable.
Ejercicio 8:
(2) Siguiendo la fonna del ejemplo CoITee, cree una jerarquía de personajes (SloryCharacler) de su película favorita, diviéndo los en buenos (GoodGu ys) y malos (BadGuys). Cree un generador para Slor yC haracler , siguiendo la fonna de CoffeeGeneralor.
15 Genéricos 403
Métodos genéricos Hasta ahora, hemos estado analizando la parametrización de clases enteras, pero también podemos parametrizar métodos de una clase. La propia clase puede ser o no genérica; esto no influ ye en la posibilidad de disponer de métodos genéricos.
Un método genéri co permite que el método va ríe independientemente de la clase. Como directriz, deberemos usar los métodos genéri cos "siempre que podamos". En otras palabras : si es posible hacer que un método sea genérico, en lugar de que lo sea la clase completa, probablemente el programa sea más claro si hacemos genérico el método. Además, si un método es estáti co, no tiene acceso a los parámetros genéricos de tipo de la clase, por lo que si esa genericidad es necesaria en el método, deberemos definirlo como un método genérico. Para definir un método genético, simplemente colocamos una lista de parámetros genéricos delante del va lor retorno del modo siguient e: jj: genericsjGenericMethods.java
public class GenericMethods public void f (T x ) { System. out. println (x . getClass () . getName () ) ; public statie void main(String[] args) { GenericMethods gm = new GenericMethods() gm.f (" 10 );
i
gm. f ( 1 ) ; gm.f(l.O) ; gm.f(l.OF) ;
gm.f ( 'e' )
i
gm.f(gm) ;
} / * Output, java .lang.String java . lang.lnteger java.lang.Double java.lang.Float java . lang.Character GenericMethods *///,La clase GenericMethods no está paralnetrizada, aunque es perfectamente posible parametrizar simultáneamente tanto una clase como sus métodos. Pero en este caso, solo el método f() tiene un parámetro de tipo, indicado por la lista de parámetros antes del tipo de retorno del método. Observe que con una clase genérica es preciso especificar los parámetros de tipo en el momento de instanciar la clase. Pero con un método genérico, usualmente no hace falta especificar los tipos de parámetro, porque el compi lador puede detenninar esos tipos por nosotros. Este mecanismo se denomina inferencia del argumento de tipo. Por tanto, las llamadas a f() parecen llamadas a método nonnales, y en la práctica f() se comporta como si estuviera infinitamente sobrecargado. El método admitirá incluso un argumento del tipo GenericMethods. Para las llamadas a f() que usen tipos primitivos entra en acción el mecanismo de conversión de tipos automática, envolviendo de manera transparente los tipos primitivos en sus objetos asociados. De hecho, los métodos genéricos y el mecanismo de conversión automática de tipos penniten eliminar parte del código que anterionnente requería utilizar conversiones de tipos manuales. Ejercicio 9:
( 1) Modifique GenericMethods.java de modo que f( ) acepte tres argumentos, cada uno de los cuales tiene que ser de un tipo parametrizado distinto.
Ejercicio 10:
( 1) Modifique el ejercicio anterior de modo que uno de los argumentos de f() no sea parametrizado.
404 Piensa en Java
Aprovechamiento de la inferencia del argumento de tipo Una de las quejas acerca de los genéricos es que aiiaden todavía más texto a nuestro código. Considere el programa holdinglMapOfLiSl.java del Ca pitulo 11 , Almacenamiento de objetos. La creación del contenedor Map de List tiene el aspecto sigui ente: Map Map map () return new HashMap{);
{
pub1ic static List 1ist() return new ArrayList{); public static LinkedList lList () return new LinkedList () ¡
{
public static Set set () return new HashSet(); public static Queue queue () return new LinkedList () i
II Ejemplos, public static veid main (String [] args) { Map